thincoder 0.12.61 → 0.12.63
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 +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- 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 +240 -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 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -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 +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- 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-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- 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 +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- 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 +17 -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 +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- 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 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- 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 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- 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 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- 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 -479
- 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 -453
- 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 -413
- 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 -381
- 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 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- 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 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- 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 -217
- package/src/prompts/discipline-normal.md +0 -179
- 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 -55
- package/src/prompts/persona-engineering.md +0 -54
- 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 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- 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 -224
package/src/agent-tools/eng.mjs
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* eng tool: enter/exit engineering mode.
|
|
3
|
-
* In engineering mode the agent follows design-before-code methodology.
|
|
4
|
-
* Toggled here at session level (in-memory flag; the session slot is the sole authority —
|
|
5
|
-
* saveSession round-trips it at turn end; /eng writes the slot directly). The old ctx
|
|
6
|
-
* state-persist hook was removed 2026-09-08 (ENG-SESSION-PROVIDER-CLEANUP D1.2): no
|
|
7
|
-
* provider existed and its legacy payload keys were never read by applySession.
|
|
8
|
-
* R16 (2026-09-06): design tokens are session-level flow credentials — mode toggles
|
|
9
|
-
* do NOT clear them (ON→OFF keeps, OFF→ON does not require a fresh review); only TTL
|
|
10
|
-
* expiry cleans tokens, at three points: restore filtering (session.mjs applySession) /
|
|
11
|
-
* eng enter expired cleanup (below + cmd-eng ON) / spawn-gate expired rejection
|
|
12
|
-
* (subagent-spawn.mjs). See docs/design/ENG-TOKEN-BINDING-TUNING.md §5/§5.1 (F-R16).
|
|
13
|
-
*/
|
|
14
|
-
import { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "../agent.mjs"
|
|
15
|
-
import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
|
|
16
|
-
|
|
17
|
-
export const engTool = {
|
|
18
|
-
name: "eng",
|
|
19
|
-
description:
|
|
20
|
-
"Enter or exit engineering mode. In engineering mode, follow design-before-code: write a design document, run advisor design review, get user approval, then implement via eng-coder subagents. " +
|
|
21
|
-
"Returns the mode state — 'Engineering mode activated/exited' (an already-active state is acknowledged).",
|
|
22
|
-
parameters: {
|
|
23
|
-
type: "object",
|
|
24
|
-
properties: {
|
|
25
|
-
action: { type: "string", enum: ["enter", "exit"], description: "Enter or exit engineering mode" },
|
|
26
|
-
},
|
|
27
|
-
required: ["action"],
|
|
28
|
-
},
|
|
29
|
-
readonly: true,
|
|
30
|
-
async execute(args, ctx) {
|
|
31
|
-
ctx.agent.config.agent ??= {}
|
|
32
|
-
if (args.action === "exit") {
|
|
33
|
-
ctx.agent.config.agent.engineering = false
|
|
34
|
-
// R16 (F-R16a): OFF 不清 token——有效 token 跨模式存活(设计评审过的产物不因
|
|
35
|
-
// 开关重复烧)。过期清理跑在另外三处(恢复过滤 / 开模式 / spawn 门禁拒)。
|
|
36
|
-
ctx.agent._advisorRound = 0 // reset convergence budget
|
|
37
|
-
ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
|
|
38
|
-
ctx.agent._touchedFiles = [] // clear mutation tracking
|
|
39
|
-
ctx.agent._lastEngState = false
|
|
40
|
-
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
41
|
-
ctx.agent._pendingReminders.push(ENG_OFF_REMINDER)
|
|
42
|
-
return "Engineering mode exited. Standard discipline now applies. You may edit files directly."
|
|
43
|
-
}
|
|
44
|
-
if (args.action === "enter") {
|
|
45
|
-
// Idempotent enter (v2 2026-08-25): already in engineering mode → pure no-op
|
|
46
|
-
// (cleanup only runs on a real off→on transition — T-R16c precondition: first
|
|
47
|
-
// exit/OFF, then enter; an already-on enter must not touch tokens at all).
|
|
48
|
-
if (ctx.agent.config.agent.engineering) {
|
|
49
|
-
return "Engineering mode already active. Existing design tokens stay valid."
|
|
50
|
-
}
|
|
51
|
-
ctx.agent.config.agent.engineering = true
|
|
52
|
-
// R16 (F-R16b ②): off→on 不重评——只清过期 token(用户裁定"打开工程模式时
|
|
53
|
-
// 应该清理"),有效 token 原样保留——遍历 Map 删过期,返回文案含清理个数。
|
|
54
|
-
const cleared = purgeExpiredDesignTokens(ctx.agent)
|
|
55
|
-
ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
|
|
56
|
-
ctx.agent._lastEngState = true
|
|
57
|
-
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
58
|
-
ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
|
|
59
|
-
let msg = "Engineering mode activated. Design-before-code enforced: write a design document first (location per your project's document conventions), run advisor with type='design', get user approval, then implement via eng-coder subagents."
|
|
60
|
-
if (cleared > 0) {
|
|
61
|
-
msg += ` Cleared ${cleared} expired design token${cleared === 1 ? "" : "s"} — 清 ${cleared} 个过期 token,有效 token 保留(TTL 内不重评)。`
|
|
62
|
-
}
|
|
63
|
-
return msg
|
|
64
|
-
}
|
|
65
|
-
return "Invalid action: expected 'enter' or 'exit'"
|
|
66
|
-
},
|
|
67
|
-
}
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* escalate-async.mjs — async 飞刀 runner (AGENT-LOOP.md §25 D-R17b — R17, 2026-09-06).
|
|
3
|
-
*
|
|
4
|
-
* The subagent action:"escalate" is DEFAULT-async at depth 0 (decision ③: the
|
|
5
|
-
* depth-0-only escalate flips to async like every other background family;
|
|
6
|
-
* `async: false` keeps the legacy synchronous flight). An async escalate runs in
|
|
7
|
-
* the SHARED "other" pool domain (decision Q2 🅰 — the entry lives in
|
|
8
|
-
* `_asyncSubagents` with role "escalate"/_pool "other", so it shares the 4-slot
|
|
9
|
-
* other domain with explore/plan/coder spawns — capacity, queueing, refill,
|
|
10
|
-
* status and cancel all come from the generic machinery):
|
|
11
|
-
* - launch → ack {id, role:"escalate", status:"running"|"queued"[, position]};
|
|
12
|
-
* the turn ends naturally, the session suspends (poolLive counts the entry).
|
|
13
|
-
* - settle THREE-WAY classification (review #4): done → merge-all mutations back
|
|
14
|
-
* into the parent's bookkeeping + overlap warning appended to the report
|
|
15
|
-
* (report-level, not a gate — round2 #4); error (child failure / turn cap) →
|
|
16
|
-
* partial mutations merged ONLY when the parent did not touch overlapping
|
|
17
|
-
* files since launch (overlap → no merge + differences listed in the report);
|
|
18
|
-
* cancelled → never reaches the digest stream (D-M6 — no merge, stopped
|
|
19
|
-
* reminder).
|
|
20
|
-
* - the classified report lands in `_pendingAsyncResults`(pending 单容器 +role——
|
|
21
|
-
* ASYNC-RESULT-CONTAINER.md D2——挂起期 settle 移交)when the settle happens in a
|
|
22
|
-
* suspension, or stays pooled for the turn-end collection otherwise;
|
|
23
|
-
* injectAsyncResult's escalate
|
|
24
|
-
* role branch delivers the digest ("报告已 merge——可继续处置" — the action
|
|
25
|
-
* domain still follows the consuming turn's tier — no family exception).
|
|
26
|
-
*/
|
|
27
|
-
import { relative, isAbsolute } from "node:path"
|
|
28
|
-
import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
|
|
29
|
-
import { runWithContinue, TURN_CAP_MARK, wrapChildCallbacks } from "../agent/spawn-child.mjs"
|
|
30
|
-
import { logEvent } from "../log.mjs"
|
|
31
|
-
import { deathLine } from "../abort-provenance.mjs"
|
|
32
|
-
import {
|
|
33
|
-
mergeChildMutations, runningPoolCount, poolDomainOf, poolLimitsFor, ASYNC_POOL_LIMITS, enqueueAsk,
|
|
34
|
-
} from "./subagent-async.mjs"
|
|
35
|
-
import { refreshQueuedTokens, nextSubagentId } from "./subagent-scheduler.mjs"
|
|
36
|
-
import { mutationSeqOf } from "./advisor-async.mjs"
|
|
37
|
-
// ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
|
|
38
|
-
import { buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
|
|
39
|
-
|
|
40
|
-
/** Parent-side mutations (absolute paths) committed AFTER the escalate launch —
|
|
41
|
-
* the overlap scan feeds the settle classification (review #4/round2 #4: the
|
|
42
|
-
* parent may have edited files while the flight ran). */
|
|
43
|
-
function parentMutationsSince(parent, launchSeq) {
|
|
44
|
-
const out = []
|
|
45
|
-
for (const m of parent?._mutLog ?? []) {
|
|
46
|
-
if (m.seq <= (launchSeq ?? -1)) continue
|
|
47
|
-
for (const p of m.paths ?? []) if (!out.includes(p)) out.push(p)
|
|
48
|
-
}
|
|
49
|
-
return out
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Files BOTH sides touched since launch (case-insensitive key on win32 —
|
|
53
|
-
* filesOverlap precedent). */
|
|
54
|
-
function overlapPaths(parent, launchSeq, childTouched) {
|
|
55
|
-
const since = parentMutationsSince(parent, launchSeq)
|
|
56
|
-
if (since.length === 0 || !childTouched?.length) return []
|
|
57
|
-
const key = (p) => (process.platform === "win32" ? String(p).toLowerCase() : String(p))
|
|
58
|
-
const sinceKeys = new Set(since.map(key))
|
|
59
|
-
const hits = childTouched.filter((p) => sinceKeys.has(key(p)))
|
|
60
|
-
// relative display form (touchedFilesNote precedent)
|
|
61
|
-
const cwd = parent?.cwd ?? process.cwd()
|
|
62
|
-
return hits.map((p) => {
|
|
63
|
-
const r = relative(cwd, p)
|
|
64
|
-
return r && !r.startsWith("..") && !isAbsolute(r) ? r : p
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Escalate-side touched files, relative display (or a placeholder). */
|
|
69
|
-
function childTouchedDisplay(child, cwd) {
|
|
70
|
-
const touched = child?._touchedFiles ?? []
|
|
71
|
-
if (touched.length === 0) return "(none recorded)"
|
|
72
|
-
const shown = touched.map((f) => {
|
|
73
|
-
const r = relative(cwd ?? process.cwd(), f)
|
|
74
|
-
return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
|
|
75
|
-
})
|
|
76
|
-
return shown.join(", ")
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Relative touched-file list (mirror of the sync path's note). */
|
|
80
|
-
function touchedFilesNote(child, cwd) {
|
|
81
|
-
const touched = child?._touchedFiles ?? []
|
|
82
|
-
if (touched.length === 0) return ""
|
|
83
|
-
const shown = touched.map((f) => {
|
|
84
|
-
const r = relative(cwd ?? process.cwd(), f)
|
|
85
|
-
return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
|
|
86
|
-
})
|
|
87
|
-
return `\nTouched files: ${shown.join(", ")}`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Async escalate settle — three-way classification (§25 D-R17b review #4):
|
|
92
|
-
* - done: merge ALL mutations (the escalation's changes are the parent's —
|
|
93
|
-
* verify/advisor guards must see them) + overlap warning into the report
|
|
94
|
-
* (report-level hint — not a gate — round2 #4);
|
|
95
|
-
* - error (child failure / turn cap): partial mutations merge ONLY without a
|
|
96
|
-
* parent-side overlap — overlap → NO merge + differences listed (the report
|
|
97
|
-
* carries both sides so the model can decide);
|
|
98
|
-
* - cancelled: nothing merges, never reaches pending (D-M6 — stopped reminder).
|
|
99
|
-
* Runs BEFORE the pending transfer / digest injection. Mutates entry.report /
|
|
100
|
-
* entry.error into the FINAL digest body (the family wording rides
|
|
101
|
-
* injectAsyncResult's escalate branch).
|
|
102
|
-
*/
|
|
103
|
-
export function classifyEscalateSettle(parent, entry) {
|
|
104
|
-
if (entry.cancelled) return { cancelled: true }
|
|
105
|
-
const child = entry.childAgent
|
|
106
|
-
const touched = child?._touchedFiles ?? []
|
|
107
|
-
const overlap = overlapPaths(parent, entry.launchSeq, touched)
|
|
108
|
-
const hasError = entry.error != null
|
|
109
|
-
const raw = hasError ? entry.error : entry.report
|
|
110
|
-
if (hasError) {
|
|
111
|
-
// error branch (child failure / turn cap — review #4): partial mutations
|
|
112
|
-
// merge ONLY when the parent did not touch overlapping files since launch.
|
|
113
|
-
// childAgent may be null when the flight died before child creation (advisor
|
|
114
|
-
// 复评 🟡2——防御:无 child 即无 partial mutations——不 merge 不崩)。
|
|
115
|
-
const merged = child != null && overlap.length === 0 && mergeChildMutations(parent, child)
|
|
116
|
-
let decision = ""
|
|
117
|
-
if (merged) {
|
|
118
|
-
decision = `\nPartial changes merged into the parent's bookkeeping (no parent-side overlap since launch).`
|
|
119
|
-
} else if (overlap.length > 0) {
|
|
120
|
-
decision = `\nPartial changes NOT merged — the parent changed overlapping files while this escalate ran: ${overlap.join(", ")}. Escalate-side changes: ${childTouchedDisplay(child, parent?.cwd)}. Review the conflict and decide what to keep (report-level — not a gate; AGENT-LOOP.md §25 D-R17b).`
|
|
121
|
-
}
|
|
122
|
-
// (nothing to merge + no overlap → the plain error report stands alone)
|
|
123
|
-
entry.error = `${raw}${decision}`
|
|
124
|
-
entry.report = null
|
|
125
|
-
return { cancelled: false, merged, overlap }
|
|
126
|
-
}
|
|
127
|
-
// done — merge-all + overlap warning into the report (report-level — not a gate)
|
|
128
|
-
const merged = mergeChildMutations(parent, child)
|
|
129
|
-
const overlapNote = overlap.length > 0
|
|
130
|
-
? `\n⚠ Overlapping writes: the parent changed ${overlap.join(", ")} while this escalate ran — mutations merged all the same; review those files before building on the report (report-level warning — not a gate; AGENT-LOOP.md §25 D-R17b round2 #4).`
|
|
131
|
-
: ""
|
|
132
|
-
entry.report = `${raw}${overlapNote}`
|
|
133
|
-
return { cancelled: false, merged, overlap }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Launch the async escalate (preflights already passed in executeEscalateAction):
|
|
138
|
-
* pool admission into the shared OTHER domain → entry in `_asyncSubagents`
|
|
139
|
-
* (generic queue/refill/status/cancel machinery) → ack. The flight starts on
|
|
140
|
-
* entry.start() (immediately, or later via maybeRefillAsync when a slot frees).
|
|
141
|
-
*/
|
|
142
|
-
export function launchEscalateAsync(parent, ctx, launch) {
|
|
143
|
-
const { task, provider, tag, effortNote } = launch
|
|
144
|
-
parent._asyncSubagents ??= new Map()
|
|
145
|
-
parent._asyncQueue ??= []
|
|
146
|
-
// Async id allocation (AGENT-LOOP.md §15 D-A1 precedent): reserve the relay
|
|
147
|
-
// counter at launch — the returned id stays stable while the entry sits queued.
|
|
148
|
-
// The [model] token (TUI block creation) is DEFERRED to actual start so queued
|
|
149
|
-
// flights don't paint an empty panel block (subagent-parity).
|
|
150
|
-
// SUBAGENT-ID-COUNTER-AGENT(2026-09-09):取号统一走 nextSubagentId(池活续号
|
|
151
|
-
// 兜底——counter 载体= agent 本体 _subAgentCounter——跨 run/跨压缩存活)。
|
|
152
|
-
const id = nextSubagentId(parent)
|
|
153
|
-
const relayPrefix = `escalate#${id}/`
|
|
154
|
-
const entry = {
|
|
155
|
-
id, role: "escalate", relayPrefix,
|
|
156
|
-
_pool: poolDomainOf("escalate"), // other — shares the domain with explore/plan/coder (§11.1 D-24a)
|
|
157
|
-
status: "queued",
|
|
158
|
-
position: undefined,
|
|
159
|
-
report: null, error: null, done: false, cancelled: false,
|
|
160
|
-
promise: null, _settle: null, _settleSeq: 0,
|
|
161
|
-
model: provider.model ?? null,
|
|
162
|
-
startedAt: null,
|
|
163
|
-
turn: 0, maxTurns: 0,
|
|
164
|
-
controller: null,
|
|
165
|
-
_files: undefined, _dependsOn: undefined,
|
|
166
|
-
childAgent: null,
|
|
167
|
-
tag, effortNote, launchSeq: mutationSeqOf(parent),
|
|
168
|
-
}
|
|
169
|
-
const limits = poolLimitsFor(parent)
|
|
170
|
-
entry.status = runningPoolCount(parent, entry._pool) >= (limits[entry._pool] ?? ASYNC_POOL_LIMITS[entry._pool])
|
|
171
|
-
? "queued" : "running"
|
|
172
|
-
entry.promise = new Promise((res) => { entry._settle = res })
|
|
173
|
-
const ctrl = new AbortController()
|
|
174
|
-
entry.controller = ctrl
|
|
175
|
-
// D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md——D5 同款:_sessionSignal 兜底)。
|
|
176
|
-
const baseSignal = buildChildSignal(parent, ctx)
|
|
177
|
-
if (baseSignal) {
|
|
178
|
-
// §20.3 站点 #10(第 24 批):hop 逐跳保 reason
|
|
179
|
-
if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
|
|
180
|
-
else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
|
|
181
|
-
}
|
|
182
|
-
// Turn mirror (⟦ev⟧turn from the child runAgent → entry.turn/maxTurns — status parity).
|
|
183
|
-
const flight = async () => {
|
|
184
|
-
entry.status = "running"
|
|
185
|
-
entry.position = undefined
|
|
186
|
-
entry.startedAt = Date.now()
|
|
187
|
-
ctx.callbacks?.onToken?.(relayPrefix + "⟦ev⟧async\x1e")
|
|
188
|
-
ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (provider.model ?? ""))
|
|
189
|
-
const child = createAgent({
|
|
190
|
-
provider,
|
|
191
|
-
tools: parent.tools,
|
|
192
|
-
config: parent.config,
|
|
193
|
-
cwd: parent.cwd,
|
|
194
|
-
memory: parent.memory,
|
|
195
|
-
// G3(施工②):overlay 摘除——coder 人格槽由 assemblePrompt 场景表承载(G3 映射)。
|
|
196
|
-
role: "coder",
|
|
197
|
-
})
|
|
198
|
-
entry.childAgent = child // settle 分类/status touched 摘要绑定(start 时刻)
|
|
199
|
-
child._logId = relayPrefix.slice(0, -1)
|
|
200
|
-
logEvent("child:spawn", { role: "escalate", id: child._logId, kind: "async", status: "running", ms: 0 })
|
|
201
|
-
const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
|
|
202
|
-
const relayOnToken = childCallbacks.onToken
|
|
203
|
-
if (relayOnToken) {
|
|
204
|
-
childCallbacks.onToken = (t) => {
|
|
205
|
-
const ev = String(t).match(/^⟦ev⟧turn\x1e(\d+)\x1e(\d+)\x1e/)
|
|
206
|
-
if (ev) {
|
|
207
|
-
entry.turn = Number(ev[1]) || 0
|
|
208
|
-
entry.maxTurns = Number(ev[2]) || 0
|
|
209
|
-
}
|
|
210
|
-
return relayOnToken(t)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
const runner = ctx.runAgent ?? runAgent
|
|
214
|
-
const runOpts = {
|
|
215
|
-
depth: 1,
|
|
216
|
-
maxTurns: parent.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS,
|
|
217
|
-
signal: entry.controller.signal,
|
|
218
|
-
}
|
|
219
|
-
// 权限按 async 子代理同款装配:AUTO 直放行;手动档经父 _permQueue(并行子代理
|
|
220
|
-
// 审批不叠弹窗)——背景飞行撞门时无 handler → denied 不悬挂(D-S7 同规则)。
|
|
221
|
-
const childPermission = parent.autoApprove
|
|
222
|
-
? async () => true
|
|
223
|
-
: async (name, toolArgs) => {
|
|
224
|
-
if (!ctx.onPermissionRequest) return false
|
|
225
|
-
const ask = () => ctx.onPermissionRequest(`escalate/${name}`, toolArgs)
|
|
226
|
-
return enqueueAsk(parent, "_permQueue", ask)
|
|
227
|
-
}
|
|
228
|
-
const report = await runWithContinue(
|
|
229
|
-
(childAgent, input, cbs, opts) => runner(childAgent, input, cbs, opts),
|
|
230
|
-
child, task,
|
|
231
|
-
{ ...childCallbacks, onPermissionRequest: childPermission },
|
|
232
|
-
runOpts,
|
|
233
|
-
{
|
|
234
|
-
// 后台飞行不弹 continue 面板(D-A3 §15 例外同款):AUTO && engineering 自动
|
|
235
|
-
// resume——escalate 只在 normal 模式可用(engineering 拒)——恒自动拒 → partial。
|
|
236
|
-
askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
|
|
237
|
-
onDeclined: (e, output) => `escalate (${tag})${entry.effortNote} ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output.slice(0, 2000)}`,
|
|
238
|
-
},
|
|
239
|
-
)
|
|
240
|
-
// done: compose the post-op body (the settle classification appends the
|
|
241
|
-
// overlap warning / merge notes afterwards)
|
|
242
|
-
return `escalate (${tag})${entry.effortNote} post-op report:\n${report || (child._capturedOutput ?? "").slice(0, 4000)}${touchedFilesNote(child, parent.cwd)}`
|
|
243
|
-
}
|
|
244
|
-
entry.start = () => {
|
|
245
|
-
flight()
|
|
246
|
-
.then((report) => {
|
|
247
|
-
// Turn-cap partial (runWithContinue auto-declined) classifies as the ERROR
|
|
248
|
-
// branch (design: 撞 turn cap = error — partial-merge decision applies):
|
|
249
|
-
// move it to entry.error so the classification + error digest wording fire.
|
|
250
|
-
if (String(report).includes(TURN_CAP_MARK)) entry.error = report
|
|
251
|
-
else entry.report = report
|
|
252
|
-
})
|
|
253
|
-
.catch((e) => {
|
|
254
|
-
// 运行失败/中止:错误文本落 entry.error(子代理同款——cancel 分支忽略它;
|
|
255
|
-
// Ctrl+I 中止的残条目由收尾消化带错误文本——不落空 "(no report)" digest)。
|
|
256
|
-
const child = entry.childAgent
|
|
257
|
-
// §20.3 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
|
|
258
|
-
entry.error = `escalate (${tag}) error: ${deathLine(e, entry.controller?.signal)}\nPartial output: ${(child?._capturedOutput ?? "").slice(0, 2000)}`
|
|
259
|
-
})
|
|
260
|
-
.finally(() => {
|
|
261
|
-
// settle 公共收尾单点(ASYNC-RESULT-CONTAINER.md D3——settleAsyncEntry):日志三连
|
|
262
|
-
// /cancelled 分支(出池+墓碑+⟦ev⟧stopped+提醒)/挂起分流(pending 单容器+出池——
|
|
263
|
-
// 统一守卫 !parentAborted——D4)/公共尾部(settleSeq/_settle/唤醒 waiter + 腾槽补位
|
|
264
|
-
// ——helper 尾部恒补)统一走共享 helper。族特有 hook = 三分类 merge 决策
|
|
265
|
-
// (classifyEscalateSettle——done/error 分类;cancelled 不经此——helper cancelled
|
|
266
|
-
// 分支先行)。
|
|
267
|
-
settleAsyncEntry(parent, entry, {
|
|
268
|
-
pool: parent._asyncSubagents,
|
|
269
|
-
ctx,
|
|
270
|
-
onAccounting: () => {
|
|
271
|
-
// Three-way settle classification (done/error/cancelled — review #4):
|
|
272
|
-
// done → merge-all + 重叠警告;error → 无父侧重叠才 partial merge;
|
|
273
|
-
// cancelled 不经此(helper cancelled 分支先行)。
|
|
274
|
-
void classifyEscalateSettle(parent, entry)
|
|
275
|
-
},
|
|
276
|
-
})
|
|
277
|
-
})
|
|
278
|
-
}
|
|
279
|
-
parent._asyncSubagents.set(String(id), entry)
|
|
280
|
-
logEvent("child:spawn", { role: "escalate", id: `escalate#${id}`, kind: "async", status: entry.status, ms: 0 })
|
|
281
|
-
if (entry.status === "queued") {
|
|
282
|
-
parent._asyncQueue.push(entry)
|
|
283
|
-
entry.position = parent._asyncQueue.length
|
|
284
|
-
refreshQueuedTokens(parent, ctx.callbacks?.onToken)
|
|
285
|
-
return JSON.stringify({ id: String(id), role: "escalate", status: "queued", position: entry.position })
|
|
286
|
-
}
|
|
287
|
-
entry.start()
|
|
288
|
-
return JSON.stringify({ id: String(id), role: "escalate", status: "running" })
|
|
289
|
-
}
|
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
|
-
}
|