thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/advisor/run.mjs
CHANGED
|
@@ -1,352 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* advisor/run.mjs — advisor execution:
|
|
3
|
-
* Message building lives in advisor.mjs.
|
|
2
|
+
* advisor/run.mjs — advisor execution: provider resolution and the review entry
|
|
3
|
+
* point. Message building lives in advisor.mjs; the tool loop lives in loop.mjs
|
|
4
|
+
* and the context/limit/tail guards in compaction.mjs(第 11 批硬帽拆分——
|
|
5
|
+
* run.mjs 原 498/500;既有 import 面经 re-export 保持不变)。
|
|
4
6
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { findProvider, providerSpec } from "../config.mjs"
|
|
7
|
-
import { toOpenAISchema } from "../tools/index.mjs"
|
|
7
|
+
import { findProvider } from "../config.mjs"
|
|
8
8
|
import { prepareAdvisorMessages } from "../advisor.mjs"
|
|
9
|
+
import { buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./messages.mjs"
|
|
9
10
|
import { appendCitationReport } from "./citations.mjs"
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
import { runAdvisorToolLoop } from "./loop.mjs"
|
|
12
|
+
import { advisorIncompleteMarker, estimateTokens } from "./compaction.mjs"
|
|
13
|
+
import { batchDocForReview } from "../agent-tools/batch-segment.mjs"
|
|
14
|
+
// 第 33 批(§17.5 模块图):护栏常量 / doc-set 键 / 记录读取——单向导入(review-streak 无回指)。
|
|
15
|
+
import {
|
|
16
|
+
MAX_DESIGN_REVIEW_STREAK, docSetKey, designReviewStreakRecord, designReviewStreakStopped,
|
|
17
|
+
} from "../agent-tools/review-streak.mjs"
|
|
18
|
+
|
|
19
|
+
// 拆分后 import 面(既有导出名逐一保面——re-export;谓词为本批新增)。
|
|
20
|
+
export { ADVISOR_THINKING_PLACEHOLDER, MAX_RESULT_CHARS, renderTimeline as _renderTimeline } from "./compaction.mjs"
|
|
21
|
+
export { advisorToolsFor, advisorToolsFor as _advisorToolsFor } from "./loop.mjs"
|
|
22
|
+
export { runAdvisorToolLoop as _runAdvisorToolLoop } from "./loop.mjs"
|
|
23
|
+
export { advisorIncompleteMarker } from "./compaction.mjs"
|
|
24
|
+
|
|
25
|
+
// Mechanical convergence cap: up to 5 rounds suffice; a 6th call means the model
|
|
26
|
+
// is looping — refuse it instead of burning tokens. §11.2 D-24b (2026-09-06): PER
|
|
27
|
+
// REVIEW INSTANCE (agent._advisorRuns); CODE REVIEWS ONLY (2026-09-07 §8 ruling)
|
|
28
|
+
// — design reviews are EXEMPT: their rounds keep advancing, the cap never refuses.
|
|
18
29
|
export const MAX_ADVISOR_ROUNDS = 5
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
function estimateTokens(messages) {
|
|
40
|
-
return messages.reduce((sum, msg) => {
|
|
41
|
-
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content || "")
|
|
42
|
-
const toolCalls = msg.tool_calls ? JSON.stringify(msg.tool_calls) : ""
|
|
43
|
-
return sum + Math.ceil((content.length + toolCalls.length) / 4)
|
|
44
|
-
}, 0)
|
|
31
|
+
/** B 启动拒绝前缀(§14.4 #2)——稳定契约单源(三消费点同串):run.mjs 生成;同步工具面
|
|
32
|
+
* 据此登记 `_advisorRefusals`;异步结算面据此不置 `_calledAdvisorThisRun`。 */
|
|
33
|
+
export const ADVISOR_LAUNCH_REFUSAL_PREFIX = "Advisor: design review launch refused"
|
|
34
|
+
|
|
35
|
+
/** 评审失败护栏稳定前缀(第 33 批 §17.4——与 `ADVISOR_LAUNCH_REFUSAL_PREFIX` 同族;
|
|
36
|
+
* 实现 grep / 用例断言锚)。凭证卫生:串内零 token / designId 值。 */
|
|
37
|
+
export const ADVISOR_DESIGN_STREAK_STOP_PREFIX = "Advisor: design review stopped"
|
|
38
|
+
|
|
39
|
+
/** kind → 人读说明(第 33 批 §17.4 逐字——结论表第三列;八类 = 五 kind + stale +
|
|
40
|
+
* no_credential + no_report,与 review-streak.mjs 分类输出同集)。 */
|
|
41
|
+
const DESIGN_REVIEW_OUTCOME_MEANINGS = {
|
|
42
|
+
timeout: "review exceeded the wall-clock budget (agent.advisor.timeoutMs)",
|
|
43
|
+
context_limit: "review exceeded the model context budget",
|
|
44
|
+
turn_cap: "review exceeded the tool-round limit",
|
|
45
|
+
empty: "the provider returned an empty response",
|
|
46
|
+
review_failed: "provider / transport error",
|
|
47
|
+
stale: "the reviewed documents changed while the review was in flight",
|
|
48
|
+
no_credential: "the token could not be written to the session ledger",
|
|
49
|
+
no_report: "the review settled without a report",
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
/** Compact early messages when context grows too large — LOCAL trimming only
|
|
48
|
-
* (no LLM summarization). MUTATES in place (splice) so the caller's array
|
|
49
|
-
* reference stays valid — a reassignment would leave the caller's logging
|
|
50
|
-
* (tool-call count, token estimate) reading a stale array. */
|
|
51
|
-
function compactMessages(messages) {
|
|
52
|
-
// Keep: system prompt, last 20 messages (≈ 10 assistant+tool exchanges),
|
|
53
|
-
// user message — the rest is summarized.
|
|
54
|
-
if (messages.length <= 20) return
|
|
55
|
-
|
|
56
|
-
const system = messages[0]
|
|
57
|
-
const recent = messages.slice(-20)
|
|
58
|
-
const old = messages.slice(1, -20)
|
|
59
|
-
|
|
60
|
-
// Count actual tool messages (old.length counts user/assistant rows too)
|
|
61
|
-
const toolCount = old.filter((m) => m.role === "tool").length
|
|
62
|
-
const keyFiles = old
|
|
63
|
-
.filter((m) => m.role === "tool")
|
|
64
|
-
.map((m) => m.content?.split("\n")[0]?.slice(0, 50)) // first line of tool results typically names the file that was read/grepped
|
|
65
|
-
.filter(Boolean)
|
|
66
|
-
.slice(0, MAX_KEY_FILES_IN_COMPACTION)
|
|
67
|
-
const filesPart = keyFiles.length > 0 ? ` Key files examined: ${keyFiles.join(", ")}` : ""
|
|
68
|
-
const summary = `Earlier exploration: ${toolCount} tool calls completed.${filesPart}`
|
|
69
|
-
|
|
70
|
-
messages.splice(0, messages.length,
|
|
71
|
-
system,
|
|
72
|
-
{ role: "user", content: `[Context compacted] ${summary}` },
|
|
73
|
-
...recent)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const { readTool, globTool, grepTool, lsTool } = await import("../tools/index.mjs")
|
|
77
|
-
const { lspTool } = await import("../tools/lsp.mjs")
|
|
78
|
-
const { codeSearchTool } = await import("../memory/code-sync.mjs")
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Advisor tool set — ZERO git, read-only ONLY, every round. The change surface
|
|
82
|
-
* comes from the review scope (paths / _touchedFiles injected by the caller),
|
|
83
|
-
* never from git: git output misled reviews (committed fixes never show in
|
|
84
|
-
* `git diff HEAD`, so "no changes" was read as "not fixed") and the user
|
|
85
|
-
* mandate is full decoupling (7d49a52 + d3be613). The reviewer reads files
|
|
86
|
-
* and searches code; it never touches git and never writes.
|
|
87
|
-
* No round parameter — the set is constant across all rounds.
|
|
88
|
-
* @param {Object} agent — only used for the code index (agent.memory); the
|
|
89
|
-
* semantic code_search tool needs it. Without a memory, the set is 5 tools.
|
|
90
|
-
*/
|
|
91
|
-
function advisorToolsFor(agent) {
|
|
92
|
-
const search = agent?.memory ? codeSearchTool(agent.memory) : null
|
|
93
|
-
const tools = search
|
|
94
|
-
? [readTool, globTool, grepTool, lsTool, lspTool, search]
|
|
95
|
-
: [readTool, globTool, grepTool, lsTool, lspTool]
|
|
96
|
-
return { schemas: tools.map(toOpenAISchema), byName: new Map(tools.map((t) => [t.name, t])) }
|
|
97
|
-
}
|
|
98
|
-
// Test seam: the tool set is pure (agent.memory → code_search inclusion).
|
|
99
|
-
export { advisorToolsFor as _advisorToolsFor }
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Tool-call progress line summary delegates to the single source describeToolArgs
|
|
103
|
-
* (../tui/tool-args.mjs) — the same function main-agent tool blocks and subagent
|
|
104
|
-
* blocks use. 2026-08-31: replaced the local picker (action/path/pattern/command-only)
|
|
105
|
-
* so advisor progress lines show the quoted-path forms everywhere else.
|
|
106
|
-
*/
|
|
107
52
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
53
|
+
* 停止结论串(第 33 批 §17.4 逐字;F29):稳定前缀 + 停止的 doc-set 清单 + 失败尝试表
|
|
54
|
+
* (记录逐条——kind + 人读说明)+ 三选项(接受现状 / 改变或缩小范围后重跑 / /new 重置)。
|
|
55
|
+
* 全文零凭证值;不自动执行任何恢复动作(发起权在父代理 / 用户)。
|
|
56
|
+
* @param {{count: number, log: string[]}|null} record — 护栏记录(`designReviewStreakRecord`)
|
|
57
|
+
* @param {string[]|null} documents — 被停的文档集(结论清单数据源)
|
|
113
58
|
*/
|
|
114
|
-
function
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
59
|
+
export function buildDesignReviewGuardMessage(record, documents) {
|
|
60
|
+
const log = Array.isArray(record?.log) ? record.log : []
|
|
61
|
+
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
62
|
+
return [
|
|
63
|
+
`${ADVISOR_DESIGN_STREAK_STOP_PREFIX} — ${MAX_DESIGN_REVIEW_STREAK} consecutive attempts on this document set produced no design token (repeated failed settlements; no further reviews will start for this set in this session).`,
|
|
64
|
+
"Document set (1 design instance — no token issued):",
|
|
65
|
+
...docList.map((d) => `- ${d}`),
|
|
66
|
+
"Attempts (most recent last):",
|
|
67
|
+
"| # | outcome | meaning |",
|
|
68
|
+
"|---|---|---|",
|
|
69
|
+
...log.map((kind, i) => `| ${i + 1} | ${kind} | ${DESIGN_REVIEW_OUTCOME_MEANINGS[kind] ?? kind} |`),
|
|
70
|
+
"Options:",
|
|
71
|
+
"1. Accept the current state and proceed — implementation for this document set stays gated (no design token).",
|
|
72
|
+
"2. Narrow or change the scope: a different document set starts a fresh budget — fix the cause first (agent.advisor.timeoutMs / advisor model / provider).",
|
|
73
|
+
"3. Start a new session (/new) to reset the guard.",
|
|
74
|
+
].join("\n")
|
|
120
75
|
}
|
|
121
|
-
// Test seam (mirrors _advisorToolsFor).
|
|
122
|
-
export { renderTimeline as _renderTimeline }
|
|
123
|
-
// Test seam (T-TS8/9): the tool loop itself — toolsOverride injects a mock tool
|
|
124
|
-
// set with controllable timing/errors (the real set comes from advisorToolsFor).
|
|
125
|
-
export { runAdvisorToolLoop as _runAdvisorToolLoop }
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Run the advisor's tool loop: chat → execute tools → repeat.
|
|
129
|
-
* Stops when the model produces text without tool calls.
|
|
130
|
-
*
|
|
131
|
-
* Progress lines (→ tool args) are emitted via onOutput between model bursts so
|
|
132
|
-
* the panel keeps moving while the advisor explores — otherwise the panel sits
|
|
133
|
-
* frozen through every tool-call phase and the review appears to have stalled.
|
|
134
|
-
*/
|
|
135
|
-
async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd, toolsOverride = null) {
|
|
136
|
-
// Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
|
|
137
|
-
// Every chunk is ALSO recorded into an ordered timeline — the persisted record
|
|
138
|
-
// must show the review process (thinking ↔ tool progress ↔ final text) at its
|
|
139
|
-
// real positions, not a summary appended at the end. Same-kind consecutive
|
|
140
|
-
// chunks merge (token streams); kind flips start a new entry.
|
|
141
|
-
const timeline = []
|
|
142
|
-
const record = (kind, text) => {
|
|
143
|
-
const last = timeline.at(-1)
|
|
144
|
-
if (last && last.kind === kind) last.text += text
|
|
145
|
-
else timeline.push({ kind, text })
|
|
146
|
-
}
|
|
147
|
-
const emit = (kind) => (text) => { record(kind, text); onOutput?.({ kind, text }) }
|
|
148
|
-
const onThink = emit("think")
|
|
149
|
-
const onText = emit("text")
|
|
150
|
-
const onTool = emit("tool")
|
|
151
|
-
// toolsOverride = test seam (T-TS8/9): the real advisor tool set, or a mock
|
|
152
|
-
// set with controllable timing/errors.
|
|
153
|
-
const { schemas: toolSchemas, byName: toolByName } = toolsOverride ?? advisorToolsFor(agent)
|
|
154
|
-
let turns = 0
|
|
155
|
-
const startTime = Date.now()
|
|
156
|
-
|
|
157
|
-
while (true) {
|
|
158
|
-
// Interrupted (Ctrl+I) — stop immediately instead of spinning a fresh uncancellable signal
|
|
159
|
-
if (signal?.aborted) return renderTimeline(timeline, "Advisor: interrupted.")
|
|
160
|
-
|
|
161
|
-
// Check review timeout (10 minutes by default; agent.advisor.timeoutMs overrides)
|
|
162
|
-
// 运行时校验(设计评审 #1,2026-08-24):手写 config.json 的非法值(0/负数/字符串)
|
|
163
|
-
// 不得静默禁用或立即触发超时——非法一律回退默认。
|
|
164
|
-
const cfg = agent.config?.advisor?.timeoutMs
|
|
165
|
-
const timeoutMs = (Number.isFinite(cfg) && cfg > 0) ? cfg : REVIEW_TIMEOUT_MS
|
|
166
|
-
if (Date.now() - startTime > timeoutMs) {
|
|
167
|
-
return renderTimeline(timeline, `Advisor: review timeout after ${Math.round(timeoutMs / 1000)}s. Partial results may be available. Try again with a narrower scope.`)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (++turns > MAX_ADVISOR_TURNS) {
|
|
171
|
-
return renderTimeline(timeline, "Advisor: stopped after " + MAX_ADVISOR_TURNS + " tool rounds — the review appears to be looping. You may retry with a narrower scope.")
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Check context window and compact if needed
|
|
175
|
-
const currentTokens = estimateTokens(messages)
|
|
176
|
-
if (currentTokens > MAX_CONTEXT_TOKENS * 0.8) {
|
|
177
|
-
onText(`\n[Context compacted: ${currentTokens} tokens → reducing to fit window]\n`)
|
|
178
|
-
compactMessages(messages)
|
|
179
|
-
if (estimateTokens(messages) > MAX_CONTEXT_TOKENS) {
|
|
180
|
-
// Report the POST-compaction count — the pre-compaction currentTokens
|
|
181
|
-
// is stale by the time compaction has run.
|
|
182
|
-
return renderTimeline(timeline, `Advisor: context window limit reached (${estimateTokens(messages)} tokens). Review incomplete — too many tool calls. Try a narrower scope.`)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// LLM generation silence: the reasoning phase produces no SSE bytes for
|
|
187
|
-
// seconds to tens of seconds (server-side prefill on large contexts, per
|
|
188
|
-
// tool-round LLM return). A placeholder keeps the panel visibly working.
|
|
189
|
-
// kind "think" (NOT "text"): the placeholder must land in the SAME buffer
|
|
190
|
-
// and position as the upcoming reasoning — a "text"-kind placeholder
|
|
191
|
-
// rendered BELOW the think block, and the reasoning stream appeared ABOVE
|
|
192
|
-
// it ("the stream runs back to the front"). Same buffer = same spot; the
|
|
193
|
-
// reasoning continues right where the placeholder sits.
|
|
194
|
-
onOutput?.({ kind: "think", text: ADVISOR_THINKING_PLACEHOLDER })
|
|
195
|
-
|
|
196
|
-
const response = await chat(provider, {
|
|
197
|
-
messages,
|
|
198
|
-
tools: toolSchemas,
|
|
199
|
-
// Pass the signal UNCONDITIONALLY: a signal aborted between the check
|
|
200
|
-
// above and here must still cancel the fetch. core.mjs composes
|
|
201
|
-
// AbortSignal.any([signal, timeout]) — an already-aborted signal makes
|
|
202
|
-
// the request fail immediately instead of ignoring the interrupt.
|
|
203
|
-
signal: signal ?? null,
|
|
204
|
-
onToken: onText,
|
|
205
|
-
onReasoning: onThink,
|
|
206
|
-
// LOGGING(vscode advisor/run.mjs parity——按 stage 可 grep)
|
|
207
|
-
// §18.6 D-TR4:轨迹元数据增补——kind=advisor(评审独立于子代理——T-TR2);role
|
|
208
|
-
// 透出调用方角色(eng-coder 内嵌评审时为 "eng-coder");session/cwd 供轨迹对回;
|
|
209
|
-
// traces 开关沿 agent.config(D-TR6)。
|
|
210
|
-
logCtx: {
|
|
211
|
-
stage: "advisor",
|
|
212
|
-
role: agent?._role ?? null,
|
|
213
|
-
kind: "advisor",
|
|
214
|
-
session: agent?._sessionStart ?? null,
|
|
215
|
-
cwd,
|
|
216
|
-
traces: agent?.config?.traces?.enabled !== false,
|
|
217
|
-
},
|
|
218
|
-
})
|
|
219
76
|
|
|
220
|
-
|
|
221
|
-
// already streamed into the timeline via onText; fall back to
|
|
222
|
-
// response.content only if nothing was recorded.
|
|
223
|
-
if (!response.toolCalls?.length) {
|
|
224
|
-
if (!response.content?.trim()) return renderTimeline(timeline) || "Advisor: (empty response — review was inconclusive)"
|
|
225
|
-
return renderTimeline(timeline) || response.content.trim()
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Push assistant message with tool calls. reasoning_content ECHO is
|
|
229
|
-
// mandatory for reasoningEcho:"required" providers (deepseek/kimi): the
|
|
230
|
-
// server stops returning reasoning_content on later rounds when the
|
|
231
|
-
// tool-call assistant history lacks it — the observed "reasoning stops
|
|
232
|
-
// after the first tool call, returns only at the final answer" symptom.
|
|
233
|
-
// Mirrors the main agent's push (agent.mjs).
|
|
234
|
-
messages.push({
|
|
235
|
-
role: "assistant",
|
|
236
|
-
content: response.content || null,
|
|
237
|
-
tool_calls: response.toolCalls.map((tc) => ({
|
|
238
|
-
id: tc.id, type: "function",
|
|
239
|
-
function: { name: tc.name, arguments: tc.arguments },
|
|
240
|
-
})),
|
|
241
|
-
...(response.reasoning && providerSpec(provider).reasoningEcho === "required"
|
|
242
|
-
? { reasoning_content: response.reasoning }
|
|
243
|
-
: {}),
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
// B1 (AGENT-LOOP.md §18.7 D-TS7): the SAME LLM reply's multiple read-only
|
|
247
|
-
// tool calls run in PARALLEL (Promise.all) — results are backfilled in
|
|
248
|
-
// toolCalls order (Promise.all preserves the input order → tool_call_id
|
|
249
|
-
// never mismatches); each tool's timeout/error is captured independently
|
|
250
|
-
// (the existing TOOL_TIMEOUT stays — one failing tool does not block the
|
|
251
|
-
// others); progress lines are emitted in toolCalls order. The read-only
|
|
252
|
-
// tool set has no side effects — no sequencing/serialization needed.
|
|
253
|
-
// Scope note (round1 review #10): B1 is ONLY in-loop tool parallelism — it
|
|
254
|
-
// does NOT solve the TODO "platform execution: advisor parallel calls are
|
|
255
|
-
// actually serial" mystery (docs/TODO.md — LOGGING evidence item), which
|
|
256
|
-
// concerns multiple advisor CALLS observed as serial, not one reply's
|
|
257
|
-
// tool calls.
|
|
258
|
-
const parsed = response.toolCalls.map((tc) => {
|
|
259
|
-
const tool = toolByName.get(tc.name)
|
|
260
|
-
let args = {}
|
|
261
|
-
let parseError = null
|
|
262
|
-
try {
|
|
263
|
-
args = JSON.parse(tc.arguments || "{}")
|
|
264
|
-
} catch (e) {
|
|
265
|
-
parseError = `Error: invalid JSON in tool arguments: ${e.message}\nRaw arguments: ${(tc.arguments || "").slice(0, 200)}`
|
|
266
|
-
}
|
|
267
|
-
return { tc, tool, args, parseError }
|
|
268
|
-
})
|
|
269
|
-
// Progress lines first, in toolCalls order (emitted before the parallel
|
|
270
|
-
// run — display order is independent of completion order).
|
|
271
|
-
for (const p of parsed) {
|
|
272
|
-
if (p.parseError) continue // parse-error tools get no progress line (legacy behavior)
|
|
273
|
-
const argsLine = describeToolArgs(p.tc.name, p.args)
|
|
274
|
-
onTool(`\n→ ${p.tc.name}${argsLine ? " " + argsLine : ""}\n`)
|
|
275
|
-
}
|
|
276
|
-
// Every tool runs CONCURRENTLY; each result/error lands in its own slot —
|
|
277
|
-
// Promise.all preserves input order, so index i always matches parsed[i].
|
|
278
|
-
const executed = await Promise.all(parsed.map(async (p) => {
|
|
279
|
-
// Parse failure → error to model immediately (no execution)
|
|
280
|
-
if (p.parseError) return p.parseError
|
|
281
|
-
if (!p.tool) return `Error: unknown tool "${p.tc.name}". Available: ${[...toolByName.keys()].join(", ")}`
|
|
282
|
-
// Execute with timeout (clear the timer when the tool wins the race —
|
|
283
|
-
// otherwise up to MAX_ADVISOR_TURNS dangling timers accumulate)
|
|
284
|
-
try {
|
|
285
|
-
let timeoutId
|
|
286
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
287
|
-
timeoutId = setTimeout(() => reject(new Error(`tool timeout after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS)
|
|
288
|
-
})
|
|
289
|
-
let toolPromise
|
|
290
|
-
try {
|
|
291
|
-
toolPromise = p.tool.execute(p.args, { cwd, agent, onOutput, signal })
|
|
292
|
-
return await Promise.race([toolPromise, timeoutPromise])
|
|
293
|
-
} finally {
|
|
294
|
-
clearTimeout(timeoutId)
|
|
295
|
-
// Timeout won → toolPromise is still pending; a later rejection
|
|
296
|
-
// would surface as an unhandled rejection. The race already
|
|
297
|
-
// consumed the result/error in the normal path, so this no-op
|
|
298
|
-
// catch only fires for the abandoned-tool case.
|
|
299
|
-
toolPromise?.catch(() => {})
|
|
300
|
-
}
|
|
301
|
-
} catch (e) {
|
|
302
|
-
const errorType = e.message.includes("timeout") ? "timeout"
|
|
303
|
-
: e.message.includes("ENOENT") ? "file_not_found"
|
|
304
|
-
: e.message.includes("permission") ? "permission_denied"
|
|
305
|
-
: "execution_error"
|
|
306
|
-
return `Error (${errorType}): ${e.message}`
|
|
307
|
-
}
|
|
308
|
-
}))
|
|
309
|
-
|
|
310
|
-
// Backfill in toolCalls order (executed[i] ↔ parsed[i]); per-result
|
|
311
|
-
// non-string serialization + line-aware truncation stay per-tool.
|
|
312
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
313
|
-
let result = executed[i]
|
|
314
|
-
if (typeof result !== "string") result = JSON.stringify(result)
|
|
315
|
-
|
|
316
|
-
if (result.length > MAX_RESULT_CHARS) {
|
|
317
|
-
const lines = result.split("\n")
|
|
318
|
-
let truncated = ""
|
|
319
|
-
let charCount = 0
|
|
320
|
-
let keptLines = 0
|
|
321
|
-
|
|
322
|
-
for (let j = 0; j < lines.length; j++) {
|
|
323
|
-
const line = lines[j]
|
|
324
|
-
if (charCount + line.length + 1 > MAX_RESULT_CHARS) break
|
|
325
|
-
truncated += line + "\n"
|
|
326
|
-
charCount += line.length + 1
|
|
327
|
-
keptLines++
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const remainingLines = lines.length - keptLines
|
|
331
|
-
result = (
|
|
332
|
-
truncated +
|
|
333
|
-
`\n… (truncated: ${remainingLines} more lines, ${result.length} chars total)\n` +
|
|
334
|
-
`To see more content, use: read(path, offset=${keptLines + 1}, limit=200)`
|
|
335
|
-
)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
messages.push({ role: "tool", tool_call_id: parsed[i].tc.id, content: result })
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
77
|
+
const MAX_UNFIXED_DISPLAY = 10 // unfixed issues shown in the cap message
|
|
342
78
|
|
|
343
79
|
/** Resolve the advisor's provider: cfg.provider/model when set, otherwise the main agent's provider */
|
|
344
80
|
export function resolveAdvisorProvider(agent) {
|
|
345
81
|
const cfg = agent.config?.advisor
|
|
346
82
|
if (cfg?.provider) {
|
|
347
83
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
84
|
+
// F-1 (ISSUE-FIX-BATCH): children carry no agent.providers (spawn childConfig copies the
|
|
85
|
+
// parent config) — fall back to config.providersList (.length: [] must not skip the list).
|
|
86
|
+
const provider = findProvider(agent.providers?.length ? agent.providers : agent.config?.providersList ?? [agent.provider], cfg.provider)
|
|
87
|
+
// F-2a (MODEL-400-FIX):无 cfg.model 时渠道克隆须重派生 model——渠道裸克隆会丢 model 键
|
|
88
|
+
// → 无 model 请求 → serde 400。
|
|
89
|
+
// MODEL-SELECTION v2(M3④):无 cfg.model → 命中渠道自己的默认模型(`provider.model`
|
|
90
|
+
// 单值);渠道无默认模型 → 父 provider 兜底(与 subagent F-2c 同构)——绝不产出静默
|
|
91
|
+
// undefined-model 请求(最极端两者皆无 → chat 前 assertProviderModel fail-fast)。
|
|
92
|
+
const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider, model: provider.model ?? agent.provider?.model }
|
|
350
93
|
if (cfg.thinking === null || cfg.thinking === false) result.thinking = undefined // explicitly off
|
|
351
94
|
else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
|
|
352
95
|
if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
|
|
@@ -388,17 +131,53 @@ function extractUnfixedIssues(priorText) {
|
|
|
388
131
|
.filter(Boolean)
|
|
389
132
|
.slice(0, MAX_UNFIXED_DISPLAY)
|
|
390
133
|
}
|
|
134
|
+
/** Review-looking guard (async settle parity): a markdown table row or ≥200 chars of prose counts as a prior. */
|
|
135
|
+
export function looksLikeReviewOutput(text) {
|
|
136
|
+
const trimmed = String(text ?? "").trim()
|
|
137
|
+
return /\|.*\|.*\|/.test(trimmed) || trimmed.length >= 200
|
|
138
|
+
}
|
|
139
|
+
/** Cap message (shared by runAdvisorReview and the async pre-check — per-review refusal). */
|
|
140
|
+
export function buildCapMessage(agent) {
|
|
141
|
+
const prior = agent._lastAdvisorOutput
|
|
142
|
+
const unfixed = prior ? extractUnfixedIssues(prior) : []
|
|
143
|
+
let message = `Advisor: convergence cap reached after ${MAX_ADVISOR_ROUNDS} rounds.\n`
|
|
144
|
+
if (unfixed.length > 0) {
|
|
145
|
+
message += `\nUnresolved issues from prior rounds:\n${unfixed.map((i) => `- ${i}`).join("\n")}\n`
|
|
146
|
+
} else {
|
|
147
|
+
message += "\nAll prior issues appear resolved.\n"
|
|
148
|
+
}
|
|
149
|
+
message += "\nOptions:\n1. Accept current state and proceed\n2. Manually review specific concerns with read/grep\n3. Start a new session (/new) to reset the advisor"
|
|
150
|
+
return message
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 压缩定锚简报(F13/§14.4 #3)——**由评审参数构建**(非模型输出):对象声明 / 文档清单 /
|
|
155
|
+
* Approval Signal 三锚;重内容(项目指南 / 方法论 / 文档地图)不入 pin(压缩的意义所在)。
|
|
156
|
+
* 形态逐字见设计 §14.4 #3(首行为机械化重挂说明)。
|
|
157
|
+
*/
|
|
158
|
+
function buildPinnedBrief(reviewType, documents, object, designToken, designId) {
|
|
159
|
+
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
160
|
+
const parts = ["[review brief — re-attached after context compaction; the original review request is no longer in the context]"]
|
|
161
|
+
const declaration = buildObjectDeclarationBlock(object)
|
|
162
|
+
if (declaration) parts.push(declaration.trimEnd())
|
|
163
|
+
if (docList.length > 0) {
|
|
164
|
+
parts.push("## Documents to Review")
|
|
165
|
+
parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
|
|
166
|
+
}
|
|
167
|
+
if (reviewType === "design" && designToken) parts.push(buildDesignApprovalBlock(designToken, designId))
|
|
168
|
+
return parts.join("\n\n")
|
|
169
|
+
}
|
|
391
170
|
|
|
392
171
|
/**
|
|
393
172
|
* Run an advisor review. reviewType: "code" (default) or "design". Returns review text or null when skipped.
|
|
394
173
|
* @param {string|null} [designToken] — injected into the design-review prompt; the advisor echoes it only on approval.
|
|
395
174
|
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review; passed through to the message builder.
|
|
396
175
|
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review.
|
|
397
|
-
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3):
|
|
398
|
-
*
|
|
399
|
-
*
|
|
176
|
+
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3): { type, target, status, reason, exclude }; absent → legacy behavior (no injection).
|
|
177
|
+
* @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
|
|
178
|
+
* the Approval Signal (passed through to the message builders).
|
|
400
179
|
*/
|
|
401
|
-
export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null, object = null) {
|
|
180
|
+
export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null, object = null, designId = null) {
|
|
402
181
|
const onOutput = callbacks?.onOutput
|
|
403
182
|
const signal = callbacks?.signal
|
|
404
183
|
const startTime = Date.now()
|
|
@@ -407,36 +186,54 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
|
|
|
407
186
|
// former advisor.enabled gate is removed — review capability has no off
|
|
408
187
|
// switch; only the guard (completion pushback) is opt-in via advisor.guard.
|
|
409
188
|
|
|
410
|
-
// Mechanical convergence cap —
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
} else {
|
|
425
|
-
message += "\nAll prior issues appear resolved.\n"
|
|
189
|
+
// Mechanical convergence cap — CODE REVIEWS ONLY (2026-09-07 §8 ruling: design
|
|
190
|
+
// reviews are exempt). _advisorRound is scoped to the current review instance
|
|
191
|
+
// (§11.2 D-24b ③), so >= MAX_ADVISOR_ROUNDS blocks the next call of THIS instance.
|
|
192
|
+
// 5 rounds max; after that the review is never pushed back.
|
|
193
|
+
if (reviewType !== "design" && (agent._advisorRound || 0) >= MAX_ADVISOR_ROUNDS) {
|
|
194
|
+
return buildCapMessage(agent)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 第 33 批(§17.5 检查点 2——内防线):同一 doc-set 连续未产出可用结算达阈值 ⇒ 直接拒绝
|
|
198
|
+
// (不建消息、不发起、零 LLM)——防直接调用方绕过工具层预检(正常工具链在预检已拒)。
|
|
199
|
+
if (reviewType === "design" && Array.isArray(documents) && documents.length > 0) {
|
|
200
|
+
const streakKey = docSetKey(documents, agent.cwd)
|
|
201
|
+
if (designReviewStreakStopped(agent, streakKey)) {
|
|
202
|
+
return buildDesignReviewGuardMessage(designReviewStreakRecord(agent, streakKey), documents)
|
|
426
203
|
}
|
|
427
|
-
message += "\nOptions:\n1. Accept current state and proceed\n2. Manually review specific concerns with read/grep\n3. Start a new session (/new) to reset the advisor"
|
|
428
|
-
|
|
429
|
-
return message
|
|
430
204
|
}
|
|
431
205
|
|
|
432
206
|
const provider = resolveAdvisorProvider(agent)
|
|
433
207
|
// Advisor always works in the agent's cwd — scope is defined by paths/documents.
|
|
434
208
|
const advisorCwd = agent.cwd
|
|
435
209
|
|
|
436
|
-
const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths, null, object)
|
|
210
|
+
const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths, null, object, designId)
|
|
211
|
+
|
|
212
|
+
// B 启动断言(fail-closed——§14.4 #2):设计评审请求内**必须**携带与本次签发 token 精确
|
|
213
|
+
// 对应的 Approval Signal——构建面补不上就拒绝启动(不发"请回显一个不存在的 token"的请求)。
|
|
214
|
+
// 拒绝报告前缀 `Advisor: design review launch refused` = 稳定契约(同步工具面据此登记
|
|
215
|
+
// _advisorRefusals;异步结算面据此不置 _calledAdvisorThisRun)。工具路径恒签发 token ⇒
|
|
216
|
+
// 该拒绝为直接调用方兜底(防御纵深——正常链不可达,如实注)。
|
|
217
|
+
if (reviewType === "design") {
|
|
218
|
+
const missing = !designToken
|
|
219
|
+
? "no design token was minted"
|
|
220
|
+
: (messages.some((m) => m.role === "user" && String(m.content ?? "").includes(`[DESIGN-TOKEN:${designToken}`))
|
|
221
|
+
? null
|
|
222
|
+
: "the request does not carry the approval signal")
|
|
223
|
+
if (missing) {
|
|
224
|
+
return `${ADVISOR_LAUNCH_REFUSAL_PREFIX} — ${missing}. Nothing was sent: a request that asks the reviewer to echo a token it cannot see would break the credential chain. Re-run advisor(type='design') to mint a fresh token.`
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// §2.20.3 批次档写通道的绑定(仅设计评审):同步路径 = 调用方(advisor 工具)传入的
|
|
229
|
+
// callbacks.batchDoc(即 resolved.run 的实例绑定);异步路径 = 本实例在跑池条目的
|
|
230
|
+
// run.batchDoc(同文档集实例键——各评审各取各档,不用单值会话态)。
|
|
231
|
+
const boundBatchDoc = reviewType === "design" ? batchDocForReview(agent, documents, callbacks) : null
|
|
232
|
+
// F13/§14.4 #3:压缩定锚简报(评审参数构建——压缩触发时由 compaction 重挂)。
|
|
233
|
+
const pinned = buildPinnedBrief(reviewType, documents, object, designToken, designId)
|
|
437
234
|
|
|
438
235
|
try {
|
|
439
|
-
const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
|
|
236
|
+
const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd, null, reviewType, boundBatchDoc, pinned)
|
|
440
237
|
|
|
441
238
|
// Host-verified citations (decision d698434): mechanically check every
|
|
442
239
|
// `file:line: content` reference in the review against the CURRENT file
|
|
@@ -444,9 +241,10 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
|
|
|
444
241
|
// the prior table instead of re-reading (three consecutive false reports
|
|
445
242
|
// cited pre-fix line content). Unverified citations must not support a
|
|
446
243
|
// push-back; the parent agent sees the verification report.
|
|
244
|
+
// F14/§14.5:解析候选 = cwd + 评审对象声明范围派生根(scope = documents + paths)。
|
|
447
245
|
let final = result
|
|
448
246
|
if (!result.trimStart().startsWith("Advisor:")) {
|
|
449
|
-
final = appendCitationReport(result, advisorCwd)
|
|
247
|
+
final = appendCitationReport(result, advisorCwd, { scope: [...(documents ?? []), ...(paths ?? [])] })
|
|
450
248
|
// Success path: keep the FULL review output for convergence rounds —
|
|
451
249
|
// round 2+ injects this verbatim and the model understands it (decision
|
|
452
250
|
// 2026-08-08: prior-table hard parsing removed; no phrase/header matching).
|
|
@@ -454,8 +252,7 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
|
|
|
454
252
|
// table row (`| a | b | c |`) or substantial prose (>200 chars). An
|
|
455
253
|
// empty or tool-progress-only reply must not become the "prior review"
|
|
456
254
|
// of round 2+.
|
|
457
|
-
const
|
|
458
|
-
const looksLikeReview = /\|.*\|.*\|/.test(trimmed) || trimmed.length >= 200
|
|
255
|
+
const looksLikeReview = looksLikeReviewOutput(final)
|
|
459
256
|
if (looksLikeReview) {
|
|
460
257
|
agent._lastAdvisorOutput = final
|
|
461
258
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* truncate.mjs — advisor 工具结果的行感知双端截断(DUAL-END-TRUNCATION F-2,
|
|
3
|
+
* DUAL-END-TRUNCATION.md,2026-09-09——CLI/VSC 双端逐字同构镜像)。
|
|
4
|
+
*
|
|
5
|
+
* 原 run.mjs 内联截断为纯头向(line-aware 从首行累加至 64K break)——评审尾部
|
|
6
|
+
* 结论/裁决被切。现改头尾双保:头行累加至预算 ~60% → 中段省略注 → 尾行累加至
|
|
7
|
+
* 剩余 ~40%(保尾结论——与 read 工具 C 方案 / offload 双端预览同构的
|
|
8
|
+
* "头+尾保留、中段截断"策略——共享设计语言、分别实现)。
|
|
9
|
+
*
|
|
10
|
+
* 纯函数无导入:maxChars 由调用方传入(run.mjs 传 MAX_RESULT_CHARS)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** advisor 截断头占比——头行累加至此份额后切中段,尾自动取剩余预算(设计 §4 ≈0.6)。 */
|
|
14
|
+
export const ADVISOR_HEAD_RATIO = 0.6
|
|
15
|
+
|
|
16
|
+
/** 尾部预算预留(字符):中段省略注 + offset 续读提示的保守预留——注的位数只减不增
|
|
17
|
+
* (middle ≤ totalChars——超大结果位数可达 ~10 位——200 字符仍宽松覆盖注+提示 ≤ ~140;
|
|
18
|
+
* 差额留给尾行)。 */
|
|
19
|
+
const NOTE_RESERVE_CHARS = 200
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 行感知双端截断:result > maxChars → 头 + `… (truncated: K more lines, TOTAL chars
|
|
23
|
+
* total)` + 尾 + offset 续读提示;≤ maxChars 原样返回(无假截断注)。K = 头尾之间
|
|
24
|
+
* 的中段行数——头(≤60%) + 尾(≤余预算) 均行级累加、绝不半行切开;中段存在时 K ≥ 1
|
|
25
|
+
* (防御:K ≤ 0 时静默返回头尾拼合——不谎报截断)。
|
|
26
|
+
*/
|
|
27
|
+
export function truncateAdvisorResult(result, maxChars) {
|
|
28
|
+
if (result.length <= maxChars) return result
|
|
29
|
+
const totalChars = result.length
|
|
30
|
+
const lines = result.split("\n")
|
|
31
|
+
const headBudget = Math.floor(maxChars * ADVISOR_HEAD_RATIO)
|
|
32
|
+
let head = ""
|
|
33
|
+
let headLines = 0
|
|
34
|
+
let chars = 0
|
|
35
|
+
while (headLines < lines.length && chars + lines[headLines].length + 1 <= headBudget) {
|
|
36
|
+
chars += lines[headLines].length + 1
|
|
37
|
+
head += lines[headLines] + "\n"
|
|
38
|
+
headLines++
|
|
39
|
+
}
|
|
40
|
+
const tailBudget = Math.max(0, maxChars - chars - NOTE_RESERVE_CHARS)
|
|
41
|
+
let tail = ""
|
|
42
|
+
let tailChars = 0
|
|
43
|
+
let tailStart = lines.length
|
|
44
|
+
while (tailStart > headLines && tailChars + lines[tailStart - 1].length + 1 <= tailBudget) {
|
|
45
|
+
tailChars += lines[tailStart - 1].length + 1
|
|
46
|
+
tailStart--
|
|
47
|
+
tail = lines[tailStart] + "\n" + tail
|
|
48
|
+
}
|
|
49
|
+
const middle = tailStart - headLines
|
|
50
|
+
if (middle <= 0) return head + tail // 头尾已覆盖全部行——绝不产生假截断注
|
|
51
|
+
return (
|
|
52
|
+
head +
|
|
53
|
+
`\n… (truncated: ${middle} more lines, ${totalChars} chars total)\n` +
|
|
54
|
+
tail +
|
|
55
|
+
`To see more content, use: read(path, offset=${headLines + 1}, limit=200)`
|
|
56
|
+
)
|
|
57
|
+
}
|