@thincoder/core 0.9.5 → 0.10.2
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 +49 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +29 -12
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-ref.mjs +96 -3
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +108 -27
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
package/advisor.mjs
CHANGED
|
@@ -127,7 +127,7 @@ export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
|
|
|
127
127
|
* @param {Object} agent — the parent agent (history used for the response table)
|
|
128
128
|
* @param {Object|null} prior — prior issue table (extracted from history when null)
|
|
129
129
|
* @param {string[]|null} [scopeFiles] — review surface for the no-response fallback (cwd-relative)
|
|
130
|
-
* @param {Object|null} [object] — review-object declaration (§18
|
|
130
|
+
* @param {Object|null} [object] — review-object declaration (AGENT-LOOP-ASYNC-POOL.md §6.18): mechanically
|
|
131
131
|
* prepended to the round-2+ follow-up so every round stays anchored (T-OA2).
|
|
132
132
|
* @returns {string} the follow-up user message — or a plain "System reminder: …"
|
|
133
133
|
* fresh-review fallback (NO brackets — some OpenAI-compatible servers parse
|
|
@@ -183,10 +183,10 @@ export { escapeLiteralEscapes }
|
|
|
183
183
|
* @param {string|null} [designToken] — design-review approval token (design only)
|
|
184
184
|
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (passed through to buildAdvisorUserMessage)
|
|
185
185
|
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review
|
|
186
|
-
* @param {Object|null} [object] — review-object declaration (§18
|
|
186
|
+
* @param {Object|null} [object] — review-object declaration (AGENT-LOOP-ASYNC-POOL.md §6.18 D-OA1): passed through
|
|
187
187
|
* to the user-message builders; mechanically injected at the start of every review
|
|
188
188
|
* round (round 1 design/code + round 2+ follow-up). Absent → legacy behavior.
|
|
189
|
-
* @param {string|null} [designId] —
|
|
189
|
+
* @param {string|null} [designId] — F2a: injected next to the design token in
|
|
190
190
|
* the Approval Signal (round 1 + round 2+ — both values, verbatim anchor).
|
|
191
191
|
*/
|
|
192
192
|
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null, object = null, designId = null) {
|
|
@@ -258,7 +258,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
258
258
|
// review surface.
|
|
259
259
|
const scopeFiles = resolveScopeFiles(agent, paths)
|
|
260
260
|
const followUp = buildAdvisorFollowUp(agent, prior, scopeFiles, object)
|
|
261
|
-
// §
|
|
261
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b (design round 2+ — async fix-round continuations must be able to
|
|
262
262
|
// re-approve): re-anchor the review scope (the convergence follow-up carries no
|
|
263
263
|
// document list) and inject the round's design token with the approval signal.
|
|
264
264
|
if (reviewType === "design") {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/assemble.mjs — 会话装配序**核单源**:配置 → 代理注入 → 记忆(+embedder)→ 文件规则 →
|
|
3
|
+
* 记忆层(项目 → 团队)→ 工具 →〔工具终形缝〕→ 代理 + 三字段(providers ∕ activeProvider ∕ activeModel)。
|
|
4
|
+
* 来源(纯搬 —— 端壳零副本):desk `src/main/agent-assemble.mjs`(其前身 = `agent-host.mjs` 装配段逐字搬运)
|
|
5
|
+
* ∕ CLI `src/cli/make-agent.mjs` 装配段——两造 1–8 步逐段对齐(同序同义),本档为其唯一实现。
|
|
6
|
+
* 本档出 `assembleAgent` + 装配缺省面 `DEFAULT_DEPS`(逐项可注入 —— 装配序可机验 U79)+ 团队层取值
|
|
7
|
+
* `teamConfig` + 装配作者 `gitAuthor` + 装配后唯一校验点 `validateProvider`。
|
|
8
|
+
* 端壳留面(各端 adapter,非本档):desk = cwd 注入(`projects.currentCwd()`)· `_slot` · 校验调用 ·
|
|
9
|
+
* `installExecRunSeams()` 尾;CLI = cwd = `process.cwd()` · MCP 合入(入 `toolsFinalize`)· `_mcpWarnings` ·
|
|
10
|
+
* `applyToolExclusions` · `attachManifest` · 校验调用。
|
|
11
|
+
* 零宿主依赖:核函数全部经 `deps` 取值(缺省 = 本档单源)⇒ 平 node 直测。
|
|
12
|
+
*/
|
|
13
|
+
import { execSync } from "node:child_process"
|
|
14
|
+
import { isAbsolute, join } from "node:path"
|
|
15
|
+
import { createAgent } from "../agent.mjs"
|
|
16
|
+
import { configDir, loadConfig } from "../config.mjs"
|
|
17
|
+
import { createMemory, syncDir } from "../memory.mjs"
|
|
18
|
+
import { discoverRules, mergeFileRules } from "../rules.mjs"
|
|
19
|
+
import { assembleBuiltinTools } from "../tools/index.mjs"
|
|
20
|
+
|
|
21
|
+
/** 装配缺省面(逐项可注入 —— 断言装配调用序用假 deps;缺省 = 本档单源)。 */
|
|
22
|
+
export const DEFAULT_DEPS = Object.freeze({
|
|
23
|
+
loadConfig, createMemory, createAgent, assembleBuiltinTools, discoverRules, syncDir,
|
|
24
|
+
team: teamConfig, author: gitAuthor,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/** 团队层配置(核 `config.memory.team` 有 repo 才成层)——原两造逐字同构,归一至此;
|
|
28
|
+
* 未配置 ⇒ null(装配跳过团队层)。缺省 name = `"default"`;`dir` 缺省 = `<configDir>/teams/<name>`。 */
|
|
29
|
+
export function teamConfig(config) {
|
|
30
|
+
const team = config?.memory?.team
|
|
31
|
+
if (!team?.repo) return null
|
|
32
|
+
const name = team.name ?? "default"
|
|
33
|
+
return { name, repo: team.repo, dir: team.dir ?? join(configDir, "teams", name) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 装配作者:git `user.name`,缺 ⇒ `"unknown"`(注入缝 = `DEFAULT_DEPS.author`)。 */
|
|
37
|
+
export function gitAuthor() {
|
|
38
|
+
try { return execSync("git config user.name", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "unknown" } catch { return "unknown" }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 装配后唯一校验点(判据 = `name && model && baseURL`):**不抛** —— 无效 ⇒ 记 `_providerInvalid`
|
|
42
|
+
* 由回合驱动消费(`{ok:false,reason:"provider-invalid"}`,零假回合);有效 ⇒ 清两标(幂等,复验径复用)。
|
|
43
|
+
* reason = `config?.providerInvalidReason ?? 三档文案`(provider 不存在 ∕ model 缺失 ∕ 缺少 baseURL)。
|
|
44
|
+
* 返回 agent 便于链式调用。 */
|
|
45
|
+
export function validateProvider(agent, config) {
|
|
46
|
+
const p = agent?.provider
|
|
47
|
+
if (p?.name && p?.model && p?.baseURL) {
|
|
48
|
+
delete agent._providerInvalid
|
|
49
|
+
delete agent._providerInvalidReason
|
|
50
|
+
return agent
|
|
51
|
+
}
|
|
52
|
+
agent._providerInvalid = true
|
|
53
|
+
agent._providerInvalidReason = config?.providerInvalidReason
|
|
54
|
+
?? (!p?.name ? "provider 不存在" : !p.model ? "model 缺失" : "缺少 baseURL")
|
|
55
|
+
return agent
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 装配一份会话代理(序见档头;`cwd` = 项目根,由各端 adapter 注入)。
|
|
59
|
+
* `deps` 逐项可注入(缺省 = 本档单源):装配序可机验(U79);`deps.injectProxy` 可选(缺省 = 动态
|
|
60
|
+
* import `../proxy.mjs`)。
|
|
61
|
+
* `toolsFinalize(baseTools, { config, cwd, provider }) → tools ∕ Promise<tools>`:工具终形缝
|
|
62
|
+
* (缺省 = 恒等)——位次 = 工具装配后、`createAgent` 前;核体 `await`(同步返回值与 Promise 同判)。 */
|
|
63
|
+
export async function assembleAgent({ cwd, deps = {}, toolsFinalize = null }) {
|
|
64
|
+
const D = { ...DEFAULT_DEPS, ...deps }
|
|
65
|
+
const config = D.loadConfig()
|
|
66
|
+
const provider = config.provider
|
|
67
|
+
const providers = config.providersList
|
|
68
|
+
// 代理注入(double opt-in: provider.proxy + config.proxy.model);config.provider 是 loadConfig 里的
|
|
69
|
+
// 独立拷贝 —— 同步注入结果(proxyUri)。
|
|
70
|
+
const injectProxy = D.injectProxy ?? (await import("../proxy.mjs")).injectProxy
|
|
71
|
+
injectProxy(providers, config)
|
|
72
|
+
if (provider?.name) provider.proxyUri = providers.find((p) => p.name === provider.name)?.proxyUri
|
|
73
|
+
|
|
74
|
+
const memory = D.createMemory({ dbPath: config.memory.dbPath })
|
|
75
|
+
// Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
|
|
76
|
+
if (config.embedding?.apiKey) {
|
|
77
|
+
const { createEmbedder } = await import("../embedding.mjs")
|
|
78
|
+
memory.embedder = createEmbedder(config.embedding)
|
|
79
|
+
}
|
|
80
|
+
// 项目级规则(.thincoder/rules/*.md)并入 config:文件规则在前(优先),config.json 规则按 pattern 去重在后。
|
|
81
|
+
// 合并体单源 = `../rules.mjs` `mergeFileRules`(B7 3a 自本处内联块抽取;`discover` 走 `D.` 注入缝)。
|
|
82
|
+
config.agent.streamRules = mergeFileRules(config.agent?.streamRules, cwd, D.discoverRules)
|
|
83
|
+
// code/doc 索引按 origin(项目根目录)隔离:检索只落本项目范围。
|
|
84
|
+
memory.codeOrigin = cwd
|
|
85
|
+
// 项目层:启动时把 `.thincoder/memory/` 目录同步进索引(有则同步、无则跳过)。
|
|
86
|
+
if (config.memory.projectDir) {
|
|
87
|
+
memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir)
|
|
88
|
+
await D.syncDir(memory, { layer: "project", dir: memory.projectOrigin })
|
|
89
|
+
}
|
|
90
|
+
// 团队层(可选):首用自动 clone;启动只索引本地目录,远端拉取走显式 sync 命令。
|
|
91
|
+
const team = D.team(config)
|
|
92
|
+
if (team) {
|
|
93
|
+
const { ensureClone } = await import("../git/gitmem.mjs")
|
|
94
|
+
await ensureClone(team)
|
|
95
|
+
await D.syncDir(memory, { layer: "team", dir: team.dir })
|
|
96
|
+
}
|
|
97
|
+
// #70(CORE-UNIFICATION TOOLS):注册表与消费侧拼装面归位核内——单源 `assembleBuiltinTools`
|
|
98
|
+
// (静态表 ∪ memory / code_search / doc_search / repo_outline / settings / peer_instances 六面 ∪
|
|
99
|
+
// 门控 read_image)。传 `model` 必需:漏传 ⇒ `read_image` 对所有模型静默消失
|
|
100
|
+
// (门判据 = specForModel(model)?.multimodal)。
|
|
101
|
+
const baseTools = await D.assembleBuiltinTools({
|
|
102
|
+
memory, cwd, projectDir: config.memory.projectDir, author: D.author(), team,
|
|
103
|
+
model: provider?.model ?? null,
|
|
104
|
+
})
|
|
105
|
+
const tools = toolsFinalize ? await toolsFinalize(baseTools, { config, cwd, provider }) : baseTools
|
|
106
|
+
const agent = D.createAgent({ provider, tools, config, cwd, memory })
|
|
107
|
+
agent.providers = providers
|
|
108
|
+
agent.activeProvider = provider?.name ?? ""
|
|
109
|
+
agent.activeModel = provider?.model ?? null
|
|
110
|
+
return agent
|
|
111
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/chat-call.mjs — 单回合模型调用(2026-09-29 P2 三拆:agent.mjs runAgent 段 4 逐字迁入 +
|
|
3
|
+
* `streamOutputAllowed` 定义随迁——agent.mjs re-export 保 import 面)。
|
|
4
|
+
* 接口 = callModelTurn(agent, ctx) → response;错误走原样:AbortError-中断臂处理完照旧 `throw e`,
|
|
5
|
+
* 由 runAgent 的 catch/finally 收束。
|
|
6
|
+
*/
|
|
7
|
+
import { chat } from "../provider/index.mjs"
|
|
8
|
+
|
|
9
|
+
/** 流式输出门(§2.5 #78 并入——VSC 三态门,纯函数可直驱测试):depth 0 恒通;
|
|
10
|
+
* consult 子代理豁免(其输出进会诊面板);其余经 `streamOutput` 显式选择进入。 */
|
|
11
|
+
export function streamOutputAllowed(depth, role, streamOutput = false) {
|
|
12
|
+
return depth === 0 || role === "consult" || streamOutput === true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function callModelTurn(agent, {
|
|
16
|
+
systemPrompt, toolSchemas, streamOutput, callbacks, signal, streamRuleFired, autoTurn, depth, turn,
|
|
17
|
+
}) {
|
|
18
|
+
const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
|
|
19
|
+
let response
|
|
20
|
+
|
|
21
|
+
// Auto-think: classify difficulty and set reasoning effort on turn 0; silent on failure.
|
|
22
|
+
if (agent.config?.agent?.autoThink && turn === 0) {
|
|
23
|
+
const { classifyAndApply } = await import("../auto-think.mjs")
|
|
24
|
+
await classifyAndApply(agent, turn).catch(() => {})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try { response = await chat(agent.provider, {
|
|
28
|
+
messages, tools: toolSchemas,
|
|
29
|
+
// §2.5 #78 并入(VSC onToken 三态门):depth 0 恒通;consult 子代理豁免(其输出进
|
|
30
|
+
// 会诊面板);escalate 等经 opts.streamOutput 显式选择进入;其余子代理不流式。
|
|
31
|
+
onToken: streamOutputAllowed(depth, agent._role, streamOutput) ? callbacks.onToken : null,
|
|
32
|
+
onReasoning: callbacks.onReasoning,
|
|
33
|
+
onWait: callbacks.onWait,
|
|
34
|
+
signal,
|
|
35
|
+
streamRules: agent.config.agent?.streamRules ?? [],
|
|
36
|
+
firedPatterns: streamRuleFired,
|
|
37
|
+
// LOGGING(LOGGING.md):llm:* 事件的语义上下文(stage=turn 主循环回合——含
|
|
38
|
+
// digest 消化轮 auto=true;child=子代理 id(spawn 时 stamp 于 child._logId))
|
|
39
|
+
// TRACES.md §6.1 D-TR4:轨迹元数据增补(role/depth/kind/session/cwd——trace-store 只读
|
|
40
|
+
// logCtx,签名不变);kind:depth>0 = subagent(consult 孩子 = consult)——子代理
|
|
41
|
+
// 对回靠 role+depth+child id(children 无 _sessionStart——不经 depth-0 设置——
|
|
42
|
+
// session 字段对子代理轨迹为 null——见 trace-store/agent.mjs 注释)。
|
|
43
|
+
logCtx: {
|
|
44
|
+
stage: "turn", turn: turn + 1, auto: autoTurn, child: agent._logId,
|
|
45
|
+
role: agent._role ?? null,
|
|
46
|
+
depth,
|
|
47
|
+
kind: depth > 0 ? (agent._role === "consult" ? "consult" : "subagent") : "turn",
|
|
48
|
+
session: agent._sessionStart ?? null,
|
|
49
|
+
cwd: agent.cwd,
|
|
50
|
+
traces: agent.config?.traces?.enabled !== false,
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// User interrupt (Ctrl+I): controller.abort({ interrupt: true, message }).
|
|
55
|
+
// Inject into history; the outer loop recreates the controller and resumes.
|
|
56
|
+
if (e.name === "AbortError" && signal?.reason?.interrupt) {
|
|
57
|
+
const msg = `[User interrupt: ${signal.reason.message}]`
|
|
58
|
+
// Dedup: if already handled during tool execution (interrupt branch below),
|
|
59
|
+
// don't push a duplicate — the outer loop still recreates the controller.
|
|
60
|
+
if (agent.history.at(-1)?.content !== msg) {
|
|
61
|
+
agent.history.push({ role: "user", content: msg })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw e
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return response
|
|
68
|
+
}
|
package/agent/child-marks.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 收编自 `spawn-child.mjs`(2026-09-20 端差·显示面消差批 §2.2 X6 实现轮):端壳侧需**静态**
|
|
5
5
|
* 导入锚点做块头注记判定,而 `spawn-child.mjs` 静态链经核 agent 栈可达 `node:sqlite`
|
|
6
|
-
* ⇒ 端壳静态闭包禁达(W8
|
|
7
|
-
* fail-closed 红)⇒ 锚点下沉**零依赖叶**(先例 = `relay-prefix.mjs` 同因下沉)。`spawn-child.mjs`
|
|
6
|
+
* ⇒ 端壳静态闭包禁达(W8 契约②判据(现载体 = 批件 `docs/batches/2026-09-29-residuals-round2.test.mjs`;
|
|
7
|
+
* 单测树重建时回迁端侧单测档):fail-closed 红)⇒ 锚点下沉**零依赖叶**(先例 = `relay-prefix.mjs` 同因下沉)。`spawn-child.mjs`
|
|
8
8
|
* 原样再导出 ⇒ 既有 import 面(核 agent-tools 族 / CLI `tool-events.mjs`)零改。
|
|
9
9
|
*
|
|
10
10
|
* 语义:消费端用 `includes()` 检出锚点 ⇒ 合成块头注记("work may be partial")——两端各一处:
|
package/agent/completion.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Checks: pending tasks, verify guard, advisor guard.
|
|
5
5
|
* Returns { action: 'continue' | 'done', content?, guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
6
|
+
* 子回合边界(#628):六推回点各触 `callbacks.onSubTurnBreak?.()` —— 零参可选(缺省 no-op);`onTurnEnd` 超集语义零改。
|
|
6
7
|
*/
|
|
7
8
|
import { hasCodeMutations } from "../advisor/repos.mjs"
|
|
8
9
|
import { pushReal } from "../context.mjs"
|
|
@@ -24,7 +25,7 @@ const MAX_EMPTY_RETRIES = 2
|
|
|
24
25
|
* @param {number} guardPushbacks - verify guard pushback count (mutated)
|
|
25
26
|
* @param {boolean} honestReminderInjected - whether exhausted-verify reminder was already sent (mutated)
|
|
26
27
|
* @param {number} advisorPushbacks - advisor guard pushback count (mutated)
|
|
27
|
-
* @param {object} callbacks - { onTurnEnd }
|
|
28
|
+
* @param {object} callbacks - { onTurnEnd, onSubTurnBreak }
|
|
28
29
|
*/
|
|
29
30
|
export function handleCompletion(agent, response, depth, turn, guardPushbacks, honestReminderInjected, advisorPushbacks, callbacks) {
|
|
30
31
|
if (!response.content) {
|
|
@@ -38,6 +39,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
38
39
|
role: "user",
|
|
39
40
|
content: "[System reminder: your last response was empty — the provider returned no content (likely reasoning was exhausted or output was truncated). Respond again, continuing your work from where you left off.]",
|
|
40
41
|
})
|
|
42
|
+
callbacks.onSubTurnBreak?.()
|
|
41
43
|
callbacks.onTurnEnd?.(agent, turn)
|
|
42
44
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
43
45
|
}
|
|
@@ -63,6 +65,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
63
65
|
role: "user",
|
|
64
66
|
content: `[System reminder: you still have pending tasks: ${pending}. Update their status with the task tool before finishing — if they're done, mark them done; if they're not applicable, remove them. (This is your only reminder — if you choose not to, finish anyway.)]`,
|
|
65
67
|
})
|
|
68
|
+
callbacks.onSubTurnBreak?.()
|
|
66
69
|
callbacks.onTurnEnd?.(agent, turn)
|
|
67
70
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
68
71
|
}
|
|
@@ -81,6 +84,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
81
84
|
role: "user",
|
|
82
85
|
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: run the project's verification yourself (per its AGENTS.md test method), then call verify declaring the outcome via verification.status. verify mechanically gates on your declaration. If verification is genuinely impossible here, say so explicitly in your reply.]",
|
|
83
86
|
})
|
|
87
|
+
callbacks.onSubTurnBreak?.()
|
|
84
88
|
callbacks.onTurnEnd?.(agent, turn)
|
|
85
89
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
86
90
|
}
|
|
@@ -94,6 +98,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
94
98
|
role: "user",
|
|
95
99
|
content: `[System reminder: (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}) verify was not passed — either your verification declared failed, was skipped without a reason, or was not declared. Fix or complete your verification, then call verify again declaring the outcome. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
|
|
96
100
|
})
|
|
101
|
+
callbacks.onSubTurnBreak?.()
|
|
97
102
|
callbacks.onTurnEnd?.(agent, turn)
|
|
98
103
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
99
104
|
}
|
|
@@ -109,6 +114,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
109
114
|
role: "user",
|
|
110
115
|
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted. You have not passed verification. Either state explicitly that your verification could not be completed, or run verify again once it is. If your verification could not be completed, say so explicitly in your reply to the user — state what you tried and what you believe is blocking you, and do not present the work as complete; the user needs to know it is unfinished.]`,
|
|
111
116
|
})
|
|
117
|
+
callbacks.onSubTurnBreak?.()
|
|
112
118
|
callbacks.onTurnEnd?.(agent, turn)
|
|
113
119
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
114
120
|
}
|
|
@@ -121,7 +127,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
121
127
|
const cfg = agent.config?.advisor
|
|
122
128
|
const advisorReview = cfg?.guard === true
|
|
123
129
|
if (depth === 0 && advisorReview && !agent.config?.agent?.engineering) {
|
|
124
|
-
// §
|
|
130
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b (T-24b4 — guard timing): an async review that is still in flight
|
|
125
131
|
// (or queued in the advisor pool) means the review was launched — the guard
|
|
126
132
|
// does NOT push back while it is pending (未决不算未评审); once it settles
|
|
127
133
|
// non-stale it marks _calledAdvisorThisRun, and a STALE settle leaves the
|
|
@@ -137,6 +143,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
137
143
|
role: "user",
|
|
138
144
|
content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${rounds + 1}). Call the \`advisor\` tool now. This is required, not optional — do not skip it even if you believe the changes are trivial — the review will be quick either way. After the review, produce a response table for every issue found (see discipline rules for format).]`,
|
|
139
145
|
})
|
|
146
|
+
callbacks.onSubTurnBreak?.()
|
|
140
147
|
callbacks.onTurnEnd?.(agent, turn)
|
|
141
148
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
142
149
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* continue-decision.mjs — 续跑判定(核单源 · #677 · I10)。
|
|
3
|
+
* 同形重复族 #127 ④(续跑循环双写——CLI `thincoder-cli/src/tui/agent-turn.mjs` ∥ VSC
|
|
4
|
+
* `thincoder-vscode/src/extension/panel-turn-loop.mjs`)收口:端壳循环只接线本函数,
|
|
5
|
+
* 零第二分支;判定口径改一处 ⇒ 两端同轮生效(原双改纪律退役)。
|
|
6
|
+
* 三格判定(口径 = TURN-CAP-CONTINUE.md D-TC15 ∕ F8):
|
|
7
|
+
* ① ContinueError(撞帽)——无人值守档(autoTurn)⇒ "cap-stop"(不静默自续);
|
|
8
|
+
* 人工档 ⇒ "ask"(续期恒须人答——AUTO 亦不自动批准)。
|
|
9
|
+
* ② 中止(AbortError ∥ reason 在场)——带注入消息的 interrupt(Ctrl+I)⇒ "resume";其余 ⇒ "stop"。
|
|
10
|
+
* ③ 其余错误 ⇒ "error";无错误(正常完成)⇒ "done"。
|
|
11
|
+
* `reason` = 调用方解析后的中止 reason(两端同式 = 回合 controller `signal.reason` 单源——#677 修正轮对齐;
|
|
12
|
+
* 核不臆测宿主 signal 源)。
|
|
13
|
+
* `autoApprove` = 接口承载(#127 形);现语义下不改判定(F8 ∕ D-TC15)。
|
|
14
|
+
*/
|
|
15
|
+
import { ContinueError } from "./helpers.mjs"
|
|
16
|
+
|
|
17
|
+
/** @returns {"done"|"cap-stop"|"ask"|"resume"|"stop"|"error"} */
|
|
18
|
+
export function continueDecision(error, { autoTurn = false, autoApprove = false, reason = null } = {}) {
|
|
19
|
+
if (error == null) return "done"
|
|
20
|
+
if (error instanceof ContinueError) return autoTurn ? "cap-stop" : "ask"
|
|
21
|
+
if (error.name === "AbortError" || reason != null) {
|
|
22
|
+
return reason?.interrupt && reason?.message ? "resume" : "stop"
|
|
23
|
+
}
|
|
24
|
+
return "error"
|
|
25
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/dispatch-gates.mjs — dispatch 门面谓词与记账(2026-09-28 拆分批 · R3——自
|
|
3
|
+
* `agent/dispatch.mjs`(498/499 贴 500 硬限)外提:动作级分类四谓词(readonly ∕ control ∕
|
|
4
|
+
* consume-design ∕ escalate)+ 工具错误落盘 `logToolError` + 执行即刻 mutation 记账
|
|
5
|
+
* `noteExecutedMutation`;**逐字迁移**、零行为改)。
|
|
6
|
+
* `executeToolCalls` 仍住 `dispatch.mjs`(对外缝零改——签名与调用点零改);单条执行体住
|
|
7
|
+
* `./dispatch-run.mjs`(其 import 本档的 `logToolError` ∕ `noteExecutedMutation`)——本档
|
|
8
|
+
* 零回引,为两者之叶。ERRORS_DIR = ~/.thincoder/tool-errors。
|
|
9
|
+
*/
|
|
10
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs"
|
|
11
|
+
import { join, resolve } from "node:path"
|
|
12
|
+
import { homedir } from "node:os"
|
|
13
|
+
import { toolTouchPaths } from "./helpers.mjs"
|
|
14
|
+
// fix A(2026-09-07):FILE_MUTATORS 的 mutation-seq 记账从
|
|
15
|
+
// 批后提交(record-results noteMutations)移到执行成功即刻——唯一记账点(取代批后段
|
|
16
|
+
// + agent.mjs 中断分支记账——不双计)——同消息 [写 + async advisor launch] 时 launch 前
|
|
17
|
+
// 完成的写在 launchSeq 之前落地 → settle 不再误判 stale(fix A 症状根因)。
|
|
18
|
+
import { noteMutations } from "../agent-tools/advisor-async.mjs"
|
|
19
|
+
|
|
20
|
+
const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Persist a tool error to ~/.thincoder/tool-errors/YYYY-MM-DD/HHmmss-toolName.log
|
|
24
|
+
* Only called for actual execution failures and malformed invocations.
|
|
25
|
+
* Skipped for intentional denials (plan mode, user reject).
|
|
26
|
+
*/
|
|
27
|
+
export function logToolError(toolName, args, error) {
|
|
28
|
+
try {
|
|
29
|
+
const now = new Date()
|
|
30
|
+
const ymd = now.toISOString().slice(0, 10)
|
|
31
|
+
const ts = now.toISOString().replace(/:/g, "").replace(/\..+/, "").replace("T", "-")
|
|
32
|
+
const dir = join(ERRORS_DIR, ymd)
|
|
33
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
34
|
+
const file = join(dir, `${ts}-${toolName.replace(/[/\\]/g, "_")}.log`)
|
|
35
|
+
const entry = [
|
|
36
|
+
`time: ${now.toISOString()}`,
|
|
37
|
+
`tool: ${toolName}`,
|
|
38
|
+
`args: ${JSON.stringify(args, null, 2).slice(0, 2000)}${JSON.stringify(args, null, 2).length > 2000 ? "… (truncated)" : ""}`,
|
|
39
|
+
`error: ${error?.message ?? String(error)}`,
|
|
40
|
+
error?.stack ? `stack:\n${error.stack}` : "",
|
|
41
|
+
].filter(Boolean).join("\n") + "\n"
|
|
42
|
+
writeFileSync(file, entry, "utf8")
|
|
43
|
+
} catch {
|
|
44
|
+
// Log failure itself must not crash the agent
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* action-level classification (AGENT-LOOP-SUBAGENT.md §6.7 D-M1): the merged subagent
|
|
50
|
+
* tool expresses spawn (side effect) and status (read-only query) through
|
|
51
|
+
* its `action` parameter — the tool-level readonly flag can no longer express both.
|
|
52
|
+
* dispatch Phase-1/Phase-2 classifies per action: status behaves as readonly
|
|
53
|
+
* (planMode pass / no permission ask / batchable), spawn keeps its non-readonly
|
|
54
|
+
* gates, escalate runs non-readonly AND serially (the retired escalate tool had no
|
|
55
|
+
* parallel flag — zero behavior change under the merged surface).
|
|
56
|
+
* AGENT-LOOP-SUBAGENT.md §6.7.2 cancel (19.5.2b round2 #4): CONTROL-class exemption — cancel only
|
|
57
|
+
* stops, never starts. isSubagentControlAction feeds the SAME two gate sites as
|
|
58
|
+
* readonly (planMode pass / no permission ask — never joins a batch approval
|
|
59
|
+
* group / no handler → not denied — digest 内 cancel 放行).
|
|
60
|
+
* AGENT-LOOP-SUBAGENT.md §6.7.2 panel (round1 #5): view 面归只读类(同 status——planMode 放行、免
|
|
61
|
+
* 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
|
|
62
|
+
* 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
|
|
63
|
+
*/
|
|
64
|
+
export function isSubagentReadonlyAction(toolName, args) {
|
|
65
|
+
// §6 memory 工具面重构(MEMORY.md §6 D-M5):memory search/list 是只读动作——与
|
|
66
|
+
// subagent status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
|
|
67
|
+
// readonly/parallel,memory 无 parallel → 按非只读串行,见 MEMORY.md §6.4 实现注)。
|
|
68
|
+
// 动作级判定——不能按工具名(同一 memory 工具的 put/delete/clear 保持侧效门)。
|
|
69
|
+
if (toolName === "memory") {
|
|
70
|
+
const action = args?.action
|
|
71
|
+
return action === "search" || action === "list"
|
|
72
|
+
}
|
|
73
|
+
// SETTINGS-TOOL.md(2026-09-05):settings list/get 是只读动作(memory search/list 同分类——
|
|
74
|
+
// planMode 放行/免审批);set 保持侧效门。
|
|
75
|
+
if (toolName === "settings") {
|
|
76
|
+
const action = args?.action
|
|
77
|
+
return action === "list" || action === "get"
|
|
78
|
+
}
|
|
79
|
+
// #9(TOOLS.md §6.19):process 动作分级——list 保持只读分类(planMode 放行/免审批/可批并行);
|
|
80
|
+
// kill 是破坏性动作(审批门 + planMode 拒)。
|
|
81
|
+
if (toolName === "process") return args?.action !== "kill"
|
|
82
|
+
if (toolName !== "subagent" || !args || typeof args !== "object") return false
|
|
83
|
+
const action = args.action
|
|
84
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.5: check 动作已删除——只读面仅剩 status(planMode 放行/免权限审批/可批并行)
|
|
85
|
+
if (action === "status") return true
|
|
86
|
+
// SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行)
|
|
87
|
+
if (action === "observe") return true
|
|
88
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
|
|
89
|
+
if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
export function isSubagentControlAction(toolName, args) {
|
|
93
|
+
if (toolName !== "subagent") return false
|
|
94
|
+
if (args?.action === "cancel") return true
|
|
95
|
+
// SUBAGENT-OBSERVE-SEND:send = 控制类豁免(同 cancel——父回合内显式调用即授权——
|
|
96
|
+
// 写子输入队列属父对子轻量引导,非产品代码写——免审批、planMode 放行、digest 内放行)
|
|
97
|
+
if (args?.action === "send") return true
|
|
98
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 panel freeze 面(D-P3 门控在 executor——只读/控制分类在此)
|
|
99
|
+
if (args?.action === "panel" && args.freeze !== undefined && args.freeze !== null && String(args.freeze) !== "") return true
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* §2.6 token 链终消费制(2026-09-07——评审 #7d dispatch 分类):consume-design =
|
|
104
|
+
* 非只读控制动作——planMode 拒绝(不入 readonly/control 豁免——与其他非只读动作同门)、
|
|
105
|
+
* 免权限审批、不入批审批分组(无文件写——控制类直行——只停既有状态不起新副作用)。
|
|
106
|
+
* 与 cancel 的不同:cancel 是控制类豁免(planMode 放行),consume-design 按设计
|
|
107
|
+
* planMode 拒绝——故不并入 isSubagentControlAction,单独谓词只接权限豁免位。
|
|
108
|
+
*/
|
|
109
|
+
export function isSubagentConsumeDesignAction(toolName, args) {
|
|
110
|
+
return toolName === "subagent" && args?.action === "consume-design"
|
|
111
|
+
}
|
|
112
|
+
export function isSubagentEscalateAction(toolName, args) {
|
|
113
|
+
return toolName === "subagent" && args?.action === "escalate"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** E1(parity-b1 §2.5——动作谓词采纳):工具面动作谓词钩子**优先**、缺省回落核名面谓词(`??` 同式)。
|
|
117
|
+
* 端壳装饰体携钩子(`gitTool.isReadonlyAction` ∕ `memoryTool.isReadonlyAction` ∕ 子代 `vscSubagentFace`
|
|
118
|
+
* 的 `isReadonlyAction/isControlAction`)——取核 dispatch 后其动作级分类(planMode 放行 ∕ 免审批)
|
|
119
|
+
* 由此保形;核内工具零钩子 ⇒ CLI ∕ desktop 逐字零变(`undefined ?? 核谓词`)。
|
|
120
|
+
* 两谓词 = 上列两门禁位(planMode ∕ 权限短路)的公共输入(同址同用,防两处漂移)。 */
|
|
121
|
+
export function readonlyActionOf(tool, toolName, args) {
|
|
122
|
+
return tool?.isReadonlyAction?.(args) ?? isSubagentReadonlyAction(toolName, args)
|
|
123
|
+
}
|
|
124
|
+
export function controlActionOf(tool, toolName, args) {
|
|
125
|
+
return tool?.isControlAction?.(args) ?? isSubagentControlAction(toolName, args)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* fix A — 唯一记账点:FILE_MUTATORS 工具执行成功即刻记 mutation seq(abs 路径)。
|
|
130
|
+
* 取代 record-results 批后段 + agent.mjs 中断分支的 noteMutations(不双计——中断+同批
|
|
131
|
+
* launch 场景 seq 单计,T-A1i)。调用时机 = 写执行成功(非 Error 前缀
|
|
132
|
+
* 结果——recordPeerWrites 同款门);routed(M2 ACP 客户端执行)成功同样记账。
|
|
133
|
+
*/
|
|
134
|
+
export function noteExecutedMutation(agent, tool, args) {
|
|
135
|
+
// #327(TOOLS.md §6.17):单源谓词恒数组 · 恒零抛——原 try/catch 外壳退休。
|
|
136
|
+
const abs = toolTouchPaths(tool, args)
|
|
137
|
+
.filter((p) => typeof p === "string" && p)
|
|
138
|
+
.map((p) => resolve(agent.cwd, p))
|
|
139
|
+
if (abs.length > 0) noteMutations(agent, abs)
|
|
140
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/dispatch-run.mjs — 单条 prepared 项的执行体(2026-09-28 拆分批 · R3——自
|
|
3
|
+
* `agent/dispatch.mjs`(498/499 贴 500 硬限)外提:原 `executeToolCalls` 内闭包 `runOne`
|
|
4
|
+
* 提升为模块级 `runPreparedItem`(**函数体逐字搬运**——闭包自由变量经形参代入,缩进回归
|
|
5
|
+
* 一级);`executeToolCalls` 两处调用点同批改指(批并行 ∕ escalate 串行分支)。
|
|
6
|
+
* 依赖方向:本档 → `./dispatch-gates.mjs`(logToolError ∕ noteExecutedMutation);主档 → 本档
|
|
7
|
+
* ——零环。
|
|
8
|
+
*/
|
|
9
|
+
import { logEvent, errText, headText } from "../log.mjs"
|
|
10
|
+
import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
|
|
11
|
+
import { runHooks } from "../hooks.mjs"
|
|
12
|
+
import { snapshotForUndo } from "../undo-stack.mjs"
|
|
13
|
+
import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites, markClaimNoted } from "../peer-domains.mjs"
|
|
14
|
+
import { logToolError, noteExecutedMutation } from "./dispatch-gates.mjs"
|
|
15
|
+
|
|
16
|
+
/** 单条 prepared 项的执行(Phase 2 体——原 `dispatch.mjs` runOne 闭包;自由变量 agent ∕ depth ∕
|
|
17
|
+
* signal ∕ callbacks 经形参代入,语义逐字保持:console 捕获 ∕ routed 短路 ∕ mutation 记账 ∕
|
|
18
|
+
* 错误收口零改)。 */
|
|
19
|
+
export async function runPreparedItem(item, agent, depth, signal, callbacks) {
|
|
20
|
+
if (item.error) return { ...item, result: `Error: ${item.error}`, ok: false }
|
|
21
|
+
if (item.denied) {
|
|
22
|
+
const reason = item.reason === "plan mode"
|
|
23
|
+
? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
|
|
24
|
+
: item.reason === "engineering design gate"
|
|
25
|
+
? `Error: design review required before any file modification. ${item.hint}`
|
|
26
|
+
: item.reason === "d5 freeze window" || item.reason === "cross-batch record write"
|
|
27
|
+
? `Error: ${item.hint}`
|
|
28
|
+
: item.reason === "denied by user"
|
|
29
|
+
? "Error: permission denied by user"
|
|
30
|
+
: item.reason === "blocked by PreToolUse hook"
|
|
31
|
+
? "Error: blocked by PreToolUse hook"
|
|
32
|
+
: "Error: no permission handler configured — this tool requires user approval but the current context doesn't support interaction (e.g. subagent or non-TUI mode)"
|
|
33
|
+
return { ...item, result: reason, ok: false }
|
|
34
|
+
}
|
|
35
|
+
// 2026-08-31 工具顺手度(用户批准"做吧"):dispatch 拦截工具执行期间的
|
|
36
|
+
// console.log/console.error——工具的探查/调试输出(原本只到终端、模型看不到)
|
|
37
|
+
// 收集后附在工具结果后回显给模型。bash 工具的输出走子进程回显(onOutput),
|
|
38
|
+
// 不走 dispatch console——拦截安全。嵌套 dispatch(subagent)各自拦截/恢复,
|
|
39
|
+
// 捕获分离(父恢复原始后子的拦截期间父捕获停止、子恢复后父继续)——正确。
|
|
40
|
+
// 声明在 try 之外:catch 块(异常路径)也要访问(报错前的探查输出回显)。
|
|
41
|
+
const capturedConsole = []
|
|
42
|
+
// LOGGING(LOGGING.md):tool:* 事件——仅真实执行(pre-gate 拦截项在下方早退分支不入事件)。
|
|
43
|
+
// 参数值永不落盘(NF-L3——工具事件不记 args);child=子代理 id(agent._logId,spawn 时 stamp)。
|
|
44
|
+
const toolT0 = Date.now()
|
|
45
|
+
const toolName = item.toolCall.name
|
|
46
|
+
logEvent("tool:call", { tool: toolName, child: agent?._logId })
|
|
47
|
+
try {
|
|
48
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b / §4.4.4):结构化写工具执行前查冲突与认领
|
|
49
|
+
// 命中(软提示——决策⑥ A 不阻止;一次目录 stat——N3 度量);足迹累积(D-L3a——"检测+
|
|
50
|
+
// 记录一次完成")与认领登记延后到执行成功(实际写过的文件)。
|
|
51
|
+
const isPeerWriteTool = PEER_WRITE_TOOLS.has(toolName)
|
|
52
|
+
const peerNote = isPeerWriteTool ? peerCollabNote(agent, item.tool, item.args) : null
|
|
53
|
+
// Snapshot for undo before side-effect tools (setupOutputPanel already fired in Phase 1)
|
|
54
|
+
if (!item.tool?.readonly && item.args) {
|
|
55
|
+
snapshotForUndo(agent, item.toolCall.name, item.args, agent.cwd)
|
|
56
|
+
}
|
|
57
|
+
// M2 ACP: route fs tools through the client (IDE buffer / diff review).
|
|
58
|
+
// toolRouter returns { handled: true, result } to short-circuit execution.
|
|
59
|
+
if (callbacks.toolRouter) {
|
|
60
|
+
const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
|
|
61
|
+
if (routed?.handled) {
|
|
62
|
+
const routedOk = !String(routed.result).startsWith("Error:")
|
|
63
|
+
const routedResult = peerNote && routedOk ? `${routed.result}\n${peerNote.text}` : routed.result
|
|
64
|
+
if (peerNote && routedOk) markClaimNoted(agent, peerNote.keys) // 提示已附加 ⇒ 落去重标记
|
|
65
|
+
if (isPeerWriteTool && routedOk) recordPeerWrites(agent, item.tool, item.args)
|
|
66
|
+
// fix A:routed 写成功(客户端执行)同样执行期即刻记账(唯一记账点)
|
|
67
|
+
if (routedOk && FILE_MUTATORS.has(toolName)) noteExecutedMutation(agent, item.tool, item.args)
|
|
68
|
+
callbacks.onToolResult?.(item.toolCall.name, routedResult, item.toolCall.id)
|
|
69
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(routedResult, 200), child: agent?._logId })
|
|
70
|
+
return { ...item, result: routedResult, ok: true }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const origConsoleLog = console.log
|
|
74
|
+
const origConsoleErr = console.error
|
|
75
|
+
console.log = (...a) => capturedConsole.push(a.map(String).join(" "))
|
|
76
|
+
console.error = (...a) => capturedConsole.push("[err] " + a.map(String).join(" "))
|
|
77
|
+
let rawResult
|
|
78
|
+
// ctx 对象提升为变量(docs/cli/design/TUI.md §6.8):subagent 阻塞 execute 返回前在 ctx 上留
|
|
79
|
+
// _subagentKey(relayPrefix 去尾)——runOne 在 execute 返回后读它作 onToolResult
|
|
80
|
+
// 第 4 参(普通工具/错误路径无此字段——undefined 兼容既有签名)。每次工具调用
|
|
81
|
+
// 独立 ctx——并行同名工具(批并行 runOne)各自带自己的 key,互不串扰。
|
|
82
|
+
const toolCtx = {
|
|
83
|
+
cwd: agent.cwd,
|
|
84
|
+
agent,
|
|
85
|
+
depth,
|
|
86
|
+
signal,
|
|
87
|
+
callbacks,
|
|
88
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b: per-call id — the advisor tool marker-keys its launch so
|
|
89
|
+
// recordToolResults can split async-ack accounting from sync settles.
|
|
90
|
+
_toolCallId: item.toolCall.id,
|
|
91
|
+
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
|
92
|
+
onQuestion: callbacks.onQuestion,
|
|
93
|
+
onPermissionRequest: callbacks.onPermissionRequest,
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
// SUBAGENT-OBSERVE-SEND D1(评审 #1):in-flight 当前工具记账——工具执行期间在
|
|
97
|
+
// agent 上留 _inflightTools Set(子代理 observe 从 dispatch 状态读——卡在长工具
|
|
98
|
+
// 调用时 history 无新回合、恰需此信号);finally 清除。批并行工具同入 Set(observe
|
|
99
|
+
// 如实返回多个在跑工具)。主会话同样记账——无害(无人读)。
|
|
100
|
+
const inflight = agent._inflightTools ?? (agent._inflightTools = new Set())
|
|
101
|
+
inflight.add(toolName)
|
|
102
|
+
try {
|
|
103
|
+
rawResult = await item.tool.execute(item.args, toolCtx)
|
|
104
|
+
} finally {
|
|
105
|
+
inflight.delete(toolName)
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
console.log = origConsoleLog
|
|
109
|
+
console.error = origConsoleErr
|
|
110
|
+
}
|
|
111
|
+
if (rawResult === undefined) throw new Error(`Tool "${item.toolCall.name}" returned undefined — all tools must return a string value`)
|
|
112
|
+
const raw = String(rawResult)
|
|
113
|
+
// 写成功(非 "Error:" 字符串结果)→ 足迹计入本回合集合(flush 在 finalizeAgentTurn)
|
|
114
|
+
if (isPeerWriteTool && !raw.startsWith("Error:")) {
|
|
115
|
+
recordPeerWrites(agent, item.tool, item.args)
|
|
116
|
+
}
|
|
117
|
+
// fix A:FILE_MUTATORS 执行成功即刻记账(唯一记账点——取代 record-results 批后
|
|
118
|
+
// 段 + agent.mjs 中断分支——不双计)——同批 launch 前的写在 launchSeq 之前落地 →
|
|
119
|
+
// async advisor settle 不误判 stale(同批 launch 后写仍保守 stale——T-A2/T-24b9)。
|
|
120
|
+
if (FILE_MUTATORS.has(toolName) && !raw.startsWith("Error:")) {
|
|
121
|
+
noteExecutedMutation(agent, item.tool, item.args)
|
|
122
|
+
}
|
|
123
|
+
// Multimodal tools keep the raw result (base64 images ride the multimodal
|
|
124
|
+
// channel); everything else offloads oversized text to disk. Flag-driven, not
|
|
125
|
+
// name-driven (consult P3, 2026-08-30).
|
|
126
|
+
const result = item.tool?.multimodal ? raw : await offloadToolResult(raw, item.toolCall.id)
|
|
127
|
+
// 2026-08-31:工具执行期间捕获的 console 输出附在结果后回显(模型视野)
|
|
128
|
+
const resultWithConsole = capturedConsole.length > 0
|
|
129
|
+
? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
130
|
+
: result
|
|
131
|
+
// R10 L3:冲突 / 认领软提示附在工具结果末尾(模型可见——不阻止写;提示附加成功才落去重标记)
|
|
132
|
+
const notedOk = Boolean(peerNote) && !raw.startsWith("Error:")
|
|
133
|
+
if (notedOk) markClaimNoted(agent, peerNote.keys)
|
|
134
|
+
const resultForModel = notedOk
|
|
135
|
+
? `${resultWithConsole}\n${peerNote.text}`
|
|
136
|
+
: resultWithConsole
|
|
137
|
+
callbacks.onToolResult?.(item.toolCall.name, resultForModel, item.toolCall.id, toolCtx._subagentKey)
|
|
138
|
+
// PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
|
|
139
|
+
runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
|
|
140
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(resultForModel, 200), child: agent?._logId })
|
|
141
|
+
return { ...item, result: resultForModel, ok: true }
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
|
|
144
|
+
logToolError(item.toolCall.name, item.args, error)
|
|
145
|
+
// User interrupt (Ctrl+C / Ctrl+I) must propagate, not become a tool error:
|
|
146
|
+
// swallowing it here would make the parent keep looping while the user
|
|
147
|
+
// asked to stop — worst case with subagents, where the child runs its
|
|
148
|
+
// whole turn budget and the interrupt appears to do nothing.
|
|
149
|
+
if (signal?.aborted) throw error
|
|
150
|
+
// LOGGING(2026-09-03 code review #4):中止先于事件——用户停不落 tool:error
|
|
151
|
+
//(vscode execute-tools parity;阻塞子代理 child:error 同款抑制)
|
|
152
|
+
logEvent("tool:error", { tool: toolName, ms: Date.now() - toolT0, err: errText(error, 200), child: agent?._logId })
|
|
153
|
+
runHooks("PostToolUseFailure", { agent, toolName: item.toolCall.name, toolArgs: item.args, error }).catch(() => {})
|
|
154
|
+
// Build contextual error: tool name + key args — #327 same family: no raw args deref (`arguments:"null"` reachable)
|
|
155
|
+
const a = item.args ?? {}
|
|
156
|
+
const ctxParts = []
|
|
157
|
+
if (a.path) ctxParts.push(`path=${a.path}`)
|
|
158
|
+
if (a.pattern) ctxParts.push(`pattern=${a.pattern}`)
|
|
159
|
+
if (a.command) ctxParts.push(`cmd=${String(a.command).slice(0, 80)}`)
|
|
160
|
+
const ctx = ctxParts.length > 0 ? ` [${ctxParts.join(", ")}]` : ""
|
|
161
|
+
// 2026-08-31:异常路径同样回显捕获的 console(工具报错前的探查输出最有价值)
|
|
162
|
+
const consolePart = capturedConsole.length > 0
|
|
163
|
+
? `\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
164
|
+
: ""
|
|
165
|
+
return { ...item, result: `Error: ${error.message}${ctx}${consolePart}`, ok: false }
|
|
166
|
+
}
|
|
167
|
+
}
|