thincoder 0.12.50 → 0.12.52
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.
- package/CHANGELOG.md +64 -3
- package/README.md +2 -2
- package/package.json +4 -3
- package/src/acp/bridge.mjs +5 -0
- package/src/agent/dispatch.mjs +19 -7
- package/src/agent/helpers.mjs +13 -1
- package/src/agent/record-results.mjs +130 -0
- package/src/agent/setup.mjs +4 -7
- package/src/agent/spawn-child.mjs +159 -0
- package/src/agent-tools/consult.mjs +94 -73
- package/src/agent-tools/escalate.mjs +53 -62
- package/src/agent-tools/skill.mjs +1 -1
- package/src/agent-tools/subagent.mjs +39 -38
- package/src/agent-tools/task.mjs +0 -2
- package/src/agent-tools/verify.mjs +0 -1
- package/src/agent.mjs +27 -112
- package/src/config.mjs +8 -103
- package/src/generate-title.mjs +30 -1
- package/src/model-specs.mjs +108 -0
- package/src/prompts/advisor-round1.md +5 -6
- package/src/prompts/advisor-round2.md +3 -4
- package/src/prompts/advisor-round3.md +3 -4
- package/src/prompts/eng-coder.md +9 -0
- package/src/prompts/engineering.md +61 -9
- package/src/prompts/system.md +2 -2
- package/src/provider/core.mjs +5 -71
- package/src/provider/normalize.mjs +81 -0
- package/src/session.mjs +40 -1
- package/src/tools/git.mjs +3 -3
- package/src/tools/shared.mjs +1 -0
- package/src/tools/system.mjs +3 -1
- package/src/tui/agent-turn.mjs +37 -364
- package/src/tui/clipboard.mjs +3 -1
- package/src/tui/dims.mjs +47 -0
- package/src/tui/fold-block.mjs +208 -0
- package/src/tui/index.mjs +33 -17
- package/src/tui/key-handler-search.mjs +1 -1
- package/src/tui/key-handler.mjs +10 -6
- package/src/tui/layout.mjs +21 -20
- package/src/tui/mouse.mjs +9 -6
- package/src/tui/pickers.mjs +1 -1
- package/src/tui/render-conversation.mjs +367 -113
- package/src/tui/render-frame.mjs +16 -90
- package/src/tui/render-loop.mjs +12 -8
- package/src/tui/render.mjs +16 -0
- package/src/tui/startup.mjs +66 -13
- package/src/tui/subagent-blocks.mjs +327 -0
- package/src/tui/tool-args.mjs +67 -0
- package/src/tui/tool-events.mjs +459 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
## [0.12.52] — 2026-08-31
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **sanitizeDisplay 吞正文(用户报障)**:正文含字面 `⟦ev⟧` 时(如讨论 ACP 桥剥除语义的结果表),D5 残段剥除 `/⟦ev⟧[^RS-GS]*/` 按字符类语义"吞到行尾"——该格到回复末尾全没了。真 token = 哨兵+字母 phase 词,收窄为 `/⟦ev⟧[A-Za-z]*/`:裸哨兵正文合法保留,live token 照剥(防伪不破,旧语义断言随测试反转)
|
|
6
|
+
- **窗口拖小不生效**:双次相同确认规则(ConPTY 防御)在"拖动只收一个 resize 事件"的场景永不触发,UI 停在旧大宽度溢出;真 resize 事件 400ms settle 后提交(growth 仍立即、untrusted 采样防御不动)
|
|
7
|
+
- **dims 误诊机器拆除**:ConPTY stale 假说整套机构(双确认/settle/看门狗/启动收敛/turn 采样/sawValid)建立在对折叠块 cols=80 的误诊上——全部删除(dims 98→45 行);现行铁律 = 渲染只读 get()、refresh() 仅事件钩子(seed/resize)、任何 sane 采样立即提交
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **头部显示版本号**:`ThinCoder 0.12.52 │ model │ cwd`(logo 右侧 dim,模块级读 package.json)
|
|
12
|
+
- **提示词**:system prompt "task tracks work for **EVERY tier — even Small**"(注意力层级对齐,修 Small 漏建 task)
|
|
13
|
+
|
|
14
|
+
### Refactor
|
|
15
|
+
|
|
16
|
+
- **model-specs.mjs**(TODO #1):MODEL_SPECS + specForModel 抽出,config.mjs 358→266;re-export 保证 23 个 importer 零改动
|
|
17
|
+
- **provider/normalize.mjs**(TODO #2):stripImagesForTextModel + normalizeToolPairing(发送前载荷净化)抽出,core.mjs 420→350
|
|
18
|
+
- **config.schema.json 删除**(TODO #8,用户裁定):从未闭环(线上 URL 未部署/代码零消费/25 键缺 20);saveConfig 不再注入 $schema
|
|
19
|
+
- **死代码清理**:15 处死 import + 9 无用转义 + 17 control-regex 注明有意;lint warnings 86→44
|
|
20
|
+
- **test/ 纳入 lint**(TODO #7):62 条存量清零,`eslint src test`
|
|
21
|
+
|
|
22
|
+
### Perf
|
|
23
|
+
|
|
24
|
+
- 慢测试修复(TODO #10):"原子写不残留 .tmp" 断言曾 readdirSync 扫 `~/.thincoder/sessions/` 全目录(3 万文件时单次 18s)——改 O(1) existsSync 直探本槽位,session 往返用例 16.4s→28ms(仅测试基建,不影响运行时)
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
|
|
28
|
+
- read_image × glm-5.3-flash 直接用例(PROVIDER.md §11 T2 落自动化);TOOLS.md §6/§7 用例表回补(T-g-1..12 / T-w / T-e,对齐 §8)
|
|
29
|
+
- 812/812 全绿;lint 0 errors(44 warnings 均为 C 类 parity 锚点)
|
|
30
|
+
|
|
31
|
+
## [0.12.51] — 2026-08-30
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **折叠系统大修(0.12.7 单框化的终章)**:公共折叠组件 `src/tui/fold-block.mjs`(147→168 行)——所有可折叠区块(思考/工具/子agent/advisor/长消息/连续 dim)统一:折叠态 = 命名头 + tail 3 行;展开态 = 空行 + ▼ 控制行 + 60% 屏幕封顶(触顶时底部第二个 ▼ 控制行必在视口内)。主输出(C.text)**永不折叠**;思考**无条件折叠**(阈值思路三次真机失败后废弃);工具摘要(C.dim)>12 行折叠。`/fold off` 全关
|
|
36
|
+
- **工具调用单框化**:`_toolBlock` 载体——头 = `❯ name 参数摘要 · running|耗时·摘要`,体 = 参数 pretty JSON + 流式输出 + 结果(dim)。旧四段式(标题/_live 滚动/done 行)废除
|
|
37
|
+
- **会话文件瘦身(deepseek 会诊方案,`slimForDisplay`)**:写入时人读线截断(tool args 300 字符 / tool 结果 500 / 多模态图剥 base64 只留 text part),机读线(contextHistory)一字不动(provider 前缀缓存/配对/多轮看图零风险);copy-on-write 绝不原地改(两线共享对象引用);VS Code 端同款
|
|
38
|
+
- **会诊(consult)体验修复**:consult_check/consult_stop 新增递增 `n` 参数(连续调用参数集不同,绕开循环检测器误报);会诊结束**会话级结算**(finishSubTasksByRole 全量冻结 N 块,单块 finishSubTask 曾留 N-1 个 running 幽灵);单条回复按 model 精确收尾(尾段归一化——`[model]` 裸名 vs `r.model` provider:model 曾是死代码);冻结墓碑防 abort 尾部 token 复活块
|
|
39
|
+
- **终端尺寸单源(`dims.mjs`)**:Windows ConPTY 的 columns/rows 不稳定(启动 falsy、输出活动期报 stale 小值)——sample-and-hold 缓存 + 事件驱动采样(启动收敛重试/resize/空闲看门狗/turn-start/turn-finally),渲染路径纯读缓存;非对称接受(变大立即提交、变小需连续两次确认)
|
|
40
|
+
- **主输出呼吸空行**:主输出段前后各空一行(渲染期插入,不写 state.lines);streaming 与落盘路径一致(首版漏 streaming 分支,用户实测"生成时不空落盘后才空")
|
|
41
|
+
- **任务面板顶部分隔线** + **子agent 运行区块段首分隔线**(`─` dim,与上方会话区切分;小终端压缩时 task 分隔线先让位)
|
|
42
|
+
- **会诊四轮**:P0(搜索缓存键/中断清扫/tool_call_id 贯穿)、P1(死代码/缓存维度/增量行数/restore 守卫/foldKey 稳定化)、P3(multimodal flag 驱动 offload/日志截断/事件语法单源)——第三轮会诊(ConPTY)与窄屏真根因(组件漏传 cols)分别独立成条
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **流式窄屏真根因**:`renderExpandedBlock`/`renderFoldedHead` 三处调用漏传 `cols` → 组件按默认 80 wrap(280 列终端"生成中左边一小块、落盘后宽");全文扫尾 + 组件 cols 纪律入文档
|
|
47
|
+
- **窄屏误诊链清理**:ConPTY 采样不再每帧 refresh(输出活动期 stale 80 污染缓存);启动收敛跑满窗口(sawValid 曾提前掐断);turn-start/turn-finally 采样点
|
|
48
|
+
- **会诊残留双根因**:finishSubTask 单块语义(N-1 幽灵 + interrupted 误标)+ 无墓碑(迟到 token 复活块)
|
|
49
|
+
- **subagent/escalate/advisor 成功调用误标 "(interrupted)"**(settleToolBlock 收尾);Ctrl+I 中断时 tool_calls 悬空(合成占位 tool 结果,strict provider 400);main 输出空行 streaming 路径缺失
|
|
50
|
+
- **搜索高亮被缓存吃掉**(convCacheKey 补 search 维度);[model] token 后头部不刷新(subSig 补 model);并行同名工具输出/耗时错配(toolCallId 贯穿)
|
|
51
|
+
- **generate-title proxy 路径 bug**(动态 import 相对路径从 src/ 解析到仓库根——静默 ERR_MODULE_NOT_FOUND 被 catch 吞,代理用户标题自创建起失效;静态导入 + 测试缝)
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- **agent.mjs 拆分**:工具结果提交/记账抽 `src/agent/record-results.mjs`(多模态延迟注入、FILE_MUTATORS 失效链、touchedFiles + reindex);`_maxTurns` 引用 DEFAULT_MAX_TURNS;escalate 删手写 onToken/output(runWithContinue 统一 capture);AUTO_REMINDER/ensureAutoReminder 单源(helpers);魔法数字具名(tool-events caps / consult 默认值);事件语法分支列表单源(EVENT_PHASE/EVENT_TYPE)
|
|
56
|
+
- **行语法统一**:三个行生产者(live flushStream / 恢复 historyToLines / 注入行)统一打 `_kind` 标记(thinking/text/tool),buildConvLines 读标记不再从颜色猜
|
|
57
|
+
- **live/restore 逐行对齐**:同一载体、同一渲染路径;恢复结果守卫与 live 共享(`slimToolResultForDisplay`:read_image base64 剥离 + 400 行封顶)
|
|
58
|
+
- **折叠 key 稳定化**:工具块 fold key 用行级 `_lineId`(loadOlder unshift 不再错绑展开态)
|
|
59
|
+
- **文档同步**:TUI.md / TUI-TOOL-OUTPUT.md / CONSULTATION.md / SESSION.md / AGENT-LOOP.md 与今日全部机制更新对齐
|
|
60
|
+
|
|
61
|
+
### Tests
|
|
62
|
+
|
|
63
|
+
- 812 全量(今日新增:折叠组件、dims 单源(sampler 注入 + 守卫 grep 断言)、会诊残留/精确收尾/墓碑、streaming 空行路径、subagent 分隔线、中断配对、工具块 ids、会话瘦身等 30+ 用例)
|
|
64
|
+
- 会诊四轮评审全部落地并回归锁定:P0/P1/P3 + ConPTY 硬度 + ByModel 生产格式(自洽世界假绿被二次会诊实锤后改用生产报文断言)
|
|
2
65
|
|
|
3
|
-
本文件记录 ThinCoder CLI 的发布历史。格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),版本遵循[语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
66
|
|
|
5
|
-
## [0.12.50] — 2026-08-29
|
|
6
67
|
|
|
7
68
|
### Changed
|
|
8
69
|
|
package/README.md
CHANGED
|
@@ -300,7 +300,7 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
|
|
|
300
300
|
- **CodeMode: sandboxed JS execution** — `execute` tool backed by `vm.Script.runInNewContext`. Compose multiple file operations (read/write/glob/grep/log) into a single script, reducing API round-trips and keeping intermediate results out of context. Sandbox strips all Node APIs, limits output to 50KB, enforces 30s timeout, and blocks private IPs in fetch. Script size capped at 50KB.
|
|
301
301
|
|
|
302
302
|
### 0.9.0 (2026-07)
|
|
303
|
-
- **
|
|
303
|
+
- **Checkpoint v2** — snapshot-before-destructive with full workspace copies, per-file restore (`cat` preview), auto-recover on failed apply, and escape-hatch hints when git checks fail
|
|
304
304
|
- **Lifecycle Hooks** — `PreToolUse` / `PostToolUse` / `PostToolUseFailure` / `Notification` events. User-defined shell commands in config, with per-tool regex matching, timeout control, and `block`/`allow`/`notify` actions. Implemented in `src/hooks.mjs`, integrated into tool dispatch.
|
|
305
305
|
- **Built-in Skills (5)** — `pdf-create`, `xlsx-create`, `frontend-design`, `code-review`, `api-design` ship with the installation. Each is a standalone markdown instruction file using zero-dependency approaches (Chrome headless for PDF, PowerShell for Excel, etc.).
|
|
306
306
|
- **Conversation message folding** — Long tool result blocks (>8 consecutive dim lines) auto-collapse to first 2 lines + "… N more lines — Enter to expand". `/fold on|off` toggles globally.
|
|
@@ -354,7 +354,7 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
|
|
|
354
354
|
- **Feat**: auto update check on startup + `/upgrade` command
|
|
355
355
|
|
|
356
356
|
### 0.8.3 (2026-07)
|
|
357
|
-
- **Output panels**: tools with `outputPanel` flag stream to scrolled panel, auto-collapse to summary on completion (bash, long tool results)
|
|
357
|
+
- **Output panels**: tools with `outputPanel` flag stream to scrolled panel, auto-collapse to summary on completion (bash, long tool results)(机制已于 2026-08-30 废除——D6 清理,见 AGENT-LOOP.md)
|
|
358
358
|
- **Checkpoint enhancements**: `cat` for file preview from snapshots, per-file rewind, auto-recover on apply failure, escape-hatch hints on errors
|
|
359
359
|
- **Bash safety**: `checkpoint-before-destructive` discipline rule; bash guard guides checkpoint instead of just commit/stash
|
|
360
360
|
- **Code review fixes**: output friendliness, readability, English-only strings, TUI polish
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thincoder",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.52",
|
|
4
4
|
"description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
"url": "git+https://github.com/xinbo-tech/thincoder.git"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"test": "node --test \"test/*.mjs\"",
|
|
34
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
35
35
|
"prepublishOnly": "npm run lint && node --test \"test/*.mjs\"",
|
|
36
|
-
"lint": "eslint src"
|
|
36
|
+
"lint": "eslint src test",
|
|
37
|
+
"test:full": "node test/run-full.mjs"
|
|
37
38
|
},
|
|
38
39
|
"author": "liwei <liwei@51marine.com> (上海新舶)",
|
|
39
40
|
"license": "MIT",
|
package/src/acp/bridge.mjs
CHANGED
|
@@ -70,6 +70,11 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
|
|
|
70
70
|
// Strip the subagent `[model]` metadata token (role#id/[model]<name>) — it's a
|
|
71
71
|
// TUI/webview display signal, not conversation content, and must not reach ACP clients.
|
|
72
72
|
if (/^[\w-]+#\d+\/\[model\]/.test(text)) return
|
|
73
|
+
// D7 (AGENT-LOOP.md §7.2): strip ⟦ev⟧ event tokens (bare or prefixed variants) —
|
|
74
|
+
// they carry RS control characters and are a TUI display signal; structured ACP
|
|
75
|
+
// mapping (tool_call_update) is tracked separately in docs/TODO.md.
|
|
76
|
+
// eslint-disable-next-line no-control-regex -- 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
77
|
+
if (/^(?:[\w-]+#\d+\/)?⟦ev⟧(?:turn|approval)\x1e/.test(text)) return
|
|
73
78
|
update("agent_message_chunk", { content: { type: "text", text } })
|
|
74
79
|
},
|
|
75
80
|
onReasoning: (text) => update("agent_thought_chunk", { content: { type: "text", text } }),
|
package/src/agent/dispatch.mjs
CHANGED
|
@@ -27,7 +27,7 @@ function logToolError(toolName, args, error) {
|
|
|
27
27
|
const entry = [
|
|
28
28
|
`time: ${now.toISOString()}`,
|
|
29
29
|
`tool: ${toolName}`,
|
|
30
|
-
`args: ${JSON.stringify(args, null, 2)}`,
|
|
30
|
+
`args: ${JSON.stringify(args, null, 2).slice(0, 2000)}${JSON.stringify(args, null, 2).length > 2000 ? "… (truncated)" : ""}`,
|
|
31
31
|
`error: ${error?.message ?? String(error)}`,
|
|
32
32
|
error?.stack ? `stack:\n${error.stack}` : "",
|
|
33
33
|
].filter(Boolean).join("\n") + "\n"
|
|
@@ -111,7 +111,16 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
111
111
|
const allowed = agent.autoApprove
|
|
112
112
|
? true
|
|
113
113
|
: callbacks.onPermissionRequest
|
|
114
|
-
? await
|
|
114
|
+
? await (async () => {
|
|
115
|
+
// D2 (AGENT-LOOP.md §7.2): announce the wait BEFORE prompting — the TUI
|
|
116
|
+
// subagent block header flips to "等待审批" so a waiting child is visibly
|
|
117
|
+
// different from a stalled one. Depth>0 only (the parent TUI shows its own
|
|
118
|
+
// permission panel). turn n/max = the child's live turn counters.
|
|
119
|
+
if (depth > 0) {
|
|
120
|
+
callbacks.onToken?.(`⟦ev⟧approval\x1e${agent._currentTurn ?? 0}\x1e${agent._maxTurns ?? 0}\x1eapproval\x1e${String(toolCall.name).slice(0, 40)}`)
|
|
121
|
+
}
|
|
122
|
+
return await callbacks.onPermissionRequest(toolCall.name, args)
|
|
123
|
+
})()
|
|
115
124
|
: false
|
|
116
125
|
if (!allowed) {
|
|
117
126
|
prepared.push({ toolCall, tool, denied: true, reason: callbacks.onPermissionRequest ? "denied by user" : "no permission handler" })
|
|
@@ -127,7 +136,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
127
136
|
|
|
128
137
|
// Panel area abolished — all tools now stream inline via onToolOutput.
|
|
129
138
|
|
|
130
|
-
callbacks.onToolCall?.(toolCall.name, args)
|
|
139
|
+
callbacks.onToolCall?.(toolCall.name, args, toolCall.id)
|
|
131
140
|
|
|
132
141
|
prepared.push({ toolCall, tool, args })
|
|
133
142
|
}
|
|
@@ -157,7 +166,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
157
166
|
if (callbacks.toolRouter) {
|
|
158
167
|
const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
|
|
159
168
|
if (routed?.handled) {
|
|
160
|
-
callbacks.onToolResult?.(item.toolCall.name, routed.result)
|
|
169
|
+
callbacks.onToolResult?.(item.toolCall.name, routed.result, item.toolCall.id)
|
|
161
170
|
return { ...item, result: routed.result, ok: true }
|
|
162
171
|
}
|
|
163
172
|
}
|
|
@@ -167,14 +176,17 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
167
176
|
depth,
|
|
168
177
|
signal,
|
|
169
178
|
callbacks,
|
|
170
|
-
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk),
|
|
179
|
+
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
|
171
180
|
onQuestion: callbacks.onQuestion,
|
|
172
181
|
onPermissionRequest: callbacks.onPermissionRequest,
|
|
173
182
|
})
|
|
174
183
|
if (rawResult === undefined) throw new Error(`Tool "${item.toolCall.name}" returned undefined — all tools must return a string value`)
|
|
175
184
|
const raw = String(rawResult)
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
// Multimodal tools keep the raw result (base64 images ride the multimodal
|
|
186
|
+
// channel); everything else offloads oversized text to disk. Flag-driven, not
|
|
187
|
+
// name-driven (consult P3, 2026-08-30).
|
|
188
|
+
const result = item.tool?.multimodal ? raw : await offloadToolResult(raw, item.toolCall.id)
|
|
189
|
+
callbacks.onToolResult?.(item.toolCall.name, result, item.toolCall.id)
|
|
178
190
|
// PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
|
|
179
191
|
runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
|
|
180
192
|
return { ...item, result, ok: true }
|
package/src/agent/helpers.mjs
CHANGED
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
* agent/helpers.mjs — Agent utility functions and constants
|
|
3
3
|
*/
|
|
4
4
|
import { configDir } from "../config.mjs"
|
|
5
|
-
import { readFileSync, readdirSync
|
|
5
|
+
import { readFileSync, readdirSync } from "node:fs"
|
|
6
6
|
import { homedir } from "node:os"
|
|
7
7
|
import { writeFile, mkdir, readdir, stat, unlink } from "node:fs/promises"
|
|
8
8
|
import { join } from "node:path"
|
|
9
9
|
import { execSync } from "node:child_process"
|
|
10
10
|
|
|
11
|
+
/** Single source for the AUTO-mode reminder (was duplicated in agent.mjs +
|
|
12
|
+
* setup.mjs — consult P2, 2026-08-30). */
|
|
13
|
+
export const AUTO_REMINDER = "[System reminder: AUTO mode is active — all tool calls are automatically approved without asking.]"
|
|
14
|
+
|
|
15
|
+
/** Inject the AUTO reminder once per history (both call sites used the same
|
|
16
|
+
* literal + guard — consolidated here). */
|
|
17
|
+
export function ensureAutoReminder(agent) {
|
|
18
|
+
if (agent.autoApprove && !agent.history.some((m) => m.content === AUTO_REMINDER)) {
|
|
19
|
+
agent.history.push({ role: "user", content: AUTO_REMINDER })
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export const DEFAULT_MAX_TURNS = 200
|
|
12
24
|
export const DEFAULT_SUBAGENT_TURNS = 100
|
|
13
25
|
export const DEFAULT_GOAL_TURNS = 200
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* record-results.mjs — tool result commit + mutation accounting (split from
|
|
3
|
+
* agent.mjs 2026-08-30, consult P2: the 100-line loop was pure bookkeeping
|
|
4
|
+
* mixed into the run loop; it now lives behind one call).
|
|
5
|
+
*
|
|
6
|
+
* Contract (moved verbatim from the run loop):
|
|
7
|
+
* - Multimodal tool results ({ text, images }) close the tool pairing with a
|
|
8
|
+
* tool message, then inject the images as a multimodal user message — but
|
|
9
|
+
* NEVER between tool messages of parallel calls (strict providers 400 when
|
|
10
|
+
* a tool message does not immediately follow its assistant tool_calls), so
|
|
11
|
+
* image injections are DEFERRED until all results are committed.
|
|
12
|
+
* - Non-vision models get a text-only System-reminder instead of image parts
|
|
13
|
+
* (image parts 400 on every subsequent request — defense-in-depth).
|
|
14
|
+
* - FILE_MUTATORS invalidate advisor/verify state (code changed → prior
|
|
15
|
+
* review/verify is stale); non-mutating side-effect tools (bash/git)
|
|
16
|
+
* invalidate verify only (user decision 2026-08-08: reviews trigger on
|
|
17
|
+
* code mutations, not environment changes).
|
|
18
|
+
* - advisor calls always advance _advisorRound (convergence budget counts
|
|
19
|
+
* attempts, not successes).
|
|
20
|
+
* - Mutations feed _touchedFiles and fire-and-forget memory reindex.
|
|
21
|
+
*/
|
|
22
|
+
import { pushReal } from "../context.mjs"
|
|
23
|
+
import { specForModel } from "../config.mjs"
|
|
24
|
+
import { FILE_MUTATORS } from "./helpers.mjs"
|
|
25
|
+
import { join } from "node:path"
|
|
26
|
+
|
|
27
|
+
let _reindexFile = null
|
|
28
|
+
|
|
29
|
+
export async function recordToolResults(agent, toolByName, results) {
|
|
30
|
+
// Multimodal user messages (injected images / not-injected reminders) must NOT be pushed
|
|
31
|
+
// between tool results of parallel calls — strict providers (DeepSeek) 400 when a tool
|
|
32
|
+
// message does not immediately follow its assistant tool_calls. Defer to after the loop.
|
|
33
|
+
// real: image injections are real messages (pushReal → _fullHistory); reminders stay machine-only.
|
|
34
|
+
const deferredUserMsgs = []
|
|
35
|
+
|
|
36
|
+
for (const { toolCall, result, ok } of results) {
|
|
37
|
+
const tool = toolByName.get(toolCall.name)
|
|
38
|
+
// Multimodal tools return JSON { text, images } — inject as multimodal user message
|
|
39
|
+
if (tool?.multimodal && ok) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(result)
|
|
42
|
+
if (parsed.images?.length) {
|
|
43
|
+
// tool message first — closes the tool_call pairing (OpenAI API requires tool result immediately after assistant with tool_calls)
|
|
44
|
+
pushReal(agent, { role: "tool", tool_call_id: toolCall.id, name: toolCall.name, content: parsed.text })
|
|
45
|
+
if (specForModel(agent.provider.model).multimodal) {
|
|
46
|
+
// then inject multimodal user message with base64 images for the model to actually "see" them on the next turn
|
|
47
|
+
deferredUserMsgs.push({
|
|
48
|
+
real: true,
|
|
49
|
+
msg: {
|
|
50
|
+
role: "user",
|
|
51
|
+
content: [{ type: "text", text: parsed.text }, ...parsed.images],
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
// Non-vision model: image parts must never enter history — text-only APIs 400 on them on EVERY
|
|
56
|
+
// subsequent request, poisoning the conversation. (read_image itself already refuses; this is defense-in-depth.)
|
|
57
|
+
deferredUserMsgs.push({
|
|
58
|
+
real: false,
|
|
59
|
+
msg: {
|
|
60
|
+
role: "user",
|
|
61
|
+
content: `[System reminder: the image returned by ${toolCall.name} was NOT injected — model ${agent.provider.model} does not support image input. Do not call ${toolCall.name} again under this provider; verify visual output programmatically instead.]`,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
} catch { /* Parse failure doesn't affect normal tool messages */ }
|
|
68
|
+
}
|
|
69
|
+
pushReal(agent, { role: "tool", tool_call_id: toolCall.id, name: toolCall.name, content: result })
|
|
70
|
+
if (tool && ok) {
|
|
71
|
+
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
72
|
+
// Direct file edit — code was changed. The prior advisor review and
|
|
73
|
+
// verify are stale: a review that ran before the edit no longer
|
|
74
|
+
// covers the current file state.
|
|
75
|
+
agent._mutatedThisRun = true
|
|
76
|
+
agent._calledAdvisorThisRun = false
|
|
77
|
+
agent._verifiedThisRun = false
|
|
78
|
+
agent._verifyPassed = undefined
|
|
79
|
+
} else if (!tool.readonly && !tool.sideEffectExempt) {
|
|
80
|
+
// Non-mutating side-effect tools (bash, git): do NOT invalidate the
|
|
81
|
+
// advisor review — a review is triggered by CODE MUTATIONS only
|
|
82
|
+
// (user decision 2026-08-08: the guard rule is "review after code
|
|
83
|
+
// changes", not "review after any environment change"; bash is
|
|
84
|
+
// barred from writing files, so it cannot change the reviewed code).
|
|
85
|
+
// Verify IS invalidated: its state snapshot (git diff, file list)
|
|
86
|
+
// may be stale after git/shell operations.
|
|
87
|
+
if (agent._verifiedThisRun) {
|
|
88
|
+
agent._verifiedThisRun = false
|
|
89
|
+
agent._verifyPassed = undefined
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (toolCall.name === "verify") agent._verifiedThisRun = true
|
|
93
|
+
if (toolCall.name === "advisor") {
|
|
94
|
+
agent._calledAdvisorThisRun = true
|
|
95
|
+
// All advisor calls (code and design) share the 5-round convergence
|
|
96
|
+
// budget — each advances _advisorRound toward MAX_ADVISOR_ROUNDS.
|
|
97
|
+
// Always advance the round — the convergence protocol cares about
|
|
98
|
+
// how many reviews have run (round 1→2→3→4→5), not how many succeeded.
|
|
99
|
+
// A failed/interrupted review is still a review attempt and should use
|
|
100
|
+
// the next round's prompt on retry.
|
|
101
|
+
agent._advisorRound++
|
|
102
|
+
}
|
|
103
|
+
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
104
|
+
const args = JSON.parse(toolCall.arguments)
|
|
105
|
+
const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
|
|
106
|
+
for (const p of paths) {
|
|
107
|
+
const abs = join(agent.cwd, p)
|
|
108
|
+
if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
|
|
109
|
+
if (agent.memory) {
|
|
110
|
+
// Fire-and-forget: don't block the agent loop on indexing.
|
|
111
|
+
// Reuses a single cached import; errors surface as pending reminders on next turn.
|
|
112
|
+
if (!_reindexFile) {
|
|
113
|
+
const mod = await import("../memory.mjs")
|
|
114
|
+
_reindexFile = mod.reindexFile
|
|
115
|
+
}
|
|
116
|
+
_reindexFile(agent.memory, agent.cwd, abs).catch((e) => {
|
|
117
|
+
agent._pendingReminders.push(`[System reminder: background indexing failed for ${toolCall.name} on ${abs}: ${e.message}. This does not affect your work — the code index will catch up on next reindex.]`)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// All tool results committed — now safe to inject deferred multimodal user messages
|
|
126
|
+
for (const { real, msg } of deferredUserMsgs) {
|
|
127
|
+
if (real) pushReal(agent, msg)
|
|
128
|
+
else agent.history.push(msg)
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/agent/setup.mjs
CHANGED
|
@@ -11,15 +11,14 @@ import { pushReal } from "../context.mjs"
|
|
|
11
11
|
import { toOpenAISchema } from "../tools/index.mjs"
|
|
12
12
|
import { loadSkills, formatSkillListing } from "../skills.mjs"
|
|
13
13
|
import {
|
|
14
|
-
escapeXml, repairHistory, listWorkDir,
|
|
14
|
+
escapeXml, repairHistory, listWorkDir,
|
|
15
15
|
collectGitContext, loadProjectInstructions, OUTLINE_INJECT_PREFIX,
|
|
16
|
-
DEFAULT_MAX_TURNS,
|
|
16
|
+
DEFAULT_MAX_TURNS, ensureAutoReminder,
|
|
17
17
|
} from "./helpers.mjs"
|
|
18
18
|
import { readFileSync, existsSync } from "node:fs"
|
|
19
19
|
import { resolve, dirname } from "node:path"
|
|
20
20
|
import { fileURLToPath } from "node:url"
|
|
21
21
|
|
|
22
|
-
const AUTO_REMINDER = "[System reminder: AUTO mode is active — all tool calls are automatically approved without asking.]"
|
|
23
22
|
const DEFAULT_COMPACT_THRESHOLD = 100_000
|
|
24
23
|
const DOC_SEARCH_LIMIT = 5
|
|
25
24
|
const DOC_CHUNK_PREVIEW_LEN = 300
|
|
@@ -154,7 +153,7 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
154
153
|
// Tail position keeps the second-precision content out of any prefix — caches stay hit.
|
|
155
154
|
agent.history.push({
|
|
156
155
|
role: "user",
|
|
157
|
-
content: `[System reminder: current time is ${timeNowLocal()} (local; timezone ${Intl.DateTimeFormat().resolvedOptions().timeZone || "local"}).`,
|
|
156
|
+
content: `[System reminder: current time is ${timeNowLocal()} (local; timezone ${Intl.DateTimeFormat().resolvedOptions().timeZone || "local"})].`,
|
|
158
157
|
transient: true,
|
|
159
158
|
})
|
|
160
159
|
|
|
@@ -285,9 +284,7 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
285
284
|
if (listing) systemPrompt += `\n\n${listing}`
|
|
286
285
|
}
|
|
287
286
|
|
|
288
|
-
|
|
289
|
-
agent.history.push({ role: "user", content: AUTO_REMINDER })
|
|
290
|
-
}
|
|
287
|
+
ensureAutoReminder(agent)
|
|
291
288
|
|
|
292
289
|
return { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt }
|
|
293
290
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawn-child.mjs — 生成侧统一子代理管线(AGENT-LOOP.md §7.2 D3)。
|
|
3
|
+
*
|
|
4
|
+
* 收编 subagent/escalate/consult 三份机械同构的重复实现:
|
|
5
|
+
* - makeRelay: _subAgentCounter + relayPrefix 生成 + `[model]` 元数据 token 发送
|
|
6
|
+
* - wrapChildCallbacks: onToken/onReasoning/onToolCall/onToolOutput 前缀包装
|
|
7
|
+
* (onToolOutput 复用 dispatch.mjs 的 onOutput 管线,D1;
|
|
8
|
+
* LLM 文本 token strip ⟦ev⟧ 哨兵——防伪造,D7)
|
|
9
|
+
* - runWithContinue: turn-cap ContinueError 循环骨架(askContinue 差异点经参数注入)
|
|
10
|
+
* - ensureChildApiKey: provider.apiKey trim/检查(apiKey 检查 ×3 合并)
|
|
11
|
+
* - clampEffort: effort 枚举钳制(enum 外丢弃——行为与 escalate/consult 现状逐字一致)
|
|
12
|
+
*
|
|
13
|
+
* 不收编(防过度抽象,留在各工具内):角色过滤/overlay/git 注入、subagent 专属
|
|
14
|
+
* (报告契约 MIN_REPORT_CHARS、mergeChildMutations)、escalate 专属(touched-files、
|
|
15
|
+
* effortNote)、consult 专属(session 队列、watchdog、main_history 工具)。
|
|
16
|
+
*/
|
|
17
|
+
import { specForModel } from "../config.mjs"
|
|
18
|
+
import { ContinueError } from "../agent.mjs"
|
|
19
|
+
|
|
20
|
+
/** 事件 token 哨兵串(D1)——LLM 正常内容混淆概率极低;字段分隔用 RS (\x1e)。 */
|
|
21
|
+
export const EVENT_SENTINEL = "⟦ev⟧"
|
|
22
|
+
const RS = "\x1e"
|
|
23
|
+
|
|
24
|
+
/** turn-cap 降级文案的公共锚点:subagent/escalate 的 onDeclined 文案必含此子串,
|
|
25
|
+
* 消费端(TUI tool-events onToolResult)用 includes() 检测"工作可能不完整"语义。
|
|
26
|
+
* 单源化(2026-08-30 评审):文案演进只改这里,消除文案与检测正则的漂移面。 */
|
|
27
|
+
export const TURN_CAP_MARK = "stopped: turn cap reached"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 构造 relay 前缀 + 发送 `[model]` 元数据 token(显示层据此更新区块头部,
|
|
31
|
+
* 不进内容流)。counter 挂在 parent agent 上,多轮/并行子代理互不冲突。
|
|
32
|
+
* @returns {string} relayPrefix,形如 "coder#3/"
|
|
33
|
+
*/
|
|
34
|
+
export function makeRelay(parent, label, emit, model) {
|
|
35
|
+
parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
|
|
36
|
+
const relayPrefix = `${label}#${parent._subAgentCounter}/`
|
|
37
|
+
emit?.(relayPrefix + "[model]" + (model ?? ""))
|
|
38
|
+
return relayPrefix
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* strip ⟦ev⟧ 哨兵序列:子 agent 的 LLM 文本 token 若以事件哨兵开头,剥除之
|
|
43
|
+
* (模型伪造事件 token 的风险面收窄到"仅生成侧发出口",D7)。
|
|
44
|
+
* **良构事件放行**(事件与内容共用 onToken 通道的必然结果):真正由 runAgent
|
|
45
|
+
* depth>0 / dispatch 发出的 turn/approval 事件 token(形如
|
|
46
|
+
* `⟦ev⟧turn\x1e{n}\x1e{max}\x1e{phase}\x1e{detail}`)必须原样通过——只有非良构的
|
|
47
|
+
* `⟦ev⟧…`(模型伪造)才剥为普通文本。
|
|
48
|
+
* 已知限制(round2 #7):单 chunk 匹配,哨兵串切在 chunk 边界(⟦e + v⟧)漏剥——
|
|
49
|
+
* 残危害由消费端 sanitizeDisplay 兜底,外观级;不引入跨 chunk carry-over 缓冲。
|
|
50
|
+
*/
|
|
51
|
+
// Single source for the event grammar branch lists (consult P3, 2026-08-30):
|
|
52
|
+
// stripEventToken (display) and stripEventTokensForCapture (capture) shared them
|
|
53
|
+
// literally — extending the event set meant touching both regexes.
|
|
54
|
+
const EVENT_PHASE = "turn|approval"
|
|
55
|
+
const EVENT_TYPE = "llm|tool|approval|done"
|
|
56
|
+
const WELL_FORMED_EVENT = new RegExp(`^${EVENT_SENTINEL}(${EVENT_PHASE})${RS}[^${RS}]*${RS}[^${RS}]*${RS}(${EVENT_TYPE})${RS}`)
|
|
57
|
+
export function stripEventToken(text) {
|
|
58
|
+
if (!text.startsWith(EVENT_SENTINEL)) return text
|
|
59
|
+
if (WELL_FORMED_EVENT.test(text)) return text // 真事件(生成侧发出),放行
|
|
60
|
+
return text.slice(EVENT_SENTINEL.length) // 模型伪造的哨兵串 → 剥为普通文本
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Review #4: 事件 token 整体剥除——用于捕获进父 LLM 历史的 output(partial
|
|
65
|
+
* 输出/空报告 fallback)。与 stripEventToken(显示路径,放行真事件供头部消费)
|
|
66
|
+
* 不同:partial 输出不需要事件语义,RS 控制字符对父 LLM 是纯噪音。
|
|
67
|
+
* detail 字段可选(turn 事件 detail 为空且带收尾 RS;approval 事件 detail 为
|
|
68
|
+
* 工具名、无收尾 RS)——round2 复核缺口修复:此前正则要求收尾 RS,approval 的
|
|
69
|
+
* detail 会作为裸工具名残留在 partial 输出里。
|
|
70
|
+
*/
|
|
71
|
+
export function stripEventTokensForCapture(text) {
|
|
72
|
+
if (!String(text).includes(EVENT_SENTINEL)) return text
|
|
73
|
+
return String(text).replace(new RegExp(`${EVENT_SENTINEL}(${EVENT_PHASE})${RS}[^${RS}]*${RS}[^${RS}]*${RS}(${EVENT_TYPE})(?:${RS}[^${RS}]*)?`, "g"), "")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 子 agent 回调前缀包装:`role#id/` 前缀转发给父回调。
|
|
78
|
+
* - onToken 先剥伪造的 ⟦ev⟧ 哨兵(D7),再带前缀转发。
|
|
79
|
+
* - onToolOutput 带**已加前缀**的 name 走父 onToolOutput(name 形如
|
|
80
|
+
* "coder#1/bash",消费端剥前缀路由进对应区块;chunk 对象/裸串原样透传)。
|
|
81
|
+
* 父回调缺省时不包装(headless 嵌入)。
|
|
82
|
+
*/
|
|
83
|
+
export function wrapChildCallbacks(relayPrefix, parentCallbacks = {}) {
|
|
84
|
+
const wrapped = {
|
|
85
|
+
onToken: parentCallbacks.onToken
|
|
86
|
+
? (t) => parentCallbacks.onToken(relayPrefix + stripEventToken(String(t)))
|
|
87
|
+
: null,
|
|
88
|
+
onReasoning: parentCallbacks.onReasoning
|
|
89
|
+
? (t) => parentCallbacks.onReasoning(relayPrefix + t)
|
|
90
|
+
: null,
|
|
91
|
+
onToolCall: parentCallbacks.onToolCall
|
|
92
|
+
? (name, args) => parentCallbacks.onToolCall(relayPrefix + name, args)
|
|
93
|
+
: null,
|
|
94
|
+
onToolOutput: parentCallbacks.onToolOutput
|
|
95
|
+
? (name, chunk) => parentCallbacks.onToolOutput(relayPrefix + name, chunk)
|
|
96
|
+
: null,
|
|
97
|
+
}
|
|
98
|
+
return wrapped
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 子 agent provider API key 检查:trim 后非空才保留;缺失返回 null(调用方
|
|
103
|
+
* 按各自业务语汇报错——subagent 抛出 / escalate·consult 返回 Error 文本)。
|
|
104
|
+
*/
|
|
105
|
+
export function ensureChildApiKey(provider) {
|
|
106
|
+
provider.apiKey = provider.apiKey?.trim() || null
|
|
107
|
+
return provider.apiKey ? provider : null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* effort 枚举钳制:pool 配置的 effort 超出模型的 reasoningEffortEnum 时
|
|
112
|
+
* 丢弃(provider/core.mjs 会在每次 chat 抛错——候选未起飞先坠机)。enum 外
|
|
113
|
+
* 丢弃即"provider 预设默认也可能是该 override 模型的 enum 外值"的同款防御。
|
|
114
|
+
* @returns {boolean} true = effort 保留;false = effort 被 clamp 丢弃(调用方决定是否拼 effortNote)
|
|
115
|
+
*/
|
|
116
|
+
export function clampEffort(provider, model, effort) {
|
|
117
|
+
if (!effort) return true
|
|
118
|
+
const enumList = specForModel(model).reasoningEffortEnum
|
|
119
|
+
if (enumList && !enumList.includes(effort)) {
|
|
120
|
+
delete provider.reasoningEffort
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
provider.reasoningEffort = effort
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* turn-cap continue 循环骨架(TURN-CAP-CONTINUE.md):ContinueError → 询问
|
|
129
|
+
* 继续(askContinue 回调,差异点注入)→ resume:true 重跑(保留 history 与
|
|
130
|
+
* mutation 记账、刷新 turn 预算);拒绝/headless → 按 onDeclined 降级返回。
|
|
131
|
+
* 非 ContinueError 错误原样抛出(dispatch.mjs 转 Error 工具结果——行为不变)。
|
|
132
|
+
*
|
|
133
|
+
* @param {Function} runner — async (child, input, callbacks, opts) => result
|
|
134
|
+
* @param {object} child — createAgent 返回的子 agent 对象
|
|
135
|
+
* @param {string} input — 任务文本
|
|
136
|
+
* @param {object} callbacks — 子 agent callbacks(含 onPermissionRequest 等)
|
|
137
|
+
* @param {object} runOpts — { depth, maxTurns, signal }(resume 由本函数管理)
|
|
138
|
+
* @param {object} hooks — { askContinue(err) => Promise<boolean>(必需),
|
|
139
|
+
* onDeclined(err, output) => string(拒绝降级)}
|
|
140
|
+
*/
|
|
141
|
+
export async function runWithContinue(runner, child, input, callbacks, runOpts, { askContinue, onDeclined }) {
|
|
142
|
+
let output = ""
|
|
143
|
+
// Review #4 fix: strip sentinel/control chars from the capture — `output` feeds
|
|
144
|
+
// onDeclined's partial-output return, which lands in the PARENT LLM history where
|
|
145
|
+
// the display-layer sanitizeDisplay backstop does not apply.
|
|
146
|
+
const capture = callbacks?.onToken
|
|
147
|
+
? (t) => { output += stripEventTokensForCapture(String(t)); child._capturedOutput = output; callbacks.onToken(t) }
|
|
148
|
+
: (t) => { output += stripEventTokensForCapture(String(t)); child._capturedOutput = output }
|
|
149
|
+
for (let resume = false; ; resume = true) {
|
|
150
|
+
try {
|
|
151
|
+
return await runner(child, input, { ...callbacks, onToken: capture }, { ...runOpts, resume })
|
|
152
|
+
} catch (e) {
|
|
153
|
+
if (!(e instanceof ContinueError)) throw e
|
|
154
|
+
const go = await askContinue(e)
|
|
155
|
+
if (go) continue
|
|
156
|
+
return onDeclined(e, output)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|