@borgee/agents-host 0.2.97 → 0.2.110

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 (104) hide show
  1. package/README.md +77 -5
  2. package/dist/agents-host.d.ts +16 -0
  3. package/dist/agents-host.js +608 -82
  4. package/dist/catalog-worker.js +21 -0
  5. package/dist/catalog-worker.js.map +7 -0
  6. package/dist/chat/chat-control-plane.d.ts +19 -4
  7. package/dist/chat/normalized-input.d.ts +9 -0
  8. package/dist/chat/normalized-input.js +24 -0
  9. package/dist/chat/sdk-chat-control-plane.d.ts +46 -6
  10. package/dist/chat/sdk-chat-control-plane.js +169 -6
  11. package/dist/cli-args.d.ts +1 -1
  12. package/dist/cli-args.js +13 -4
  13. package/dist/config.d.ts +3 -1
  14. package/dist/config.js +25 -0
  15. package/dist/context/injection.d.ts +16 -0
  16. package/dist/context/injection.js +37 -21
  17. package/dist/context/prompt.d.ts +12 -0
  18. package/dist/context/prompt.js +25 -0
  19. package/dist/context/skill-manual.js +1 -1
  20. package/dist/context/turn-preparation.d.ts +2 -1
  21. package/dist/context/turn-preparation.js +15 -0
  22. package/dist/execution-telemetry.d.ts +96 -0
  23. package/dist/execution-telemetry.js +583 -0
  24. package/dist/gateway/channel-file-workspace.d.ts +15 -0
  25. package/dist/gateway/channel-file-workspace.js +130 -0
  26. package/dist/gateway/localhost-gateway.js +167 -2
  27. package/dist/hosted-turn-content.js +3 -2
  28. package/dist/local-config.js +34 -1
  29. package/dist/managed-daemon.js +15 -0
  30. package/dist/native-environment.d.ts +2 -0
  31. package/dist/native-environment.js +9 -0
  32. package/dist/native-protocol-types/codec.d.ts +49 -0
  33. package/dist/native-protocol-types/commands.d.ts +71 -0
  34. package/dist/native-protocol-types/compatibility.d.ts +5 -0
  35. package/dist/native-protocol-types/cursor.d.ts +7 -0
  36. package/dist/native-protocol-types/envelope.d.ts +1105 -0
  37. package/dist/native-protocol-types/file-changes.d.ts +20 -0
  38. package/dist/native-protocol-types/history.d.ts +487 -0
  39. package/dist/native-protocol-types/index.d.ts +18 -0
  40. package/dist/native-protocol-types/interactions.d.ts +619 -0
  41. package/dist/native-protocol-types/messages.d.ts +2482 -0
  42. package/dist/native-protocol-types/public-validation.d.ts +10 -0
  43. package/dist/native-protocol-types/remote-host-uplink.d.ts +67 -0
  44. package/dist/native-protocol-types/resources.d.ts +126 -0
  45. package/dist/native-protocol-types/session-settings.d.ts +22 -0
  46. package/dist/native-protocol-types/snapshot.d.ts +575 -0
  47. package/dist/native-protocol-types/timeline.d.ts +1503 -0
  48. package/dist/native-protocol-types/tool-result.d.ts +26 -0
  49. package/dist/native-protocol-types/uplink.d.ts +56 -0
  50. package/dist/native-protocol-types/version.d.ts +5 -0
  51. package/dist/native-provider-types/commands.d.ts +51 -0
  52. package/dist/native-provider-types/control.d.ts +192 -0
  53. package/dist/native-provider-types/file-changes.d.ts +9 -0
  54. package/dist/native-provider-types/index.d.ts +8 -0
  55. package/dist/native-provider-types/interactions.d.ts +7 -0
  56. package/dist/native-provider-types/observation.d.ts +209 -0
  57. package/dist/native-provider-types/provider.d.ts +124 -0
  58. package/dist/native-provider-types/session-settings.d.ts +17 -0
  59. package/dist/native-provider-types/testing.d.ts +27 -0
  60. package/dist/native-provider-types/tool-result.d.ts +21 -0
  61. package/dist/native-providers.d.ts +21 -0
  62. package/dist/native-providers.js +18402 -0
  63. package/dist/native-providers.js.map +7 -0
  64. package/dist/plugin-sdk.js +10713 -229
  65. package/dist/plugin-sdk.js.map +4 -4
  66. package/dist/policy/copilot-permission.js +1 -8
  67. package/dist/policy/gateway-authorization.d.ts +1 -1
  68. package/dist/policy/gateway-authorization.js +22 -2
  69. package/dist/providers/claude/adapter.js +1 -0
  70. package/dist/providers/claude/cli-client.js +4 -0
  71. package/dist/providers/codex/adapter.js +1 -0
  72. package/dist/providers/codex/cli-client.js +5 -0
  73. package/dist/providers/copilot/adapter.js +4 -0
  74. package/dist/providers/copilot/cli-client.js +11 -1
  75. package/dist/providers/copilot/sdk-session.d.ts +12 -3
  76. package/dist/providers/copilot/sdk-session.js +230 -6
  77. package/dist/providers/create-provider.js +24 -0
  78. package/dist/providers/normalized/adapter.d.ts +107 -0
  79. package/dist/providers/normalized/adapter.js +1650 -0
  80. package/dist/providers/normalized/control-policy.d.ts +5 -0
  81. package/dist/providers/normalized/control-policy.js +12 -0
  82. package/dist/providers/normalized/interactions.d.ts +3 -0
  83. package/dist/providers/normalized/interactions.js +39 -0
  84. package/dist/providers/normalized/legacy-input-ownership.d.ts +34 -0
  85. package/dist/providers/normalized/legacy-input-ownership.js +123 -0
  86. package/dist/providers/normalized/session-management.d.ts +67 -0
  87. package/dist/providers/normalized/session-management.js +334 -0
  88. package/dist/providers/normalized/session-ownership.d.ts +3 -0
  89. package/dist/providers/normalized/session-ownership.js +90 -0
  90. package/dist/providers/prompt-usage.d.ts +8 -0
  91. package/dist/providers/prompt-usage.js +52 -0
  92. package/dist/providers/provider-adapter.d.ts +43 -1
  93. package/dist/providers/provider-adapter.js +8 -0
  94. package/dist/state-paths.d.ts +2 -0
  95. package/dist/state-paths.js +6 -0
  96. package/dist/types.d.ts +66 -0
  97. package/dist/typing-lease.d.ts +14 -0
  98. package/dist/typing-lease.js +31 -0
  99. package/dist/vendor/agent-provider-codex/LICENSE +211 -0
  100. package/dist/vendor/agent-provider-codex/NOTICE +13 -0
  101. package/package.json +22 -13
  102. package/skills/borgee-agent/SKILL.md +12 -4
  103. package/skills/borgee-agent/scripts/borgee-agent.mjs +110 -0
  104. package/skills/borgee-agent/scripts/borgee-agent.py +116 -0
@@ -1,6 +1,5 @@
1
1
  import { summarizeError } from '../debug.js';
2
2
  const LEGACY_MISSING_ALLOW_OPTION_MESSAGE = 'Copilot ACP requested permission but did not offer an allow option';
3
- const SDK_APPROVE_FOR_SESSION_OPTION_ID = 'sdk-approve-for-session';
4
3
  const SDK_APPROVE_ONCE_OPTION_ID = 'sdk-approve-once';
5
4
  const SDK_REJECT_OPTION_ID = 'sdk-reject';
6
5
  function legacySelectPermissionOption(options) {
@@ -279,12 +278,8 @@ export function resolveCopilotSdkPermissionResponse(context) {
279
278
  feedback: 'Managed policy requires interactive approval, which this hosted runtime cannot provide.',
280
279
  };
281
280
  }
281
+ // Session grants bypass subsequent host policy checks; approve each SDK request independently.
282
282
  const options = [
283
- {
284
- optionId: SDK_APPROVE_FOR_SESSION_OPTION_ID,
285
- name: 'Allow for this session',
286
- kind: 'allow_always',
287
- },
288
283
  {
289
284
  optionId: SDK_APPROVE_ONCE_OPTION_ID,
290
285
  name: 'Allow once',
@@ -312,8 +307,6 @@ export function resolveCopilotSdkPermissionResponse(context) {
312
307
  return { kind: 'reject' };
313
308
  }
314
309
  switch (response.outcome.optionId) {
315
- case SDK_APPROVE_FOR_SESSION_OPTION_ID:
316
- return { kind: 'approve-for-session' };
317
310
  case SDK_APPROVE_ONCE_OPTION_ID:
318
311
  return { kind: 'approve-once' };
319
312
  default:
@@ -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':
@@ -55,6 +55,7 @@ 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.
@@ -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';
@@ -1506,6 +1507,9 @@ export class ClaudeCliClient {
1506
1507
  catch (error) {
1507
1508
  throw markSessionTainted(error);
1508
1509
  }
1510
+ const usage = normalizeAcpPromptUsage(response.usage);
1511
+ if (usage)
1512
+ options?.onUsage?.(usage);
1509
1513
  const output = collector.getFinalText();
1510
1514
  if (response.stopReason === 'cancelled') {
1511
1515
  throw new ProviderTurnCancelledError('Claude ACP turn cancelled', {
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  import type { ActiveSession, ContentBlock } from '@agentclientprotocol/sdk';
2
2
  import { type MessageOptions, type PermissionHandler, type ResumeSessionConfig, type SessionConfig, type SessionEventHandler } from '@github/copilot-sdk';
3
- import type { ProviderBackgroundRunProgress } from '../../types.js';
3
+ import type { ProviderBackgroundRunProgress, ProviderGenerateOptions } from '../../types.js';
4
4
  type CopilotSessionUpdate = Awaited<ReturnType<ActiveSession['nextUpdate']>>;
5
5
  type CopilotPromptResponse = Awaited<ReturnType<ActiveSession['prompt']>>;
6
6
  interface CopilotSdkTaskInfo {
@@ -77,6 +77,7 @@ export declare class CopilotSdkSessionAdapter {
77
77
  private readonly onAutonomousReply;
78
78
  private readonly onAutonomousTurnPresenceChanged;
79
79
  private readonly onBackgroundTaskExecutionsChanged;
80
+ private readonly backgroundUsageOwnerTimeoutMs;
80
81
  readonly transport = "sdk";
81
82
  private readonly pendingUpdates;
82
83
  private readonly updateWaiters;
@@ -98,11 +99,12 @@ export declare class CopilotSdkSessionAdapter {
98
99
  private readonly backgroundTaskExecutions;
99
100
  private backgroundTaskExecutionsDirty;
100
101
  private readonly backgroundWaits;
102
+ private readonly backgroundUsageOwners;
101
103
  private lastSyntheticExecutionStartedAt;
102
104
  private closePromise?;
103
- constructor(session: CopilotSdkSessionLike, onBackgroundRun: (run: ProviderBackgroundRunProgress) => void, onBackgroundAgentPresenceChanged: (active: boolean) => void, onBackgroundRunError: (error: Error) => void, onAutonomousReply: (reply: CopilotAutonomousReply) => void, onAutonomousTurnPresenceChanged: (active: boolean) => void, initialBackgroundTaskExecutions?: Readonly<Record<string, CopilotBackgroundExecutionState>>, onBackgroundTaskExecutionsChanged?: (executions: Readonly<Record<string, CopilotBackgroundExecutionState>>) => Promise<void>);
105
+ constructor(session: CopilotSdkSessionLike, onBackgroundRun: (run: ProviderBackgroundRunProgress) => void, onBackgroundAgentPresenceChanged: (active: boolean) => void, onBackgroundRunError: (error: Error) => void, onAutonomousReply: (reply: CopilotAutonomousReply) => void, onAutonomousTurnPresenceChanged: (active: boolean) => void, initialBackgroundTaskExecutions?: Readonly<Record<string, CopilotBackgroundExecutionState>>, onBackgroundTaskExecutionsChanged?: (executions: Readonly<Record<string, CopilotBackgroundExecutionState>>) => Promise<void>, backgroundUsageOwnerTimeoutMs?: number);
104
106
  get sessionId(): string;
105
- prompt(input: string | ContentBlock[]): Promise<CopilotPromptResponse>;
107
+ prompt(input: string | ContentBlock[], telemetry?: Pick<ProviderGenerateOptions, 'onExecutionContinuation' | 'onUsage'>): Promise<CopilotPromptResponse>;
106
108
  nextUpdate(): Promise<CopilotSessionUpdate>;
107
109
  interruptMainTurn(): Promise<boolean>;
108
110
  cancelAllBackgroundWork(): Promise<boolean>;
@@ -139,6 +141,13 @@ export declare class CopilotSdkSessionAdapter {
139
141
  private resolveBackgroundWait;
140
142
  private backgroundWaitFor;
141
143
  private handleOwnedTurnEvent;
144
+ private recordTurnUsage;
145
+ private deliverAccumulatedTurnUsage;
146
+ private recordBackgroundUsage;
147
+ private settleBackgroundUsageOwner;
148
+ private settleBackgroundUsageGroup;
149
+ private updateBackgroundUsageRegistryState;
150
+ private refreshMissingBackgroundTerminalGrace;
142
151
  private interruptAttachedTurn;
143
152
  private finishActiveTurn;
144
153
  private resolveEventTurnId;
@@ -1,6 +1,7 @@
1
1
  import { accessSync, constants as fsConstants, existsSync, statSync } from 'node:fs';
2
2
  import { delimiter, extname, isAbsolute, resolve } from 'node:path';
3
3
  import { CopilotClient, RuntimeConnection, } from '@github/copilot-sdk';
4
+ import { normalizeProviderCallUsage } from '../prompt-usage.js';
4
5
  function isExecutableFile(path) {
5
6
  if (!existsSync(path) || !statSync(path).isFile()) {
6
7
  return false;
@@ -79,6 +80,15 @@ export function createCopilotSdkSessionConfig(cwd, onPermissionRequest) {
79
80
  onPermissionRequest,
80
81
  };
81
82
  }
83
+ const DEFAULT_BACKGROUND_USAGE_OWNER_TIMEOUT_MS = 30 * 60 * 1_000;
84
+ function addUsage(current, next) {
85
+ return {
86
+ inputTokens: (current?.inputTokens ?? 0) + next.inputTokens,
87
+ outputTokens: (current?.outputTokens ?? 0) + next.outputTokens,
88
+ cacheReadTokens: (current?.cacheReadTokens ?? 0) + next.cacheReadTokens,
89
+ cacheWriteTokens: (current?.cacheWriteTokens ?? 0) + next.cacheWriteTokens,
90
+ };
91
+ }
82
92
  const BACKGROUND_COMPLETION_SIGNAL_TTL_MS = 30_000;
83
93
  const MAX_BACKGROUND_COMPLETION_SIGNALS = 32;
84
94
  function normalizeError(error) {
@@ -193,6 +203,7 @@ export class CopilotSdkSessionAdapter {
193
203
  onAutonomousReply;
194
204
  onAutonomousTurnPresenceChanged;
195
205
  onBackgroundTaskExecutionsChanged;
206
+ backgroundUsageOwnerTimeoutMs;
196
207
  transport = 'sdk';
197
208
  pendingUpdates = [];
198
209
  updateWaiters = [];
@@ -214,9 +225,10 @@ export class CopilotSdkSessionAdapter {
214
225
  backgroundTaskExecutions;
215
226
  backgroundTaskExecutionsDirty = false;
216
227
  backgroundWaits = new Map();
228
+ backgroundUsageOwners = new Map();
217
229
  lastSyntheticExecutionStartedAt = 0;
218
230
  closePromise;
219
- constructor(session, onBackgroundRun, onBackgroundAgentPresenceChanged, onBackgroundRunError, onAutonomousReply, onAutonomousTurnPresenceChanged, initialBackgroundTaskExecutions = {}, onBackgroundTaskExecutionsChanged = async () => { }) {
231
+ constructor(session, onBackgroundRun, onBackgroundAgentPresenceChanged, onBackgroundRunError, onAutonomousReply, onAutonomousTurnPresenceChanged, initialBackgroundTaskExecutions = {}, onBackgroundTaskExecutionsChanged = async () => { }, backgroundUsageOwnerTimeoutMs = DEFAULT_BACKGROUND_USAGE_OWNER_TIMEOUT_MS) {
220
232
  this.session = session;
221
233
  this.onBackgroundRun = onBackgroundRun;
222
234
  this.onBackgroundAgentPresenceChanged = onBackgroundAgentPresenceChanged;
@@ -224,6 +236,7 @@ export class CopilotSdkSessionAdapter {
224
236
  this.onAutonomousReply = onAutonomousReply;
225
237
  this.onAutonomousTurnPresenceChanged = onAutonomousTurnPresenceChanged;
226
238
  this.onBackgroundTaskExecutionsChanged = onBackgroundTaskExecutionsChanged;
239
+ this.backgroundUsageOwnerTimeoutMs = backgroundUsageOwnerTimeoutMs;
227
240
  this.backgroundTaskExecutions = new Map(Object.entries(initialBackgroundTaskExecutions).map(([taskId, execution]) => [
228
241
  taskId,
229
242
  { ...execution },
@@ -233,7 +246,7 @@ export class CopilotSdkSessionAdapter {
233
246
  get sessionId() {
234
247
  return this.session.sessionId;
235
248
  }
236
- async prompt(input) {
249
+ async prompt(input, telemetry = {}) {
237
250
  if (this.closePromise) {
238
251
  throw new Error('Copilot SDK session is closed');
239
252
  }
@@ -249,7 +262,13 @@ export class CopilotSdkSessionAdapter {
249
262
  const turn = {
250
263
  messageIdsWithDeltas: new Set(),
251
264
  ownedTurnIds: new Set(),
265
+ usageKeys: new Set(),
266
+ backgroundAgentIds: new Set(),
267
+ backgroundSucceeded: true,
268
+ onUsage: telemetry.onUsage,
269
+ onExecutionContinuation: telemetry.onExecutionContinuation,
252
270
  userMessageObserved: false,
271
+ usageDelivered: false,
253
272
  aborted: false,
254
273
  cancelRequested: false,
255
274
  resolve: resolveTurn,
@@ -263,6 +282,7 @@ export class CopilotSdkSessionAdapter {
263
282
  if (this.activeTurn === turn) {
264
283
  this.activeTurn = undefined;
265
284
  }
285
+ this.deliverAccumulatedTurnUsage(turn);
266
286
  turn.reject(normalizeError(error));
267
287
  }
268
288
  return completion;
@@ -338,9 +358,16 @@ export class CopilotSdkSessionAdapter {
338
358
  this.autonomousTurnIdByRuntimeTurnId.clear();
339
359
  this.autonomousTurnIdByInteractionId.clear();
340
360
  this.refreshAutonomousTurnPresence();
361
+ for (const owner of new Set(this.backgroundUsageOwners.values())) {
362
+ this.settleBackgroundUsageGroup(owner, false);
363
+ }
364
+ this.backgroundUsageOwners.clear();
341
365
  this.closePromise = (async () => {
342
366
  this.unsubscribe();
343
- this.activeTurn?.reject(reason ?? new Error('Copilot SDK session closed'));
367
+ if (this.activeTurn) {
368
+ this.deliverAccumulatedTurnUsage(this.activeTurn);
369
+ this.activeTurn.reject(reason ?? new Error('Copilot SDK session closed'));
370
+ }
344
371
  this.activeTurn = undefined;
345
372
  for (const waiter of this.updateWaiters.splice(0)) {
346
373
  waiter.reject(reason ?? new Error('Copilot SDK session closed'));
@@ -398,10 +425,11 @@ export class CopilotSdkSessionAdapter {
398
425
  }
399
426
  const fingerprint = JSON.stringify(run);
400
427
  const previous = this.reportedBackgroundTasks.get(run.providerRunId);
428
+ this.reportedBackgroundTasks.set(run.providerRunId, run);
429
+ this.updateBackgroundUsageRegistryState(run.providerRunId, run.state === 'running' || run.state === 'waiting');
401
430
  if (previous && JSON.stringify(previous) === fingerprint) {
402
431
  continue;
403
432
  }
404
- this.reportedBackgroundTasks.set(run.providerRunId, run);
405
433
  this.onBackgroundRun(run);
406
434
  }
407
435
  for (const [providerRunId, previous] of this.reportedBackgroundTasks) {
@@ -411,6 +439,10 @@ export class CopilotSdkSessionAdapter {
411
439
  this.backgroundTaskExecutionsDirty = true;
412
440
  }
413
441
  this.backgroundWaits.delete(providerRunId);
442
+ if (this.activeTurn?.backgroundAgentIds.delete(providerRunId)) {
443
+ this.activeTurn.backgroundSucceeded = false;
444
+ }
445
+ this.refreshMissingBackgroundTerminalGrace(providerRunId);
414
446
  if (previous.state === 'running' || previous.state === 'waiting') {
415
447
  this.onBackgroundRun({
416
448
  ...previous,
@@ -666,7 +698,46 @@ export class CopilotSdkSessionAdapter {
666
698
  this.noteBackgroundCompletion(event);
667
699
  return;
668
700
  }
701
+ if (event.type === 'session.error') {
702
+ if (event.agentId) {
703
+ if (this.activeTurn?.backgroundAgentIds.delete(event.agentId)) {
704
+ this.activeTurn.backgroundSucceeded = false;
705
+ }
706
+ this.settleBackgroundUsageOwner(event.agentId, false);
707
+ }
708
+ else {
709
+ for (const owner of new Set(this.backgroundUsageOwners.values())) {
710
+ this.settleBackgroundUsageGroup(owner, false);
711
+ }
712
+ }
713
+ }
669
714
  if (event.agentId) {
715
+ if (event.type === 'subagent.started' && this.activeTurn) {
716
+ this.activeTurn.backgroundAgentIds.add(event.agentId);
717
+ }
718
+ else if (event.type === 'assistant.usage') {
719
+ if (this.activeTurn &&
720
+ this.activeTurn.backgroundAgentIds.has(event.agentId)) {
721
+ this.recordTurnUsage(this.activeTurn, event);
722
+ }
723
+ else {
724
+ this.recordBackgroundUsage(event.agentId, event);
725
+ }
726
+ }
727
+ else if (event.type === 'subagent.completed' ||
728
+ event.type === 'subagent.failed') {
729
+ const succeeded = event.type === 'subagent.completed' && event.data.cancelled !== true;
730
+ if (this.activeTurn?.backgroundAgentIds.delete(event.agentId) && !succeeded) {
731
+ this.activeTurn.backgroundSucceeded = false;
732
+ }
733
+ this.settleBackgroundUsageOwner(event.agentId, succeeded);
734
+ }
735
+ else if (event.type === 'abort') {
736
+ if (this.activeTurn?.backgroundAgentIds.delete(event.agentId)) {
737
+ this.activeTurn.backgroundSucceeded = false;
738
+ }
739
+ this.settleBackgroundUsageOwner(event.agentId, false);
740
+ }
670
741
  this.handleBackgroundAgentEvent(event, event.agentId);
671
742
  return;
672
743
  }
@@ -765,7 +836,7 @@ export class CopilotSdkSessionAdapter {
765
836
  this.discardAutonomousTurn(turn);
766
837
  return;
767
838
  }
768
- if (event.type === 'assistant.idle') {
839
+ if (event.type === 'assistant.idle' || event.type === 'session.idle') {
769
840
  this.finishAutonomousTurn(turn);
770
841
  }
771
842
  }
@@ -906,15 +977,25 @@ export class CopilotSdkSessionAdapter {
906
977
  rawOutput: event.data.result ?? event.data.error,
907
978
  }));
908
979
  return;
980
+ case 'assistant.usage': {
981
+ this.recordTurnUsage(this.activeTurn, event);
982
+ return;
983
+ }
909
984
  case 'abort':
985
+ this.deliverAccumulatedTurnUsage(this.activeTurn);
910
986
  this.activeTurn.aborted = true;
911
987
  return;
912
988
  case 'session.error': {
913
989
  const turn = this.activeTurn;
914
990
  this.activeTurn = undefined;
991
+ this.deliverAccumulatedTurnUsage(turn);
915
992
  turn.reject(new Error(event.data.message));
916
993
  return;
917
994
  }
995
+ case 'session.idle': {
996
+ this.finishActiveTurn(false);
997
+ return;
998
+ }
918
999
  case 'assistant.idle': {
919
1000
  this.finishActiveTurn(event.data.aborted === true);
920
1001
  return;
@@ -923,6 +1004,93 @@ export class CopilotSdkSessionAdapter {
923
1004
  return;
924
1005
  }
925
1006
  }
1007
+ recordTurnUsage(turn, event) {
1008
+ const usageKey = event.data.apiCallId ?? event.id;
1009
+ if (turn.usageKeys.has(usageKey))
1010
+ return;
1011
+ const usage = normalizeProviderCallUsage(event.data);
1012
+ if (!usage)
1013
+ return;
1014
+ turn.usageKeys.add(usageKey);
1015
+ turn.usage = addUsage(turn.usage, usage);
1016
+ }
1017
+ deliverAccumulatedTurnUsage(turn) {
1018
+ if (!turn.usage || turn.usageDelivered)
1019
+ return;
1020
+ turn.usageDelivered = true;
1021
+ turn.onUsage?.({ ...turn.usage });
1022
+ }
1023
+ recordBackgroundUsage(agentId, event) {
1024
+ const owner = this.backgroundUsageOwners.get(agentId);
1025
+ if (!owner)
1026
+ return;
1027
+ const usageKey = event.data.apiCallId ?? event.id;
1028
+ if (owner.usageKeys.has(usageKey))
1029
+ return;
1030
+ const usage = normalizeProviderCallUsage(event.data);
1031
+ if (!usage)
1032
+ return;
1033
+ owner.usageKeys.add(usageKey);
1034
+ owner.onUsage?.(usage);
1035
+ this.refreshMissingBackgroundTerminalGrace(agentId);
1036
+ }
1037
+ settleBackgroundUsageOwner(agentId, succeeded) {
1038
+ const owner = this.backgroundUsageOwners.get(agentId);
1039
+ if (!owner)
1040
+ return;
1041
+ const timer = owner.missingTerminalTimers.get(agentId);
1042
+ if (timer)
1043
+ clearTimeout(timer);
1044
+ owner.missingTerminalTimers.delete(agentId);
1045
+ owner.succeeded &&= succeeded;
1046
+ owner.agentIds.delete(agentId);
1047
+ this.backgroundUsageOwners.delete(agentId);
1048
+ if (owner.agentIds.size === 0) {
1049
+ this.settleBackgroundUsageGroup(owner, owner.succeeded);
1050
+ }
1051
+ }
1052
+ settleBackgroundUsageGroup(owner, succeeded) {
1053
+ for (const timer of owner.missingTerminalTimers.values()) {
1054
+ clearTimeout(timer);
1055
+ }
1056
+ owner.missingTerminalTimers.clear();
1057
+ for (const agentId of owner.agentIds) {
1058
+ if (this.backgroundUsageOwners.get(agentId) === owner) {
1059
+ this.backgroundUsageOwners.delete(agentId);
1060
+ }
1061
+ }
1062
+ owner.agentIds.clear();
1063
+ owner.resolve(succeeded && owner.succeeded);
1064
+ }
1065
+ updateBackgroundUsageRegistryState(agentId, active) {
1066
+ const owner = this.backgroundUsageOwners.get(agentId);
1067
+ if (!owner)
1068
+ return;
1069
+ const existing = owner.missingTerminalTimers.get(agentId);
1070
+ if (active) {
1071
+ if (existing)
1072
+ clearTimeout(existing);
1073
+ owner.missingTerminalTimers.delete(agentId);
1074
+ return;
1075
+ }
1076
+ this.refreshMissingBackgroundTerminalGrace(agentId);
1077
+ }
1078
+ refreshMissingBackgroundTerminalGrace(agentId) {
1079
+ const owner = this.backgroundUsageOwners.get(agentId);
1080
+ if (!owner)
1081
+ return;
1082
+ const registryRun = this.reportedBackgroundTasks.get(agentId);
1083
+ if (registryRun?.state === 'running' || registryRun?.state === 'waiting') {
1084
+ this.updateBackgroundUsageRegistryState(agentId, true);
1085
+ return;
1086
+ }
1087
+ const existing = owner.missingTerminalTimers.get(agentId);
1088
+ if (existing)
1089
+ clearTimeout(existing);
1090
+ owner.missingTerminalTimers.set(agentId, setTimeout(() => {
1091
+ this.settleBackgroundUsageOwner(agentId, false);
1092
+ }, this.backgroundUsageOwnerTimeoutMs));
1093
+ }
926
1094
  async interruptAttachedTurn(turn) {
927
1095
  try {
928
1096
  const result = await this.session.rpc.interruptMainTurn({ flushQueued: false });
@@ -935,6 +1103,7 @@ export class CopilotSdkSessionAdapter {
935
1103
  return;
936
1104
  }
937
1105
  this.activeTurn = undefined;
1106
+ this.deliverAccumulatedTurnUsage(turn);
938
1107
  turn.reject(normalizeError(error));
939
1108
  }
940
1109
  }
@@ -946,7 +1115,58 @@ export class CopilotSdkSessionAdapter {
946
1115
  this.activeTurn = undefined;
947
1116
  const stopReason = aborted || turn.aborted ? 'cancelled' : 'end_turn';
948
1117
  this.emitUpdate(stopUpdate(stopReason));
949
- turn.resolve({ stopReason });
1118
+ if (!turn.backgroundSucceeded) {
1119
+ turn.onExecutionContinuation?.(Promise.resolve(false));
1120
+ }
1121
+ if (turn.backgroundAgentIds.size > 0 &&
1122
+ turn.onExecutionContinuation) {
1123
+ let resolveContinuation;
1124
+ const completion = new Promise((resolve) => {
1125
+ resolveContinuation = resolve;
1126
+ });
1127
+ const owner = {
1128
+ agentIds: new Set(turn.backgroundAgentIds),
1129
+ usageKeys: new Set(turn.usageKeys),
1130
+ onUsage: turn.onUsage,
1131
+ succeeded: true,
1132
+ missingTerminalTimers: new Map(),
1133
+ resolve: resolveContinuation,
1134
+ };
1135
+ for (const agentId of owner.agentIds) {
1136
+ const previous = this.backgroundUsageOwners.get(agentId);
1137
+ if (previous) {
1138
+ const previousTimer = previous.missingTerminalTimers.get(agentId);
1139
+ if (previousTimer)
1140
+ clearTimeout(previousTimer);
1141
+ previous.missingTerminalTimers.delete(agentId);
1142
+ previous.agentIds.delete(agentId);
1143
+ previous.succeeded = false;
1144
+ if (previous.agentIds.size === 0) {
1145
+ this.settleBackgroundUsageGroup(previous, false);
1146
+ }
1147
+ }
1148
+ this.backgroundUsageOwners.set(agentId, owner);
1149
+ this.refreshMissingBackgroundTerminalGrace(agentId);
1150
+ }
1151
+ turn.onExecutionContinuation(completion);
1152
+ }
1153
+ turn.resolve({
1154
+ stopReason,
1155
+ ...(turn.usage && !turn.usageDelivered
1156
+ ? {
1157
+ usage: {
1158
+ inputTokens: turn.usage.inputTokens,
1159
+ outputTokens: turn.usage.outputTokens,
1160
+ totalTokens: turn.usage.inputTokens +
1161
+ turn.usage.outputTokens +
1162
+ turn.usage.cacheReadTokens +
1163
+ turn.usage.cacheWriteTokens,
1164
+ cachedReadTokens: turn.usage.cacheReadTokens,
1165
+ cachedWriteTokens: turn.usage.cacheWriteTokens,
1166
+ },
1167
+ }
1168
+ : {}),
1169
+ });
950
1170
  }
951
1171
  resolveEventTurnId(event) {
952
1172
  switch (event.type) {
@@ -956,6 +1176,10 @@ export class CopilotSdkSessionAdapter {
956
1176
  case 'tool.execution_start':
957
1177
  case 'tool.execution_complete':
958
1178
  return event.data.turnId ?? this.currentRootTurnId;
1179
+ case 'session.idle':
1180
+ // Session idle is terminal only after the owned root turn has ended.
1181
+ return this.currentRootTurnId ? undefined : this.lastRootTurnId;
1182
+ case 'assistant.usage':
959
1183
  case 'assistant.idle':
960
1184
  return this.currentRootTurnId ?? this.lastRootTurnId;
961
1185
  default:
@@ -1,3 +1,7 @@
1
+ import { join } from 'node:path';
2
+ import { configureHostedNativeSession, createHostedNativeProvider } from '../native-providers.js';
3
+ import { createSessionOwnershipLock, readProtectedCodexSessions } from './normalized/session-ownership.js';
4
+ import { NormalizedProviderAdapter } from './normalized/adapter.js';
1
5
  import { FileChannelContextStore } from '../context/injection.js';
2
6
  import { ServerProjectionStrategyResolver } from '../context/projection-strategy.js';
3
7
  import { createProviderConnectionsSessionStore } from '../connections-state-store.js';
@@ -59,6 +63,10 @@ class CliBackedProviderV2 {
59
63
  async generateReply(input, options) {
60
64
  const text = await this.cli.generateReply(input.turn, {
61
65
  onProgress: createAwaitingUserProgressHandler(options?.onProgress),
66
+ ...(options?.onUsage ? { onUsage: options.onUsage } : {}),
67
+ ...(options?.onExecutionContinuation
68
+ ? { onExecutionContinuation: options.onExecutionContinuation }
69
+ : {}),
62
70
  });
63
71
  // Read the session AFTER the turn: a first turn on a channel opens the
64
72
  // session as it runs, so before this point there is nothing to report.
@@ -195,6 +203,22 @@ export function createProvider(config, debugLogger, options = {}) {
195
203
  return projectionStrategyResolver.resolve(input.channelId);
196
204
  },
197
205
  });
206
+ const outputMode = config.providerOutputMode ?? 'legacy';
207
+ if (outputMode === 'normalized') {
208
+ const provider = createHostedNativeProvider(config.provider, config.nativeProviderCommand, config);
209
+ return new NormalizedProviderAdapter({
210
+ ...(config.provider === 'codex' ? { sessionManagement: { root: config.sessionWorkspaceRoot,
211
+ store: new FileCodexChannelSessionStore({ resolvePath: () => join(config.stateRootDir, 'independent-sessions', 'codex.json') }),
212
+ loadProtectedNativeIds: () => readProtectedCodexSessions(config.stateRootDir),
213
+ withOwnershipLock: createSessionOwnershipLock(join(config.stateRootDir, 'independent-sessions')),
214
+ } } : {}),
215
+ provider, prepare: (input) => turnPreparer.prepare(input),
216
+ ...(config.provider === 'codex' ? { prepareSession: (channelId) => turnPreparer.prepareSession(channelId, 'codex') } : {}),
217
+ resolveAgentId: () => config.resolveStableAgentId?.(), serverUrl: config.borgeeBaseUrl,
218
+ configureSession: (session) => configureHostedNativeSession(session, config.provider),
219
+ store: new FileCodexChannelSessionStore({ resolvePath: (agentId) => join(config.stateRootDir, 'native-sessions', config.provider, encodeURIComponent(agentId) + '.json') }),
220
+ });
221
+ }
198
222
  switch (config.provider) {
199
223
  case 'claude': {
200
224
  return createClaudeProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer, idleBackendShutdownMs);