v0.8.6: general mode, clipboard text, PDF text extraction
Working mode: --mode coding|general (CST_MODE, settings.json, /mode, WebUI run settings). general swaps the default framing for a general assistant one -- workflow, safety notes, tool wording (run_shell, the subagents) -- and /status shows it; minimal (the paper profile) stays byte-frozen. The prompt state carries the mode so a switch leaves a system note in history. Clipboard: clipboard_read / clipboard_write tools (16 MiB cap read, default 64 KiB window; writing is a WRITE permission with a preview) on pbcopy / wl-copy / xclip / pbpaste / wl-paste / xclip and the Win32 clipboard. In the TUI, /attach paste falls back to clipboard text when there is no image on it. PDF: a dependency-free text extractor (src/pdf_text.c): inflate, Flate/ASCIIHex/ASCII85, PNG/TIFF predictors, layout-independent object scan, object streams, page tree walk with inherited resources, content lexer (Tj/TJ/'/" and the Td/TD/T*/Tm spacing heuristics), per-font ToUnicode CMaps and inline images skipped. Encrypted files and pages with no text layer are refused with the reason. Attachments: /attach (a path or files dropped on the terminal) now take images, PDFs and text. A PDF is stored as its text layer with the mime still saying application/pdf and its page count; anything else has to be UTF-8 text. Documents travel inline as text parts, so CST_NO_IMAGES and text-only models no longer drop them, while images keep their old path. Tests: test_pdf_text (plain, bare and zlib-framed Flate, inline image, CID + ToUnicode, object stream, error paths), documents in test_attach, inlined documents in test_llm_body, mode in test_config.
This commit is contained in:
+108
-6
@@ -177,8 +177,62 @@ static int tool_on(const cst_agent_config_t *cfg, const char *name) {
|
||||
return cst_tool_active(cfg, cst_tool_find(name));
|
||||
}
|
||||
|
||||
/* /mode general (cst_mode_name covers "" and unknown values). */
|
||||
static int general_mode(const cst_agent_config_t *cfg) {
|
||||
return cfg && strcmp(cst_mode_name(cfg->mode), "general") == 0;
|
||||
}
|
||||
|
||||
/* Workflow for /mode general: gather, then do the work, then check it. */
|
||||
static void put_workflow_general(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "\nWorkflow:\n1) Gather what you need");
|
||||
const char *explore[] = {"list_dir", "glob", "grep"};
|
||||
int n = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (tool_on(cfg, explore[i]))
|
||||
cst_buf_printf(b, "%s%s", n++ ? ", " : " (", explore[i]);
|
||||
}
|
||||
if (n)
|
||||
cst_buf_puts(b, ")");
|
||||
if (tool_on(cfg, "read_file"))
|
||||
cst_buf_puts(b, "; read big files in slices with read_file offset/limit, "
|
||||
"and use the documents the user attached");
|
||||
cst_buf_puts(b, ".\n2) Do the work");
|
||||
n = 0;
|
||||
if (tool_on(cfg, "write_file")) {
|
||||
cst_buf_puts(b, ": write_file for new files");
|
||||
n++;
|
||||
}
|
||||
if (tool_on(cfg, "edit_file"))
|
||||
cst_buf_printf(b, "%sedit_file for small exact replacements",
|
||||
n++ ? ", " : ": ");
|
||||
if (tool_on(cfg, "apply_patch"))
|
||||
cst_buf_printf(b, "%sapply_patch for multi-hunk diffs to existing files",
|
||||
n++ ? ", " : ": ");
|
||||
if (tool_on(cfg, "run_shell"))
|
||||
cst_buf_printf(b, "%srun_shell for commands, computations, data "
|
||||
"processing and other CLIs",
|
||||
n++ ? ", " : ": ");
|
||||
cst_buf_puts(b, ".\n");
|
||||
if (tool_on(cfg, "run_shell"))
|
||||
cst_buf_puts(b, "3) Check the result before saying it works (re-read it, "
|
||||
"run it, or verify the numbers); fix and re-run until it "
|
||||
"holds.\n");
|
||||
if (tool_on(cfg, "todo_write"))
|
||||
cst_buf_puts(b, "- For work with several steps keep a todo list with "
|
||||
"todo_write and update it as you go.\n");
|
||||
if (tool_on(cfg, "ask_user"))
|
||||
cst_buf_puts(b, "- Use ask_user only when a decision is truly the user's; "
|
||||
"otherwise choose sensibly and say so.\n");
|
||||
cst_buf_puts(b, "Finish with a short plain-text summary of the result.\n"
|
||||
"Never invent other tool names.\n");
|
||||
}
|
||||
|
||||
/* Workflow section for the default tool profile; names only active tools. */
|
||||
static void put_workflow(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
if (general_mode(cfg)) {
|
||||
put_workflow_general(b, cfg);
|
||||
return;
|
||||
}
|
||||
cst_buf_puts(b, "\nWorkflow:\n1) Inspect only what you need");
|
||||
const char *explore[] = {"list_dir", "glob", "grep"};
|
||||
int n = 0;
|
||||
@@ -235,6 +289,25 @@ static const char DEF_INTRO[] =
|
||||
"could not verify, say so.\n"
|
||||
"- Keep going until the task is done or truly blocked.\n\n";
|
||||
|
||||
/* Same role, framed for /mode general: the workspace may hold documents,
|
||||
* data or anything else, not just code. */
|
||||
static const char DEF_INTRO_GENERAL[] =
|
||||
"You are CST-Agent, a general-purpose assistant working in the user's "
|
||||
"local workspace. All tool paths are relative to the workspace root and "
|
||||
"shell commands run there; the material you work on may be code, "
|
||||
"documents, data or anything else.\n\n"
|
||||
"Principles:\n"
|
||||
"- Reply in the user's language. Be concise: lead with the result, no "
|
||||
"filler.\n"
|
||||
"- Understand before answering: read the files, documents and sources the "
|
||||
"task refers to instead of guessing; never invent content, numbers or "
|
||||
"tool results.\n"
|
||||
"- Do what was asked, at the scope asked: leave unrelated files, data and "
|
||||
"material alone.\n"
|
||||
"- Check your work when you can (re-read it, run it, cross-check the "
|
||||
"source); if you could not verify something, say so.\n"
|
||||
"- Keep going until the task is done or truly blocked.\n\n";
|
||||
|
||||
static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "\nTool use:\n");
|
||||
if (tool_on(cfg, "run_shell") &&
|
||||
@@ -248,15 +321,26 @@ static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "- Read a file before editing it and again after a failed "
|
||||
"edit. The `N|` prefixes in read_file output are not part "
|
||||
"of the file.\n");
|
||||
if (general_mode(cfg) && tool_on(cfg, "web_fetch"))
|
||||
cst_buf_puts(b, "- Use web_fetch for pages the user points you to; say so "
|
||||
"when a page could not be fetched instead of guessing its "
|
||||
"content.\n");
|
||||
cst_buf_puts(b, "- When a call fails, read the error and change approach "
|
||||
"instead of repeating the same call.\n");
|
||||
}
|
||||
|
||||
static void put_safety(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "\nSafety:\n"
|
||||
"- Do not commit, push or rewrite git history unless asked. "
|
||||
"Do not delete or overwrite files you did not create unless "
|
||||
"the task requires it. Never print secrets or API keys.\n");
|
||||
if (general_mode(cfg))
|
||||
cst_buf_puts(b, "\nSafety:\n"
|
||||
"- Do not delete or overwrite files or data you did not "
|
||||
"create unless the task requires it, and do not send the "
|
||||
"user's content anywhere the task did not ask for. Never "
|
||||
"print secrets or API keys.\n");
|
||||
else
|
||||
cst_buf_puts(b, "\nSafety:\n"
|
||||
"- Do not commit, push or rewrite git history unless asked. "
|
||||
"Do not delete or overwrite files you did not create unless "
|
||||
"the task requires it. Never print secrets or API keys.\n");
|
||||
if (tool_on(cfg, "run_shell"))
|
||||
cst_buf_puts(b, "- run_shell may run in a sandbox (writes only inside the "
|
||||
"workspace and temp dir; the network works). If a command is "
|
||||
@@ -365,7 +449,9 @@ char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
|
||||
const char *project_notes) {
|
||||
int minimal = !cfg || cfg->tool_profile == CST_TOOLS_MINIMAL;
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, minimal ? PROMPT_BASE : DEF_INTRO);
|
||||
cst_buf_puts(&b, minimal ? PROMPT_BASE
|
||||
: general_mode(cfg) ? DEF_INTRO_GENERAL
|
||||
: DEF_INTRO);
|
||||
if (cst_protocol_is_native(p))
|
||||
cst_buf_puts(&b, minimal ? SEC_NATIVE
|
||||
: "Call tools through the native "
|
||||
@@ -2241,11 +2327,12 @@ int cst_agent_undo_available(const cst_agent_t *a) {
|
||||
* later changes reach the model as persisted notes appended to the history,
|
||||
* which keeps the cached prefix intact. */
|
||||
|
||||
#define PROMPT_VERSION 2
|
||||
#define PROMPT_VERSION 3
|
||||
|
||||
typedef struct {
|
||||
int plan;
|
||||
char approval[96];
|
||||
char mode[16];
|
||||
char branch[128];
|
||||
char day[16];
|
||||
char notes[24]; /* hash of AGENTS.md, "" = none */
|
||||
@@ -2255,6 +2342,7 @@ static void state_now(const cst_agent_t *a, pstate_t *s) {
|
||||
memset(s, 0, sizeof(*s));
|
||||
s->plan = a->cfg.plan_mode;
|
||||
snprintf(s->approval, sizeof(s->approval), "%s", approval_desc(&a->cfg));
|
||||
snprintf(s->mode, sizeof(s->mode), "%s", cst_mode_name(a->cfg.mode));
|
||||
git_branch(a->cfg.workspace_root, s->branch, sizeof(s->branch));
|
||||
today(s->day, sizeof(s->day));
|
||||
if (a->notes && a->notes[0])
|
||||
@@ -2267,6 +2355,7 @@ static cJSON *state_json(const pstate_t *s) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(o, "plan", s->plan);
|
||||
cJSON_AddStringToObject(o, "approval", s->approval);
|
||||
cJSON_AddStringToObject(o, "mode", s->mode);
|
||||
cJSON_AddStringToObject(o, "branch", s->branch);
|
||||
cJSON_AddStringToObject(o, "day", s->day);
|
||||
cJSON_AddStringToObject(o, "notes", s->notes);
|
||||
@@ -2282,6 +2371,7 @@ static void state_parse(const cJSON *o, pstate_t *s) {
|
||||
if (cJSON_IsString(v)) \
|
||||
snprintf(s->field, sizeof(s->field), "%s", v->valuestring);
|
||||
STATE_STR("approval", approval)
|
||||
STATE_STR("mode", mode)
|
||||
STATE_STR("branch", branch)
|
||||
STATE_STR("day", day)
|
||||
STATE_STR("notes", notes)
|
||||
@@ -2293,6 +2383,7 @@ static char *system_meta(const cst_agent_t *a, const pstate_t *s) {
|
||||
cJSON_AddStringToObject(o, "protocol", cst_protocol_name(a->cfg.protocol));
|
||||
cJSON_AddNumberToObject(o, "pv", PROMPT_VERSION);
|
||||
cJSON_AddNumberToObject(o, "tools", a->cfg.tool_profile);
|
||||
cJSON_AddStringToObject(o, "mode", cst_mode_name(a->cfg.mode));
|
||||
cJSON_AddItemToObject(o, "state", state_json(s));
|
||||
char *out = cJSON_PrintUnformatted(o);
|
||||
cJSON_Delete(o);
|
||||
@@ -2312,10 +2403,13 @@ static int system_current(const cst_message_t *m, const cst_agent_config_t *c) {
|
||||
const cJSON *pv = cJSON_GetObjectItemCaseSensitive(meta, "pv");
|
||||
const cJSON *pr = cJSON_GetObjectItemCaseSensitive(meta, "protocol");
|
||||
const cJSON *tp = cJSON_GetObjectItemCaseSensitive(meta, "tools");
|
||||
const cJSON *md = cJSON_GetObjectItemCaseSensitive(meta, "mode");
|
||||
int ok = cJSON_IsNumber(pv) && pv->valueint == PROMPT_VERSION &&
|
||||
cJSON_IsString(pr) &&
|
||||
strcmp(pr->valuestring, cst_protocol_name(c->protocol)) == 0 &&
|
||||
cJSON_IsNumber(tp) && tp->valueint == c->tool_profile;
|
||||
ok = ok && cJSON_IsString(md) &&
|
||||
strcmp(md->valuestring, cst_mode_name(c->mode)) == 0;
|
||||
cJSON_Delete(meta);
|
||||
return ok;
|
||||
}
|
||||
@@ -2393,6 +2487,14 @@ static void sync_notes(cst_agent_t *a) {
|
||||
"is available again.\n");
|
||||
if (strcmp(now.approval, was.approval) != 0)
|
||||
cst_buf_printf(&b, "[cst] Approval mode is now %s.\n", now.approval);
|
||||
if (strcmp(now.mode, was.mode) != 0)
|
||||
cst_buf_printf(&b,
|
||||
"[cst] Working mode is now %s%s. Earlier instructions about "
|
||||
"how to frame the work no longer apply.\n",
|
||||
now.mode,
|
||||
strcmp(now.mode, "general") == 0
|
||||
? " (general: documents, data, research)"
|
||||
: " (coding: repo, build, tests)");
|
||||
if (strcmp(now.branch, was.branch) != 0)
|
||||
cst_buf_printf(&b, "[cst] The git branch is now %s.\n",
|
||||
now.branch[0] ? now.branch : "(none)");
|
||||
|
||||
Reference in New Issue
Block a user