sidekick-shared 0.29.1 → 0.30.0
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 +26 -0
- package/README.md +171 -60
- package/dist/aggregation/EventAggregator.d.ts +79 -4
- package/dist/aggregation/EventAggregator.js +391 -37
- package/dist/aggregation/types.d.ts +27 -3
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +23 -3
- package/dist/context/sessionContext.js +5 -1
- package/dist/esm/aggregation/EventAggregator.js +392 -38
- package/dist/esm/browser.js +3 -1
- package/dist/esm/context/sessionContext.js +6 -2
- package/dist/esm/fileContinuity.js +149 -0
- package/dist/esm/index.js +5 -2
- package/dist/esm/node.js +2 -0
- package/dist/esm/parsers/codexParser.js +192 -3
- package/dist/esm/parsers/subagentEndState.js +61 -0
- package/dist/esm/parsers/subagentScanner.js +4 -0
- package/dist/esm/parsers/subagentTraceParser.js +6 -0
- package/dist/esm/promptHistory.js +22 -11
- package/dist/esm/providers/claudeCode.js +50 -27
- package/dist/esm/providers/codex.js +153 -163
- package/dist/esm/providers/codexCompressedReader.js +184 -0
- package/dist/esm/providers/codexDatabase.js +47 -2
- package/dist/esm/providers/codexRollouts.js +20 -2
- package/dist/esm/providers/codexSubagents.js +456 -0
- package/dist/esm/providers/compressedRollouts.js +330 -0
- package/dist/esm/providers/factory.js +4 -1
- package/dist/esm/providers/rolloutWalker.js +32 -4
- package/dist/esm/schemas/sessionEvent.js +32 -4
- package/dist/esm/sessionMonitor.js +7 -1
- package/dist/esm/sessionPreviews.js +9 -0
- package/dist/esm/sessionPromptHistory.js +5 -3
- package/dist/esm/subagentActivity.js +125 -0
- package/dist/esm/subagentDisplayState.js +81 -0
- package/dist/esm/subagentSpawn.js +206 -6
- package/dist/esm/testing/codexSubagentFixtures.js +336 -0
- package/dist/esm/toolCallOrigins.js +17 -10
- package/dist/esm/toolCategory.js +6 -2
- package/dist/esm/transcript.js +6 -2
- package/dist/esm/turns/assistantTurn.js +6 -3
- package/dist/esm/usage/usageEvents.js +4 -1
- package/dist/esm/watchers/eventBridge.js +31 -1
- package/dist/esm/watchers/jsonlTail.js +11 -42
- package/dist/esm/watchers/jsonlWatcher.js +19 -9
- package/dist/esm/watchers/providerReaderWatcher.js +5 -4
- package/dist/fileContinuity.d.ts +95 -0
- package/dist/fileContinuity.js +187 -0
- package/dist/index.d.ts +14 -7
- package/dist/index.js +39 -10
- package/dist/node.d.ts +4 -0
- package/dist/node.js +16 -2
- package/dist/parsers/codexParser.d.ts +44 -0
- package/dist/parsers/codexParser.js +192 -3
- package/dist/parsers/subagentEndState.d.ts +20 -0
- package/dist/parsers/subagentEndState.js +65 -0
- package/dist/parsers/subagentScanner.js +4 -0
- package/dist/parsers/subagentTraceParser.d.ts +8 -0
- package/dist/parsers/subagentTraceParser.js +6 -0
- package/dist/promptHistory.d.ts +13 -3
- package/dist/promptHistory.js +22 -11
- package/dist/providers/claudeCode.js +50 -27
- package/dist/providers/codex.d.ts +35 -5
- package/dist/providers/codex.js +150 -160
- package/dist/providers/codexCompressedReader.d.ts +58 -0
- package/dist/providers/codexCompressedReader.js +221 -0
- package/dist/providers/codexDatabase.d.ts +25 -0
- package/dist/providers/codexDatabase.js +48 -2
- package/dist/providers/codexRollouts.d.ts +16 -2
- package/dist/providers/codexRollouts.js +20 -2
- package/dist/providers/codexSubagents.d.ts +66 -0
- package/dist/providers/codexSubagents.js +495 -0
- package/dist/providers/compressedRollouts.d.ts +114 -0
- package/dist/providers/compressedRollouts.js +380 -0
- package/dist/providers/factory.d.ts +5 -0
- package/dist/providers/factory.js +4 -1
- package/dist/providers/rolloutWalker.d.ts +15 -2
- package/dist/providers/rolloutWalker.js +33 -4
- package/dist/providers/types.d.ts +22 -1
- package/dist/schemas/sessionEvent.d.ts +26 -2
- package/dist/schemas/sessionEvent.js +31 -3
- package/dist/sessionMonitor.d.ts +6 -0
- package/dist/sessionMonitor.js +7 -1
- package/dist/sessionPreviews.js +9 -0
- package/dist/sessionPromptHistory.d.ts +5 -0
- package/dist/sessionPromptHistory.js +5 -36
- package/dist/subagentActivity.d.ts +91 -0
- package/dist/subagentActivity.js +132 -0
- package/dist/subagentDisplayState.d.ts +81 -0
- package/dist/subagentDisplayState.js +86 -0
- package/dist/subagentSpawn.d.ts +100 -5
- package/dist/subagentSpawn.js +218 -7
- package/dist/testing/codexSubagentFixtures.d.ts +48 -0
- package/dist/testing/codexSubagentFixtures.js +344 -0
- package/dist/toolCallOrigins.d.ts +5 -0
- package/dist/toolCallOrigins.js +17 -10
- package/dist/toolCategory.js +6 -2
- package/dist/transcript.js +5 -1
- package/dist/turns/assistantTurn.d.ts +4 -1
- package/dist/turns/assistantTurn.js +5 -2
- package/dist/types/codex.d.ts +71 -2
- package/dist/types/sessionEvent.d.ts +38 -3
- package/dist/usage/usageEvents.d.ts +4 -1
- package/dist/usage/usageEvents.js +4 -1
- package/dist/watchers/eventBridge.js +30 -0
- package/dist/watchers/jsonlTail.js +11 -42
- package/dist/watchers/jsonlWatcher.d.ts +5 -1
- package/dist/watchers/jsonlWatcher.js +18 -8
- package/dist/watchers/providerReaderWatcher.js +5 -4
- package/package.json +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent lifecycle records as `system` session events.
|
|
3
|
+
*
|
|
4
|
+
* Codex records its multi-agent lifecycle in the parent's rollout:
|
|
5
|
+
* `item_completed` → `SubAgentActivity` items link a `spawn_agent` call to the
|
|
6
|
+
* child thread it started (`started`), mark each finished child turn
|
|
7
|
+
* (`completed`, Codex 0.150+), and note `followup_task` / `send_message`
|
|
8
|
+
* calls (`interacted`) and `interrupt_agent` calls (`interrupted`); a child's
|
|
9
|
+
* report arrives as an `agent_message` whose text starts
|
|
10
|
+
* `Message Type: FINAL_ANSWER`. With
|
|
11
|
+
* `CodexRolloutParserOptions.includeSubagentActivity`, the parser turns each
|
|
12
|
+
* into one `system` event with `origin: { kind: 'subagent-activity' }` whose
|
|
13
|
+
* `message.content` is a single `{ type: 'subagent_activity', ... }` block.
|
|
14
|
+
*
|
|
15
|
+
* Those events are bookkeeping, not messages: transcripts, context
|
|
16
|
+
* attribution, and message counts skip them, as they do task notifications.
|
|
17
|
+
*
|
|
18
|
+
* Pure: no filesystem access. Browser-safe.
|
|
19
|
+
*
|
|
20
|
+
* @module subagentActivity
|
|
21
|
+
*/
|
|
22
|
+
/** `origin.kind` of a subagent-activity `system` event. */
|
|
23
|
+
export const SUBAGENT_ACTIVITY_ORIGIN_KIND = 'subagent-activity';
|
|
24
|
+
/** `type` of the content block that carries the activity. */
|
|
25
|
+
export const SUBAGENT_ACTIVITY_BLOCK_TYPE = 'subagent_activity';
|
|
26
|
+
const KINDS = new Set([
|
|
27
|
+
'started',
|
|
28
|
+
'completed',
|
|
29
|
+
'interacted',
|
|
30
|
+
'interrupted',
|
|
31
|
+
'final-answer',
|
|
32
|
+
]);
|
|
33
|
+
const STRING_FIELDS = [
|
|
34
|
+
'callId',
|
|
35
|
+
'tool',
|
|
36
|
+
'turnId',
|
|
37
|
+
'agentThreadId',
|
|
38
|
+
'agentPath',
|
|
39
|
+
'recipient',
|
|
40
|
+
'model',
|
|
41
|
+
'reasoningEffort',
|
|
42
|
+
'parentThreadId',
|
|
43
|
+
'parentTurnId',
|
|
44
|
+
'text',
|
|
45
|
+
];
|
|
46
|
+
/** Whether an event is a subagent-activity `system` event (see the module docs). */
|
|
47
|
+
export function isSubagentActivitySystemEvent(event) {
|
|
48
|
+
return event.type === 'system' && event.origin?.kind === SUBAGENT_ACTIVITY_ORIGIN_KIND;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The activity a subagent-activity `system` event carries, copied with only
|
|
52
|
+
* its known string fields; `null` for any other event or a malformed block.
|
|
53
|
+
*/
|
|
54
|
+
export function subagentActivityFromEvent(event) {
|
|
55
|
+
if (!isSubagentActivitySystemEvent(event))
|
|
56
|
+
return null;
|
|
57
|
+
const content = event.message?.content;
|
|
58
|
+
if (!Array.isArray(content))
|
|
59
|
+
return null;
|
|
60
|
+
for (const block of content) {
|
|
61
|
+
if (!block || typeof block !== 'object')
|
|
62
|
+
continue;
|
|
63
|
+
const raw = block;
|
|
64
|
+
if (raw.type !== SUBAGENT_ACTIVITY_BLOCK_TYPE)
|
|
65
|
+
continue;
|
|
66
|
+
if (typeof raw.kind !== 'string' || !KINDS.has(raw.kind))
|
|
67
|
+
return null;
|
|
68
|
+
const activity = { kind: raw.kind };
|
|
69
|
+
for (const field of STRING_FIELDS) {
|
|
70
|
+
const value = raw[field];
|
|
71
|
+
if (typeof value === 'string' && value.length > 0)
|
|
72
|
+
activity[field] = value;
|
|
73
|
+
}
|
|
74
|
+
return activity;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** A one-line, content-free description, e.g. `Subagent /root/review started`. */
|
|
79
|
+
export function describeSubagentActivity(activity) {
|
|
80
|
+
const who = activity.agentPath ?? activity.agentThreadId ?? 'subagent';
|
|
81
|
+
const name = `Subagent ${who}`;
|
|
82
|
+
switch (activity.kind) {
|
|
83
|
+
case 'started':
|
|
84
|
+
return `${name} started`;
|
|
85
|
+
case 'completed':
|
|
86
|
+
return `${name} finished a turn`;
|
|
87
|
+
case 'interacted':
|
|
88
|
+
return activity.tool === 'send_message'
|
|
89
|
+
? `Message sent to subagent ${who}`
|
|
90
|
+
: `${name} given a follow-up task`;
|
|
91
|
+
case 'interrupted':
|
|
92
|
+
return `${name} interrupted`;
|
|
93
|
+
case 'final-answer':
|
|
94
|
+
return `${name} reported its final answer`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const MESSAGE_HEADER = /^Message Type:[ \t]*([A-Z_]+)[ \t]*(?:\r?\n|$)/;
|
|
98
|
+
/**
|
|
99
|
+
* Parses a Codex inter-agent message text. Returns `null` unless it starts
|
|
100
|
+
* with a `Message Type:` line.
|
|
101
|
+
*/
|
|
102
|
+
export function parseCodexAgentMessageText(text) {
|
|
103
|
+
if (typeof text !== 'string')
|
|
104
|
+
return null;
|
|
105
|
+
const header = MESSAGE_HEADER.exec(text);
|
|
106
|
+
if (!header)
|
|
107
|
+
return null;
|
|
108
|
+
const rest = text.slice(header[0].length);
|
|
109
|
+
const payloadAt = /(?:^|\n)Payload:[ \t]*(?:\r?\n|$)/.exec(rest);
|
|
110
|
+
const head = payloadAt ? rest.slice(0, payloadAt.index) : rest;
|
|
111
|
+
const payload = payloadAt ? rest.slice(payloadAt.index + payloadAt[0].length) : '';
|
|
112
|
+
const field = (name) => {
|
|
113
|
+
const match = new RegExp(`(?:^|\\n)${name}:[ \\t]*([^\\n]*)`).exec(head);
|
|
114
|
+
const value = match?.[1]?.trim();
|
|
115
|
+
return value ? value : undefined;
|
|
116
|
+
};
|
|
117
|
+
const taskName = field('Task name');
|
|
118
|
+
const sender = field('Sender');
|
|
119
|
+
return {
|
|
120
|
+
messageType: header[1],
|
|
121
|
+
...(taskName ? { taskName } : {}),
|
|
122
|
+
...(sender ? { sender } : {}),
|
|
123
|
+
payload,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How to show a subagent: its lifecycle's outcome once something settled
|
|
3
|
+
* it, otherwise `running`, unless its transcript or its parent says it has
|
|
4
|
+
* stopped in all but name.
|
|
5
|
+
*
|
|
6
|
+
* Some background agents never get a final notification (they stopped after
|
|
7
|
+
* an interim "may resume" notice, the user stopped them from the UI, or a
|
|
8
|
+
* restart killed them). Their lifecycles stay `running`. This rule reads
|
|
9
|
+
* them as:
|
|
10
|
+
*
|
|
11
|
+
* - `finished-unreported`: the transcript ended cleanly (last assistant turn
|
|
12
|
+
* `end_turn` / `stop_sequence`, no tool call unanswered) and has been quiet
|
|
13
|
+
* for {@link SUBAGENT_UNREPORTED_QUIET_MS}.
|
|
14
|
+
* - `ended`: neither the parent session nor the transcript has been active for
|
|
15
|
+
* {@link SUBAGENT_PARENT_IDLE_ENDED_MS}.
|
|
16
|
+
*
|
|
17
|
+
* Neither is stored: the state is recomputed from the latest data on every
|
|
18
|
+
* call, so an agent that resumes (its transcript grows, or a later
|
|
19
|
+
* notification reopens its lifecycle) reads as `running` again. Silence alone
|
|
20
|
+
* is never enough for `finished-unreported`: live agents can be quiet for
|
|
21
|
+
* hours.
|
|
22
|
+
*
|
|
23
|
+
* Pure: no filesystem access. Browser-safe.
|
|
24
|
+
*
|
|
25
|
+
* @module subagentDisplayState
|
|
26
|
+
*/
|
|
27
|
+
/** Quiet time after a clean end before a running agent reads as finished without a report (2 min). */
|
|
28
|
+
export const SUBAGENT_UNREPORTED_QUIET_MS = 2 * 60_000;
|
|
29
|
+
/** Parent and transcript idle time after which a running agent reads as ended (6 h). */
|
|
30
|
+
export const SUBAGENT_PARENT_IDLE_ENDED_MS = 6 * 60 * 60_000;
|
|
31
|
+
function toMs(value) {
|
|
32
|
+
if (value === null || value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const ms = value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
35
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves how to show a subagent (see the module docs). Settled lifecycles
|
|
39
|
+
* map to their outcome (`completed` when none is recorded). A running one is
|
|
40
|
+
* `stopped` / `completed` when its meta flags say so, `finished-unreported`
|
|
41
|
+
* after a clean end and {@link SUBAGENT_UNREPORTED_QUIET_MS} of quiet,
|
|
42
|
+
* `ended` when the parent and transcript have both been idle for longer than
|
|
43
|
+
* {@link SUBAGENT_PARENT_IDLE_ENDED_MS}, and `running` otherwise.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveSubagentDisplayState(lifecycle, trace, options) {
|
|
46
|
+
if (lifecycle.status !== 'running') {
|
|
47
|
+
return { state: lifecycle.outcome ?? 'completed', reason: 'settled' };
|
|
48
|
+
}
|
|
49
|
+
if (trace?.stoppedByUser)
|
|
50
|
+
return { state: 'stopped', reason: 'meta' };
|
|
51
|
+
if (trace?.finished)
|
|
52
|
+
return { state: 'completed', reason: 'meta' };
|
|
53
|
+
const now = toMs(options.now) ?? Date.now();
|
|
54
|
+
const lastActivity = toMs(trace?.lastActivityAt);
|
|
55
|
+
if (trace?.endedCleanly &&
|
|
56
|
+
lastActivity !== undefined &&
|
|
57
|
+
now - lastActivity >= SUBAGENT_UNREPORTED_QUIET_MS) {
|
|
58
|
+
return { state: 'finished-unreported', reason: 'clean-end' };
|
|
59
|
+
}
|
|
60
|
+
const parentLast = toMs(options.parentLastActivity);
|
|
61
|
+
if (parentLast !== undefined) {
|
|
62
|
+
const latest = Math.max(parentLast, lastActivity ?? -Infinity);
|
|
63
|
+
if (now - latest > SUBAGENT_PARENT_IDLE_ENDED_MS) {
|
|
64
|
+
return { state: 'ended', reason: 'parent-idle' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { state: 'running' };
|
|
68
|
+
}
|
|
69
|
+
const LABELS = {
|
|
70
|
+
running: 'running',
|
|
71
|
+
'finished-unreported': 'finished (no report)',
|
|
72
|
+
ended: 'ended',
|
|
73
|
+
completed: 'completed',
|
|
74
|
+
failed: 'failed',
|
|
75
|
+
stopped: 'stopped',
|
|
76
|
+
killed: 'killed',
|
|
77
|
+
};
|
|
78
|
+
/** A short label: `running`, `finished (no report)`, `ended`, `completed`, `failed`, `stopped`, `killed`. */
|
|
79
|
+
export function subagentDisplayLabel(state) {
|
|
80
|
+
return LABELS[state] ?? state;
|
|
81
|
+
}
|
|
@@ -1,22 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recognizes
|
|
2
|
+
* Recognizes subagent spawns, background-task notifications, and the other
|
|
3
|
+
* records that settle a background agent.
|
|
3
4
|
*
|
|
4
5
|
* Claude Code's subagent tool is `Agent`; `Task` is its earlier name (and the
|
|
5
6
|
* name OpenCode's `task` tool is normalized to). A background agent's spawning
|
|
6
7
|
* tool call gets its result right away ("Async agent launched successfully.")
|
|
7
8
|
* and its completion arrives later as a `<task-notification>` block, delivered
|
|
8
|
-
* either as a user line or as a mid-turn `queued_command` attachment.
|
|
9
|
+
* either as a user line or as a mid-turn `queued_command` attachment. When no
|
|
10
|
+
* notification comes, a `task_status` attachment, an `agents_killed` system
|
|
11
|
+
* line, or a plain-text "background agents were stopped by the user" notice
|
|
12
|
+
* can still say how it ended.
|
|
13
|
+
*
|
|
14
|
+
* Codex spawns with `spawn_agent` (parsed as `Spawn_agent`); its result
|
|
15
|
+
* (`{"task_name": "/root/x"}`) is always a background launch.
|
|
9
16
|
*
|
|
10
17
|
* Pure: no filesystem access, so it is safe for browser bundles.
|
|
11
18
|
*
|
|
12
19
|
* @module subagentSpawn
|
|
13
20
|
*/
|
|
14
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Claude Code / OpenCode tool names that spawn a subagent: `Agent` (current)
|
|
23
|
+
* and `Task` (earlier name). Codex's `spawn_agent` is not in this list;
|
|
24
|
+
* {@link isSubagentSpawnTool} recognizes it as well.
|
|
25
|
+
*/
|
|
15
26
|
export const SUBAGENT_SPAWN_TOOL_NAMES = Object.freeze(['Agent', 'Task']);
|
|
16
27
|
const SPAWN_TOOLS = new Set(SUBAGENT_SPAWN_TOOL_NAMES);
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
/** Codex's raw spawn tool name, and the name the Codex parser normalizes it to. */
|
|
29
|
+
const CODEX_SPAWN_RAW = 'spawn_agent';
|
|
30
|
+
const CODEX_SPAWN_TOOLS = new Set([CODEX_SPAWN_RAW, 'Spawn_agent']);
|
|
31
|
+
/**
|
|
32
|
+
* Whether a tool call is Codex's `spawn_agent`: by name (`Spawn_agent` as
|
|
33
|
+
* parsed, or the raw `spawn_agent`), or by the raw name the Codex parser keeps
|
|
34
|
+
* in the input (`_sidekickRawToolName`).
|
|
35
|
+
*/
|
|
36
|
+
export function isCodexSubagentSpawnTool(name, input) {
|
|
37
|
+
if (typeof name === 'string' && CODEX_SPAWN_TOOLS.has(name))
|
|
38
|
+
return true;
|
|
39
|
+
return (!!input &&
|
|
40
|
+
typeof input === 'object' &&
|
|
41
|
+
input._sidekickRawToolName === CODEX_SPAWN_RAW);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether a tool call spawns a subagent: one of
|
|
45
|
+
* {@link SUBAGENT_SPAWN_TOOL_NAMES} (`Agent`, `Task`), or Codex's
|
|
46
|
+
* `spawn_agent` (see {@link isCodexSubagentSpawnTool}; pass the call's input
|
|
47
|
+
* to also match on its raw name).
|
|
48
|
+
*/
|
|
49
|
+
export function isSubagentSpawnTool(name, input) {
|
|
50
|
+
if (typeof name === 'string' && SPAWN_TOOLS.has(name))
|
|
51
|
+
return true;
|
|
52
|
+
return isCodexSubagentSpawnTool(name, input);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Recognizes a Codex `spawn_agent` result (`{"task_name": "/root/x"}`, as a
|
|
56
|
+
* JSON string or object). `null` for anything else.
|
|
57
|
+
*/
|
|
58
|
+
export function parseCodexSpawnLaunch(result) {
|
|
59
|
+
let value = result;
|
|
60
|
+
if (typeof value !== 'string') {
|
|
61
|
+
const text = contentText(value);
|
|
62
|
+
value = text || value;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'string') {
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
if (!trimmed.startsWith('{'))
|
|
67
|
+
return null;
|
|
68
|
+
try {
|
|
69
|
+
value = JSON.parse(trimmed);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
76
|
+
return null;
|
|
77
|
+
const taskName = value.task_name;
|
|
78
|
+
if (typeof taskName !== 'string')
|
|
79
|
+
return null;
|
|
80
|
+
return taskName ? { agentPath: taskName } : {};
|
|
20
81
|
}
|
|
21
82
|
const ASYNC_LAUNCH_PREFIX = /^\s*Async agent launched successfully\b/;
|
|
22
83
|
const AGENT_ID_LINE = /(?:^|\n)[ \t]*agentId:[ \t]*([A-Za-z0-9][\w.@-]*)/;
|
|
@@ -161,3 +222,142 @@ export function taskNotificationOutcome(status) {
|
|
|
161
222
|
return 'completed';
|
|
162
223
|
}
|
|
163
224
|
}
|
|
225
|
+
const STOPPED_PLURAL = /^\s*(\d+)\s+background agents? (?:were|was) stopped by the user:\s*([\s\S]*?)\s*$/i;
|
|
226
|
+
const STOPPED_SINGULAR = /^\s*Background agent "([\s\S]*)" was stopped by the user\.?\s*$/i;
|
|
227
|
+
/**
|
|
228
|
+
* Parses Claude Code's plain-text notice that the user stopped background
|
|
229
|
+
* agents: `2 background agents were stopped by the user: "A", "B".` (a
|
|
230
|
+
* `queued_command` attachment's prompt) or `Background agent "A" was stopped
|
|
231
|
+
* by the user.` (a queue-operation line). `null` for any other text.
|
|
232
|
+
*/
|
|
233
|
+
export function parseAgentsStoppedNotice(text) {
|
|
234
|
+
if (typeof text !== 'string')
|
|
235
|
+
return null;
|
|
236
|
+
const singular = STOPPED_SINGULAR.exec(text);
|
|
237
|
+
if (singular)
|
|
238
|
+
return { descriptions: [singular[1].trim()].filter(Boolean) };
|
|
239
|
+
const plural = STOPPED_PLURAL.exec(text);
|
|
240
|
+
if (!plural)
|
|
241
|
+
return null;
|
|
242
|
+
let list = plural[2].trim();
|
|
243
|
+
if (list.endsWith('.'))
|
|
244
|
+
list = list.slice(0, -1).trimEnd();
|
|
245
|
+
if (!list.startsWith('"') || !list.endsWith('"') || list.length < 2)
|
|
246
|
+
return null;
|
|
247
|
+
const descriptions = list
|
|
248
|
+
.slice(1, -1)
|
|
249
|
+
.split(/",\s*"/)
|
|
250
|
+
.map((description) => description.trim())
|
|
251
|
+
.filter(Boolean);
|
|
252
|
+
return descriptions.length > 0 ? { descriptions } : null;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The stop notice a task-notification event carries (a `queued_command`
|
|
256
|
+
* prompt that is plain text, not a `<task-notification>` block). `null`
|
|
257
|
+
* otherwise.
|
|
258
|
+
*/
|
|
259
|
+
export function agentsStoppedNoticeFromEvent(event) {
|
|
260
|
+
if (!isTaskNotificationSystemEvent(event))
|
|
261
|
+
return null;
|
|
262
|
+
return parseAgentsStoppedNotice(contentText(event.message?.content));
|
|
263
|
+
}
|
|
264
|
+
/** Claude Code's `agents_killed` system line: the user killed every running background agent. */
|
|
265
|
+
export const AGENTS_KILLED_SUBTYPE = 'agents_killed';
|
|
266
|
+
/**
|
|
267
|
+
* Whether an event, or a raw Claude Code JSONL line, is an `agents_killed`
|
|
268
|
+
* system line (`{ type: 'system', subtype: 'agents_killed' }`).
|
|
269
|
+
*/
|
|
270
|
+
export function isAgentsKilledSystemEvent(event) {
|
|
271
|
+
if (!event || typeof event !== 'object')
|
|
272
|
+
return false;
|
|
273
|
+
const line = event;
|
|
274
|
+
return line.type === 'system' && line.subtype === AGENTS_KILLED_SUBTYPE;
|
|
275
|
+
}
|
|
276
|
+
/** `origin.kind` of the opt-in `system` event made from a `task_status` attachment. */
|
|
277
|
+
export const TASK_STATUS_ORIGIN_KIND = 'task-status';
|
|
278
|
+
/** `type` of the content block that carries the status. */
|
|
279
|
+
export const TASK_STATUS_BLOCK_TYPE = 'task_status';
|
|
280
|
+
const AGENT_TASK_TYPE = 'local_agent';
|
|
281
|
+
function nonEmpty(value) {
|
|
282
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The background agent's status a raw Claude Code `task_status` attachment
|
|
286
|
+
* line reports (`{ type: 'attachment', attachment: { type: 'task_status',
|
|
287
|
+
* taskId, taskType: 'local_agent', status, description } }`). `null` for any
|
|
288
|
+
* other value, including background shell tasks (`local_bash`).
|
|
289
|
+
*/
|
|
290
|
+
export function taskStatusFromAttachment(raw) {
|
|
291
|
+
if (!raw || typeof raw !== 'object')
|
|
292
|
+
return null;
|
|
293
|
+
const line = raw;
|
|
294
|
+
if (line.type !== 'attachment' || !line.attachment || typeof line.attachment !== 'object') {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const attachment = line.attachment;
|
|
298
|
+
if (attachment.type !== 'task_status' || attachment.taskType !== AGENT_TASK_TYPE)
|
|
299
|
+
return null;
|
|
300
|
+
const taskId = nonEmpty(attachment.taskId);
|
|
301
|
+
const status = nonEmpty(attachment.status);
|
|
302
|
+
if (!taskId || !status)
|
|
303
|
+
return null;
|
|
304
|
+
const description = nonEmpty(attachment.description);
|
|
305
|
+
return { taskId, taskType: AGENT_TASK_TYPE, status, ...(description ? { description } : {}) };
|
|
306
|
+
}
|
|
307
|
+
/** Whether a task status is final (anything but `running` / `pending`). */
|
|
308
|
+
export function isFinalTaskStatus(status) {
|
|
309
|
+
if (!status)
|
|
310
|
+
return false;
|
|
311
|
+
const lower = status.toLowerCase();
|
|
312
|
+
return lower !== 'running' && lower !== 'pending';
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The synthetic `system` event `extractSessionEvents(raw, {
|
|
316
|
+
* includeTaskNotifications: true })` makes from a background agent's
|
|
317
|
+
* `task_status` attachment. It is bookkeeping, not a message or a prompt.
|
|
318
|
+
*/
|
|
319
|
+
export function isTaskStatusSystemEvent(event) {
|
|
320
|
+
return event.type === 'system' && event.origin?.kind === TASK_STATUS_ORIGIN_KIND;
|
|
321
|
+
}
|
|
322
|
+
/** The status an opt-in task-status `system` event carries; `null` for any other event. */
|
|
323
|
+
export function taskStatusFromEvent(event) {
|
|
324
|
+
if (!isTaskStatusSystemEvent(event))
|
|
325
|
+
return null;
|
|
326
|
+
const content = event.message?.content;
|
|
327
|
+
if (!Array.isArray(content))
|
|
328
|
+
return null;
|
|
329
|
+
for (const block of content) {
|
|
330
|
+
if (!block || typeof block !== 'object')
|
|
331
|
+
continue;
|
|
332
|
+
const raw = block;
|
|
333
|
+
if (raw.type !== TASK_STATUS_BLOCK_TYPE)
|
|
334
|
+
continue;
|
|
335
|
+
const taskId = nonEmpty(raw.taskId);
|
|
336
|
+
const status = nonEmpty(raw.status);
|
|
337
|
+
if (!taskId || !status)
|
|
338
|
+
return null;
|
|
339
|
+
const description = nonEmpty(raw.description);
|
|
340
|
+
return {
|
|
341
|
+
taskId,
|
|
342
|
+
taskType: nonEmpty(raw.taskType) ?? AGENT_TASK_TYPE,
|
|
343
|
+
status,
|
|
344
|
+
...(description ? { description } : {}),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* A one-line description of a stop: `Background agents stopped by the user (2)`,
|
|
351
|
+
* or, for an `agents_killed` line (`null`), `Background agents stopped by the user`.
|
|
352
|
+
*/
|
|
353
|
+
export function describeAgentsStopped(notice) {
|
|
354
|
+
const count = notice?.descriptions.length;
|
|
355
|
+
return count
|
|
356
|
+
? `Background agents stopped by the user (${count})`
|
|
357
|
+
: 'Background agents stopped by the user';
|
|
358
|
+
}
|
|
359
|
+
/** A one-line description of a task status, e.g. `Background agent "Explore auth" completed`. */
|
|
360
|
+
export function describeTaskStatus(status) {
|
|
361
|
+
const name = status.description ? `Background agent "${status.description}"` : 'Background agent';
|
|
362
|
+
return `${name} ${status.status}`;
|
|
363
|
+
}
|