@thincoder/core 0.9.1
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/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
|
@@ -0,0 +1,174 @@
|
|
|
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 — 轮次仅作提示词衰减与显示,**无机械上限**). §15 F16 (2026-09-11 第 13 批): the SYNC
|
|
20
|
+
* called-mark is gated by the single incomplete-tail predicate
|
|
21
|
+
* (advisorIncompleteMarker) — a truncated-then-settled CODE review must not
|
|
22
|
+
* count as covered (the guard re-pushes); design reviews keep the mark
|
|
23
|
+
* (no code face — settle-side `failureVerdict` parity).
|
|
24
|
+
* - Mutations feed _touchedFiles and fire-and-forget memory reindex.
|
|
25
|
+
*/
|
|
26
|
+
import { pushReal } from "../context.mjs"
|
|
27
|
+
import { specForModel } from "../config.mjs"
|
|
28
|
+
import { FILE_MUTATORS } from "./helpers.mjs"
|
|
29
|
+
import { resolve } from "node:path"
|
|
30
|
+
import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.mjs"
|
|
31
|
+
import { looksLikeReviewOutput, advisorIncompleteMarker } from "../advisor/run.mjs"
|
|
32
|
+
|
|
33
|
+
let _reindexFile = null
|
|
34
|
+
|
|
35
|
+
export async function recordToolResults(agent, toolByName, results) {
|
|
36
|
+
// Multimodal user messages (injected images / not-injected reminders) must NOT be pushed
|
|
37
|
+
// between tool results of parallel calls — strict providers (DeepSeek) 400 when a tool
|
|
38
|
+
// message does not immediately follow its assistant tool_calls. Defer to after the loop.
|
|
39
|
+
// real: image injections are real messages (pushReal → _fullHistory); reminders stay machine-only.
|
|
40
|
+
const deferredUserMsgs = []
|
|
41
|
+
|
|
42
|
+
for (const { toolCall, result, ok } of results) {
|
|
43
|
+
const tool = toolByName.get(toolCall.name)
|
|
44
|
+
// Multimodal tools return JSON { text, images } — inject as multimodal user message
|
|
45
|
+
if (tool?.multimodal && ok) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(result)
|
|
48
|
+
if (parsed.images?.length) {
|
|
49
|
+
// tool message first — closes the tool_call pairing (OpenAI API requires tool result immediately after assistant with tool_calls)
|
|
50
|
+
pushReal(agent, { role: "tool", tool_call_id: toolCall.id, name: toolCall.name, content: parsed.text })
|
|
51
|
+
if (specForModel(agent.provider.model).multimodal) {
|
|
52
|
+
// then inject multimodal user message with base64 images for the model to actually "see" them on the next turn
|
|
53
|
+
deferredUserMsgs.push({
|
|
54
|
+
real: true,
|
|
55
|
+
msg: {
|
|
56
|
+
role: "user",
|
|
57
|
+
content: [{ type: "text", text: parsed.text }, ...parsed.images],
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
} else {
|
|
61
|
+
// Non-vision model: image parts must never enter history — text-only APIs 400 on them on EVERY
|
|
62
|
+
// subsequent request, poisoning the conversation. (read_image itself already refuses; this is defense-in-depth.)
|
|
63
|
+
deferredUserMsgs.push({
|
|
64
|
+
real: false,
|
|
65
|
+
msg: {
|
|
66
|
+
role: "user",
|
|
67
|
+
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.]`,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
} catch { /* Parse failure doesn't affect normal tool messages */ }
|
|
74
|
+
}
|
|
75
|
+
pushReal(agent, { role: "tool", tool_call_id: toolCall.id, name: toolCall.name, content: result })
|
|
76
|
+
if (tool && ok) {
|
|
77
|
+
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
78
|
+
// Direct file edit — code was changed. The prior advisor review and
|
|
79
|
+
// verify are stale: a review that ran before the edit no longer
|
|
80
|
+
// covers the current file state.
|
|
81
|
+
// §29 fix A(AGENT-LOOP.md §29——2026-09-07):mutation-seq 记账已移到 dispatch
|
|
82
|
+
// runOne 执行成功即刻(唯一记账点——取代本批后段 + agent.mjs 中断分支——不双计)——
|
|
83
|
+
// 此处仅剩 guard 标志失效(顺序语义:批内同消息的 sync advisor 提交仍在其后置位)。
|
|
84
|
+
agent._mutatedThisRun = true
|
|
85
|
+
agent._calledAdvisorThisRun = false
|
|
86
|
+
agent._verifiedThisRun = false
|
|
87
|
+
agent._verifyPassed = undefined
|
|
88
|
+
} else if (!tool.readonly && !tool.sideEffectExempt) {
|
|
89
|
+
// Non-mutating side-effect tools (bash, git): do NOT invalidate the
|
|
90
|
+
// advisor review — a review is triggered by CODE MUTATIONS only
|
|
91
|
+
// (user decision 2026-08-08: the guard rule is "review after code
|
|
92
|
+
// changes", not "review after any environment change"; bash is
|
|
93
|
+
// barred from writing files, so it cannot change the reviewed code).
|
|
94
|
+
// Verify IS invalidated: its state snapshot (git diff, file list)
|
|
95
|
+
// may be stale after git/shell operations.
|
|
96
|
+
if (agent._verifiedThisRun) {
|
|
97
|
+
agent._verifiedThisRun = false
|
|
98
|
+
agent._verifyPassed = undefined
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (toolCall.name === "verify") agent._verifiedThisRun = true
|
|
102
|
+
if (toolCall.name === "advisor") {
|
|
103
|
+
// §11.2 D-24b (settle accounting split — fix #2): an ASYNC launch returns an
|
|
104
|
+
// ack and settles later — the settle callback owns its called/round/token
|
|
105
|
+
// accounting. Only the SYNC path (depth>0 / explicit async:false) accounts
|
|
106
|
+
// here — per-review instance round++ (marker-keyed by tool call id) + the
|
|
107
|
+
// legacy mirror._advisorRound_ counter stays for display/back-compat.
|
|
108
|
+
// REFUSED launches (type gate / scope guard / same-scope in flight / launch
|
|
109
|
+
// refusal) count as neither a call nor a completion: no called-mark (the guard
|
|
110
|
+
// must keep pushing until a review really runs), no round advance.
|
|
111
|
+
const refused = agent._advisorRefusals?.has(toolCall.id)
|
|
112
|
+
const asyncAck = agent._advisorAsyncAcks?.has(toolCall.id)
|
|
113
|
+
if (refused) {
|
|
114
|
+
agent._advisorRefusals.delete(toolCall.id)
|
|
115
|
+
} else if (asyncAck) {
|
|
116
|
+
agent._advisorAsyncAcks.delete(toolCall.id)
|
|
117
|
+
} else {
|
|
118
|
+
const reviewId = agent._advisorSyncCalls?.get(toolCall.id)
|
|
119
|
+
const run = reviewId !== undefined ? advisorRuns(agent).get(reviewId) : undefined
|
|
120
|
+
// F16 同步面(第 13 批 §15.2——ADVISOR-CONVERGENCE.md §15):未完成尾 + 非设计面
|
|
121
|
+
// (含类型不可判——run 缺失的 legacy 直调)⇒ 不置「已覆盖」(与 settle 面
|
|
122
|
+
// 逐条 parity;设计评审保持计入——无代码面)。
|
|
123
|
+
const incomplete = advisorIncompleteMarker(String(result))
|
|
124
|
+
if (!(incomplete && run?.reviewType !== "design")) agent._calledAdvisorThisRun = true
|
|
125
|
+
if (reviewId !== undefined) {
|
|
126
|
+
if (run) {
|
|
127
|
+
run.round++
|
|
128
|
+
agent._advisorRound = run.round
|
|
129
|
+
// Prior of round 2+ = the last REVIEW-LOOKING output (run.mjs parity).
|
|
130
|
+
// F2e (§29.1): the sync settle's engine-approved suffix (stored on the
|
|
131
|
+
// run by settleDesignReview) is stripped with exact truncation — the
|
|
132
|
+
// prior never carries the raw token / designId.
|
|
133
|
+
if (looksLikeReviewOutput(result)) {
|
|
134
|
+
run.priorOutput = stripApprovedSuffix(result, run.approvedSuffix)
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
agent._advisorRound++
|
|
138
|
+
}
|
|
139
|
+
agent._advisorSyncCalls.delete(toolCall.id)
|
|
140
|
+
} else {
|
|
141
|
+
// Direct/legacy callers without a resolution marker — plain mirror
|
|
142
|
+
// increment (the per-review registry never saw this call).
|
|
143
|
+
agent._advisorRound++
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
148
|
+
const args = JSON.parse(toolCall.arguments)
|
|
149
|
+
const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
|
|
150
|
+
for (const p of paths) {
|
|
151
|
+
const abs = resolve(agent.cwd, p)
|
|
152
|
+
if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
|
|
153
|
+
if (agent.memory) {
|
|
154
|
+
// Fire-and-forget: don't block the agent loop on indexing.
|
|
155
|
+
// Reuses a single cached import; errors surface as pending reminders on next turn.
|
|
156
|
+
if (!_reindexFile) {
|
|
157
|
+
const mod = await import("../memory.mjs")
|
|
158
|
+
_reindexFile = mod.reindexFile
|
|
159
|
+
}
|
|
160
|
+
_reindexFile(agent.memory, agent.cwd, abs).catch((e) => {
|
|
161
|
+
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.]`)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// All tool results committed — now safe to inject deferred multimodal user messages
|
|
170
|
+
for (const { real, msg } of deferredUserMsgs) {
|
|
171
|
+
if (real) pushReal(agent, msg)
|
|
172
|
+
else agent.history.push(msg)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* relay-prefix.mjs — relay 前缀(`role#id/`)文法**单一权威**(第 27 批 §12.3①——R-A2.3)。
|
|
3
|
+
*
|
|
4
|
+
* 零依赖(不 import 任何模块)——TUI(`src/tui/`)与 ACP(`src/acp/`)双向可导入,无环;
|
|
5
|
+
* 生成侧 `src/agent/spawn-child.mjs` 再导出(枢纽)。文法 = 自
|
|
6
|
+
* `src/tui/subagent-blocks.mjs` 逐字迁入(第 27 批 §12.3②——零语义改动)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
|
|
10
|
+
export const RELAY_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
11
|
+
|
|
12
|
+
/** 嵌套 relay 前缀通用解析(循环解析任意深度——显示契约 docs/design/TUI.md §6):
|
|
13
|
+
* `eng-coder#2/explore#1/read` → { head(块路由), inner[], label(inner 链——R23:
|
|
14
|
+
* 子块折叠键/外层 currentTool 全路径用), rest }。
|
|
15
|
+
* 单层 = inner[]/label ""——与既有单段匹配语义零改;无前缀 → null。 */
|
|
16
|
+
export function parseRelayPath(text) {
|
|
17
|
+
const segments = []
|
|
18
|
+
let rest = String(text)
|
|
19
|
+
for (;;) {
|
|
20
|
+
const m = rest.match(RELAY_PREFIX_RE)
|
|
21
|
+
if (!m) break
|
|
22
|
+
segments.push(`${m[1]}#${m[2]}`)
|
|
23
|
+
rest = rest.slice(m[0].length)
|
|
24
|
+
}
|
|
25
|
+
if (segments.length === 0) return null
|
|
26
|
+
return {
|
|
27
|
+
head: segments[0],
|
|
28
|
+
inner: segments.slice(1),
|
|
29
|
+
label: segments.slice(1).join("/"),
|
|
30
|
+
rest,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 构造向单一实现(本批统一的两处构造点——`makeRelay` 与 async 取号分支共用;
|
|
35
|
+
* 另两处字面构造(escalate/advisor async)不在本批声明面——批次档 §2 修正轮登记,如需收敛另批):
|
|
36
|
+
* `relayPrefixOf("coder", 3)` → `"coder#3/"`。 */
|
|
37
|
+
export function relayPrefixOf(label, id) {
|
|
38
|
+
return `${label}#${id}/`
|
|
39
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-stages.mjs — runAgent 主循环的阶段函数族(CLI,2026-09-05 实践轮:agent.mjs
|
|
3
|
+
* runAgent 383 ≥300 按骨干—细节两层提取——VS Code 端同构文件(双端 parity——拆分
|
|
4
|
+
* 两端同步))。本文件承载:回合压缩检查(runCompactionCheck)、回合注入组
|
|
5
|
+
* (injectTurnReminders——plan cadence + eng 状态)、回合收尾(finalizeAgentTurn——
|
|
6
|
+
* consult 清理/async 池分流/guard 继承)与其收集器(collectSettledAsync——自
|
|
7
|
+
* agent.mjs 迁入,agent.mjs 内无其他引用)。verbatim 迁移 + 签名化,语义零变。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
|
|
11
|
+
import { ensureAutoReminder, injectEngineeringReminder, ContinueError, snapshotGuard } from "./helpers.mjs"
|
|
12
|
+
import { pushManifestStateReminder } from "./setup-reminders.mjs"
|
|
13
|
+
import { cleanupConsultSessions } from "../agent-tools/consult.mjs"
|
|
14
|
+
import { logEvent } from "../log.mjs"
|
|
15
|
+
// ASYNC-RESULT-CONTAINER.md D1:池 accessor(absorb 双池——advisor 独立池无队列)
|
|
16
|
+
import { getAsyncPool, releaseSettledEntry } from "../agent-tools/async-settle.mjs"
|
|
17
|
+
// 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-SUBAGENT.md §6.20):中止分支「只清已死」收尾单点
|
|
18
|
+
import { discardAbortedPool, discardAbortedAdvisors } from "../agent-tools/async-discard.mjs"
|
|
19
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末域登记 flush(写工具钩子累积 →
|
|
20
|
+
// 整写一次本实例 peers 文件——无写入跳过;失败容忍不抛)
|
|
21
|
+
import { flushPeerDomains } from "../peer-domains.mjs"
|
|
22
|
+
// 第 30 批 D1(Stop 钩子——AGENT-LOOP.md §21):主会话 run 终止事件(触发块见 finalizeAgentTurn)
|
|
23
|
+
import { runHooks } from "../hooks.mjs"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 响应后置提醒注入(2026-09-05 实践轮——自 runAgent 响应处理链提取,verbatim):
|
|
27
|
+
* 流规则 warning(warn 动作——流已完成——去重注入,模型下轮可见)+ 异常 finish
|
|
28
|
+
* reason 警告(响应可能不完整/截断——reasonMap 人类化描述注入)。
|
|
29
|
+
*/
|
|
30
|
+
export function injectResponseReminders(agent, response) {
|
|
31
|
+
// Stream rule warnings (action: "warn"): stream completed; inject de-duplicated
|
|
32
|
+
// warnings so the model sees them before its next response.
|
|
33
|
+
if (response._warnings?.length) {
|
|
34
|
+
const deDuplicated = [...new Map(response._warnings.map(w => [w.name || w.pattern, w])).values()]
|
|
35
|
+
agent.history.push({
|
|
36
|
+
role: "user",
|
|
37
|
+
content: `[System reminder — stream rule warnings from your last response:\n${deDuplicated.map(w => `- ${w.name || w.pattern}: ${w.message}`).join("\n")}]`,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Warn on abnormal finish reasons — the response may be incomplete/truncated.
|
|
42
|
+
if (response.finishReason && response.finishReason !== "stop" && response.finishReason !== "tool_calls") {
|
|
43
|
+
const reasonMap = {
|
|
44
|
+
length: "output token limit reached after exhausting continuations",
|
|
45
|
+
insufficient_system_resource: "provider inference resources exhausted — consider retrying or switching models",
|
|
46
|
+
content_filter: "response blocked by provider content filtering",
|
|
47
|
+
}
|
|
48
|
+
const detail = reasonMap[response.finishReason] || `unknown reason "${response.finishReason}"`
|
|
49
|
+
agent.history.push({
|
|
50
|
+
role: "user",
|
|
51
|
+
content: `[System reminder: the previous turn ended abnormally — ${detail}. The assistant response that follows may be incomplete.]`,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 回合压缩检查(2026-09-05 实践轮——自 runAgent 主循环提取):仅安全点(history 以
|
|
58
|
+
* 完整交换结尾——user/tool 位)。成功:失败计数清零/计划提醒 cadence 复位/停滞检测
|
|
59
|
+
* 计数复位(recentCallSigs——runAgent 回合级局部——经 ctx 传对象引用)/压缩完成事件
|
|
60
|
+
* /AUTO 提醒重注入。失败:AbortError 透传;计数 + Q3 onCompressFail;
|
|
61
|
+
* COMPRESS_FAILURE_LIMIT 连续失败降级 compressFallback(确定性截断——不发 LLM)。
|
|
62
|
+
*/
|
|
63
|
+
export async function runCompactionCheck(agent, ctx) {
|
|
64
|
+
const { threshold, callbacks, compactionOverhead, signal, recentCallSigs } = ctx
|
|
65
|
+
try {
|
|
66
|
+
if (await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)) {
|
|
67
|
+
agent._compressFailures = 0
|
|
68
|
+
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
69
|
+
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
70
|
+
// Completion info (CONTEXT-COMPACTION §7 D-C2): { mode, tokensFreed, elapsedMs } —
|
|
71
|
+
// the TUI panel renders it; callers that ignore the arg keep prior onCompress semantics.
|
|
72
|
+
callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
73
|
+
ensureAutoReminder(agent)
|
|
74
|
+
}
|
|
75
|
+
} catch (compressError) {
|
|
76
|
+
// AbortError must not be swallowed: user cancellation must propagate
|
|
77
|
+
if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
|
|
78
|
+
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
79
|
+
// Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
|
|
80
|
+
// COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
|
|
81
|
+
callbacks?.onCompressFail?.(compressError)
|
|
82
|
+
if (agent._compressFailures >= COMPRESS_FAILURE_LIMIT) {
|
|
83
|
+
agent._compressFailures = 0
|
|
84
|
+
if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 回合注入组(2026-09-05 实践轮——自 runAgent 主循环提取):plan-mode 提醒 cadence
|
|
91
|
+
* (稀疏 2 轮/满 5 轮/新用户消息重置——限制不淡化)+ 工程模式状态注入(depth-0 每新
|
|
92
|
+
* 用户消息一次——见 injectEngineeringReminder)。verbatim——语义零变。
|
|
93
|
+
*/
|
|
94
|
+
export async function injectTurnReminders(agent, ctx) {
|
|
95
|
+
const { depth } = ctx
|
|
96
|
+
// Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
|
|
97
|
+
// (sparse every 2 turns, full every 5 / on new user message) so the restriction never fades.
|
|
98
|
+
if (agent.planMode) {
|
|
99
|
+
const lastMsg = agent.history.at(-1)
|
|
100
|
+
const realUserMsg = lastMsg?.role === "user"
|
|
101
|
+
&& typeof lastMsg.content === "string"
|
|
102
|
+
&& !lastMsg.content.startsWith("[System reminder:")
|
|
103
|
+
&& !lastMsg.content.startsWith("[User interrupt:")
|
|
104
|
+
const newUserSince = realUserMsg && agent.history.length > (agent._planReminderAtLen ?? 0)
|
|
105
|
+
const { planReminderForTurn } = await import("../agent-tools/plan.mjs")
|
|
106
|
+
const reminder = planReminderForTurn(agent, newUserSince)
|
|
107
|
+
if (reminder) {
|
|
108
|
+
agent._planReminderAtLen = agent.history.length + 1
|
|
109
|
+
agent.history.push({ role: "user", content: reminder, transient: true })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Engineering-mode status injection on every new user message (design-before-code
|
|
114
|
+
// vs standard discipline) — see injectEngineeringReminder.
|
|
115
|
+
if (depth === 0) {
|
|
116
|
+
injectEngineeringReminder(agent)
|
|
117
|
+
pushManifestStateReminder(agent, { depth }) // M1 情境行(MANIFEST.md §2.6——同序组,eng 之后)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 回合收尾(2026-09-05 实践轮——自 runAgent finally 提取,verbatim + 签名化):写足迹
|
|
123
|
+
* flush → Stop 钩子(第 30 批 D1——ctx.depth === 0 的主会话 run 终止事件,
|
|
124
|
+
* fire-and-forget)→ consult 清理(无孤儿烧 token 的 consult children)→ async 池回合尾
|
|
125
|
+
* 分流(Ctrl+C 清池不注入 / ContinueError 留池续跑 / 其余 collectSettledAsync)→ auto-turn
|
|
126
|
+
* guard 标记继承(对象字段清单——正常结束才继承;中止丢弃;ContinueError 由续跑快照)。
|
|
127
|
+
*/
|
|
128
|
+
export async function finalizeAgentTurn(agent, ctx) {
|
|
129
|
+
const { signal, autoTurn, suspDriven, thrownError, depth } = ctx
|
|
130
|
+
// R10 L3(MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末登记 flush——本回合写足迹整写
|
|
131
|
+
// 一次(首行执行:收尾链后续任何异常都不吞登记);无写入 → 跳过(hot 窗口自然衰减)。
|
|
132
|
+
flushPeerDomains(agent)
|
|
133
|
+
// 第 30 批 D1(Stop 钩子):主会话 run 终止 → fire-and-forget(非阻塞;失败静默——与 PostToolUse 同语义)。
|
|
134
|
+
// 排除用户中止(Ctrl+C / Ctrl+I——中止后 TUI 重建 controller 续跑或已显式停回合)与 AbortError 展开。
|
|
135
|
+
if (depth === 0 && !signal?.aborted && thrownError?.name !== "AbortError") {
|
|
136
|
+
runHooks("Stop", {
|
|
137
|
+
agent,
|
|
138
|
+
error: thrownError && !(thrownError instanceof ContinueError) ? thrownError : undefined,
|
|
139
|
+
extra: {
|
|
140
|
+
turn: agent._currentTurn ?? 0,
|
|
141
|
+
reason: thrownError instanceof ContinueError ? "maxTurns" : thrownError ? "error" : "done",
|
|
142
|
+
},
|
|
143
|
+
}).catch(() => {})
|
|
144
|
+
}
|
|
145
|
+
// R17(AGENT-LOOP.md §25 D-R17a): consultation sessions are cross-turn
|
|
146
|
+
// background work now — NO unconditional turn-end cleanup (the old rule aborted
|
|
147
|
+
// leftover consult children at every turn end because check-loop consumption was
|
|
148
|
+
// turn-scoped). Sessions stay alive across normal turn ends (and ContinueError
|
|
149
|
+
// resumes); the Ctrl+C abort branch below is the only path that aborts them
|
|
150
|
+
// (cleanupConsultSessions — marked stopped → no digest), and the suspension
|
|
151
|
+
// driver aborts them on its own abort unwind.
|
|
152
|
+
// Async subagent turn-end handling (AGENT-LOOP.md §15 D-A3 + §17 D-S1). Lifecycle:
|
|
153
|
+
// - Ctrl+C (plain abort): children were aborted with the parent signal — discard dead
|
|
154
|
+
// entries (tombstone/out-of-pool/notice — AGENT-LOOP-SUBAGENT.md §6.20; no stale
|
|
155
|
+
// errors injected — user explicitly stopped); consultation sessions are cross-turn
|
|
156
|
+
// background work since R17 — the abort branch is the ONLY normal-path place that
|
|
157
|
+
// aborts them (cleanupConsultSessions marks stopped → they never reach the digest).
|
|
158
|
+
// - Ctrl+I (interrupt): keeps the pool AND the consult sessions: the turn
|
|
159
|
+
// resumes with the interrupt message, children stay tracked (in a suspension
|
|
160
|
+
// session children hold agent._sessionSignal and a digest's own Ctrl+I must
|
|
161
|
+
// not orphan them).
|
|
162
|
+
// - ContinueError (turn cap): no wait, no injection — children keep running and
|
|
163
|
+
// the RESUME run's turn-end collection takes over.
|
|
164
|
+
// - anything else: inject the SETTLED entries only; running/queued stay in the
|
|
165
|
+
// pool for the suspension session (D-S1 — no allSettled turn-end wait).
|
|
166
|
+
if (signal?.aborted && !signal?.reason?.interrupt) {
|
|
167
|
+
const subPool = getAsyncPool(agent, "subagent")
|
|
168
|
+
const advPool = getAsyncPool(agent, "advisor")
|
|
169
|
+
if ((subPool?.size ?? 0) > 0 || (advPool?.size ?? 0) > 0) {
|
|
170
|
+
logEvent("ev:stopped", { poolN: (subPool?.size ?? 0) + (advPool?.size ?? 0), where: "turn-end-abort" })
|
|
171
|
+
}
|
|
172
|
+
// §6.20:只清已死条目(存活/已 settle 留池——收尾见 async-discard.mjs;原无条件清池已弃)。
|
|
173
|
+
discardAbortedPool(agent)
|
|
174
|
+
discardAbortedAdvisors(agent)
|
|
175
|
+
// R17: the consult family dies with the user stop (marked stopped — no digest).
|
|
176
|
+
cleanupConsultSessions(agent)
|
|
177
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器——中止丢弃 consult/escalate 族停靠
|
|
178
|
+
// (原 _pendingConsultResults/_pendingEscalateResults 同口径——VSC 同语义 filter);
|
|
179
|
+
// subagent/advisor 停靠保留(挂起期 settle 的已完成结果不随中止丢)。
|
|
180
|
+
agent._pendingAsyncResults = (agent._pendingAsyncResults ?? []).filter((e) => e.role !== "consult" && e.role !== "escalate")
|
|
181
|
+
} else if (thrownError instanceof ContinueError) {
|
|
182
|
+
// keep _asyncSubagents/_asyncAdvisors/_consultSessions — the resumed run continues them
|
|
183
|
+
} else {
|
|
184
|
+
const injectedAdvisor = await collectSettledAsync(agent, { suspDriven })
|
|
185
|
+
// §11.2 D-24b: a normally-ended USER run with no review/child activity closes
|
|
186
|
+
// the OPEN code instances — the converged/abandoned thread must not burn the
|
|
187
|
+
// 5-round cap of a later task (the next code review starts a fresh round 1).
|
|
188
|
+
// Digest auto-turns are exempt (their disposition precedes the fix round).
|
|
189
|
+
// A run whose turn-end collection just INJECTED a settled review report
|
|
190
|
+
// (headless, suspDriven=false) must NOT close: the model has not digested
|
|
191
|
+
// the findings yet — the fix round that follows still continues the thread.
|
|
192
|
+
if (!autoTurn && !injectedAdvisor) {
|
|
193
|
+
const { closeOpenCodeAdvisorRuns } = await import("../agent-tools/advisor-async.mjs")
|
|
194
|
+
closeOpenCodeAdvisorRuns(agent)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
agent._inAutoTurn = false
|
|
198
|
+
// §17 D-S6: auto-turn guard marks survive into the next USER run (restored at its
|
|
199
|
+
// !resume reset above). Normal ends only — abort discards; ContinueError lets the
|
|
200
|
+
// auto-resumed run snapshot at its own end.
|
|
201
|
+
if (autoTurn && !(signal?.aborted && !signal?.reason?.interrupt) && !(thrownError instanceof ContinueError)) {
|
|
202
|
+
// 键清单/快照形态单源 = helpers.mjs(P2 机制层端差批 §2.18——端侧经同一 helper 取同清单)
|
|
203
|
+
agent._inheritedGuard = snapshotGuard(agent)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Turn-end async subagent collection (AGENT-LOOP.md §17 D-S1 + §17.5 supersede):
|
|
209
|
+
* two modes, selected by the caller's driver context (17.5.2/17.5.4 #2):
|
|
210
|
+
* - suspDriven=false (fallback — headless/direct runAgent callers without a
|
|
211
|
+
* suspension driver): inject every entry that SETTLED during this run
|
|
212
|
+
* (XML-escaped report/error — child reports may carry file/webpage content;
|
|
213
|
+
* >64K offloaded with preview + path) and remove it from the pool. Running/
|
|
214
|
+
* queued STAY — no allSettled wait. Results never lost without a session.
|
|
215
|
+
* - suspDriven=true (the interaction layer runs suspensionSession after this
|
|
216
|
+
* run): NO direct inject — settled entries STAY pooled (settled not consumed)
|
|
217
|
+
* for the session's first sweepSettledToPending → digest turn (§17.5 — done
|
|
218
|
+
* 条目留池等消化轮注入;status 在 sweep 前仍从池读——17.5.2 不变面(§19.8:check 已删——自动通道为唯一消费方))。
|
|
219
|
+
* maybeRefillAsync runs in both modes (starts queued heads whose slot freed).
|
|
220
|
+
* Single ownership: entries settled inside a suspension session were moved to
|
|
221
|
+
* _pendingAsyncResults by the settle callback, so this only sees user-turn
|
|
222
|
+
* settles (no double inject — D-S3 points ①/②). The ⟦ev⟧done freeze is NOT
|
|
223
|
+
* emitted here — each settle callback emits it (§15 D-A3).
|
|
224
|
+
* 2026-09-05 实践轮:自 agent.mjs 迁入(agent.mjs 内仅 finalize 引用——随收尾同迁)。
|
|
225
|
+
*/
|
|
226
|
+
async function collectSettledAsync(agent, { suspDriven = false } = {}) {
|
|
227
|
+
const maps = [getAsyncPool(agent, "subagent"), getAsyncPool(agent, "advisor")].filter((m) => m instanceof Map && m.size > 0)
|
|
228
|
+
if (maps.length === 0) return false
|
|
229
|
+
const { maybeRefillAsync, injectAsyncResult } = await import("../agent-tools/subagent.mjs")
|
|
230
|
+
maybeRefillAsync(agent) // start queued heads now that slots may have freed — no waiting
|
|
231
|
+
if (suspDriven) return false // §17.5: settled stays pooled — the suspension session digests it
|
|
232
|
+
let injectedAdvisor = false
|
|
233
|
+
for (const map of maps) {
|
|
234
|
+
for (const e of [...map.values()]) {
|
|
235
|
+
if (!e.done) continue // still running — stays in the pool (D-S1)
|
|
236
|
+
await injectAsyncResult(agent, e)
|
|
237
|
+
if (e.role === "advisor") injectedAdvisor = true
|
|
238
|
+
// TUI-OOM-ROOTCAUSE(§23.3.1 消费点①——回合尾收集):注入完成 → 释放条目持有
|
|
239
|
+
releaseSettledEntry(e)
|
|
240
|
+
map.delete(String(e.id))
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return injectedAdvisor
|
|
244
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/setup-reminders.mjs — per-turn transient reminders
|
|
3
|
+
* (SESSION.md §6.11 — 2026-09-06 需求池 R5/R8/R9/R11 合并设计; R10 L1 peer 注入同文件).
|
|
4
|
+
*
|
|
5
|
+
* One unified transient user reminder per turn covers the whole self-awareness
|
|
6
|
+
* family: env identity (R8 — §6.10 D-1 END 常量先例:静态常量,不做 cmdline 判别),
|
|
7
|
+
* engineering mode (R9), active model (R11), session slot (F1), restart awareness
|
|
8
|
+
* (R5 resumed). §6.11(2026-09-08):resumed 按会话跟踪——恢复事件由 session.mjs
|
|
9
|
+
* applySession 武装(agent._envResumed——载入历史非空 = 恢复);进程重启句独立走
|
|
10
|
+
* agent._processRestartPending(bin 启动 resume 路径设)——双信号互不绑门(N6)。
|
|
11
|
+
* Change awareness needs no dedicated injection — every turn carries the
|
|
12
|
+
* CURRENT state, so a mode/model flip shows up in the next turn's line.
|
|
13
|
+
* git is NOT a field here (§6.11: CLI 不重复注入 clean|dirty 摘要) — the rich
|
|
14
|
+
* git-context injection (branch/commits/uncommitted — helpers.mjs
|
|
15
|
+
* collectGitContext, wired in setup.mjs) carries it.
|
|
16
|
+
* Peer awareness (R10 L1) rides the same per-turn transient channel:
|
|
17
|
+
* pushPeerReminder injects the live co-cwd instance list when present.
|
|
18
|
+
* Manifest state rides it too (#28 — `docs/core/design/MANIFEST.md` §2.6):
|
|
19
|
+
* pushManifestStateReminder carries the project phase as a self-healing single live line.
|
|
20
|
+
* #34 adds the per-turn value refresh (mtime-gated re-read — §2.6 ③b / KD-M1-17..M1-19):
|
|
21
|
+
* the line always carries the CURRENT on-disk phase, not the assembly-time snapshot.
|
|
22
|
+
*
|
|
23
|
+
* #113(S1 续轮第二批——VSC 侧并入 ④ 段):`pushInjections`(机器行专用注入——编辑器上下文
|
|
24
|
+
* 等端侧采集内容;同文去重)与 `appendImagePointer`(粘贴图指引——非多模态模型可见报错)
|
|
25
|
+
* 两档并入——内容由端采集 / 传入(核内零端名),核供注入纪律单点。
|
|
26
|
+
*/
|
|
27
|
+
import { statSync } from "node:fs"
|
|
28
|
+
import { END } from "../session-slots.mjs"
|
|
29
|
+
import { peerInstances } from "../peer-instances.mjs"
|
|
30
|
+
import { specForModel } from "../config.mjs"
|
|
31
|
+
import { manifestFilePath, readManifest } from "../manifest.mjs"
|
|
32
|
+
|
|
33
|
+
/** env-state line builder — pure, unit-testable.
|
|
34
|
+
* §6.11(F1):slot 字段入行——位置在 model 后 resumed 前(N3:无绑定 → 显式 null——
|
|
35
|
+
* 不读 manifest active 共享指针——粘性 _slot 才是"本 agent 之槽")。 */
|
|
36
|
+
export function envStateLine({ mode, model, slot, resumed }) {
|
|
37
|
+
return `[System reminder: env: ${END}, mode: ${mode}, model: ${model}, slot: ${slot}, resumed: ${resumed ? "yes" : "no"}.]`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Push the per-turn env-state reminder (setup.mjs calls it for depth-0 runs —
|
|
42
|
+
* the line describes the MAIN agent's host/mode/model/slot identity). resumed=yes
|
|
43
|
+
* exactly once per session restore: applySession (session.mjs) arms
|
|
44
|
+
* agent._envResumed when restored history is non-empty; consumed here so every
|
|
45
|
+
* later turn reads resumed=no(§6.11 F2——每次恢复一次;/new 与空历史槽切换不武装——
|
|
46
|
+
* F3 伪触发消除)。slot = 粘性 agent._slot(§6.11 N2/N3——无绑定显式 null)。
|
|
47
|
+
* Degrades safely: activeModel null → provider.model → "unknown" (T-E12);
|
|
48
|
+
* a missing config/history never throws (T-E13).
|
|
49
|
+
*/
|
|
50
|
+
export function pushEnvStateReminder(agent) {
|
|
51
|
+
const mode = agent.config?.agent?.engineering ? "eng" : "normal"
|
|
52
|
+
const model = agent.activeModel ?? agent.provider?.model ?? "unknown"
|
|
53
|
+
const slot = agent._slot ?? null
|
|
54
|
+
const resumed = agent._envResumed === true
|
|
55
|
+
agent._envResumed = false
|
|
56
|
+
agent.history.push({ role: "user", content: envStateLine({ mode, model, slot, resumed }), transient: true })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 情境行前缀——本行族识别符(摘旧行 / 会话重建认领同用;行形逐字见 manifestStateLine)。 */
|
|
60
|
+
const MANIFEST_LINE_PREFIX = "[System reminder: project state: "
|
|
61
|
+
|
|
62
|
+
/** phase → discipline 标签映射(判据单源 = 需求 v2 §9.1);未知值不在表内 → 无标签。 */
|
|
63
|
+
const MANIFEST_DISCIPLINE = Object.freeze({ "initial-dev": "light", production: "strict" })
|
|
64
|
+
|
|
65
|
+
/** 情境行构造 — pure, unit-testable(模块契约 `docs/core/design/MANIFEST.md` §2.6,与
|
|
66
|
+
* envStateLine 同族)。行形逐字(**单字段**——2026-09-17 裁撤批后):
|
|
67
|
+
* `[System reminder: project state: phase: <值> (discipline: <light|strict>).]`
|
|
68
|
+
* 未知 phase → 只出值、不编判据(无标签)。 */
|
|
69
|
+
export function manifestStateLine({ phase }) {
|
|
70
|
+
const discipline = MANIFEST_DISCIPLINE[phase]
|
|
71
|
+
return `${MANIFEST_LINE_PREFIX}phase: ${phase}${discipline ? ` (discipline: ${discipline})` : ""}.]`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ③b 取值前置步(#34——`docs/core/design/MANIFEST.md` §2.6 条 3 · KD-M1-17–M1-19):数据档
|
|
76
|
+
* mtime 门控重读——盘上 mtime ≠ `agent._manifestMtime`(含缓存未设 = 首次观测)⇒ `readManifest`
|
|
77
|
+
* 重读并采纳(`agent.manifest` ← 新值 + 缓存 ← 观测值);mtime 未变 ⇒ 零重读。
|
|
78
|
+
* **失败保守**:`agent.cwd` 缺失 / stat 失败 / 读抛错 / 读回非法 ⇒ 不更新、不清零、不抛——
|
|
79
|
+
* 沿用上次已知好值,缓存不推进(⇒ 下回合重试,自愈)。路径 = `manifestFilePath(agent.cwd)`
|
|
80
|
+
* (项目根解析与读点同源——KD-M1-18,非「锚目录 + 档名」);缓存载体 = `agent._manifestMtime`
|
|
81
|
+
* (per-agent——KD-M1-19,同族先例 `agent._slotMtime`)。本步失败只降级注入面(运行期),
|
|
82
|
+
* 与 E2 启动门槛(会话起点)两分——见 §2.5「运行期失败退化 vs 启动门槛」。
|
|
83
|
+
* @param {object} agent 主 agent(`agent.cwd` / `agent.manifest`)
|
|
84
|
+
*/
|
|
85
|
+
function refreshManifest(agent) {
|
|
86
|
+
if (!agent.cwd) return // 无锚 ⇒ 跳过本步(零 I/O,沿用内存值——§2.6 条 3 边界)
|
|
87
|
+
let mtimeMs
|
|
88
|
+
try {
|
|
89
|
+
mtimeMs = statSync(manifestFilePath(agent.cwd)).mtimeMs
|
|
90
|
+
} catch {
|
|
91
|
+
return // stat 失败(档删 / 不可读)⇒ 保守(不更新、不清零、不抛;缓存不推进)
|
|
92
|
+
}
|
|
93
|
+
if (agent._manifestMtime === mtimeMs) return // 未变 ⇒ 零重读(每回合仅一次路径解析 + 一次 statSync)
|
|
94
|
+
let m
|
|
95
|
+
try {
|
|
96
|
+
m = readManifest(agent.cwd)
|
|
97
|
+
} catch {
|
|
98
|
+
return // 读抛错(权限 / 目录等)⇒ 保守(同上)
|
|
99
|
+
}
|
|
100
|
+
if (!m.ok) return // 读回非法 / 缺失 ⇒ 保守(沿用旧值)
|
|
101
|
+
agent.manifest = m.manifest
|
|
102
|
+
agent._manifestMtime = mtimeMs
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 情境行注入(#28——`docs/core/design/MANIFEST.md` §2.6 模块契约):manifest 的
|
|
107
|
+
* `phase` 逐回合进模型上下文(判据序 ①–⑥ + 取值前置步 ③b = §2.6 表;#34 起值变由盘面驱动)。**自愈单活体**——
|
|
108
|
+
* 同文活体在 → 幂等(零历史变更);值变 → 就地摘旧行 + 落新行(保 `history` 数组引用);
|
|
109
|
+
* 压缩 / 会话重建吞行 → 下一回合重推(不落 system 槽——架构 E5.1 #5,`context.mjs`
|
|
110
|
+
* 压缩面零触碰)。`transient:true` ⇒ 不进人读线(`_fullHistory`)。
|
|
111
|
+
* @param {object} agent 主 agent(`agent.manifest` / `agent.history` / `agent.cwd`——③b 取值)
|
|
112
|
+
* @param {{depth?: number}} [opts] 注入深度(仅 depth-0)
|
|
113
|
+
* @returns {boolean} 是否落新行(测试断言用)
|
|
114
|
+
*/
|
|
115
|
+
export function pushManifestStateReminder(agent, { depth = 0 } = {}) {
|
|
116
|
+
if (depth !== 0) return false // ① 仅 depth-0(子代理读任务书——架构 §2.4 M5 零 manifest 读面)
|
|
117
|
+
if (agent.config?.agent?.engineering !== true) return false // ② 模式门:仅工程模式
|
|
118
|
+
if (!agent.manifest) return false // ③ 无 manifest(normal 路径 / 装配缺失)→ 零注入
|
|
119
|
+
refreshManifest(agent) // ③b 取值前置步(#34):mtime 门控重读——行构造须取刷新后的值
|
|
120
|
+
const history = agent.history
|
|
121
|
+
const line = manifestStateLine({ phase: agent.manifest.phase })
|
|
122
|
+
// ④ 同文 user 行已在(活体)→ 幂等——零历史变更(同文口径 = pushInjections 的 history.some)
|
|
123
|
+
if (history.some((m) => m.content === line)) return false
|
|
124
|
+
// ⑤ 值已变 → 就地 splice 摘旧行(`role === "user"` 全匹配),保 history 数组引用。会话重建后
|
|
125
|
+
// `_manifestLine` 不随历史回来 → 先按行族前缀从 history 认领现存活体(否则旧行残留 +
|
|
126
|
+
// 新行入列 = 双活体,违 §2.6 定案「单活体」)。
|
|
127
|
+
let old = agent._manifestLine
|
|
128
|
+
if (!old) {
|
|
129
|
+
for (const m of history) {
|
|
130
|
+
if (typeof m?.content === "string" && m.content.startsWith(MANIFEST_LINE_PREFIX)) old = m.content
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (old && old !== line) {
|
|
134
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
135
|
+
if (history[i]?.role === "user" && history[i].content === old) history.splice(i, 1)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
history.push({ role: "user", content: line, transient: true }) // ⑥ 落新行 + 记活体
|
|
139
|
+
agent._manifestLine = line
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* R10 L1 peer reminder(MULTI-INSTANCE-COLLAB §2a.4 D-L1a——仿 pushEnvStateReminder
|
|
145
|
+
* 形态:depth-0、transient:true——注入纪律同 env-state)。有同伴(非 self > 0)才注入
|
|
146
|
+
* ——无同伴零开销(peerInstances 惰性 mtime/TTL 缓存保证:命中零 exec)。任何
|
|
147
|
+
* 感知失败静默跳过(注入绝不打断回合)。文案(设计逐字):"本目录另有 N 个活跃
|
|
148
|
+
* thincoder({end} pid={pid}…)——文件操作注意避让"。
|
|
149
|
+
*
|
|
150
|
+
* **async**(TUI 假死批 2026-09-18):读面已异步化(探测 `execFile` promise);装配点
|
|
151
|
+
* `prepareRun` 本就 async ⇒ `await pushPeerReminder(agent)`——**注入时序与文案零变**
|
|
152
|
+
* (env-state 后、time reminder 前)。
|
|
153
|
+
*/
|
|
154
|
+
export async function pushPeerReminder(agent) {
|
|
155
|
+
let peers
|
|
156
|
+
try {
|
|
157
|
+
peers = (await peerInstances(agent.cwd)).filter((p) => !p.self)
|
|
158
|
+
} catch {
|
|
159
|
+
return // 感知失败降级——不注入
|
|
160
|
+
}
|
|
161
|
+
if (peers.length === 0) return
|
|
162
|
+
const who = peers.map((p) => (p.end ? `${p.end} pid=${p.pid}` : `pid=${p.pid}`)).join("、")
|
|
163
|
+
agent.history.push({
|
|
164
|
+
role: "user",
|
|
165
|
+
content: `[System reminder: 本目录另有 ${peers.length} 个活跃 thincoder(${who})——文件操作注意避让]`,
|
|
166
|
+
transient: true,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 机器行专用注入单点(#113 VSC 侧并入——编辑器上下文等端侧采集内容):`injections` =
|
|
172
|
+
* 单条或数组,每条 `{ content }`。纪律与 CLI parity:只进机器行(transient——持久化层
|
|
173
|
+
* 可丢;不进人读记录——自动上下文不得污染人读历史);**同文去重**(幂等注入——history
|
|
174
|
+
* 已有等文消息则跳过)。非法条目静默跳过(注入绝不打断回合)。内容由**端装配**采集并
|
|
175
|
+
* 传入(核内零端名——④ 段)。
|
|
176
|
+
*/
|
|
177
|
+
export function pushInjections(history, injections) {
|
|
178
|
+
const list = Array.isArray(injections) ? injections : (injections ? [injections] : [])
|
|
179
|
+
for (const inj of list) {
|
|
180
|
+
if (!inj || typeof inj.content !== "string") continue
|
|
181
|
+
if (history.some((m) => m.content === inj.content)) continue
|
|
182
|
+
history.push({ role: "user", content: inj.content, transient: true })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 粘贴图指引(#113 VSC 侧并入——贴图指引按端注入):`images` = 绝对路径列表(端侧保存
|
|
188
|
+
* 的粘贴图档)。把指引追加到**真实用户消息**尾部(content 保持字符串形态——不用旧
|
|
189
|
+
* image_url parts 数组),模型经 `read_image` 查看(其多模态路径注入图像 part)。非多模态
|
|
190
|
+
* 模型 ⇒ **抛错**(可见错误,绝不静默丢图)。depth>0 / 无图 / 消息缺失 ⇒ no-op。
|
|
191
|
+
*/
|
|
192
|
+
export function appendImagePointer(userMsg, images, providerModel, { depth } = {}) {
|
|
193
|
+
if (depth !== 0 || !userMsg || !Array.isArray(images) || images.length === 0) return
|
|
194
|
+
const spec = specForModel(providerModel)
|
|
195
|
+
if (!spec.multimodal) {
|
|
196
|
+
throw new Error("This model does not support pasted images. Switch to a vision-capable model (Kimi K3, Qwen, GLM, etc.) or attach the image as a file and let the model read it.")
|
|
197
|
+
}
|
|
198
|
+
userMsg.content += `\n\n[Attached images: ${images.join(" | ")}] — use the read_image tool to view them before answering.`
|
|
199
|
+
}
|