thincoder 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +2 -1
  2. package/bin/thincoder.mjs +25 -25
  3. package/package.json +1 -1
  4. package/src/agent/dispatch.mjs +17 -12
  5. package/src/agent/helpers.mjs +22 -11
  6. package/src/agent/setup.mjs +32 -10
  7. package/src/agent-tools/goal.mjs +7 -6
  8. package/src/agent-tools/plan.mjs +3 -3
  9. package/src/agent-tools/recent-changes.mjs +3 -3
  10. package/src/agent-tools/skill.mjs +6 -6
  11. package/src/agent-tools/subagent.mjs +19 -18
  12. package/src/agent-tools/task.mjs +4 -4
  13. package/src/agent-tools/verify.mjs +21 -19
  14. package/src/agent-tools.mjs +3 -3
  15. package/src/agent.mjs +58 -37
  16. package/src/cli/distill-command.mjs +2 -2
  17. package/src/cli/make-agent.mjs +9 -9
  18. package/src/cli/memory-command.mjs +1 -1
  19. package/src/cli/permission.mjs +3 -3
  20. package/src/cli/setup-wizard.mjs +15 -15
  21. package/src/config.mjs +55 -53
  22. package/src/context.mjs +59 -56
  23. package/src/distill.mjs +35 -35
  24. package/src/embedding.mjs +17 -17
  25. package/src/git/checkpoint.mjs +211 -38
  26. package/src/git/gitmem.mjs +21 -20
  27. package/src/markdown.mjs +13 -13
  28. package/src/mcp/helpers.mjs +6 -1
  29. package/src/mcp/transport-http.mjs +2 -1
  30. package/src/mcp/transport-stdio.mjs +3 -2
  31. package/src/mcp/transport-ws.mjs +3 -2
  32. package/src/mcp.mjs +5 -2
  33. package/src/memory/code-index.mjs +16 -14
  34. package/src/memory/code-sync.mjs +36 -26
  35. package/src/memory/core.mjs +42 -35
  36. package/src/memory/docs.mjs +24 -15
  37. package/src/memory/schema.mjs +28 -27
  38. package/src/memory.mjs +2 -2
  39. package/src/prompts/coder.md +1 -1
  40. package/src/prompts/discipline.md +3 -0
  41. package/src/prompts/main.md +1 -2
  42. package/src/prompts/system.md +21 -16
  43. package/src/provider/core.mjs +11 -6
  44. package/src/provider/index.mjs +2 -2
  45. package/src/provider/rate.mjs +11 -11
  46. package/src/session.mjs +73 -40
  47. package/src/skills.mjs +17 -17
  48. package/src/tools/checkpoint.md +6 -2
  49. package/src/tools/file.mjs +16 -12
  50. package/src/tools/git.mjs +83 -12
  51. package/src/tools/index.mjs +1 -1
  52. package/src/tools/patch.mjs +20 -18
  53. package/src/tools/repomap-parse.mjs +17 -17
  54. package/src/tools/repomap.mjs +29 -29
  55. package/src/tools/shared.mjs +49 -28
  56. package/src/tools/system.mjs +163 -118
  57. package/src/tools/web.mjs +6 -6
  58. package/src/tui/agent-turn.mjs +82 -26
  59. package/src/tui/ansi.mjs +4 -2
  60. package/src/tui/clipboard.mjs +2 -2
  61. package/src/tui/cmd-auto.mjs +1 -1
  62. package/src/tui/cmd-clear.mjs +1 -1
  63. package/src/tui/cmd-config.mjs +2 -2
  64. package/src/tui/cmd-exit.mjs +2 -2
  65. package/src/tui/cmd-extract.mjs +1 -1
  66. package/src/tui/cmd-goal.mjs +4 -4
  67. package/src/tui/cmd-help.mjs +1 -1
  68. package/src/tui/cmd-init.mjs +3 -3
  69. package/src/tui/cmd-mcp.mjs +5 -5
  70. package/src/tui/cmd-model.mjs +1 -1
  71. package/src/tui/cmd-new.mjs +1 -1
  72. package/src/tui/cmd-plan.mjs +1 -1
  73. package/src/tui/cmd-reindex.mjs +2 -2
  74. package/src/tui/cmd-restore.mjs +1 -1
  75. package/src/tui/cmd-session.mjs +1 -1
  76. package/src/tui/cmd-skills.mjs +1 -1
  77. package/src/tui/cmd-think.mjs +2 -2
  78. package/src/tui/config-helpers.mjs +6 -6
  79. package/src/tui/distill-cmd.mjs +3 -3
  80. package/src/tui/index.mjs +131 -72
  81. package/src/tui/interaction.mjs +13 -12
  82. package/src/tui/key-handler.mjs +31 -23
  83. package/src/tui/layout.mjs +22 -17
  84. package/src/tui/pickers.mjs +19 -19
  85. package/src/tui/render-frame.mjs +37 -11
  86. package/src/tui/render.mjs +25 -24
  87. package/src/tui/slash-commands.mjs +14 -14
  88. package/src/tui/startup.mjs +15 -14
  89. package/src/tui/wizard.mjs +11 -11
  90. package/src/tui.mjs +2 -2
  91. package/bin/thincoder.js +0 -4
  92. package/src/tools/bash.mjs +0 -144
  93. package/src/tools/glob.mjs +0 -51
  94. package/src/tools/grep.mjs +0 -100
  95. package/src/tools/ls.mjs +0 -36
  96. package/src/tools.mjs +0 -2
  97. package/src/tui-render.mjs +0 -4
package/README.md CHANGED
@@ -12,7 +12,8 @@ Design philosophy (the entire meaning of the name): if the Node standard library
12
12
 
13
13
  ## Features
14
14
 
15
- - **Agent loop**: LLM tool-call loop, 100-turn cap against runaway, completion guard blocks unverified changes
15
+ - **Fix-verify loop**: file changes without `verify` get pushed back syntax check + tests must pass before the agent can claim completion (auto-repair up to 3 rounds)
16
+ - **Checkpoint system**: auto-snapshot before every user task, `list`/`create`/`rewind` tools for the model, single-file restore — rewinding itself is reversible (pre-rewind state auto-saved)
16
17
  - **Codebase understanding** ⭐0.5.0: `repo_outline` (dependency outline, auto-injected at startup), `code_search` (source FTS5 + vectors + JSDoc extraction), `doc_search` (docs chunked by ## headings) — background indexing, auto-incremental updates on file writes, three tools guided by "structure → intent → details"
17
18
  - **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from five leading Chinese vendors — DeepSeek / Kimi / GLM / Qwen / MiniMax. No legacy model compatibility, no local model support. Auto-matched context windows, truncation-resume protocols (prefix/partial), thinking-mode APIs (thinking.type / reasoning_effort), reasoning_content echo strategies (reasoningEcho), output limits, temperature range clamping — all five deeply adapted.
18
19
  - **Toolset**: `read` / `write` / `edit` / `bash` / `glob` (supports `**`) / `grep` / `websearch` / `ls` / `fetch` + `read_image` (image/video paste) + three retrieval tools + MCP — all zero-dependency, file tools confined to the working directory
package/bin/thincoder.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * thincoder — 命令入口
5
- * thincoder 启动 TUI
6
- * thincoder chat "..." 一次性 agent 问答(可调用工具,流式输出)
7
- * thincoder memory <sub> 记忆管理:list / search / put / remove
8
- * thincoder upgrade npm 升级到最新版
9
- * thincoder -v 显示版本号
10
- * thincoder --help 显示帮助
4
+ * thincoder — CLI entry point
5
+ * thincoder Launch the interactive TUI
6
+ * thincoder chat "..." One-shot agent run (tools enabled, streamed)
7
+ * thincoder memory <sub> Memory management: list / search / put / remove
8
+ * thincoder upgrade Update to the latest version from npm
9
+ * thincoder -v Print version
10
+ * thincoder --help Print help
11
11
  */
12
12
 
13
13
  import { readFileSync } from "node:fs"
@@ -15,7 +15,7 @@ import { join } from "node:path"
15
15
  import { runAgent } from "../src/agent.mjs"
16
16
  import { loadConfig, configPath } from "../src/config.mjs"
17
17
  import { createMemory, syncDir } from "../src/memory.mjs"
18
- import { makeAgent, teamConfig, gitAuthor } from "../src/cli/make-agent.mjs"
18
+ import { assembleAgent, teamConfig, gitAuthor } from "../src/cli/make-agent.mjs"
19
19
  import { memoryCommand } from "../src/cli/memory-command.mjs"
20
20
  import { setupWizard } from "../src/cli/setup-wizard.mjs"
21
21
  import { summarize, askPermission } from "../src/cli/permission.mjs"
@@ -24,7 +24,7 @@ import { distillCommand } from "../src/cli/distill-command.mjs"
24
24
  const [command, ...args] = process.argv.slice(2)
25
25
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
26
26
 
27
- // 顶层兜底:任何未捕获错误打印一行消息干净退出,不糊用户一脸 stack
27
+ // Top-level safety net: print one-line error and exit cleanly, no stack traces to the user
28
28
  process.on("uncaughtException", (error) => {
29
29
  console.error(`[error] ${error.message}`)
30
30
  exitSoon(1)
@@ -38,7 +38,7 @@ const USAGE = `thincoder - thin coding agent
38
38
 
39
39
  Usage:
40
40
  thincoder Launch the interactive TUI
41
- thincoder chat <prompt> One-shot agent run (tools enabled), streams reply to stdout
41
+ thincoder chat [--auto] <prompt> One-shot agent run (tools enabled), streams reply to stdout; --auto approves all tool calls
42
42
  thincoder memory list [--type=<t>] List memory entries
43
43
  thincoder memory search <query> Search memory
44
44
  thincoder memory put --type=<t> --title=<t> --content=<c> [--tags=<t>]
@@ -51,21 +51,21 @@ Usage:
51
51
  thincoder upgrade Update to the latest version from npm
52
52
  thincoder -v, --version Print version
53
53
 
54
- Config: ~/.thincoder/config.json (providers[] + activeProvider;TUI 内用 /provider、/model 管理)
54
+ Config: ~/.thincoder/config.json (providers[] + activeProvider; manage via /provider, /model in TUI)
55
55
  Env: THINCODER_API_KEY, THINCODER_BASE_URL, THINCODER_MODEL, THINCODER_ACTIVE_PROVIDER
56
56
  `
57
57
 
58
- /** key 时的统一提示 */
58
+ /** Unified message when no API key is configured */
59
59
  function noKeyMessage() {
60
- return `还没有配置 API key。运行 thincoder 进入 TUI,用 /provider add /provider key 配置;或直接编辑 ${configPath}`
60
+ return `No API key configured yet. Run "thincoder" to enter TUI, use /provider add and /provider key; or edit ${configPath} directly`
61
61
  }
62
62
 
63
- /** 延迟退出:fetch 后立刻 process.exit Windows/Node 24 会触发 libuv 断言,让一句柄关完再走 */
63
+ /** Delay exit: process.exit right after fetch triggers libuv assertion on Windows/Node 24; let handles drain first */
64
64
  function exitSoon(code) {
65
65
  setTimeout(() => process.exit(code), 100)
66
66
  }
67
67
 
68
- /** 语义化版本比较:a<b 返回 -1,相等 0a>b 返回 1;非数字段按字符串比 */
68
+ /** Semantic version comparison: a<b returns -1, equal 0, a>b returns 1; non-numeric segments compare as strings */
69
69
  function compareVersions(a, b) {
70
70
  const pa = String(a).split("."), pb = String(b).split(".")
71
71
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
@@ -90,7 +90,7 @@ switch (command) {
90
90
  break
91
91
  }
92
92
 
93
- const agent = await makeAgent()
93
+ const agent = await assembleAgent()
94
94
  if (!agent.provider.apiKey) {
95
95
  if (!process.stdin.isTTY) {
96
96
  console.error(noKeyMessage())
@@ -104,7 +104,7 @@ switch (command) {
104
104
  }
105
105
  agent.provider = p
106
106
  agent.activeProvider = p.name
107
- // 向导可能配了 embedding key:挂上向量检索
107
+ // Wizard may have configured an embedding key: attach vector search
108
108
  const fresh = loadConfig()
109
109
  if (fresh.embedding?.apiKey && agent.memory && !agent.memory.embedder) {
110
110
  const { createEmbedder } = await import("../src/embedding.mjs")
@@ -112,13 +112,13 @@ switch (command) {
112
112
  }
113
113
  }
114
114
  if (auto) agent.autoApprove = true
115
- // 累计 token 用量,结束时输出到 stderr(不污染 stdout 管道)
115
+ // Accumulate token usage, output to stderr at the end (don't pollute stdout pipe)
116
116
  const usageTotal = { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }
117
117
  try {
118
118
  await runAgent(agent, prompt, {
119
119
  onToken: (text) => process.stdout.write(text),
120
120
  onWait: ({ phase, seconds }) => {
121
- console.error(phase === "gate" ? `[rate-limit] TPM 节流等待 ~${seconds}s` : `[rate-limit] 429,${seconds}s 后重试`)
121
+ console.error(phase === "gate" ? `[rate-limit] TPM throttle waiting ~${seconds}s` : `[rate-limit] 429 response, retrying in ${seconds}s`)
122
122
  },
123
123
  onToolCall: (name, toolArgs) => {
124
124
  console.error(`\n[tool] ${name} ${summarize(toolArgs)}`)
@@ -128,7 +128,7 @@ switch (command) {
128
128
  console.error(`[done] ${name} -> ${preview.split("\n")[0]}`)
129
129
  },
130
130
  onToolOutput: (name, chunk) => process.stderr.write(chunk),
131
- onCompress: () => console.error(`\n[context] 上下文过长,已自动压缩(早期对话由 LLM 摘要)`),
131
+ onCompress: () => console.error(`\n[context] Context too long, auto-compacted (early conversation summarized by LLM)`),
132
132
  onTaskUpdate: (items) => {
133
133
  const done = items.filter((i) => i.status === "done").length
134
134
  const current = items.find((i) => i.status === "in_progress")
@@ -230,7 +230,7 @@ switch (command) {
230
230
 
231
231
  case "tui":
232
232
  case undefined: {
233
- const agent = await makeAgent()
233
+ const agent = await assembleAgent()
234
234
  const config = loadConfig()
235
235
  // 恢复上次的会话(同一项目目录);provider 按保存的名字切回(用户上次可能换过模型)
236
236
  const { loadSession, applySession } = await import("../src/session.mjs")
@@ -275,16 +275,16 @@ switch (command) {
275
275
  try {
276
276
  remote = execSync("npm view thincoder version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()
277
277
  } catch {
278
- console.error("[upgrade] 无法查询 npm registry,请确认网络和 npm 已安装")
278
+ console.error("[upgrade] Unable to query npm registry — check your network connection and that npm is installed")
279
279
  exitSoon(1)
280
280
  break
281
281
  }
282
282
  if (compareVersions(local, remote) >= 0) {
283
- console.log(`ThinCoder ${local} 已是最新。`)
283
+ console.log(`ThinCoder ${local} is already the latest.`)
284
284
  } else {
285
- console.log(`升级: ${local} → ${remote}`)
285
+ console.log(`Upgrading: ${local} → ${remote}`)
286
286
  execSync("npm install -g thincoder@latest", { stdio: "inherit" })
287
- console.log(`已升级到 ${remote}`)
287
+ console.log(`Upgraded to ${remote}`)
288
288
  }
289
289
  break
290
290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,17 +1,17 @@
1
1
  /**
2
- * agent/dispatch.mjs — 工具调用两段式执行
2
+ * agent/dispatch.mjs — two-phase tool call execution
3
3
  */
4
4
  import { offloadToolResult } from "./helpers.mjs"
5
5
 
6
6
  /**
7
- * 两段式执行:
8
- * 阶段一(串行):逐个解析参数 + planMode 检查 + 权限确认(有副作用工具)
9
- * 阶段二(保序执行):严格按模型调用顺序——连续的只读/parallel 工具并发成组,
10
- * 有副作用工具在原位置逐个串行(写后读同一文件的一批调用,读必须看到写后的内容)。
11
- * 返回按调用顺序排列的结果数组(每项含 ok 标记执行成败)。
7
+ * Two-phase execution:
8
+ * Phase 1 (serial): parse args one by one + planMode check + permission confirmation (side-effecting tools)
9
+ * Phase 2 (order-preserving): strictly preserve model call order — consecutive readonly/parallel tools run as concurrent batches,
10
+ * side-effecting tools run serially in their original position (if a batch writes-then-reads the same file, the read must see the post-write content).
11
+ * Returns a results array in call order (each entry has an ok flag indicating success/failure).
12
12
  */
13
13
  export async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth = 0, signal) {
14
- // ---- 阶段一:串行准备 ----
14
+ // ---- Phase 1: serial preparation ----
15
15
  const prepared = []
16
16
  for (const toolCall of toolCalls) {
17
17
  const tool = toolByName.get(toolCall.name)
@@ -34,7 +34,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
34
34
  }
35
35
 
36
36
  if (!tool.readonly) {
37
- // autoApprove 短路:agent 已标记自动批准时不再询问
37
+ // autoApprove short-circuit: skip prompt when agent is already marked for auto-approval
38
38
  const allowed = agent.autoApprove
39
39
  ? true
40
40
  : callbacks.onPermissionRequest
@@ -50,17 +50,20 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
50
50
  prepared.push({ toolCall, tool, args })
51
51
  }
52
52
 
53
- // ---- 阶段二:保序执行 ----
53
+ // ---- Phase 2: order-preserving execution ----
54
54
  const runOne = async (item) => {
55
55
  if (item.error) return { ...item, result: `Error: ${item.error}`, ok: false }
56
56
  if (item.denied) {
57
57
  const reason = item.reason === "plan mode"
58
58
  ? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
59
- : "Error: permission denied by user"
59
+ : item.reason === "denied by user"
60
+ ? "Error: permission denied by user"
61
+ : "Error: no permission handler configured — this tool requires user approval but the current context doesn't support interaction (e.g. subagent or non-TUI mode)"
60
62
  return { ...item, result: reason, ok: false }
61
63
  }
62
64
  try {
63
- const raw = String(await item.tool.execute(item.args, {
65
+ if (item.tool.outputPanel) callbacks.setupOutputPanel?.(item.toolCall.name)
66
+ const rawResult = await item.tool.execute(item.args, {
64
67
  cwd: agent.cwd,
65
68
  agent,
66
69
  depth,
@@ -69,7 +72,9 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
69
72
  onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk),
70
73
  onQuestion: callbacks.onQuestion,
71
74
  onPermissionRequest: callbacks.onPermissionRequest,
72
- }))
75
+ })
76
+ if (rawResult === undefined) throw new Error(`Tool "${item.toolCall.name}" returned undefined — all tools must return a string value`)
77
+ const raw = String(rawResult)
73
78
  const result = item.toolCall.name === "read_image" ? raw : await offloadToolResult(raw, item.toolCall.id)
74
79
  callbacks.onToolResult?.(item.toolCall.name, result)
75
80
  return { ...item, result, ok: true }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * agent/helpers.mjs — Agent 工具函数与常量
2
+ * agent/helpers.mjs — Agent utility functions and constants
3
3
  */
4
4
  import { configDir } from "../config.mjs"
5
5
  import { readFileSync, readdirSync } from "node:fs"
@@ -8,7 +8,7 @@ import { join } from "node:path"
8
8
  import { execSync } from "node:child_process"
9
9
 
10
10
  export const DEFAULT_MAX_TURNS = 100
11
- export const DEFAULT_SUBAGENT_TURNS = 20
11
+ export const DEFAULT_SUBAGENT_TURNS = 100
12
12
  export const DEFAULT_GOAL_TURNS = 200
13
13
  export const MIN_REPORT_CHARS = 200
14
14
  export const REPORT_CONTINUATION =
@@ -18,17 +18,23 @@ export const REPORT_CONTINUATION =
18
18
  const TOOL_RESULT_OFFLOAD_LIMIT = 16_000
19
19
  const TOOL_RESULT_PREVIEW = 2_000
20
20
 
21
+ const GIT_TIMEOUT_MS = 5000
22
+ const MAX_GIT_CHANGES_DISPLAY = 20
23
+
21
24
  export const OUTLINE_INJECT_PREFIX = "[System reminder: project dependency outline:"
22
25
  export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete"])
23
26
 
27
+ /** Escape XML special characters in a string for safe embedding in XML/HTML */
24
28
  export function escapeXml(s) {
25
29
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;")
26
30
  }
27
31
 
32
+ /** Canonicalize a tool call signature for stall detection: name + stable JSON args */
28
33
  export function tryCanonicalize(name, args) {
29
34
  try { return name + ":" + JSON.stringify(JSON.parse(args)) } catch { return name + ":" + args }
30
35
  }
31
36
 
37
+ /** Offload oversized tool results (>16k chars) to disk, returning a preview + file path */
32
38
  export async function offloadToolResult(text, callId) {
33
39
  if (text.length <= TOOL_RESULT_OFFLOAD_LIMIT) return text
34
40
  try {
@@ -46,9 +52,10 @@ export async function offloadToolResult(text, callId) {
46
52
  }
47
53
  }
48
54
 
55
+ /** Collect git branch, recent commits, and working tree status as context text */
49
56
  export function collectGitContext(cwd) {
50
57
  try {
51
- const opts = { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }
58
+ const opts = { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: GIT_TIMEOUT_MS }
52
59
  const branch = execSync("git branch --show-current", opts).trim()
53
60
  const log = execSync("git --no-pager log --oneline -5", opts).trim()
54
61
  const status = execSync("git status --short", opts).trim()
@@ -56,7 +63,7 @@ export function collectGitContext(cwd) {
56
63
  return [
57
64
  `Git context: on branch \`${branch || "(detached)"}\`${dirty ? `, ${dirty} uncommitted change(s)` : ", working tree clean"}.`,
58
65
  log ? `Recent commits:\n${log}` : "",
59
- status ? `Uncommitted:\n${status.split("\n").slice(0, 20).join("\n")}${dirty > 20 ? `\n… (${dirty - 20} more)` : ""}` : "",
66
+ status ? `Uncommitted:\n${status.split("\n").slice(0, MAX_GIT_CHANGES_DISPLAY).join("\n")}${dirty > MAX_GIT_CHANGES_DISPLAY ? `\n… (${dirty - MAX_GIT_CHANGES_DISPLAY} more)` : ""}` : "",
60
67
  ].filter(Boolean).join("\n")
61
68
  } catch {
62
69
  return ""
@@ -71,18 +78,19 @@ export class ContinueError extends Error {
71
78
  }
72
79
  }
73
80
 
81
+ /** Repair malformed conversation history: remove orphan tool messages and fill missing tool results */
74
82
  export function repairHistory(history) {
75
83
  const out = []
76
84
  let dirty = false
77
- const knownIds = new Set() // 迄今 assistant 声明过的 tool_call id
85
+ const knownIds = new Set() // tool_call ids declared by assistant so far
78
86
  for (let i = 0; i < history.length; i++) {
79
87
  const m = history[i]
80
- // assistant 消息:无正文且无 tool_calls,丢弃
88
+ // empty assistant message: no content and no tool_calls, discard
81
89
  if (m.role === "assistant" && !m.tool_calls?.length && !m.content) {
82
90
  dirty = true
83
91
  continue
84
92
  }
85
- // 孤儿 tool 消息:没有对应的 assistant tool_calls 声明,丢弃
93
+ // orphan tool message: no matching assistant tool_calls declaration, discard
86
94
  if (m.role === "tool" && !knownIds.has(m.tool_call_id)) {
87
95
  dirty = true
88
96
  continue
@@ -91,7 +99,7 @@ export function repairHistory(history) {
91
99
  if (m.role !== "assistant" || !m.tool_calls?.length) continue
92
100
 
93
101
  for (const tc of m.tool_calls) knownIds.add(tc.id)
94
- // 收集紧随其后(下一个非 tool 消息之前)的 tool 结果 id
102
+ // collect tool result ids that immediately follow (before the next non-tool message)
95
103
  const answered = new Set()
96
104
  let j = i + 1
97
105
  while (j < history.length && history[j].role === "tool") {
@@ -99,11 +107,11 @@ export function repairHistory(history) {
99
107
  answered.add(history[j].tool_call_id)
100
108
  out.push(history[j])
101
109
  } else {
102
- dirty = true // 孤儿 tool 结果,丢弃
110
+ dirty = true // orphan tool result, discard
103
111
  }
104
112
  j++
105
113
  }
106
- i = j - 1 // 外层 for 会再 +1
114
+ i = j - 1 // outer for will increment again
107
115
 
108
116
  for (const tc of m.tool_calls) {
109
117
  if (!answered.has(tc.id)) {
@@ -119,6 +127,7 @@ export function repairHistory(history) {
119
127
  return dirty ? out : history
120
128
  }
121
129
 
130
+ /** List working directory contents as a tree (directories expanded up to subMax entries each) */
122
131
  export function listWorkDir(cwd, { rootMax = 30, subMax = 10 } = {}) {
123
132
  const SKIP = new Set([".git", "node_modules"])
124
133
  let entries
@@ -158,12 +167,14 @@ export function listWorkDir(cwd, { rootMax = 30, subMax = 10 } = {}) {
158
167
  return lines.join("\n")
159
168
  }
160
169
 
170
+ /** Return the set of tool names that are marked as read-only */
161
171
  export function readonlyToolNames(tools) {
162
172
  return new Set(tools.filter((t) => t.readonly).map((t) => t.name))
163
173
  }
164
174
 
165
175
  const MAX_INSTRUCTION_CHARS = 32_000
166
176
 
177
+ /** Load AGENTS.md / project_rules.md from the project root, return as project instructions */
167
178
  export async function loadProjectInstructions(cwd) {
168
179
  const parts = []
169
180
  for (const name of ["AGENTS.md", "project_rules.md"]) {
@@ -172,7 +183,7 @@ export async function loadProjectInstructions(cwd) {
172
183
  if (!content) continue
173
184
  const key = name.toLowerCase()
174
185
  parts.push(`<!-- From: ${join(cwd, name)} -->\n${content}`)
175
- } catch { /* 文件不存在 */ }
186
+ } catch { /* file does not exist */ }
176
187
  }
177
188
  const merged = parts.join("\n\n")
178
189
  if (!merged) return ""
@@ -1,5 +1,5 @@
1
1
  /**
2
- * agent/setup.mjs — runAgent 的前置准备:上下文注入、system prompt 构建、工具注入
2
+ * agent/setup.mjs — runAgent pre-flight setup: context injection, system prompt construction, tool injection
3
3
  */
4
4
  import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
5
5
  import { search as memorySearch, docSearch } from "../memory.mjs"
@@ -14,26 +14,41 @@ import {
14
14
  } from "./helpers.mjs"
15
15
 
16
16
  const AUTO_REMINDER = "[System reminder: AUTO mode is active — all tool calls are automatically approved without asking.]"
17
+ const DEFAULT_COMPACT_THRESHOLD = 100_000
18
+ const DOC_SEARCH_LIMIT = 5
19
+ const DOC_CHUNK_PREVIEW_LEN = 300
20
+ const MEMORY_SEARCH_LIMIT = 3
17
21
 
18
22
  /**
19
- * 准备一次 agent run:注入上下文、构建 system prompt、注入工具。
20
- * 返回主循环需要的所有状态,同时把初始化消息写入 agent.history
23
+ * Prepare an agent run: inject context, build system prompt, inject tools.
24
+ * Returns all state needed by the main loop, and writes initialization messages into agent.history.
21
25
  */
22
26
  export async function prepareRun(agent, input, callbacks, {
23
27
  depth = 0, signal, overrideTurns, resume, systemPrompt: corePrompt, disciplineRules, mainOverlay,
24
28
  } = {}) {
25
29
  const maxTurns = overrideTurns ?? agent.config?.agent?.maxTurns ?? DEFAULT_MAX_TURNS
26
- const threshold = agent.config?.agent?.compactThreshold ?? 100_000
30
+ const threshold = agent.config?.agent?.compactThreshold ?? DEFAULT_COMPACT_THRESHOLD
27
31
 
28
32
  agent._lastPromptTokens = null
29
33
  agent._usageAtLen = null
30
34
  agent.history = repairHistory(agent.history)
31
35
 
32
36
  if (!resume) {
37
+ // Git context: branch, recent commits, uncommitted changes
38
+ if (depth === 0) {
39
+ const gitCtx = collectGitContext(agent.cwd)
40
+ if (gitCtx) {
41
+ agent.history.push({
42
+ role: "user",
43
+ content: `[System reminder: git context:\n${escapeXml(gitCtx)}]`,
44
+ transient: true,
45
+ })
46
+ }
47
+ }
33
48
  if (depth === 0) {
34
49
  const tree = listWorkDir(agent.cwd)
35
50
  if (tree) {
36
- agent.history.push({ role: "user", content: `[System reminder: working directory snapshot:\n<untrusted_cwd_listing>\n${escapeXml(tree)}\n</untrusted_cwd_listing>]`, transient: true })
51
+ agent.history.push({ role: "user", content: `[System reminder: working directory snapshot (${new Date().toISOString()}):\n<untrusted_cwd_listing>\n${escapeXml(tree)}\n</untrusted_cwd_listing>]`, transient: true })
37
52
  }
38
53
  if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
39
54
  try {
@@ -42,11 +57,11 @@ export async function prepareRun(agent, input, callbacks, {
42
57
  if (summary && !summary.startsWith("(no indexed")) {
43
58
  agent.history.push({ role: "user", content: `${OUTLINE_INJECT_PREFIX}\n${summary}]`, transient: true })
44
59
  }
45
- } catch { /* 索引未就绪不报错 */ }
60
+ } catch { /* index not ready — suppress error */ }
46
61
  }
47
62
  }
48
63
  if (agent.memory) {
49
- const docs = await docSearch(agent.memory, input, { limit: 5 })
64
+ const docs = await docSearch(agent.memory, input, { limit: DOC_SEARCH_LIMIT })
50
65
  if (docs.length > 0) {
51
66
  const count = agent.memory.db.prepare(`SELECT COUNT(*) AS n FROM doc_chunks`).get()?.n ?? 0
52
67
  const more = count > docs.length ? ` (${count} chunks indexed total — call doc_search if you need more)` : ""
@@ -54,12 +69,12 @@ export async function prepareRun(agent, input, callbacks, {
54
69
  role: "user",
55
70
  content:
56
71
  `[Relevant documentation${more}:\n` +
57
- docs.map((d) => `- ${d.path}${d.heading ? " > " + d.heading : ""}: <untrusted_doc_chunk>${escapeXml(d.content.slice(0, 300))}</untrusted_doc_chunk>`).join("\n") +
72
+ docs.map((d) => `- ${d.path}${d.heading ? " > " + d.heading : ""}: <untrusted_doc_chunk>${escapeXml(d.content.slice(0, DOC_CHUNK_PREVIEW_LEN))}</untrusted_doc_chunk>`).join("\n") +
58
73
  "]",
59
74
  transient: true,
60
75
  })
61
76
  }
62
- const memories = await memorySearch(agent.memory, input, { limit: 3 })
77
+ const memories = await memorySearch(agent.memory, input, { limit: MEMORY_SEARCH_LIMIT })
63
78
  if (memories.length > 0) {
64
79
  agent.history.push({
65
80
  role: "user",
@@ -71,6 +86,13 @@ export async function prepareRun(agent, input, callbacks, {
71
86
  })
72
87
  }
73
88
  }
89
+ if (depth === 0) {
90
+ agent.history.push({
91
+ role: "user",
92
+ content: `[System reminder: current time is ${new Date().toISOString()}.]`,
93
+ transient: true,
94
+ })
95
+ }
74
96
  agent.history.push({ role: "user", content: input })
75
97
  }
76
98
 
@@ -81,7 +103,7 @@ export async function prepareRun(agent, input, callbacks, {
81
103
  agent._pendingReminders = []
82
104
  }
83
105
 
84
- // task/plan 工具随主循环注入;subagent/skill/goal/verify 只在顶层注入
106
+ // task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
85
107
  const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool } = await import("../agent-tools.mjs")
86
108
  const tools = [...agent.tools, taskTool, planTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool] : [])]
87
109
  const toolSchemas = tools.map(toOpenAISchema)
@@ -1,7 +1,8 @@
1
1
  /**
2
- * goal 工具:长程自主目标的生命周期管理(完成合约制)。
3
- * 三态:active / complete / blocked;完成要过 verify 证据门槛,
4
- * 阻塞要同一条件连续 3 次才受理;系统每轮注入状态 + 预算进度 + 审计纪律。
2
+ * goal tool: lifecycle management for long-running autonomous goals (completion contract).
3
+ * Three states: active / complete / blocked. Completion must pass a verify evidence threshold;
4
+ * blocked is only accepted after the same condition persists 3 consecutive times.
5
+ * The system injects status + budget progress + audit discipline every turn.
5
6
  */
6
7
  export const goalTool = {
7
8
  name: "goal",
@@ -39,7 +40,7 @@ export const goalTool = {
39
40
  setAt: Date.now(),
40
41
  status: "active",
41
42
  turnsUsed: 0,
42
- _blockTally: null, // { reason, count } — 同一阻塞条件的连续次数(blocked 审计用)
43
+ _blockTally: null, // { reason, count } — consecutive count of the same blocking condition (for blocked audit)
43
44
  }
44
45
  return `Goal set: ${agent.goal.objective}\nDone when: ${agent.goal.criteria}\nThe system will inject goal status every turn. Completion and blocked claims are audited — see the reminders.`
45
46
  }
@@ -47,7 +48,7 @@ export const goalTool = {
47
48
  return `Error: no active goal to '${args.action}' (current: ${agent.goal?.status ?? "none"}). Set one first.`
48
49
  }
49
50
  if (args.action === "complete") {
50
- // 证据链门槛:本轮改过文件却没跑过 verify,不许宣布完成(对齐完成守卫)
51
+ // Evidence chain threshold: files were mutated this run without verify — refuse completion (aligns with completion guard)
51
52
  if (agent._mutatedThisRun && !agent._verifiedThisRun) {
52
53
  return "Error: files were modified but verify has not run. Run the check your criteria names AND the verify tool before marking the goal complete — false completion is the worst outcome of autonomous work."
53
54
  }
@@ -56,7 +57,7 @@ export const goalTool = {
56
57
  }
57
58
  if (args.action === "blocked") {
58
59
  if (!args.reason) return "Error: 'reason' required for 'blocked' action."
59
- // 阻塞审计:同一条件须连续出现 3 次(换过方法仍被同一条件挡住才算真阻塞)
60
+ // Blocked audit: same condition must appear 3 consecutive times (only counts as real blocking if different approaches still hit the same wall)
60
61
  const tally = agent.goal._blockTally
61
62
  const count = tally?.reason === args.reason ? tally.count + 1 : 1
62
63
  agent.goal._blockTally = { reason: args.reason, count }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * plan 工具:进入/退出规划模式。
3
- * 规划模式下只允许只读工具——探索代码、设计方案,不写代码。
4
- * 用户确认方案后退出规划模式开始实现。
2
+ * plan tool: enter/exit plan mode.
3
+ * In plan mode only read-only tools are allowed — explore code, design solutions, no code writing.
4
+ * After the user approves the plan, exit plan mode and start implementing.
5
5
  */
6
6
  export const planTool = {
7
7
  name: "plan",
@@ -1,7 +1,7 @@
1
1
  /**
2
- * recent_changes 工具:列出本轮 agent 触碰过的文件(write/edit/insert_after/delete)。
3
- * git status 更精确——只看本会话的变更,不关心 git 追踪状态。
4
- * 帮助模型在长任务中回顾自己改了什么。
2
+ * recent_changes tool: list files touched by this agent run (write/edit/insert_after/delete).
3
+ * More precise than git status only looks at this session's changes, independent of git tracking.
4
+ * Helps the model recall what it already modified during long tasks.
5
5
  */
6
6
  export const recentChangesTool = {
7
7
  name: "recent_changes",
@@ -2,9 +2,9 @@ import { loadSkills, formatSkillListing, readSkill } from "../skills.mjs"
2
2
  import { escapeXml } from "../agent.mjs"
3
3
 
4
4
  /**
5
- * skill 工具:按需加载项目技能文件(.thincoder/skills/*.md)。
6
- * 加载后技能内容以 <skill-loaded> 包裹写入对话,供后续参考。
7
- * 列出所有可用技能用 action="list"
5
+ * skill tool: load project skill files on demand (.thincoder/skills/*.md).
6
+ * After loading, skill content is injected into the conversation wrapped in <skill-loaded> for subsequent reference.
7
+ * Use action="list" to see all available skills.
8
8
  */
9
9
  export const skillTool = {
10
10
  name: "skill",
@@ -26,8 +26,8 @@ export const skillTool = {
26
26
  return skills.map((s) => `- ${s.name}: ${s.description}`).join("\n")
27
27
  }
28
28
  if (!args.name) return "Error: skill name required for 'load' action."
29
- // 去重:history 里已有同名 <skill-loaded> 块就直接遵循它,不重复展开(历史即账本;
30
- // 被压缩掉后这里自然查不到,会重新加载——正确行为)
29
+ // Dedup: skip reloading if history already contains an <skill-loaded> block with the same name
30
+ // (history is the ledger; if it got compacted away we naturally won't find it here — correct behavior)
31
31
  if (ctx.agent.history?.some((m) => typeof m.content === "string" && m.content.includes(`<skill-loaded name="${args.name}"`))) {
32
32
  return `Skill "${args.name}" is already loaded in this conversation — follow the instructions in the existing <skill-loaded> block above. Do not reload it.`
33
33
  }
@@ -36,7 +36,7 @@ export const skillTool = {
36
36
  const available = skills.map((s) => s.name).join(", ")
37
37
  return `Error: skill "${args.name}" not found. Available: ${available || "(none)"}`
38
38
  }
39
- // 注入 skill 内容到 history(下一条 user 消息)
39
+ // Inject skill content into history (will appear as the next user message)
40
40
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
41
41
  ctx.agent._pendingReminders.push(
42
42
  `<skill-loaded name="${args.name}" source=".thincoder/skills/${args.name}.md">\n${escapeXml(content)}\n</skill-loaded>\n\nFollow the skill's instructions above for the current task.`