thincoder 0.12.29 → 0.12.31
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/README.md +1 -0
- package/package.json +1 -1
- package/src/agent/setup.mjs +28 -6
- package/src/agent-tools/consult.mjs +288 -0
- package/src/agent-tools/escalate.mjs +152 -0
- package/src/agent.mjs +13 -4
- package/src/config.mjs +14 -0
- package/src/prompts/advisor-round1.md +15 -4
- package/src/prompts/advisor-round2.md +4 -1
- package/src/prompts/advisor-round3.md +4 -1
- package/src/prompts/consult-base.md +23 -0
- package/src/prompts/discipline.md +6 -1
- package/src/prompts/main.md +19 -1
- package/src/tui/cmd-config.mjs +88 -5
- package/src/tui/slash-commands.mjs +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ Design philosophy (the entire meaning of the name): if the Node standard library
|
|
|
21
21
|
- **Two-phase tool scheduling**: permission prompts serialized, read-only tools parallelized, side-effect tools serialized
|
|
22
22
|
- **Session persistence** ⭐0.5.0: unlimited archive slots, `/session` to switch anytime, tool results visible after restore. Process-level isolation — multiple instances in the same directory each get their own session slot
|
|
23
23
|
- **Concurrent subagents**: three roles — `explore`/`plan`/`coder` — dispatched in parallel, streaming output visible, reports land in the conversation; per-subagent model override (`subagent` tool `model` arg or `agent.subagentModel` config — e.g. discuss with `glm-5.2`, let `deepseek-v4-flash` implement)
|
|
24
|
+
- **Multi-model consultation + 飞刀 (escalate)** ⭐0.12.30: `consult_start`/`consult_check`/`consult_stop` run several configured models in parallel as independent read-only consultants (each with its own TUI activity card, `main_history` access to the failure trail); `escalate` flies in a stronger model for a single expert implementation run with full write access. Candidate pool = `agent.consultModels` ([{ provider, model, effort? }], up to 5); budgets via `agent.consultTurns` / `agent.consultTimeoutMs`
|
|
24
25
|
- **Plan Mode**: read-only exploration + design, implement after user approval
|
|
25
26
|
- **AUTO mode**: `/auto` full authorization, no confirmations on long tasks
|
|
26
27
|
- **Task tracking**: `task` tool breaks down multi-step work, status bar ✓n/m live progress, auto-filters completed items
|
package/package.json
CHANGED
package/src/agent/setup.mjs
CHANGED
|
@@ -168,6 +168,17 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
168
168
|
// task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
|
|
169
169
|
// eng-coder subagents get advisor for mandatory design review before coding
|
|
170
170
|
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool } = await import("../agent-tools.mjs")
|
|
171
|
+
const { consultStartTool, consultCheckTool, consultStopTool } = await import("../agent-tools/consult.mjs")
|
|
172
|
+
const { escalateTool } = await import("../agent-tools/escalate.mjs")
|
|
173
|
+
const { CONSULT_BASE } = await import("../agent.mjs")
|
|
174
|
+
// withPool: decorate consult_start/escalate descriptions with the CURRENT candidate pool
|
|
175
|
+
// so the model knows which models it can pick (CLI parity with the plugin).
|
|
176
|
+
const withPool = (tool) => {
|
|
177
|
+
const models = agent.config?.agent?.consultModels ?? []
|
|
178
|
+
const list = models.map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ")
|
|
179
|
+
if (!list) return tool
|
|
180
|
+
return { ...tool, description: tool.description + `\nCurrently configured consultants (this tool's pool): ${list}` }
|
|
181
|
+
}
|
|
171
182
|
// Role enum is mutually exclusive: normal mode has "coder", engineering mode has "eng-coder"
|
|
172
183
|
const subagentRoles = (depth === 0 && agent.config?.agent?.engineering)
|
|
173
184
|
? {
|
|
@@ -192,13 +203,19 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
192
203
|
},
|
|
193
204
|
} : subagentTool
|
|
194
205
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
206
|
+
// Consult/escalate tools registered only when configured — an unconfigured pool would
|
|
207
|
+
// otherwise make the model call them and eat an error turn (plugin parity).
|
|
208
|
+
const consultTools = (agent.config?.agent?.consultModels ?? []).length
|
|
209
|
+
? [withPool(consultStartTool), consultCheckTool, consultStopTool, withPool(escalateTool)]
|
|
210
|
+
: []
|
|
211
|
+
const depthOnly = depth === 0 ? [filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, advisorTool, ...consultTools]
|
|
212
|
+
// Write-permission coder sub-agents (subagent role="coder" + escalate): the
|
|
213
|
+
// system prompt names verify (system.md) and advisor (discipline.md) — without them an
|
|
214
|
+
// escalate hit "unknown tool" and fell back to bash node --check / npm test to
|
|
215
|
+
// self-verify (2026-08-16 deepseek escalate diagnosis; plugin parity).
|
|
200
216
|
: agent._role === "eng-coder" ? [advisorTool, verifyTool]
|
|
201
217
|
: agent._role === "coder" ? [verifyTool, advisorTool]
|
|
218
|
+
: agent._role === "consult" ? [recentChangesTool]
|
|
202
219
|
: []
|
|
203
220
|
const tools = [...agent.tools, taskTool, planTool, timerTool, ...depthOnly]
|
|
204
221
|
const toolSchemas = tools.map(toOpenAISchema)
|
|
@@ -208,7 +225,12 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
208
225
|
// system prompt
|
|
209
226
|
const needsDiscipline = depth === 0 || agent._role === "coder" || agent._role === "eng-coder"
|
|
210
227
|
let base
|
|
211
|
-
if (
|
|
228
|
+
if (agent._role === "consult") {
|
|
229
|
+
// consult children: a lean, purpose-built base prompt (consult-base.md) — NOT the full
|
|
230
|
+
// main-agent system.md (whose coding-agent persona, checklist/task/verify workflows and
|
|
231
|
+
// tool references conflict with a read-only diagnosis and cost tokens every turn).
|
|
232
|
+
base = CONSULT_BASE
|
|
233
|
+
} else if ((depth === 0 || agent._role === "eng-coder") && agent.config?.agent?.engineering) {
|
|
212
234
|
// Engineering mode: strict methodology, NO standard discipline injection.
|
|
213
235
|
// Falling back to standard discipline on METHODOLOGY.md absence would leak
|
|
214
236
|
// advisor enforcement into engineering mode — the two prompt sets stay separate.
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consult.mjs — multi-model consultation ("会诊", docs/design/CONSULTATION.md). CLI port.
|
|
3
|
+
*
|
|
4
|
+
* Three tools: consult_start (non-blocking spawn) / consult_check (read the next
|
|
5
|
+
* reply as it arrives) / consult_stop (abort the rest). The mechanism does ZERO
|
|
6
|
+
* judging — the main agent reads replies and verifies with its own tools.
|
|
7
|
+
*
|
|
8
|
+
* CLI adaptation (vs the VS Code plugin): the child runner is CLI's runAgent
|
|
9
|
+
* (runAgent(child, input, callbacks, opts) — an agent object, not provider+cwd);
|
|
10
|
+
* children are built with createAgent({ role: "consult", readonly tools,
|
|
11
|
+
* CONSULT_BASE overlay }); activity streams to the parent TUI via the relay
|
|
12
|
+
* prefix `consult#<id>/` (same channel subagent uses), not onSubagent/onToolPanel.
|
|
13
|
+
*/
|
|
14
|
+
import { createAgent, runAgent, readonlyToolNames } from "../agent.mjs"
|
|
15
|
+
import { resolveChildProvider } from "./subagent.mjs"
|
|
16
|
+
|
|
17
|
+
function consultLabel(m) {
|
|
18
|
+
return `${m.provider}:${m.model}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read-only tool injected into consultation children (via createAgent's tools).
|
|
22
|
+
* Lets the consultant pull the main agent's conversation history on demand —
|
|
23
|
+
* the failure trail is first-class evidence, not a retelling. */
|
|
24
|
+
export function makeMainHistoryTool(parentAgent) {
|
|
25
|
+
return {
|
|
26
|
+
name: "main_history",
|
|
27
|
+
readonly: true,
|
|
28
|
+
description:
|
|
29
|
+
"Read the main agent's conversation history — what has been tried, the exact errors, recent context. " +
|
|
30
|
+
"Use it to ground your analysis in the actual failure trail instead of guessing.\n" +
|
|
31
|
+
"Parameters:\n" +
|
|
32
|
+
"- limit: Number of recent messages to return (default 20, max 100)",
|
|
33
|
+
parameters: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: { limit: { type: "number", description: "Recent messages (default 20, max 100)" } },
|
|
36
|
+
},
|
|
37
|
+
async execute({ limit }) {
|
|
38
|
+
const n = Math.min(Math.max(limit ?? 20, 1), 100)
|
|
39
|
+
const h = parentAgent?.history ?? []
|
|
40
|
+
const slice = h.slice(-n)
|
|
41
|
+
if (slice.length === 0) return "(empty history)"
|
|
42
|
+
const render = (m) => {
|
|
43
|
+
let content
|
|
44
|
+
if (typeof m.content === "string") content = m.content
|
|
45
|
+
else if (Array.isArray(m.content)) {
|
|
46
|
+
content = m.content.map((part) => {
|
|
47
|
+
if (part?.type === "image_url" || part?.type === "image") return "[image omitted]"
|
|
48
|
+
if (part?.type === "text") return part.text ?? ""
|
|
49
|
+
return JSON.stringify(part)
|
|
50
|
+
}).join("\n")
|
|
51
|
+
} else content = m.content == null ? "" : JSON.stringify(m.content)
|
|
52
|
+
const calls = Array.isArray(m.tool_calls)
|
|
53
|
+
? m.tool_calls.map((c) => `[tool: ${c.function?.name ?? c.name}(${String(c.function?.arguments ?? c.args ?? "").slice(0, 200)})]`).join("\n")
|
|
54
|
+
: ""
|
|
55
|
+
return `--- [${m.role}] ---\n${content}${calls ? "\n" + calls : ""}`
|
|
56
|
+
}
|
|
57
|
+
const BUDGET = 60_000
|
|
58
|
+
let out = ""
|
|
59
|
+
for (let i = slice.length - 1; i >= 0; i--) {
|
|
60
|
+
const line = render(slice[i])
|
|
61
|
+
if (out.length + line.length > BUDGET) { out = `(earlier messages trimmed — budget ${BUDGET} chars)\n\n` + out; break }
|
|
62
|
+
out = out ? line + "\n\n" + out : line
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Wake every parked consult_check waiter. */
|
|
70
|
+
function wakeWaiters(session) {
|
|
71
|
+
const w = session.waiters.splice(0)
|
|
72
|
+
for (const resolve of w) { try { resolve(false) } catch { /* noop */ } }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function settleChild(session, id, label, ok, payload) {
|
|
76
|
+
if (ok) {
|
|
77
|
+
session.received++
|
|
78
|
+
session.replies.push({ model: label, reply: payload })
|
|
79
|
+
} else if (session.stopped) {
|
|
80
|
+
session.terminated = (session.terminated ?? 0) + 1
|
|
81
|
+
} else {
|
|
82
|
+
session.failed++
|
|
83
|
+
session.replies.push({ model: label, reply: `(consultation failed: ${payload})`, failed: true })
|
|
84
|
+
}
|
|
85
|
+
session.pending--
|
|
86
|
+
wakeWaiters(session)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
90
|
+
const agent = ctx.agent
|
|
91
|
+
const timeoutMs = agent?.config?.agent?.consultTimeoutMs ?? 600_000
|
|
92
|
+
let timedOut = false
|
|
93
|
+
const watchdog = setTimeout(() => {
|
|
94
|
+
timedOut = true
|
|
95
|
+
try { ctrl.abort() } catch { /* already settled */ }
|
|
96
|
+
}, timeoutMs)
|
|
97
|
+
const label = consultLabel(m)
|
|
98
|
+
try {
|
|
99
|
+
// Provider resolution: consultModels entries are { provider, model, effort? } — resolve
|
|
100
|
+
// via the subagent's provider resolver ("provider:model" handles cross-provider picks).
|
|
101
|
+
const provider = resolveChildProvider(agent, `${m.provider}:${m.model}`)
|
|
102
|
+
if (!provider?.apiKey?.trim() && !process.env.THINCODER_API_KEY) {
|
|
103
|
+
// resolveChildProvider may still lack a key; fail loudly like the plugin precheck
|
|
104
|
+
}
|
|
105
|
+
if (m.effort) provider.reasoningEffort = m.effort
|
|
106
|
+
|
|
107
|
+
// Read-only consultant: filter the parent tool set down to readonly tools + main_history.
|
|
108
|
+
const allowed = readonlyToolNames(agent.tools ?? [])
|
|
109
|
+
const tools = [
|
|
110
|
+
...(agent.tools ?? []).filter((t) => allowed.has(t.name)),
|
|
111
|
+
makeMainHistoryTool(agent),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const child = createAgent({
|
|
115
|
+
provider,
|
|
116
|
+
tools,
|
|
117
|
+
config: agent.config,
|
|
118
|
+
cwd: agent.cwd,
|
|
119
|
+
memory: agent.memory,
|
|
120
|
+
// No overlay: setup.mjs already selects CONSULT_BASE as the base prompt for
|
|
121
|
+
// role "consult" (overlay + base would concatenate it twice).
|
|
122
|
+
role: "consult",
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Activity relay: `consult#<subId>/` prefix → the parent TUI's subTasks panel
|
|
126
|
+
// (same channel subagent uses — parallel consultants stay independent).
|
|
127
|
+
agent._subAgentCounter = (agent._subAgentCounter ?? 0) + 1
|
|
128
|
+
const subId = agent._subAgentCounter
|
|
129
|
+
const relayPrefix = `consult#${subId}/`
|
|
130
|
+
const childCallbacks = {
|
|
131
|
+
onToken: ctx.callbacks?.onToken ? (t) => ctx.callbacks.onToken(`${relayPrefix}${t}`) : null,
|
|
132
|
+
onReasoning: ctx.callbacks?.onReasoning ? (r) => ctx.callbacks.onReasoning(`${relayPrefix}${r}`) : null,
|
|
133
|
+
onToolCall: ctx.callbacks?.onToolCall ? (name, args) => ctx.callbacks.onToolCall(`${relayPrefix}${name}`, args) : null,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const runner = ctx.runAgent ?? runAgent
|
|
137
|
+
const result = await runner(child, "# Problem\n" + problem, childCallbacks, {
|
|
138
|
+
depth: 1,
|
|
139
|
+
maxTurns: agent?.config?.agent?.consultTurns ?? 40,
|
|
140
|
+
signal: ctrl.signal,
|
|
141
|
+
})
|
|
142
|
+
settleChild(session, id, label, true, String(result ?? ""))
|
|
143
|
+
} catch (e) {
|
|
144
|
+
const note = timedOut ? `consultation timed out after ${Math.round(timeoutMs / 60000)}min (agent.consultTimeoutMs)` : e?.message ?? String(e)
|
|
145
|
+
settleChild(session, id, label, false, note)
|
|
146
|
+
} finally {
|
|
147
|
+
clearTimeout(watchdog)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Turn-end cleanup (called from runAgent's finally): abort every leftover
|
|
152
|
+
* consultation controller, wake parked waiters, clear the session map. */
|
|
153
|
+
export function cleanupConsultSessions(agent) {
|
|
154
|
+
for (const s of agent._consultSessions?.values() ?? []) {
|
|
155
|
+
s.stopped = true
|
|
156
|
+
for (const c of s.controllers ?? []) { try { c.abort() } catch { /* already settled */ } }
|
|
157
|
+
for (const w of s.waiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
158
|
+
}
|
|
159
|
+
agent._consultSessions?.clear()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const consultStartTool = {
|
|
163
|
+
name: "consult_start",
|
|
164
|
+
readonly: false,
|
|
165
|
+
sideEffectExempt: true,
|
|
166
|
+
description:
|
|
167
|
+
"Start a parallel multi-model consultation (会诊) for a hard problem you are stuck on (repeated failures, no headway). " +
|
|
168
|
+
"Several configured models (agent.consultModels) analyze the same problem INDEPENDENTLY and in parallel. " +
|
|
169
|
+
"Non-blocking: returns immediately with a consult id. Then call consult_check(id) to read each reply as it " +
|
|
170
|
+
"arrives, judge/verify it yourself with your own tools, and call consult_stop(id) once a reply is good enough.\n" +
|
|
171
|
+
"Parameters:\n" +
|
|
172
|
+
"- problem (required): a brief — the symptom, what you already tried (failure trail), and entry-point files. " +
|
|
173
|
+
"Do NOT paste raw error logs; consultants pull the main session history themselves via their main_history tool.",
|
|
174
|
+
parameters: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: { problem: { type: "string", description: "Problem brief (symptom + failure trail + entry files)" } },
|
|
177
|
+
required: ["problem"],
|
|
178
|
+
},
|
|
179
|
+
async execute({ problem }, ctx) {
|
|
180
|
+
if (typeof problem !== "string" || !problem.trim()) return "Error: problem is required and must be a non-empty string"
|
|
181
|
+
const agent = ctx.agent
|
|
182
|
+
if (!agent) return "Error: consult requires an agent context"
|
|
183
|
+
const models = agent.config?.agent?.consultModels ?? []
|
|
184
|
+
if (!Array.isArray(models) || models.length === 0)
|
|
185
|
+
return "Consultation is not configured — add agent.consultModels ([{ provider, model }], up to 5) to ~/.thincoder/config.json"
|
|
186
|
+
if (models.length > 5) return `Error: consultModels supports at most 5 models (got ${models.length})`
|
|
187
|
+
|
|
188
|
+
agent._consultSessions ??= new Map()
|
|
189
|
+
const id = String((agent._consultIdCounter = (agent._consultIdCounter ?? 0) + 1))
|
|
190
|
+
const session = {
|
|
191
|
+
id, controllers: [], replies: [], pending: 0, waiters: [],
|
|
192
|
+
failed: 0, terminated: 0, stopped: false, received: 0, total: models.length,
|
|
193
|
+
models: models.map(consultLabel),
|
|
194
|
+
}
|
|
195
|
+
agent._consultSessions.set(id, session)
|
|
196
|
+
|
|
197
|
+
for (const m of models) {
|
|
198
|
+
session.pending++
|
|
199
|
+
const ctrl = new AbortController()
|
|
200
|
+
session.controllers.push(ctrl)
|
|
201
|
+
if (ctx.signal) {
|
|
202
|
+
if (ctx.signal.aborted) ctrl.abort()
|
|
203
|
+
else ctx.signal.addEventListener("abort", () => ctrl.abort(), { once: true })
|
|
204
|
+
}
|
|
205
|
+
// Fire and forget — each child settles itself into the session queue.
|
|
206
|
+
runConsultChild(ctx, session, id, m, problem, ctrl)
|
|
207
|
+
}
|
|
208
|
+
return JSON.stringify({ id, models: session.models })
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const consultCheckTool = {
|
|
213
|
+
name: "consult_check",
|
|
214
|
+
readonly: true,
|
|
215
|
+
description:
|
|
216
|
+
"Read the NEXT consultation reply (whichever model answered first). Blocks until a reply arrives or all models " +
|
|
217
|
+
"have settled. The reply is raw and unjudged — verify/adopt it with your own tools. When done is true, no more " +
|
|
218
|
+
"replies are coming.\n" +
|
|
219
|
+
"Call it ALONE in a turn — do NOT batch it with calls that depend on its reply (readonly tools run in parallel).\n" +
|
|
220
|
+
"Parameters:\n" +
|
|
221
|
+
"- id (required): the consult id from consult_start",
|
|
222
|
+
parameters: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: { id: { type: "string", description: "Consult id" } },
|
|
225
|
+
required: ["id"],
|
|
226
|
+
},
|
|
227
|
+
async execute({ id }, ctx) {
|
|
228
|
+
const s = ctx.agent?._consultSessions?.get(String(id))
|
|
229
|
+
if (!s) return JSON.stringify({ error: "unknown consult id" })
|
|
230
|
+
const abortAll = () => { for (const c of s.controllers) { try { c.abort() } catch { /* noop */ } } }
|
|
231
|
+
if (ctx.signal?.aborted) abortAll()
|
|
232
|
+
|
|
233
|
+
for (;;) {
|
|
234
|
+
if (s.replies.length > 0) {
|
|
235
|
+
const r = s.replies.shift()
|
|
236
|
+
return JSON.stringify({
|
|
237
|
+
reply: r.reply, model: r.model, failedReply: r.failed === true,
|
|
238
|
+
received: s.received,
|
|
239
|
+
failed: s.failed,
|
|
240
|
+
terminated: s.terminated ?? 0, total: s.total,
|
|
241
|
+
done: s.replies.length === 0 && s.pending === 0,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
if (s.pending === 0) {
|
|
245
|
+
return JSON.stringify({ done: true, received: s.received, failed: s.failed, total: s.total })
|
|
246
|
+
}
|
|
247
|
+
const stopped = await new Promise((resolve) => {
|
|
248
|
+
function cleanup() {
|
|
249
|
+
const i = s.waiters.indexOf(w)
|
|
250
|
+
if (i >= 0) s.waiters.splice(i, 1)
|
|
251
|
+
ctx.signal?.removeEventListener("abort", onAbort)
|
|
252
|
+
}
|
|
253
|
+
function w() { cleanup(); resolve(false) }
|
|
254
|
+
function onAbort() { cleanup(); abortAll(); resolve(true) }
|
|
255
|
+
s.waiters.push(w)
|
|
256
|
+
if (ctx.signal) {
|
|
257
|
+
if (ctx.signal.aborted) { onAbort(); return }
|
|
258
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true })
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
if (stopped) return JSON.stringify({ done: true, stopped: true, received: s.received, failed: s.failed, total: s.total })
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export const consultStopTool = {
|
|
267
|
+
name: "consult_stop",
|
|
268
|
+
readonly: false,
|
|
269
|
+
sideEffectExempt: true,
|
|
270
|
+
description:
|
|
271
|
+
"Terminate the still-running consultations of a session once a reply is good enough — saves tokens and time. " +
|
|
272
|
+
"Already-answered replies stay available for consult_check.\n" +
|
|
273
|
+
"Parameters:\n" +
|
|
274
|
+
"- id (required): the consult id from consult_start",
|
|
275
|
+
parameters: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: { id: { type: "string", description: "Consult id" } },
|
|
278
|
+
required: ["id"],
|
|
279
|
+
},
|
|
280
|
+
async execute({ id }, ctx) {
|
|
281
|
+
const s = ctx.agent?._consultSessions?.get(String(id))
|
|
282
|
+
if (!s) return JSON.stringify({ error: "unknown consult id" })
|
|
283
|
+
const n = s.pending
|
|
284
|
+
s.stopped = true
|
|
285
|
+
for (const c of s.controllers) { try { c.abort() } catch { /* already settled */ } }
|
|
286
|
+
return JSON.stringify({ stopped: n })
|
|
287
|
+
},
|
|
288
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* escalate.mjs — 飞刀 (the "flying knife", docs/design/ESCALATE.md). CLI port.
|
|
3
|
+
*
|
|
4
|
+
* Hand an implementation task to a STRONGER model — like a hospital flying in an
|
|
5
|
+
* outside expert (飞刀): the expert arrives, operates personally (WRITE access),
|
|
6
|
+
* hands back the post-op report, leaves. Complementary to consult (parallel
|
|
7
|
+
* READ-ONLY opinions for judgment calls).
|
|
8
|
+
*
|
|
9
|
+
* Candidate pool = all consultModels rows. The tool is only registered when the
|
|
10
|
+
* pool is non-empty (setup.mjs).
|
|
11
|
+
*
|
|
12
|
+
* CLI adaptation (vs the VS Code plugin): the child runner is CLI's runAgent
|
|
13
|
+
* (runAgent(child, input, callbacks, opts) — an agent object, not provider+cwd);
|
|
14
|
+
* the child is createAgent({ role: "coder", CODER_OVERLAY }); activity streams to
|
|
15
|
+
* the parent TUI via the relay prefix `escalate#<id>/`; mutations merge via the
|
|
16
|
+
* CLI mergeChildMutations(parent, child) (agent object, not a state sink).
|
|
17
|
+
*/
|
|
18
|
+
import { isAbsolute, relative } from "node:path"
|
|
19
|
+
import { createAgent, runAgent, ContinueError, CODER_OVERLAY } from "../agent.mjs"
|
|
20
|
+
import { resolveChildProvider, mergeChildMutations } from "./subagent.mjs"
|
|
21
|
+
|
|
22
|
+
const label = (m) => `${m.provider}:${m.model}`
|
|
23
|
+
|
|
24
|
+
export const escalateTool = {
|
|
25
|
+
name: "escalate",
|
|
26
|
+
sideEffectExempt: true, // the child's mutations are tracked and reviewed, like subagent
|
|
27
|
+
description:
|
|
28
|
+
"TERMINOLOGY (one word for one thing): 'escalate' is the ONLY name — the tool, and the " +
|
|
29
|
+
"role of the expert sub-agent it spawns, are both called 'escalate'; 飞刀 is the Chinese " +
|
|
30
|
+
"alias. When the user says 飞刀 / escalate / 'fly in <model>', call THIS tool directly — " +
|
|
31
|
+
"never via a script importing this module. " +
|
|
32
|
+
"Hand an implementation task to a stronger model (飞刀 — a flown-in expert). " +
|
|
33
|
+
"It gets WRITE access and does the work itself — reads, edits, runs tests — then returns " +
|
|
34
|
+
"a post-op report (what changed, why, verification). You review the report and report to " +
|
|
35
|
+
"the user. Use it when YOU judge the task calls for stronger hands (complex multi-file " +
|
|
36
|
+
"refactoring, an intractable bug, intricate algorithm work — or work beyond your " +
|
|
37
|
+
"comfortable ability). Early or late, your judgment; the cost is one expert run, " +
|
|
38
|
+
"comparable to doing it yourself. For parallel READ-ONLY opinions use consult_start instead. " +
|
|
39
|
+
"Not available in engineering mode (implementation goes through eng-coder subagents there).\n" +
|
|
40
|
+
"Parameters:\n" +
|
|
41
|
+
"- task (required): the task description — goal, constraints, entry files, acceptance criteria\n" +
|
|
42
|
+
"- model (optional): pick a specific consultant as 'provider:model'; default = the first consult model",
|
|
43
|
+
parameters: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
task: { type: "string", description: "Task description with acceptance criteria" },
|
|
47
|
+
model: { type: "string", description: "Candidate 'provider:model' from the consult models (optional)" },
|
|
48
|
+
},
|
|
49
|
+
required: ["task"],
|
|
50
|
+
},
|
|
51
|
+
async execute({ task, model }, ctx) {
|
|
52
|
+
const parent = ctx.agent
|
|
53
|
+
if ((ctx.depth ?? 0) > 0) return "Error: escalate is only available at depth 0 (an escalate's work cannot be delegated again)"
|
|
54
|
+
if (parent?.config?.agent?.engineering) {
|
|
55
|
+
return "Error: engineering mode is ON — escalate is unavailable (it spawns a coder sub-agent, which engineering mode forbids). Use subagent with role='eng-coder' and a designToken from advisor(type='design') instead."
|
|
56
|
+
}
|
|
57
|
+
const pool = parent?.config?.agent?.consultModels ?? []
|
|
58
|
+
if (pool.length === 0) return "Error: no escalate candidates — configure at least one consult model (agent.consultModels)"
|
|
59
|
+
|
|
60
|
+
const wanted = typeof model === "string" ? model.replace(/\s+\([^)]*\)\s*$/, "").trim() : model
|
|
61
|
+
const pick = wanted ? pool.find((m) => label(m) === wanted) : pool[0]
|
|
62
|
+
if (!pick) {
|
|
63
|
+
return `Error: "${model}" is not a consult candidate. Available: ${pool.map(label).join(", ")}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let provider
|
|
67
|
+
try {
|
|
68
|
+
provider = resolveChildProvider(parent, `${pick.provider}:${pick.model}`)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return `Error: ${e.message}`
|
|
71
|
+
}
|
|
72
|
+
if (!provider?.apiKey?.trim() && !process.env.THINCODER_API_KEY) {
|
|
73
|
+
return `Error: provider "${pick.provider}" has no API key — set it in config.json (or THINCODER_API_KEY) before flying it in`
|
|
74
|
+
}
|
|
75
|
+
if (pick.effort) provider.reasoningEffort = pick.effort
|
|
76
|
+
|
|
77
|
+
parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
|
|
78
|
+
const subId = parent._subAgentCounter
|
|
79
|
+
const tag = label(pick)
|
|
80
|
+
const relayPrefix = `escalate#${subId}/`
|
|
81
|
+
|
|
82
|
+
const timeoutMs = parent?.config?.agent?.consultTimeoutMs ?? 600_000
|
|
83
|
+
let timedOut = false
|
|
84
|
+
const ctrl = new AbortController()
|
|
85
|
+
const watchdog = setTimeout(() => {
|
|
86
|
+
timedOut = true
|
|
87
|
+
try { ctrl.abort() } catch { /* already settled */ }
|
|
88
|
+
}, timeoutMs)
|
|
89
|
+
if (ctx.signal) {
|
|
90
|
+
if (ctx.signal.aborted) ctrl.abort()
|
|
91
|
+
else ctx.signal.addEventListener("abort", () => ctrl.abort(), { once: true })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let output = ""
|
|
95
|
+
const childCallbacks = {
|
|
96
|
+
onToken: ctx.callbacks?.onToken ? (t) => { output += t; ctx.callbacks.onToken(`${relayPrefix}${t}`) } : (t) => { output += t },
|
|
97
|
+
onReasoning: ctx.callbacks?.onReasoning ? (r) => ctx.callbacks.onReasoning(`${relayPrefix}${r}`) : null,
|
|
98
|
+
onToolCall: ctx.callbacks?.onToolCall ? (name, args) => ctx.callbacks.onToolCall(`${relayPrefix}${name}`, args) : null,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Declared outside try so the catch can merge mutations even on a partial failure.
|
|
102
|
+
let child = null
|
|
103
|
+
try {
|
|
104
|
+
// Full write path (role "coder"): permission gate via the parent's onPermissionRequest,
|
|
105
|
+
// recent-changes tracking, mutations merge into the parent below.
|
|
106
|
+
child = createAgent({
|
|
107
|
+
provider,
|
|
108
|
+
tools: parent.tools,
|
|
109
|
+
config: parent.config,
|
|
110
|
+
cwd: parent.cwd,
|
|
111
|
+
memory: parent.memory,
|
|
112
|
+
overlay: CODER_OVERLAY,
|
|
113
|
+
role: "coder",
|
|
114
|
+
})
|
|
115
|
+
const runner = ctx.runAgent ?? runAgent
|
|
116
|
+
const report = await runner(child, task, {
|
|
117
|
+
...childCallbacks,
|
|
118
|
+
onPermissionRequest: ctx.onPermissionRequest ?? null,
|
|
119
|
+
}, {
|
|
120
|
+
depth: 1,
|
|
121
|
+
maxTurns: parent.config?.agent?.subagentTurns ?? 100,
|
|
122
|
+
signal: ctrl.signal,
|
|
123
|
+
})
|
|
124
|
+
// Escalate mutations are the parent's mutations: verify/advisor guards must see them
|
|
125
|
+
mergeChildMutations(parent, child)
|
|
126
|
+
return `escalate (${tag}) post-op report:\n${report || output.slice(0, 4000)}${touchedFilesNote(child, parent.cwd)}`
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// Even a failed surgery may have written files — merge whatever the child touched.
|
|
129
|
+
if (child) mergeChildMutations(parent, child)
|
|
130
|
+
const msg = e?.message ?? String(e)
|
|
131
|
+
if (ctx.signal?.aborted || (!timedOut && e?.name === "AbortError")) throw e
|
|
132
|
+
if (e instanceof ContinueError) {
|
|
133
|
+
return `escalate (${tag}) stopped: turn cap reached (${e.turns} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output.slice(0, 2000)}`
|
|
134
|
+
}
|
|
135
|
+
const note = timedOut ? `timed out after ${Math.round(timeoutMs / 60000)}min (agent.consultTimeoutMs)` : msg
|
|
136
|
+
return `escalate (${tag}) error: ${note}\nPartial output: ${output.slice(0, 2000)}`
|
|
137
|
+
} finally {
|
|
138
|
+
clearTimeout(watchdog)
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Relative touched-file list appended to every return (child paths are absolute). */
|
|
144
|
+
function touchedFilesNote(child, cwd) {
|
|
145
|
+
const touched = child?._touchedFiles ?? []
|
|
146
|
+
if (touched.length === 0) return ""
|
|
147
|
+
const shown = touched.map((f) => {
|
|
148
|
+
const r = relative(cwd ?? process.cwd(), f)
|
|
149
|
+
return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
|
|
150
|
+
})
|
|
151
|
+
return `\nTouched files: ${shown.join(", ")}`
|
|
152
|
+
}
|
package/src/agent.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { executeToolCalls } from "./agent/dispatch.mjs"
|
|
|
13
13
|
import { prepareRun } from "./agent/setup.mjs"
|
|
14
14
|
import { injectPostTurn, STALL_WINDOW_SIZE, STALL_THRESHOLD, GOAL_BUDGET_WARN_RATIO } from "./agent/post-turn.mjs"
|
|
15
15
|
import { handleCompletion } from "./agent/completion.mjs"
|
|
16
|
+
import { cleanupConsultSessions } from "./agent-tools/consult.mjs"
|
|
16
17
|
import {
|
|
17
18
|
escapeXml, tryCanonicalize, repairHistory, listWorkDir,
|
|
18
19
|
readonlyToolNames, collectGitContext, loadProjectInstructions,
|
|
@@ -26,15 +27,17 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
26
27
|
const SYSTEM_PROMPT = readFileSync(join(__dirname, "prompts", "system.md"), "utf8")
|
|
27
28
|
const DISCIPLINE_RULES = readFileSync(join(__dirname, "prompts", "discipline.md"), "utf8")
|
|
28
29
|
const MAIN_OVERLAY = readFileSync(join(__dirname, "prompts", "main.md"), "utf8")
|
|
29
|
-
let _EXPLORE, _CODER, _PLAN, _ENG_CODER
|
|
30
|
+
let _EXPLORE, _CODER, _PLAN, _ENG_CODER, _CONSULT_BASE
|
|
30
31
|
try { _EXPLORE = readFileSync(join(__dirname, "prompts", "explore.md"), "utf8") } catch { _EXPLORE = "" }
|
|
31
32
|
try { _CODER = readFileSync(join(__dirname, "prompts", "coder.md"), "utf8") } catch { _CODER = "" }
|
|
32
33
|
try { _PLAN = readFileSync(join(__dirname, "prompts", "plan.md"), "utf8") } catch { _PLAN = "" }
|
|
33
34
|
try { _ENG_CODER = readFileSync(join(__dirname, "prompts", "eng-coder.md"), "utf8") } catch { _ENG_CODER = "" }
|
|
35
|
+
try { _CONSULT_BASE = readFileSync(join(__dirname, "prompts", "consult-base.md"), "utf8") } catch { _CONSULT_BASE = "" }
|
|
34
36
|
export const EXPLORE_OVERLAY = _EXPLORE
|
|
35
37
|
export const CODER_OVERLAY = _CODER
|
|
36
38
|
export const PLAN_OVERLAY = _PLAN
|
|
37
39
|
export const ENG_CODER_OVERLAY = _ENG_CODER
|
|
40
|
+
export const CONSULT_BASE = _CONSULT_BASE
|
|
38
41
|
|
|
39
42
|
// exported for consumption by agent-tools.mjs
|
|
40
43
|
export {
|
|
@@ -142,7 +145,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
142
145
|
tools: toolSchemas,
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
|
|
148
|
+
try {
|
|
149
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
146
150
|
// Update turn counter for status bar display
|
|
147
151
|
agent._currentTurn = turn + 1
|
|
148
152
|
agent._maxTurns = maxTurns
|
|
@@ -439,7 +443,12 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
439
443
|
}
|
|
440
444
|
|
|
441
445
|
injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
|
|
442
|
-
|
|
446
|
+
}
|
|
443
447
|
|
|
444
|
-
|
|
448
|
+
throw new ContinueError(maxTurns)
|
|
449
|
+
} finally {
|
|
450
|
+
// Turn-end cleanup: abort any leftover consultation children (consult_start spawns
|
|
451
|
+
// fire-and-forget runners; a completed turn must not let them keep burning tokens).
|
|
452
|
+
cleanupConsultSessions(agent)
|
|
453
|
+
}
|
|
445
454
|
}
|
package/src/config.mjs
CHANGED
|
@@ -49,6 +49,11 @@ const DEFAULTS = {
|
|
|
49
49
|
goalTurns: 200,
|
|
50
50
|
compactThreshold: 100000,
|
|
51
51
|
verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
|
|
52
|
+
// Multi-model consultation ("会诊") + escalate ("飞刀") — CLI parity with the VS Code plugin.
|
|
53
|
+
// consultModels: candidate pool for BOTH consult and escalate ({ provider, model, effort? }, up to 5).
|
|
54
|
+
consultModels: [],
|
|
55
|
+
consultTurns: 40, // per-consultant tool-turn budget (diagnosis tasks)
|
|
56
|
+
consultTimeoutMs: 600000, // wall-clock ceiling per consultant (10min)
|
|
52
57
|
streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
|
|
53
58
|
advisor: { enabled: false }, // code review; { enabled: true, provider: "deepseek", model: "deepseek-chat", thinking: { type: "enabled" }, reasoningEffort: "max", guard: true }
|
|
54
59
|
autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
|
|
@@ -234,6 +239,15 @@ export function loadConfig() {
|
|
|
234
239
|
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
235
240
|
}
|
|
236
241
|
|
|
242
|
+
// Consult/escalate pool validation (CLI parity with the plugin): up to 5 candidates.
|
|
243
|
+
const cm = merged.agent.consultModels
|
|
244
|
+
if (cm !== undefined && !Array.isArray(cm)) {
|
|
245
|
+
throw new Error(`agent.consultModels must be an array of { provider, model } entries (got ${typeof cm})`)
|
|
246
|
+
}
|
|
247
|
+
if (Array.isArray(cm) && cm.length > 5) {
|
|
248
|
+
throw new Error(`agent.consultModels supports at most 5 models (got ${cm.length})`)
|
|
249
|
+
}
|
|
250
|
+
|
|
237
251
|
// Backward compatibility: promote root-level config fields to agent sub-object
|
|
238
252
|
if (config.verifyGuard !== undefined) {
|
|
239
253
|
merged.agent.verifyGuard = config.verifyGuard
|
|
@@ -5,7 +5,11 @@ You have a budget of 30 tool rounds (chat turns) — plan your exploration accor
|
|
|
5
5
|
|
|
6
6
|
Review workflow:
|
|
7
7
|
1. The files to review are listed in the review scope. Read them in full. The review scope defines exactly which files to inspect.
|
|
8
|
-
2. **READ THE PROJECT GUIDE FIRST** — the `## Project Guide (AGENTS.md)` section in the review context maps the project's structure
|
|
8
|
+
2. **READ THE PROJECT GUIDE FIRST** — the `## Project Guide (AGENTS.md)` section in the review context maps the project's structure.
|
|
9
|
+
- It tells you where the requirements/design documents live.
|
|
10
|
+
- Read whatever documents the guide names — no fixed file names are assumed.
|
|
11
|
+
- **The user's requirements live in those documents; the conversation background is only a supplement.**
|
|
12
|
+
- If the guide names none, judge from the conversation background and say so explicitly if requirements are unclear.
|
|
9
13
|
3. Read the specified files for full context. **Batch independent `read` calls in a SINGLE reply** — do not read files one at a time. Each round-trip counts against your limit.
|
|
10
14
|
4. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
11
15
|
5. Produce your review table.
|
|
@@ -17,11 +21,18 @@ Budget rules:
|
|
|
17
21
|
- **Batch everything**: multiple `read` calls in one reply, multiple `grep` calls in one reply. Serializing tool calls wastes your round budget.
|
|
18
22
|
|
|
19
23
|
Rules:
|
|
20
|
-
- First judge the task from the conversation background
|
|
24
|
+
- First judge the task from the conversation background.
|
|
25
|
+
- If the changes are clearly non-code (static docs, README, CHANGELOG), reply immediately with the all-clear phrase — `"All clear — no code changes to review."` — and do NOT spend tool calls exploring.
|
|
26
|
+
- The host recognizes it via the "all clear" / "no 🔴" / "review passed" / "no issues found" markers, matched case-insensitively.
|
|
27
|
+
- Prompts and configs that shape behaviour are NOT exempt — review them normally.
|
|
21
28
|
- **Requirement fit**: check the implementation against what the user actually asked for — a review is not only about "is the code correct" but also "is this what the user wanted". Two comparisons:
|
|
22
29
|
- (a) **Claim vs implementation**: the implementer's stated intent (conversation background / response table / commit message) vs what the implementation actually does — claiming X but delivering Y is a gap.
|
|
23
|
-
- (b) **Expectation vs shape**: the requirements documents named by the Project Guide (AGENTS.md) and explicit user expectations vs the delivered shape
|
|
24
|
-
|
|
30
|
+
- (b) **Expectation vs shape**: the requirements documents named by the Project Guide (AGENTS.md) and explicit user expectations vs the delivered shape.
|
|
31
|
+
- "asked for A, got B" is a gap (e.g. "the record must keep the real order" vs a summary appended at the end).
|
|
32
|
+
- **The requirements documents are the primary reference — read them (workflow step 2) before judging fit. Do not judge against expectations you cannot see.**
|
|
33
|
+
- **Known limit**: the conversation background only includes the last 3 user–assistant exchanges — older user expectations may not be visible, which is why the requirements documents are the primary reference.
|
|
34
|
+
- (a) is the primary check (needs only recent context).
|
|
35
|
+
- (b) is best-effort — check what the docs/background show, do NOT treat an invisible expectation as a gap.
|
|
25
36
|
- **Severity**: 🔴 = the user's explicit request was not fulfilled; 🟡 = fulfilled but in a suboptimal or misleading way. Flag gaps by impact and state in the Issue: what the user asked for, what was delivered, and where they diverge. Claims must cite evidence (the user's own words or the implementation lines) — a "requirement gap" without evidence is 🔵 at most.
|
|
26
37
|
- Reply in the same language as the conversation background.
|
|
27
38
|
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
@@ -8,7 +8,10 @@ Review workflow:
|
|
|
8
8
|
1. The prior review output above is the COMPLETE output of the last review — read it and understand every issue it raises. The affected files are named in it — read them in full. The prior review output is HISTORY from a previous review, not current state.
|
|
9
9
|
2. STALE-CONTEXT WARNING: any content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.
|
|
10
10
|
3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-review item names them or a fix appears to contradict the task itself.
|
|
11
|
-
4. **ALWAYS
|
|
11
|
+
4. **ALWAYS `read` the current file before judging an item fixed or unfixed.**
|
|
12
|
+
- Never decide from the prior review output alone — fixes may already be committed.
|
|
13
|
+
- (You have NO git tool this round; any git output in earlier messages is historical and untrustworthy.)
|
|
14
|
+
- Batch independent tool calls in one reply.
|
|
12
15
|
5. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
13
16
|
6. Produce your review table.
|
|
14
17
|
|
|
@@ -7,7 +7,10 @@ Review workflow:
|
|
|
7
7
|
1. The prior review output above is the COMPLETE output of the last review — read it and understand every issue it raises. The affected files are named in it — read them in full. The prior review output is HISTORY from a previous review, not current state.
|
|
8
8
|
2. STALE-CONTEXT WARNING: any content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.
|
|
9
9
|
3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-review item names them.
|
|
10
|
-
4. **ALWAYS
|
|
10
|
+
4. **ALWAYS `read` the current file before judging an item fixed or unfixed.**
|
|
11
|
+
- Never decide from the prior review output alone — fixes may already be committed.
|
|
12
|
+
- (You have NO git tool this round; any git output in earlier messages is historical and untrustworthy.)
|
|
13
|
+
- Batch independent tool calls in one reply.
|
|
11
14
|
5. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
12
15
|
6. Produce your review table.
|
|
13
16
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
You are one of several independent expert consultants analyzing the same problem in parallel — each on a different model. Your value is a perspective the main agent may be missing.
|
|
2
|
+
|
|
3
|
+
**Language:** reply in the user's language; keep code, commands, identifiers, file paths, and technical terms in their original form.
|
|
4
|
+
|
|
5
|
+
**Rules:**
|
|
6
|
+
- You are READ-ONLY: analyze and recommend, never modify files. The main agent implements.
|
|
7
|
+
- You have a `main_history` tool — pull the main agent's conversation history (what was tried, exact errors) BEFORE theorizing. Ground your analysis in the actual failure trail.
|
|
8
|
+
- Do not wait for or coordinate with the other consultants; they cannot see you.
|
|
9
|
+
- Work within your budget (~40 tool turns, up to ~10 minutes wall-clock): pull main_history first, read the 2–5 entry-point files it points at, and STOP. Reading targeted files is the expected behavior; full-repo scans are over budget — but do NOT skip reading entirely and theorize from the brief alone.
|
|
10
|
+
- Brief paths can be wrong (missing a directory prefix, renamed files) — verify with glob/ls before concluding a file "does not exist".
|
|
11
|
+
- Prefer local files first; use web search only when the question needs external facts (an API's current behavior, an upstream doc) — never to rediscover what is in the repo.
|
|
12
|
+
- Be concrete: root cause first, then a specific, actionable fix. If verification is possible, state exactly how the main agent can verify your recommendation (commands, files to check, expected outcome).
|
|
13
|
+
- Be honest: do not fabricate file contents or line numbers you did not actually read.
|
|
14
|
+
|
|
15
|
+
Structure your final answer as:
|
|
16
|
+
## Diagnosis
|
|
17
|
+
(root cause analysis)
|
|
18
|
+
## Recommendation
|
|
19
|
+
(the concrete fix)
|
|
20
|
+
## Verification
|
|
21
|
+
(how to prove it — commands / files / expected outcome; omit only if the question is purely conceptual)
|
|
22
|
+
|
|
23
|
+
Keep the whole answer concise — it is pasted verbatim into the main agent's context, so ~500 words is ideal; no filler.
|
|
@@ -11,5 +11,10 @@ Debugging strategy:
|
|
|
11
11
|
- Fix one thing at a time. Don't change multiple things at once.
|
|
12
12
|
- Don't get stuck reading code — write tests, add logs. Trust the runtime over your theories.
|
|
13
13
|
|
|
14
|
+
UI & interface design:
|
|
15
|
+
- A value with a FIXED set of choices (enum, level, mode, flag) must be OPTIONS — picker / menu / choices / buttons. Never free-text input.
|
|
16
|
+
- Free-text for a discrete value forces the user to guess the exact spelling, needs manual validation, and fails silently on typos. This has happened repeatedly (e.g. reasoning-effort levels typed by hand).
|
|
17
|
+
- Free-text is correct ONLY when the input is genuinely open-ended (a name, a path, a message).
|
|
18
|
+
|
|
14
19
|
Review discipline (standard mode only — engineering mode has its own review timing rules):
|
|
15
|
-
- **Advisor:** call after changing code. Must provide scope: `paths` (files/dirs to review) or `documents` (context). Response table: `| # | Action | Detail |`. Round 2 verifies the prior issue table + flags obvious new issues; round 3+ strictly verifies only the prior issue table (no new-issue hunting). Max 5 rounds total.
|
|
20
|
+
- **Advisor:** call after changing code. Must provide scope: `paths` (files/dirs to review) or `documents` (context). Response table: `| # | Action | Detail |`. Round 2 verifies the prior issue table + flags obvious new issues; round 3+ strictly verifies only the prior issue table (no new-issue hunting). Max 5 rounds total.
|
package/src/prompts/main.md
CHANGED
|
@@ -13,7 +13,8 @@ Delegate well — spawn subagents for independent subtasks.
|
|
|
13
13
|
- Delegate breadth-first exploration; do precision edits yourself.
|
|
14
14
|
- Never give parallel subagents tasks that edit the same files — conflicts waste everyone's time.
|
|
15
15
|
- When a coder subagent finishes, verify its report: read the files it claims to have changed, run the tests — do not trust subagent reports blindly.
|
|
16
|
-
- If a subagent fails or returns ambiguous results, don't spin:
|
|
16
|
+
- If a subagent fails or returns ambiguous results, don't spin: narrow the task and retry, or handle it yourself.
|
|
17
|
+
- Escalate EARLY, on up-front ability judgment — if the task is beyond your comfortable ability, hand it to a stronger model (escalate) before burning attempts, not after.
|
|
17
18
|
- When multiple subagent reports conflict, read the relevant code yourself to arbitrate — never merge conflicting claims.
|
|
18
19
|
|
|
19
20
|
Set goals for autonomous work — long-running tasks need a verifiable completion criterion (a machine-checkable proof, not vague effort).
|
|
@@ -21,6 +22,23 @@ Completion claims are audited; declaring blocked requires 3 genuine attempts aga
|
|
|
21
22
|
|
|
22
23
|
Load skills when relevant — project skills (.thincoder/skills/) contain reusable workflows and reference material.
|
|
23
24
|
|
|
25
|
+
Consult for independent perspectives (会诊) — a second opinion when YOU judge it pays for itself:
|
|
26
|
+
- Fits a stubborn bug, a judgment call with real tradeoffs, or a design decision worth cross-checking.
|
|
27
|
+
- Requires agent.consultModels configured.
|
|
28
|
+
- Flow: consult_start with a brief → consult_check to read each reply as it arrives → judge/verify with your own tools → consult_stop the rest once one is good enough.
|
|
29
|
+
- The brief decides the quality: symptom + what you already tried + entry-point files, ~150 words max.
|
|
30
|
+
- Each consult runs N parallel sessions — weigh the cost yourself.
|
|
31
|
+
|
|
32
|
+
Escalate to a stronger model (飞刀) — hand implementation to a stronger model when YOU judge the task needs stronger hands:
|
|
33
|
+
- Fits a complex multi-file refactor, an intractable bug, intricate algorithm work — or work beyond your comfortable ability.
|
|
34
|
+
- Escalate EARLY, on up-front judgment — not after burning failed attempts.
|
|
35
|
+
- `escalate(task)` gets WRITE access and does the work itself; you review its report (read the changed files, run the tests).
|
|
36
|
+
- Terminology: `escalate` is the only technical name; 飞刀 is the Chinese alias.
|
|
37
|
+
- When the user says "飞刀" / "会诊" / "consult", call the `escalate` or `consult_start` tool directly — they are in YOUR tool table. Never write a script that imports the module.
|
|
38
|
+
- Contrast with consult_start: parallel READ-ONLY opinions for judgment calls, not write access.
|
|
39
|
+
|
|
40
|
+
Consultations are bound to the current turn: a user interrupt (or turn end) terminates them — after an interruption, start a fresh consultation instead of referencing the old consult id.
|
|
41
|
+
|
|
24
42
|
**How you finish:**
|
|
25
43
|
|
|
26
44
|
After a batch of edits, follow the self-review checklist from the Coding discipline.
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
3
3
|
|
|
4
4
|
/** /config command: view and set agent/embedding/proxy config. */
|
|
5
5
|
export async function handleConfigCommand(ctx, args = []) {
|
|
6
|
-
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey } = ctx
|
|
6
|
+
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey, pickModelForSlot } = ctx
|
|
7
7
|
const { configPath } = await import("../config.mjs")
|
|
8
8
|
const ac = agent.config?.agent ?? {}
|
|
9
9
|
const ec = agent.config?.embedding ?? {}
|
|
@@ -142,16 +142,87 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
142
142
|
}
|
|
143
143
|
if (sub) { pushLine("Usage: /config [embedkey]", C.error); return }
|
|
144
144
|
|
|
145
|
+
/** 会诊/飞刀候选池子菜单:列出 / 添加 / 编辑 effort / 删除 consultModels 条目。 */
|
|
146
|
+
async function pickEffort(current) {
|
|
147
|
+
const levels = ["none", "min", "low", "medium", "high", "max"]
|
|
148
|
+
const entries = levels.map((l) => ({ type: "item", text: l === current ? `${l} ← current` : l, action: l }))
|
|
149
|
+
const c = await showPicker("Reasoning effort", entries, { defaultIndex: Math.max(0, levels.indexOf(current ?? "none")) })
|
|
150
|
+
return c ? c.action : null // Esc → null (keep unchanged)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function consultMenu() {
|
|
154
|
+
let idx = 0
|
|
155
|
+
for (;;) {
|
|
156
|
+
const cm = agent.config?.agent?.consultModels ?? []
|
|
157
|
+
const entries = [
|
|
158
|
+
{ type: "header", text: `Consult/escalate pool: ${cm.length} model(s) (max 5)` },
|
|
159
|
+
...cm.map((m, i) => ({ type: "item", text: `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`, action: "edit", index: i })),
|
|
160
|
+
{ type: "item", text: cm.length ? "+ Add model" : "+ Add model (none yet)", action: "add" },
|
|
161
|
+
]
|
|
162
|
+
const c = await showPicker("Consult models", entries, { defaultIndex: idx })
|
|
163
|
+
if (!c) return // Esc → 返回主菜单
|
|
164
|
+
if (c.action === "add") {
|
|
165
|
+
if (cm.length >= 5) { pushLine("At most 5 consult models", C.error); continue }
|
|
166
|
+
// BOTH provider and model are pickers (discipline: options, never free-text) —
|
|
167
|
+
// pickModelForSlot reuses /model's provider list + async-fetched model list.
|
|
168
|
+
const picked = await pickModelForSlot()
|
|
169
|
+
if (!picked) continue
|
|
170
|
+
const effort = await pickEffort(null)
|
|
171
|
+
const entry = { provider: picked.provider, model: picked.model }
|
|
172
|
+
if (effort && effort !== "none") entry.effort = effort
|
|
173
|
+
const next = [...cm, entry]
|
|
174
|
+
await saveProxy((raw) => { raw.agent ??= {}; raw.agent.consultModels = next })
|
|
175
|
+
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
176
|
+
pushLine(`Added ${entry.provider}:${entry.model}${entry.effort ? ` (${entry.effort})` : ""}`, C.tool)
|
|
177
|
+
idx = 0
|
|
178
|
+
} else if (c.action === "edit") {
|
|
179
|
+
// Per-model sub-menu: change effort or remove.
|
|
180
|
+
const m = cm[c.index]
|
|
181
|
+
const tag = `${m.provider}:${m.model}`
|
|
182
|
+
const subEntries = [
|
|
183
|
+
{ type: "header", text: `${tag} — effort: ${m.effort ?? "(none)"}` },
|
|
184
|
+
{ type: "item", text: `Change effort (current: ${m.effort ?? "none"})`, action: "effort" },
|
|
185
|
+
{ type: "item", text: "Remove", action: "remove" },
|
|
186
|
+
]
|
|
187
|
+
const s = await showPicker(tag, subEntries, {})
|
|
188
|
+
if (!s) continue
|
|
189
|
+
if (s.action === "remove") {
|
|
190
|
+
const next = cm.filter((_, i) => i !== c.index)
|
|
191
|
+
await saveProxy((raw) => { raw.agent ??= {}; raw.agent.consultModels = next })
|
|
192
|
+
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
193
|
+
pushLine(`Removed ${tag}`, C.tool)
|
|
194
|
+
} else if (s.action === "effort") {
|
|
195
|
+
const effort = await pickEffort(m.effort)
|
|
196
|
+
if (effort === null) { continue } // Esc 保持
|
|
197
|
+
const next = cm.map((x, i) => {
|
|
198
|
+
if (i !== c.index) return x
|
|
199
|
+
if (effort === "none") { const { effort, ...rest } = x; return rest }
|
|
200
|
+
return { ...x, effort }
|
|
201
|
+
})
|
|
202
|
+
await saveProxy((raw) => { raw.agent ??= {}; raw.agent.consultModels = next })
|
|
203
|
+
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
204
|
+
const after = next[c.index]
|
|
205
|
+
pushLine(`${tag} effort = ${after?.effort ?? "none"}`, C.tool)
|
|
206
|
+
}
|
|
207
|
+
idx = 0
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
145
212
|
// ── Main config loop ──
|
|
146
213
|
let running = true
|
|
147
214
|
let mainIdx = 0 // 记住上次选中位置,改完一项回主菜单时恢复
|
|
148
215
|
while (running) {
|
|
216
|
+
const consultCount = (ac.consultModels ?? []).length
|
|
149
217
|
const mainEntries = [
|
|
150
|
-
{ type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | embedding=${agent.memory?.embedder ? "on" : "off"}` },
|
|
218
|
+
{ type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | consult=${consultCount} model(s) | embedding=${agent.memory?.embedder ? "on" : "off"}` },
|
|
151
219
|
{ type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 100}`, action: "agent.maxTurns" },
|
|
152
220
|
{ type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
|
|
153
221
|
{ type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
|
|
154
222
|
{ type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
|
|
223
|
+
{ type: "item", text: `agent.consultModels = ${consultCount} model(s)${consultCount ? ` (${(ac.consultModels ?? []).map((m) => m.provider + ":" + m.model).join(", ")})` : ""}`, action: "consult" },
|
|
224
|
+
{ type: "item", text: `agent.consultTurns = ${ac.consultTurns ?? 40}`, action: "agent.consultTurns" },
|
|
225
|
+
{ type: "item", text: `agent.consultTimeoutMs = ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, action: "agent.consultTimeoutMs" },
|
|
155
226
|
{ type: "item", text: "Set embedding API key", action: "embedkey" },
|
|
156
227
|
{ type: "item", text: `proxy = ${proxySummary()}`, action: "proxy" },
|
|
157
228
|
{ type: "item", text: "View full config", action: "view" },
|
|
@@ -169,6 +240,9 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
169
240
|
pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
|
|
170
241
|
pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
|
|
171
242
|
pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
|
|
243
|
+
pushLine(`agent.consultModels: ${(ac.consultModels ?? []).map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ") || "(none)"}`, C.dim)
|
|
244
|
+
pushLine(`agent.consultTurns: ${ac.consultTurns ?? 40}`, C.dim)
|
|
245
|
+
pushLine(`agent.consultTimeoutMs: ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, C.dim)
|
|
172
246
|
pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
|
|
173
247
|
pushLine(`proxy: ${proxySummary()}`, C.dim)
|
|
174
248
|
pushLine(`Config file: ${configPath}`, C.dim)
|
|
@@ -181,6 +255,11 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
181
255
|
continue
|
|
182
256
|
}
|
|
183
257
|
|
|
258
|
+
if (choice.action === "consult") {
|
|
259
|
+
await consultMenu()
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
184
263
|
if (choice.action === "embedkey") {
|
|
185
264
|
if (await setEmbedKey()) running = false
|
|
186
265
|
continue
|
|
@@ -205,23 +284,27 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
205
284
|
|
|
206
285
|
// Numeric config items
|
|
207
286
|
const label = choice.action
|
|
287
|
+
const isTimeout = label === "agent.consultTimeoutMs"
|
|
208
288
|
const current = label === "agent.maxTurns" ? (ac.maxTurns ?? 100)
|
|
209
289
|
: label === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
|
|
210
290
|
: label === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
|
|
291
|
+
: label === "agent.consultTurns" ? (ac.consultTurns ?? 40)
|
|
292
|
+
: isTimeout ? Math.round((ac.consultTimeoutMs ?? 600000) / 60000)
|
|
211
293
|
: ""
|
|
212
|
-
const val = await askQuestion(`${label} (current: ${current}):`)
|
|
294
|
+
const val = await askQuestion(`${label} (current: ${current}${isTimeout ? " min" : ""}):`)
|
|
213
295
|
if (!val) continue
|
|
214
296
|
try {
|
|
215
297
|
const num = Number(val)
|
|
216
298
|
if (isNaN(num)) { pushLine("Value must be a number", C.error); continue }
|
|
299
|
+
const stored = isTimeout ? Math.round(num * 60000) : num
|
|
217
300
|
await saveProxy((raw) => {
|
|
218
301
|
const keys = label.split(".")
|
|
219
302
|
let obj = raw
|
|
220
303
|
for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
|
|
221
|
-
obj[keys[keys.length - 1]] =
|
|
304
|
+
obj[keys[keys.length - 1]] = stored
|
|
222
305
|
})
|
|
223
306
|
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
224
|
-
pushLine(`${label} = ${val}`, C.tool)
|
|
307
|
+
pushLine(`${label} = ${isTimeout ? `${val} min (${stored} ms)` : val}`, C.tool)
|
|
225
308
|
pushLine("(restart to apply)", C.dim)
|
|
226
309
|
running = false
|
|
227
310
|
} catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
|
|
@@ -46,7 +46,7 @@ export const SLASH_COMMANDS = [
|
|
|
46
46
|
{ name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
|
|
47
47
|
{ name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
|
|
48
48
|
{ name: "/upgrade", group: "System", desc: "check for updates & upgrade" },
|
|
49
|
-
{ name: "/config", group: "System", desc: "agent config (embedding, proxy, turns,
|
|
49
|
+
{ name: "/config", group: "System", desc: "agent config (embedding, proxy, turns, threshold, consult pool)" },
|
|
50
50
|
{ name: "/new", group: "Session", desc: "new session (old one archived to slot)" },
|
|
51
51
|
{ name: "/session", group: "Session", desc: "list/switch archived sessions" },
|
|
52
52
|
{ name: "/clear", group: "Session", desc: "clear screen" },
|