@kolisachint/hoocode-agent 0.4.110 → 0.4.112
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 +107 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +10 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +17 -2
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/marketplace.d.ts +36 -5
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
- package/dist/core/extensions/plugins/marketplace.js +63 -17
- package/dist/core/extensions/plugins/marketplace.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/scheduler.d.ts +8 -2
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +9 -3
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +2 -2
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.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 +16 -4
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +211 -67
- 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/extensions/core/loop.d.ts.map +1 -1
- package/dist/extensions/core/loop.js +4 -1
- package/dist/extensions/core/loop.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +5 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +37 -15
- package/dist/extensions/core/marketplace.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/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
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
* fresh, isolated child process (SubagentPool) and only its final answer is
|
|
8
8
|
* returned to the parent.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* `enableSubagent
|
|
10
|
+
* Enabled by default (the `enableSubagent` setting defaults to true); disable
|
|
11
|
+
* with `enableSubagent: false`. The `--enable-subagents` flag still force-enables
|
|
12
|
+
* it. Nesting is bounded by the tree-wide depth cap (maxSubagentDepth, default 2:
|
|
13
|
+
* a spawned subagent may itself delegate one more level, and depth-2 grandchildren
|
|
14
|
+
* cannot). See buildSessionOptions in main.ts.
|
|
12
15
|
*/
|
|
13
16
|
import { Text } from "@kolisachint/hoocode-tui";
|
|
14
17
|
import { Type } from "typebox";
|
|
18
|
+
import { agentColorFor } from "../../modes/interactive/theme/theme.js";
|
|
15
19
|
import { TASK_TOOL_NAME } from "../agent-frontmatter.js";
|
|
16
20
|
import { loadAgentRegistry } from "../agent-registry.js";
|
|
17
21
|
import { defineTool } from "../extensions/types.js";
|
|
22
|
+
import { formatDurationSecs } from "../format-duration.js";
|
|
18
23
|
import { getProviderExhaustion } from "../provider-health.js";
|
|
19
24
|
import { SessionManager } from "../session-manager.js";
|
|
20
25
|
import { delegateAllowList, isDelegateAllowed } from "../subagent-depth.js";
|
|
@@ -33,7 +38,21 @@ export { summarizeAgentDescription } from "../agent-registry.js";
|
|
|
33
38
|
* `<available_agents>` block the system prompt emits whenever the Task tool is
|
|
34
39
|
* active (see agent-session `_rebuildSystemPrompt`); this appendix references
|
|
35
40
|
* that list rather than re-rendering the roster and paying for it twice. */
|
|
36
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Build the main-session subagent instructions appended to the system prompt.
|
|
43
|
+
*
|
|
44
|
+
* The detailed background/barrier guidance (the three heaviest bullets) is only
|
|
45
|
+
* emitted when the project actually has background-capable agents; otherwise a
|
|
46
|
+
* single concise line covers the per-call `background: true` escape hatch. This
|
|
47
|
+
* keeps the always-on cost down for the common case where nothing runs in the
|
|
48
|
+
* background. `cwd` is used only to detect those agents.
|
|
49
|
+
*/
|
|
50
|
+
export function buildTaskMainPrompt(cwd = process.cwd()) {
|
|
51
|
+
const hasBackgroundAgents = collectBackgroundAgentNames(cwd).size > 0;
|
|
52
|
+
const backgroundGuidance = hasBackgroundAgents
|
|
53
|
+
? `- Background agents run non-blocking (or force per call with \`background: true\`): you get a short "explore#1 finished" notification and pull the full result with \`TaskOutput\` (e.g. \`TaskOutput("explore#1")\`); \`TaskOutput(list: true)\` shows what's running.
|
|
54
|
+
- After dispatching, don't idle — keep doing independent work (read/edit unrelated files, draft, dispatch more); barrier with \`TaskOutput(wait: true)\` (a named task, or all outstanding when no id) only when you genuinely can't proceed.`
|
|
55
|
+
: `- You can force any task to run in the background with \`background: true\` (non-blocking): keep working, then pull its result with \`TaskOutput\` (e.g. \`TaskOutput("explore#1")\`) or block on it with \`TaskOutput(wait: true)\`.`;
|
|
37
56
|
return `You have access to the **Task** tool. Use it to delegate self-contained tasks to specialized subagents that run in their own isolated context and return only their final answer. Pick an agent by name from the <available_agents> list in this prompt and pass it as \`subagent_type\`.
|
|
38
57
|
|
|
39
58
|
When to delegate:
|
|
@@ -50,9 +69,8 @@ Guidelines:
|
|
|
50
69
|
- Do NOT delegate tasks that require tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about.
|
|
51
70
|
- The subagent returns ONLY its final answer. Its intermediate reasoning, tool calls, and output are hidden from you.
|
|
52
71
|
- Delegate proactively when work is self-contained or parallelizable: multi-step investigation, read-only exploration (use \`explore\`), research before changes (use \`plan\`), drafting a standalone file/section, or running a long command/test suite. Dispatch independent subtasks in the same turn. Handle only trivial single-step edits or tightly interactive back-and-forth inline.
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
- 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.
|
|
72
|
+
${backgroundGuidance}
|
|
73
|
+
- 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
74
|
- 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
75
|
}
|
|
58
76
|
const taskParams = Type.Object({
|
|
@@ -106,18 +124,37 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
106
124
|
return override;
|
|
107
125
|
return backgroundAgents.has(String(toolCall.arguments?.subagent_type ?? ""));
|
|
108
126
|
},
|
|
109
|
-
// Kept
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"Do NOT use for tasks needing tight back-and-forth with your current reasoning, or edits to files you are actively reasoning about. Delegate proactively for self-contained or parallelizable work; handle only trivial single-step or tightly interactive work inline.",
|
|
116
|
-
].join("\n"),
|
|
127
|
+
// Kept to mechanics only: the when-to-use / when-not-to guidance lives once in
|
|
128
|
+
// the system-prompt block (buildTaskMainPrompt), the available agents are
|
|
129
|
+
// listed there too, and the `complexity`/`background`/`resume_task_id`
|
|
130
|
+
// semantics live in their parameter descriptions. Repeating any of that here
|
|
131
|
+
// would re-spend those tokens on every turn.
|
|
132
|
+
description: "Delegate a focused task to a specialized subagent that runs in a fresh, isolated context (it cannot see this conversation). Choose one of the available agents (listed in the system prompt) via `subagent_type` and pass everything it needs via `prompt`; the subagent returns only its final answer.",
|
|
117
133
|
promptSnippet: "delegate a self-contained task to a specialized subagent (choose via subagent_type)",
|
|
118
134
|
parameters: taskParams,
|
|
119
|
-
async execute(_toolCallId, params,
|
|
135
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
120
136
|
const pool = getSubagentPool(ctx.cwd);
|
|
137
|
+
// User-initiated cancel (Esc/abort): kill the dispatched run's whole
|
|
138
|
+
// process tree and let the dispatch settle with status "cancelled" —
|
|
139
|
+
// distinct from "failed" everywhere it surfaces (panel, inbox, result).
|
|
140
|
+
// Cold dispatches only: a warm worker has no cancel surface (it is
|
|
141
|
+
// reused), so warm runs still complete server-side and are discarded.
|
|
142
|
+
const cancelOnAbort = async (poolRunId, run) => {
|
|
143
|
+
if (!signal)
|
|
144
|
+
return run();
|
|
145
|
+
const onAbort = () => {
|
|
146
|
+
pool.cancel?.(poolRunId);
|
|
147
|
+
};
|
|
148
|
+
if (signal.aborted)
|
|
149
|
+
onAbort();
|
|
150
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
151
|
+
try {
|
|
152
|
+
return await run();
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
signal.removeEventListener("abort", onAbort);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
121
158
|
// Pre-flight: if the inherited provider recently exhausted its quota (the
|
|
122
159
|
// parent's own turn failed with a usage/rate-limit error that did not
|
|
123
160
|
// recover), skip the spawn. Subagents run on the same provider, so this
|
|
@@ -126,12 +163,16 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
126
163
|
const provider = ctx.model?.provider;
|
|
127
164
|
const exhaustion = provider ? getProviderExhaustion(provider) : undefined;
|
|
128
165
|
if (exhaustion) {
|
|
166
|
+
const skippedRunId = newDispatchTaskId();
|
|
129
167
|
const skipped = taskStore.create(params.description?.trim() || summarize(params.prompt), {
|
|
130
168
|
source: "subagent",
|
|
131
169
|
subagentMode: params.subagent_type,
|
|
132
|
-
agent:
|
|
170
|
+
agent: skippedRunId,
|
|
171
|
+
linkedTaskId: linkedTodoId(),
|
|
133
172
|
});
|
|
173
|
+
registerSubagentDispatch(skippedRunId, subagentInbox.nextLabel(params.subagent_type));
|
|
134
174
|
taskStore.update(skipped.id, { status: "failed", note: `${provider} exhausted` });
|
|
175
|
+
taskStore.patchAgent(skippedRunId, { state: "failed" });
|
|
135
176
|
return {
|
|
136
177
|
content: [
|
|
137
178
|
{
|
|
@@ -156,23 +197,27 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
156
197
|
const resumeId = params.resume_task_id?.trim();
|
|
157
198
|
if (resumeId) {
|
|
158
199
|
const summary = params.description?.trim() || summarize(params.prompt);
|
|
200
|
+
const runId = newDispatchTaskId();
|
|
201
|
+
const label = subagentInbox.nextLabel(params.subagent_type);
|
|
159
202
|
const task = taskStore.create(summary, {
|
|
160
203
|
source: "subagent",
|
|
161
204
|
subagentMode: params.subagent_type,
|
|
162
|
-
agent:
|
|
205
|
+
agent: runId,
|
|
206
|
+
linkedTaskId: linkedTodoId(),
|
|
163
207
|
});
|
|
164
|
-
registerSubagentDispatch(
|
|
208
|
+
registerSubagentDispatch(runId, label);
|
|
165
209
|
taskStore.update(task.id, { status: "in_progress" });
|
|
166
210
|
try {
|
|
167
|
-
const dispatchResult = await pool.resume(resumeId, params.prompt, {
|
|
211
|
+
const dispatchResult = await cancelOnAbort(runId, () => pool.resume(resumeId, params.prompt, {
|
|
168
212
|
model: ctx.model?.id,
|
|
169
213
|
provider: ctx.model?.provider,
|
|
170
|
-
|
|
214
|
+
taskId: runId,
|
|
215
|
+
}));
|
|
171
216
|
// The session lives under the original task id; keep it as the resume handle.
|
|
172
|
-
return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, resumeId);
|
|
217
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, runId, task.id, resumeId);
|
|
173
218
|
}
|
|
174
219
|
catch (error) {
|
|
175
|
-
|
|
220
|
+
markDispatchFailed(task.id, runId);
|
|
176
221
|
throw error;
|
|
177
222
|
}
|
|
178
223
|
}
|
|
@@ -188,12 +233,23 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
188
233
|
throw new Error(`Unknown subagent_type: "${params.subagent_type}". Available agents: ${available || "(none)"}.`);
|
|
189
234
|
}
|
|
190
235
|
const summary = params.description?.trim() || summarize(params.prompt);
|
|
236
|
+
// One roster row per dispatch, keyed by the (pre-allocated) pool task id.
|
|
237
|
+
// Keying by agent TYPE made concurrent same-type runs share a single row:
|
|
238
|
+
// their activity, state, and stats collided last-writer-wins. The inbox
|
|
239
|
+
// already keys by task id; this aligns the panel to the same model. The
|
|
240
|
+
// pool's task_progress events carry the task id, so patching by this id
|
|
241
|
+
// routes live activity to the right row.
|
|
242
|
+
const poolTaskId = newDispatchTaskId();
|
|
243
|
+
const label = subagentInbox.nextLabel(params.subagent_type);
|
|
191
244
|
const task = taskStore.create(summary, {
|
|
192
245
|
source: "subagent",
|
|
193
246
|
subagentMode: params.subagent_type,
|
|
194
|
-
agent:
|
|
247
|
+
agent: poolTaskId,
|
|
248
|
+
// Tie this run to the plan item it executes (the single in_progress
|
|
249
|
+
// TodoWrite task, when unambiguous) so the panel can nest it there.
|
|
250
|
+
linkedTaskId: linkedTodoId(),
|
|
195
251
|
});
|
|
196
|
-
registerSubagentDispatch(
|
|
252
|
+
registerSubagentDispatch(poolTaskId, label);
|
|
197
253
|
taskStore.update(task.id, { status: "in_progress" });
|
|
198
254
|
// Fork agents inherit the parent's conversation via a forked session.
|
|
199
255
|
const forkSessionFile = def.fork
|
|
@@ -210,11 +266,9 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
210
266
|
// recompute it here to choose the notify-and-pull return shape.
|
|
211
267
|
const isBackground = params.background ?? backgroundAgents.has(params.subagent_type);
|
|
212
268
|
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);
|
|
269
|
+
// Notify-and-pull: register the dispatch in the inbox under the
|
|
270
|
+
// pre-allocated id, await it, retain the body in the inbox, and return a
|
|
271
|
+
// compact notification (not the body). The model pulls it with TaskOutput.
|
|
218
272
|
subagentInbox.observe(pool);
|
|
219
273
|
subagentInbox.start(poolTaskId, label, params.subagent_type);
|
|
220
274
|
// Warm path (opt-in): run on a reused RPC worker, retain the body in the
|
|
@@ -229,10 +283,10 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
229
283
|
cwd: ctx.cwd,
|
|
230
284
|
model: dispatchModel,
|
|
231
285
|
provider: ctx.model?.provider,
|
|
232
|
-
}, (activity) => taskStore.patchAgent(
|
|
286
|
+
}, (activity) => taskStore.patchAgent(poolTaskId, { activity }));
|
|
233
287
|
const dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);
|
|
234
288
|
subagentInbox.finish(poolTaskId, dispatchResult);
|
|
235
|
-
return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, poolTaskId, {
|
|
289
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, poolTaskId, {
|
|
236
290
|
taskId: poolTaskId,
|
|
237
291
|
label,
|
|
238
292
|
});
|
|
@@ -245,23 +299,23 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
245
299
|
}
|
|
246
300
|
}
|
|
247
301
|
try {
|
|
248
|
-
const dispatchResult = await pool.dispatch(params.prompt, {
|
|
302
|
+
const dispatchResult = await cancelOnAbort(poolTaskId, () => pool.dispatch(params.prompt, {
|
|
249
303
|
forceAgent: params.subagent_type,
|
|
250
304
|
context: "",
|
|
251
305
|
model: dispatchModel,
|
|
252
306
|
provider: ctx.model?.provider,
|
|
253
307
|
sessionFile: forkSessionFile,
|
|
254
308
|
taskId: poolTaskId,
|
|
255
|
-
});
|
|
309
|
+
}));
|
|
256
310
|
subagentInbox.finish(poolTaskId, dispatchResult);
|
|
257
|
-
return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, poolTaskId, {
|
|
311
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, poolTaskId, {
|
|
258
312
|
taskId: poolTaskId,
|
|
259
313
|
label,
|
|
260
314
|
});
|
|
261
315
|
}
|
|
262
316
|
catch (error) {
|
|
263
317
|
const reason = error instanceof Error ? error.message : String(error);
|
|
264
|
-
|
|
318
|
+
markDispatchFailed(task.id, poolTaskId);
|
|
265
319
|
subagentInbox.fail(poolTaskId, reason);
|
|
266
320
|
// A background dispatch reports failure as a compact notification, not a
|
|
267
321
|
// thrown tool error — the call was already answered by a placeholder.
|
|
@@ -295,15 +349,16 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
295
349
|
},
|
|
296
350
|
// Mirror the cold pool's live progress on the task panel roster so a
|
|
297
351
|
// warm dispatch reads as busy (⋯ grep), not stuck.
|
|
298
|
-
(activity) => taskStore.patchAgent(
|
|
352
|
+
(activity) => taskStore.patchAgent(poolTaskId, { activity }));
|
|
299
353
|
const dispatchResult = warmResultToTaskResult(warmResult, params.subagent_type, task.id);
|
|
300
|
-
return finalizeDispatchResult(dispatchResult, params.subagent_type, task.id, undefined);
|
|
354
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, undefined);
|
|
301
355
|
}
|
|
302
356
|
catch (error) {
|
|
303
357
|
// A genuine infra failure (worker crash/timeout) retries cold; any other
|
|
304
358
|
// error is a real dispatch failure and propagates.
|
|
305
359
|
if (!(error instanceof WarmWorkerError)) {
|
|
306
360
|
taskStore.update(task.id, { status: "failed" });
|
|
361
|
+
taskStore.patchAgent(poolTaskId, { state: "failed", activity: "" });
|
|
307
362
|
throw error;
|
|
308
363
|
}
|
|
309
364
|
console.error(`[WARM] ${params.subagent_type} fell back to cold spawn: ${error.message}`);
|
|
@@ -312,23 +367,27 @@ export function createTaskToolDefinition(cwd = process.cwd()) {
|
|
|
312
367
|
}
|
|
313
368
|
// Foreground: block the turn and return the subagent's full answer inline.
|
|
314
369
|
try {
|
|
315
|
-
const dispatchResult = await pool.dispatch(params.prompt, {
|
|
370
|
+
const dispatchResult = await cancelOnAbort(poolTaskId, () => pool.dispatch(params.prompt, {
|
|
316
371
|
forceAgent: params.subagent_type,
|
|
317
372
|
context: "",
|
|
318
373
|
model: dispatchModel,
|
|
319
374
|
provider: ctx.model?.provider,
|
|
320
375
|
sessionFile: forkSessionFile,
|
|
321
|
-
|
|
322
|
-
|
|
376
|
+
taskId: poolTaskId,
|
|
377
|
+
}));
|
|
378
|
+
return finalizeDispatchResult(dispatchResult, params.subagent_type, poolTaskId, task.id, dispatchResult.task_id);
|
|
323
379
|
}
|
|
324
380
|
catch (error) {
|
|
325
|
-
|
|
381
|
+
markDispatchFailed(task.id, poolTaskId);
|
|
326
382
|
throw error;
|
|
327
383
|
}
|
|
328
384
|
},
|
|
329
385
|
renderCall(args, theme) {
|
|
330
386
|
const type = args.subagent_type ?? "agent";
|
|
331
|
-
|
|
387
|
+
// The [type] tag carries the agent's identity color — the same hue this
|
|
388
|
+
// agent has in the task panel and TaskOutput — so a transcript full of
|
|
389
|
+
// concurrent dispatches is scannable by color.
|
|
390
|
+
const text = theme.fg("toolTitle", theme.bold("Agent ")) + theme.fg(agentColorFor(type), `[${type}]`);
|
|
332
391
|
return new Text(text, 0, 0);
|
|
333
392
|
},
|
|
334
393
|
});
|
|
@@ -350,13 +409,47 @@ export function resolveForkSessionFile(def, parentSessionPath, cwd) {
|
|
|
350
409
|
return undefined;
|
|
351
410
|
}
|
|
352
411
|
}
|
|
412
|
+
/** Pre-allocated pool task id for a dispatch (matches the pool's own format). */
|
|
413
|
+
function newDispatchTaskId() {
|
|
414
|
+
return `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The TodoWrite plan item this dispatch is (most plausibly) working on: the
|
|
418
|
+
* single in_progress main-agent task. TodoWrite discipline keeps exactly one
|
|
419
|
+
* item in_progress, so when that holds the link is unambiguous; with zero or
|
|
420
|
+
* several in_progress items no link is recorded rather than guessing. Used to
|
|
421
|
+
* nest the run under its plan item in the task panel's flat lens.
|
|
422
|
+
*/
|
|
423
|
+
function linkedTodoId() {
|
|
424
|
+
const inProgress = taskStore
|
|
425
|
+
.list()
|
|
426
|
+
.filter((t) => t.source === undefined &&
|
|
427
|
+
t.agent === undefined &&
|
|
428
|
+
t.parentTaskId === undefined &&
|
|
429
|
+
t.status === "in_progress");
|
|
430
|
+
return inProgress.length === 1 ? inProgress[0]?.id : undefined;
|
|
431
|
+
}
|
|
353
432
|
/**
|
|
354
|
-
* Register
|
|
355
|
-
*
|
|
356
|
-
*
|
|
433
|
+
* Register this dispatch in the task store's roster, keyed by its run id (the
|
|
434
|
+
* pool task id) so concurrent runs of the same agent type each get their own
|
|
435
|
+
* row, state, activity, and stats. The friendly label ("explore#1") names the
|
|
436
|
+
* row the same way the inbox names background tasks.
|
|
357
437
|
*/
|
|
358
|
-
function registerSubagentDispatch(
|
|
359
|
-
taskStore.upsertAgent({ id:
|
|
438
|
+
function registerSubagentDispatch(runId, label) {
|
|
439
|
+
taskStore.upsertAgent({ id: runId, name: label, role: "subagent", kind: "subagent", state: "running" });
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Catch-path bookkeeping: mark the dispatch failed unless it already settled.
|
|
443
|
+
* finalizeDispatchResult records a terminal status (failed/cancelled) *before*
|
|
444
|
+
* throwing, so a blanket "failed" here would clobber a cancellation with a
|
|
445
|
+
* failure the moment the thrown error passed through.
|
|
446
|
+
*/
|
|
447
|
+
function markDispatchFailed(taskStoreId, runId) {
|
|
448
|
+
const current = taskStore.list().find((t) => t.id === taskStoreId);
|
|
449
|
+
if (current && current.status !== "pending" && current.status !== "in_progress")
|
|
450
|
+
return;
|
|
451
|
+
taskStore.update(taskStoreId, { status: "failed" });
|
|
452
|
+
taskStore.patchAgent(runId, { state: "failed", activity: "" });
|
|
360
453
|
}
|
|
361
454
|
/** Names of agents configured to run in the background (non-blocking). */
|
|
362
455
|
function collectBackgroundAgentNames(cwd) {
|
|
@@ -426,26 +519,29 @@ function warmResultToTaskResult(warm, agentType, taskStoreId) {
|
|
|
426
519
|
* inbox, so it returns a compact, self-contained notification (success or
|
|
427
520
|
* failure) and never throws — the call was already answered by a placeholder.
|
|
428
521
|
*/
|
|
429
|
-
function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resumeHandle, background) {
|
|
522
|
+
function finalizeDispatchResult(dispatchResult, subagentType, runAgentId, taskStoreId, resumeHandle, background) {
|
|
430
523
|
const result = dispatchResult.result;
|
|
431
524
|
const resultData = result?.result_data;
|
|
432
525
|
const usage = resultData?.usage;
|
|
433
526
|
// Merge the child's own task subtree under the dispatching task so nested
|
|
434
527
|
// delegation (depth >= 2) is visible in the subagents lens's task tree.
|
|
435
528
|
mergeChildTaskTree(resultData?.task_tree, taskStoreId);
|
|
436
|
-
// Roll
|
|
437
|
-
// header carries the agent's own token/cost totals.
|
|
529
|
+
// Roll this run's usage into its own roster row (rows are per-dispatch).
|
|
438
530
|
if (usage) {
|
|
439
|
-
taskStore.addAgentStats(
|
|
531
|
+
taskStore.addAgentStats(runAgentId, { input: usage.input, output: usage.output, cost: usage.cost });
|
|
440
532
|
}
|
|
441
533
|
if (!result || !result.ok) {
|
|
534
|
+
// A user-initiated cancel is not a failure: it gets its own terminal
|
|
535
|
+
// status so the panel shows ⊘ cancelled (dim) instead of ✗ failed (red).
|
|
536
|
+
const cancelled = result?.status === "cancelled";
|
|
442
537
|
const failNote = result?.usedInheritedModelFallback ? "inherited-model retry failed" : undefined;
|
|
443
|
-
taskStore.update(taskStoreId, { status: "failed", usage, note: failNote });
|
|
444
|
-
taskStore.patchAgent(
|
|
538
|
+
taskStore.update(taskStoreId, { status: cancelled ? "cancelled" : "failed", usage, note: failNote });
|
|
539
|
+
taskStore.patchAgent(runAgentId, { state: cancelled ? "cancelled" : "failed", activity: "" });
|
|
445
540
|
const reason = result?.error ?? (result?.status ? `subagent ${result.status}` : "unknown error");
|
|
446
541
|
if (background) {
|
|
542
|
+
const verdict = cancelled ? "cancelled ⊘" : "failed ✗";
|
|
447
543
|
return {
|
|
448
|
-
content: [{ type: "text", text: `${background.label}
|
|
544
|
+
content: [{ type: "text", text: `${background.label} ${verdict} — ${reason}` }],
|
|
449
545
|
details: {
|
|
450
546
|
subagent_type: subagentType,
|
|
451
547
|
ok: false,
|
|
@@ -456,20 +552,20 @@ function finalizeDispatchResult(dispatchResult, subagentType, taskStoreId, resum
|
|
|
456
552
|
},
|
|
457
553
|
};
|
|
458
554
|
}
|
|
555
|
+
if (cancelled) {
|
|
556
|
+
throw new Error(`Subagent (${subagentType}) cancelled by user.`);
|
|
557
|
+
}
|
|
459
558
|
const stderr = result?.stderr?.trim();
|
|
460
559
|
throw new Error(`Subagent (${subagentType}) failed: ${reason}${stderr ? `\nstderr: ${stderr.slice(-500)}` : ""}`);
|
|
461
560
|
}
|
|
462
561
|
// Leave the task in the store with its final status; it stays visible in the
|
|
463
562
|
// 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
|
|
563
|
+
// fell back to the inherited model rather than emitting a chat message — and
|
|
564
|
+
// clear any stale note otherwise (note is clearable via an explicit undefined).
|
|
465
565
|
const fallbackNote = dispatchResult.result?.usedInheritedModelFallback ? "ran on inherited model" : undefined;
|
|
466
566
|
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" });
|
|
567
|
+
// Rows are per-run, so this run settles to done regardless of siblings.
|
|
568
|
+
taskStore.patchAgent(runAgentId, { state: "done", activity: "" });
|
|
473
569
|
let answer = resultData?.summary || "(subagent returned no output)";
|
|
474
570
|
// Partial results are resumable; surface the handle so the parent can continue.
|
|
475
571
|
if (result.status === "partial" && resumeHandle) {
|
|
@@ -504,7 +600,7 @@ const taskOutputParams = Type.Object({
|
|
|
504
600
|
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
601
|
})),
|
|
506
602
|
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.",
|
|
603
|
+
description: "List all background subagents with their status (running/done/failed/cancelled) and current activity. No result bodies are returned.",
|
|
508
604
|
})),
|
|
509
605
|
wait: Type.Optional(Type.Boolean({
|
|
510
606
|
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 +608,13 @@ const taskOutputParams = Type.Object({
|
|
|
512
608
|
timeout_ms: Type.Optional(Type.Number({ description: "Maximum time to block in wait mode, in milliseconds (default 120000)." })),
|
|
513
609
|
});
|
|
514
610
|
const TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 120_000;
|
|
515
|
-
/**
|
|
611
|
+
/**
|
|
612
|
+
* Elapsed time a record has run (so far, or until it settled), in the same
|
|
613
|
+
* format the task panel uses so the two surfaces always agree.
|
|
614
|
+
*/
|
|
516
615
|
function recordElapsed(rec) {
|
|
517
616
|
const end = rec.endedAt ?? Date.now();
|
|
518
|
-
return
|
|
617
|
+
return formatDurationSecs((end - rec.startedAt) / 1000);
|
|
519
618
|
}
|
|
520
619
|
/** A compact roster of every known background subagent — status + activity, no bodies. */
|
|
521
620
|
function formatTaskRoster() {
|
|
@@ -536,6 +635,8 @@ function formatTaskRoster() {
|
|
|
536
635
|
return `- ${r.label} done (uncollected) ${when} — ${r.summaryLine ?? ""}`;
|
|
537
636
|
case "collected":
|
|
538
637
|
return `- ${r.label} collected ${when} — ${r.summaryLine ?? ""}`;
|
|
638
|
+
case "cancelled":
|
|
639
|
+
return `- ${r.label} cancelled ⊘ — ${r.error ?? "cancelled by user"}`;
|
|
539
640
|
default:
|
|
540
641
|
return `- ${r.label} ${r.lifecycle} ✗ — ${r.error ?? "unknown error"}`;
|
|
541
642
|
}
|
|
@@ -629,19 +730,62 @@ export function createTaskOutputToolDefinition() {
|
|
|
629
730
|
details: { task_id: handle, status: "collected", ok: true },
|
|
630
731
|
};
|
|
631
732
|
}
|
|
632
|
-
// failed / stalled / timeout
|
|
733
|
+
// failed / stalled / timeout / cancelled
|
|
734
|
+
const glyph = rec.lifecycle === "cancelled" ? "⊘" : "✗";
|
|
633
735
|
return {
|
|
634
736
|
content: [
|
|
635
|
-
{
|
|
737
|
+
{
|
|
738
|
+
type: "text",
|
|
739
|
+
text: `${rec.label} ${rec.lifecycle} ${glyph} — ${rec.error ?? "unknown error"}.`,
|
|
740
|
+
},
|
|
636
741
|
],
|
|
637
742
|
details: { task_id: handle, status: rec.lifecycle, ok: false },
|
|
638
743
|
};
|
|
639
744
|
},
|
|
640
745
|
renderCall(args, theme) {
|
|
641
746
|
const target = args.list ? "list" : String(args.task_id ?? "");
|
|
642
|
-
|
|
747
|
+
// A friendly label ("explore#1") gets its agent's identity color so the
|
|
748
|
+
// pull visually pairs with the dispatch that produced it; raw ids and
|
|
749
|
+
// "list" stay dim.
|
|
750
|
+
const hashIdx = target.indexOf("#");
|
|
751
|
+
const styledTarget = hashIdx > 0 ? theme.fg(agentColorFor(target.slice(0, hashIdx)), target) : theme.fg("dim", target);
|
|
752
|
+
const text = theme.fg("toolTitle", theme.bold("TaskOutput ")) +
|
|
753
|
+
styledTarget +
|
|
754
|
+
(args.wait ? theme.fg("dim", " (wait)") : "");
|
|
643
755
|
return new Text(text, 0, 0);
|
|
644
756
|
},
|
|
757
|
+
renderResult(result, _options, theme) {
|
|
758
|
+
// Display-only colorization of the roster/status lines (the text sent to
|
|
759
|
+
// the model stays plain). Lines that aren't roster rows — collected
|
|
760
|
+
// bodies, status sentences — render exactly like the default fallback.
|
|
761
|
+
const text = result.content
|
|
762
|
+
.map((c) => (c.type === "text" ? c.text : ""))
|
|
763
|
+
.filter(Boolean)
|
|
764
|
+
.join("\n");
|
|
765
|
+
if (!text)
|
|
766
|
+
return new Text("", 0, 0);
|
|
767
|
+
const rosterLine = /^- (\S+)\s{2}(running|done \(uncollected\)|collected|failed|stalled|timeout|cancelled)(.*)$/;
|
|
768
|
+
const styled = text
|
|
769
|
+
.split("\n")
|
|
770
|
+
.map((line) => {
|
|
771
|
+
const match = rosterLine.exec(line);
|
|
772
|
+
if (!match)
|
|
773
|
+
return theme.fg("toolOutput", line);
|
|
774
|
+
const [, label, status, rest] = match;
|
|
775
|
+
const hashIdx = label.indexOf("#");
|
|
776
|
+
const labelColor = hashIdx > 0 ? agentColorFor(label.slice(0, hashIdx)) : "accent";
|
|
777
|
+
const statusColor = status === "running"
|
|
778
|
+
? "warning"
|
|
779
|
+
: status.startsWith("done")
|
|
780
|
+
? "success"
|
|
781
|
+
: status === "collected" || status === "cancelled"
|
|
782
|
+
? "muted"
|
|
783
|
+
: "error";
|
|
784
|
+
return `- ${theme.fg(labelColor, label)} ${theme.fg(statusColor, status)}${theme.fg("dim", rest)}`;
|
|
785
|
+
})
|
|
786
|
+
.join("\n");
|
|
787
|
+
return new Text(styled, 0, 0);
|
|
788
|
+
},
|
|
645
789
|
});
|
|
646
790
|
}
|
|
647
791
|
//# sourceMappingURL=subagent.js.map
|