thincoder 0.12.53 → 0.12.58
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 +74 -0
- package/bin/thincoder.mjs +17 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +1 -1
- package/src/acp.mjs +60 -18
- package/src/advisor/messages.mjs +4 -2
- package/src/advisor/run.mjs +2 -2
- package/src/agent/dispatch.mjs +66 -26
- package/src/agent/helpers.mjs +13 -2
- package/src/agent/setup.mjs +16 -3
- package/src/agent/spawn-child.mjs +3 -1
- package/src/agent-tools/advisor.mjs +19 -9
- package/src/agent-tools/eng.mjs +2 -0
- package/src/agent-tools/subagent-check.mjs +107 -0
- package/src/agent-tools/subagent.mjs +205 -42
- package/src/agent.mjs +68 -3
- package/src/cli/make-agent.mjs +25 -0
- package/src/cli/memory-command.mjs +28 -7
- package/src/config.mjs +120 -8
- package/src/context.mjs +28 -7
- package/src/escape.mjs +110 -22
- package/src/git/checkpoint.mjs +32 -6
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/core.mjs +78 -10
- package/src/memory/docs.mjs +33 -7
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +23 -0
- package/src/prompts/discipline.md +17 -3
- package/src/prompts/engineering.md +62 -5
- package/src/prompts/main.md +1 -0
- package/src/prompts/system.md +2 -1
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +90 -26
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session-migrate.mjs +6 -0
- package/src/session-slots.mjs +361 -0
- package/src/session.mjs +282 -306
- package/src/tools/apply_patch.md +2 -0
- package/src/tools/bash.md +2 -2
- package/src/tools/edit-batch.mjs +104 -0
- package/src/tools/edit.md +3 -0
- package/src/tools/execute.md +4 -4
- package/src/tools/execute.mjs +14 -22
- package/src/tools/file.mjs +17 -55
- package/src/tools/file_ops.md +1 -1
- package/src/tools/git-checkpoint.mjs +143 -0
- package/src/tools/git-ext.mjs +173 -0
- package/src/tools/git.md +21 -8
- package/src/tools/git.mjs +55 -177
- package/src/tools/lint.md +1 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/patch.mjs +1 -1
- package/src/tools/shared.mjs +7 -20
- package/src/tui/agent-turn.mjs +3 -3
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +2 -2
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +255 -114
- package/src/tui/cmd-new.mjs +6 -6
- package/src/tui/cmd-restore.mjs +27 -6
- package/src/tui/cmd-session.mjs +17 -4
- package/src/tui/index.mjs +28 -27
- package/src/tui/interaction.mjs +28 -1
- package/src/tui/key-handler.mjs +14 -2
- package/src/tui/layout.mjs +81 -25
- package/src/tui/mouse.mjs +41 -2
- package/src/tui/pickers.mjs +62 -4
- package/src/tui/render-conversation.mjs +36 -93
- package/src/tui/render-frame.mjs +40 -16
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +4 -2
- package/src/tui/subagent-blocks.mjs +119 -4
- package/src/tui/subagent-panel.mjs +81 -0
- package/src/tui/tool-events.mjs +61 -16
- package/src/tui/tui-lifecycle.mjs +45 -0
package/src/agent-tools/eng.mjs
CHANGED
|
@@ -22,6 +22,7 @@ export const engTool = {
|
|
|
22
22
|
if (args.action === "exit") {
|
|
23
23
|
ctx.agent.config.agent.engineering = false
|
|
24
24
|
ctx.agent._engDesignToken = null // stale token from prior design review invalidated
|
|
25
|
+
ctx.agent._engDesignTokens = new Map() // multi-design slots die with the mode (2026-09-01 fix #2)
|
|
25
26
|
ctx.agent._engDesignReviewed = false // reset gate state
|
|
26
27
|
ctx.agent._advisorRound = 0 // reset convergence budget
|
|
27
28
|
ctx.agent._touchedFiles = [] // clear mutation tracking
|
|
@@ -49,6 +50,7 @@ export const engTool = {
|
|
|
49
50
|
}
|
|
50
51
|
ctx.agent.config.agent.engineering = true
|
|
51
52
|
ctx.agent._engDesignToken = null // off→on transition requires a fresh design review
|
|
53
|
+
ctx.agent._engDesignTokens = new Map() // multi-design slots die with the mode (2026-09-01 fix #2)
|
|
52
54
|
ctx.agent._lastEngState = true
|
|
53
55
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
54
56
|
ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-check.mjs — async subagent 结果消费侧(AGENT-LOOP.md §15 D-A2)。
|
|
3
|
+
*
|
|
4
|
+
* 与 subagent.mjs 的生成侧(async 分支 + 槽位队列)分离,保持 subagent.mjs
|
|
5
|
+
* 在 500 行硬限内;本模块只承载 subagent_check 工具 + 其等待机制。
|
|
6
|
+
*
|
|
7
|
+
* 语义(D-A2):
|
|
8
|
+
* - id 缺省 → 按 ARRIVAL ORDER 返回下一个已完成的子代理(先完成先返回)
|
|
9
|
+
* - id 给定 → 阻塞到该 id 完成(queued 项先等启动再等完成)
|
|
10
|
+
* - n(必填)→ 1-based 递增读数,per-run 计数器(runAgent 非 resume 重置、
|
|
11
|
+
* turn-end 清空);乱序/重复 n 拒绝且不消费结果;超 MAX_ASYNC_CHECKS 上限
|
|
12
|
+
* 报错引导走回合收尾自动等待
|
|
13
|
+
* - 消费后从 map 删除——已消费 id 再查 = 与未知 id 同款错误(T12)
|
|
14
|
+
*/
|
|
15
|
+
import { MAX_ASYNC_CHECKS } from "./subagent.mjs"
|
|
16
|
+
|
|
17
|
+
/** Wait for an async entry to settle (or the parent signal to abort), parked on
|
|
18
|
+
* the agent's waiter list — the entry settle finally wakes every waiter (same
|
|
19
|
+
* pattern as consult_check's session waiters). Returns "aborted" on signal. */
|
|
20
|
+
function wakeOnAsyncSettle(agent, ctx) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const cleanup = () => {
|
|
23
|
+
const i = (agent._asyncWaiters ?? []).indexOf(w)
|
|
24
|
+
if (i >= 0) agent._asyncWaiters.splice(i, 1)
|
|
25
|
+
ctx.signal?.removeEventListener("abort", onAbort)
|
|
26
|
+
}
|
|
27
|
+
const w = () => { cleanup(); resolve("settled") }
|
|
28
|
+
const onAbort = () => { cleanup(); resolve("aborted") }
|
|
29
|
+
;(agent._asyncWaiters ??= []).push(w)
|
|
30
|
+
if (ctx.signal) {
|
|
31
|
+
if (ctx.signal.aborted) { onAbort(); return }
|
|
32
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true })
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* subagent_check (AGENT-LOOP.md §15 D-A2) — fetch async subagent results.
|
|
39
|
+
* - id omitted → the next completed child in ARRIVAL order (first finished first)
|
|
40
|
+
* - id given → block until THAT child finishes (queued items wait for their start)
|
|
41
|
+
* - n (required) → 1-based read counter, strictly incrementing per run (loop
|
|
42
|
+
* guard); capped at MAX_ASYNC_CHECKS per turn — beyond that, use the turn-end
|
|
43
|
+
* auto-wait. Errors never consume results.
|
|
44
|
+
* Consumed entries are deleted from the map — a re-check of the same id is the
|
|
45
|
+
* same "unknown async subagent id" error (T12).
|
|
46
|
+
*/
|
|
47
|
+
export const subagentCheckTool = {
|
|
48
|
+
name: "subagent_check",
|
|
49
|
+
readonly: true,
|
|
50
|
+
description:
|
|
51
|
+
"Fetch the result of an async subagent (subagent with async:true). Spawn async children to keep working in your own turn while they run in the background, then collect their reports here. Multiple async children return in completion (arrival) order — the first finished is returned first, so fast results are handled immediately instead of waiting for the slowest. Blocks until the target finishes.\n" +
|
|
52
|
+
"When done is true, no more results are coming (all finished and consumed) — anything left arrives automatically at turn end.\n" +
|
|
53
|
+
"Parameters:\n" +
|
|
54
|
+
"- id (optional): the subagent id from the async spawn return. Omit to fetch the next completed child (arrival order).\n" +
|
|
55
|
+
"- n (required): 1-based read counter — pass 1 on the first check, 2 on the next, and so on. Consecutive checks must be distinct tool calls (loop detector); at most 3 checks per turn — use the turn-end auto-wait for the rest.",
|
|
56
|
+
parameters: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
id: { type: "string", description: "Optional subagent id (from the async spawn return). Omit = next completed child (arrival order)." },
|
|
60
|
+
n: { type: "number", description: "1-based read counter: 1 for the first check of the turn, incrementing with each subsequent check (loop detector — consecutive checks must be distinct tool calls)." },
|
|
61
|
+
},
|
|
62
|
+
required: ["n"],
|
|
63
|
+
},
|
|
64
|
+
async execute({ id, n }, ctx) {
|
|
65
|
+
const agent = ctx.agent
|
|
66
|
+
const map = agent._asyncSubagents ?? new Map()
|
|
67
|
+
// Strict 1-based incrementing read counter (D-A2, review #1): out-of-order /
|
|
68
|
+
// repeated n is rejected WITHOUT consuming a result (T14).
|
|
69
|
+
const lastN = agent._asyncCheckLastN ?? 0
|
|
70
|
+
if (!Number.isInteger(n) || n !== lastN + 1) {
|
|
71
|
+
return JSON.stringify({ status: "error", error: "invalid read counter — pass n = lastN+1" })
|
|
72
|
+
}
|
|
73
|
+
if (n > MAX_ASYNC_CHECKS) {
|
|
74
|
+
return JSON.stringify({ status: "error", error: "check limit exceeded — use turn-end auto-wait for the rest" })
|
|
75
|
+
}
|
|
76
|
+
agent._asyncCheckLastN = n
|
|
77
|
+
|
|
78
|
+
let target = null
|
|
79
|
+
if (id !== undefined && id !== null && String(id) !== "") {
|
|
80
|
+
target = map.get(String(id))
|
|
81
|
+
// Unknown OR already-consumed ids (consumed entries are deleted) — same error (T12).
|
|
82
|
+
if (!target) return JSON.stringify({ id: String(id), status: "error", error: `unknown async subagent id: ${id}` })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Block until the target settles (specific id / next completed in arrival order).
|
|
86
|
+
for (;;) {
|
|
87
|
+
if (target) {
|
|
88
|
+
if (target.done) break
|
|
89
|
+
const woke = await wakeOnAsyncSettle(agent, ctx)
|
|
90
|
+
if (woke === "aborted") return JSON.stringify({ done: true, stopped: true })
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
const completed = [...map.values()].filter((e) => e.done)
|
|
94
|
+
if (completed.length > 0) {
|
|
95
|
+
target = completed.sort((a, b) => (a._settleSeq ?? 0) - (b._settleSeq ?? 0))[0]
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
if (map.size === 0) return JSON.stringify({ done: true })
|
|
99
|
+
const woke = await wakeOnAsyncSettle(agent, ctx)
|
|
100
|
+
if (woke === "aborted") return JSON.stringify({ done: true, stopped: true })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
map.delete(String(target.id))
|
|
104
|
+
if (target.error) return JSON.stringify({ id: String(target.id), status: "error", error: target.error })
|
|
105
|
+
return JSON.stringify({ id: String(target.id), role: target.role, status: "done", report: target.report ?? "" })
|
|
106
|
+
},
|
|
107
|
+
}
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
import { makeRelay, wrapChildCallbacks, runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
8
8
|
import { validateDesignToken } from "./advisor.mjs"
|
|
9
9
|
|
|
10
|
+
// Async subagent limits (AGENT-LOOP.md §15 D-A4): mechanical concurrency cap for
|
|
11
|
+
// background spawns + the per-turn check budget (consult-style loop guard).
|
|
12
|
+
export const ASYNC_SUBAGENT_LIMIT = 4
|
|
13
|
+
export const MAX_ASYNC_CHECKS = 3
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* subagent tool: spawn a child agent to handle an independent subtask (isolated context, only the report is returned).
|
|
12
17
|
* - role: "explore" — read-only tools, search/read/analyze (suitable for codebase exploration)
|
|
@@ -52,6 +57,38 @@ export function resolveChildProvider(parent, modelArg) {
|
|
|
52
57
|
return { ...parent.provider, model: modelArg }
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the design-token slot for an eng-coder spawn (2026-09-01 multi-design, FR3):
|
|
62
|
+
* - designId given → exact slot lookup (no match = explicit error, never a fuzzy guess)
|
|
63
|
+
* - designId omitted → exactly ONE slot must exist (single-design compatibility); with
|
|
64
|
+
* multiple slots we refuse rather than pick one (T16: never silently aim the wrong design)
|
|
65
|
+
* Returns { token } on success; throws with a parent-actionable message otherwise.
|
|
66
|
+
* The HMAC/TTL check itself stays in validateDesignToken (unchanged).
|
|
67
|
+
*/
|
|
68
|
+
export function resolveDesignSlot(parent, designIdArg) {
|
|
69
|
+
const slots = parent._engDesignTokens
|
|
70
|
+
const hasSlots = slots instanceof Map && slots.size > 0
|
|
71
|
+
const legacy = parent._engDesignToken
|
|
72
|
+
// eng(exit/enter) resets the single mirror to force a fresh review (eng.mjs) —
|
|
73
|
+
// a non-empty slot map surviving that reset must NOT resurrect stale tokens:
|
|
74
|
+
// mirror cleared + slots present = re-entered engineering mode → re-review.
|
|
75
|
+
if (!legacy && hasSlots) {
|
|
76
|
+
throw new Error("Design tokens were reset (engineering mode was re-entered) — run advisor with type='design' again and spawn with the fresh designId+token pair.")
|
|
77
|
+
}
|
|
78
|
+
if (designIdArg) {
|
|
79
|
+
if (!hasSlots || !slots.has(designIdArg)) {
|
|
80
|
+
throw new Error(`designId not found — no approved design review holds this id. Run advisor with type='design' again and pass the designId echoed with the token. (session holds ${hasSlots ? slots.size : 0} approved design slot(s))`)
|
|
81
|
+
}
|
|
82
|
+
return { token: slots.get(designIdArg) }
|
|
83
|
+
}
|
|
84
|
+
if (hasSlots && slots.size > 1) {
|
|
85
|
+
throw new Error(`Multiple approved designs in this session (${slots.size}) — pass the designId parameter (echoed with each token) to choose which design this eng-coder spawn belongs to.`)
|
|
86
|
+
}
|
|
87
|
+
if (hasSlots && slots.size === 1) return { token: [...slots.values()][0] }
|
|
88
|
+
if (legacy) return { token: legacy } // single-slot mirror fallback (pre-multi-slot sessions)
|
|
89
|
+
throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
export const subagentTool = {
|
|
56
93
|
name: "subagent",
|
|
57
94
|
description:
|
|
@@ -61,8 +98,9 @@ export const subagentTool = {
|
|
|
61
98
|
"- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). Receives git context auto-injected (branch, recent commits, working-tree state) when the project is a git repo. Its report must list what it searched and what it did NOT find. Fast — specify thoroughness in the task: quick / medium / thorough (default medium).\n" +
|
|
62
99
|
"- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
|
|
63
100
|
"- coder — full implementation. The parent's complete read/write/execute toolset plus verify and advisor for self-review. Its final report must include a delivery transparency table with one row per task requirement (Done / Simplified / Not done — no deferred column).\n" +
|
|
64
|
-
"- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review.\n" +
|
|
101
|
+
"- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review. The advisor's Approved reply also echoes a designId — pass it as the designId arg: required to pick between designs when several approved reviews are active, optional for a single design. The delivery report echoes the designId back for the audit fix round.\n" +
|
|
65
102
|
"Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-coder. The schema enum reflects the active mode.\n\n" +
|
|
103
|
+
"Async spawn (AGENT-LOOP.md §15): pass async:true to spawn WITHOUT waiting — returns {id, role, status:\"running\"} immediately so you can keep working in your own turn (read/check files, run other tools) while the child runs in the background. Collect results with subagent_check — multiple async children return in completion (arrival) order, first finished first, so fast results are handled immediately. Use async when your own turn must keep moving; use the default blocking spawn when you must see the report before continuing. Async spawns are capped at 4 concurrent (further spawns queue with a position), and top-level only.\n\n" +
|
|
66
104
|
"Writing the prompt:\n" +
|
|
67
105
|
"- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
|
|
68
106
|
"- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
|
|
@@ -76,6 +114,8 @@ export const subagentTool = {
|
|
|
76
114
|
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required." },
|
|
77
115
|
model: { type: "string", description: "Provider/model override for this sub-agent: 'provider:model', a provider name from config, or a model name on the parent's provider. Defaults to the agent.subagentModel config, then the parent's provider. Useful for offloading heavy work to a cheaper model." },
|
|
78
116
|
designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
|
|
117
|
+
designId: { type: "string", description: "Optional when role='eng-coder': the designId echoed with the approved token by advisor(type='design'). Required to pick between designs when several approved reviews are active in the session — each eng-coder carries its own designId+token pair so parallel implementations never overwrite each other. Optional for a single design." },
|
|
118
|
+
async: { type: "boolean", description: "true = spawn without waiting — returns {id} immediately, fetch results later via subagent_check. Default false (blocking)." },
|
|
79
119
|
},
|
|
80
120
|
required: ["task"],
|
|
81
121
|
},
|
|
@@ -107,9 +147,12 @@ export const subagentTool = {
|
|
|
107
147
|
|
|
108
148
|
// eng-coder token gate: the design review must have passed and the caller must
|
|
109
149
|
// present the exact token advisor issued — otherwise the child is not authorized to code.
|
|
150
|
+
// 2026-09-01: multi-design slots — the token is located by designId (exact slot,
|
|
151
|
+
// single-slot fallthrough); HMAC/TTL validation itself is unchanged.
|
|
152
|
+
let issuedToken
|
|
110
153
|
if (role === "eng-coder") {
|
|
111
|
-
|
|
112
|
-
if (!
|
|
154
|
+
issuedToken = resolveDesignSlot(parent, args.designId).token
|
|
155
|
+
if (!issuedToken || args.designToken !== issuedToken || !validateDesignToken(args.designToken)) {
|
|
113
156
|
throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
|
|
114
157
|
}
|
|
115
158
|
}
|
|
@@ -164,6 +207,12 @@ export const subagentTool = {
|
|
|
164
207
|
|
|
165
208
|
// Token-verified design review → child is authorized to modify files without re-reviewing
|
|
166
209
|
if (role === "eng-coder") child._engDesignReviewed = true
|
|
210
|
+
// designId+token ride the child bookkeeping: the delivery report carries the designId
|
|
211
|
+
// so the divergence-audit fix round re-spawns with the SAME slot (2026-09-01 FR3).
|
|
212
|
+
if (role === "eng-coder" && issuedToken) {
|
|
213
|
+
child._engDesignId = args.designId ?? null
|
|
214
|
+
child._engDesignToken = issuedToken
|
|
215
|
+
}
|
|
167
216
|
|
|
168
217
|
// explore/plan: inject git context (branch/recent commits/working tree state) — exploration and planning both relate to current repo state (inspired by kimi-code's promptPrefix)
|
|
169
218
|
let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
|
|
@@ -176,13 +225,22 @@ export const subagentTool = {
|
|
|
176
225
|
// pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
|
|
177
226
|
// with the same role stay independent and don't overwrite each other.
|
|
178
227
|
// Format: role#id/ → onToken("coder#2/writing..."), onToolCall("coder#2/read", args)
|
|
179
|
-
|
|
228
|
+
// Async id allocation (AGENT-LOOP.md §15 D-A1): reserve the relay counter at
|
|
229
|
+
// spawn time — the returned id must be stable while the item sits in the queue.
|
|
230
|
+
// The [model] token (TUI block creation) is DEFERRED to actual start so queued
|
|
231
|
+
// children don't paint an empty panel block ("queued 态不显示").
|
|
232
|
+
let relayPrefix
|
|
233
|
+
if (args.async === true) {
|
|
234
|
+
parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
|
|
235
|
+
relayPrefix = `${role}#${parent._subAgentCounter}/`
|
|
236
|
+
} else {
|
|
237
|
+
relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
|
|
238
|
+
}
|
|
180
239
|
const childOpts = {
|
|
181
240
|
onPermissionRequest: childPermission,
|
|
182
241
|
...wrapChildCallbacks(relayPrefix, ctx.callbacks),
|
|
183
242
|
}
|
|
184
243
|
const childRunOpts = buildChildRunOpts(ctx)
|
|
185
|
-
let report = ""
|
|
186
244
|
// Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3):
|
|
187
245
|
// hitting the cap asks the user via the SAME y/n panel the main agent uses —
|
|
188
246
|
// unlimited continues, resume:true keeps the child's history + mutation bookkeeping,
|
|
@@ -196,49 +254,154 @@ export const subagentTool = {
|
|
|
196
254
|
parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
|
|
197
255
|
return parent._permQueue
|
|
198
256
|
}
|
|
199
|
-
const declined = { partial: null }
|
|
200
|
-
report = await runWithContinue(
|
|
201
|
-
(child, input, cbs, opts) => runAgent(child, input, cbs, opts), // opts = childRunOpts + resume (managed by the pipeline)
|
|
202
|
-
child, input, childOpts, childRunOpts,
|
|
203
|
-
{
|
|
204
|
-
askContinue: askSubagentContinue,
|
|
205
|
-
onDeclined: (e, output) => {
|
|
206
|
-
if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
|
|
207
|
-
// Early return semantics (unchanged from the inline loop): the declined
|
|
208
|
-
// partial-work message is returned WITHOUT the MIN_REPORT_CHARS expansion —
|
|
209
|
-
// re-prompting a capped child for a longer report is wrong.
|
|
210
|
-
// Review #2 fix: use the pipeline-captured output (the `report` variable is
|
|
211
|
-
// still "" at this point — runWithContinue hasn't returned yet).
|
|
212
|
-
declined.partial = `Subagent (${role}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output || ""}`
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
)
|
|
216
|
-
if (declined.partial !== null) return declined.partial
|
|
217
|
-
|
|
218
|
-
// Report too short = incomplete handoff: send back for expansion once (inspired by kimi-code's summaryPolicy: min 200 chars, retry 1 time).
|
|
219
|
-
// The child agent's history is still intact; the continuation instruction is appended as new input so it can see its own earlier work.
|
|
220
|
-
if (report.length < MIN_REPORT_CHARS) {
|
|
221
|
-
report = await runAgent(child, REPORT_CONTINUATION, childOpts, childRunOpts)
|
|
222
|
-
}
|
|
223
257
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
258
|
+
// ── Async branch (AGENT-LOOP.md §15 D-A1/D-A6): spawn without waiting ──
|
|
259
|
+
// The child runs the EXACT blocking pipeline (runChildPipeline below — relay /
|
|
260
|
+
// turn-cap / permission / MIN_REPORT_CHARS / mergeChildMutations all unchanged),
|
|
261
|
+
// but the parent does not await it: the promise is parked in _asyncSubagents and
|
|
262
|
+
// consumed via subagent_check or the turn-end auto-wait. Slot queue: running
|
|
263
|
+
// count < ASYNC_SUBAGENT_LIMIT → start now; ≥ limit → enqueue (status "queued",
|
|
264
|
+
// position = queue index) — never rejected, never requiring the model to batch.
|
|
265
|
+
if (args.async === true) {
|
|
266
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
267
|
+
throw new Error("async spawn only available at the top level")
|
|
268
|
+
}
|
|
269
|
+
parent._asyncSubagents ??= new Map()
|
|
270
|
+
parent._asyncQueue ??= []
|
|
271
|
+
const running = [...parent._asyncSubagents.values()].filter((e) => e.status === "running").length
|
|
272
|
+
const id = parent._subAgentCounter
|
|
273
|
+
const entry = {
|
|
274
|
+
id, role, relayPrefix,
|
|
275
|
+
status: running >= ASYNC_SUBAGENT_LIMIT ? "queued" : "running",
|
|
276
|
+
position: undefined,
|
|
277
|
+
report: null, error: null, done: false,
|
|
278
|
+
promise: null, _settle: null, _settleSeq: 0,
|
|
279
|
+
}
|
|
280
|
+
// The settle signal — resolves when the run chain settles (never rejects).
|
|
281
|
+
entry.promise = new Promise((res) => { entry._settle = res })
|
|
282
|
+
entry.start = () => {
|
|
283
|
+
entry.status = "running"
|
|
284
|
+
entry.position = undefined
|
|
285
|
+
// Deferred [model] emit: the TUI block is created at ACTUAL start.
|
|
286
|
+
ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (childProvider.model ?? ""))
|
|
287
|
+
// Turn-cap on background children NEVER pops the continue panel (D-A3):
|
|
288
|
+
// auto-decline, the partial-work report carries the cap reason.
|
|
289
|
+
runChildPipeline(child, input, childOpts, childRunOpts, {
|
|
290
|
+
parent, role, args,
|
|
291
|
+
askContinue: () => Promise.resolve(false),
|
|
292
|
+
})
|
|
293
|
+
.then((report) => { entry.report = report })
|
|
294
|
+
.catch((err) => { entry.error = err?.message ?? String(err) })
|
|
295
|
+
.finally(() => {
|
|
296
|
+
entry.status = "done" // running 数口径(D-A1/D-A2/T6):已完成未消费不计入
|
|
297
|
+
entry.done = true
|
|
298
|
+
// D-A3 发射时机(2026-09-02 用户实证修正):settle 同刻发射 ⟦ev⟧done——
|
|
299
|
+
// TUI routeSubToken 立即冻结区块,冻结位置 = 完成时刻的会话流位置
|
|
300
|
+
// (回合收尾统一发会把块堆在结论之后)。父会话已 abort 不发:TUI 已按
|
|
301
|
+
// interrupted 冻结,晚到 token 经 tombstone 丢弃——显式守卫更干净。
|
|
302
|
+
if (!ctx.signal?.aborted) {
|
|
303
|
+
ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
304
|
+
}
|
|
305
|
+
entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
|
|
306
|
+
entry._settle()
|
|
307
|
+
for (const w of parent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
308
|
+
maybeRefillAsync(parent)
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
parent._asyncSubagents.set(String(id), entry)
|
|
312
|
+
if (entry.status === "queued") {
|
|
313
|
+
parent._asyncQueue.push(entry)
|
|
314
|
+
entry.position = parent._asyncQueue.length
|
|
315
|
+
return JSON.stringify({ id: String(id), role, status: "queued", position: entry.position })
|
|
316
|
+
}
|
|
317
|
+
entry.start()
|
|
318
|
+
return JSON.stringify({ id: String(id), role, status: "running" })
|
|
236
319
|
}
|
|
237
320
|
|
|
238
|
-
|
|
321
|
+
// ── Blocking path (unchanged semantics): await the full pipeline ──
|
|
322
|
+
return await runChildPipeline(child, input, childOpts, childRunOpts, {
|
|
323
|
+
parent, role, args,
|
|
324
|
+
askContinue: askSubagentContinue,
|
|
325
|
+
})
|
|
239
326
|
},
|
|
240
327
|
}
|
|
241
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Shared post-spawn pipeline (blocking AND async — AGENT-LOOP.md §15 D-A1: the
|
|
331
|
+
* async branch reuses the exact same spawn-child pipeline, "全不变"):
|
|
332
|
+
* turn-cap continue loop → declined partial-work return → MIN_REPORT_CHARS
|
|
333
|
+
* expansion → eng-coder mutation merge → designId suffix. Returns the report.
|
|
334
|
+
* onDeclined lives here (identical for both paths) — only askContinue differs:
|
|
335
|
+
* blocking asks the user via the permission panel, async auto-declines.
|
|
336
|
+
*/
|
|
337
|
+
async function runChildPipeline(child, input, childOpts, childRunOpts, { parent, role, args, askContinue }) {
|
|
338
|
+
const declined = { partial: null }
|
|
339
|
+
let report = await runWithContinue(
|
|
340
|
+
(child, input, cbs, opts) => runAgent(child, input, cbs, opts), // opts = childRunOpts + resume (managed by the pipeline)
|
|
341
|
+
child, input, childOpts, childRunOpts,
|
|
342
|
+
{
|
|
343
|
+
askContinue,
|
|
344
|
+
onDeclined: (e, output) => {
|
|
345
|
+
if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
|
|
346
|
+
// Early return semantics (unchanged from the inline loop): the declined
|
|
347
|
+
// partial-work message is returned WITHOUT the MIN_REPORT_CHARS expansion —
|
|
348
|
+
// re-prompting a capped child for a longer report is wrong.
|
|
349
|
+
// Review #2 fix: use the pipeline-captured output (the `report` variable is
|
|
350
|
+
// still "" at this point — runWithContinue hasn't returned yet).
|
|
351
|
+
declined.partial = `Subagent (${role}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output || ""}`
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
)
|
|
355
|
+
if (declined.partial !== null) {
|
|
356
|
+
// declined eng-coder delivery still carries its designId — the fix round
|
|
357
|
+
// re-spawns with the same slot (2026-09-01).
|
|
358
|
+
if (role === "eng-coder") declined.partial += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
|
|
359
|
+
return declined.partial
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Report too short = incomplete handoff: send back for expansion once (inspired by kimi-code's summaryPolicy: min 200 chars, retry 1 time).
|
|
363
|
+
// The child agent's history is still intact; the continuation instruction is appended as new input so it can see its own earlier work.
|
|
364
|
+
if (report.length < MIN_REPORT_CHARS) {
|
|
365
|
+
report = await runAgent(child, REPORT_CONTINUATION, childOpts, childRunOpts)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Engineering mode mechanical code gate: delegated file changes must not
|
|
369
|
+
// bypass the parent's advisor/verify guards. Merge the child's mutations
|
|
370
|
+
// into the parent so "advisor mandatory at both gates" is enforced, not just
|
|
371
|
+
// promised in the engineering prompt.
|
|
372
|
+
// CRITICAL: Only merge if child actually mutated files (defense-in-depth against
|
|
373
|
+
// runAgent throwing before any writes occurred).
|
|
374
|
+
// Review #8 clarification: eng-coder ONLY is intentional — the mechanical
|
|
375
|
+
// two-gate merge exists for engineering mode; plain `coder` children carry
|
|
376
|
+
// their own verify/advisor self-review discipline (per tool description), and
|
|
377
|
+
// normal mode has no parent advisor/verify gate to feed.
|
|
378
|
+
if (role === "eng-coder" && child._mutatedThisRun) {
|
|
379
|
+
mergeChildMutations(parent, child)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// designId rides the delivery report (2026-09-01): the divergence-audit fix round
|
|
383
|
+
// re-spawns with the SAME designId+token — the parent copies it from here, and the
|
|
384
|
+
// prompt tells the model exactly where the matching token came from.
|
|
385
|
+
if (role === "eng-coder") {
|
|
386
|
+
report += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse this designId with the same designToken (from the approved advisor type='design' review) when re-spawning this eng-coder for an audit fix round.`
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return report
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Slot-queue refill (AGENT-LOOP.md §15 D-A1/D-A6): start queue heads while a
|
|
393
|
+
* running slot is free — called from every settle (completion frees a slot) and
|
|
394
|
+
* from the turn-end collection's refill loop. Serial by construction: one slot
|
|
395
|
+
* frees per settle, one head starts per call. */
|
|
396
|
+
export function maybeRefillAsync(parent) {
|
|
397
|
+
const queue = parent._asyncQueue ?? []
|
|
398
|
+
while (queue.length > 0) {
|
|
399
|
+
const running = [...(parent._asyncSubagents?.values() ?? [])].filter((e) => e.status === "running").length
|
|
400
|
+
if (running >= ASYNC_SUBAGENT_LIMIT) return
|
|
401
|
+
queue.shift().start()
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
242
405
|
/**
|
|
243
406
|
* Child agent run options — the parent's abort signal MUST propagate to the
|
|
244
407
|
* child: without it, Ctrl+C aborts the parent's controller but the child keeps
|
package/src/agent.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { cleanupConsultSessions } from "./agent-tools/consult.mjs"
|
|
|
18
18
|
import {
|
|
19
19
|
escapeXml, repairHistory, listWorkDir, ensureAutoReminder,
|
|
20
20
|
readonlyToolNames, collectGitContext, loadProjectInstructions,
|
|
21
|
-
ContinueError,
|
|
21
|
+
ContinueError, offloadToolResult,
|
|
22
22
|
DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
|
|
23
23
|
MIN_REPORT_CHARS, REPORT_CONTINUATION,
|
|
24
24
|
} from "./agent/helpers.mjs"
|
|
@@ -140,6 +140,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
140
140
|
agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
|
|
141
141
|
agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
|
|
142
142
|
agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
|
|
143
|
+
agent._asyncCheckLastN = 0 // subagent_check read counter is per-run (§15 D-A2): a fresh user turn restarts from 1
|
|
143
144
|
}
|
|
144
145
|
// eng-coder authorization is set by subagent.mjs AFTER token validation but BEFORE runAgent —
|
|
145
146
|
// only reset for the top-level agent (depth 0); child runs must keep their granted authorization
|
|
@@ -164,6 +165,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
164
165
|
tools: toolSchemas,
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
let thrownError = null
|
|
167
169
|
try {
|
|
168
170
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
169
171
|
// Update turn counter for status bar display
|
|
@@ -184,16 +186,25 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
184
186
|
agent._compressFailures = 0
|
|
185
187
|
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
186
188
|
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
187
|
-
|
|
189
|
+
// Completion info (CONTEXT-COMPACTION §7 D-C2): { mode: "summary", tokensFreed, elapsedMs }
|
|
190
|
+
// from compressIfNeeded, or { mode: "fallback", tailMessages } from compressFallback below —
|
|
191
|
+
// the TUI panel renders the matching completion state. Existing callers that ignore the
|
|
192
|
+
// argument keep the exact previous onCompress semantics.
|
|
193
|
+
callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
188
194
|
ensureAutoReminder(agent)
|
|
189
195
|
}
|
|
190
196
|
} catch (compressError) {
|
|
191
197
|
// AbortError must not be swallowed: user cancellation must propagate
|
|
192
198
|
if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
|
|
193
199
|
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
200
|
+
// Q3 visibility (CONTEXT-COMPACTION §7 D-C1): a failed compression is no longer silent —
|
|
201
|
+
// the frontend updates the compression panel with the error text (and logs to stderr).
|
|
202
|
+
// Failure STRATEGY is unchanged: COMPRESS_FAILURE_LIMIT consecutive failures still degrade
|
|
203
|
+
// to compressFallback — this only adds observability.
|
|
204
|
+
callbacks?.onCompressFail?.(compressError)
|
|
194
205
|
if (agent._compressFailures >= COMPRESS_FAILURE_LIMIT) {
|
|
195
206
|
agent._compressFailures = 0
|
|
196
|
-
if (compressFallback(agent)) callbacks.onCompress?.()
|
|
207
|
+
if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
197
208
|
}
|
|
198
209
|
}
|
|
199
210
|
}
|
|
@@ -420,9 +431,63 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
420
431
|
}
|
|
421
432
|
|
|
422
433
|
throw new ContinueError(maxTurns)
|
|
434
|
+
} catch (e) {
|
|
435
|
+
thrownError = e
|
|
436
|
+
throw e
|
|
423
437
|
} finally {
|
|
424
438
|
// Turn-end cleanup: abort any leftover consultation children (consult_start spawns
|
|
425
439
|
// fire-and-forget runners; a completed turn must not let them keep burning tokens).
|
|
426
440
|
cleanupConsultSessions(agent)
|
|
441
|
+
// Async subagent turn-end collection (AGENT-LOOP.md §15 D-A3). Lifecycle:
|
|
442
|
+
// - Ctrl+C / Ctrl+I (signal aborted): children were aborted with the parent
|
|
443
|
+
// signal — clear WITHOUT injecting stale errors (user explicitly stopped).
|
|
444
|
+
// - ContinueError (turn cap): no wait, no injection — children keep running
|
|
445
|
+
// and the RESUME run's turn-end collection takes over.
|
|
446
|
+
// - anything else: refill loop → wait for all → inject reports → clear.
|
|
447
|
+
if (signal?.aborted) {
|
|
448
|
+
agent._asyncSubagents?.clear()
|
|
449
|
+
agent._asyncQueue = []
|
|
450
|
+
agent._asyncCheckLastN = 0
|
|
451
|
+
} else if (thrownError instanceof ContinueError) {
|
|
452
|
+
// keep _asyncSubagents + the check counter — the resumed run continues them
|
|
453
|
+
} else {
|
|
454
|
+
await collectAsyncSubagents(agent)
|
|
455
|
+
agent._asyncCheckLastN = 0
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Turn-end async subagent collection (AGENT-LOOP.md §15 D-A3):
|
|
462
|
+
* 1. refill loop — start queued heads while slots free (each settle already
|
|
463
|
+
* refills via its finally; this drains the tail), keeping the cap ≤4 serial.
|
|
464
|
+
* 2. wait for every entry to settle (queued entries start through the refill
|
|
465
|
+
* chain — the drain loop converges when nothing is running and the queue is empty).
|
|
466
|
+
* 3. inject one user-role reminder per entry: the report/error text XML-escaped
|
|
467
|
+
* (child reports may carry content from files/webpages — reminder discipline),
|
|
468
|
+
* >64K offloaded to disk with a preview + path.
|
|
469
|
+
* 4. clear the map. (The ⟦ev⟧done freeze signal is NOT emitted here — D-A3
|
|
470
|
+
* 2026-09-02: each entry's settle callback emits it at completion time so
|
|
471
|
+
* blocks freeze at their completion position in the stream.)
|
|
472
|
+
*/
|
|
473
|
+
async function collectAsyncSubagents(agent) {
|
|
474
|
+
const map = agent._asyncSubagents
|
|
475
|
+
if (!map || map.size === 0) return
|
|
476
|
+
const { maybeRefillAsync } = await import("./agent-tools/subagent.mjs")
|
|
477
|
+
for (;;) {
|
|
478
|
+
maybeRefillAsync(agent)
|
|
479
|
+
const running = [...map.values()].filter((e) => e.status === "running")
|
|
480
|
+
if (running.length === 0) break
|
|
481
|
+
await Promise.allSettled(running.map((e) => e.promise))
|
|
482
|
+
}
|
|
483
|
+
for (const e of [...map.values()]) {
|
|
484
|
+
const body = e.error ?? e.report ?? "(no report)"
|
|
485
|
+
const preview = await offloadToolResult(String(body), `async-subagent-${e.id}`)
|
|
486
|
+
pushReal(agent, {
|
|
487
|
+
role: "user",
|
|
488
|
+
content: `[System reminder: async subagent #${e.id} (${e.role}) finished]\n${escapeXml(preview)}`,
|
|
489
|
+
})
|
|
427
490
|
}
|
|
491
|
+
map.clear()
|
|
492
|
+
agent._asyncQueue = []
|
|
428
493
|
}
|
package/src/cli/make-agent.mjs
CHANGED
|
@@ -107,6 +107,31 @@ export async function assembleAgent() {
|
|
|
107
107
|
agent.activeProvider = config.activeProvider
|
|
108
108
|
agent.activeModel = config.activeModel ?? null
|
|
109
109
|
agent._mcpWarnings = mcpWarnings
|
|
110
|
+
// SESSION.md §8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
|
|
111
|
+
// 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
|
|
112
|
+
validateProvider(agent)
|
|
113
|
+
return agent
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* SESSION.md §8 D-S1 — provider 有效性校验(assembleAgent 后唯一校验点,TUI/chat 两路径同源)。
|
|
118
|
+
* 判据(评审 #1/#2):仅 model/baseURL 缺失判 invalid——**不得用 MODEL_SPECS 成员资格判无效**
|
|
119
|
+
* (未知模型 = 受支持场景:自定义端点模型不在 spec 表是常态,误判会让自定义模型用户每次恢复都弹重选)。
|
|
120
|
+
* apiKey 缺失不判(既有 wizard /model 流程处理)。幂等:有效时清标记,无效时置标记 + 原因。
|
|
121
|
+
* 不抛错、不退出。返回 agent 便于链式调用。
|
|
122
|
+
*/
|
|
123
|
+
export function validateProvider(agent) {
|
|
124
|
+
const ok = Boolean(agent.provider?.name && agent.provider.model && agent.provider.baseURL)
|
|
125
|
+
if (ok) {
|
|
126
|
+
delete agent._providerInvalid
|
|
127
|
+
delete agent._providerInvalidReason
|
|
128
|
+
} else {
|
|
129
|
+
agent._providerInvalid = true
|
|
130
|
+
agent._providerInvalidReason = !agent.provider?.name
|
|
131
|
+
? "provider 不存在"
|
|
132
|
+
: !agent.provider.model ? "model 缺失"
|
|
133
|
+
: "缺少 baseURL"
|
|
134
|
+
}
|
|
110
135
|
return agent
|
|
111
136
|
}
|
|
112
137
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { loadConfig } from "../config.mjs"
|
|
3
|
+
import { teamConfig } from "./make-agent.mjs"
|
|
4
|
+
import { put, search, list, deleteByUid } from "../memory/core.mjs"
|
|
2
5
|
|
|
3
|
-
/** thincoder memory <list|search|put|remove> subcommands
|
|
4
|
-
|
|
6
|
+
/** thincoder memory <list|search|put|remove> subcommands.
|
|
7
|
+
* opts.dirs: { project, team } layer directories for project/team file deletion (tests inject their own);
|
|
8
|
+
* falls back to the same config-derived dirs the agent uses. */
|
|
9
|
+
export async function memoryCommand(memory, args, opts = {}) {
|
|
5
10
|
const [sub, ...rest] = args
|
|
6
11
|
|
|
7
12
|
const flags = {}
|
|
@@ -37,12 +42,18 @@ export async function memoryCommand(memory, args) {
|
|
|
37
42
|
break
|
|
38
43
|
}
|
|
39
44
|
case "remove": {
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
42
|
-
console.error("Usage: thincoder memory remove <
|
|
45
|
+
const uid = positional[0]
|
|
46
|
+
if (!uid) {
|
|
47
|
+
console.error("Usage: thincoder memory remove <uid> (uid: personal:<n> | project:<origin>:<path> | team:<origin>:<path>; bare <n> = personal)")
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const entry = await deleteByUid(memory, uid, { dirs: opts.dirs ?? cliDirs() })
|
|
52
|
+
console.log(`Removed ${entry.id}: ${entry.title}`)
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(e.message)
|
|
43
55
|
return 1
|
|
44
56
|
}
|
|
45
|
-
console.log((await remove(memory, id)) ? `Removed #${id}` : `No entry #${id}`)
|
|
46
57
|
break
|
|
47
58
|
}
|
|
48
59
|
default:
|
|
@@ -51,6 +62,16 @@ export async function memoryCommand(memory, args) {
|
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/** Layer directories for project/team file deletion — derived from the same config the agent uses. */
|
|
66
|
+
function cliDirs() {
|
|
67
|
+
const config = loadConfig()
|
|
68
|
+
const dirs = { project: null, team: null }
|
|
69
|
+
if (config.memory?.projectDir) dirs.project = join(process.cwd(), config.memory.projectDir)
|
|
70
|
+
const team = teamConfig(config)
|
|
71
|
+
if (team) dirs.team = team.dir
|
|
72
|
+
return dirs
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
function printEntries(entries) {
|
|
55
76
|
if (entries.length === 0) {
|
|
56
77
|
console.log("(no entries)")
|