@kolisachint/hoocode-agent 0.4.109 → 0.4.111
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 +63 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/format-duration.d.ts +7 -0
- package/dist/core/format-duration.d.ts.map +1 -0
- package/dist/core/format-duration.js +16 -0
- package/dist/core/format-duration.js.map +1 -0
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +10 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/lifeguard.d.ts +15 -0
- package/dist/core/lifeguard.d.ts.map +1 -1
- package/dist/core/lifeguard.js +48 -18
- package/dist/core/lifeguard.js.map +1 -1
- package/dist/core/subagent-inbox.d.ts +1 -1
- package/dist/core/subagent-inbox.d.ts.map +1 -1
- package/dist/core/subagent-inbox.js +2 -0
- package/dist/core/subagent-inbox.js.map +1 -1
- package/dist/core/subagent-pool-instance.d.ts.map +1 -1
- package/dist/core/subagent-pool-instance.js +18 -11
- package/dist/core/subagent-pool-instance.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +25 -3
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +120 -25
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/subagent-result.d.ts.map +1 -1
- package/dist/core/subagent-result.js +6 -4
- package/dist/core/subagent-result.js.map +1 -1
- package/dist/core/task-store.d.ts +45 -3
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +51 -5
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/team-view.d.ts.map +1 -1
- package/dist/core/team-view.js +2 -0
- package/dist/core/team-view.js.map +1 -1
- package/dist/core/token-budget.d.ts +13 -1
- package/dist/core/token-budget.d.ts.map +1 -1
- package/dist/core/token-budget.js +18 -1
- package/dist/core/token-budget.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +184 -53
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/tools/todo.d.ts.map +1 -1
- package/dist/core/tools/todo.js +39 -7
- package/dist/core/tools/todo.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +10 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +27 -1
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +22 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +302 -80
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/team-attach-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/team-attach-panel.js +15 -5
- package/dist/modes/interactive/components/team-attach-panel.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +8 -1
- package/dist/modes/interactive/theme/light.json +8 -1
- package/dist/modes/interactive/theme/theme-schema.json +28 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +52 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.js +5 -2
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
- package/dist/modes/rpc/jsonl.d.ts +16 -1
- package/dist/modes/rpc/jsonl.d.ts.map +1 -1
- package/dist/modes/rpc/jsonl.js +53 -8
- package/dist/modes/rpc/jsonl.js.map +1 -1
- package/dist/utils/atomic-file.d.ts +13 -0
- package/dist/utils/atomic-file.d.ts.map +1 -0
- package/dist/utils/atomic-file.js +26 -0
- package/dist/utils/atomic-file.js.map +1 -0
- package/examples/extensions/bash-spawn-hook.ts +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { Text } from "@kolisachint/hoocode-tui";
|
|
14
14
|
import { Type } from "typebox";
|
|
15
|
+
import { agentColorFor } from "../../modes/interactive/theme/theme.js";
|
|
15
16
|
import { TASK_TOOL_NAME } from "../agent-frontmatter.js";
|
|
16
17
|
import { loadAgentRegistry } from "../agent-registry.js";
|
|
17
18
|
import { defineTool } from "../extensions/types.js";
|
|
19
|
+
import { formatDurationSecs } from "../format-duration.js";
|
|
18
20
|
import { getProviderExhaustion } from "../provider-health.js";
|
|
19
21
|
import { SessionManager } from "../session-manager.js";
|
|
20
22
|
import { delegateAllowList, isDelegateAllowed } from "../subagent-depth.js";
|
|
@@ -53,6 +55,7 @@ Guidelines:
|
|
|
53
55
|
- Some agents run in the background (non-blocking); force it per call with \`background: true\` (or \`background: false\` to wait inline). A background Task does not block your turn and does not return its answer inline: you get a short notification ("explore#1 finished") and the full result is held for you to pull with \`TaskOutput\`. Keep working in the meantime.
|
|
54
56
|
- After dispatching background work, DO NOT stop and wait — that wastes the parallelism and looks stuck. Immediately continue with the next useful thing: read or edit an independent file, draft the parts of your answer that don't depend on the pending result, or dispatch more independent subtasks. Only barrier (with \`TaskOutput(wait: true)\`) when you genuinely cannot proceed without the result. Prefer background dispatch for any self-contained or parallelizable work so your turn never blocks on a subagent.
|
|
55
57
|
- Use **TaskOutput** to manage background subagents: \`TaskOutput(list: true)\` shows every running/finished subagent and what each is doing; \`TaskOutput("explore#1")\` reads a finished subagent's full result (or reports its status if still running); \`TaskOutput(wait: true)\` blocks until a named task — or, with no task_id, ALL outstanding subagents — finish. Dispatch a batch in one turn, then barrier on them with \`TaskOutput(wait: true)\` only once you've exhausted the work you can do without them.
|
|
58
|
+
- When working through a TodoWrite plan, mark the plan item in_progress BEFORE dispatching subagents for it: each dispatch is attributed to the current in_progress item in the user's task panel, so dispatching first (or with several items in_progress) leaves the run unattributed.
|
|
56
59
|
- To continue a previous subagent (for example one that returned partial results), call Task again with \`resume_task_id\` set to its task_id; it resumes with its full prior transcript and \`prompt\` is your follow-up.`;
|
|
57
60
|
}
|
|
58
61
|
const taskParams = Type.Object({
|
|
@@ -116,8 +119,29 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
116
119
|
].join("\n"),
|
|
117
120
|
promptSnippet: "delegate a self-contained task to a specialized subagent (choose via subagent_type)",
|
|
118
121
|
parameters: taskParams,
|
|
119
|
-
async execute(_toolCallId, params,
|
|
122
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
120
123
|
const pool = getSubagentPool(ctx.cwd);
|
|
124
|
+
// User-initiated cancel (Esc/abort): kill the dispatched run's whole
|
|
125
|
+
// process tree and let the dispatch settle with status "cancelled" —
|
|
126
|
+
// distinct from "failed" everywhere it surfaces (panel, inbox, result).
|
|
127
|
+
// Cold dispatches only: a warm worker has no cancel surface (it is
|
|
128
|
+
// reused), so warm runs still complete server-side and are discarded.
|
|
129
|
+
const cancelOnAbort = async (poolRunId, run) => {
|
|
130
|
+
if (!signal)
|
|
131
|
+
return run();
|
|
132
|
+
const onAbort = () => {
|
|
133
|
+
pool.cancel?.(poolRunId);
|
|
134
|
+
};
|
|
135
|
+
if (signal.aborted)
|
|
136
|
+
onAbort();
|
|
137
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
138
|
+
try {
|
|
139
|
+
return await run();
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
signal.removeEventListener("abort", onAbort);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
121
145
|
// Pre-flight: if the inherited provider recently exhausted its quota (the
|
|
122
146
|
// parent's own turn failed with a usage/rate-limit error that did not
|
|
123
147
|
// recover), skip the spawn. Subagents run on the same provider, so this
|
|
@@ -126,12 +150,16 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
126
150
|
const provider = ctx.model?.provider;
|
|
127
151
|
const exhaustion = provider ? getProviderExhaustion(provider) : undefined;
|
|
128
152
|
if (exhaustion) {
|
|
153
|
+
const skippedRunId = newDispatchTaskId();
|
|
129
154
|
const skipped = taskStore.create(params.description?.trim() || summarize(params.prompt), {
|
|
130
155
|
source: "subagent",
|
|
131
156
|
subagentMode: params.subagent_type,
|
|
132
|
-
agent:
|
|
157
|
+
agent: skippedRunId,
|
|
158
|
+
linkedTaskId: linkedTodoId(),
|
|
133
159
|
});
|
|
160
|
+
registerSubagentDispatch(skippedRunId, subagentInbox.nextLabel(params.subagent_type));
|
|
134
161
|
taskStore.update(skipped.id, { status: "failed", note: `${provider} exhausted` });
|
|
162
|
+
taskStore.patchAgent(skippedRunId, { state: "failed" });
|
|
135
163
|
return {
|
|
136
164
|
content: [
|
|
137
165
|
{
|
|
@@ -156,23 +184,27 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
156
184
|
const resumeId = params.resume_task_id?.trim();
|
|
157
185
|
if (resumeId) {
|
|
158
186
|
const summary = params.description?.trim() || summarize(params.prompt);
|
|
187
|
+
const runId = newDispatchTaskId();
|
|
188
|
+
const label = subagentInbox.nextLabel(params.subagent_type);
|
|
159
189
|
const task = taskStore.create(summary, {
|
|
160
190
|
source: "subagent",
|
|
161
191
|
subagentMode: params.subagent_type,
|
|
162
|
-
agent:
|
|
192
|
+
agent: runId,
|
|
193
|
+
linkedTaskId: linkedTodoId(),
|
|
163
194
|
});
|
|
164
|
-
registerSubagentDispatch(
|
|
195
|
+
registerSubagentDispatch(runId, label);
|
|
165
196
|
taskStore.update(task.id, { status: "in_progress" });
|
|
166
197
|
try {
|
|
167
|
-
const dispatchResult = await pool.resume(resumeId, params.prompt, {
|
|
198
|
+
const dispatchResult = await cancelOnAbort(runId, () => pool.resume(resumeId, params.prompt, {
|
|
168
199
|
model: ctx.model?.id,
|
|
169
200
|
provider: ctx.model?.provider,
|
|
170
|
-
|
|
201
|
+
taskId: runId,
|
|
202
|
+
}));
|
|
171
203
|
// The session lives under the original task id; keep it as the resume handle.
|
|
172
|
-
return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, resumeId);
|
|
204
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, runId, task.id, resumeId);
|
|
173
205
|
}
|
|
174
206
|
catch (error) {
|
|
175
|
-
|
|
207
|
+
markDispatchFailed(task.id, runId);
|
|
176
208
|
throw error;
|
|
177
209
|
}
|
|
178
210
|
}
|
|
@@ -188,12 +220,23 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
188
220
|
throw new Error(`Unknown subagent_type: "${params.subagent_type}". Available agents: ${available || "(none)"}.`);
|
|
189
221
|
}
|
|
190
222
|
const summary = params.description?.trim() || summarize(params.prompt);
|
|
223
|
+
// One roster row per dispatch, keyed by the (pre-allocated) pool task id.
|
|
224
|
+
// Keying by agent TYPE made concurrent same-type runs share a single row:
|
|
225
|
+
// their activity, state, and stats collided last-writer-wins. The inbox
|
|
226
|
+
// already keys by task id; this aligns the panel to the same model. The
|
|
227
|
+
// pool's task_progress events carry the task id, so patching by this id
|
|
228
|
+
// routes live activity to the right row.
|
|
229
|
+
const poolTaskId = newDispatchTaskId();
|
|
230
|
+
const label = subagentInbox.nextLabel(params.subagent_type);
|
|
191
231
|
const task = taskStore.create(summary, {
|
|
192
232
|
source: "subagent",
|
|
193
233
|
subagentMode: params.subagent_type,
|
|
194
|
-
agent:
|
|
234
|
+
agent: poolTaskId,
|
|
235
|
+
// Tie this run to the plan item it executes (the single in_progress
|
|
236
|
+
// TodoWrite task, when unambiguous) so the panel can nest it there.
|
|
237
|
+
linkedTaskId: linkedTodoId(),
|
|
195
238
|
});
|
|
196
|
-
registerSubagentDispatch(
|
|
239
|
+
registerSubagentDispatch(poolTaskId, label);
|
|
197
240
|
taskStore.update(task.id, { status: "in_progress" });
|
|
198
241
|
// Fork agents inherit the parent's conversation via a forked session.
|
|
199
242
|
const forkSessionFile = def.fork
|
|
@@ -210,11 +253,9 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
210
253
|
// recompute it here to choose the notify-and-pull return shape.
|
|
211
254
|
const isBackground = params.background ?? backgroundAgents.has(params.subagent_type);
|
|
212
255
|
if (isBackground) {
|
|
213
|
-
// Notify-and-pull: register the dispatch in the inbox under
|
|
214
|
-
// id, await it, retain the body in the inbox, and return a
|
|
215
|
-
// notification (not the body). The model pulls
|
|
216
|
-
const poolTaskId = `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
217
|
-
const label = subagentInbox.nextLabel(params.subagent_type);
|
|
256
|
+
// Notify-and-pull: register the dispatch in the inbox under the
|
|
257
|
+
// pre-allocated id, await it, retain the body in the inbox, and return a
|
|
258
|
+
// compact notification (not the body). The model pulls it with TaskOutput.
|
|
218
259
|
subagentInbox.observe(pool);
|
|
219
260
|
subagentInbox.start(poolTaskId, label, params.subagent_type);
|
|
220
261
|
// Warm path (opt-in): run on a reused RPC worker, retain the body in the
|
|
@@ -229,10 +270,10 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
229
270
|
cwd: ctx.cwd,
|
|
230
271
|
model: dispatchModel,
|
|
231
272
|
provider: ctx.model?.provider,
|
|
232
|
-
}, (activity) => taskStore.patchAgent(
|
|
273
|
+
}, (activity) => taskStore.patchAgent(poolTaskId, { activity }));
|
|
233
274
|
const dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);
|
|
234
275
|
subagentInbox.finish(poolTaskId, dispatchResult);
|
|
235
|
-
return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, poolTaskId, {
|
|
276
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, poolTaskId, {
|
|
236
277
|
taskId: poolTaskId,
|
|
237
278
|
label,
|
|
238
279
|
});
|
|
@@ -245,23 +286,23 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
245
286
|
}
|
|
246
287
|
}
|
|
247
288
|
try {
|
|
248
|
-
const dispatchResult = await pool.dispatch(params.prompt, {
|
|
289
|
+
const dispatchResult = await cancelOnAbort(poolTaskId, () => pool.dispatch(params.prompt, {
|
|
249
290
|
forceAgent: params.subagent_type,
|
|
250
291
|
context: "",
|
|
251
292
|
model: dispatchModel,
|
|
252
293
|
provider: ctx.model?.provider,
|
|
253
294
|
sessionFile: forkSessionFile,
|
|
254
295
|
taskId: poolTaskId,
|
|
255
|
-
});
|
|
296
|
+
}));
|
|
256
297
|
subagentInbox.finish(poolTaskId, dispatchResult);
|
|
257
|
-
return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, poolTaskId, {
|
|
298
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, poolTaskId, {
|
|
258
299
|
taskId: poolTaskId,
|
|
259
300
|
label,
|
|
260
301
|
});
|
|
261
302
|
}
|
|
262
303
|
catch (error) {
|
|
263
304
|
const reason = error instanceof Error ? error.message : String(error);
|
|
264
|
-
|
|
305
|
+
markDispatchFailed(task.id, poolTaskId);
|
|
265
306
|
subagentInbox.fail(poolTaskId, reason);
|
|
266
307
|
// A background dispatch reports failure as a compact notification, not a
|
|
267
308
|
// thrown tool error — the call was already answered by a placeholder.
|
|
@@ -295,15 +336,16 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
295
336
|
},
|
|
296
337
|
// Mirror the cold pool's live progress on the task panel roster so a
|
|
297
338
|
// warm dispatch reads as busy (⋯ grep), not stuck.
|
|
298
|
-
(activity) => taskStore.patchAgent(
|
|
339
|
+
(activity) => taskStore.patchAgent(poolTaskId, { activity }));
|
|
299
340
|
const dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);
|
|
300
|
-
return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, undefined);
|
|
341
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, undefined);
|
|
301
342
|
}
|
|
302
343
|
catch (error) {
|
|
303
344
|
// A genuine infra failure (worker crash/timeout) retries cold; any other
|
|
304
345
|
// error is a real dispatch failure and propagates.
|
|
305
346
|
if (!(error instanceof WarmWorkerError)) {
|
|
306
347
|
taskStore.update(task.id, { status: "failed" });
|
|
348
|
+
taskStore.patchAgent(poolTaskId, { state: "failed", activity: "" });
|
|
307
349
|
throw error;
|
|
308
350
|
}
|
|
309
351
|
console.error(`[WARM] ${params.subagent_type} fell back to cold spawn: ${error.message}`);
|
|
@@ -312,23 +354,27 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
312
354
|
}
|
|
313
355
|
// Foreground: block the turn and return the subagent's full answer inline.
|
|
314
356
|
try {
|
|
315
|
-
const dispatchResult = await pool.dispatch(params.prompt, {
|
|
357
|
+
const dispatchResult = await cancelOnAbort(poolTaskId, () => pool.dispatch(params.prompt, {
|
|
316
358
|
forceAgent: params.subagent_type,
|
|
317
359
|
context: "",
|
|
318
360
|
model: dispatchModel,
|
|
319
361
|
provider: ctx.model?.provider,
|
|
320
362
|
sessionFile: forkSessionFile,
|
|
321
|
-
|
|
322
|
-
|
|
363
|
+
taskId: poolTaskId,
|
|
364
|
+
}));
|
|
365
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, dispatchResult.task_id);
|
|
323
366
|
}
|
|
324
367
|
catch (error) {
|
|
325
|
-
|
|
368
|
+
markDispatchFailed(task.id, poolTaskId);
|
|
326
369
|
throw error;
|
|
327
370
|
}
|
|
328
371
|
},
|
|
329
372
|
renderCall(args, theme) {
|
|
330
373
|
const type = args.subagent_type ?? "agent";
|
|
331
|
-
|
|
374
|
+
// The [type] tag carries the agent's identity color — the same hue this
|
|
375
|
+
// agent has in the task panel and TaskOutput — so a transcript full of
|
|
376
|
+
// concurrent dispatches is scannable by color.
|
|
377
|
+
const text = theme.fg("toolTitle", theme.bold("Agent ")) + theme.fg(agentColorFor(type), `[${type}]`);
|
|
332
378
|
return new Text(text, 0, 0);
|
|
333
379
|
},
|
|
334
380
|
});
|
|
@@ -350,13 +396,47 @@ export function resolveForkSessionFile(def, parentSessionPath, cwd) {
|
|
|
350
396
|
return undefined;
|
|
351
397
|
}
|
|
352
398
|
}
|
|
399
|
+
/** Pre-allocated pool task id for a dispatch (matches the pool's own format). */
|
|
400
|
+
function newDispatchTaskId() {
|
|
401
|
+
return `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
402
|
+
}
|
|
353
403
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
404
|
+
* The TodoWrite plan item this dispatch is (most plausibly) working on: the
|
|
405
|
+
* single in_progress main-agent task. TodoWrite discipline keeps exactly one
|
|
406
|
+
* item in_progress, so when that holds the link is unambiguous; with zero or
|
|
407
|
+
* several in_progress items no link is recorded rather than guessing. Used to
|
|
408
|
+
* nest the run under its plan item in the task panel's flat lens.
|
|
357
409
|
*/
|
|
358
|
-
function
|
|
359
|
-
|
|
410
|
+
function linkedTodoId() {
|
|
411
|
+
const inProgress = taskStore
|
|
412
|
+
.list()
|
|
413
|
+
.filter((t) => t.source === undefined &&
|
|
414
|
+
t.agent === undefined &&
|
|
415
|
+
t.parentTaskId === undefined &&
|
|
416
|
+
t.status === "in_progress");
|
|
417
|
+
return inProgress.length === 1 ? inProgress[0]?.id : undefined;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Register this dispatch in the task store's roster, keyed by its run id (the
|
|
421
|
+
* pool task id) so concurrent runs of the same agent type each get their own
|
|
422
|
+
* row, state, activity, and stats. The friendly label ("explore#1") names the
|
|
423
|
+
* row the same way the inbox names background tasks.
|
|
424
|
+
*/
|
|
425
|
+
function registerSubagentDispatch(runId, label) {
|
|
426
|
+
taskStore.upsertAgent({ id: runId, name: label, role: "subagent", kind: "subagent", state: "running" });
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Catch-path bookkeeping: mark the dispatch failed unless it already settled.
|
|
430
|
+
* finalizeDispatchResult records a terminal status (failed/cancelled) *before*
|
|
431
|
+
* throwing, so a blanket "failed" here would clobber a cancellation with a
|
|
432
|
+
* failure the moment the thrown error passed through.
|
|
433
|
+
*/
|
|
434
|
+
function markDispatchFailed(taskStoreId, runId) {
|
|
435
|
+
const current = taskStore.list().find((t) => t.id === taskStoreId);
|
|
436
|
+
if (current && current.status !== "pending" && current.status !== "in_progress")
|
|
437
|
+
return;
|
|
438
|
+
taskStore.update(taskStoreId, { status: "failed" });
|
|
439
|
+
taskStore.patchAgent(runId, { state: "failed", activity: "" });
|
|
360
440
|
}
|
|
361
441
|
/** Names of agents configured to run in the background (non-blocking). */
|
|
362
442
|
function collectBackgroundAgentNames(cwd) {
|
|
@@ -426,26 +506,29 @@ function warmResultToTaskResult(warm, agentType, taskStoreId) {
|
|
|
426
506
|
* inbox, so it returns a compact, self-contained notification (success or
|
|
427
507
|
* failure) and never throws — the call was already answered by a placeholder.
|
|
428
508
|
*/
|
|
429
|
-
function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resumeHandle, background) {
|
|
509
|
+
function finalizeDispatchResult(dispatchResult, subagentType, runAgentId, taskStoreId, resumeHandle, background) {
|
|
430
510
|
const result = dispatchResult.result;
|
|
431
511
|
const resultData = result?.result_data;
|
|
432
512
|
const usage = resultData?.usage;
|
|
433
513
|
// Merge the child's own task subtree under the dispatching task so nested
|
|
434
514
|
// delegation (depth >= 2) is visible in the subagents lens's task tree.
|
|
435
515
|
mergeChildTaskTree(resultData?.task_tree, taskStoreId);
|
|
436
|
-
// Roll
|
|
437
|
-
// header carries the agent's own token/cost totals.
|
|
516
|
+
// Roll this run's usage into its own roster row (rows are per-dispatch).
|
|
438
517
|
if (usage) {
|
|
439
|
-
taskStore.addAgentStats(
|
|
518
|
+
taskStore.addAgentStats(runAgentId, { input: usage.input, output: usage.output, cost: usage.cost });
|
|
440
519
|
}
|
|
441
520
|
if (!result || !result.ok) {
|
|
521
|
+
// A user-initiated cancel is not a failure: it gets its own terminal
|
|
522
|
+
// status so the panel shows ⊘ cancelled (dim) instead of ✗ failed (red).
|
|
523
|
+
const cancelled = result?.status === "cancelled";
|
|
442
524
|
const failNote = result?.usedInheritedModelFallback ? "inherited-model retry failed" : undefined;
|
|
443
|
-
taskStore.update(taskStoreId, { status: "failed", usage, note: failNote });
|
|
444
|
-
taskStore.patchAgent(
|
|
525
|
+
taskStore.update(taskStoreId, { status: cancelled ? "cancelled" : "failed", usage, note: failNote });
|
|
526
|
+
taskStore.patchAgent(runAgentId, { state: cancelled ? "cancelled" : "failed", activity: "" });
|
|
445
527
|
const reason = result?.error ?? (result?.status ? `subagent ${result.status}` : "unknown error");
|
|
446
528
|
if (background) {
|
|
529
|
+
const verdict = cancelled ? "cancelled ⊘" : "failed ✗";
|
|
447
530
|
return {
|
|
448
|
-
content: [{ type: "text", text: `${background.label}
|
|
531
|
+
content: [{ type: "text", text: `${background.label} ${verdict} — ${reason}` }],
|
|
449
532
|
details: {
|
|
450
533
|
subagent_type: subagentType,
|
|
451
534
|
ok: false,
|
|
@@ -456,20 +539,20 @@ function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resum
|
|
|
456
539
|
},
|
|
457
540
|
};
|
|
458
541
|
}
|
|
542
|
+
if (cancelled) {
|
|
543
|
+
throw new Error(`Subagent (${subagentType}) cancelled by user.`);
|
|
544
|
+
}
|
|
459
545
|
const stderr = result?.stderr?.trim();
|
|
460
546
|
throw new Error(`Subagent (${subagentType}) failed: ${reason}${stderr ? `\nstderr: ${stderr.slice(-500)}` : ""}`);
|
|
461
547
|
}
|
|
462
548
|
// Leave the task in the store with its final status; it stays visible in the
|
|
463
549
|
// task panel until the next user message arrives. Surface a ⚠ cue when the run
|
|
464
|
-
// fell back to the inherited model rather than emitting a chat message
|
|
550
|
+
// fell back to the inherited model rather than emitting a chat message — and
|
|
551
|
+
// clear any stale note otherwise (note is clearable via an explicit undefined).
|
|
465
552
|
const fallbackNote = dispatchResult.result?.usedInheritedModelFallback ? "ran on inherited model" : undefined;
|
|
466
553
|
taskStore.update(taskStoreId, { status: "done", usage, note: fallbackNote });
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
const siblingLive = taskStore
|
|
470
|
-
.list()
|
|
471
|
-
.some((t) => t.agent === subagentType && (t.status === "in_progress" || t.status === "pending"));
|
|
472
|
-
taskStore.patchAgent(subagentType, { state: siblingLive ? "running" : "done" });
|
|
554
|
+
// Rows are per-run, so this run settles to done regardless of siblings.
|
|
555
|
+
taskStore.patchAgent(runAgentId, { state: "done", activity: "" });
|
|
473
556
|
let answer = resultData?.summary || "(subagent returned no output)";
|
|
474
557
|
// Partial results are resumable; surface the handle so the parent can continue.
|
|
475
558
|
if (result.status === "partial" && resumeHandle) {
|
|
@@ -504,7 +587,7 @@ const taskOutputParams = Type.Object({
|
|
|
504
587
|
description: 'Handle of a background subagent — its task_id or friendly label (e.g. "explore#1") from a Task notification. Omit (or set list:true) to see every background task.',
|
|
505
588
|
})),
|
|
506
589
|
list: Type.Optional(Type.Boolean({
|
|
507
|
-
description: "List all background subagents with their status (running/done/failed) and current activity. No result bodies are returned.",
|
|
590
|
+
description: "List all background subagents with their status (running/done/failed/cancelled) and current activity. No result bodies are returned.",
|
|
508
591
|
})),
|
|
509
592
|
wait: Type.Optional(Type.Boolean({
|
|
510
593
|
description: "Block until the named task finishes — or, with no task_id, until all outstanding subagents finish (a swarm barrier) — before returning. Bounded by timeout_ms.",
|
|
@@ -512,10 +595,13 @@ const taskOutputParams = Type.Object({
|
|
|
512
595
|
timeout_ms: Type.Optional(Type.Number({ description: "Maximum time to block in wait mode, in milliseconds (default 120000)." })),
|
|
513
596
|
});
|
|
514
597
|
const TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 120_000;
|
|
515
|
-
/**
|
|
598
|
+
/**
|
|
599
|
+
* Elapsed time a record has run (so far, or until it settled), in the same
|
|
600
|
+
* format the task panel uses so the two surfaces always agree.
|
|
601
|
+
*/
|
|
516
602
|
function recordElapsed(rec) {
|
|
517
603
|
const end = rec.endedAt ?? Date.now();
|
|
518
|
-
return
|
|
604
|
+
return formatDurationSecs((end - rec.startedAt) / 1000);
|
|
519
605
|
}
|
|
520
606
|
/** A compact roster of every known background subagent — status + activity, no bodies. */
|
|
521
607
|
function formatTaskRoster() {
|
|
@@ -536,6 +622,8 @@ function formatTaskRoster() {
|
|
|
536
622
|
return `- ${r.label} done (uncollected) ${when} — ${r.summaryLine ?? ""}`;
|
|
537
623
|
case "collected":
|
|
538
624
|
return `- ${r.label} collected ${when} — ${r.summaryLine ?? ""}`;
|
|
625
|
+
case "cancelled":
|
|
626
|
+
return `- ${r.label} cancelled ⊘ — ${r.error ?? "cancelled by user"}`;
|
|
539
627
|
default:
|
|
540
628
|
return `- ${r.label} ${r.lifecycle} ✗ — ${r.error ?? "unknown error"}`;
|
|
541
629
|
}
|
|
@@ -629,19 +717,62 @@ export function createTaskOutputToolDefinition() {
|
|
|
629
717
|
details: { task_id: handle, status: "collected", ok: true },
|
|
630
718
|
};
|
|
631
719
|
}
|
|
632
|
-
// failed / stalled / timeout
|
|
720
|
+
// failed / stalled / timeout / cancelled
|
|
721
|
+
const glyph = rec.lifecycle === "cancelled" ? "⊘" : "✗";
|
|
633
722
|
return {
|
|
634
723
|
content: [
|
|
635
|
-
{
|
|
724
|
+
{
|
|
725
|
+
type: "text",
|
|
726
|
+
text: `${rec.label} ${rec.lifecycle} ${glyph} — ${rec.error ?? "unknown error"}.`,
|
|
727
|
+
},
|
|
636
728
|
],
|
|
637
729
|
details: { task_id: handle, status: rec.lifecycle, ok: false },
|
|
638
730
|
};
|
|
639
731
|
},
|
|
640
732
|
renderCall(args, theme) {
|
|
641
733
|
const target = args.list ? "list" : String(args.task_id ?? "");
|
|
642
|
-
|
|
734
|
+
// A friendly label ("explore#1") gets its agent's identity color so the
|
|
735
|
+
// pull visually pairs with the dispatch that produced it; raw ids and
|
|
736
|
+
// "list" stay dim.
|
|
737
|
+
const hashIdx = target.indexOf("#");
|
|
738
|
+
const styledTarget = hashIdx > 0 ? theme.fg(agentColorFor(target.slice(0, hashIdx)), target) : theme.fg("dim", target);
|
|
739
|
+
const text = theme.fg("toolTitle", theme.bold("TaskOutput ")) +
|
|
740
|
+
styledTarget +
|
|
741
|
+
(args.wait ? theme.fg("dim", " (wait)") : "");
|
|
643
742
|
return new Text(text, 0, 0);
|
|
644
743
|
},
|
|
744
|
+
renderResult(result, _options, theme) {
|
|
745
|
+
// Display-only colorization of the roster/status lines (the text sent to
|
|
746
|
+
// the model stays plain). Lines that aren't roster rows — collected
|
|
747
|
+
// bodies, status sentences — render exactly like the default fallback.
|
|
748
|
+
const text = result.content
|
|
749
|
+
.map((c) => (c.type === "text" ? c.text : ""))
|
|
750
|
+
.filter(Boolean)
|
|
751
|
+
.join("\n");
|
|
752
|
+
if (!text)
|
|
753
|
+
return new Text("", 0, 0);
|
|
754
|
+
const rosterLine = /^- (\S+)\s{2}(running|done \(uncollected\)|collected|failed|stalled|timeout|cancelled)(.*)$/;
|
|
755
|
+
const styled = text
|
|
756
|
+
.split("\n")
|
|
757
|
+
.map((line) => {
|
|
758
|
+
const match = rosterLine.exec(line);
|
|
759
|
+
if (!match)
|
|
760
|
+
return theme.fg("toolOutput", line);
|
|
761
|
+
const [, label, status, rest] = match;
|
|
762
|
+
const hashIdx = label.indexOf("#");
|
|
763
|
+
const labelColor = hashIdx > 0 ? agentColorFor(label.slice(0, hashIdx)) : "accent";
|
|
764
|
+
const statusColor = status === "running"
|
|
765
|
+
? "warning"
|
|
766
|
+
: status.startsWith("done")
|
|
767
|
+
? "success"
|
|
768
|
+
: status === "collected" || status === "cancelled"
|
|
769
|
+
? "muted"
|
|
770
|
+
: "error";
|
|
771
|
+
return `- ${theme.fg(labelColor, label)} ${theme.fg(statusColor, status)}${theme.fg("dim", rest)}`;
|
|
772
|
+
})
|
|
773
|
+
.join("\n");
|
|
774
|
+
return new Text(styled, 0, 0);
|
|
775
|
+
},
|
|
645
776
|
});
|
|
646
777
|
}
|
|
647
778
|
//# sourceMappingURL=subagent.js.map
|