thincoder 0.12.62 → 0.12.64
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 +27 -0
- package/README.md +46 -49
- package/bin/thincoder.mjs +68 -34
- package/package.json +7 -6
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +257 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -379
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +60 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +1 -1
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +1 -1
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +35 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +6 -5
- package/src/tui/cmd-plan.mjs +9 -0
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +24 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +33 -11
- package/src/tui/index.mjs +18 -10
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +22 -59
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +15 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +14 -14
- package/src/tui/subagent-blocks.mjs +20 -3
- package/src/tui/subagent-freeze.mjs +73 -2
- package/src/tui/suspension-drive.mjs +57 -23
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/tui-lifecycle.mjs +9 -0
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +6 -2
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -244
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -243
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -204
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -473
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -295
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -309
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -482
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -459
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -417
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -392
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -88
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/ledger.mjs +0 -227
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -415
- package/src/memory/core.mjs +0 -299
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -419
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/scan.mjs +0 -95
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -258
- package/src/prompts/discipline-normal.md +0 -185
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -60
- package/src/prompts/persona-engineering.md +0 -55
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -221
- package/src/session-guard.mjs +0 -59
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-segments.mjs +0 -100
- package/src/session-slots.mjs +0 -492
- package/src/session-store.mjs +0 -441
- package/src/session.mjs +0 -492
- package/src/skills.mjs +0 -153
- package/src/text-budget.mjs +0 -46
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -355
package/src/agent-tools/goal.mjs
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* goal tool: lifecycle management for long-running autonomous goals (completion contract).
|
|
3
|
-
* Three states: active / complete / blocked. Completion must pass a verify evidence threshold;
|
|
4
|
-
* blocked is only accepted after the same condition persists 3 consecutive times.
|
|
5
|
-
* The system injects status + budget progress + audit discipline every turn.
|
|
6
|
-
*/
|
|
7
|
-
export const goalTool = {
|
|
8
|
-
name: "goal",
|
|
9
|
-
description:
|
|
10
|
-
"Manage a long-running autonomous goal. " +
|
|
11
|
-
"action='set': create or replace the goal — must have a verifiable completion criterion (a machine-checkable proof, not vague effort). " +
|
|
12
|
-
"action='complete': mark achieved — only after the criterion's check has actually passed. " +
|
|
13
|
-
"action='blocked': report an impasse (requires 'reason') — only after 3 genuine attempts. " +
|
|
14
|
-
"action='cancel': abandon the goal. " +
|
|
15
|
-
"Returns a status line — the goal set/updated/completed/blocked/cancelled confirmation, or Error: ... with the reason.",
|
|
16
|
-
parameters: {
|
|
17
|
-
type: "object",
|
|
18
|
-
properties: {
|
|
19
|
-
action: { type: "string", enum: ["set", "complete", "blocked", "cancel"], description: "Goal lifecycle action" },
|
|
20
|
-
objective: { type: "string", description: "What you are trying to accomplish (for 'set')" },
|
|
21
|
-
criteria: { type: "string", description: "How completion is PROVEN: the exact check to run, e.g. 'npm test passes', 'grep finds no TODO marker' (required for 'set')" },
|
|
22
|
-
reason: { type: "string", description: "The blocking condition (required for 'blocked')" },
|
|
23
|
-
},
|
|
24
|
-
required: ["action"],
|
|
25
|
-
},
|
|
26
|
-
readonly: true,
|
|
27
|
-
async execute(args, ctx) {
|
|
28
|
-
const agent = ctx.agent
|
|
29
|
-
if (args.action === "cancel") {
|
|
30
|
-
agent.goal = null
|
|
31
|
-
return "Goal cancelled. If the goal was blocked or impossible, explain why in your next message — the user can clarify, adjust scope, or confirm cancellation."
|
|
32
|
-
}
|
|
33
|
-
if (args.action === "set") {
|
|
34
|
-
if (!args.objective) return "Error: 'objective' required for 'set' action."
|
|
35
|
-
if (!args.criteria) {
|
|
36
|
-
return "Error: 'criteria' required for 'set' — a goal without a machine-checkable proof of completion is a wish, not a goal. Name the exact check (tests, command output, search result) that proves it's done."
|
|
37
|
-
}
|
|
38
|
-
agent.goal = {
|
|
39
|
-
objective: String(args.objective).slice(0, 500),
|
|
40
|
-
criteria: String(args.criteria).slice(0, 500),
|
|
41
|
-
setAt: Date.now(),
|
|
42
|
-
status: "active",
|
|
43
|
-
turnsUsed: 0,
|
|
44
|
-
_blockTally: null, // { reason, count } — consecutive count of the same blocking condition (for blocked audit)
|
|
45
|
-
}
|
|
46
|
-
return `Goal set: ${agent.goal.objective}\nDone when: ${agent.goal.criteria}\nThe system will inject goal status every turn. Completion and blocked claims are audited — see the reminders.`
|
|
47
|
-
}
|
|
48
|
-
if (!agent.goal || agent.goal.status !== "active") {
|
|
49
|
-
return `Error: no active goal to '${args.action}' (current: ${agent.goal?.status ?? "none"}). Set one first.`
|
|
50
|
-
}
|
|
51
|
-
if (args.action === "complete") {
|
|
52
|
-
// Evidence chain threshold: files were mutated this run without verify — refuse completion (aligns with completion guard)
|
|
53
|
-
if (agent._mutatedThisRun && !agent._verifiedThisRun) {
|
|
54
|
-
return "Error: files were modified but verify has not run. Run the check your criteria names AND the verify tool before marking the goal complete — false completion is the worst outcome of autonomous work."
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Independent judge: verify the goal was actually achieved
|
|
58
|
-
// Only applies when the agent is at depth 0 (not a subagent) and has history to review
|
|
59
|
-
if (ctx.depth === 0 && agent.history.length > 2) {
|
|
60
|
-
try {
|
|
61
|
-
// Extract recent activity: last 4 assistant messages (summarizing what was done)
|
|
62
|
-
const recent = agent.history.filter(m => m.role === "assistant").slice(-4)
|
|
63
|
-
const activity = recent.map(m => (m.content ?? "").slice(0, 500)).join("\n---\n")
|
|
64
|
-
const { chat } = await import("../provider/index.mjs")
|
|
65
|
-
const judgeRes = await chat(agent.provider, {
|
|
66
|
-
messages: [{
|
|
67
|
-
role: "user",
|
|
68
|
-
content: `You are an independent goal judge. Evaluate whether this goal has been achieved based on the agent's activity.
|
|
69
|
-
|
|
70
|
-
Goal: ${agent.goal.objective}
|
|
71
|
-
Success criteria: ${agent.goal.criteria}
|
|
72
|
-
|
|
73
|
-
Recent agent activity:
|
|
74
|
-
${activity || "(no activity recorded)"}
|
|
75
|
-
|
|
76
|
-
Has this goal been achieved? Answer ONLY "YES" or "NO" followed by a one-sentence reason.`,
|
|
77
|
-
}],
|
|
78
|
-
tools: [],
|
|
79
|
-
signal: AbortSignal.timeout(10_000),
|
|
80
|
-
// §18.6 D-TR4/D-TR6(2026-09-04 fix round1):goal 独立评审调用经 chat()
|
|
81
|
-
// 唯一采集点——补轨迹元数据 + traces 开关透传(agent.config.traces.enabled
|
|
82
|
-
// ——关=不落盘必须全覆盖——与 agent.mjs/context.mjs 同模式)
|
|
83
|
-
logCtx: {
|
|
84
|
-
stage: "goal", kind: "goal",
|
|
85
|
-
role: agent._role ?? null, depth: ctx.depth,
|
|
86
|
-
session: agent._sessionStart ?? null, cwd: agent.cwd,
|
|
87
|
-
traces: agent.config?.traces?.enabled !== false,
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
const verdict = (judgeRes.content ?? "").trim()
|
|
91
|
-
if (verdict.toUpperCase().startsWith("NO")) {
|
|
92
|
-
return `Goal NOT complete (judge says NO): ${verdict.slice(2).trim()}\n\nContinue working or report blocked if this is a true impasse.`
|
|
93
|
-
}
|
|
94
|
-
if (!verdict.toUpperCase().startsWith("YES")) {
|
|
95
|
-
return `Goal completion unverified — judge response ambiguous: "${verdict.slice(0, 200)}". Re-check your criteria and try again with clear evidence.`
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
// Judge unavailable — allow completion but note it
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
agent.goal.status = "complete"
|
|
103
|
-
return `Goal verified complete ✓: ${agent.goal.objective}\nIn your next message, summarize the evidence (what check ran, what it showed) — the user should be able to audit this claim.`
|
|
104
|
-
}
|
|
105
|
-
if (args.action === "blocked") {
|
|
106
|
-
if (!args.reason) return "Error: 'reason' required for 'blocked' action."
|
|
107
|
-
// Blocked audit: same condition must appear 3 consecutive times (only counts as real blocking if different approaches still hit the same wall)
|
|
108
|
-
const tally = agent.goal._blockTally
|
|
109
|
-
const count = tally?.reason === args.reason ? tally.count + 1 : 1
|
|
110
|
-
agent.goal._blockTally = { reason: args.reason, count }
|
|
111
|
-
if (count < 3) {
|
|
112
|
-
return `Blocked not accepted yet (${count}/3 for this condition). Try a genuinely different approach first; report blocked only if the same condition stops you ${3 - count} more time(s).`
|
|
113
|
-
}
|
|
114
|
-
agent.goal.status = "blocked"
|
|
115
|
-
return `Goal marked blocked after 3 attempts: ${args.reason}\nExplain the blocker to the user in your next message — what you tried, and what you need (clarification, permission, a decision).`
|
|
116
|
-
}
|
|
117
|
-
return `Error: unknown action '${args.action}'.`
|
|
118
|
-
},
|
|
119
|
-
}
|
package/src/agent-tools/plan.mjs
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* plan tool: enter/exit plan mode.
|
|
3
|
-
* In plan mode only read-only tools are allowed — explore code, design solutions, no code writing.
|
|
4
|
-
* After the user approves the plan, exit plan mode and start implementing.
|
|
5
|
-
*
|
|
6
|
-
* Reminder cadence (kimi-code style): while plan mode is active the agent loop
|
|
7
|
-
* re-injects reminders — sparse every 2 turns, full every 5 turns or when the
|
|
8
|
-
* user sends a new message — so the constraint never fades from context.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const PLAN_FULL_REMINDER =
|
|
12
|
-
"[System reminder: plan mode is ON. Workflow: (1) explore/read codebase with read-only tools, " +
|
|
13
|
-
"(2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit' " +
|
|
14
|
-
"so the user can approve it. Only read-only tools are allowed — do not write, edit, or run mutation commands. " +
|
|
15
|
-
"Your turn must end with either a clarifying question to the user or a call to plan with action='exit'.]"
|
|
16
|
-
|
|
17
|
-
const PLAN_SPARSE_REMINDER =
|
|
18
|
-
"[System reminder: plan mode still active — read-only tools only (the current plan file exempt). " +
|
|
19
|
-
"Design the solution, then call plan with action='exit' for user approval.]"
|
|
20
|
-
|
|
21
|
-
const PLAN_EXIT_REMINDER =
|
|
22
|
-
"[System reminder: plan mode is now OFF. Start implementing your plan — edit files, run commands. " +
|
|
23
|
-
"No need for a task list (plan already covered that) or further confirmation.]"
|
|
24
|
-
|
|
25
|
-
/** Turns between reminder re-injections while plan mode is active */
|
|
26
|
-
const SPARSE_INTERVAL = 2
|
|
27
|
-
const FULL_INTERVAL = 5
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Decide which plan-mode reminder (if any) to inject this turn.
|
|
31
|
-
* @param {object} agent — the agent object (mutated: tracks reminder state)
|
|
32
|
-
* @param {boolean} userMessageSince — whether a user message arrived since the last reminder
|
|
33
|
-
* @returns {string|null} reminder text or null
|
|
34
|
-
*/
|
|
35
|
-
export function planReminderForTurn(agent, userMessageSince) {
|
|
36
|
-
if (!agent.planMode) {
|
|
37
|
-
agent._planTurnsSinceReminder = 0
|
|
38
|
-
agent._planTurnsSinceSparse = 0
|
|
39
|
-
return null
|
|
40
|
-
}
|
|
41
|
-
agent._planTurnsSinceReminder = (agent._planTurnsSinceReminder ?? 0) + 1
|
|
42
|
-
agent._planTurnsSinceSparse = (agent._planTurnsSinceSparse ?? 0) + 1
|
|
43
|
-
if (userMessageSince || agent._planTurnsSinceReminder >= FULL_INTERVAL) {
|
|
44
|
-
agent._planTurnsSinceReminder = 0
|
|
45
|
-
agent._planTurnsSinceSparse = 0
|
|
46
|
-
return PLAN_FULL_REMINDER
|
|
47
|
-
}
|
|
48
|
-
if (agent._planTurnsSinceSparse >= SPARSE_INTERVAL) {
|
|
49
|
-
agent._planTurnsSinceSparse = 0
|
|
50
|
-
return PLAN_SPARSE_REMINDER
|
|
51
|
-
}
|
|
52
|
-
return null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const planTool = {
|
|
56
|
-
name: "plan",
|
|
57
|
-
description:
|
|
58
|
-
"Enter or exit plan mode. In plan mode you are restricted to READ-ONLY tools: read files, search code, run read-only shell commands. Use plan mode before complex multi-step tasks — explore the codebase, design the architecture, present a plan to the user. When the user approves, exit plan mode and implement. For simple single-file edits, skip plan mode and just make the change.",
|
|
59
|
-
parameters: {
|
|
60
|
-
type: "object",
|
|
61
|
-
properties: {
|
|
62
|
-
action: { type: "string", enum: ["enter", "exit"], description: "Enter or exit plan mode" },
|
|
63
|
-
},
|
|
64
|
-
required: ["action"],
|
|
65
|
-
},
|
|
66
|
-
readonly: true,
|
|
67
|
-
async execute(args, ctx) {
|
|
68
|
-
if (args.action === "exit") {
|
|
69
|
-
ctx.agent.planMode = false
|
|
70
|
-
ctx.agent._planTurnsSinceReminder = 0
|
|
71
|
-
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
72
|
-
ctx.agent._pendingReminders.push(PLAN_EXIT_REMINDER)
|
|
73
|
-
return "Plan mode exited. You may now edit files and run commands."
|
|
74
|
-
}
|
|
75
|
-
ctx.agent.planMode = true
|
|
76
|
-
ctx.agent._planTurnsSinceReminder = 0
|
|
77
|
-
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
78
|
-
ctx.agent._pendingReminders.push(PLAN_FULL_REMINDER)
|
|
79
|
-
return "Plan mode activated. You are now restricted to READ-ONLY tools. Explore the codebase, understand the architecture, design a solution. Present your plan to the user for approval before writing any code."
|
|
80
|
-
},
|
|
81
|
-
}
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* agent-tools/read-history.mjs — read_history tool (SESSION.md §9 + §13 R19 cross-session).
|
|
3
|
-
*
|
|
4
|
-
* Query message history — THIS session by default, any session on disk with `path`
|
|
5
|
-
* (SESSION.md §13 R19): an explicit session file path deep-queries that file's
|
|
6
|
-
* history line; "cwd:<dir>" discovers the sessions stored for that directory.
|
|
7
|
-
*
|
|
8
|
-
* Default (no path) — THIS session's full human-readable record (record store when bound
|
|
9
|
-
* — disk-backed, SESSION.md §14.3.7; agent._fullHistory memory fallback otherwise:
|
|
10
|
-
* NEVER compacted, audit-complete). Use to recall what was said or done earlier:
|
|
11
|
-
* design decisions, tool-call timing, past rulings.
|
|
12
|
-
*
|
|
13
|
-
* Filters AND together: role / keyword (message text) / tool (tool messages by
|
|
14
|
-
* name + assistant messages that declared the call) / since-until (epoch ms
|
|
15
|
-
* window, inclusive-inclusive, matches ONLY messages that carry ts) /
|
|
16
|
-
* limit (default 50, clamped to 200) / direction (oldest/newest — which end of
|
|
17
|
-
* the matched set the limit window is taken from).
|
|
18
|
-
*
|
|
19
|
-
* Returns a JSON array in chronological order. Every message without ts comes
|
|
20
|
-
* back as ts:null and can never match a time window (legacy sessions). Content
|
|
21
|
-
* is truncated to ~500 chars with an explicit marker — full text lives in the
|
|
22
|
-
* session file. assistant tool_calls are summarized to a name list (arguments
|
|
23
|
-
* never expanded).
|
|
24
|
-
*
|
|
25
|
-
* Cross-session (SESSION.md §13 D-R19a): path = a session file path (absolute, or
|
|
26
|
-
* relative to the project cwd) → read that file's history line and apply the SAME
|
|
27
|
-
* filter surface; path = "cwd:<dir>" → list every slot stored for that directory
|
|
28
|
-
* (slot number + full file path + title/message count/updatedAt — no dead-slot
|
|
29
|
-
* filtering, v1 decision). Single-file retrieval is guarded by a line-scan cap
|
|
30
|
-
* (READ_HISTORY_SCAN_MAX — an oversized file is refused before it is read whole)
|
|
31
|
-
* plus a message-count cap (READ_HISTORY_MAX_MESSAGES = 50,000 — L24 双保险第二道).
|
|
32
|
-
*
|
|
33
|
-
* readonly: true — planMode pass / no permission ask. Registered depth-0 only:
|
|
34
|
-
* subagents get their own throwaway history, so querying "the session" from a
|
|
35
|
-
* child would be semantically confusing (SESSION.md §9.5 refinement 1 + §13 T-R19.4).
|
|
36
|
-
* §13 R19 extension mirrored per SESSION.md §13 — double-end isomorphic, no
|
|
37
|
-
* cross-end byte test (thincoder-vscode/src/agent-tools/read-history.mjs).
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
import { openSync, readSync, closeSync, readFileSync, existsSync, statSync } from "node:fs"
|
|
41
|
-
import { isAbsolute, resolve } from "node:path"
|
|
42
|
-
import { listSlots, slotPath } from "../session-slots.mjs"
|
|
43
|
-
|
|
44
|
-
const DEFAULT_LIMIT = 50
|
|
45
|
-
const MAX_LIMIT = 200
|
|
46
|
-
const CONTENT_CAP = 500
|
|
47
|
-
const VALID_ROLES = new Set(["user", "assistant", "tool"])
|
|
48
|
-
|
|
49
|
-
/** 单槽检索行扫护栏(SESSION.md §13 D-R19a——评审 #3 定稿:超限不再读全文,返回定稿错误文案)。 */
|
|
50
|
-
export const READ_HISTORY_SCAN_MAX = 200_000
|
|
51
|
-
|
|
52
|
-
/** L24 消息数预算(评审 #2 钉死——双保险第二道):行扫按物理 \n 行计——JSON 单行槽
|
|
53
|
-
* 行扫不设防——parse 后 history 数组长度超限即拒(同款定稿文案——双端同常量同文案)。 */
|
|
54
|
-
export const READ_HISTORY_MAX_MESSAGES = 50_000
|
|
55
|
-
|
|
56
|
-
/** 超限错误文案(SESSION.md §13——逐字定稿——T-R19.7 断言)。 */
|
|
57
|
-
const TOO_LARGE_ERROR = JSON.stringify({ error: "session too large — refine keyword or since/until" })
|
|
58
|
-
|
|
59
|
-
/** 检索/记忆族消歧总纲(SESSION.md §13 D-R19b——逐字定稿——read_history 描述尾段——T-R19.5 锚)。 */
|
|
60
|
-
const SEARCH_FAMILY_GUIDE =
|
|
61
|
-
"检索/记忆族选哪个:查**本会话**说过/裁定过 → read_history(默认);查**别的会话/项目**旧对话 → read_history 带 path/cwd 参数;查**本 run 改过哪些文件** → recent_changes;查**跨会话已存知识/约定**(memory)→ memory search;查**项目设计文档** → doc_search;查**代码实现** → code_search;查 git 历史快照 → checkpoint cat/versions。read_history 只查会话消息——文件级改动用 recent_changes——知识与约定用 memory——互相不替代。"
|
|
62
|
-
|
|
63
|
-
/** Message text for keyword matching + output: strings pass through; multimodal content arrays → text parts joined (never crashes, empty parts skipped, images ignored). */
|
|
64
|
-
function messageText(m) {
|
|
65
|
-
if (typeof m?.content === "string") return m.content
|
|
66
|
-
if (Array.isArray(m?.content)) {
|
|
67
|
-
return m.content
|
|
68
|
-
.map((p) => (p && typeof p === "object" && p.type === "text" ? p.text ?? "" : ""))
|
|
69
|
-
.filter((t) => t.length > 0)
|
|
70
|
-
.join(" ")
|
|
71
|
-
}
|
|
72
|
-
return ""
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Truncate long content (~500 chars) with an explicit marker. The cut never splits a UTF-16
|
|
76
|
-
* surrogate pair (emoji etc.) — a lone high surrogate in tool output would be an eyesore at
|
|
77
|
-
* minimum; the send layer sanitizes it anyway, but clean output costs nothing (setup.mjs
|
|
78
|
-
* safeSliceUTF16 same rule). */
|
|
79
|
-
function truncateContent(text) {
|
|
80
|
-
const t = String(text ?? "")
|
|
81
|
-
if (t.length <= CONTENT_CAP) return t
|
|
82
|
-
let end = CONTENT_CAP
|
|
83
|
-
if (t.charCodeAt(end - 1) >= 0xd800 && t.charCodeAt(end - 1) <= 0xdbff) end-- // high surrogate at the cut → step back
|
|
84
|
-
return t.slice(0, end) + `\n… (truncated: ${t.length - end} chars — full text is in the session file)`
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Tool name across both stored shapes ({function:{name}} and flat {name}). */
|
|
88
|
-
function toolCallName(tc) {
|
|
89
|
-
return tc?.function?.name ?? tc?.name ?? ""
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Parse a ts window boundary (epoch ms number; numeric strings tolerated). Returns the number or an error string. */
|
|
93
|
-
function parseTs(value, label) {
|
|
94
|
-
if (value === undefined || value === null) return null
|
|
95
|
-
const n = typeof value === "number" ? value : Number(value)
|
|
96
|
-
if (!Number.isFinite(n)) return `Error: invalid ${label} "${value}" — must be epoch milliseconds (number)`
|
|
97
|
-
return n
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Map one matched message to its JSON entry shape. */
|
|
101
|
-
function toEntry(m) {
|
|
102
|
-
const entry = {
|
|
103
|
-
ts: typeof m.ts === "number" ? m.ts : null,
|
|
104
|
-
role: m.role ?? null,
|
|
105
|
-
}
|
|
106
|
-
if (m.name !== undefined) entry.name = m.name
|
|
107
|
-
if (m.tool_call_id !== undefined) entry.tool_call_id = m.tool_call_id
|
|
108
|
-
entry.content = truncateContent(messageText(m))
|
|
109
|
-
if (Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
110
|
-
entry.tool_calls = m.tool_calls.map(toolCallName).filter(Boolean)
|
|
111
|
-
}
|
|
112
|
-
return entry
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** AND-filter one history message (role/keyword/tool/since-until) — shared by the in-memory
|
|
116
|
-
* default and the cross-session file query (SESSION.md §13 D-R19a: 同 filter 面应用). */
|
|
117
|
-
function matches(m, { role, kwRe, tool, since, until }) {
|
|
118
|
-
if (!m || typeof m !== "object") return false
|
|
119
|
-
if (role !== undefined && m.role !== role) return false
|
|
120
|
-
if (kwRe) {
|
|
121
|
-
const text = messageText(m)
|
|
122
|
-
if (!kwRe.test(text)) return false
|
|
123
|
-
}
|
|
124
|
-
if (tool) {
|
|
125
|
-
const byName = m.role === "tool" && m.name === tool
|
|
126
|
-
const byDeclaration = m.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.some((tc) => toolCallName(tc) === tool)
|
|
127
|
-
if (!byName && !byDeclaration) return false
|
|
128
|
-
}
|
|
129
|
-
const ts = m.ts
|
|
130
|
-
if (since !== null || until !== null) {
|
|
131
|
-
if (typeof ts !== "number") return false // no ts → no time-window match
|
|
132
|
-
if (since !== null && ts < since) return false
|
|
133
|
-
if (until !== null && ts > until) return false
|
|
134
|
-
}
|
|
135
|
-
return true
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Direction picks the END of the matched set; output stays chronological either way. */
|
|
139
|
-
function formatMatches(matched, direction, limit) {
|
|
140
|
-
const windowed = direction === "oldest" ? matched.slice(0, limit) : matched.slice(-limit)
|
|
141
|
-
return JSON.stringify(windowed.map(toEntry))
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Line-scan guard: stream-count physical newlines, bailing the moment the cap is crossed —
|
|
145
|
-
* an oversized file is refused BEFORE it is read whole ("不再读全文"——SESSION.md §13 D-R19a). */
|
|
146
|
-
function exceedsScanMax(file) {
|
|
147
|
-
const CHUNK = 64 * 1024
|
|
148
|
-
let fd = null
|
|
149
|
-
try {
|
|
150
|
-
fd = openSync(file, "r")
|
|
151
|
-
const buf = Buffer.alloc(CHUNK)
|
|
152
|
-
let newlines = 0
|
|
153
|
-
for (;;) {
|
|
154
|
-
const n = readSync(fd, buf, 0, CHUNK, null)
|
|
155
|
-
if (n <= 0) break
|
|
156
|
-
for (let i = 0; i < n; i++) {
|
|
157
|
-
if (buf[i] === 0x0a) newlines++
|
|
158
|
-
}
|
|
159
|
-
if (newlines > READ_HISTORY_SCAN_MAX) return true
|
|
160
|
-
}
|
|
161
|
-
return false
|
|
162
|
-
} catch {
|
|
163
|
-
return false // 行扫失败 → 交由后续读取/解析路径给出真实错误
|
|
164
|
-
} finally {
|
|
165
|
-
if (fd !== null) {
|
|
166
|
-
try { closeSync(fd) } catch { /* ignore */ }
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Cross-session deep query: one session file, same filter surface (§13 D-R19a). */
|
|
172
|
-
function querySessionFile(pathArg, { role, kwRe, tool, since, until, direction, limit }, baseCwd) {
|
|
173
|
-
const file = isAbsolute(pathArg) ? pathArg : resolve(baseCwd ?? process.cwd(), pathArg)
|
|
174
|
-
if (!existsSync(file)) return `Error: session file not found: ${file}`
|
|
175
|
-
if (exceedsScanMax(file)) return TOO_LARGE_ERROR
|
|
176
|
-
let text
|
|
177
|
-
try {
|
|
178
|
-
text = readFileSync(file, "utf8")
|
|
179
|
-
} catch (e) {
|
|
180
|
-
return `Error: failed to read session file ${file}: ${e.message}`
|
|
181
|
-
}
|
|
182
|
-
let data
|
|
183
|
-
try {
|
|
184
|
-
data = JSON.parse(text)
|
|
185
|
-
} catch (e) {
|
|
186
|
-
return `Error: ${file} is not a valid session file (corrupt JSON: ${e.message})`
|
|
187
|
-
}
|
|
188
|
-
if (!data || typeof data !== "object" || !Array.isArray(data.history)) {
|
|
189
|
-
return `Error: ${file} is not a valid session file (no history array)`
|
|
190
|
-
}
|
|
191
|
-
if (data.history.length > READ_HISTORY_MAX_MESSAGES) {
|
|
192
|
-
// L24 消息数第二道(parse 后——行扫按物理行、单行 JSON 槽行扫不设防——超限同款拒绝)。
|
|
193
|
-
return TOO_LARGE_ERROR
|
|
194
|
-
}
|
|
195
|
-
const matched = data.history.filter((m) => matches(m, { role, kwRe, tool, since, until }))
|
|
196
|
-
return formatMatches(matched, direction, limit)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Discovery surface (path = "cwd:<dir>"): list every slot stored for that directory, one line
|
|
200
|
-
* per slot — slot number + FULL session file path + title/message count/updatedAt(§13 D-R19a
|
|
201
|
-
* ——评审 #2:摘要必须含寻址字段——模型第二步深查 = 复制行内文件路径重调 path=)。 */
|
|
202
|
-
function discoverCwd(raw, baseCwd) {
|
|
203
|
-
const dir = resolve(baseCwd ?? process.cwd(), raw)
|
|
204
|
-
let st = null
|
|
205
|
-
try {
|
|
206
|
-
st = statSync(dir)
|
|
207
|
-
} catch { /* fallthrough to the explicit error below */ }
|
|
208
|
-
if (st === null || !st.isDirectory()) {
|
|
209
|
-
return `Error: unknown cwd "${raw}" — no session directory for this cwd (directory not found: ${dir})`
|
|
210
|
-
}
|
|
211
|
-
const slots = listSlots(dir) // 时间序(updatedAt 降序)——含 manifest 记录的全部槽(v1 不做死槽过滤)
|
|
212
|
-
if (slots.length === 0) {
|
|
213
|
-
return `(no session slots found for cwd: ${dir} — no sessions started there yet)`
|
|
214
|
-
}
|
|
215
|
-
const lines = slots.map((s) => {
|
|
216
|
-
const title = s.title ? `"${s.title}"` : "(untitled)"
|
|
217
|
-
return `slot ${s.slot}: ${slotPath(dir, s.slot)} — title: ${title}, messages: ${s.messageCount}, updatedAt: ${s.updatedAt}`
|
|
218
|
-
})
|
|
219
|
-
return `Session slots for cwd: ${dir} (newest first):\n${lines.join("\n")}`
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export const readHistoryTool = {
|
|
223
|
-
name: "read_history",
|
|
224
|
-
description:
|
|
225
|
-
"Query message history — THIS session by default, any session on disk with `path`. " +
|
|
226
|
-
"Default (no path): THIS session's full record (never compacted, audit-complete) — recall what " +
|
|
227
|
-
"was said or done earlier: design decisions, tool-call timing, past rulings. " +
|
|
228
|
-
"Filters combine with AND: role / keyword (case-insensitive substring of message text) / " +
|
|
229
|
-
"tool (tool result messages by name AND the assistant messages that declared the call — pair with tool_call_id / ts for timing) / " +
|
|
230
|
-
"since-until (epoch ms time window; only messages with ts can match) / limit (default 50, clamped to 200) / direction (which end of the matches to take). " +
|
|
231
|
-
"Returns a JSON array in chronological order: [{ts, role, name?, tool_call_id?, content (≈500 chars, truncated marker), tool_calls (names only)}]. " +
|
|
232
|
-
"Messages without ts return ts:null. Content is truncated — the full text is in the session file. " +
|
|
233
|
-
"Cross-session (path, optional): a session file path deep-queries THAT session's history with the same filters " +
|
|
234
|
-
"(relative paths resolve against the project cwd); \"cwd:<dir>\" lists every session slot stored for that directory — " +
|
|
235
|
-
"one line per slot: slot number + full session file path + title + message count + updatedAt; copy a listed file path into path= to deep-query it. " +
|
|
236
|
-
"A session file over 50,000 messages or 200,000 lines is refused (\"session too large\") instead of being read whole.\n" +
|
|
237
|
-
SEARCH_FAMILY_GUIDE,
|
|
238
|
-
parameters: {
|
|
239
|
-
type: "object",
|
|
240
|
-
properties: {
|
|
241
|
-
role: { type: "string", enum: ["user", "assistant", "tool"], description: "Only messages with this role." },
|
|
242
|
-
keyword: { type: "string", description: "Case-insensitive substring of the message text (multimodal messages match on their text parts)." },
|
|
243
|
-
tool: { type: "string", description: "Only messages for this tool: role=tool messages with name=tool, plus assistant messages that declared a call to it." },
|
|
244
|
-
since: { type: "integer", description: "Earliest ts to match, epoch ms, INCLUSIVE. Messages without ts never match a time window." },
|
|
245
|
-
until: { type: "integer", description: "Latest ts to match, epoch ms, INCLUSIVE. since > until yields an empty result." },
|
|
246
|
-
limit: { type: "integer", description: "Maximum messages to return (default 50; larger values are clamped to 200)." },
|
|
247
|
-
direction: { type: "string", enum: ["oldest", "newest"], description: "Take the limit window from the oldest or newest end of the matched set (default newest)." },
|
|
248
|
-
path: { type: "string", description: "Optional — query another session instead of this one: a session file path (as listed by a \"cwd:<dir>\" call) deep-queries that session; \"cwd:<dir>\" lists that directory's session slots (slot number + full file path + title + message count + updatedAt)." },
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
readonly: true,
|
|
252
|
-
execute(args, ctx) {
|
|
253
|
-
const a = args ?? {}
|
|
254
|
-
if (a.path !== undefined && (typeof a.path !== "string" || a.path.trim().length === 0)) {
|
|
255
|
-
return `Error: invalid path "${a.path}" — must be a session file path or "cwd:<dir>"`
|
|
256
|
-
}
|
|
257
|
-
const role = a.role
|
|
258
|
-
if (role !== undefined && (typeof role !== "string" || !VALID_ROLES.has(role))) {
|
|
259
|
-
return `Error: invalid role "${role}" — valid roles: user, assistant, tool`
|
|
260
|
-
}
|
|
261
|
-
const direction = a.direction ?? "newest"
|
|
262
|
-
if (direction !== "oldest" && direction !== "newest") {
|
|
263
|
-
return `Error: invalid direction "${direction}" — valid values: oldest, newest`
|
|
264
|
-
}
|
|
265
|
-
const since = parseTs(a.since, "since")
|
|
266
|
-
if (typeof since === "string") return since
|
|
267
|
-
const until = parseTs(a.until, "until")
|
|
268
|
-
if (typeof until === "string") return until
|
|
269
|
-
let limit = DEFAULT_LIMIT
|
|
270
|
-
if (a.limit !== undefined) {
|
|
271
|
-
limit = Math.floor(Number(a.limit))
|
|
272
|
-
if (!Number.isFinite(limit)) return `Error: invalid limit "${a.limit}" — must be a number`
|
|
273
|
-
limit = Math.min(Math.max(1, limit), MAX_LIMIT)
|
|
274
|
-
}
|
|
275
|
-
const keyword = typeof a.keyword === "string" && a.keyword.length > 0 ? a.keyword : null
|
|
276
|
-
// Case-insensitive substring WITHOUT copying the full message text: the human line is
|
|
277
|
-
// never compacted (绑定态存储行 = slimForDisplay 产物——匹配基准 delta 见 SESSION.md
|
|
278
|
-
// §14.3.7 / T-RS8b) — single tool results can be hundreds of KB to MBs. Lowercase the
|
|
279
|
-
// needle once and run a regex-i test over the haystack (escaping regex metachars so the
|
|
280
|
-
// keyword stays a literal substring).
|
|
281
|
-
const kwRe = keyword ? new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : null
|
|
282
|
-
const tool = typeof a.tool === "string" && a.tool.length > 0 ? a.tool : null
|
|
283
|
-
const baseCwd = ctx.agent?.cwd ?? process.cwd()
|
|
284
|
-
|
|
285
|
-
const pathArg = typeof a.path === "string" && a.path.trim().length > 0 ? a.path.trim() : null
|
|
286
|
-
if (pathArg !== null) {
|
|
287
|
-
return pathArg.startsWith("cwd:")
|
|
288
|
-
? discoverCwd(pathArg.slice("cwd:".length), baseCwd)
|
|
289
|
-
: querySessionFile(pathArg, { role, kwRe, tool, since, until, direction, limit }, baseCwd)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// 本会话(无 path):绑定记录存储 → 方向流式迭代(磁盘为准——全量可见、内存窗口外
|
|
293
|
-
// 可命中;§14.3.7);未绑定(测试 / 模式 F)→ 内存 _fullHistory 既有过滤路径(回退保留)。
|
|
294
|
-
// 方向语义不变:newest 自尾向前取满 limit → 反转回时间序(输出恒时间序)。
|
|
295
|
-
const store = ctx.agent?._recordStore
|
|
296
|
-
if (store?.iterate) {
|
|
297
|
-
const taken = []
|
|
298
|
-
for (const m of store.iterate(direction)) {
|
|
299
|
-
if (!matches(m, { role, kwRe, tool, since, until })) continue
|
|
300
|
-
taken.push(m)
|
|
301
|
-
if (taken.length >= limit) break
|
|
302
|
-
}
|
|
303
|
-
return formatMatches(direction === "newest" ? taken.reverse() : taken, "oldest", limit)
|
|
304
|
-
}
|
|
305
|
-
const history = Array.isArray(ctx.agent?._fullHistory) ? ctx.agent._fullHistory : []
|
|
306
|
-
const matched = history.filter((m) => matches(m, { role, kwRe, tool, since, until }))
|
|
307
|
-
return formatMatches(matched, direction, limit)
|
|
308
|
-
},
|
|
309
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* recent_changes tool: list files touched by this agent run (write/edit/insert_after/delete).
|
|
3
|
-
* More precise than git status — only looks at this session's changes, independent of git tracking.
|
|
4
|
-
* Helps the model recall what it already modified during long tasks.
|
|
5
|
-
*/
|
|
6
|
-
export const recentChangesTool = {
|
|
7
|
-
name: "recent_changes",
|
|
8
|
-
description:
|
|
9
|
-
"Show files modified in this agent run (write/edit/insert_after/delete). " +
|
|
10
|
-
"Use when you need to remember which files you've already touched — during long multi-file tasks, " +
|
|
11
|
-
"it's easy to lose track. This is scoped to the current run, unlike git status which shows all uncommitted changes. " +
|
|
12
|
-
"For session-level history (what was said in a session), use read_history.",
|
|
13
|
-
parameters: {
|
|
14
|
-
type: "object",
|
|
15
|
-
properties: {},
|
|
16
|
-
},
|
|
17
|
-
readonly: true,
|
|
18
|
-
execute(args, ctx) {
|
|
19
|
-
const files = ctx.agent._touchedFiles ?? []
|
|
20
|
-
if (files.length === 0) return "(no files modified in this run yet)"
|
|
21
|
-
const deduped = [...new Set(files)]
|
|
22
|
-
return `Touched ${deduped.length} file(s) this run:\n${deduped.join("\n")}`
|
|
23
|
-
},
|
|
24
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* review-streak.mjs — design-review failure streak guard(第 33 批——2026-09-11)。
|
|
3
|
-
*
|
|
4
|
-
* 同一 doc-set 的 design 评审**连续**未产出可用结算(宿主截断尾五 kind + 陈旧结算 +
|
|
5
|
-
* 凭证落盘失败 + 无报告结算)达 `MAX_DESIGN_REVIEW_STREAK` 次 ⇒ 后续发起被拒
|
|
6
|
-
* (工具层预检 + `runAdvisorReview` 内防线两级——零 LLM / 不耗轮次 / 不置「评审已覆盖」)。
|
|
7
|
-
* 设计权威 = `docs/design/ADVISOR-CONVERGENCE.md` §17(需求 = requirements 档 §12 F28/F29 + N20/N21)。
|
|
8
|
-
*
|
|
9
|
-
* 中立模块(**不 import 任何 src/ 模块**——打断潜在环,§17.5 模块图):
|
|
10
|
-
* - `normAbs` 自 advisor-settle.mjs 迁入(原处 re-export——既有 import 面零变);
|
|
11
|
-
* - `docSetKey` 自 advisor-async.mjs 迁入(原为私有——零 import 面)。
|
|
12
|
-
* 分类单源:`designReviewOutcome` 为异步结算 / 同步面两计数点共用(D-SK8——防两处分类漂移)。
|
|
13
|
-
* 载体 = 会话级内存 `agent._designReviewStreaks`(Map——不落盘、不进 session 文件;
|
|
14
|
-
* eng 模式切换不清护栏——与 `_advisorRuns` 刻意不同步,§17.9 #5 定案)。
|
|
15
|
-
*/
|
|
16
|
-
import { join } from "node:path"
|
|
17
|
-
|
|
18
|
-
/** 连续未产出可用结算的阈值(三振——§17.2 表 1 选定 N=3;`MAX_ADVISOR_ROUNDS` 零改动)。 */
|
|
19
|
-
export const MAX_DESIGN_REVIEW_STREAK = 3
|
|
20
|
-
|
|
21
|
-
/** ABS 归一(cwd 相对 → cwd 拼接)——陈旧判定 / 冻结拦截 / doc-set 键同源(§14.14 E-4)。 */
|
|
22
|
-
export function normAbs(p, cwd) {
|
|
23
|
-
const s = String(p)
|
|
24
|
-
return /^[a-zA-Z]:[\\/]/.test(s) || s.startsWith("/") || s.startsWith("\\\\") ? s : join(cwd, s)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Canonical scope key for design reviews — the document multi-set
|
|
28
|
-
* (order-insensitive, ABS-path normalized — launch 与 continuation 的写法差异
|
|
29
|
-
* ("./docs/x.md" vs "docs/x.md"、反斜杠) 不误建新实例).
|
|
30
|
-
* 第 33 批自 advisor-async.mjs 逐字迁入(护栏与实例续跑同锚单源)。 */
|
|
31
|
-
export function docSetKey(documents, cwd) {
|
|
32
|
-
const list = [...new Set((documents ?? [])
|
|
33
|
-
.filter((d) => typeof d === "string" && d.trim())
|
|
34
|
-
.map((d) => normAbs(d, cwd)))]
|
|
35
|
-
return JSON.stringify(list.sort())
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** 护栏适用的键判据:空清单(`[]` / `null` 的 `docSetKey` 产出 = "[]")不适用——不计数、
|
|
39
|
-
* 不停止(§17.9 #3 登记;fail-open 于此面)。 */
|
|
40
|
-
export function designReviewStreakApplies(key) {
|
|
41
|
-
return typeof key === "string" && key.length > 0 && key !== "[]"
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 结算分类(纯函数单源——§17.3 优先级表,自上而下首个命中;输入 = 宿主可见事实,
|
|
46
|
-
* **零 LLM 输出解析**,N20)。neutral = 无信息事件(取消 / 中断 / 拒发)——不打断连续计数。
|
|
47
|
-
* @param {{launchRefused?: boolean, stale?: boolean, hasResult?: boolean,
|
|
48
|
-
* incomplete?: string|null, persistFailed?: boolean}} input
|
|
49
|
-
* @returns {{reset: boolean, count: string|null}} reset=true 计数复位;count=需加一的类名;
|
|
50
|
-
* 两者皆空 = neutral(不动计数)。
|
|
51
|
-
*/
|
|
52
|
-
export function designReviewOutcome(input) {
|
|
53
|
-
const { launchRefused = false, stale = false, hasResult = true, incomplete = null, persistFailed = false } = input ?? {}
|
|
54
|
-
if (launchRefused) return { reset: false, count: null } // 1. 未发起请求(无尝试发生——§14.4 既有语义)
|
|
55
|
-
if (stale) return { reset: false, count: "stale" } // 2. 陈旧结算(未产出可用凭证)
|
|
56
|
-
if (!hasResult) return { reset: false, count: "no_report" } // 3. 无报告(fail-closed)
|
|
57
|
-
if (incomplete && incomplete !== "interrupted") return { reset: false, count: String(incomplete) } // 4. 宿主截断尾五 kind
|
|
58
|
-
if (incomplete === "interrupted") return { reset: false, count: null } // 5. 用户 / 系统中断类(与 cancelled 同族)
|
|
59
|
-
if (persistFailed) return { reset: false, count: "no_credential" } // 6. pass 但槽落盘失败(无可用凭证)
|
|
60
|
-
return { reset: true, count: null } // 7. 可用判决(pass 且落盘成功 / changes-required)——连续链断点
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 记录只读视图(停止判定与结论表的数据源);空清单键 / 无载体 ⇒ null。 */
|
|
64
|
-
export function designReviewStreakRecord(agent, key) {
|
|
65
|
-
if (!designReviewStreakApplies(key)) return null
|
|
66
|
-
const streaks = agent?._designReviewStreaks
|
|
67
|
-
return streaks instanceof Map ? (streaks.get(key) ?? null) : null
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** 计数落账(§17.4):reset ⇒ 删除该键记录;count ⇒ `{count: prev+1, log: […].slice(-N)}`;
|
|
71
|
-
* neutral ⇒ 不动。载体懒初始化。 */
|
|
72
|
-
export function noteDesignReviewOutcome(agent, key, outcome) {
|
|
73
|
-
if (!agent || !designReviewStreakApplies(key) || !outcome) return
|
|
74
|
-
if (outcome.reset) {
|
|
75
|
-
if (agent._designReviewStreaks instanceof Map) agent._designReviewStreaks.delete(key)
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
if (typeof outcome.count !== "string" || !outcome.count) return
|
|
79
|
-
const streaks = agent._designReviewStreaks instanceof Map
|
|
80
|
-
? agent._designReviewStreaks
|
|
81
|
-
: (agent._designReviewStreaks = new Map())
|
|
82
|
-
const prev = streaks.get(key)
|
|
83
|
-
streaks.set(key, {
|
|
84
|
-
count: (prev?.count ?? 0) + 1,
|
|
85
|
-
log: [...(prev?.log ?? []), outcome.count].slice(-MAX_DESIGN_REVIEW_STREAK),
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** 停止判定:记录达阈值即停;空清单键恒 false。停止不可自解除(复位仅经可用判决——被拒后
|
|
90
|
-
* 无法发生;会话结束随载体清零)。 */
|
|
91
|
-
export function designReviewStreakStopped(agent, key) {
|
|
92
|
-
return (designReviewStreakRecord(agent, key)?.count ?? 0) >= MAX_DESIGN_REVIEW_STREAK
|
|
93
|
-
}
|