thincoder 0.12.54 → 0.12.59
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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- 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.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
package/src/advisor/run.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Message building lives in advisor.mjs.
|
|
4
4
|
*/
|
|
5
5
|
import { chat } from "../provider/core.mjs"
|
|
6
|
-
import { findProvider,
|
|
6
|
+
import { findProvider, providerSpec } from "../config.mjs"
|
|
7
7
|
import { toOpenAISchema } from "../tools/index.mjs"
|
|
8
8
|
import { prepareAdvisorMessages } from "../advisor.mjs"
|
|
9
9
|
import { appendCitationReport } from "./citations.mjs"
|
|
@@ -120,6 +120,9 @@ function renderTimeline(timeline, tail = "") {
|
|
|
120
120
|
}
|
|
121
121
|
// Test seam (mirrors _advisorToolsFor).
|
|
122
122
|
export { renderTimeline as _renderTimeline }
|
|
123
|
+
// Test seam (T-TS8/9): the tool loop itself — toolsOverride injects a mock tool
|
|
124
|
+
// set with controllable timing/errors (the real set comes from advisorToolsFor).
|
|
125
|
+
export { runAdvisorToolLoop as _runAdvisorToolLoop }
|
|
123
126
|
|
|
124
127
|
/**
|
|
125
128
|
* Run the advisor's tool loop: chat → execute tools → repeat.
|
|
@@ -129,7 +132,7 @@ export { renderTimeline as _renderTimeline }
|
|
|
129
132
|
* the panel keeps moving while the advisor explores — otherwise the panel sits
|
|
130
133
|
* frozen through every tool-call phase and the review appears to have stalled.
|
|
131
134
|
*/
|
|
132
|
-
async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd) {
|
|
135
|
+
async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd, toolsOverride = null) {
|
|
133
136
|
// Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
|
|
134
137
|
// Every chunk is ALSO recorded into an ordered timeline — the persisted record
|
|
135
138
|
// must show the review process (thinking ↔ tool progress ↔ final text) at its
|
|
@@ -145,7 +148,9 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
145
148
|
const onThink = emit("think")
|
|
146
149
|
const onText = emit("text")
|
|
147
150
|
const onTool = emit("tool")
|
|
148
|
-
|
|
151
|
+
// toolsOverride = test seam (T-TS8/9): the real advisor tool set, or a mock
|
|
152
|
+
// set with controllable timing/errors.
|
|
153
|
+
const { schemas: toolSchemas, byName: toolByName } = toolsOverride ?? advisorToolsFor(agent)
|
|
149
154
|
let turns = 0
|
|
150
155
|
const startTime = Date.now()
|
|
151
156
|
|
|
@@ -198,6 +203,18 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
198
203
|
signal: signal ?? null,
|
|
199
204
|
onToken: onText,
|
|
200
205
|
onReasoning: onThink,
|
|
206
|
+
// LOGGING(vscode advisor/run.mjs parity——按 stage 可 grep)
|
|
207
|
+
// §18.6 D-TR4:轨迹元数据增补——kind=advisor(评审独立于子代理——T-TR2);role
|
|
208
|
+
// 透出调用方角色(eng-coder 内嵌评审时为 "eng-coder");session/cwd 供轨迹对回;
|
|
209
|
+
// traces 开关沿 agent.config(D-TR6)。
|
|
210
|
+
logCtx: {
|
|
211
|
+
stage: "advisor",
|
|
212
|
+
role: agent?._role ?? null,
|
|
213
|
+
kind: "advisor",
|
|
214
|
+
session: agent?._sessionStart ?? null,
|
|
215
|
+
cwd,
|
|
216
|
+
traces: agent?.config?.traces?.enabled !== false,
|
|
217
|
+
},
|
|
201
218
|
})
|
|
202
219
|
|
|
203
220
|
// No tool calls — this is the final review text. The final answer was
|
|
@@ -221,13 +238,24 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
221
238
|
id: tc.id, type: "function",
|
|
222
239
|
function: { name: tc.name, arguments: tc.arguments },
|
|
223
240
|
})),
|
|
224
|
-
...(response.reasoning &&
|
|
241
|
+
...(response.reasoning && providerSpec(provider).reasoningEcho === "required"
|
|
225
242
|
? { reasoning_content: response.reasoning }
|
|
226
243
|
: {}),
|
|
227
244
|
})
|
|
228
245
|
|
|
229
|
-
//
|
|
230
|
-
|
|
246
|
+
// B1 (AGENT-LOOP.md §18.7 D-TS7): the SAME LLM reply's multiple read-only
|
|
247
|
+
// tool calls run in PARALLEL (Promise.all) — results are backfilled in
|
|
248
|
+
// toolCalls order (Promise.all preserves the input order → tool_call_id
|
|
249
|
+
// never mismatches); each tool's timeout/error is captured independently
|
|
250
|
+
// (the existing TOOL_TIMEOUT stays — one failing tool does not block the
|
|
251
|
+
// others); progress lines are emitted in toolCalls order. The read-only
|
|
252
|
+
// tool set has no side effects — no sequencing/serialization needed.
|
|
253
|
+
// Scope note (round1 review #10): B1 is ONLY in-loop tool parallelism — it
|
|
254
|
+
// does NOT solve the TODO "platform execution: advisor parallel calls are
|
|
255
|
+
// actually serial" mystery (docs/TODO.md — LOGGING evidence item), which
|
|
256
|
+
// concerns multiple advisor CALLS observed as serial, not one reply's
|
|
257
|
+
// tool calls.
|
|
258
|
+
const parsed = response.toolCalls.map((tc) => {
|
|
231
259
|
const tool = toolByName.get(tc.name)
|
|
232
260
|
let args = {}
|
|
233
261
|
let parseError = null
|
|
@@ -236,63 +264,69 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
236
264
|
} catch (e) {
|
|
237
265
|
parseError = `Error: invalid JSON in tool arguments: ${e.message}\nRaw arguments: ${(tc.arguments || "").slice(0, 200)}`
|
|
238
266
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
267
|
+
return { tc, tool, args, parseError }
|
|
268
|
+
})
|
|
269
|
+
// Progress lines first, in toolCalls order (emitted before the parallel
|
|
270
|
+
// run — display order is independent of completion order).
|
|
271
|
+
for (const p of parsed) {
|
|
272
|
+
if (p.parseError) continue // parse-error tools get no progress line (legacy behavior)
|
|
273
|
+
const argsLine = describeToolArgs(p.tc.name, p.args)
|
|
274
|
+
onTool(`\n→ ${p.tc.name}${argsLine ? " " + argsLine : ""}\n`)
|
|
275
|
+
}
|
|
276
|
+
// Every tool runs CONCURRENTLY; each result/error lands in its own slot —
|
|
277
|
+
// Promise.all preserves input order, so index i always matches parsed[i].
|
|
278
|
+
const executed = await Promise.all(parsed.map(async (p) => {
|
|
279
|
+
// Parse failure → error to model immediately (no execution)
|
|
280
|
+
if (p.parseError) return p.parseError
|
|
281
|
+
if (!p.tool) return `Error: unknown tool "${p.tc.name}". Available: ${[...toolByName.keys()].join(", ")}`
|
|
282
|
+
// Execute with timeout (clear the timer when the tool wins the race —
|
|
283
|
+
// otherwise up to MAX_ADVISOR_TURNS dangling timers accumulate)
|
|
284
|
+
try {
|
|
285
|
+
let timeoutId
|
|
286
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
287
|
+
timeoutId = setTimeout(() => reject(new Error(`tool timeout after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS)
|
|
288
|
+
})
|
|
289
|
+
let toolPromise
|
|
254
290
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
clearTimeout(timeoutId)
|
|
265
|
-
// Timeout won → toolPromise is still pending; a later rejection
|
|
266
|
-
// would surface as an unhandled rejection. The race already
|
|
267
|
-
// consumed the result/error in the normal path, so this no-op
|
|
268
|
-
// catch only fires for the abandoned-tool case.
|
|
269
|
-
toolPromise?.catch(() => {})
|
|
270
|
-
}
|
|
271
|
-
} catch (e) {
|
|
272
|
-
const errorType = e.message.includes("timeout") ? "timeout"
|
|
273
|
-
: e.message.includes("ENOENT") ? "file_not_found"
|
|
274
|
-
: e.message.includes("permission") ? "permission_denied"
|
|
275
|
-
: "execution_error"
|
|
276
|
-
result = `Error (${errorType}): ${e.message}`
|
|
291
|
+
toolPromise = p.tool.execute(p.args, { cwd, agent, onOutput, signal })
|
|
292
|
+
return await Promise.race([toolPromise, timeoutPromise])
|
|
293
|
+
} finally {
|
|
294
|
+
clearTimeout(timeoutId)
|
|
295
|
+
// Timeout won → toolPromise is still pending; a later rejection
|
|
296
|
+
// would surface as an unhandled rejection. The race already
|
|
297
|
+
// consumed the result/error in the normal path, so this no-op
|
|
298
|
+
// catch only fires for the abandoned-tool case.
|
|
299
|
+
toolPromise?.catch(() => {})
|
|
277
300
|
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
const errorType = e.message.includes("timeout") ? "timeout"
|
|
303
|
+
: e.message.includes("ENOENT") ? "file_not_found"
|
|
304
|
+
: e.message.includes("permission") ? "permission_denied"
|
|
305
|
+
: "execution_error"
|
|
306
|
+
return `Error (${errorType}): ${e.message}`
|
|
278
307
|
}
|
|
308
|
+
}))
|
|
309
|
+
|
|
310
|
+
// Backfill in toolCalls order (executed[i] ↔ parsed[i]); per-result
|
|
311
|
+
// non-string serialization + line-aware truncation stay per-tool.
|
|
312
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
313
|
+
let result = executed[i]
|
|
279
314
|
if (typeof result !== "string") result = JSON.stringify(result)
|
|
280
|
-
|
|
281
|
-
// Line-aware truncation: preserve line integrity
|
|
315
|
+
|
|
282
316
|
if (result.length > MAX_RESULT_CHARS) {
|
|
283
317
|
const lines = result.split("\n")
|
|
284
318
|
let truncated = ""
|
|
285
319
|
let charCount = 0
|
|
286
320
|
let keptLines = 0
|
|
287
|
-
|
|
288
|
-
for (let
|
|
289
|
-
const line = lines[
|
|
321
|
+
|
|
322
|
+
for (let j = 0; j < lines.length; j++) {
|
|
323
|
+
const line = lines[j]
|
|
290
324
|
if (charCount + line.length + 1 > MAX_RESULT_CHARS) break
|
|
291
325
|
truncated += line + "\n"
|
|
292
326
|
charCount += line.length + 1
|
|
293
327
|
keptLines++
|
|
294
328
|
}
|
|
295
|
-
|
|
329
|
+
|
|
296
330
|
const remainingLines = lines.length - keptLines
|
|
297
331
|
result = (
|
|
298
332
|
truncated +
|
|
@@ -300,8 +334,8 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
300
334
|
`To see more content, use: read(path, offset=${keptLines + 1}, limit=200)`
|
|
301
335
|
)
|
|
302
336
|
}
|
|
303
|
-
|
|
304
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content: result })
|
|
337
|
+
|
|
338
|
+
messages.push({ role: "tool", tool_call_id: parsed[i].tc.id, content: result })
|
|
305
339
|
}
|
|
306
340
|
}
|
|
307
341
|
}
|
|
@@ -359,8 +393,12 @@ function extractUnfixedIssues(priorText) {
|
|
|
359
393
|
* Run an advisor review. reviewType: "code" (default) or "design". Returns review text or null when skipped.
|
|
360
394
|
* @param {string|null} [designToken] — injected into the design-review prompt; the advisor echoes it only on approval.
|
|
361
395
|
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review; passed through to the message builder.
|
|
396
|
+
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review.
|
|
397
|
+
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3):
|
|
398
|
+
* { type, target, status, reason, exclude }; mechanically injected at the
|
|
399
|
+
* start of every review round's user message. Absent → legacy behavior (no injection).
|
|
362
400
|
*/
|
|
363
|
-
export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null) {
|
|
401
|
+
export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null, object = null) {
|
|
364
402
|
const onOutput = callbacks?.onOutput
|
|
365
403
|
const signal = callbacks?.signal
|
|
366
404
|
const startTime = Date.now()
|
|
@@ -395,7 +433,7 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
|
|
|
395
433
|
// Advisor always works in the agent's cwd — scope is defined by paths/documents.
|
|
396
434
|
const advisorCwd = agent.cwd
|
|
397
435
|
|
|
398
|
-
const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths)
|
|
436
|
+
const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths, null, object)
|
|
399
437
|
|
|
400
438
|
try {
|
|
401
439
|
const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
|
package/src/advisor.mjs
CHANGED
|
@@ -45,12 +45,13 @@ import { readFileSync } from "node:fs"
|
|
|
45
45
|
import { join, dirname } from "node:path"
|
|
46
46
|
import { fileURLToPath } from "node:url"
|
|
47
47
|
import { extractAgentResponseTable } from "./advisor/history.mjs"
|
|
48
|
-
import { buildAdvisorUserMessage, resolveScopeFiles } from "./advisor/messages.mjs"
|
|
48
|
+
import { buildAdvisorUserMessage, resolveScopeFiles, buildObjectDeclarationBlock } from "./advisor/messages.mjs"
|
|
49
49
|
import { buildConvergenceBody } from "./advisor/convergence.mjs"
|
|
50
50
|
import { escapeLiteralEscapes } from "./escape.mjs"
|
|
51
51
|
// Re-export for run.mjs and tests (keeps their imports from "../advisor.mjs" stable)
|
|
52
52
|
export { ADVISOR_MD_PATH, extractAgentResponseTable, extractConversationBackground } from "./advisor/history.mjs"
|
|
53
53
|
export { buildAdvisorUserMessage } from "./advisor/messages.mjs"
|
|
54
|
+
export { buildObjectDeclarationBlock } from "./advisor/messages.mjs"
|
|
54
55
|
|
|
55
56
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
56
57
|
|
|
@@ -135,13 +136,15 @@ export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
|
|
|
135
136
|
* @param {Object} agent — the parent agent (history used for the response table)
|
|
136
137
|
* @param {Object|null} prior — prior issue table (extracted from history when null)
|
|
137
138
|
* @param {string[]|null} [scopeFiles] — review surface for the no-response fallback (cwd-relative)
|
|
139
|
+
* @param {Object|null} [object] — review-object declaration (§18.8): mechanically
|
|
140
|
+
* prepended to the round-2+ follow-up so every round stays anchored (T-OA2).
|
|
138
141
|
* @returns {string} the follow-up user message — or a plain "System reminder: …"
|
|
139
142
|
* fresh-review fallback (NO brackets — some OpenAI-compatible servers parse
|
|
140
143
|
* '['-prefixed content as structured data / expand escapes) when no prior
|
|
141
144
|
* review exists at all (caller misuse; the response-table extraction would
|
|
142
145
|
* otherwise scan history from index 0 and could match an unrelated stale table)
|
|
143
146
|
*/
|
|
144
|
-
export function buildAdvisorFollowUp(agent, prior, scopeFiles = null) {
|
|
147
|
+
export function buildAdvisorFollowUp(agent, prior, scopeFiles = null, object = null) {
|
|
145
148
|
// Convergence follow-up REQUIRES a prior review record — the full output of
|
|
146
149
|
// the last review, injected VERBATIM (decision 2026-08-08: the model
|
|
147
150
|
// understands the review output; no table/header/phrase parsing). The caller
|
|
@@ -164,7 +167,9 @@ export function buildAdvisorFollowUp(agent, prior, scopeFiles = null) {
|
|
|
164
167
|
: "(Agent did not provide a response table — perform a fresh full review; the review surface is unknown, ask the user for the file list)"
|
|
165
168
|
const response = extractAgentResponseTable(agent.history) || noResponseFallback
|
|
166
169
|
const round = (agent._advisorRound || 0) + 1
|
|
167
|
-
|
|
170
|
+
// Review-object declaration FIRST (T-OA2 — round 2+ stays anchored, no re-archaeology).
|
|
171
|
+
const declaration = buildObjectDeclarationBlock(object)
|
|
172
|
+
return (declaration ? declaration + "\n" : "") + buildConvergenceBody(p, response, round, scopeFiles)
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
/**
|
|
@@ -187,8 +192,11 @@ export { escapeLiteralEscapes }
|
|
|
187
192
|
* @param {string|null} [designToken] — design-review approval token (design only)
|
|
188
193
|
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (passed through to buildAdvisorUserMessage)
|
|
189
194
|
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review
|
|
195
|
+
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1): passed through
|
|
196
|
+
* to the user-message builders; mechanically injected at the start of every review
|
|
197
|
+
* round (round 1 design/code + round 2+ follow-up). Absent → legacy behavior.
|
|
190
198
|
*/
|
|
191
|
-
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null) {
|
|
199
|
+
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null, object = null) {
|
|
192
200
|
// Deterministic convergence state (decision 2026-08-08): round 2+ requires
|
|
193
201
|
// _advisorRound > 0 AND a stored prior review output. No history parsing.
|
|
194
202
|
// priorParam (direct callers) wins over the stored output — same derivation
|
|
@@ -201,7 +209,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
201
209
|
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
202
210
|
return [
|
|
203
211
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
204
|
-
{ role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths)) },
|
|
212
|
+
{ role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object)) },
|
|
205
213
|
]
|
|
206
214
|
}
|
|
207
215
|
|
|
@@ -226,7 +234,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
226
234
|
agent._advisorRound = 0
|
|
227
235
|
}
|
|
228
236
|
// Mutations exist → KEEP the round (cap keeps advancing through retries).
|
|
229
|
-
const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths)
|
|
237
|
+
const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object)
|
|
230
238
|
return [
|
|
231
239
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
232
240
|
{
|
|
@@ -257,6 +265,6 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
257
265
|
const scopeFiles = resolveScopeFiles(agent, paths)
|
|
258
266
|
return [
|
|
259
267
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
260
|
-
{ role: "user", content: escapeLiteralEscapes(buildAdvisorFollowUp(agent, prior, scopeFiles)) },
|
|
268
|
+
{ role: "user", content: escapeLiteralEscapes(buildAdvisorFollowUp(agent, prior, scopeFiles, object)) },
|
|
261
269
|
]
|
|
262
270
|
}
|
package/src/agent/dispatch.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent/dispatch.mjs — two-phase tool call execution
|
|
3
3
|
*/
|
|
4
|
+
import { logEvent, errText, headText } from "../log.mjs"
|
|
4
5
|
import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
|
|
5
6
|
import { runHooks } from "../hooks.mjs"
|
|
6
7
|
import { snapshotForUndo } from "../tui/cmd-undo.mjs"
|
|
@@ -37,6 +38,55 @@ function logToolError(toolName, args, error) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* §19 action-level classification (AGENT-LOOP.md §19 D-M1): the merged subagent
|
|
43
|
+
* tool expresses spawn (side effect) and check/status (read-only queries) through
|
|
44
|
+
* its `action` parameter — the tool-level readonly flag can no longer express both.
|
|
45
|
+
* dispatch Phase-1/Phase-2 classifies per action: check/status behave as readonly
|
|
46
|
+
* (planMode pass / no permission ask / batchable), spawn keeps its non-readonly
|
|
47
|
+
* gates, escalate runs non-readonly AND serially (the retired escalate tool had no
|
|
48
|
+
* parallel flag — zero behavior change under the merged surface).
|
|
49
|
+
* §19.5 cancel (19.5.2b round2 #4): CONTROL-class exemption — cancel only
|
|
50
|
+
* stops, never starts. isSubagentControlAction feeds the SAME two gate sites as
|
|
51
|
+
* readonly (planMode pass / no permission ask — never joins a batch approval
|
|
52
|
+
* group / no handler → not denied — digest 内 cancel 放行).
|
|
53
|
+
* §19.6 panel (round1 #5): view 面归只读类(同 check/status——planMode 放行、免
|
|
54
|
+
* 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
|
|
55
|
+
* 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
|
|
56
|
+
*/
|
|
57
|
+
function isSubagentReadonlyAction(toolName, args) {
|
|
58
|
+
// §6 memory 工具面重构(MEMORY.md §6 D-M5):memory search/list 是只读动作——与
|
|
59
|
+
// subagent check/status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
|
|
60
|
+
// readonly/parallel,memory 无 parallel → 按非只读串行,见 MEMORY.md §6.4 实现注)。
|
|
61
|
+
// 动作级判定——不能按工具名(同一 memory 工具的 put/delete/clear 保持侧效门)。
|
|
62
|
+
if (toolName === "memory") {
|
|
63
|
+
const action = args?.action
|
|
64
|
+
return action === "search" || action === "list"
|
|
65
|
+
}
|
|
66
|
+
// SETTINGS-TOOL.md(2026-09-05):settings list/get 是只读动作(memory search/list 同分类——
|
|
67
|
+
// planMode 放行/免审批);set 保持侧效门。
|
|
68
|
+
if (toolName === "settings") {
|
|
69
|
+
const action = args?.action
|
|
70
|
+
return action === "list" || action === "get"
|
|
71
|
+
}
|
|
72
|
+
if (toolName !== "subagent" || !args || typeof args !== "object") return false
|
|
73
|
+
const action = args.action
|
|
74
|
+
if (action === "check" || action === "status") return true
|
|
75
|
+
// §19.6 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
|
|
76
|
+
if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
function isSubagentControlAction(toolName, args) {
|
|
80
|
+
if (toolName !== "subagent") return false
|
|
81
|
+
if (args?.action === "cancel") return true
|
|
82
|
+
// §19.6 panel freeze 面(D-P3 门控在 executor——只读/控制分类在此)
|
|
83
|
+
if (args?.action === "panel" && args.freeze !== undefined && args.freeze !== null && String(args.freeze) !== "") return true
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
function isSubagentEscalateAction(toolName, args) {
|
|
87
|
+
return toolName === "subagent" && args?.action === "escalate"
|
|
88
|
+
}
|
|
89
|
+
|
|
40
90
|
/**
|
|
41
91
|
* Two-phase execution:
|
|
42
92
|
* Phase 1 (serial): parse args one by one + planMode check + permission confirmation (side-effecting tools)
|
|
@@ -46,7 +96,12 @@ function logToolError(toolName, args, error) {
|
|
|
46
96
|
*/
|
|
47
97
|
export async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth = 0, signal) {
|
|
48
98
|
// ---- Phase 1: serial preparation ----
|
|
99
|
+
// Pre-gates run per tool (parse/planMode/engineering gates); non-readonly tools
|
|
100
|
+
// that REACH the permission stage are collected into one batch — a single merged
|
|
101
|
+
// ask covers the whole toolCalls array (§16 D-B1, "approve all / one by one /
|
|
102
|
+
// deny"). Tools stopped by a pre-gate never join the batch (review #7).
|
|
49
103
|
const prepared = []
|
|
104
|
+
const permPending = [] // { toolCall, tool, args } — reached the permission stage
|
|
50
105
|
for (const toolCall of toolCalls) {
|
|
51
106
|
const tool = toolByName.get(toolCall.name)
|
|
52
107
|
let args
|
|
@@ -64,7 +119,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
64
119
|
continue
|
|
65
120
|
}
|
|
66
121
|
|
|
67
|
-
if (agent.planMode && !tool.readonly) {
|
|
122
|
+
if (agent.planMode && !tool.readonly && !isSubagentReadonlyAction(toolCall.name, args) && !isSubagentControlAction(toolCall.name, args)) {
|
|
68
123
|
prepared.push({ toolCall, tool, denied: true, reason: "plan mode" })
|
|
69
124
|
continue
|
|
70
125
|
}
|
|
@@ -106,39 +161,82 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
106
161
|
}
|
|
107
162
|
}
|
|
108
163
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return await callbacks.onPermissionRequest(toolCall.name, args)
|
|
123
|
-
})()
|
|
124
|
-
: false
|
|
125
|
-
if (!allowed) {
|
|
126
|
-
prepared.push({ toolCall, tool, denied: true, reason: callbacks.onPermissionRequest ? "denied by user" : "no permission handler" })
|
|
164
|
+
// Readonly tools (and autoApprove — the short-circuit, unchanged for the
|
|
165
|
+
// whole batch too) skip the permission stage entirely.
|
|
166
|
+
// §18 D-E3 task-domain authorization (spawn-time): an eng-coder child's
|
|
167
|
+
// tools skip the permission ASK stage exactly like autoApprove — granted by
|
|
168
|
+
// the parent spawn (approved design + task = authorization; subagent.mjs
|
|
169
|
+
// sets _engTaskAuthorized on the child). Everything EARLIER in Phase 1
|
|
170
|
+
// (JSON parse / unknown tool / planMode / design-token gates) ran unchanged
|
|
171
|
+
// — the exemption never widens what reaches this stage (round4 #3, T-E14).
|
|
172
|
+
// PreToolUse hooks still run below. Non-eng-coder children keep the manual
|
|
173
|
+
// parent ask (human in the loop).
|
|
174
|
+
if (tool.readonly || isSubagentReadonlyAction(toolCall.name, args) || isSubagentControlAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
|
|
175
|
+
if (!(await runHooks("PreToolUse", { agent, toolName: toolCall.name, toolArgs: args }))) {
|
|
176
|
+
prepared.push({ toolCall, tool, denied: true, reason: "blocked by PreToolUse hook" })
|
|
127
177
|
continue
|
|
128
178
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!(await runHooks("PreToolUse", { agent, toolName: toolCall.name, toolArgs: args }))) {
|
|
133
|
-
prepared.push({ toolCall, tool, denied: true, reason: "blocked by PreToolUse hook" })
|
|
179
|
+
// Panel area abolished — all tools now stream inline via onToolOutput.
|
|
180
|
+
callbacks.onToolCall?.(toolCall.name, args, toolCall.id)
|
|
181
|
+
prepared.push({ toolCall, tool, args })
|
|
134
182
|
continue
|
|
135
183
|
}
|
|
184
|
+
permPending.push({ toolCall, tool, args })
|
|
185
|
+
}
|
|
136
186
|
|
|
137
|
-
|
|
187
|
+
// ---- Permission stage: one merged ask for the whole batch (§16 D-B1) ----
|
|
188
|
+
// >1 non-readonly tools in the same toolCalls array → a single
|
|
189
|
+
// onBatchPermissionRequest({ tools, count }) ask; verdicts:
|
|
190
|
+
// "approveAll" → batch-scope allowance (autoApprove style, NOT persistent)
|
|
191
|
+
// "deny" → the whole batch is rejected, no second ask
|
|
192
|
+
// "oneByOne" (or anything else / no handler) → the existing per-item
|
|
193
|
+
// onPermissionRequest channel, signature unchanged (NF-B1: ACP bridge /
|
|
194
|
+
// headless / old versions without the new callback are never harmed).
|
|
195
|
+
if (permPending.length > 0) {
|
|
196
|
+
let batchAllowed = null // true = approveAll, false = deny, null = per-item fallback
|
|
197
|
+
if (permPending.length > 1 && callbacks.onBatchPermissionRequest) {
|
|
198
|
+
const verdict = await callbacks.onBatchPermissionRequest({
|
|
199
|
+
tools: permPending.map((p) => ({ name: p.toolCall.name, args: p.args })),
|
|
200
|
+
count: permPending.length,
|
|
201
|
+
})
|
|
202
|
+
if (verdict === "approveAll") batchAllowed = true
|
|
203
|
+
else if (verdict === "deny") batchAllowed = false
|
|
204
|
+
// anything else (oneByOne/unknown) → fall through to the per-item channel
|
|
205
|
+
}
|
|
206
|
+
for (const p of permPending) {
|
|
207
|
+
let allowed
|
|
208
|
+
if (batchAllowed === true) allowed = true
|
|
209
|
+
else if (batchAllowed === false) allowed = false
|
|
210
|
+
else if (callbacks.onPermissionRequest) {
|
|
211
|
+
allowed = await (async () => {
|
|
212
|
+
// D2 (AGENT-LOOP.md §7.2): announce the wait BEFORE prompting — the TUI
|
|
213
|
+
// subagent block header flips to "等待审批" so a waiting child is visibly
|
|
214
|
+
// different from a stalled one. Depth>0 only (the parent TUI shows its own
|
|
215
|
+
// permission panel). turn n/max = the child's live turn counters.
|
|
216
|
+
if (depth > 0) {
|
|
217
|
+
callbacks.onToken?.(`⟦ev⟧approval\x1e${agent._currentTurn ?? 0}\x1e${agent._maxTurns ?? 0}\x1eapproval\x1e${String(p.toolCall.name).slice(0, 40)}`)
|
|
218
|
+
}
|
|
219
|
+
return await callbacks.onPermissionRequest(p.toolCall.name, p.args)
|
|
220
|
+
})()
|
|
221
|
+
} else allowed = false
|
|
222
|
+
if (!allowed) {
|
|
223
|
+
prepared.push({
|
|
224
|
+
toolCall: p.toolCall, tool: p.tool, denied: true,
|
|
225
|
+
reason: (callbacks.onPermissionRequest || batchAllowed === false) ? "denied by user" : "no permission handler",
|
|
226
|
+
})
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
138
229
|
|
|
139
|
-
|
|
230
|
+
// PreToolUse hooks: allow user scripts to gate tool execution
|
|
231
|
+
if (!(await runHooks("PreToolUse", { agent, toolName: p.toolCall.name, toolArgs: p.args }))) {
|
|
232
|
+
prepared.push({ toolCall: p.toolCall, tool: p.tool, denied: true, reason: "blocked by PreToolUse hook" })
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
140
235
|
|
|
141
|
-
|
|
236
|
+
// Panel area abolished — all tools now stream inline via onToolOutput.
|
|
237
|
+
callbacks.onToolCall?.(p.toolCall.name, p.args, p.toolCall.id)
|
|
238
|
+
prepared.push({ toolCall: p.toolCall, tool: p.tool, args: p.args })
|
|
239
|
+
}
|
|
142
240
|
}
|
|
143
241
|
|
|
144
242
|
// ---- Phase 2: order-preserving execution ----
|
|
@@ -163,6 +261,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
163
261
|
// 捕获分离(父恢复原始后子的拦截期间父捕获停止、子恢复后父继续)——正确。
|
|
164
262
|
// 声明在 try 之外:catch 块(异常路径)也要访问(报错前的探查输出回显)。
|
|
165
263
|
const capturedConsole = []
|
|
264
|
+
// LOGGING(LOGGING.md):tool:* 事件——仅真实执行(pre-gate 拦截项在下方早退分支不入事件)。
|
|
265
|
+
// 参数值永不落盘(NF-L3——工具事件不记 args);child=子代理 id(agent._logId,spawn 时 stamp)。
|
|
266
|
+
const toolT0 = Date.now()
|
|
267
|
+
const toolName = item.toolCall.name
|
|
268
|
+
logEvent("tool:call", { tool: toolName, child: agent?._logId })
|
|
166
269
|
try {
|
|
167
270
|
// Snapshot for undo before side-effect tools (setupOutputPanel already fired in Phase 1)
|
|
168
271
|
if (!item.tool?.readonly && item.args) {
|
|
@@ -174,6 +277,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
174
277
|
const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
|
|
175
278
|
if (routed?.handled) {
|
|
176
279
|
callbacks.onToolResult?.(item.toolCall.name, routed.result, item.toolCall.id)
|
|
280
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(routed.result, 200), child: agent?._logId })
|
|
177
281
|
return { ...item, result: routed.result, ok: true }
|
|
178
282
|
}
|
|
179
283
|
}
|
|
@@ -182,17 +286,22 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
182
286
|
console.log = (...a) => capturedConsole.push(a.map(String).join(" "))
|
|
183
287
|
console.error = (...a) => capturedConsole.push("[err] " + a.map(String).join(" "))
|
|
184
288
|
let rawResult
|
|
289
|
+
// ctx 对象提升为变量(§7.2.3):subagent 阻塞 execute 返回前在 ctx 上留
|
|
290
|
+
// _subagentKey(relayPrefix 去尾)——runOne 在 execute 返回后读它作 onToolResult
|
|
291
|
+
// 第 4 参(普通工具/错误路径无此字段——undefined 兼容既有签名)。每次工具调用
|
|
292
|
+
// 独立 ctx——并行同名工具(批并行 runOne)各自带自己的 key,互不串扰。
|
|
293
|
+
const toolCtx = {
|
|
294
|
+
cwd: agent.cwd,
|
|
295
|
+
agent,
|
|
296
|
+
depth,
|
|
297
|
+
signal,
|
|
298
|
+
callbacks,
|
|
299
|
+
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
|
300
|
+
onQuestion: callbacks.onQuestion,
|
|
301
|
+
onPermissionRequest: callbacks.onPermissionRequest,
|
|
302
|
+
}
|
|
185
303
|
try {
|
|
186
|
-
rawResult = await item.tool.execute(item.args,
|
|
187
|
-
cwd: agent.cwd,
|
|
188
|
-
agent,
|
|
189
|
-
depth,
|
|
190
|
-
signal,
|
|
191
|
-
callbacks,
|
|
192
|
-
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
|
193
|
-
onQuestion: callbacks.onQuestion,
|
|
194
|
-
onPermissionRequest: callbacks.onPermissionRequest,
|
|
195
|
-
})
|
|
304
|
+
rawResult = await item.tool.execute(item.args, toolCtx)
|
|
196
305
|
} finally {
|
|
197
306
|
console.log = origConsoleLog
|
|
198
307
|
console.error = origConsoleErr
|
|
@@ -207,9 +316,10 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
207
316
|
const resultWithConsole = capturedConsole.length > 0
|
|
208
317
|
? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
209
318
|
: result
|
|
210
|
-
callbacks.onToolResult?.(item.toolCall.name, resultWithConsole, item.toolCall.id)
|
|
319
|
+
callbacks.onToolResult?.(item.toolCall.name, resultWithConsole, item.toolCall.id, toolCtx._subagentKey)
|
|
211
320
|
// PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
|
|
212
321
|
runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
|
|
322
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(resultWithConsole, 200), child: agent?._logId })
|
|
213
323
|
return { ...item, result: resultWithConsole, ok: true }
|
|
214
324
|
} catch (error) {
|
|
215
325
|
// Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
|
|
@@ -219,6 +329,9 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
219
329
|
// asked to stop — worst case with subagents, where the child runs its
|
|
220
330
|
// whole turn budget and the interrupt appears to do nothing.
|
|
221
331
|
if (signal?.aborted) throw error
|
|
332
|
+
// LOGGING(2026-09-03 code review #4):中止先于事件——用户停不落 tool:error
|
|
333
|
+
//(vscode execute-tools parity;阻塞子代理 child:error 同款抑制)
|
|
334
|
+
logEvent("tool:error", { tool: toolName, ms: Date.now() - toolT0, err: errText(error, 200), child: agent?._logId })
|
|
222
335
|
runHooks("PostToolUseFailure", { agent, toolName: item.toolCall.name, toolArgs: item.args, error }).catch(() => {})
|
|
223
336
|
// Build contextual error: tool name + key args so the model can reason about what went wrong
|
|
224
337
|
const ctxParts = []
|
|
@@ -242,7 +355,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
242
355
|
batch = []
|
|
243
356
|
}
|
|
244
357
|
for (const item of prepared) {
|
|
245
|
-
|
|
358
|
+
// escalate action keeps the retired escalate tool's serial placement (no
|
|
359
|
+
// parallel flag): it flushes the batch and runs alone in call order (§19 —
|
|
360
|
+
// spawn stays parallel; check/status classify as readonly and batch freely).
|
|
361
|
+
if (item.tool && !item.tool.readonly
|
|
362
|
+
&& (!item.tool.parallel || isSubagentEscalateAction(item.tool.name, item.args))) {
|
|
246
363
|
await flush()
|
|
247
364
|
results.push(await runOne(item))
|
|
248
365
|
} else {
|