thincoder 0.12.58 → 0.12.60

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 (158) hide show
  1. package/CHANGELOG.md +78 -2
  2. package/README.md +3 -3
  3. package/bin/thincoder.mjs +88 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +135 -26
  6. package/src/advisor/messages.mjs +57 -4
  7. package/src/advisor/run.mjs +119 -79
  8. package/src/advisor.mjs +34 -7
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +182 -22
  11. package/src/agent/helpers.mjs +71 -4
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +107 -20
  16. package/src/agent/spawn-child.mjs +54 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +133 -109
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +154 -104
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +26 -30
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/goal.mjs +11 -1
  25. package/src/agent-tools/read-history.mjs +284 -0
  26. package/src/agent-tools/recent-changes.mjs +2 -1
  27. package/src/agent-tools/settings.mjs +152 -0
  28. package/src/agent-tools/skill.mjs +2 -1
  29. package/src/agent-tools/subagent-actions.mjs +470 -0
  30. package/src/agent-tools/subagent-async.mjs +382 -0
  31. package/src/agent-tools/subagent-panel.mjs +153 -0
  32. package/src/agent-tools/subagent-run.mjs +202 -0
  33. package/src/agent-tools/subagent-scheduler.mjs +343 -0
  34. package/src/agent-tools/subagent-spawn.mjs +406 -0
  35. package/src/agent-tools/subagent.mjs +203 -377
  36. package/src/agent-tools/task.mjs +4 -3
  37. package/src/agent-tools/timer.mjs +9 -4
  38. package/src/agent-tools/verify.mjs +198 -238
  39. package/src/agent-tools.mjs +1 -0
  40. package/src/agent.mjs +145 -242
  41. package/src/auto-think.mjs +14 -0
  42. package/src/cli/distill-command.mjs +10 -4
  43. package/src/cli/make-agent.mjs +4 -1
  44. package/src/cli/memory-command.mjs +2 -1
  45. package/src/cli/permission.mjs +8 -1
  46. package/src/cli/setup-wizard.mjs +17 -12
  47. package/src/config.mjs +61 -8
  48. package/src/context.mjs +81 -163
  49. package/src/crash-reports.mjs +123 -0
  50. package/src/distill.mjs +30 -12
  51. package/src/escape.mjs +6 -4
  52. package/src/explore-distill.mjs +155 -0
  53. package/src/log.mjs +195 -0
  54. package/src/memory/code-sync.mjs +2 -1
  55. package/src/memory/core.mjs +11 -72
  56. package/src/memory/delete.mjs +234 -0
  57. package/src/memory/docs.mjs +206 -87
  58. package/src/memory.mjs +3 -1
  59. package/src/model-specs.mjs +15 -1
  60. package/src/peer-domains.mjs +265 -0
  61. package/src/peer-instances.mjs +231 -0
  62. package/src/prompt-overlays.mjs +25 -0
  63. package/src/prompts/advisor-design.md +18 -39
  64. package/src/prompts/advisor-round1.md +20 -32
  65. package/src/prompts/advisor-round2.md +16 -16
  66. package/src/prompts/advisor-round3.md +16 -16
  67. package/src/prompts/coder.md +7 -28
  68. package/src/prompts/consult-base.md +4 -11
  69. package/src/prompts/discipline.md +31 -44
  70. package/src/prompts/eng-coder.md +9 -34
  71. package/src/prompts/engineering-sub.md +10 -8
  72. package/src/prompts/engineering.md +61 -264
  73. package/src/prompts/explore.md +4 -14
  74. package/src/prompts/main.md +18 -35
  75. package/src/prompts/methodology-template.md +32 -38
  76. package/src/prompts/plan.md +2 -9
  77. package/src/prompts/system.md +18 -35
  78. package/src/provider/core.mjs +62 -69
  79. package/src/provider/errors.mjs +76 -0
  80. package/src/provider/retry.mjs +8 -45
  81. package/src/session-gc.mjs +214 -0
  82. package/src/session-guard.mjs +47 -0
  83. package/src/session-rename.mjs +38 -0
  84. package/src/session-slots.mjs +181 -58
  85. package/src/session.mjs +48 -89
  86. package/src/token-ttl.mjs +273 -0
  87. package/src/tools/apply_patch.md +3 -1
  88. package/src/tools/bash.md +1 -1
  89. package/src/tools/checklist-sync.mjs +181 -0
  90. package/src/tools/checklist.mjs +52 -39
  91. package/src/tools/delete.md +1 -0
  92. package/src/tools/edit-batch.mjs +131 -44
  93. package/src/tools/edit-diff.mjs +348 -0
  94. package/src/tools/edit.md +20 -13
  95. package/src/tools/execute.md +7 -7
  96. package/src/tools/execute.mjs +55 -24
  97. package/src/tools/file.mjs +25 -70
  98. package/src/tools/file_ops.md +2 -1
  99. package/src/tools/get_current_time.md +3 -1
  100. package/src/tools/git.mjs +14 -6
  101. package/src/tools/glob-dialect.mjs +130 -0
  102. package/src/tools/glob.md +3 -3
  103. package/src/tools/grep.md +1 -1
  104. package/src/tools/hashline_edit.md +2 -0
  105. package/src/tools/index.mjs +3 -3
  106. package/src/tools/insert_after.md +2 -1
  107. package/src/tools/lint.md +2 -0
  108. package/src/tools/lsp.md +4 -1
  109. package/src/tools/ops.mjs +175 -3
  110. package/src/tools/patch.mjs +84 -13
  111. package/src/tools/question.md +5 -1
  112. package/src/tools/repomap.mjs +1 -1
  113. package/src/tools/shared.mjs +18 -25
  114. package/src/tools/system.mjs +50 -30
  115. package/src/tools/tree.md +2 -1
  116. package/src/tools/wait_for.md +22 -0
  117. package/src/tools/web.mjs +5 -3
  118. package/src/tools/websearch.md +2 -1
  119. package/src/tools/write.md +2 -0
  120. package/src/traces/trace-store.mjs +224 -0
  121. package/src/tui/agent-turn.mjs +179 -27
  122. package/src/tui/clipboard.mjs +15 -4
  123. package/src/tui/cmd-config.mjs +77 -16
  124. package/src/tui/cmd-eng.mjs +20 -16
  125. package/src/tui/cmd-extract.mjs +1 -1
  126. package/src/tui/cmd-mcp.mjs +17 -2
  127. package/src/tui/cmd-new.mjs +3 -2
  128. package/src/tui/cmd-session.mjs +19 -4
  129. package/src/tui/cmd-think.mjs +11 -11
  130. package/src/tui/cmd-upgrade.mjs +19 -4
  131. package/src/tui/config-helpers.mjs +28 -16
  132. package/src/tui/distill-cmd.mjs +1 -1
  133. package/src/tui/index.mjs +31 -96
  134. package/src/tui/interaction.mjs +13 -2
  135. package/src/tui/key-handler.mjs +105 -155
  136. package/src/tui/key-modes.mjs +215 -0
  137. package/src/tui/layout.mjs +22 -1
  138. package/src/tui/mouse.mjs +46 -0
  139. package/src/tui/pickers.mjs +51 -25
  140. package/src/tui/render-conversation.mjs +13 -161
  141. package/src/tui/render-frame.mjs +27 -10
  142. package/src/tui/render-loop.mjs +4 -1
  143. package/src/tui/render-segments.mjs +182 -0
  144. package/src/tui/startup.mjs +40 -0
  145. package/src/tui/subagent-blocks.mjs +272 -262
  146. package/src/tui/subagent-children.mjs +176 -0
  147. package/src/tui/subagent-freeze.mjs +172 -0
  148. package/src/tui/subagent-panel.mjs +125 -12
  149. package/src/tui/suspension-drive.mjs +351 -0
  150. package/src/tui/tool-args.mjs +10 -2
  151. package/src/tui/tool-display.mjs +142 -0
  152. package/src/tui/tool-events.mjs +127 -231
  153. package/src/tui/tui-lifecycle.mjs +29 -0
  154. package/src/tui/update-notice.mjs +76 -0
  155. package/src/tui/wizard.mjs +48 -12
  156. package/src/agent-tools/escalate.mjs +0 -179
  157. package/src/agent-tools/subagent-check.mjs +0 -107
  158. package/src/tools/exec-prelude.mjs +0 -84
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * wizard.mjs — first-launch config wizard
3
- * Extracted from index.mjs: provider select → step-by-step input (name/baseURL/model/key/embedkey) → persist → then model picker.
3
+ * Extracted from index.mjs: provider select → step-by-step input (name/baseURL/model/format/key/embedkey)
4
+ * → persist → then model picker. Custom 分支含 API format 步(D-C2,TUI.md §10.6D)。
4
5
  * Accesses shared state and UI functions from the startTUI closure via ctx object.
5
6
  * ctx: { agent, state, pushLine, pushLabel, render, persistRaw, openModelPicker }
6
7
  */
@@ -23,7 +24,13 @@ export function createWizard(ctx) {
23
24
  }
24
25
  for (const [name, p] of Object.entries(PRESETS)) {
25
26
  if (!agent.providers.some((x) => x.name === name)) {
26
- items.push({ kind: "preset", name, baseURL: p.baseURL, model: p.model, label: `${name} (${p.desc})` })
27
+ items.push({
28
+ kind: "preset", name, baseURL: p.baseURL, model: p.model, label: `${name} (${p.desc})`,
29
+ // 预设自身声明的扩展字段随 preset 直达落盘(code review 🟡——与 pickers preset 路径同构;
30
+ // claude/gemini 缺 format、deepseek/glm 缺 thinking/maxTokens 会静默错配);不新增提问步。
31
+ format: p.format, thinking: p.thinking, reasoningEffort: p.reasoningEffort,
32
+ maxTokens: p.maxTokens, chatPath: p.chatPath,
33
+ })
27
34
  }
28
35
  }
29
36
  items.push({ kind: "custom", name: null, label: "Custom endpoint…" })
@@ -45,6 +52,14 @@ export function createWizard(ctx) {
45
52
  prompt: "Enter model name (e.g. gpt-4o)",
46
53
  validate: (v) => v.length > 0 || "Model name required",
47
54
  },
55
+ // D-C2(TUI.md §10.6D):Custom 分支的 API format 步(endpoint 后 key 前——与 Add Provider
56
+ // picker 路径两入口一致;默认 openai)。空输入 = openai 直过(Enter 即默认——同 D-C1 index=0);
57
+ // Esc 在该步沿用 wizard 既有“Esc 随时跳过”语义(取消整个向导——无半配置落盘)。
58
+ format: {
59
+ prompt: "API format [openai/anthropic/google] (Enter = openai default)",
60
+ // 大小写不敏感(旧手输口径——输入统一 toLowerCase 后落盘);空输入 = openai 默认
61
+ validate: (v) => !v || /^(openai|anthropic|google)$/i.test(v) || "API format must be one of: openai, anthropic, google",
62
+ },
48
63
  key: {
49
64
  prompt: "Enter API key",
50
65
  validate: (v) => v.length > 0 || "Key must not be empty",
@@ -54,7 +69,7 @@ export function createWizard(ctx) {
54
69
  validate: () => true, // skippable
55
70
  },
56
71
  }
57
- const WIZARD_NEXT = { name: "baseURL", baseURL: "model", model: "key", key: "embedkey", embedkey: null }
72
+ const WIZARD_NEXT = { name: "baseURL", baseURL: "model", model: "format", format: "key", key: "embedkey", embedkey: null }
58
73
 
59
74
  function startWizard() {
60
75
  state.wizard = { step: "provider", index: 0, scroll: 0, selectedLine: 0, fields: {}, error: null, lines: [] }
@@ -94,6 +109,11 @@ export function createWizard(ctx) {
94
109
  w.step = "name"
95
110
  } else {
96
111
  w.fields = { name: item.name, baseURL: item.baseURL, model: item.model }
112
+ // preset 直达:预设声明的扩展字段(format/thinking/maxTokens/chatPath…)直接带进 fields——
113
+ // 无 format 提问步(T-C4)但落盘不丢字段(code review 🟡——picker preset 路径同款复制)。
114
+ for (const k of ["format", "thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
115
+ if (item[k]) w.fields[k] = item[k]
116
+ }
97
117
  w.step = "key"
98
118
  }
99
119
  renderWizard()
@@ -111,7 +131,9 @@ export function createWizard(ctx) {
111
131
  w.error = null
112
132
  state.input = []
113
133
  state.cursor = 0
114
- w.fields[w.step === "key" ? "key" : w.step] = w.step === "baseURL" ? value.replace(/\/+$/, "") : value
134
+ w.fields[w.step === "key" ? "key" : w.step] = w.step === "baseURL" ? value.replace(/\/+$/, "")
135
+ : w.step === "format" ? (value.toLowerCase() || "openai") // 空输入 = openai 默认(D-C2)
136
+ : value
115
137
  const next = WIZARD_NEXT[w.step]
116
138
  if (next) {
117
139
  w.step = next
@@ -131,9 +153,27 @@ export function createWizard(ctx) {
131
153
  async function finishWizard() {
132
154
  const f = state.wizard.fields
133
155
  state.wizard = null
156
+ // D-C2:format 非默认(anthropic/google)时落盘;openai = 默认省略(与 D-C1 picker 路径同构)
157
+ const providerRec = { name: f.name, baseURL: f.baseURL, model: f.model, apiKey: f.key }
158
+ if (f.format && f.format !== "openai") providerRec.format = f.format
159
+ // code review 🟡:preset 直达带来的扩展字段一并落盘(truthy 语义与 pickers preset 分支一致——
160
+ // thinking: null 不落;Custom 路径无这些字段不受影响)
161
+ for (const k of ["thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
162
+ if (f[k]) providerRec[k] = f[k]
163
+ }
164
+ // D-F5a(wizard finishWizard——清单外同型写回补正)先盘后存:磁盘 fresh raw 单操作
165
+ // (upsert 目标项 + active 指针)——冲突放弃不留下内存 ghost(F5 约定)
166
+ await persistRaw((raw) => {
167
+ raw.providers ??= []
168
+ const existing = raw.providers.find((p) => p?.name === f.name)
169
+ if (existing) Object.assign(existing, providerRec)
170
+ else raw.providers.push(providerRec)
171
+ raw.activeProvider = f.name
172
+ raw.activeModel = undefined // reset to default model
173
+ })
134
174
  const existing = agent.providers.find((p) => p.name === f.name)
135
- if (existing) Object.assign(existing, { baseURL: f.baseURL, model: f.model, apiKey: f.key })
136
- else agent.providers.push({ name: f.name, baseURL: f.baseURL, model: f.model, apiKey: f.key })
175
+ if (existing) Object.assign(existing, providerRec)
176
+ else agent.providers.push(providerRec)
137
177
  agent.activeProvider = f.name
138
178
  agent.activeModel = null
139
179
  agent.provider = { ...agent.providers.find((p) => p.name === f.name) }
@@ -141,20 +181,16 @@ export function createWizard(ctx) {
141
181
  const { resolveCompactThreshold } = await import("../config.mjs")
142
182
  agent.config.agent.compactThreshold = resolveCompactThreshold(null, f.model).value
143
183
  }
144
- await persistRaw((raw) => {
145
- raw.providers = agent.providers
146
- raw.activeProvider = f.name
147
- raw.activeModel = undefined // reset to default model
148
- })
149
184
  agent.config.activeProvider = f.name
150
185
  agent.config.activeModel = null
151
186
  pushLabel(`❯ Setup`, ansi.bold + C.tool)
152
187
  pushLine(`Setup complete: ${f.name} / ${f.model} (saved to config)`, C.tool)
153
188
  // embedding key: if provided, enable vector search; if not, show how to enable later
154
189
  if (f.embedkey) {
190
+ // D-F5b 语义先盘后存(embedding 单键补丁——冲突放弃不留 ghost)
191
+ await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
155
192
  agent.config.embedding ??= {}
156
193
  agent.config.embedding.apiKey = f.embedkey
157
- await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
158
194
  if (agent.memory && !agent.memory.embedder) {
159
195
  const { createEmbedder } = await import("../embedding.mjs")
160
196
  agent.memory.embedder = createEmbedder(agent.config.embedding)
@@ -1,179 +0,0 @@
1
- /**
2
- * escalate.mjs — 飞刀 (the "flying knife", docs/design/ESCALATE.md). CLI port.
3
- *
4
- * Hand an implementation task to a STRONGER model — like a hospital flying in an
5
- * outside expert (飞刀): the expert arrives, operates personally (WRITE access),
6
- * hands back the post-op report, leaves. Complementary to consult (parallel
7
- * READ-ONLY opinions for judgment calls).
8
- *
9
- * Candidate pool = all consultModels rows. The tool is only registered when the
10
- * pool is non-empty (setup.mjs).
11
- *
12
- * CLI adaptation (vs the VS Code plugin): the child runner is CLI's runAgent
13
- * (runAgent(child, input, callbacks, opts) — an agent object, not provider+cwd);
14
- * the child is createAgent({ role: "coder", CODER_OVERLAY }); activity streams to
15
- * the parent TUI via the relay prefix `escalate#<id>/`; mutations merge via the
16
- * CLI mergeChildMutations(parent, child) (agent object, not a state sink).
17
- */
18
- import { isAbsolute, relative } from "node:path"
19
- import { createAgent, runAgent, CODER_OVERLAY, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
20
- import { resolveChildProvider, mergeChildMutations } from "./subagent.mjs"
21
- import { makeRelay, wrapChildCallbacks, runWithContinue, ensureChildApiKey, clampEffort, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
22
-
23
- const label = (m) => `${m.provider}:${m.model}`
24
-
25
- export const escalateTool = {
26
- name: "escalate",
27
- sideEffectExempt: true, // the child's mutations are tracked and reviewed, like subagent
28
- description:
29
- "TERMINOLOGY (one word for one thing): 'escalate' is the ONLY name — the tool, and the " +
30
- "role of the expert sub-agent it spawns, are both called 'escalate'; 飞刀 is the Chinese " +
31
- "alias. When the user says 飞刀 / escalate / 'fly in <model>', call THIS tool directly — " +
32
- "never via a script importing this module. " +
33
- "Hand an implementation task to a stronger model (飞刀 — a flown-in expert). " +
34
- "It gets WRITE access and does the work itself — reads, edits, runs tests — then returns " +
35
- "a post-op report (what changed, why, verification). You review the report and report to " +
36
- "the user. Use it when YOU judge the task calls for stronger hands (complex multi-file " +
37
- "refactoring, an intractable bug, intricate algorithm work — or work beyond your " +
38
- "comfortable ability). Early or late, your judgment; the cost is one expert run, " +
39
- "comparable to doing it yourself. For parallel READ-ONLY opinions use consult_start instead. " +
40
- "Not available in engineering mode (implementation goes through eng-coder subagents there).\n" +
41
- "Parameters:\n" +
42
- "- task (required): the task description — goal, constraints, entry files, acceptance criteria\n" +
43
- "- model (optional): pick a specific consultant as 'provider:model'; default = the first consult model",
44
- parameters: {
45
- type: "object",
46
- properties: {
47
- task: { type: "string", description: "Task description with acceptance criteria" },
48
- model: { type: "string", description: "Candidate 'provider:model' from the consult models (optional)" },
49
- },
50
- required: ["task"],
51
- },
52
- async execute({ task, model }, ctx) {
53
- const parent = ctx.agent
54
- if ((ctx.depth ?? 0) > 0) return "Error: escalate is only available at depth 0 (an escalate's work cannot be delegated again)"
55
- if (parent?.config?.agent?.engineering) {
56
- return "Error: engineering mode is ON — escalate is unavailable (it spawns a coder sub-agent, which engineering mode forbids). Use subagent with role='eng-coder' and a designToken from advisor(type='design') instead."
57
- }
58
- const pool = parent?.config?.agent?.consultModels ?? []
59
- if (pool.length === 0) return "Error: no escalate candidates — configure at least one consult model (agent.consultModels)"
60
-
61
- const wanted = typeof model === "string" ? model.replace(/\s+\([^)]*\)\s*$/, "").trim() : model
62
- const pick = wanted ? pool.find((m) => label(m) === wanted) : pool[0]
63
- if (!pick) {
64
- return `Error: "${model}" is not a consult candidate. Available: ${pool.map(label).join(", ")}`
65
- }
66
-
67
- let provider
68
- try {
69
- provider = resolveChildProvider(parent, `${pick.provider}:${pick.model}`)
70
- } catch (e) {
71
- return `Error: ${e.message}`
72
- }
73
- if (!ensureChildApiKey(provider)) {
74
- return `Error: provider "${pick.provider}" has no API key — set it in config.json before flying it in`
75
- }
76
- let effortNote = ""
77
- if (pick.effort && !clampEffort(provider, pick.model, pick.effort)) {
78
- // Out-of-enum effort dropped (see clampEffort): the provider preset default may ALSO be
79
- // out-of-enum for this override model — e.g. qwenplan preset default "high" is
80
- // invalid for qwen3.8-max, enum xhigh/medium/low.
81
- effortNote = ` (effort "${pick.effort}" unsupported by ${pick.model}, dropped)`
82
- }
83
-
84
- const tag = label(pick)
85
- const relayPrefix = makeRelay(parent, "escalate", ctx.callbacks?.onToken, provider.model ?? tag)
86
- // Report the escalated model to the display layer (it may differ from the parent's)
87
- // — makeRelay already emitted the `[model]` metadata token above.
88
-
89
- // No wall-clock watchdog — turn cap only, exactly like subagent (the verified write
90
- // path). Rationale (2026-08-16): a fixed wall-clock aborts NORMAL-but-slow surgery —
91
- // two max-effort consultants hit a 10min wall just READING files. Hang protection is
92
- // already covered by FETCH_TIMEOUT_MS (per LLM call) and the user's Stop (parent
93
- // signal propagates directly below). maxTurns is the cost budget; hitting it asks
94
- // the user whether to continue (main-agent parity), falling back to partial work.
95
-
96
- // No custom onToken here (consult P2, 2026-08-30): wrapChildCallbacks already
97
- // applies the prefixed relay + D7 sentinel strip for the display path, and
98
- // runWithContinue owns the capture (stripEventTokensForCapture) for the
99
- // partial-output return — a hand-rolled duplicate ran the strip twice and
100
- // maintained a second output buffer.
101
- const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
102
-
103
- // Declared outside try so the catch can merge mutations even on a partial failure.
104
- let child = null
105
- try {
106
- // Full write path (role "coder"): permission gate via the parent's onPermissionRequest,
107
- // recent-changes tracking, mutations merge into the parent below.
108
- child = createAgent({
109
- provider,
110
- tools: parent.tools,
111
- config: parent.config,
112
- cwd: parent.cwd,
113
- memory: parent.memory,
114
- overlay: CODER_OVERLAY,
115
- role: "coder",
116
- })
117
- const runner = ctx.runAgent ?? runAgent
118
- const runOpts = {
119
- depth: 1,
120
- maxTurns: parent.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS, // review #7: constant, not literal (single source with subagent.mjs)
121
- signal: ctx.signal ?? null,
122
- }
123
- // Turn-cap continue via runWithContinue (§7.2 D3), main-agent parity (tui/agent-turn.mjs):
124
- // when the child hits ContinueError, ask the user through the SAME channel as child
125
- // write approval (ctx.onPermissionRequest). The name "continue" renders the TUI's
126
- // dedicated y/n Continue panel. The resumed run passes resume:true, so runAgent does
127
- // NOT re-inject the task text (setup.mjs skips input on resume) and keeps the child's
128
- // history + mutation bookkeeping, with a fresh maxTurns budget per run. No permission
129
- // handler (headless) or a declined prompt falls through to the partial-work return.
130
- // Continues are UNLIMITED — the user can decline at any prompt.
131
- const report = await runWithContinue(
132
- async (childAgent, input, cbs, opts) => {
133
- // Merge mid-run mutations even when the run throws — the outer catch keeps
134
- // handling createAgent failures; AbortError still propagates (user Stop).
135
- try {
136
- return await runner(childAgent, input, cbs, opts)
137
- } catch (e) {
138
- mergeChildMutations(parent, childAgent)
139
- throw e
140
- }
141
- },
142
- child, task, { ...childCallbacks, onPermissionRequest: parent.autoApprove ? async () => true : (ctx.onPermissionRequest ?? null) },
143
- runOpts,
144
- {
145
- // escalate has NO permQueue: prompts go straight to the user (T-L spec).
146
- askContinue: (e) => (ctx.onPermissionRequest
147
- ? ctx.onPermissionRequest("continue", { turns: e.turn, agent: tag })
148
- : Promise.resolve(false)),
149
- onDeclined: (e, output) => `escalate (${tag}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output.slice(0, 2000)}`,
150
- },
151
- ).catch((e) => {
152
- // Generic run failure (not ContinueError): match the original loop's return shape —
153
- // error text + partial output, mutations already merged in the runner wrapper.
154
- if (ctx.signal?.aborted || e?.name === "AbortError") throw e
155
- return `escalate (${tag}) error: ${e?.message ?? String(e)}\nPartial output: ${(child._capturedOutput ?? "").slice(0, 2000)}`
156
- })
157
- // Escalate mutations are the parent's mutations: verify/advisor guards must see them
158
- mergeChildMutations(parent, child)
159
- return `escalate (${tag})${effortNote} post-op report:\n${report || (child._capturedOutput ?? "").slice(0, 4000)}${touchedFilesNote(child, parent.cwd)}`
160
- } catch (e) {
161
- // Reached only when createAgent itself fails or the continue prompt throws —
162
- // run failures are handled above (mutations merge inside the runner wrapper).
163
- if (child) mergeChildMutations(parent, child)
164
- if (ctx.signal?.aborted || e?.name === "AbortError") throw e
165
- return `escalate (${tag}) error: ${e?.message ?? String(e)}`
166
- }
167
- },
168
- }
169
-
170
- /** Relative touched-file list appended to every return (child paths are absolute). */
171
- function touchedFilesNote(child, cwd) {
172
- const touched = child?._touchedFiles ?? []
173
- if (touched.length === 0) return ""
174
- const shown = touched.map((f) => {
175
- const r = relative(cwd ?? process.cwd(), f)
176
- return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
177
- })
178
- return `\nTouched files: ${shown.join(", ")}`
179
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * subagent-check.mjs — async subagent 结果消费侧(AGENT-LOOP.md §15 D-A2)。
3
- *
4
- * 与 subagent.mjs 的生成侧(async 分支 + 槽位队列)分离,保持 subagent.mjs
5
- * 在 500 行硬限内;本模块只承载 subagent_check 工具 + 其等待机制。
6
- *
7
- * 语义(D-A2):
8
- * - id 缺省 → 按 ARRIVAL ORDER 返回下一个已完成的子代理(先完成先返回)
9
- * - id 给定 → 阻塞到该 id 完成(queued 项先等启动再等完成)
10
- * - n(必填)→ 1-based 递增读数,per-run 计数器(runAgent 非 resume 重置、
11
- * turn-end 清空);乱序/重复 n 拒绝且不消费结果;超 MAX_ASYNC_CHECKS 上限
12
- * 报错引导走回合收尾自动等待
13
- * - 消费后从 map 删除——已消费 id 再查 = 与未知 id 同款错误(T12)
14
- */
15
- import { MAX_ASYNC_CHECKS } from "./subagent.mjs"
16
-
17
- /** Wait for an async entry to settle (or the parent signal to abort), parked on
18
- * the agent's waiter list — the entry settle finally wakes every waiter (same
19
- * pattern as consult_check's session waiters). Returns "aborted" on signal. */
20
- function wakeOnAsyncSettle(agent, ctx) {
21
- return new Promise((resolve) => {
22
- const cleanup = () => {
23
- const i = (agent._asyncWaiters ?? []).indexOf(w)
24
- if (i >= 0) agent._asyncWaiters.splice(i, 1)
25
- ctx.signal?.removeEventListener("abort", onAbort)
26
- }
27
- const w = () => { cleanup(); resolve("settled") }
28
- const onAbort = () => { cleanup(); resolve("aborted") }
29
- ;(agent._asyncWaiters ??= []).push(w)
30
- if (ctx.signal) {
31
- if (ctx.signal.aborted) { onAbort(); return }
32
- ctx.signal.addEventListener("abort", onAbort, { once: true })
33
- }
34
- })
35
- }
36
-
37
- /**
38
- * subagent_check (AGENT-LOOP.md §15 D-A2) — fetch async subagent results.
39
- * - id omitted → the next completed child in ARRIVAL order (first finished first)
40
- * - id given → block until THAT child finishes (queued items wait for their start)
41
- * - n (required) → 1-based read counter, strictly incrementing per run (loop
42
- * guard); capped at MAX_ASYNC_CHECKS per turn — beyond that, use the turn-end
43
- * auto-wait. Errors never consume results.
44
- * Consumed entries are deleted from the map — a re-check of the same id is the
45
- * same "unknown async subagent id" error (T12).
46
- */
47
- export const subagentCheckTool = {
48
- name: "subagent_check",
49
- readonly: true,
50
- description:
51
- "Fetch the result of an async subagent (subagent with async:true). Spawn async children to keep working in your own turn while they run in the background, then collect their reports here. Multiple async children return in completion (arrival) order — the first finished is returned first, so fast results are handled immediately instead of waiting for the slowest. Blocks until the target finishes.\n" +
52
- "When done is true, no more results are coming (all finished and consumed) — anything left arrives automatically at turn end.\n" +
53
- "Parameters:\n" +
54
- "- id (optional): the subagent id from the async spawn return. Omit to fetch the next completed child (arrival order).\n" +
55
- "- n (required): 1-based read counter — pass 1 on the first check, 2 on the next, and so on. Consecutive checks must be distinct tool calls (loop detector); at most 3 checks per turn — use the turn-end auto-wait for the rest.",
56
- parameters: {
57
- type: "object",
58
- properties: {
59
- id: { type: "string", description: "Optional subagent id (from the async spawn return). Omit = next completed child (arrival order)." },
60
- n: { type: "number", description: "1-based read counter: 1 for the first check of the turn, incrementing with each subsequent check (loop detector — consecutive checks must be distinct tool calls)." },
61
- },
62
- required: ["n"],
63
- },
64
- async execute({ id, n }, ctx) {
65
- const agent = ctx.agent
66
- const map = agent._asyncSubagents ?? new Map()
67
- // Strict 1-based incrementing read counter (D-A2, review #1): out-of-order /
68
- // repeated n is rejected WITHOUT consuming a result (T14).
69
- const lastN = agent._asyncCheckLastN ?? 0
70
- if (!Number.isInteger(n) || n !== lastN + 1) {
71
- return JSON.stringify({ status: "error", error: "invalid read counter — pass n = lastN+1" })
72
- }
73
- if (n > MAX_ASYNC_CHECKS) {
74
- return JSON.stringify({ status: "error", error: "check limit exceeded — use turn-end auto-wait for the rest" })
75
- }
76
- agent._asyncCheckLastN = n
77
-
78
- let target = null
79
- if (id !== undefined && id !== null && String(id) !== "") {
80
- target = map.get(String(id))
81
- // Unknown OR already-consumed ids (consumed entries are deleted) — same error (T12).
82
- if (!target) return JSON.stringify({ id: String(id), status: "error", error: `unknown async subagent id: ${id}` })
83
- }
84
-
85
- // Block until the target settles (specific id / next completed in arrival order).
86
- for (;;) {
87
- if (target) {
88
- if (target.done) break
89
- const woke = await wakeOnAsyncSettle(agent, ctx)
90
- if (woke === "aborted") return JSON.stringify({ done: true, stopped: true })
91
- continue
92
- }
93
- const completed = [...map.values()].filter((e) => e.done)
94
- if (completed.length > 0) {
95
- target = completed.sort((a, b) => (a._settleSeq ?? 0) - (b._settleSeq ?? 0))[0]
96
- break
97
- }
98
- if (map.size === 0) return JSON.stringify({ done: true })
99
- const woke = await wakeOnAsyncSettle(agent, ctx)
100
- if (woke === "aborted") return JSON.stringify({ done: true, stopped: true })
101
- }
102
-
103
- map.delete(String(target.id))
104
- if (target.error) return JSON.stringify({ id: String(target.id), status: "error", error: target.error })
105
- return JSON.stringify({ id: String(target.id), role: target.role, status: "done", report: target.report ?? "" })
106
- },
107
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * exec-prelude.mjs — sandbox API injected into `execute`'s child process.
3
- *
4
- * The `execute` tool spawns `node --input-type=module --eval` and `import()`-s
5
- * this file first, so the user's code gets readFile/writeFile/glob/grep/log/require
6
- * without hand-writing fs boilerplate. Confines file paths to the workspace root
7
- * (THINCODER_EXEC_ROOT, default cwd) — an orthopedic guard, NOT a sandbox:
8
- * require()/process/import()/fetch() are full Node, the same boundary as bash
9
- * (project philosophy: no fake sandbox; transparency + audit).
10
- */
11
- import { createRequire } from "node:module"
12
- import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync, readdirSync } from "node:fs"
13
- import { resolve, relative, dirname, join, isAbsolute, sep } from "node:path"
14
-
15
- const require = createRequire(join(process.cwd(), "__exec__.js"))
16
- const root = process.env.THINCODER_EXEC_ROOT || process.cwd()
17
-
18
- /** Resolve a path against the working dir, asserting it stays within the workspace root. */
19
- function safe(p) {
20
- if (typeof p !== "string") throw new Error(`Path must be a string, got ${typeof p}`)
21
- const abs = resolve(process.cwd(), p)
22
- const rel = relative(root, abs)
23
- // isAbsolute(rel) covers cross-drive (relative() returns an absolute path then)
24
- if (isAbsolute(rel) || rel === ".." || rel.startsWith(".." + sep)) {
25
- throw new Error(`Path traversal denied: ${p}`)
26
- }
27
- return abs
28
- }
29
-
30
- function globToRegex(pattern) {
31
- const DS = "\u0001", DP = "\u0002"
32
- const escaped = pattern
33
- .replace(/\*\*\//g, DS).replace(/\*\*/g, DP)
34
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
35
- .replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")
36
- .replace(new RegExp(DS, "g"), "(?:.+/)?").replace(new RegExp(DP, "g"), ".*")
37
- return new RegExp(`^${escaped}$`)
38
- }
39
-
40
- globalThis.require = require
41
- globalThis.readFile = (p) => {
42
- const abs = safe(p)
43
- if (!existsSync(abs)) throw new Error(`File not found: ${p}`)
44
- if (statSync(abs).size > 5_000_000) throw new Error(`File too large: ${p}`)
45
- return readFileSync(abs, "utf8").replace(/\r\n/g, "\n")
46
- }
47
- globalThis.writeFile = (p, content) => {
48
- const abs = safe(p)
49
- mkdirSync(dirname(abs), { recursive: true })
50
- writeFileSync(abs, String(content), "utf8")
51
- }
52
- globalThis.glob = (pattern) => {
53
- if (typeof pattern !== "string") throw new Error("glob pattern must be a string")
54
- const re = globToRegex(pattern)
55
- const out = []
56
- function walk(dir, rel) {
57
- let entries
58
- try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
59
- for (const e of entries) {
60
- if (e.name.startsWith(".") || e.name === "node_modules") continue
61
- const rp = rel ? `${rel}/${e.name}` : e.name
62
- if (e.isDirectory()) walk(join(dir, e.name), rp)
63
- else if (re.test(rp)) out.push(rp)
64
- }
65
- }
66
- walk(process.cwd(), "")
67
- const capped = out.slice(0, 200)
68
- if (out.length > 200) capped.push(`... (${out.length - 200} more)`)
69
- return capped
70
- }
71
- globalThis.grep = (pattern, file) => {
72
- if (typeof pattern !== "string") throw new Error("grep pattern must be a string")
73
- if (typeof file !== "string") throw new Error("grep file must be a string")
74
- const abs = safe(file)
75
- if (!existsSync(abs)) throw new Error(`File not found: ${file}`)
76
- const re = new RegExp(pattern)
77
- const lines = readFileSync(abs, "utf8").replace(/\r\n/g, "\n").split("\n")
78
- const m = []
79
- for (let i = 0; i < lines.length; i++) if (re.test(lines[i])) m.push(`${i + 1}: ${lines[i].slice(0, 200)}`)
80
- const capped = m.slice(0, 100)
81
- if (m.length > 100) capped.push(`... (${m.length - 100} more)`)
82
- return capped
83
- }
84
- globalThis.log = (...a) => console.log(a.map((x) => (x && typeof x === "object" ? JSON.stringify(x) : String(x))).join(" "))