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:
WHC
2026-10-11 11:09:39 +08:00
parent 50abcf1a91
commit e0b0eddf3a
28 changed files with 3291 additions and 111 deletions
+108 -6
View File
@@ -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)");