@borgee/agents-host 0.2.94 → 0.2.101

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 (40) hide show
  1. package/dist/agents-host.d.ts +8 -0
  2. package/dist/agents-host.js +266 -64
  3. package/dist/background-runs.d.ts +4 -0
  4. package/dist/background-runs.js +8 -4
  5. package/dist/chat/chat-control-plane.d.ts +14 -3
  6. package/dist/chat/sdk-chat-control-plane.d.ts +31 -5
  7. package/dist/chat/sdk-chat-control-plane.js +134 -3
  8. package/dist/context/skill-manual.js +1 -1
  9. package/dist/execution-telemetry.d.ts +96 -0
  10. package/dist/execution-telemetry.js +583 -0
  11. package/dist/gateway/channel-file-workspace.d.ts +15 -0
  12. package/dist/gateway/channel-file-workspace.js +130 -0
  13. package/dist/gateway/localhost-gateway.js +140 -0
  14. package/dist/plugin-sdk.js +85 -4
  15. package/dist/plugin-sdk.js.map +2 -2
  16. package/dist/policy/gateway-authorization.d.ts +1 -1
  17. package/dist/policy/gateway-authorization.js +22 -2
  18. package/dist/progress-to-activity.d.ts +3 -3
  19. package/dist/providers/claude/adapter.d.ts +2 -1
  20. package/dist/providers/claude/adapter.js +11 -0
  21. package/dist/providers/claude/cli-client.d.ts +3 -1
  22. package/dist/providers/claude/cli-client.js +267 -13
  23. package/dist/providers/codex/adapter.js +1 -0
  24. package/dist/providers/codex/cli-client.js +5 -0
  25. package/dist/providers/copilot/adapter.js +4 -0
  26. package/dist/providers/copilot/cli-client.js +11 -1
  27. package/dist/providers/copilot/sdk-session.d.ts +12 -3
  28. package/dist/providers/copilot/sdk-session.js +222 -5
  29. package/dist/providers/create-provider.js +4 -0
  30. package/dist/providers/prompt-usage.d.ts +8 -0
  31. package/dist/providers/prompt-usage.js +52 -0
  32. package/dist/state-paths.d.ts +2 -0
  33. package/dist/state-paths.js +6 -0
  34. package/dist/types.d.ts +41 -0
  35. package/dist/typing-lease.d.ts +14 -0
  36. package/dist/typing-lease.js +31 -0
  37. package/package.json +9 -9
  38. package/skills/borgee-agent/SKILL.md +12 -4
  39. package/skills/borgee-agent/scripts/borgee-agent.mjs +89 -0
  40. package/skills/borgee-agent/scripts/borgee-agent.py +90 -0
@@ -2,7 +2,7 @@ import type { IncomingMessage } from 'node:http';
2
2
  export type GatewayDecisionReason = 'browser-origin-not-allowed' | 'method-not-allowed' | 'missing-or-invalid-token' | 'invalid-token' | 'channel-mismatch' | 'not-found' | 'bootstrap-unavailable' | 'authorized';
3
3
  export interface GatewayChannelRoute {
4
4
  channelId: string;
5
- resource: 'bootstrap' | 'me' | 'history' | 'draft' | 'users' | 'messages' | 'tasks' | 'current-task';
5
+ resource: 'bootstrap' | 'me' | 'history' | 'files' | 'file-content' | 'file-download' | 'file-sync' | 'file-publish' | 'draft' | 'users' | 'messages' | 'tasks' | 'current-task';
6
6
  }
7
7
  export interface GatewayTaskRoute {
8
8
  taskId: string;
@@ -31,9 +31,23 @@ function decodePathSegment(segment) {
31
31
  }
32
32
  }
33
33
  export function matchChannelRoute(pathname, allowCollaborationRoutes = false) {
34
+ const fileOperation = /^\/v1\/channels\/([^/]+)\/files\/(content|download|sync|publish)$/.exec(pathname);
35
+ if (fileOperation) {
36
+ const channelId = decodePathSegment(fileOperation[1]);
37
+ if (channelId == null) {
38
+ return null;
39
+ }
40
+ const resources = {
41
+ content: 'file-content',
42
+ download: 'file-download',
43
+ sync: 'file-sync',
44
+ publish: 'file-publish',
45
+ };
46
+ return { channelId, resource: resources[fileOperation[2]] };
47
+ }
34
48
  const resources = allowCollaborationRoutes
35
- ? '(bootstrap|me|history|draft|users|messages|tasks|current-task)'
36
- : '(bootstrap|me|history|users|tasks|current-task)';
49
+ ? '(bootstrap|me|history|files|draft|users|messages|tasks|current-task)'
50
+ : '(bootstrap|me|history|files|users|tasks|current-task)';
37
51
  const match = new RegExp(`^/v1/channels/([^/]+)/${resources}$`).exec(pathname);
38
52
  if (!match) {
39
53
  return null;
@@ -91,10 +105,16 @@ function allowedMethodsForRoute(route, collaborationRoutesEnabled) {
91
105
  case 'bootstrap':
92
106
  case 'me':
93
107
  case 'history':
108
+ case 'files':
109
+ case 'file-content':
94
110
  case 'draft':
95
111
  case 'users':
96
112
  case 'task-history':
97
113
  return ['GET'];
114
+ case 'file-download':
115
+ case 'file-sync':
116
+ case 'file-publish':
117
+ return ['POST'];
98
118
  case 'messages':
99
119
  return collaborationRoutesEnabled ? ['POST'] : [];
100
120
  case 'tasks':
@@ -1,8 +1,8 @@
1
1
  import type { AgentActivity } from './plugin-sdk.js';
2
2
  import type { ProviderProgressUpdate } from './types.js';
3
- /** Everything a provider reports. The turn boundary is the reporter's own and never arrives here. */
4
- export type ReportedActivity = Exclude<AgentActivity, {
5
- shape: 'turn' | 'background_run';
3
+ /** Absolute provider state accepted by the foreground activity coalescer. */
4
+ export type ReportedActivity = Extract<AgentActivity, {
5
+ shape: 'activity' | 'plan' | 'output';
6
6
  }>;
7
7
  /**
8
8
  * Restates one progress update in the rail's own vocabulary.
@@ -1,5 +1,5 @@
1
1
  import { type ProviderAdapter } from '../provider-adapter.js';
2
- import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../../types.js';
2
+ import type { ProviderAutonomousReply, ProviderGenerateOptions, ProviderInput, ProviderReply } from '../../types.js';
3
3
  import { ProviderTurnPreparer } from '../../context/turn-preparation.js';
4
4
  import { ClaudeCliClient } from './cli-client.js';
5
5
  export declare const CLAUDE_HOSTED_PROVIDER_CAPABILITIES: import("../provider-adapter.js").ProviderCapabilities;
@@ -10,6 +10,7 @@ export declare class ClaudeProviderAdapter implements ProviderAdapter {
10
10
  readonly cancelBackgroundRun?: (channelId: string, providerRunId: string) => Promise<boolean>;
11
11
  constructor(cli: ClaudeCliClient, turnPreparer: Pick<ProviderTurnPreparer, 'prepare'>);
12
12
  generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
13
+ onAutonomousReply(handler: ((reply: ProviderAutonomousReply) => void) | undefined): void;
13
14
  cancelTurn(channelId: string): Promise<boolean>;
14
15
  dispose(): Promise<void>;
15
16
  }
@@ -55,11 +55,22 @@ export class ClaudeProviderAdapter {
55
55
  const preparedTurn = await this.turnPreparer.prepare(input);
56
56
  const text = await this.cli.generateReply(preparedTurn, {
57
57
  onProgress: createAwaitingUserProgressHandler(options?.onProgress),
58
+ ...(options?.onUsage ? { onUsage: options.onUsage } : {}),
58
59
  });
59
60
  // Read the session AFTER the turn: a first turn on a channel opens the
60
61
  // session as it runs, so before this point there is nothing to report.
61
62
  return { ...parseProviderReply(text), sessionId: this.cli.sessionIdForChannel(input.channelId) };
62
63
  }
64
+ onAutonomousReply(handler) {
65
+ this.cli.onAutonomousReply(handler
66
+ ? (reply) => {
67
+ const parsed = parseProviderReply(reply.text);
68
+ if (parsed.text.trim()) {
69
+ handler({ ...reply, text: parsed.text });
70
+ }
71
+ }
72
+ : undefined);
73
+ }
63
74
  async cancelTurn(channelId) {
64
75
  return this.cli.cancelTurn(channelId);
65
76
  }
@@ -1,7 +1,7 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { PROTOCOL_VERSION, client, methods, ndJsonStream } from '@agentclientprotocol/sdk';
3
3
  import { type DebugLogger } from '../../debug.js';
4
- import type { PreparedProviderTurnInput, ProviderGenerateOptions } from '../../types.js';
4
+ import type { PreparedProviderTurnInput, ProviderAutonomousReply, ProviderGenerateOptions } from '../../types.js';
5
5
  import type { ClaudeChannelSessionStore } from './session-store.js';
6
6
  interface ClaudeAcpRuntime {
7
7
  spawn: typeof spawn;
@@ -78,9 +78,11 @@ export declare class ClaudeCliClient {
78
78
  private sessionStoreWriteQueue;
79
79
  private sessionCapabilities;
80
80
  private targetedBackgroundRunCancellationAvailable;
81
+ private autonomousReplyHandler?;
81
82
  private readonly idleBackendShutdown;
82
83
  private childStderr;
83
84
  constructor(command: string, args?: string[], runtimeOverrides?: Partial<ClaudeAcpRuntime>, sessionStore?: ClaudeChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined, logger?: DebugLogger, imageInputConfig?: ClaudeHostedImageInputConfig);
85
+ onAutonomousReply(handler: ((reply: ProviderAutonomousReply) => void) | undefined): void;
84
86
  generateReply(turn: PreparedProviderTurnInput, options?: ProviderGenerateOptions): Promise<string>;
85
87
  generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
86
88
  dispose(): Promise<void>;
@@ -9,6 +9,7 @@ import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaud
9
9
  import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
10
10
  import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
11
11
  import { AcpProgressCollector } from '../acp-progress-collector.js';
12
+ import { normalizeAcpPromptUsage } from '../prompt-usage.js';
12
13
  import { ProviderTurnCancelledError } from '../provider-adapter.js';
13
14
  import { readClaudeActivityMetadata } from './activity-metadata.js';
14
15
  import { ClaudeBackgroundRunObserver } from './background-run-observer.js';
@@ -18,17 +19,26 @@ import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
18
19
  import { isDiscussionOnlyResolvedWorkingFolder } from '../../context/resolved-working-folder.js';
19
20
  import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
20
21
  import { CLAUDE_TASK_CANCELLATION_METHOD, supportsClaudeTaskCancellation, } from './task-cancellation-protocol.js';
22
+ const BACKGROUND_COMPLETION_SIGNAL_TTL_MS = 30_000;
23
+ const MAX_BACKGROUND_COMPLETION_SIGNALS = 32;
24
+ const AUTONOMOUS_REPLY_IDLE_FLUSH_MS = 100;
21
25
  class ClaudeSessionUpdatePump {
22
26
  session;
23
27
  onFailure;
28
+ autonomousEvents;
24
29
  onForegroundSettledWithBackground;
25
30
  observer;
26
31
  activeTurn;
32
+ autonomousTurns = new Map();
33
+ backgroundCompletionSignals = [];
34
+ backgroundCompletionSignalExpiry;
35
+ autonomousTurnPending = false;
27
36
  closed = false;
28
37
  failure;
29
- constructor(session, onFailure, onBackgroundTasksActiveChanged, onForegroundSettledWithBackground, targetedCancellationSupported) {
38
+ constructor(session, onFailure, onBackgroundTasksActiveChanged, autonomousEvents, onForegroundSettledWithBackground, targetedCancellationSupported) {
30
39
  this.session = session;
31
40
  this.onFailure = onFailure;
41
+ this.autonomousEvents = autonomousEvents;
32
42
  this.onForegroundSettledWithBackground = onForegroundSettledWithBackground;
33
43
  this.observer = new ClaudeBackgroundRunObserver(undefined, {
34
44
  onBackgroundTasksActiveChanged,
@@ -49,6 +59,7 @@ class ClaudeSessionUpdatePump {
49
59
  const turn = {
50
60
  queue: [],
51
61
  foregroundIsolationFence,
62
+ ownedForegroundTurn: this.resolveForegroundTurn(foregroundIsolationFence),
52
63
  };
53
64
  this.activeTurn = turn;
54
65
  this.observer.setOnProgress(onProgress);
@@ -85,9 +96,199 @@ class ClaudeSessionUpdatePump {
85
96
  }
86
97
  this.closed = true;
87
98
  this.observer.dispose();
99
+ if (this.backgroundCompletionSignalExpiry) {
100
+ clearTimeout(this.backgroundCompletionSignalExpiry);
101
+ this.backgroundCompletionSignalExpiry = undefined;
102
+ }
103
+ this.backgroundCompletionSignals.length = 0;
104
+ for (const turn of this.autonomousTurns.values()) {
105
+ if (turn.flushTimer) {
106
+ clearTimeout(turn.flushTimer);
107
+ }
108
+ if (turn.discardTimer) {
109
+ clearTimeout(turn.discardTimer);
110
+ }
111
+ }
112
+ this.autonomousTurns.clear();
113
+ this.refreshAutonomousTurnPresence();
88
114
  const turn = this.activeTurn;
89
115
  this.activeTurn = undefined;
90
- turn?.waiter?.reject(new Error('Claude ACP session update pump is closed'));
116
+ turn?.waiter?.reject(new Error('Claude ACP session update stream is closed'));
117
+ }
118
+ resolveForegroundTurn(foregroundIsolationFence) {
119
+ const nextForegroundTurn = foregroundIsolationFence === undefined ? 1 : foregroundIsolationFence + 1;
120
+ return Number.isSafeInteger(nextForegroundTurn) && nextForegroundTurn > 0
121
+ ? nextForegroundTurn
122
+ : undefined;
123
+ }
124
+ autonomousTurnFor(foregroundTurn) {
125
+ let turn = this.autonomousTurns.get(foregroundTurn);
126
+ if (turn) {
127
+ return turn;
128
+ }
129
+ turn = {
130
+ eventId: `claude-background-${foregroundTurn}`,
131
+ confirmed: false,
132
+ text: '',
133
+ };
134
+ this.autonomousTurns.set(foregroundTurn, turn);
135
+ return turn;
136
+ }
137
+ noteBackgroundCompletionSignal(foregroundTurn, eventId) {
138
+ const turn = this.autonomousTurnFor(foregroundTurn);
139
+ if (turn.discardTimer) {
140
+ clearTimeout(turn.discardTimer);
141
+ turn.discardTimer = undefined;
142
+ }
143
+ this.pruneBackgroundCompletionSignals();
144
+ const existingIndex = this.backgroundCompletionSignals.findIndex((signal) => signal.foregroundTurn === foregroundTurn);
145
+ if (existingIndex >= 0) {
146
+ this.backgroundCompletionSignals.splice(existingIndex, 1);
147
+ }
148
+ this.backgroundCompletionSignals.push({
149
+ eventId,
150
+ foregroundTurn,
151
+ expiresAt: Date.now() + BACKGROUND_COMPLETION_SIGNAL_TTL_MS,
152
+ });
153
+ if (this.backgroundCompletionSignals.length > MAX_BACKGROUND_COMPLETION_SIGNALS) {
154
+ this.backgroundCompletionSignals.splice(0, this.backgroundCompletionSignals.length - MAX_BACKGROUND_COMPLETION_SIGNALS);
155
+ }
156
+ this.scheduleBackgroundCompletionSignalExpiry();
157
+ turn.confirmed = true;
158
+ turn.eventId = eventId;
159
+ if (turn.text.trim()) {
160
+ this.scheduleAutonomousReplyFlush(foregroundTurn, turn);
161
+ }
162
+ this.refreshAutonomousTurnPresence();
163
+ return turn;
164
+ }
165
+ consumeAutonomousLifecycle(update) {
166
+ if (update.update.sessionUpdate !== 'session_info_update') {
167
+ return;
168
+ }
169
+ const provider = asObject(update.update._meta)?.claudeCode;
170
+ const lifecycle = asObject(asObject(provider)?.taskLifecycle);
171
+ if (lifecycle?.event !== 'notification' || lifecycle?.status !== 'completed') {
172
+ return;
173
+ }
174
+ const foregroundTurn = asObject(provider)?.foregroundTurn;
175
+ if (!Number.isSafeInteger(foregroundTurn) || foregroundTurn <= 0) {
176
+ return;
177
+ }
178
+ const taskId = typeof lifecycle.taskId === 'string' ? lifecycle.taskId : 'task';
179
+ this.noteBackgroundCompletionSignal(foregroundTurn, `${taskId}:completed`);
180
+ }
181
+ consumeAutonomousUpdate(update) {
182
+ if (update.update.sessionUpdate !== 'agent_message_chunk' || update.update.content.type !== 'text') {
183
+ return;
184
+ }
185
+ const provider = asObject(update.update._meta)?.claudeCode;
186
+ const foregroundTurn = asObject(provider)?.foregroundTurn;
187
+ if (!Number.isSafeInteger(foregroundTurn) || foregroundTurn <= 0) {
188
+ return;
189
+ }
190
+ const lane = asObject(provider)?.foregroundLane;
191
+ const activeForegroundTurn = this.activeTurn?.ownedForegroundTurn;
192
+ if (foregroundTurn === activeForegroundTurn) {
193
+ return;
194
+ }
195
+ if (lane !== 'background' && !this.backgroundCompletionSignals.some((signal) => signal.foregroundTurn === foregroundTurn)) {
196
+ return;
197
+ }
198
+ const turn = this.autonomousTurnFor(foregroundTurn);
199
+ turn.text += update.update.content.text;
200
+ if (turn.confirmed) {
201
+ this.scheduleAutonomousReplyFlush(foregroundTurn, turn);
202
+ }
203
+ }
204
+ scheduleAutonomousReplyFlush(foregroundTurn, turn) {
205
+ if (turn.flushTimer) {
206
+ clearTimeout(turn.flushTimer);
207
+ }
208
+ turn.flushTimer = setTimeout(() => {
209
+ turn.flushTimer = undefined;
210
+ this.flushAutonomousReply(foregroundTurn, turn);
211
+ }, AUTONOMOUS_REPLY_IDLE_FLUSH_MS);
212
+ turn.flushTimer.unref?.();
213
+ }
214
+ flushAutonomousReply(foregroundTurn, turn) {
215
+ const text = turn.text.trim();
216
+ if (turn.confirmed && text) {
217
+ this.autonomousEvents.onReply?.({
218
+ eventId: turn.eventId,
219
+ text,
220
+ });
221
+ }
222
+ else if (turn.confirmed) {
223
+ turn.discardTimer = setTimeout(() => {
224
+ turn.discardTimer = undefined;
225
+ this.discardAutonomousTurn(foregroundTurn, turn);
226
+ }, BACKGROUND_COMPLETION_SIGNAL_TTL_MS);
227
+ turn.discardTimer.unref?.();
228
+ this.refreshAutonomousTurnPresence();
229
+ return;
230
+ }
231
+ this.discardAutonomousTurn(foregroundTurn, turn);
232
+ }
233
+ discardAutonomousTurn(foregroundTurn, turn) {
234
+ if (turn.flushTimer) {
235
+ clearTimeout(turn.flushTimer);
236
+ turn.flushTimer = undefined;
237
+ }
238
+ if (turn.discardTimer) {
239
+ clearTimeout(turn.discardTimer);
240
+ turn.discardTimer = undefined;
241
+ }
242
+ if (this.autonomousTurns.get(foregroundTurn) === turn) {
243
+ this.autonomousTurns.delete(foregroundTurn);
244
+ }
245
+ const signalIndex = this.backgroundCompletionSignals.findIndex((signal) => signal.foregroundTurn === foregroundTurn);
246
+ if (signalIndex >= 0) {
247
+ this.backgroundCompletionSignals.splice(signalIndex, 1);
248
+ this.scheduleBackgroundCompletionSignalExpiry();
249
+ return;
250
+ }
251
+ this.refreshAutonomousTurnPresence();
252
+ }
253
+ pruneBackgroundCompletionSignals() {
254
+ const now = Date.now();
255
+ while (this.backgroundCompletionSignals.length > 0
256
+ && this.backgroundCompletionSignals[0].expiresAt <= now) {
257
+ const expired = this.backgroundCompletionSignals.shift();
258
+ if (!expired) {
259
+ continue;
260
+ }
261
+ const turn = this.autonomousTurns.get(expired.foregroundTurn);
262
+ if (turn && !turn.text.trim()) {
263
+ this.discardAutonomousTurn(expired.foregroundTurn, turn);
264
+ }
265
+ }
266
+ }
267
+ scheduleBackgroundCompletionSignalExpiry() {
268
+ if (this.backgroundCompletionSignalExpiry) {
269
+ clearTimeout(this.backgroundCompletionSignalExpiry);
270
+ this.backgroundCompletionSignalExpiry = undefined;
271
+ }
272
+ this.pruneBackgroundCompletionSignals();
273
+ const next = this.backgroundCompletionSignals[0];
274
+ if (!next) {
275
+ this.refreshAutonomousTurnPresence();
276
+ return;
277
+ }
278
+ this.backgroundCompletionSignalExpiry = setTimeout(() => {
279
+ this.backgroundCompletionSignalExpiry = undefined;
280
+ this.pruneBackgroundCompletionSignals();
281
+ this.scheduleBackgroundCompletionSignalExpiry();
282
+ }, Math.max(0, next.expiresAt - Date.now()));
283
+ this.backgroundCompletionSignalExpiry.unref?.();
284
+ }
285
+ refreshAutonomousTurnPresence() {
286
+ const pending = this.autonomousTurns.size > 0 || this.backgroundCompletionSignals.length > 0;
287
+ if (pending === this.autonomousTurnPending) {
288
+ return;
289
+ }
290
+ this.autonomousTurnPending = pending;
291
+ this.autonomousEvents.onPendingChanged?.(pending);
91
292
  }
92
293
  async run() {
93
294
  try {
@@ -98,8 +299,12 @@ class ClaudeSessionUpdatePump {
98
299
  }
99
300
  const lifecycle = update.kind === 'session_update' && this.observer.consume(update.update);
100
301
  if (lifecycle) {
302
+ this.consumeAutonomousLifecycle(update);
101
303
  continue;
102
304
  }
305
+ if (update.kind === 'session_update') {
306
+ this.consumeAutonomousUpdate(update);
307
+ }
103
308
  if (update.kind === 'session_update'
104
309
  && isClaudeUpdateFromIsolatedTurn(update.update, this.activeTurn?.foregroundIsolationFence)) {
105
310
  continue;
@@ -731,6 +936,7 @@ export class ClaudeCliClient {
731
936
  sessionStoreWriteQueue = Promise.resolve();
732
937
  sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
733
938
  targetedBackgroundRunCancellationAvailable = false;
939
+ autonomousReplyHandler;
734
940
  idleBackendShutdown;
735
941
  childStderr = '';
736
942
  constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), imageInputConfig = {}) {
@@ -759,6 +965,9 @@ export class ClaudeCliClient {
759
965
  });
760
966
  void this.fatalPromise.catch(() => { });
761
967
  }
968
+ onAutonomousReply(handler) {
969
+ this.autonomousReplyHandler = handler;
970
+ }
762
971
  async generateReply(channelIdOrTurn, promptOrOptions, maybeOptions) {
763
972
  if (this.fatalError) {
764
973
  throw this.fatalError;
@@ -790,6 +999,7 @@ export class ClaudeCliClient {
790
999
  async dispose() {
791
1000
  const error = new Error('Claude ACP backend stopped');
792
1001
  this.disposing = true;
1002
+ this.autonomousReplyHandler = undefined;
793
1003
  this.idleBackendShutdown.cancel();
794
1004
  this.logger?.debug('stopping Claude ACP backend');
795
1005
  const closed = this.connection?.closed ?? Promise.resolve();
@@ -936,6 +1146,7 @@ export class ClaudeCliClient {
936
1146
  sessionPersistence,
937
1147
  backgroundTasksActive: false,
938
1148
  backgroundContinuationPending: false,
1149
+ sessionHasPendingAutonomousTurn: false,
939
1150
  persistentFollowUpAdopted: false,
940
1151
  processing: false,
941
1152
  queue: [],
@@ -1170,8 +1381,10 @@ export class ClaudeCliClient {
1170
1381
  && state.sessionVisibilityKey === state.visibilityKey) {
1171
1382
  return;
1172
1383
  }
1173
- if (state.backgroundTasksActive || state.backgroundContinuationPending) {
1174
- throw new Error('Claude session scope cannot change while provider-owned background work is running');
1384
+ if (state.backgroundTasksActive
1385
+ || state.backgroundContinuationPending
1386
+ || state.sessionHasPendingAutonomousTurn) {
1387
+ throw new Error('Claude session scope cannot change while provider-owned background delivery is running');
1175
1388
  }
1176
1389
  this.logger?.debug('recycling Claude ACP session after session scope changed', {
1177
1390
  channelId,
@@ -1183,6 +1396,7 @@ export class ClaudeCliClient {
1183
1396
  state.session = undefined;
1184
1397
  state.backgroundTasksActive = false;
1185
1398
  state.backgroundContinuationPending = false;
1399
+ state.sessionHasPendingAutonomousTurn = false;
1186
1400
  state.foregroundIsolationFence = undefined;
1187
1401
  state.sessionCwd = undefined;
1188
1402
  state.sessionVisibilityKey = undefined;
@@ -1219,6 +1433,40 @@ export class ClaudeCliClient {
1219
1433
  }
1220
1434
  this.reconcileIdleChannelState(state);
1221
1435
  this.idleBackendShutdown.reconcile();
1436
+ }, {
1437
+ onReply: (reply) => {
1438
+ const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
1439
+ if (!binding) {
1440
+ return;
1441
+ }
1442
+ const [, state] = binding;
1443
+ if (state.session !== session) {
1444
+ return;
1445
+ }
1446
+ this.autonomousReplyHandler?.({
1447
+ channelId: state.channelId,
1448
+ eventId: reply.eventId,
1449
+ text: reply.text,
1450
+ });
1451
+ },
1452
+ onPendingChanged: (active) => {
1453
+ const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
1454
+ if (!binding) {
1455
+ return;
1456
+ }
1457
+ const [, state] = binding;
1458
+ if (state.session !== session) {
1459
+ return;
1460
+ }
1461
+ state.sessionHasPendingAutonomousTurn = active;
1462
+ if (active) {
1463
+ this.promoteWorkerRoute(state, session);
1464
+ this.idleBackendShutdown.cancel();
1465
+ return;
1466
+ }
1467
+ this.reconcileIdleChannelState(state);
1468
+ this.idleBackendShutdown.reconcile();
1469
+ },
1222
1470
  }, (foregroundTurn) => {
1223
1471
  const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
1224
1472
  if (!binding) {
@@ -1259,6 +1507,9 @@ export class ClaudeCliClient {
1259
1507
  catch (error) {
1260
1508
  throw markSessionTainted(error);
1261
1509
  }
1510
+ const usage = normalizeAcpPromptUsage(response.usage);
1511
+ if (usage)
1512
+ options?.onUsage?.(usage);
1262
1513
  const output = collector.getFinalText();
1263
1514
  if (response.stopReason === 'cancelled') {
1264
1515
  throw new ProviderTurnCancelledError('Claude ACP turn cancelled', {
@@ -1270,12 +1521,6 @@ export class ClaudeCliClient {
1270
1521
  }
1271
1522
  return output;
1272
1523
  }
1273
- const settledForegroundTurn = update.kind === 'session_update'
1274
- ? readClaudeForegroundSettledTurn(update.update)
1275
- : undefined;
1276
- if (settledForegroundTurn !== undefined) {
1277
- return collector.getFinalText();
1278
- }
1279
1524
  if (update.update.sessionUpdate === 'agent_message_chunk' &&
1280
1525
  update.update.content.type === 'text') {
1281
1526
  compactionObserver.consumeAnswerChunk(update.update.content.text);
@@ -1315,11 +1560,13 @@ export class ClaudeCliClient {
1315
1560
  state.session = undefined;
1316
1561
  state.backgroundTasksActive = false;
1317
1562
  state.backgroundContinuationPending = false;
1563
+ state.sessionHasPendingAutonomousTurn = false;
1318
1564
  state.foregroundIsolationFence = undefined;
1319
1565
  state.sessionCwd = undefined;
1320
1566
  state.sessionVisibilityKey = undefined;
1321
1567
  state.sessionPromise = undefined;
1322
1568
  state.activeTurn = undefined;
1569
+ state.persistentFollowUpAdopted = false;
1323
1570
  if (!state.processing) {
1324
1571
  this.channels.delete(channelId);
1325
1572
  }
@@ -1346,9 +1593,11 @@ export class ClaudeCliClient {
1346
1593
  state.session = undefined;
1347
1594
  state.backgroundTasksActive = false;
1348
1595
  state.backgroundContinuationPending = false;
1596
+ state.sessionHasPendingAutonomousTurn = false;
1349
1597
  state.foregroundIsolationFence = undefined;
1350
1598
  state.sessionCwd = undefined;
1351
1599
  state.sessionVisibilityKey = undefined;
1600
+ state.persistentFollowUpAdopted = false;
1352
1601
  }
1353
1602
  this.logger?.debug('discarding tainted Claude ACP session', { channelId });
1354
1603
  this.closeSession(session);
@@ -1370,7 +1619,7 @@ export class ClaudeCliClient {
1370
1619
  if (route?.state !== state
1371
1620
  || route.phase !== 'promoted'
1372
1621
  || state.session !== session
1373
- || (!state.backgroundTasksActive && !state.persistentFollowUpAdopted)) {
1622
+ || (!state.backgroundTasksActive && !state.sessionHasPendingAutonomousTurn && !state.persistentFollowUpAdopted)) {
1374
1623
  return;
1375
1624
  }
1376
1625
  await this.persistSessionBestEffort(state.channelId, session.sessionId, state.visibilityKey, state.sessionCwd ?? state.cwd ?? this.runtime.cwd);
@@ -1408,7 +1657,8 @@ export class ClaudeCliClient {
1408
1657
  if (route?.state === state
1409
1658
  && route.phase === 'promoted'
1410
1659
  && !state.backgroundTasksActive
1411
- && !state.backgroundContinuationPending) {
1660
+ && !state.backgroundContinuationPending
1661
+ && !state.sessionHasPendingAutonomousTurn) {
1412
1662
  if (state.routingKey === state.channelId) {
1413
1663
  this.releaseWorkerRoute(state);
1414
1664
  }
@@ -1425,7 +1675,8 @@ export class ClaudeCliClient {
1425
1675
  }
1426
1676
  if (state.sessionPersistence !== 'ephemeral'
1427
1677
  || state.backgroundTasksActive
1428
- || state.backgroundContinuationPending) {
1678
+ || state.backgroundContinuationPending
1679
+ || state.sessionHasPendingAutonomousTurn) {
1429
1680
  return;
1430
1681
  }
1431
1682
  const session = state.session;
@@ -1633,9 +1884,11 @@ export class ClaudeCliClient {
1633
1884
  state.session = undefined;
1634
1885
  state.backgroundTasksActive = false;
1635
1886
  state.backgroundContinuationPending = false;
1887
+ state.sessionHasPendingAutonomousTurn = false;
1636
1888
  state.foregroundIsolationFence = undefined;
1637
1889
  state.sessionCwd = undefined;
1638
1890
  state.sessionVisibilityKey = undefined;
1891
+ state.persistentFollowUpAdopted = false;
1639
1892
  this.channels.delete(channelId);
1640
1893
  }
1641
1894
  this.channelWorkerRoutes.clear();
@@ -1670,6 +1923,7 @@ export class ClaudeCliClient {
1670
1923
  if (state.activeTurn ||
1671
1924
  state.backgroundTasksActive ||
1672
1925
  state.backgroundContinuationPending ||
1926
+ state.sessionHasPendingAutonomousTurn ||
1673
1927
  state.queue.length > 0 ||
1674
1928
  state.sessionPromise ||
1675
1929
  state.processing) {
@@ -27,6 +27,7 @@ export class CodexProviderAdapter {
27
27
  const preparedTurn = await this.turnPreparer.prepare(input);
28
28
  const text = await this.cli.generateReply(preparedTurn, {
29
29
  onProgress: createAwaitingUserProgressHandler(options?.onProgress),
30
+ ...(options?.onUsage ? { onUsage: options.onUsage } : {}),
30
31
  });
31
32
  // Read the session AFTER the turn: a first turn on a channel opens the
32
33
  // session as it runs, so before this point there is nothing to report.
@@ -6,6 +6,7 @@ import spawn from 'cross-spawn';
6
6
  import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
7
7
  import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
8
8
  import { AcpProgressCollector } from '../acp-progress-collector.js';
9
+ import { normalizeAcpPromptUsage } from '../prompt-usage.js';
9
10
  import { ProviderTurnCancelledError } from '../provider-adapter.js';
10
11
  import { assertCodexProjectDocumentSize, buildCodexProjectDocument } from './project-doc.js';
11
12
  import { isGatewayCredentialSidecarBasename } from '../../context/injection.js';
@@ -850,6 +851,10 @@ export class CodexCliClient {
850
851
  catch (error) {
851
852
  throw markSessionTainted(error);
852
853
  }
854
+ const usage = normalizeAcpPromptUsage(response.usage);
855
+ if (usage) {
856
+ options?.onUsage?.(usage);
857
+ }
853
858
  const output = collector.getFinalText();
854
859
  if (response.stopReason === 'cancelled') {
855
860
  throw new ProviderTurnCancelledError('Codex ACP turn cancelled', {
@@ -27,6 +27,10 @@ export class CopilotProviderAdapter {
27
27
  const preparedTurn = await this.turnPreparer.prepare(input);
28
28
  const text = await this.cli.generateReply(preparedTurn, {
29
29
  onProgress: createAwaitingUserProgressHandler(options?.onProgress),
30
+ ...(options?.onUsage ? { onUsage: options.onUsage } : {}),
31
+ ...(options?.onExecutionContinuation
32
+ ? { onExecutionContinuation: options.onExecutionContinuation }
33
+ : {}),
30
34
  });
31
35
  // Read the session AFTER the turn: a first turn on a channel opens the
32
36
  // session as it runs, so before this point there is nothing to report.
@@ -4,6 +4,7 @@ import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientpr
4
4
  import { toCopilotBackgroundActivity } from './activity-metadata.js';
5
5
  import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
6
6
  import { AcpProgressCollector, } from '../acp-progress-collector.js';
7
+ import { normalizeAcpPromptUsage } from '../prompt-usage.js';
7
8
  import { ProviderTurnCancelledError } from '../provider-adapter.js';
8
9
  import { resolveCopilotPermissionResponse, resolveCopilotSdkPermissionResponse, } from '../../policy/copilot-permission.js';
9
10
  import { isDiscussionOnlyResolvedWorkingFolder } from '../../context/resolved-working-folder.js';
@@ -1001,7 +1002,12 @@ export class CopilotCliClient {
1001
1002
  this.closeSession(session);
1002
1003
  }
1003
1004
  async runTurn(session, prompt, options, onBackgroundAgentStarted) {
1004
- const promptPromise = this.raceWithFatal(session.prompt(prompt));
1005
+ const promptPromise = this.raceWithFatal(isCopilotSdkSession(session)
1006
+ ? session.prompt(prompt, {
1007
+ onUsage: options?.onUsage,
1008
+ onExecutionContinuation: options?.onExecutionContinuation,
1009
+ })
1010
+ : session.prompt(prompt));
1005
1011
  const promptFailure = new Promise((_, reject) => {
1006
1012
  void promptPromise.catch((error) => reject(markSessionTainted(error)));
1007
1013
  });
@@ -1042,6 +1048,10 @@ export class CopilotCliClient {
1042
1048
  catch (error) {
1043
1049
  throw markSessionTainted(error);
1044
1050
  }
1051
+ const usage = normalizeAcpPromptUsage(response.usage);
1052
+ if (usage) {
1053
+ options?.onUsage?.(usage);
1054
+ }
1045
1055
  if (response.stopReason === 'cancelled') {
1046
1056
  throw new ProviderTurnCancelledError('Copilot turn cancelled');
1047
1057
  }