@agentclientprotocol/claude-agent-acp 0.94.1-preview.1 → 0.94.1-preview.10
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 +21 -9
- package/dist/acp-agent.d.ts +39 -3
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +145 -66
- package/dist/acp-subagents.d.ts +2 -1
- package/dist/acp-subagents.d.ts.map +1 -1
- package/dist/air-extension.d.ts +5 -0
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +5 -0
- package/dist/clear-context-coordinator.d.ts +1 -0
- package/dist/clear-context-coordinator.d.ts.map +1 -1
- package/dist/clear-context-coordinator.js +1 -0
- package/dist/index.js +2 -3
- package/dist/native-subagents.d.ts +64 -0
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +224 -9
- package/dist/permissions/options/shared.d.ts +3 -0
- package/dist/permissions/options/shared.d.ts.map +1 -1
- package/dist/permissions/options/tools.js +1 -1
- package/dist/serve.d.ts +19 -11
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +26 -18
- package/dist/session-index/activity.d.ts +4 -0
- package/dist/session-index/activity.d.ts.map +1 -1
- package/dist/session-index/activity.js +16 -0
- package/dist/session-index/background-tasks.d.ts +69 -0
- package/dist/session-index/background-tasks.d.ts.map +1 -0
- package/dist/session-index/background-tasks.js +173 -0
- package/dist/session-index/service.d.ts +2 -0
- package/dist/session-index/service.d.ts.map +1 -1
- package/dist/session-index/service.js +6 -1
- package/dist/session-index/session-index.d.ts +14 -0
- package/dist/session-index/session-index.d.ts.map +1 -1
- package/dist/session-index/session-index.js +25 -2
- package/dist/session-index/transcript-scan.d.ts +8 -1
- package/dist/session-index/transcript-scan.d.ts.map +1 -1
- package/dist/session-index/transcript-scan.js +22 -1
- package/dist/session-titles.d.ts +44 -16
- package/dist/session-titles.d.ts.map +1 -1
- package/dist/session-titles.js +89 -27
- package/dist/subagent-replay.d.ts +21 -1
- package/dist/subagent-replay.d.ts.map +1 -1
- package/dist/subagent-replay.js +51 -3
- package/dist/task-notifications.d.ts +30 -0
- package/dist/task-notifications.d.ts.map +1 -0
- package/dist/task-notifications.js +62 -0
- package/package.json +1 -1
package/dist/session-titles.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI-generated session titles.
|
|
3
3
|
*
|
|
4
|
-
* Claude Code
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Claude Code titles a new SDK session itself: when it reads the first prompt,
|
|
5
|
+
* it asks a small model for a title and stores it in the transcript as an
|
|
6
|
+
* `ai-title` entry, a few seconds in. It sends no message when it does. This
|
|
7
|
+
* module asks for the title with the `generate_session_title` control request,
|
|
8
|
+
* which returns the stored title or waits for the one being generated, and
|
|
9
|
+
* publishes it over ACP as a `session_info_update`. Where the CLI does not
|
|
10
|
+
* title the session itself (a resumed session, or one with
|
|
11
|
+
* `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or
|
|
12
|
+
* `CLAUDE_CODE_DISABLE_TERMINAL_TITLE` set), the same request generates it.
|
|
9
13
|
*
|
|
10
14
|
* {@link SessionTitles} holds the per-session state and is owned by `Session`.
|
|
11
|
-
* `acp-agent.ts` drives it from
|
|
15
|
+
* `acp-agent.ts` drives it from five places: {@link SessionTitles.onPrompt} and
|
|
12
16
|
* {@link SessionTitles.onAssistantText} to collect text,
|
|
17
|
+
* {@link SessionTitles.onPromptQueued} once a prompt is sent to the CLI,
|
|
13
18
|
* {@link SessionTitles.onTurnEnd} at `session_state_changed: idle`, and
|
|
14
19
|
* {@link SessionTitles.reset} on `conversation_reset`.
|
|
15
20
|
*/
|
|
@@ -53,6 +58,10 @@ export function appendTitleContext(previous, text) {
|
|
|
53
58
|
const next = (previous ?? "") + text;
|
|
54
59
|
return next.length > MAX_TITLE_CONTEXT_LENGTH ? next.slice(-MAX_TITLE_CONTEXT_LENGTH) : next;
|
|
55
60
|
}
|
|
61
|
+
/** The text chunks of a prompt, one per line. */
|
|
62
|
+
function promptText(prompt) {
|
|
63
|
+
return prompt.flatMap((chunk) => (chunk.type === "text" ? [chunk.text] : [])).join("\n");
|
|
64
|
+
}
|
|
56
65
|
/** Whether this SDK still offers on-demand title generation. */
|
|
57
66
|
function supportsTitleGeneration(query) {
|
|
58
67
|
return typeof query.generateSessionTitle === "function";
|
|
@@ -74,6 +83,10 @@ export class SessionTitles {
|
|
|
74
83
|
* the same field as a user `/rename`, so re-titling could silently overwrite
|
|
75
84
|
* one. Released by {@link reset}, and when generation yields nothing. */
|
|
76
85
|
settled = false;
|
|
86
|
+
/** Whether {@link onPromptQueued} asked for a title from the latest prompt.
|
|
87
|
+
* A turn end then does not ask again: like the CLI, a prompt that yields
|
|
88
|
+
* no title gets one try, and the next prompt tries again. */
|
|
89
|
+
askedAtPrompt = false;
|
|
77
90
|
/** The title a client set, which no generated title replaces. */
|
|
78
91
|
explicit = new ExplicitTitle();
|
|
79
92
|
/** The title last stored through {@link setExplicitTitle}, as stored
|
|
@@ -100,10 +113,32 @@ export class SessionTitles {
|
|
|
100
113
|
if (this.settled) {
|
|
101
114
|
return;
|
|
102
115
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
this.context = appendTitleContext(this.context, `\n${promptText(prompt)}\n`);
|
|
117
|
+
}
|
|
118
|
+
/** Ask for the title as soon as a prompt is sent to the CLI, instead of
|
|
119
|
+
* waiting for `idle`: `idle` comes only after the whole turn and its
|
|
120
|
+
* background tasks, often minutes after the CLI stored its title. The CLI
|
|
121
|
+
* answers `generate_session_title` while the turn runs, and with the title
|
|
122
|
+
* it stored or is generating, so this makes no second model call. The
|
|
123
|
+
* prompt alone is the description, because the CLI titles from the prompt
|
|
124
|
+
* alone. When this yields no title, the stored summary is published and
|
|
125
|
+
* the next prompt tries again. */
|
|
126
|
+
onPromptQueued(session, prompt) {
|
|
127
|
+
this.askedAtPrompt = false;
|
|
128
|
+
// `session.cancelled` is not checked: after a cancelled turn it stays set
|
|
129
|
+
// until the next turn starts, which is after this prompt is queued.
|
|
130
|
+
if (this.settled || this.explicit.active || session.queryClosed) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!supportsTitleGeneration(session.query)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const description = promptText(prompt).trim();
|
|
137
|
+
if (description.length < MIN_TITLE_CONTEXT_LENGTH) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.askedAtPrompt = true;
|
|
141
|
+
this.startGeneration(() => this.titleFromPrompt(session, description));
|
|
107
142
|
}
|
|
108
143
|
/** Collect the assistant's own answer for the title. Chunks are appended
|
|
109
144
|
* verbatim so streamed text reassembles. No-op once the title is settled. */
|
|
@@ -117,6 +152,7 @@ export class SessionTitles {
|
|
|
117
152
|
* `conversation_reset`, which mounts a fresh transcript. */
|
|
118
153
|
reset() {
|
|
119
154
|
this.settled = false;
|
|
155
|
+
this.askedAtPrompt = false;
|
|
120
156
|
this.explicit.reset();
|
|
121
157
|
this.context = undefined;
|
|
122
158
|
this.lastTitle = undefined;
|
|
@@ -129,10 +165,12 @@ export class SessionTitles {
|
|
|
129
165
|
* title — a user `/rename` or one generated earlier; the SDK folds both into
|
|
130
166
|
* that one field. Adopt it and latch, so neither is ever titled over.
|
|
131
167
|
*
|
|
132
|
-
* With no title yet,
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
168
|
+
* With no title yet, and none asked for at the latest prompt (see
|
|
169
|
+
* {@link onPromptQueued}), ask the SDK to generate one in the background
|
|
170
|
+
* from the prompts and replies so far: it is a ~2s small-model call and
|
|
171
|
+
* turn-end must not wait on it. That covers a prompt too short to title
|
|
172
|
+
* from, such as "hi". Only when that is not possible do we fall back to
|
|
173
|
+
* `summary`, which for an SDK-driven session is just the raw first prompt. */
|
|
136
174
|
async onTurnEnd(session) {
|
|
137
175
|
if (this.explicit.settledByClient) {
|
|
138
176
|
// No title is generated over the one the client set, but a custom title
|
|
@@ -163,11 +201,9 @@ export class SessionTitles {
|
|
|
163
201
|
const fallback = info?.summary
|
|
164
202
|
? { title: info.summary, lastModified: info.lastModified }
|
|
165
203
|
: undefined;
|
|
166
|
-
if (this.canRequest(session)) {
|
|
167
|
-
this.
|
|
168
|
-
this.
|
|
169
|
-
this.agent.logger.error(`Session ${this.sessionId}: session title update failed: ${error}`);
|
|
170
|
-
}));
|
|
204
|
+
if (!this.askedAtPrompt && this.canRequest(session)) {
|
|
205
|
+
const description = this.context?.trim() ?? "";
|
|
206
|
+
this.startGeneration(() => this.requestGenerateTitle(session, description, fallback));
|
|
171
207
|
return;
|
|
172
208
|
}
|
|
173
209
|
// Only while this session has no title of its own: `summary` degrades to the
|
|
@@ -259,14 +295,40 @@ export class SessionTitles {
|
|
|
259
295
|
}
|
|
260
296
|
return (this.context?.trim().length ?? 0) >= MIN_TITLE_CONTEXT_LENGTH;
|
|
261
297
|
}
|
|
262
|
-
/**
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
298
|
+
/** Latch the title and run `generation` in the background, so neither the
|
|
299
|
+
* prompt nor turn-end waits on a model call. A client title change waits
|
|
300
|
+
* for it to finish (see {@link ExplicitTitle.track}). */
|
|
301
|
+
startGeneration(generation) {
|
|
302
|
+
this.settled = true;
|
|
303
|
+
this.explicit.track(generation().catch((error) => {
|
|
304
|
+
this.agent.logger.error(`Session ${this.sessionId}: session title update failed: ${error}`);
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
/** The prompt-time title: the one the session file already holds (a resumed
|
|
308
|
+
* session's title, or a `/rename`), else one generated from `description`.
|
|
309
|
+
* The file is read first because the CLI checks only the titles it holds in
|
|
310
|
+
* memory before generating one. */
|
|
311
|
+
async titleFromPrompt(session, description) {
|
|
312
|
+
const info = await this.readSessionInfo(session);
|
|
313
|
+
if (this.agent.sessions[this.sessionId] !== session || this.explicit.active) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (info?.customTitle) {
|
|
317
|
+
this.context = undefined;
|
|
318
|
+
await this.publish(info.customTitle, info.lastModified);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const fallback = info?.summary
|
|
322
|
+
? { title: info.summary, lastModified: info.lastModified }
|
|
323
|
+
: undefined;
|
|
324
|
+
await this.requestGenerateTitle(session, description, fallback);
|
|
325
|
+
}
|
|
326
|
+
/** Generate a title from `description` and publish it. `persist` writes it
|
|
327
|
+
* to the session file, which is what carries it into `session/list` and
|
|
328
|
+
* `session/load`. A null or failed generation releases the latch so a later
|
|
329
|
+
* prompt or turn end retries, and publishes `fallback` (the stored summary)
|
|
330
|
+
* so a backend that can't generate titles is no worse off than before. */
|
|
331
|
+
async requestGenerateTitle(session, description, fallback) {
|
|
270
332
|
let title = null;
|
|
271
333
|
try {
|
|
272
334
|
title =
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* history to the child session. Every child's end goes out before the replay
|
|
9
9
|
* ends, since no subagent from before the reload still runs.
|
|
10
10
|
*
|
|
11
|
+
* As live, the parent is not shown the Agent or Task call of a child that the
|
|
12
|
+
* replay announces: the launch's prompt is the parent's outgoing message, and
|
|
13
|
+
* a foreground child's answer is its reply to the parent. A child's
|
|
14
|
+
* successful SubagentHandback call is its message to the parent instead.
|
|
15
|
+
* Unlike live, the replay does not tell runs apart, so a child that handed
|
|
16
|
+
* back any report gets no reply from its launch's result.
|
|
17
|
+
*
|
|
11
18
|
* A launch without metadata has no known id, so it is not announced: the RFD
|
|
12
19
|
* forbids inventing one. Its Agent call stays an ordinary tool call of the
|
|
13
20
|
* parent, and a notice says that subagent history is missing.
|
|
@@ -33,6 +40,8 @@ export type ReplayChild = {
|
|
|
33
40
|
notified?: SubagentState;
|
|
34
41
|
announced: boolean;
|
|
35
42
|
promptSent: boolean;
|
|
43
|
+
/** Whether it handed a report to its parent with a SubagentHandback call. */
|
|
44
|
+
handedBack?: boolean;
|
|
36
45
|
};
|
|
37
46
|
export declare class RfdSubagentReplay {
|
|
38
47
|
private readonly history;
|
|
@@ -45,6 +54,8 @@ export declare class RfdSubagentReplay {
|
|
|
45
54
|
private readonly omitted;
|
|
46
55
|
/** Children whose transcript could not be read: only their association is replayed. */
|
|
47
56
|
private withoutHistory;
|
|
57
|
+
/** The SubagentHandback calls, with their report and whether their result failed. */
|
|
58
|
+
private readonly handbacks;
|
|
48
59
|
constructor(history: SubagentHistory, publish: Publish);
|
|
49
60
|
/**
|
|
50
61
|
* Registers the launches, the launch results, and the SendMessage calls in
|
|
@@ -73,6 +84,15 @@ export declare class RfdSubagentReplay {
|
|
|
73
84
|
* comes from the launch call.
|
|
74
85
|
*/
|
|
75
86
|
childReplayed(child: ReplayChild, messages: readonly SessionMessage[]): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Takes a replayed update of a tool call, from the transcript of `owner`
|
|
89
|
+
* when it is a child's. Returns whether the client is not shown it: an
|
|
90
|
+
* update of the launch of a child that the replay shows, or of a successful
|
|
91
|
+
* SubagentHandback call of `owner`. A foreground launch's completed result
|
|
92
|
+
* goes out as the child's reply, and a hand-back as the child's message to
|
|
93
|
+
* its parent.
|
|
94
|
+
*/
|
|
95
|
+
toolCallUpdate(update: AcpSessionNotification["update"], owner: ReplayChild | undefined): Promise<boolean>;
|
|
76
96
|
/**
|
|
77
97
|
* Takes a persisted task notification. Returns whether it was a child's:
|
|
78
98
|
* its task id is the agent id.
|
|
@@ -86,7 +106,7 @@ export declare class RfdSubagentReplay {
|
|
|
86
106
|
*/
|
|
87
107
|
finish(supportsNotices: boolean, rootSessionId: string): Promise<void>;
|
|
88
108
|
private registerLaunch;
|
|
89
|
-
/** The prompt of the launch, with the launch's id, as live. */
|
|
109
|
+
/** The prompt of the launch, with the launch's id, to the parent and the child, as live. */
|
|
90
110
|
private sendPrompt;
|
|
91
111
|
/**
|
|
92
112
|
* A message that a SendMessage call delivered. Claude Code frames it for the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-replay.d.ts","sourceRoot":"","sources":["../src/subagent-replay.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"subagent-replay.d.ts","sourceRoot":"","sources":["../src/subagent-replay.ts"],"names":[],"mappings":"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"}
|
package/dist/subagent-replay.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isNativeSubagentControlTool, nativeSubagentState, rfdEndState, rfdLabels, } from "./native-subagents.js";
|
|
1
|
+
import { HANDBACK_TOOL, handbackMessage, isNativeSubagentControlTool, launchReply, linkedSessionMessages, nativeSubagentState, rfdEndState, rfdLabels, } from "./native-subagents.js";
|
|
2
2
|
import { noticeUpdate } from "./session-notices.js";
|
|
3
3
|
export class RfdSubagentReplay {
|
|
4
4
|
history;
|
|
@@ -11,6 +11,8 @@ export class RfdSubagentReplay {
|
|
|
11
11
|
omitted = new Set();
|
|
12
12
|
/** Children whose transcript could not be read: only their association is replayed. */
|
|
13
13
|
withoutHistory = 0;
|
|
14
|
+
/** The SubagentHandback calls, with their report and whether their result failed. */
|
|
15
|
+
handbacks = new Map();
|
|
14
16
|
constructor(history, publish) {
|
|
15
17
|
this.history = history;
|
|
16
18
|
this.publish = publish;
|
|
@@ -26,6 +28,11 @@ export class RfdSubagentReplay {
|
|
|
26
28
|
if (isNativeSubagentControlTool(block.name)) {
|
|
27
29
|
this.registerLaunch(block.id, inputOf(block), ownerSessionId);
|
|
28
30
|
}
|
|
31
|
+
else if (block.name === HANDBACK_TOOL) {
|
|
32
|
+
const message = handbackMessage(block.input);
|
|
33
|
+
if (message)
|
|
34
|
+
this.handbacks.set(block.id, { message, failed: false, sent: false });
|
|
35
|
+
}
|
|
29
36
|
else if (block.name === "SendMessage") {
|
|
30
37
|
const input = inputOf(block);
|
|
31
38
|
if (typeof input.to === "string" && typeof input.message === "string") {
|
|
@@ -40,6 +47,9 @@ export class RfdSubagentReplay {
|
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
else if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
50
|
+
const handback = this.handbacks.get(block.tool_use_id);
|
|
51
|
+
if (handback && block.is_error === true)
|
|
52
|
+
handback.failed = true;
|
|
43
53
|
const child = this.children.get(block.tool_use_id);
|
|
44
54
|
// A background launch answers at once; its result is not the end.
|
|
45
55
|
if (child && !child.record.background) {
|
|
@@ -106,6 +116,42 @@ export class RfdSubagentReplay {
|
|
|
106
116
|
await this.sendPrompt(child, child.input.prompt);
|
|
107
117
|
}
|
|
108
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Takes a replayed update of a tool call, from the transcript of `owner`
|
|
121
|
+
* when it is a child's. Returns whether the client is not shown it: an
|
|
122
|
+
* update of the launch of a child that the replay shows, or of a successful
|
|
123
|
+
* SubagentHandback call of `owner`. A foreground launch's completed result
|
|
124
|
+
* goes out as the child's reply, and a hand-back as the child's message to
|
|
125
|
+
* its parent.
|
|
126
|
+
*/
|
|
127
|
+
async toolCallUpdate(update, owner) {
|
|
128
|
+
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
|
|
132
|
+
if (owner && handback && !handback.failed) {
|
|
133
|
+
if (!handback.sent) {
|
|
134
|
+
handback.sent = true;
|
|
135
|
+
owner.handedBack = true;
|
|
136
|
+
for (const view of linkedSessionMessages(update.toolCallId, owner.sessionId, owner.parentSessionId, handback.message)) {
|
|
137
|
+
await this.publish(view);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
const child = this.children.get(update.toolCallId);
|
|
143
|
+
if (!child)
|
|
144
|
+
return false;
|
|
145
|
+
if (update.status === "completed" &&
|
|
146
|
+
child.announced &&
|
|
147
|
+
!child.record.background &&
|
|
148
|
+
!child.handedBack) {
|
|
149
|
+
const reply = launchReply(child.launchId, child.sessionId, child.parentSessionId, update);
|
|
150
|
+
if (reply)
|
|
151
|
+
await this.publish(reply);
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
109
155
|
/**
|
|
110
156
|
* Takes a persisted task notification. Returns whether it was a child's:
|
|
111
157
|
* its task id is the agent id.
|
|
@@ -168,10 +214,12 @@ export class RfdSubagentReplay {
|
|
|
168
214
|
this.children.set(toolUseId, child);
|
|
169
215
|
this.byAgentId.set(record.agentId, child);
|
|
170
216
|
}
|
|
171
|
-
/** The prompt of the launch, with the launch's id, as live. */
|
|
217
|
+
/** The prompt of the launch, with the launch's id, to the parent and the child, as live. */
|
|
172
218
|
async sendPrompt(child, text) {
|
|
173
219
|
child.promptSent = true;
|
|
174
|
-
|
|
220
|
+
for (const view of linkedSessionMessages(child.launchId, child.parentSessionId, child.sessionId, text)) {
|
|
221
|
+
await this.publish(view);
|
|
222
|
+
}
|
|
175
223
|
}
|
|
176
224
|
/**
|
|
177
225
|
* A message that a SendMessage call delivered. Claude Code frames it for the
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<task-notification>` inputs that Claude Code writes to a transcript
|
|
3
|
+
* when a background task stops. The replay of a session (`acp-agent.ts`) and
|
|
4
|
+
* the session index (`session-index/background-tasks.ts`) read them here, so
|
|
5
|
+
* both read the same fields.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TASK_NOTIFICATION_TAG = "task-notification";
|
|
8
|
+
/** The text between the first `<tag>` and the next `</tag>` after it. */
|
|
9
|
+
export declare function markerTagText(text: string, tag: string): string | undefined;
|
|
10
|
+
/** The fields of a persisted `<task-notification>`, in the shape of the SDK
|
|
11
|
+
* `task_notification` frame that reported the same stop live. */
|
|
12
|
+
export type PersistedTaskNotification = {
|
|
13
|
+
task_id: string;
|
|
14
|
+
status: string;
|
|
15
|
+
tool_use_id?: string;
|
|
16
|
+
summary?: string;
|
|
17
|
+
output_file?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Returns each `<task-notification>` in user-message content (a string or its
|
|
21
|
+
* text blocks). The `<result>` body is model context that the live frame does
|
|
22
|
+
* not carry, so the scan stops at it and a tag quoted in it cannot match.
|
|
23
|
+
*
|
|
24
|
+
* Only a notification with a `<task-id>` and a `<status>` is returned. A
|
|
25
|
+
* notification without a status comes while its task still runs: the
|
|
26
|
+
* check-in of a quiet shell ("is still running after …") or a `Monitor`
|
|
27
|
+
* event.
|
|
28
|
+
*/
|
|
29
|
+
export declare function taskNotificationsOf(content: unknown): PersistedTaskNotification[];
|
|
30
|
+
//# sourceMappingURL=task-notifications.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-notifications.d.ts","sourceRoot":"","sources":["../src/task-notifications.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ3E;AAED;kEACkE;AAClE,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,yBAAyB,EAAE,CAkCjF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<task-notification>` inputs that Claude Code writes to a transcript
|
|
3
|
+
* when a background task stops. The replay of a session (`acp-agent.ts`) and
|
|
4
|
+
* the session index (`session-index/background-tasks.ts`) read them here, so
|
|
5
|
+
* both read the same fields.
|
|
6
|
+
*/
|
|
7
|
+
export const TASK_NOTIFICATION_TAG = "task-notification";
|
|
8
|
+
/** The text between the first `<tag>` and the next `</tag>` after it. */
|
|
9
|
+
export function markerTagText(text, tag) {
|
|
10
|
+
const open = `<${tag}>`;
|
|
11
|
+
const close = `</${tag}>`;
|
|
12
|
+
const start = text.indexOf(open);
|
|
13
|
+
if (start === -1)
|
|
14
|
+
return undefined;
|
|
15
|
+
const end = text.indexOf(close, start + open.length);
|
|
16
|
+
if (end === -1)
|
|
17
|
+
return undefined;
|
|
18
|
+
return text.slice(start + open.length, end);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns each `<task-notification>` in user-message content (a string or its
|
|
22
|
+
* text blocks). The `<result>` body is model context that the live frame does
|
|
23
|
+
* not carry, so the scan stops at it and a tag quoted in it cannot match.
|
|
24
|
+
*
|
|
25
|
+
* Only a notification with a `<task-id>` and a `<status>` is returned. A
|
|
26
|
+
* notification without a status comes while its task still runs: the
|
|
27
|
+
* check-in of a quiet shell ("is still running after …") or a `Monitor`
|
|
28
|
+
* event.
|
|
29
|
+
*/
|
|
30
|
+
export function taskNotificationsOf(content) {
|
|
31
|
+
const texts = typeof content === "string"
|
|
32
|
+
? [content]
|
|
33
|
+
: Array.isArray(content)
|
|
34
|
+
? content.flatMap((block) => block?.type === "text" && typeof block.text === "string" ? [block.text] : [])
|
|
35
|
+
: [];
|
|
36
|
+
const open = `<${TASK_NOTIFICATION_TAG}>`;
|
|
37
|
+
const close = `</${TASK_NOTIFICATION_TAG}>`;
|
|
38
|
+
const notifications = [];
|
|
39
|
+
for (const text of texts) {
|
|
40
|
+
let start = text.indexOf(open);
|
|
41
|
+
while (start !== -1) {
|
|
42
|
+
const end = text.indexOf(close, start + open.length);
|
|
43
|
+
if (end === -1)
|
|
44
|
+
break;
|
|
45
|
+
const body = text.slice(start + open.length, end).split("<result>", 1)[0];
|
|
46
|
+
const field = (tag) => markerTagText(body, tag)?.trim() || undefined;
|
|
47
|
+
const taskId = field("task-id");
|
|
48
|
+
const status = field("status");
|
|
49
|
+
if (taskId && status) {
|
|
50
|
+
notifications.push({
|
|
51
|
+
task_id: taskId,
|
|
52
|
+
status,
|
|
53
|
+
tool_use_id: field("tool-use-id"),
|
|
54
|
+
summary: field("summary"),
|
|
55
|
+
output_file: field("output-file"),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
start = text.indexOf(open, end + close.length);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return notifications;
|
|
62
|
+
}
|
package/package.json
CHANGED