@saccolabs/pi-claude-cli 0.4.13 → 0.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/index.ts +9 -1
- package/package.json +1 -1
- package/src/process-manager.ts +10 -0
- package/src/provider.ts +117 -0
- package/src/task-tracker.ts +113 -20
- package/src/types.ts +18 -0
package/README.md
CHANGED
|
@@ -51,6 +51,9 @@ Requires the `claude` binary on your login-shell PATH (`npm install -g @anthropi
|
|
|
51
51
|
- Surfaces sub-agent fan-outs: one marker when a `Task` agent starts and one
|
|
52
52
|
when it reports, plus live per-agent progress on the `claude-subagents`
|
|
53
53
|
status key — so a fan-out is no longer a blank pane
|
|
54
|
+
- Background sub-agents get to finish: a `result` while agents are still
|
|
55
|
+
running ends a cycle, not the turn, so their reports reach the model
|
|
56
|
+
instead of dying with the subprocess
|
|
54
57
|
- Configurable thinking effort across the full ladder (low to max), mapped 1:1 for every model: the level the host asks for is the level the CLI gets
|
|
55
58
|
- Cross-platform subprocess management (Windows, macOS, Linux)
|
|
56
59
|
- Inactivity timeout and process registry for cleanup
|
package/index.ts
CHANGED
|
@@ -80,7 +80,15 @@ function publishTaskProgress(state: TaskTrackerState): void {
|
|
|
80
80
|
if (json === lastSubagentsJson) return;
|
|
81
81
|
lastSubagentsJson = json;
|
|
82
82
|
try {
|
|
83
|
-
|
|
83
|
+
// An empty snapshot means the episode is over: CLEAR the key rather than
|
|
84
|
+
// pushing `{"tasks":[],...}`. A host reads a present status as live
|
|
85
|
+
// state, so an empty one left standing is a strip that still claims
|
|
86
|
+
// agents when there are none.
|
|
87
|
+
setStatus.call(
|
|
88
|
+
uiContext!.ui,
|
|
89
|
+
SUBAGENTS_STATUS_KEY,
|
|
90
|
+
state.tasks.length === 0 ? undefined : json,
|
|
91
|
+
);
|
|
84
92
|
} catch {
|
|
85
93
|
/* never break a turn over a status push */
|
|
86
94
|
}
|
package/package.json
CHANGED
package/src/process-manager.ts
CHANGED
|
@@ -58,6 +58,16 @@ export function spawnClaude(
|
|
|
58
58
|
modelId,
|
|
59
59
|
"--permission-prompt-tool",
|
|
60
60
|
"stdio",
|
|
61
|
+
// `AskUserQuestion` renders a picker in the CLI's own TUI, and `-p` has
|
|
62
|
+
// no TUI. The call therefore cannot be answered by anyone: it returns
|
|
63
|
+
// "The user did not answer the questions." after a full round trip, and
|
|
64
|
+
// the model reads that as a person who declined and falls back to asking
|
|
65
|
+
// in prose. Captured 2026-08-28 on claude-sonnet-5, which then wrote
|
|
66
|
+
// "happy to discuss in plain text instead". Take the tool away so the
|
|
67
|
+
// model asks in prose the first time — the host's UI shows prose, and
|
|
68
|
+
// the user can answer it.
|
|
69
|
+
"--disallowedTools",
|
|
70
|
+
"AskUserQuestion",
|
|
61
71
|
];
|
|
62
72
|
|
|
63
73
|
// Hermetic mode: keep the user's Claude Code environment out of pi turns.
|
package/src/provider.ts
CHANGED
|
@@ -62,6 +62,49 @@ const INACTIVITY_TIMEOUT_MS =
|
|
|
62
62
|
? Number(process.env.PI_CLAUDE_CLI_TIMEOUT_MS)
|
|
63
63
|
: 300_000;
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* BACKGROUND SUB-AGENTS REPORT BACK, AND THIS IS WHY THEY DIDN'T.
|
|
67
|
+
*
|
|
68
|
+
* The CLI answers a background `Agent` call in milliseconds with "Async agent
|
|
69
|
+
* launched successfully… you will be notified when it completes", and the
|
|
70
|
+
* model — correctly, per its own tool contract — ends its turn to wait. The
|
|
71
|
+
* CLI then emits `result` for that turn WHILE the agents keep running.
|
|
72
|
+
* Killing on that first result is what made every fan-out a dead end: the
|
|
73
|
+
* agents died mid-tool-call, their reports were never written, and the turn
|
|
74
|
+
* closed on "Now waiting on the three investigation agents".
|
|
75
|
+
*
|
|
76
|
+
* The loop the host was assumed to owe the CLI turned out to be the CLI's
|
|
77
|
+
* own. Captured 2026-08-28 on claude 2.1.231, holding the process open past
|
|
78
|
+
* `result`: the sub-agent finished at +25s, `task_notification` carried its
|
|
79
|
+
* report, the CLI re-invoked the model unprompted, and it answered with the
|
|
80
|
+
* findings and emitted a SECOND `result`. Nothing was written to stdin to
|
|
81
|
+
* make that happen.
|
|
82
|
+
*
|
|
83
|
+
* So the fix is to stop killing: while `pendingAgents()` is non-zero, a
|
|
84
|
+
* `result` is a cycle boundary, not the end of the episode. Two bounds keep a
|
|
85
|
+
* runaway fan-out from holding a turn open forever — the inactivity timer
|
|
86
|
+
* (which `task_progress` keeps resetting for as long as agents do real work)
|
|
87
|
+
* and the wall clock below.
|
|
88
|
+
*
|
|
89
|
+
* `applyResult` is safe to call per cycle: the CLI's `modelUsage` is
|
|
90
|
+
* cumulative for the session, verified across a two-result episode
|
|
91
|
+
* (cache-read 68,718 → 161,295), so the last one wins rather than summing.
|
|
92
|
+
*/
|
|
93
|
+
const WAIT_FOR_AGENTS = process.env.PI_CLAUDE_CLI_NO_AGENT_WAIT !== "1";
|
|
94
|
+
|
|
95
|
+
/** Hard ceiling on holding a turn open for sub-agents. */
|
|
96
|
+
const AGENT_WAIT_TIMEOUT_MS =
|
|
97
|
+
Number(process.env.PI_CLAUDE_CLI_AGENT_WAIT_MS) > 0
|
|
98
|
+
? Number(process.env.PI_CLAUDE_CLI_AGENT_WAIT_MS)
|
|
99
|
+
: 900_000;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Backstop on continuation cycles. Each completing agent costs one, and an
|
|
103
|
+
* agent may launch more; this only exists so a pathological loop cannot spin
|
|
104
|
+
* without end.
|
|
105
|
+
*/
|
|
106
|
+
const MAX_AGENT_CONTINUATIONS = 32;
|
|
107
|
+
|
|
65
108
|
/** Extended stream options: pi's SimpleStreamOptions plus optional cwd and mcpConfigPath */
|
|
66
109
|
type StreamViaCLiOptions = SimpleStreamOptions & {
|
|
67
110
|
cwd?: string;
|
|
@@ -240,9 +283,41 @@ export function streamViaCli(
|
|
|
240
283
|
// Inactivity timeout: kill subprocess if no stdout for INACTIVITY_TIMEOUT_MS
|
|
241
284
|
let inactivityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
242
285
|
|
|
286
|
+
// Sub-agent wait state (see WAIT_FOR_AGENTS): how many `result`
|
|
287
|
+
// envelopes have been treated as cycle boundaries, and the wall-clock
|
|
288
|
+
// backstop that stops waiting no matter what the agents are doing.
|
|
289
|
+
let agentContinuations = 0;
|
|
290
|
+
let agentWaitTimer: ReturnType<typeof setTimeout> | undefined;
|
|
291
|
+
let agentWaitExpired = false;
|
|
292
|
+
let waitingForAgents = false;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* End the episode on what it already has: kill the CLI and close the
|
|
296
|
+
* reader without pushing an error.
|
|
297
|
+
*
|
|
298
|
+
* Only correct once a `result` has been seen. Before that, silence means
|
|
299
|
+
* a wedged CLI and the turn has nothing — which is what the inactivity
|
|
300
|
+
* timeout's error is for.
|
|
301
|
+
*/
|
|
302
|
+
function endTurnOnPartialWork() {
|
|
303
|
+
agentWaitExpired = true;
|
|
304
|
+
clearTimeout(inactivityTimer);
|
|
305
|
+
clearTimeout(agentWaitTimer);
|
|
306
|
+
cleanupProcess(proc!);
|
|
307
|
+
rl.close();
|
|
308
|
+
}
|
|
309
|
+
|
|
243
310
|
function resetInactivityTimer() {
|
|
244
311
|
if (inactivityTimer !== undefined) clearTimeout(inactivityTimer);
|
|
245
312
|
inactivityTimer = setTimeout(() => {
|
|
313
|
+
// Waiting on sub-agents is the one state where silence is not a
|
|
314
|
+
// failed turn: the model already spoke and the result already
|
|
315
|
+
// landed. An agent that dies without notifying must not convert
|
|
316
|
+
// that into an error and throw the content away.
|
|
317
|
+
if (waitingForAgents) {
|
|
318
|
+
endTurnOnPartialWork();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
246
321
|
forceKillProcess(proc!);
|
|
247
322
|
endStreamWithError(
|
|
248
323
|
`Claude CLI subprocess timed out: no output for ${INACTIVITY_TIMEOUT_MS / 1000} seconds`,
|
|
@@ -289,6 +364,7 @@ export function streamViaCli(
|
|
|
289
364
|
// Handle subprocess close -- surface crashes with stderr and exit code
|
|
290
365
|
proc.on("close", (code: number | null, _signal: string | null) => {
|
|
291
366
|
clearTimeout(inactivityTimer);
|
|
367
|
+
clearTimeout(agentWaitTimer);
|
|
292
368
|
if (broken) return; // resume-miss retry owns the stream
|
|
293
369
|
if (code !== 0 && code !== null) {
|
|
294
370
|
const stderr = getStderr();
|
|
@@ -424,8 +500,39 @@ export function streamViaCli(
|
|
|
424
500
|
// handoff toolCall) is already accumulated; usage still applies.
|
|
425
501
|
bridge.applyResult(r);
|
|
426
502
|
}
|
|
503
|
+
|
|
504
|
+
// Sub-agents still working: this result ends a CYCLE, not the
|
|
505
|
+
// episode. Leave the CLI alive and keep reading — it re-invokes the
|
|
506
|
+
// model itself once they report, and emits another result. See
|
|
507
|
+
// WAIT_FOR_AGENTS for the capture this is built on.
|
|
508
|
+
if (
|
|
509
|
+
WAIT_FOR_AGENTS &&
|
|
510
|
+
!isError &&
|
|
511
|
+
!selfInterrupted &&
|
|
512
|
+
!aborted &&
|
|
513
|
+
!agentWaitExpired &&
|
|
514
|
+
agentContinuations < MAX_AGENT_CONTINUATIONS &&
|
|
515
|
+
taskTracker.pendingAgents() > 0
|
|
516
|
+
) {
|
|
517
|
+
agentContinuations++;
|
|
518
|
+
waitingForAgents = true;
|
|
519
|
+
if (agentWaitTimer === undefined) {
|
|
520
|
+
agentWaitTimer = setTimeout(() => {
|
|
521
|
+
// Give up waiting, but let the turn end on its own content:
|
|
522
|
+
// the launch markers and whatever the model already said are
|
|
523
|
+
// real, and an error here would throw them away.
|
|
524
|
+
endTurnOnPartialWork();
|
|
525
|
+
}, AGENT_WAIT_TIMEOUT_MS);
|
|
526
|
+
// A pending wait must never hold the host process open.
|
|
527
|
+
agentWaitTimer.unref?.();
|
|
528
|
+
}
|
|
529
|
+
resetInactivityTimer();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
427
533
|
// For success, handoff and error alike: clean up the subprocess
|
|
428
534
|
clearTimeout(inactivityTimer);
|
|
535
|
+
clearTimeout(agentWaitTimer);
|
|
429
536
|
cleanupProcess(proc!);
|
|
430
537
|
rl.close();
|
|
431
538
|
}
|
|
@@ -495,6 +602,16 @@ export function streamViaCli(
|
|
|
495
602
|
stream.end();
|
|
496
603
|
} finally {
|
|
497
604
|
cleanupSystemPromptFile();
|
|
605
|
+
// The sub-agent channel is state ABOUT a turn, so it must not outlive
|
|
606
|
+
// one. Left standing, the last snapshot pins whatever the agents were
|
|
607
|
+
// doing when the episode ended — a host then shows "running" for
|
|
608
|
+
// agents that finished, or for agents that died, until the next turn
|
|
609
|
+
// happens to publish something else.
|
|
610
|
+
try {
|
|
611
|
+
options?.onTaskProgress?.({ tasks: [], active: 0, completed: 0 });
|
|
612
|
+
} catch {
|
|
613
|
+
/* a status push must never break a turn */
|
|
614
|
+
}
|
|
498
615
|
}
|
|
499
616
|
})();
|
|
500
617
|
|
package/src/task-tracker.ts
CHANGED
|
@@ -30,6 +30,23 @@
|
|
|
30
30
|
* What this deliberately does NOT do: build a tree. No task envelope names its
|
|
31
31
|
* parent task, so a nested agent is indistinguishable from a top-level one
|
|
32
32
|
* here. The list is flat, and honestly so.
|
|
33
|
+
*
|
|
34
|
+
* TWO THINGS THIS CHANNEL IS NOT. Both were shipped as sub-agents once, and
|
|
35
|
+
* both put rows in pidex that named work no agent ever did:
|
|
36
|
+
*
|
|
37
|
+
* - **Not every task is an agent.** `task_started` carries `task_type`, and
|
|
38
|
+
* the CLI auto-backgrounds a slow `Bash` into a `local_bash` task with the
|
|
39
|
+
* tool's own `description`. Captured 2026-08-28 on claude 2.1.231: a
|
|
40
|
+
* sub-agent's internal `find` surfaced as `[Claude Code · Task
|
|
41
|
+
* {"status":"started","description":"Search for local source checkout of
|
|
42
|
+
* pi-claude-cli"}]` — a fourth "agent" in a three-agent fan-out. Only
|
|
43
|
+
* `local_agent` and `remote_agent` are sub-agents.
|
|
44
|
+
* - **Not every notification belongs to this episode.** `task_notification`
|
|
45
|
+
* carries no `description` and no `task_type` — only `task_id`. A task
|
|
46
|
+
* started in an EARLIER turn notifies in a later one, and a tracker that
|
|
47
|
+
* invents a placeholder from the id emits `{"status":"stopped",
|
|
48
|
+
* "description":"a8de7d982d824b56a"}`. An id this tracker never saw start
|
|
49
|
+
* is not this episode's business, so it is dropped.
|
|
33
50
|
*/
|
|
34
51
|
|
|
35
52
|
import type {
|
|
@@ -38,8 +55,48 @@ import type {
|
|
|
38
55
|
TaskTrackerState,
|
|
39
56
|
} from "./types.js";
|
|
40
57
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Marker argument previews are truncated to keep one row on one line.
|
|
60
|
+
*
|
|
61
|
+
* Raised from 120 when `task_id` joined the payload: at 120 the id pushed
|
|
62
|
+
* `subagent_type` off the end, so a fan-out stopped saying WHICH kind of
|
|
63
|
+
* agent it launched — the one fact the row exists to carry.
|
|
64
|
+
*/
|
|
65
|
+
export const ARGS_PREVIEW_LIMIT = 200;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Per-field cap on the description, applied BEFORE the whole-payload cap.
|
|
69
|
+
*
|
|
70
|
+
* Ordering fields human-first is not enough on its own: one long description
|
|
71
|
+
* eats the entire preview and takes `subagent_type` and `task_id` with it,
|
|
72
|
+
* which is how a 300-character task name could cost a host the identity it
|
|
73
|
+
* needs to collapse the row. Clip the one unbounded field instead, so the
|
|
74
|
+
* small structural ones always survive.
|
|
75
|
+
*/
|
|
76
|
+
export const DESCRIPTION_PREVIEW_LIMIT = 120;
|
|
77
|
+
|
|
78
|
+
function clipDescription(value: string): string {
|
|
79
|
+
return value.length > DESCRIPTION_PREVIEW_LIMIT
|
|
80
|
+
? `${value.slice(0, DESCRIPTION_PREVIEW_LIMIT)}…`
|
|
81
|
+
: value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `task_type` values that mean "a sub-agent". Everything else the CLI tracks
|
|
86
|
+
* as a task — `local_bash`, `local_shell`, `local_workflow`, `main_session` —
|
|
87
|
+
* is plumbing this channel must not report as an agent.
|
|
88
|
+
*
|
|
89
|
+
* An event with NO `task_type` is treated as an agent: older CLIs omit the
|
|
90
|
+
* field, and going silent on them would be a worse regression than the stray
|
|
91
|
+
* row this filter exists to remove.
|
|
92
|
+
*/
|
|
93
|
+
const AGENT_TASK_TYPES = new Set(["local_agent", "remote_agent"]);
|
|
94
|
+
|
|
95
|
+
export function isAgentTaskType(taskType: unknown): boolean {
|
|
96
|
+
return taskType === undefined || taskType === null
|
|
97
|
+
? true
|
|
98
|
+
: typeof taskType === "string" && AGENT_TASK_TYPES.has(taskType);
|
|
99
|
+
}
|
|
43
100
|
|
|
44
101
|
/**
|
|
45
102
|
* Build a `[Claude Code · Task …]` marker.
|
|
@@ -73,24 +130,34 @@ export interface TaskTracker {
|
|
|
73
130
|
apply(event: ClaudeTaskEvent): string | undefined;
|
|
74
131
|
/** Current state of every sub-agent seen this episode. */
|
|
75
132
|
snapshot(): TaskTrackerState;
|
|
133
|
+
/**
|
|
134
|
+
* Sub-agents that started this episode and have not reported yet.
|
|
135
|
+
*
|
|
136
|
+
* The provider waits on this before it tears the CLI down: the CLI emits
|
|
137
|
+
* its turn `result` the moment the model stops talking, which for a
|
|
138
|
+
* background fan-out is long before the agents finish.
|
|
139
|
+
*/
|
|
140
|
+
pendingAgents(): number;
|
|
76
141
|
}
|
|
77
142
|
|
|
78
143
|
export function createTaskTracker(): TaskTracker {
|
|
79
144
|
/** Insertion-ordered, so the snapshot reads in launch order. */
|
|
80
145
|
const tasks = new Map<string, TaskSnapshot>();
|
|
81
146
|
|
|
82
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Patch a task this tracker already knows. Returns undefined for an id it
|
|
149
|
+
* never saw start — a task from an earlier episode, or one filtered out as
|
|
150
|
+
* not-an-agent. Inventing a placeholder here is what named rows after raw
|
|
151
|
+
* task ids; see the header.
|
|
152
|
+
*/
|
|
153
|
+
function patch(
|
|
154
|
+
id: string,
|
|
155
|
+
changes: Partial<TaskSnapshot>,
|
|
156
|
+
): TaskSnapshot | undefined {
|
|
83
157
|
const existing = tasks.get(id);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// reorders; the id is a truthful placeholder until the start names it.
|
|
88
|
-
description: id,
|
|
89
|
-
status: "running",
|
|
90
|
-
};
|
|
91
|
-
Object.assign(next, patch);
|
|
92
|
-
tasks.set(id, next);
|
|
93
|
-
return next;
|
|
158
|
+
if (!existing) return undefined;
|
|
159
|
+
Object.assign(existing, changes);
|
|
160
|
+
return existing;
|
|
94
161
|
}
|
|
95
162
|
|
|
96
163
|
return {
|
|
@@ -100,20 +167,32 @@ export function createTaskTracker(): TaskTracker {
|
|
|
100
167
|
|
|
101
168
|
switch (event.subtype) {
|
|
102
169
|
case "task_started": {
|
|
103
|
-
|
|
170
|
+
// Plumbing, not a sub-agent: an auto-backgrounded Bash arrives here
|
|
171
|
+
// wearing the tool's own description. See the header.
|
|
172
|
+
if (!isAgentTaskType(event.task_type)) return undefined;
|
|
173
|
+
const task: TaskSnapshot = {
|
|
174
|
+
taskId: id,
|
|
104
175
|
description: event.description ?? id,
|
|
105
176
|
subagentType: event.subagent_type,
|
|
177
|
+
taskType: event.task_type,
|
|
178
|
+
toolUseId: event.tool_use_id,
|
|
106
179
|
status: "running",
|
|
107
|
-
}
|
|
180
|
+
};
|
|
181
|
+
tasks.set(id, task);
|
|
182
|
+
// `skip_transcript` suppresses the ROW, never the tracking: a
|
|
183
|
+
// hidden sub-agent still holds the turn open, and the provider
|
|
184
|
+
// reads `pendingAgents()` to decide when the turn may end.
|
|
185
|
+
if (event.skip_transcript === true) return undefined;
|
|
108
186
|
return taskMarker({
|
|
109
187
|
status: "started",
|
|
110
|
-
description: task.description,
|
|
188
|
+
description: clipDescription(task.description),
|
|
111
189
|
subagent_type: task.subagentType,
|
|
190
|
+
task_id: task.taskId,
|
|
112
191
|
});
|
|
113
192
|
}
|
|
114
193
|
|
|
115
194
|
case "task_progress": {
|
|
116
|
-
|
|
195
|
+
patch(id, {
|
|
117
196
|
// `description` on a progress event is the CURRENT step ("Running
|
|
118
197
|
// …"), not the task's own description. Keep them apart: the task
|
|
119
198
|
// name was set at start and must not be overwritten by a step.
|
|
@@ -129,27 +208,33 @@ export function createTaskTracker(): TaskTracker {
|
|
|
129
208
|
|
|
130
209
|
case "task_updated": {
|
|
131
210
|
const status = event.patch?.status;
|
|
132
|
-
|
|
211
|
+
if (status) patch(id, { status });
|
|
133
212
|
return undefined;
|
|
134
213
|
}
|
|
135
214
|
|
|
136
215
|
case "task_notification": {
|
|
137
|
-
const task =
|
|
216
|
+
const task = patch(id, {
|
|
138
217
|
status: event.status ?? "completed",
|
|
139
218
|
outputFile: event.output_file,
|
|
219
|
+
summary: event.summary,
|
|
140
220
|
toolUses: event.usage?.tool_uses ?? tasks.get(id)?.toolUses,
|
|
141
221
|
totalTokens:
|
|
142
222
|
event.usage?.total_tokens ?? tasks.get(id)?.totalTokens,
|
|
143
223
|
durationMs: event.usage?.duration_ms ?? tasks.get(id)?.durationMs,
|
|
144
224
|
currentStep: undefined,
|
|
145
225
|
});
|
|
226
|
+
// Not ours: a task from an earlier episode, or one filtered out at
|
|
227
|
+
// start. A notification carries no description, so the only row
|
|
228
|
+
// this could produce would be named after a raw task id.
|
|
229
|
+
if (!task) return undefined;
|
|
146
230
|
// The sub-agent's full report reaches the model as the Task tool's
|
|
147
231
|
// own result. Repeating it here would duplicate kilobytes into the
|
|
148
232
|
// transcript, so the marker carries the shape of the work, not its
|
|
149
233
|
// output.
|
|
150
234
|
return taskMarker({
|
|
151
235
|
status: task.status,
|
|
152
|
-
description: task.description,
|
|
236
|
+
description: clipDescription(task.description),
|
|
237
|
+
task_id: task.taskId,
|
|
153
238
|
tool_uses: task.toolUses,
|
|
154
239
|
total_tokens: task.totalTokens,
|
|
155
240
|
duration_ms: task.durationMs,
|
|
@@ -169,6 +254,14 @@ export function createTaskTracker(): TaskTracker {
|
|
|
169
254
|
completed: list.filter((t) => t.status !== "running").length,
|
|
170
255
|
};
|
|
171
256
|
},
|
|
257
|
+
|
|
258
|
+
pendingAgents(): number {
|
|
259
|
+
let pending = 0;
|
|
260
|
+
for (const task of tasks.values()) {
|
|
261
|
+
if (task.status === "running") pending++;
|
|
262
|
+
}
|
|
263
|
+
return pending;
|
|
264
|
+
},
|
|
172
265
|
};
|
|
173
266
|
}
|
|
174
267
|
|
package/src/types.ts
CHANGED
|
@@ -139,6 +139,14 @@ export interface ClaudeTaskEvent {
|
|
|
139
139
|
/** Partial state change on `task_updated`. */
|
|
140
140
|
patch?: { status?: string; end_time?: number };
|
|
141
141
|
last_tool_name?: string;
|
|
142
|
+
/**
|
|
143
|
+
* The CLI's own "do not put this in a transcript" hint. Set on tasks it
|
|
144
|
+
* considers plumbing; honoured for markers, not for tracking, because a
|
|
145
|
+
* hidden sub-agent still holds the turn open.
|
|
146
|
+
*/
|
|
147
|
+
skip_transcript?: boolean;
|
|
148
|
+
/** The sub-agent's report, on `task_notification`. Live state, not content. */
|
|
149
|
+
summary?: string;
|
|
142
150
|
usage?: {
|
|
143
151
|
total_tokens?: number;
|
|
144
152
|
tool_uses?: number;
|
|
@@ -152,6 +160,14 @@ export interface TaskSnapshot {
|
|
|
152
160
|
/** Names the task. Set at `task_started`, never overwritten by a step. */
|
|
153
161
|
description: string;
|
|
154
162
|
subagentType?: string;
|
|
163
|
+
/**
|
|
164
|
+
* The CLI's task kind — `local_agent` / `remote_agent` for a sub-agent.
|
|
165
|
+
* Undefined on a CLI too old to send it, which is read as "agent" so the
|
|
166
|
+
* tracker keeps working rather than going silent.
|
|
167
|
+
*/
|
|
168
|
+
taskType?: string;
|
|
169
|
+
/** The `Agent` tool call that launched this task; the join key a host needs. */
|
|
170
|
+
toolUseId?: string;
|
|
155
171
|
status: string;
|
|
156
172
|
/** The step running right now, cleared when the task ends. */
|
|
157
173
|
currentStep?: string;
|
|
@@ -160,6 +176,8 @@ export interface TaskSnapshot {
|
|
|
160
176
|
totalTokens?: number;
|
|
161
177
|
durationMs?: number;
|
|
162
178
|
outputFile?: string;
|
|
179
|
+
/** The sub-agent's own report, once it finishes. */
|
|
180
|
+
summary?: string;
|
|
163
181
|
}
|
|
164
182
|
|
|
165
183
|
/** Every sub-agent seen this episode, in launch order. */
|