@agentclientprotocol/claude-agent-acp 0.94.0 → 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.
Files changed (50) hide show
  1. package/README.md +21 -9
  2. package/dist/acp-agent.d.ts +39 -3
  3. package/dist/acp-agent.d.ts.map +1 -1
  4. package/dist/acp-agent.js +196 -67
  5. package/dist/acp-subagents.d.ts +2 -1
  6. package/dist/acp-subagents.d.ts.map +1 -1
  7. package/dist/air-extension.d.ts +5 -0
  8. package/dist/air-extension.d.ts.map +1 -1
  9. package/dist/air-extension.js +5 -0
  10. package/dist/async-tasks.d.ts +19 -0
  11. package/dist/async-tasks.d.ts.map +1 -1
  12. package/dist/async-tasks.js +52 -0
  13. package/dist/clear-context-coordinator.d.ts +1 -0
  14. package/dist/clear-context-coordinator.d.ts.map +1 -1
  15. package/dist/clear-context-coordinator.js +1 -0
  16. package/dist/index.js +2 -3
  17. package/dist/native-subagents.d.ts +64 -0
  18. package/dist/native-subagents.d.ts.map +1 -1
  19. package/dist/native-subagents.js +224 -9
  20. package/dist/permissions/options/shared.d.ts +3 -0
  21. package/dist/permissions/options/shared.d.ts.map +1 -1
  22. package/dist/permissions/options/tools.js +1 -1
  23. package/dist/serve.d.ts +19 -11
  24. package/dist/serve.d.ts.map +1 -1
  25. package/dist/serve.js +26 -18
  26. package/dist/session-index/activity.d.ts +4 -0
  27. package/dist/session-index/activity.d.ts.map +1 -1
  28. package/dist/session-index/activity.js +16 -0
  29. package/dist/session-index/background-tasks.d.ts +69 -0
  30. package/dist/session-index/background-tasks.d.ts.map +1 -0
  31. package/dist/session-index/background-tasks.js +173 -0
  32. package/dist/session-index/service.d.ts +2 -0
  33. package/dist/session-index/service.d.ts.map +1 -1
  34. package/dist/session-index/service.js +6 -1
  35. package/dist/session-index/session-index.d.ts +14 -0
  36. package/dist/session-index/session-index.d.ts.map +1 -1
  37. package/dist/session-index/session-index.js +25 -2
  38. package/dist/session-index/transcript-scan.d.ts +8 -1
  39. package/dist/session-index/transcript-scan.d.ts.map +1 -1
  40. package/dist/session-index/transcript-scan.js +22 -1
  41. package/dist/session-titles.d.ts +44 -16
  42. package/dist/session-titles.d.ts.map +1 -1
  43. package/dist/session-titles.js +89 -27
  44. package/dist/subagent-replay.d.ts +21 -1
  45. package/dist/subagent-replay.d.ts.map +1 -1
  46. package/dist/subagent-replay.js +51 -3
  47. package/dist/task-notifications.d.ts +30 -0
  48. package/dist/task-notifications.d.ts.map +1 -0
  49. package/dist/task-notifications.js +62 -0
  50. package/package.json +1 -1
@@ -7,8 +7,12 @@
7
7
  *
8
8
  * A last message longer than the tail window leaves no message in it: then
9
9
  * the tail window grows, up to {@link MAX_TAIL_SIZE}, until it holds one.
10
+ *
11
+ * The open background tasks come from the tail, on top of the tasks of an
12
+ * earlier scan (see `background-tasks.ts`).
10
13
  */
11
14
  import * as fs from "node:fs/promises";
15
+ import { backgroundTasksOf } from "./background-tasks.js";
12
16
  import { firstPrompt, isUserPrompt, mediaPrompt, promptOf } from "./first-prompt.js";
13
17
  const CHUNK_SIZE = 64 * 1024;
14
18
  /** The largest tail window read to find the last message. */
@@ -88,6 +92,8 @@ identity) {
88
92
  }
89
93
  }
90
94
  }
95
+ const previousTasks = grownWithinTail ? previous.backgroundTasks : undefined;
96
+ facts.backgroundTasks = tailBackgroundTasks(read.tail, size, previousTasks);
91
97
  // No worktree record in the tail (or before it, by an earlier scan): the
92
98
  // one `claude --worktree` writes at the start may still be in the head.
93
99
  if (facts.agentWorktree === undefined) {
@@ -107,8 +113,9 @@ identity) {
107
113
  return facts;
108
114
  const key = `${filePath}\0${size}\0${identity ?? ""}`;
109
115
  const searched = fullySearched.get(key);
116
+ // The tasks can differ by the earlier scan.
110
117
  if (searched)
111
- return searched;
118
+ return { ...searched, backgroundTasks: facts.backgroundTasks };
112
119
  let result = facts;
113
120
  const handle = await fs.open(filePath, "r");
114
121
  try {
@@ -150,6 +157,7 @@ identity) {
150
157
  // A turn end the wider tail lacks may lie before it: the
151
158
  // narrow scan inherited it from an earlier scan of this file.
152
159
  lastTurnEndedAt: wider.lastTurnEndedAt ?? facts.lastTurnEndedAt,
160
+ backgroundTasks: facts.backgroundTasks,
153
161
  hasMessages: facts.hasMessages,
154
162
  lastPromptAt,
155
163
  // Searched to the start of the file, or found.
@@ -170,6 +178,19 @@ identity) {
170
178
  }
171
179
  return result;
172
180
  }
181
+ /**
182
+ * The background tasks of the tail of a file of `size` bytes. `previous`:
183
+ * the tasks of an earlier scan, when the file only grew within the tail
184
+ * since. The set is complete when the tail is the whole file, or when it
185
+ * holds all records after a complete earlier set.
186
+ */
187
+ function tailBackgroundTasks(tail, size, previous) {
188
+ if (size <= CHUNK_SIZE)
189
+ return backgroundTasksOf(tail, { complete: true });
190
+ if (previous?.complete)
191
+ return backgroundTasksOf(tail, { complete: true, after: previous });
192
+ return backgroundTasksOf(tail, { complete: false });
193
+ }
173
194
  const RELOCATED_MARKER = '"relocated"';
174
195
  /** The cwd of the last `relocated` record of the tail: where the session
175
196
  * was moved, whatever cwd its earlier messages carry. */
@@ -1,15 +1,20 @@
1
1
  /**
2
2
  * AI-generated session titles.
3
3
  *
4
- * Claude Code's own auto-title generation never runs under the Agent SDK — the
5
- * latch that arms it is pre-set in the headless path — so `SDKSessionInfo.summary`
6
- * degrades to the raw first prompt. This module asks the CLI for a real title
7
- * instead, via the `generate_session_title` control request, and publishes it
8
- * over ACP as a `session_info_update`.
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 four places: {@link SessionTitles.onPrompt} and
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
  */
@@ -36,6 +41,10 @@ export declare class SessionTitles {
36
41
  * the same field as a user `/rename`, so re-titling could silently overwrite
37
42
  * one. Released by {@link reset}, and when generation yields nothing. */
38
43
  private settled;
44
+ /** Whether {@link onPromptQueued} asked for a title from the latest prompt.
45
+ * A turn end then does not ask again: like the CLI, a prompt that yields
46
+ * no title gets one try, and the next prompt tries again. */
47
+ private askedAtPrompt;
39
48
  /** The title a client set, which no generated title replaces. */
40
49
  private readonly explicit;
41
50
  /** The title last stored through {@link setExplicitTitle}, as stored
@@ -52,6 +61,15 @@ export declare class SessionTitles {
52
61
  /** Collect a prompt's own text for the title, skipping openers not worth
53
62
  * titling after. No-op once the title is settled. */
54
63
  onPrompt(prompt: PromptRequest["prompt"]): void;
64
+ /** Ask for the title as soon as a prompt is sent to the CLI, instead of
65
+ * waiting for `idle`: `idle` comes only after the whole turn and its
66
+ * background tasks, often minutes after the CLI stored its title. The CLI
67
+ * answers `generate_session_title` while the turn runs, and with the title
68
+ * it stored or is generating, so this makes no second model call. The
69
+ * prompt alone is the description, because the CLI titles from the prompt
70
+ * alone. When this yields no title, the stored summary is published and
71
+ * the next prompt tries again. */
72
+ onPromptQueued(session: Session, prompt: PromptRequest["prompt"]): void;
55
73
  /** Collect the assistant's own answer for the title. Chunks are appended
56
74
  * verbatim so streamed text reassembles. No-op once the title is settled. */
57
75
  onAssistantText(content: ContentBlock): void;
@@ -65,10 +83,12 @@ export declare class SessionTitles {
65
83
  * title — a user `/rename` or one generated earlier; the SDK folds both into
66
84
  * that one field. Adopt it and latch, so neither is ever titled over.
67
85
  *
68
- * With no title yet, ask the SDK to generate one in the background: it is a
69
- * ~2s small-model call and turn-end must not wait on it. Only when that is
70
- * not possible do we fall back to `summary`, which for an SDK-driven session
71
- * is just the raw first prompt. */
86
+ * With no title yet, and none asked for at the latest prompt (see
87
+ * {@link onPromptQueued}), ask the SDK to generate one in the background
88
+ * from the prompts and replies so far: it is a ~2s small-model call and
89
+ * turn-end must not wait on it. That covers a prompt too short to title
90
+ * from, such as "hi". Only when that is not possible do we fall back to
91
+ * `summary`, which for an SDK-driven session is just the raw first prompt. */
72
92
  onTurnEnd(session: Session): Promise<void>;
73
93
  /** Applies a title that the client chose (a rename, an archive or an
74
94
  * unarchive). Settles the title for good, waits for a generation in flight
@@ -87,12 +107,20 @@ export declare class SessionTitles {
87
107
  /** Whether to ask the SDK for a generated title now: once per session, on a
88
108
  * live query, with enough collected text for the generator to work with. */
89
109
  private canRequest;
90
- /** Generate a title from the session's own text and publish it. `persist`
91
- * writes it to the session file, which is what carries it into
92
- * `session/list` and `session/load`. A null or failed generation releases the
93
- * latch so a later turn retries, and publishes `fallback` (the stored
94
- * summary) so a backend that can't generate titles is no worse off than
95
- * before. */
110
+ /** Latch the title and run `generation` in the background, so neither the
111
+ * prompt nor turn-end waits on a model call. A client title change waits
112
+ * for it to finish (see {@link ExplicitTitle.track}). */
113
+ private startGeneration;
114
+ /** The prompt-time title: the one the session file already holds (a resumed
115
+ * session's title, or a `/rename`), else one generated from `description`.
116
+ * The file is read first because the CLI checks only the titles it holds in
117
+ * memory before generating one. */
118
+ private titleFromPrompt;
119
+ /** Generate a title from `description` and publish it. `persist` writes it
120
+ * to the session file, which is what carries it into `session/list` and
121
+ * `session/load`. A null or failed generation releases the latch so a later
122
+ * prompt or turn end retries, and publishes `fallback` (the stored summary)
123
+ * so a backend that can't generate titles is no worse off than before. */
96
124
  private requestGenerateTitle;
97
125
  }
98
126
  //# sourceMappingURL=session-titles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-titles.d.ts","sourceRoot":"","sources":["../src/session-titles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAErF,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AA+B9D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqBlD;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGrF;AAOD;;iDAEiD;AACjD,qBAAa,aAAa;IAqCtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IArC5B;sEACkE;IAClE,OAAO,CAAC,SAAS,CAAC,CAAS;IAE3B;yEACqE;IACrE,OAAO,CAAC,OAAO,CAAC,CAAS;IAEzB;;;8EAG0E;IAC1E,OAAO,CAAC,OAAO,CAAS;IAExB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAEhD;;iBAEa;IACb,OAAO,CAAC,cAAc,CAAC,CAAS;IAEhC,4EAA4E;IAC5E,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAAC,CAAoD;IAE3E,IAAI,WAAW,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,wDAAwD;IACxD,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAEnC;gBAGkB,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM;IAGpC;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,IAAI;IAW/C;kFAC8E;IAC9E,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAO5C;iEAC6D;IAC7D,KAAK,IAAI,IAAI;IAQb;;;;;;;;;;wCAUoC;IAC9B,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDhD;;;;;8DAK0D;IACpD,gBAAgB,CACpB,OAAO,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC,GAAG,SAAS,EACtE,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC;IAkChB,2DAA2D;IAC3D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIxC;oFACgF;YAClE,eAAe;IAS7B,uEAAuE;YACzD,OAAO;IAgBrB;iFAC6E;IAC7E,OAAO,CAAC,UAAU;IAUlB;;;;;kBAKc;YACA,oBAAoB;CAoCnC"}
1
+ {"version":3,"file":"session-titles.d.ts","sourceRoot":"","sources":["../src/session-titles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAErF,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AA+B9D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqBlD;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGrF;AAYD;;iDAEiD;AACjD,qBAAa,aAAa;IA0CtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IA1C5B;sEACkE;IAClE,OAAO,CAAC,SAAS,CAAC,CAAS;IAE3B;yEACqE;IACrE,OAAO,CAAC,OAAO,CAAC,CAAS;IAEzB;;;8EAG0E;IAC1E,OAAO,CAAC,OAAO,CAAS;IAExB;;kEAE8D;IAC9D,OAAO,CAAC,aAAa,CAAS;IAE9B,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAEhD;;iBAEa;IACb,OAAO,CAAC,cAAc,CAAC,CAAS;IAEhC,4EAA4E;IAC5E,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAAC,CAAoD;IAE3E,IAAI,WAAW,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,wDAAwD;IACxD,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAEnC;gBAGkB,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM;IAGpC;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,IAAI;IAO/C;;;;;;;uCAOmC;IACnC,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,IAAI;IAkBvE;kFAC8E;IAC9E,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAO5C;iEAC6D;IAC7D,KAAK,IAAI,IAAI;IASb;;;;;;;;;;;;mFAY+E;IACzE,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA4ChD;;;;;8DAK0D;IACpD,gBAAgB,CACpB,OAAO,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC,GAAG,SAAS,EACtE,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC;IAkChB,2DAA2D;IAC3D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIxC;oFACgF;YAClE,eAAe;IAS7B,uEAAuE;YACzD,OAAO;IAgBrB;iFAC6E;IAC7E,OAAO,CAAC,UAAU;IAUlB;;8DAE0D;IAC1D,OAAO,CAAC,eAAe;IASvB;;;wCAGoC;YACtB,eAAe;IAgB7B;;;;+EAI2E;YAC7D,oBAAoB;CAoCnC"}
@@ -1,15 +1,20 @@
1
1
  /**
2
2
  * AI-generated session titles.
3
3
  *
4
- * Claude Code's own auto-title generation never runs under the Agent SDK — the
5
- * latch that arms it is pre-set in the headless path — so `SDKSessionInfo.summary`
6
- * degrades to the raw first prompt. This module asks the CLI for a real title
7
- * instead, via the `generate_session_title` control request, and publishes it
8
- * over ACP as a `session_info_update`.
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 four places: {@link SessionTitles.onPrompt} and
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
- const promptText = prompt
104
- .flatMap((chunk) => (chunk.type === "text" ? [chunk.text] : []))
105
- .join("\n");
106
- this.context = appendTitleContext(this.context, `\n${promptText}\n`);
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, ask the SDK to generate one in the background: it is a
133
- * ~2s small-model call and turn-end must not wait on it. Only when that is
134
- * not possible do we fall back to `summary`, which for an SDK-driven session
135
- * is just the raw first prompt. */
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.settled = true;
168
- this.explicit.track(this.requestGenerateTitle(session, fallback).catch((error) => {
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
- /** Generate a title from the session's own text and publish it. `persist`
263
- * writes it to the session file, which is what carries it into
264
- * `session/list` and `session/load`. A null or failed generation releases the
265
- * latch so a later turn retries, and publishes `fallback` (the stored
266
- * summary) so a backend that can't generate titles is no worse off than
267
- * before. */
268
- async requestGenerateTitle(session, fallback) {
269
- const description = this.context?.trim() ?? "";
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;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAShF,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE7E,KAAK,OAAO,GAAG,CAAC,YAAY,EAAE,sBAAsB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE,8DAA8D;AAC9D,MAAM,MAAM,WAAW,GAAG;IACxB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;IACvB,wCAAwC;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,mEAAmE;IACnE,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAUF,qBAAa,iBAAiB;IAW1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAX1B,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyB;IACtD,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,uFAAuF;IACvF,OAAO,CAAC,cAAc,CAAK;gBAGR,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,OAAO;IAGnC;;;OAGG;IACH,IAAI,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI;IA6BvE,kFAAkF;IAClF,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAIzD,mFAAmF;IAC7E,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBjD;;;;;;;;;;OAUG;IACG,YAAY,CAChB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,OAAO,CAAC;IAanB;;;;OAIG;IACG,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3F;;;OAGG;IACH,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAOzD;;;;;OAKG;IACG,MAAM,CAAC,eAAe,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B5E,OAAO,CAAC,cAAc;IAwBtB,+DAA+D;YACjD,UAAU;IAKxB;;;;;;;OAOG;YACW,YAAY;YAoBZ,WAAW;CAiB1B;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,WAAW,GAAG,QAAQ,GAAG,WAAW,CAMtC"}
1
+ {"version":3,"file":"subagent-replay.d.ts","sourceRoot":"","sources":["../src/subagent-replay.ts"],"names":[],"mappings":"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"}
@@ -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
- await this.sendMessage(child, child.launchId, text, child.parentSessionId);
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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.94.0",
6
+ "version": "0.94.1-preview.10",
7
7
  "description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
8
8
  "main": "dist/lib.js",
9
9
  "types": "dist/lib.d.ts",