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.
- package/CHANGELOG.md +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/agent/dispatch.mjs
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent/dispatch.mjs — two-phase tool call execution
|
|
3
3
|
*/
|
|
4
|
+
import { logEvent, errText, headText } from "../log.mjs"
|
|
4
5
|
import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
|
|
5
6
|
import { runHooks } from "../hooks.mjs"
|
|
6
7
|
import { snapshotForUndo } from "../tui/cmd-undo.mjs"
|
|
7
8
|
import { isDocFile } from "../advisor/repos.mjs"
|
|
9
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):写工具钩子——peerCollabNote(执行前冲突
|
|
10
|
+
// 检测——软提示不阻止)+ recordPeerWrites(成功后累积本回合写足迹——回合末 flush)。
|
|
11
|
+
import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites } from "../peer-domains.mjs"
|
|
8
12
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs"
|
|
9
|
-
import { join } from "node:path"
|
|
13
|
+
import { join, resolve } from "node:path"
|
|
10
14
|
import { homedir } from "node:os"
|
|
15
|
+
// §29 fix A(AGENT-LOOP.md §29——2026-09-07):FILE_MUTATORS 的 mutation-seq 记账从
|
|
16
|
+
// 批后提交(record-results noteMutations)移到执行成功即刻——唯一记账点(取代批后段
|
|
17
|
+
// + agent.mjs 中断分支记账——不双计)——同消息 [写 + async advisor launch] 时 launch 前
|
|
18
|
+
// 完成的写在 launchSeq 之前落地 → settle 不再误判 stale(§29 症状根因)。
|
|
19
|
+
import { noteMutations } from "../agent-tools/advisor-async.mjs"
|
|
20
|
+
import { anyLiveDesignSlot } from "../token-ttl.mjs"
|
|
11
21
|
|
|
12
22
|
const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
|
|
13
23
|
|
|
@@ -37,6 +47,88 @@ function logToolError(toolName, args, error) {
|
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
/**
|
|
51
|
+
* §19 action-level classification (AGENT-LOOP.md §19 D-M1): the merged subagent
|
|
52
|
+
* tool expresses spawn (side effect) and status (read-only query) through
|
|
53
|
+
* its `action` parameter — the tool-level readonly flag can no longer express both.
|
|
54
|
+
* dispatch Phase-1/Phase-2 classifies per action: status behaves as readonly
|
|
55
|
+
* (planMode pass / no permission ask / batchable), spawn keeps its non-readonly
|
|
56
|
+
* gates, escalate runs non-readonly AND serially (the retired escalate tool had no
|
|
57
|
+
* parallel flag — zero behavior change under the merged surface).
|
|
58
|
+
* §19.5 cancel (19.5.2b round2 #4): CONTROL-class exemption — cancel only
|
|
59
|
+
* stops, never starts. isSubagentControlAction feeds the SAME two gate sites as
|
|
60
|
+
* readonly (planMode pass / no permission ask — never joins a batch approval
|
|
61
|
+
* group / no handler → not denied — digest 内 cancel 放行).
|
|
62
|
+
* §19.6 panel (round1 #5): view 面归只读类(同 status——planMode 放行、免
|
|
63
|
+
* 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
|
|
64
|
+
* 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
|
|
65
|
+
*/
|
|
66
|
+
function isSubagentReadonlyAction(toolName, args) {
|
|
67
|
+
// §6 memory 工具面重构(MEMORY.md §6 D-M5):memory search/list 是只读动作——与
|
|
68
|
+
// subagent status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
|
|
69
|
+
// readonly/parallel,memory 无 parallel → 按非只读串行,见 MEMORY.md §6.4 实现注)。
|
|
70
|
+
// 动作级判定——不能按工具名(同一 memory 工具的 put/delete/clear 保持侧效门)。
|
|
71
|
+
if (toolName === "memory") {
|
|
72
|
+
const action = args?.action
|
|
73
|
+
return action === "search" || action === "list"
|
|
74
|
+
}
|
|
75
|
+
// SETTINGS-TOOL.md(2026-09-05):settings list/get 是只读动作(memory search/list 同分类——
|
|
76
|
+
// planMode 放行/免审批);set 保持侧效门。
|
|
77
|
+
if (toolName === "settings") {
|
|
78
|
+
const action = args?.action
|
|
79
|
+
return action === "list" || action === "get"
|
|
80
|
+
}
|
|
81
|
+
if (toolName !== "subagent" || !args || typeof args !== "object") return false
|
|
82
|
+
const action = args.action
|
|
83
|
+
// §19.8: check 动作已删除——只读面仅剩 status(planMode 放行/免权限审批/可批并行)
|
|
84
|
+
if (action === "status") return true
|
|
85
|
+
// SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行)
|
|
86
|
+
if (action === "observe") return true
|
|
87
|
+
// §19.6 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
|
|
88
|
+
if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
function isSubagentControlAction(toolName, args) {
|
|
92
|
+
if (toolName !== "subagent") return false
|
|
93
|
+
if (args?.action === "cancel") return true
|
|
94
|
+
// SUBAGENT-OBSERVE-SEND:send = 控制类豁免(同 cancel——父回合内显式调用即授权——
|
|
95
|
+
// 写子输入队列属父对子轻量引导,非产品代码写——免审批、planMode 放行、digest 内放行)
|
|
96
|
+
if (args?.action === "send") return true
|
|
97
|
+
// §19.6 panel freeze 面(D-P3 门控在 executor——只读/控制分类在此)
|
|
98
|
+
if (args?.action === "panel" && args.freeze !== undefined && args.freeze !== null && String(args.freeze) !== "") return true
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* §2.6 token 链终消费制(2026-09-07——评审 #7d dispatch 分类):consume-design =
|
|
103
|
+
* 非只读控制动作——planMode 拒绝(不入 readonly/control 豁免——与其他非只读动作同门)、
|
|
104
|
+
* 免权限审批、不入批审批分组(无文件写——控制类直行——只停既有状态不起新副作用)。
|
|
105
|
+
* 与 cancel 的不同:cancel 是控制类豁免(planMode 放行),consume-design 按设计
|
|
106
|
+
* planMode 拒绝——故不并入 isSubagentControlAction,单独谓词只接权限豁免位。
|
|
107
|
+
*/
|
|
108
|
+
function isSubagentConsumeDesignAction(toolName, args) {
|
|
109
|
+
return toolName === "subagent" && args?.action === "consume-design"
|
|
110
|
+
}
|
|
111
|
+
function isSubagentEscalateAction(toolName, args) {
|
|
112
|
+
return toolName === "subagent" && args?.action === "escalate"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* §29 fix A — 唯一记账点:FILE_MUTATORS 工具执行成功即刻记 mutation seq(abs 路径)。
|
|
117
|
+
* 取代 record-results 批后段 + agent.mjs 中断分支的 noteMutations(不双计——中断+同批
|
|
118
|
+
* launch 场景 seq 单计,AGENT-LOOP.md §29 T-A1i)。调用时机 = 写执行成功(非 Error 前缀
|
|
119
|
+
* 结果——recordPeerWrites 同款门);routed(M2 ACP 客户端执行)成功同样记账。
|
|
120
|
+
*/
|
|
121
|
+
function noteExecutedMutation(agent, tool, args) {
|
|
122
|
+
let paths
|
|
123
|
+
try {
|
|
124
|
+
paths = tool.touchedPaths ? tool.touchedPaths(args ?? {}) : [args?.path]
|
|
125
|
+
} catch { return }
|
|
126
|
+
const abs = (paths ?? [])
|
|
127
|
+
.filter((p) => typeof p === "string" && p)
|
|
128
|
+
.map((p) => resolve(agent.cwd, p))
|
|
129
|
+
if (abs.length > 0) noteMutations(agent, abs)
|
|
130
|
+
}
|
|
131
|
+
|
|
40
132
|
/**
|
|
41
133
|
* Two-phase execution:
|
|
42
134
|
* Phase 1 (serial): parse args one by one + planMode check + permission confirmation (side-effecting tools)
|
|
@@ -69,7 +161,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
69
161
|
continue
|
|
70
162
|
}
|
|
71
163
|
|
|
72
|
-
if (agent.planMode && !tool.readonly) {
|
|
164
|
+
if (agent.planMode && !tool.readonly && !isSubagentReadonlyAction(toolCall.name, args) && !isSubagentControlAction(toolCall.name, args)) {
|
|
73
165
|
prepared.push({ toolCall, tool, denied: true, reason: "plan mode" })
|
|
74
166
|
continue
|
|
75
167
|
}
|
|
@@ -88,14 +180,18 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
88
180
|
}
|
|
89
181
|
|
|
90
182
|
// Engineering mode PARENT gate: the parent agent must not touch code files
|
|
91
|
-
// before the design review passed. Signaled by
|
|
92
|
-
//
|
|
93
|
-
// eng-coder-only and reset per run). Exemptions cover ONLY design artifacts
|
|
183
|
+
// before the design review passed. Signaled by a live design slot (design-review
|
|
184
|
+
// approval — persists in the session slot, survives across turns; _engDesignReviewed
|
|
185
|
+
// is eng-coder-only and reset per run). Exemptions cover ONLY design artifacts
|
|
94
186
|
// (docs/** and root-level docs like METHODOLOGY.md/README.md/AGENTS.md/
|
|
95
187
|
// LICENSE) — writing them IS the design/methodology step. Everything under
|
|
96
188
|
// src/ (incl. src/prompts/*.md) is product code, not documentation, and
|
|
97
189
|
// needs a design token. Mechanically blocks "talk then code".
|
|
98
|
-
|
|
190
|
+
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):资格判据 = 权威槽"任一活槽存在"
|
|
191
|
+
// (anyLiveDesignSlot——查内存 Map,miss 回读槽文件——单值镜像 `_engDesignToken`
|
|
192
|
+
// 已退役,门禁不再读镜像——AC4)。
|
|
193
|
+
if (agent.config?.agent?.engineering && depth === 0
|
|
194
|
+
&& !anyLiveDesignSlot(agent)
|
|
99
195
|
&& FILE_MUTATORS.has(toolCall.name)) {
|
|
100
196
|
const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
|
|
101
197
|
// Unknown/missing paths (non-string, e.g. no path argument) are treated
|
|
@@ -113,7 +209,15 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
113
209
|
|
|
114
210
|
// Readonly tools (and autoApprove — the short-circuit, unchanged for the
|
|
115
211
|
// whole batch too) skip the permission stage entirely.
|
|
116
|
-
|
|
212
|
+
// §18 D-E3 task-domain authorization (spawn-time): an eng-coder child's
|
|
213
|
+
// tools skip the permission ASK stage exactly like autoApprove — granted by
|
|
214
|
+
// the parent spawn (approved design + task = authorization; subagent.mjs
|
|
215
|
+
// sets _engTaskAuthorized on the child). Everything EARLIER in Phase 1
|
|
216
|
+
// (JSON parse / unknown tool / planMode / design-token gates) ran unchanged
|
|
217
|
+
// — the exemption never widens what reaches this stage (round4 #3, T-E14).
|
|
218
|
+
// PreToolUse hooks still run below. Non-eng-coder children keep the manual
|
|
219
|
+
// parent ask (human in the loop).
|
|
220
|
+
if (tool.readonly || isSubagentReadonlyAction(toolCall.name, args) || isSubagentControlAction(toolCall.name, args) || isSubagentConsumeDesignAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
|
|
117
221
|
if (!(await runHooks("PreToolUse", { agent, toolName: toolCall.name, toolArgs: args }))) {
|
|
118
222
|
prepared.push({ toolCall, tool, denied: true, reason: "blocked by PreToolUse hook" })
|
|
119
223
|
continue
|
|
@@ -203,7 +307,17 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
203
307
|
// 捕获分离(父恢复原始后子的拦截期间父捕获停止、子恢复后父继续)——正确。
|
|
204
308
|
// 声明在 try 之外:catch 块(异常路径)也要访问(报错前的探查输出回显)。
|
|
205
309
|
const capturedConsole = []
|
|
310
|
+
// LOGGING(LOGGING.md):tool:* 事件——仅真实执行(pre-gate 拦截项在下方早退分支不入事件)。
|
|
311
|
+
// 参数值永不落盘(NF-L3——工具事件不记 args);child=子代理 id(agent._logId,spawn 时 stamp)。
|
|
312
|
+
const toolT0 = Date.now()
|
|
313
|
+
const toolName = item.toolCall.name
|
|
314
|
+
logEvent("tool:call", { tool: toolName, child: agent?._logId })
|
|
206
315
|
try {
|
|
316
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):结构化写工具执行前查 conflicts
|
|
317
|
+
// (命中他实例 hot 域 → 结果附软提示——决策⑥ A 不阻止;一次目录 stat——N3 度量);
|
|
318
|
+
// 足迹累积(D-L3a——"检测+记录一次完成")延后到执行成功(实际写过的文件)。
|
|
319
|
+
const isPeerWriteTool = PEER_WRITE_TOOLS.has(toolName)
|
|
320
|
+
const peerNote = isPeerWriteTool ? peerCollabNote(agent.cwd, item.tool, item.args) : null
|
|
207
321
|
// Snapshot for undo before side-effect tools (setupOutputPanel already fired in Phase 1)
|
|
208
322
|
if (!item.tool?.readonly && item.args) {
|
|
209
323
|
snapshotForUndo(agent, item.toolCall.name, item.args, agent.cwd)
|
|
@@ -213,8 +327,14 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
213
327
|
if (callbacks.toolRouter) {
|
|
214
328
|
const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
|
|
215
329
|
if (routed?.handled) {
|
|
216
|
-
|
|
217
|
-
|
|
330
|
+
const routedOk = !String(routed.result).startsWith("Error:")
|
|
331
|
+
const routedResult = peerNote && routedOk ? `${routed.result}\n${peerNote}` : routed.result
|
|
332
|
+
if (isPeerWriteTool && routedOk) recordPeerWrites(agent, item.tool, item.args)
|
|
333
|
+
// §29 fix A:routed 写成功(客户端执行)同样执行期即刻记账(唯一记账点)
|
|
334
|
+
if (routedOk && FILE_MUTATORS.has(toolName)) noteExecutedMutation(agent, item.tool, item.args)
|
|
335
|
+
callbacks.onToolResult?.(item.toolCall.name, routedResult, item.toolCall.id)
|
|
336
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(routedResult, 200), child: agent?._logId })
|
|
337
|
+
return { ...item, result: routedResult, ok: true }
|
|
218
338
|
}
|
|
219
339
|
}
|
|
220
340
|
const origConsoleLog = console.log
|
|
@@ -222,23 +342,51 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
222
342
|
console.log = (...a) => capturedConsole.push(a.map(String).join(" "))
|
|
223
343
|
console.error = (...a) => capturedConsole.push("[err] " + a.map(String).join(" "))
|
|
224
344
|
let rawResult
|
|
345
|
+
// ctx 对象提升为变量(§7.2.3):subagent 阻塞 execute 返回前在 ctx 上留
|
|
346
|
+
// _subagentKey(relayPrefix 去尾)——runOne 在 execute 返回后读它作 onToolResult
|
|
347
|
+
// 第 4 参(普通工具/错误路径无此字段——undefined 兼容既有签名)。每次工具调用
|
|
348
|
+
// 独立 ctx——并行同名工具(批并行 runOne)各自带自己的 key,互不串扰。
|
|
349
|
+
const toolCtx = {
|
|
350
|
+
cwd: agent.cwd,
|
|
351
|
+
agent,
|
|
352
|
+
depth,
|
|
353
|
+
signal,
|
|
354
|
+
callbacks,
|
|
355
|
+
// §24 D-24b: per-call id — the advisor tool marker-keys its launch so
|
|
356
|
+
// recordToolResults can split async-ack accounting from sync settles.
|
|
357
|
+
_toolCallId: item.toolCall.id,
|
|
358
|
+
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
|
359
|
+
onQuestion: callbacks.onQuestion,
|
|
360
|
+
onPermissionRequest: callbacks.onPermissionRequest,
|
|
361
|
+
}
|
|
225
362
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
363
|
+
// SUBAGENT-OBSERVE-SEND D1(评审 #1):in-flight 当前工具记账——工具执行期间在
|
|
364
|
+
// agent 上留 _inflightTools Set(子代理 observe 从 dispatch 状态读——卡在长工具
|
|
365
|
+
// 调用时 history 无新回合、恰需此信号);finally 清除。批并行工具同入 Set(observe
|
|
366
|
+
// 如实返回多个在跑工具)。主会话同样记账——无害(无人读)。
|
|
367
|
+
const inflight = agent._inflightTools ?? (agent._inflightTools = new Set())
|
|
368
|
+
inflight.add(toolName)
|
|
369
|
+
try {
|
|
370
|
+
rawResult = await item.tool.execute(item.args, toolCtx)
|
|
371
|
+
} finally {
|
|
372
|
+
inflight.delete(toolName)
|
|
373
|
+
}
|
|
236
374
|
} finally {
|
|
237
375
|
console.log = origConsoleLog
|
|
238
376
|
console.error = origConsoleErr
|
|
239
377
|
}
|
|
240
378
|
if (rawResult === undefined) throw new Error(`Tool "${item.toolCall.name}" returned undefined — all tools must return a string value`)
|
|
241
379
|
const raw = String(rawResult)
|
|
380
|
+
// 写成功(非 "Error:" 字符串结果)→ 足迹计入本回合集合(flush 在 finalizeAgentTurn)
|
|
381
|
+
if (isPeerWriteTool && !raw.startsWith("Error:")) {
|
|
382
|
+
recordPeerWrites(agent, item.tool, item.args)
|
|
383
|
+
}
|
|
384
|
+
// §29 fix A:FILE_MUTATORS 执行成功即刻记账(唯一记账点——取代 record-results 批后
|
|
385
|
+
// 段 + agent.mjs 中断分支——不双计)——同批 launch 前的写在 launchSeq 之前落地 →
|
|
386
|
+
// async advisor settle 不误判 stale(同批 launch 后写仍保守 stale——T-A2/T-24b9)。
|
|
387
|
+
if (FILE_MUTATORS.has(toolName) && !raw.startsWith("Error:")) {
|
|
388
|
+
noteExecutedMutation(agent, item.tool, item.args)
|
|
389
|
+
}
|
|
242
390
|
// Multimodal tools keep the raw result (base64 images ride the multimodal
|
|
243
391
|
// channel); everything else offloads oversized text to disk. Flag-driven, not
|
|
244
392
|
// name-driven (consult P3, 2026-08-30).
|
|
@@ -247,10 +395,15 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
247
395
|
const resultWithConsole = capturedConsole.length > 0
|
|
248
396
|
? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
249
397
|
: result
|
|
250
|
-
|
|
398
|
+
// R10 L3:冲突软提示附在工具结果末尾(模型可见——不阻止写)
|
|
399
|
+
const resultForModel = peerNote && !raw.startsWith("Error:")
|
|
400
|
+
? `${resultWithConsole}\n${peerNote}`
|
|
401
|
+
: resultWithConsole
|
|
402
|
+
callbacks.onToolResult?.(item.toolCall.name, resultForModel, item.toolCall.id, toolCtx._subagentKey)
|
|
251
403
|
// PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
|
|
252
404
|
runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
|
|
253
|
-
|
|
405
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(resultForModel, 200), child: agent?._logId })
|
|
406
|
+
return { ...item, result: resultForModel, ok: true }
|
|
254
407
|
} catch (error) {
|
|
255
408
|
// Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
|
|
256
409
|
logToolError(item.toolCall.name, item.args, error)
|
|
@@ -259,6 +412,9 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
259
412
|
// asked to stop — worst case with subagents, where the child runs its
|
|
260
413
|
// whole turn budget and the interrupt appears to do nothing.
|
|
261
414
|
if (signal?.aborted) throw error
|
|
415
|
+
// LOGGING(2026-09-03 code review #4):中止先于事件——用户停不落 tool:error
|
|
416
|
+
//(vscode execute-tools parity;阻塞子代理 child:error 同款抑制)
|
|
417
|
+
logEvent("tool:error", { tool: toolName, ms: Date.now() - toolT0, err: errText(error, 200), child: agent?._logId })
|
|
262
418
|
runHooks("PostToolUseFailure", { agent, toolName: item.toolCall.name, toolArgs: item.args, error }).catch(() => {})
|
|
263
419
|
// Build contextual error: tool name + key args so the model can reason about what went wrong
|
|
264
420
|
const ctxParts = []
|
|
@@ -282,7 +438,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
282
438
|
batch = []
|
|
283
439
|
}
|
|
284
440
|
for (const item of prepared) {
|
|
285
|
-
|
|
441
|
+
// escalate action keeps the retired escalate tool's serial placement (no
|
|
442
|
+
// parallel flag): it flushes the batch and runs alone in call order (§19 —
|
|
443
|
+
// spawn stays parallel; status classifies as readonly and batch freely).
|
|
444
|
+
if (item.tool && !item.tool.readonly
|
|
445
|
+
&& (!item.tool.parallel || isSubagentEscalateAction(item.tool.name, item.args))) {
|
|
286
446
|
await flush()
|
|
287
447
|
results.push(await runOne(item))
|
|
288
448
|
} else {
|
package/src/agent/helpers.mjs
CHANGED
|
@@ -33,7 +33,9 @@ export const REPORT_CONTINUATION =
|
|
|
33
33
|
"4. Anything left undone or worth follow-up"
|
|
34
34
|
|
|
35
35
|
const TOOL_RESULT_OFFLOAD_LIMIT = 64 * 1024 // 65536 chars — offload only above 64K (2026-08-24)
|
|
36
|
-
const TOOL_RESULT_PREVIEW = 64 * 1024 //
|
|
36
|
+
const TOOL_RESULT_PREVIEW = 64 * 1024 // total preview budget: head + middle note + tail ≤ 65536 (aligns with CLI/VS Code webview)
|
|
37
|
+
const TOOL_RESULT_PREVIEW_HEAD = 16 * 1024 // head slice preserved (2026-09-04 §5 — dual-end preview)
|
|
38
|
+
const TOOL_RESULT_PREVIEW_TAIL = 48 * 1024 // nominal tail slice (results/errors/stats live here — actual tail = budget remainder, see buildDualEndPreview)
|
|
37
39
|
|
|
38
40
|
/** UTF-16 安全截断(2026-09-02 deepseek 400 根因):slice(0, N) 按码元切会把 emoji 代理对切成孤立
|
|
39
41
|
* 高代理(如 🔴=U+D83D+DD34 只剩 D83D)——deepseek 解析器严格 UTF-16 报 400
|
|
@@ -46,6 +48,33 @@ function safeSliceUTF16(text, max) {
|
|
|
46
48
|
return text.slice(0, max)
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/** UTF-16 safe END slice (2026-09-04 §5 dual-end preview — review #5: both boundaries must not split
|
|
52
|
+
* a surrogate pair). Same rule as safeSliceUTF16, mirrored: if the slice START lands on a LOW
|
|
53
|
+
* surrogate (DC00-DFFF — the second half of a pair whose high half sits just before the boundary),
|
|
54
|
+
* advance one code unit so the slice never begins with an orphan low surrogate. */
|
|
55
|
+
function safeSliceUTF16End(text, max) {
|
|
56
|
+
if (text.length <= max) return text
|
|
57
|
+
const start = text.length - max
|
|
58
|
+
const cp = text.charCodeAt(start)
|
|
59
|
+
if (cp >= 0xdc00 && cp <= 0xdfff) return text.slice(start + 1)
|
|
60
|
+
return text.slice(start)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Dual-end preview (design §5 D-4.1): head + middle-omitted note + tail — the tail carries
|
|
64
|
+
* results/errors/stats that a pure-head truncation would cut off.
|
|
65
|
+
* Budget (round1 review #2, fixed): head + note + tail ≤ TOOL_RESULT_PREVIEW (65536) — tail is
|
|
66
|
+
* computed from constants (tail = TOOL_RESULT_PREVIEW − head − noteLen), never hardcoded.
|
|
67
|
+
* The note length depends on the omitted digit count; text.length's digit count is an upper bound
|
|
68
|
+
* for omitted (< text.length), so budgeting with it keeps the total ≤ 65536 while the printed
|
|
69
|
+
* note reports the actual omitted count. Both boundaries run surrogate-safe slices (review #5). */
|
|
70
|
+
function buildDualEndPreview(text) {
|
|
71
|
+
const head = safeSliceUTF16(text, TOOL_RESULT_PREVIEW_HEAD)
|
|
72
|
+
const noteFn = (omitted) => `\n\n… [middle omitted: ${omitted} chars] …\n\n`
|
|
73
|
+
const tailLen = Math.min(TOOL_RESULT_PREVIEW_TAIL, TOOL_RESULT_PREVIEW - TOOL_RESULT_PREVIEW_HEAD - noteFn(text.length).length)
|
|
74
|
+
const tail = safeSliceUTF16End(text, tailLen)
|
|
75
|
+
return head + noteFn(Math.max(0, text.length - head.length - tail.length)) + tail
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
/** Offload-dir write-time self-cleanup retention window (2026-08-21): files older than 3 days are deleted on the next offload. */
|
|
50
79
|
export const TMP_RETENTION_MS = 3 * 24 * 3600 * 1000
|
|
51
80
|
|
|
@@ -88,7 +117,8 @@ export async function cleanupOldToolResults(dir) {
|
|
|
88
117
|
}
|
|
89
118
|
}
|
|
90
119
|
|
|
91
|
-
/** Offload oversized tool results (>64K chars) to disk, returning a preview + file path
|
|
120
|
+
/** Offload oversized tool results (>64K chars) to disk, returning a head+tail preview + file path
|
|
121
|
+
* (2026-09-04 §5 — dual-end preview; the failed-offload fallback uses the same dual-end slice).
|
|
92
122
|
* Writes trigger write-time self-cleanup of the offload dir first (dir param overridable for tests). */
|
|
93
123
|
export async function offloadToolResult(text, callId, dir = join(configDir, "tool-results")) {
|
|
94
124
|
if (text.length <= TOOL_RESULT_OFFLOAD_LIMIT) return text
|
|
@@ -98,12 +128,13 @@ export async function offloadToolResult(text, callId, dir = join(configDir, "too
|
|
|
98
128
|
const file = join(dir, `${Date.now()}-${String(callId).replace(/[^a-zA-Z0-9_-]/g, "_")}.log`)
|
|
99
129
|
await writeFile(file, text, "utf8")
|
|
100
130
|
return (
|
|
101
|
-
|
|
131
|
+
buildDualEndPreview(text) +
|
|
102
132
|
`\n\n[... output too large (${text.length} chars total), full content saved to: ${file}\n` +
|
|
103
133
|
`Page through it with the read tool (offset/limit) or sed -n 'START,ENDp' — do NOT re-run the tool blindly.]`
|
|
104
134
|
)
|
|
105
135
|
} catch {
|
|
106
|
-
|
|
136
|
+
// review #3: fallback uses the same dual-end slice (head + omitted note + tail, no path hint)
|
|
137
|
+
return buildDualEndPreview(text) + `\n\n[... truncated: ${text.length} chars total, offload to disk failed]`
|
|
107
138
|
}
|
|
108
139
|
}
|
|
109
140
|
|
|
@@ -257,3 +288,39 @@ export async function loadProjectInstructions(cwd) {
|
|
|
257
288
|
merged
|
|
258
289
|
)
|
|
259
290
|
}
|
|
291
|
+
|
|
292
|
+
// Engineering mode reminders + auto-turn digest domain + mode injector
|
|
293
|
+
// (2026-09-05 module-split:自 agent.mjs 迁入——agent.mjs 530 > 500 硬限——agent.mjs
|
|
294
|
+
// re-export 保 import 面:eng.mjs / cmd-eng.mjs / 测试从 agent.mjs import)
|
|
295
|
+
|
|
296
|
+
/** Engineering mode reminder — shared with eng.mjs tool. */
|
|
297
|
+
export const ENG_ON_REMINDER =
|
|
298
|
+
"[System reminder: engineering mode is ON — design-before-code enforced. " +
|
|
299
|
+
"Workflow: Requirements doc → Design doc → advisor(type='design') → " +
|
|
300
|
+
"user approval → eng-coder implementation. Code changes go through eng-coder " +
|
|
301
|
+
"subagents only. Advisor calls are NOT per-turn-mandatory — call only at " +
|
|
302
|
+
"flow nodes or when the user asks.]"
|
|
303
|
+
|
|
304
|
+
/** Engineering mode OFF reminder — shared with the eng tool and the injector. */
|
|
305
|
+
export const ENG_OFF_REMINDER =
|
|
306
|
+
"[System reminder: engineering mode is now OFF — standard discipline applies. " +
|
|
307
|
+
"Changes go through the normal workflow: you may edit files directly, advisor/verify " +
|
|
308
|
+
"guards apply per config.]"
|
|
309
|
+
|
|
310
|
+
/** Manual-tier auto-turn digest domain (AGENT-LOOP.md §17 D-S6): organize-only.
|
|
311
|
+
* Injected per manual auto-turn run — writes/execute/spawns/questions are also
|
|
312
|
+
* mechanically denied (no permission handler + spawn gate); this steers first. */
|
|
313
|
+
export const AUTO_TURN_DIGEST_DOMAIN =
|
|
314
|
+
"[System reminder: auto-turn — background async subagents finished while there was no user message, and this turn runs automatically to digest their reports (the finished-report reminders above). No one is waiting for this reply, so organize only: 1) summarize each finished report's key points into this conversation for the user to read later; 2) update the task list with the task tool (allowed) to mark finished work done; 3) write decision points with a suggested next step as text — do not execute it. FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the summaries are written.]"
|
|
315
|
+
|
|
316
|
+
/** Engineering-mode status injection — one reminder on EVERY transition (2026-08-25:
|
|
317
|
+
* OFF is announced too — the model must know the gates lifted; silence after /eng-off
|
|
318
|
+
* left it guessing. Covers TUI /eng, resume, and any path bypassing the eng tool.) */
|
|
319
|
+
export function injectEngineeringReminder(agent) {
|
|
320
|
+
const eng = agent.config?.agent?.engineering ?? false
|
|
321
|
+
if (eng !== agent._lastEngState) {
|
|
322
|
+
agent.history.push({ role: "user", content: eng ? ENG_ON_REMINDER : ENG_OFF_REMINDER, transient: true })
|
|
323
|
+
}
|
|
324
|
+
agent._lastEngState = eng
|
|
325
|
+
}
|
|
326
|
+
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
import { pushReal } from "../context.mjs"
|
|
23
23
|
import { specForModel } from "../config.mjs"
|
|
24
24
|
import { FILE_MUTATORS } from "./helpers.mjs"
|
|
25
|
-
import {
|
|
25
|
+
import { resolve } from "node:path"
|
|
26
|
+
import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.mjs"
|
|
27
|
+
import { looksLikeReviewOutput } from "../advisor/run.mjs"
|
|
26
28
|
|
|
27
29
|
let _reindexFile = null
|
|
28
30
|
|
|
@@ -72,6 +74,9 @@ export async function recordToolResults(agent, toolByName, results) {
|
|
|
72
74
|
// Direct file edit — code was changed. The prior advisor review and
|
|
73
75
|
// verify are stale: a review that ran before the edit no longer
|
|
74
76
|
// covers the current file state.
|
|
77
|
+
// §29 fix A(AGENT-LOOP.md §29——2026-09-07):mutation-seq 记账已移到 dispatch
|
|
78
|
+
// runOne 执行成功即刻(唯一记账点——取代本批后段 + agent.mjs 中断分支——不双计)——
|
|
79
|
+
// 此处仅剩 guard 标志失效(顺序语义:批内同消息的 sync advisor 提交仍在其后置位)。
|
|
75
80
|
agent._mutatedThisRun = true
|
|
76
81
|
agent._calledAdvisorThisRun = false
|
|
77
82
|
agent._verifiedThisRun = false
|
|
@@ -91,20 +96,51 @@ export async function recordToolResults(agent, toolByName, results) {
|
|
|
91
96
|
}
|
|
92
97
|
if (toolCall.name === "verify") agent._verifiedThisRun = true
|
|
93
98
|
if (toolCall.name === "advisor") {
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// the
|
|
101
|
-
|
|
99
|
+
// §24 D-24b (settle accounting split — fix #2): an ASYNC launch returns an
|
|
100
|
+
// ack and settles later — the settle callback owns its called/round/token
|
|
101
|
+
// accounting. Only the SYNC path (depth>0 / explicit async:false) accounts
|
|
102
|
+
// here — per-review instance round++ (marker-keyed by tool call id) + the
|
|
103
|
+
// legacy mirror._advisorRound_ counter stays for display/back-compat.
|
|
104
|
+
// REFUSED launches (pool full / per-review cap) count as neither a call
|
|
105
|
+
// nor a completion: no called-mark (the guard must keep pushing until a
|
|
106
|
+
// review really runs), no round advance.
|
|
107
|
+
const refused = agent._advisorRefusals?.has(toolCall.id)
|
|
108
|
+
const asyncAck = agent._advisorAsyncAcks?.has(toolCall.id)
|
|
109
|
+
if (refused) {
|
|
110
|
+
agent._advisorRefusals.delete(toolCall.id)
|
|
111
|
+
} else if (asyncAck) {
|
|
112
|
+
agent._advisorAsyncAcks.delete(toolCall.id)
|
|
113
|
+
} else {
|
|
114
|
+
agent._calledAdvisorThisRun = true
|
|
115
|
+
const reviewId = agent._advisorSyncCalls?.get(toolCall.id)
|
|
116
|
+
if (reviewId !== undefined) {
|
|
117
|
+
const run = advisorRuns(agent).get(reviewId)
|
|
118
|
+
if (run) {
|
|
119
|
+
run.round++
|
|
120
|
+
agent._advisorRound = run.round
|
|
121
|
+
// Prior of round 2+ = the last REVIEW-LOOKING output (run.mjs parity).
|
|
122
|
+
// F2e (§29.1): the sync settle's engine-approved suffix (stored on the
|
|
123
|
+
// run by settleDesignReview) is stripped with exact truncation — the
|
|
124
|
+
// prior never carries the raw token / designId.
|
|
125
|
+
if (looksLikeReviewOutput(result)) {
|
|
126
|
+
run.priorOutput = stripApprovedSuffix(result, run.approvedSuffix)
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
agent._advisorRound++
|
|
130
|
+
}
|
|
131
|
+
agent._advisorSyncCalls.delete(toolCall.id)
|
|
132
|
+
} else {
|
|
133
|
+
// Direct/legacy callers without a resolution marker — plain mirror
|
|
134
|
+
// increment (the per-review registry never saw this call).
|
|
135
|
+
agent._advisorRound++
|
|
136
|
+
}
|
|
137
|
+
}
|
|
102
138
|
}
|
|
103
139
|
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
104
140
|
const args = JSON.parse(toolCall.arguments)
|
|
105
141
|
const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
|
|
106
142
|
for (const p of paths) {
|
|
107
|
-
const abs =
|
|
143
|
+
const abs = resolve(agent.cwd, p)
|
|
108
144
|
if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
|
|
109
145
|
if (agent.memory) {
|
|
110
146
|
// Fire-and-forget: don't block the agent loop on indexing.
|