@borgee/agents-host 0.2.97 → 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.
- package/dist/agents-host.d.ts +8 -0
- package/dist/agents-host.js +258 -64
- package/dist/chat/chat-control-plane.d.ts +13 -3
- package/dist/chat/sdk-chat-control-plane.d.ts +30 -5
- package/dist/chat/sdk-chat-control-plane.js +131 -3
- package/dist/context/skill-manual.js +1 -1
- package/dist/execution-telemetry.d.ts +96 -0
- package/dist/execution-telemetry.js +583 -0
- package/dist/gateway/channel-file-workspace.d.ts +15 -0
- package/dist/gateway/channel-file-workspace.js +130 -0
- package/dist/gateway/localhost-gateway.js +140 -0
- package/dist/plugin-sdk.js +72 -3
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/gateway-authorization.d.ts +1 -1
- package/dist/policy/gateway-authorization.js +22 -2
- package/dist/providers/claude/adapter.js +1 -0
- package/dist/providers/claude/cli-client.js +4 -0
- package/dist/providers/codex/adapter.js +1 -0
- package/dist/providers/codex/cli-client.js +5 -0
- package/dist/providers/copilot/adapter.js +4 -0
- package/dist/providers/copilot/cli-client.js +11 -1
- package/dist/providers/copilot/sdk-session.d.ts +12 -3
- package/dist/providers/copilot/sdk-session.js +222 -5
- package/dist/providers/create-provider.js +4 -0
- package/dist/providers/prompt-usage.d.ts +8 -0
- package/dist/providers/prompt-usage.js +52 -0
- package/dist/state-paths.d.ts +2 -0
- package/dist/state-paths.js +6 -0
- package/dist/types.d.ts +41 -0
- package/dist/typing-lease.d.ts +14 -0
- package/dist/typing-lease.js +31 -0
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +12 -4
- package/skills/borgee-agent/scripts/borgee-agent.mjs +89 -0
- 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':
|
|
@@ -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
|
|
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
|
|
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
|
}
|
|
@@ -906,12 +977,18 @@ 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
|
}
|
|
@@ -923,6 +1000,93 @@ export class CopilotSdkSessionAdapter {
|
|
|
923
1000
|
return;
|
|
924
1001
|
}
|
|
925
1002
|
}
|
|
1003
|
+
recordTurnUsage(turn, event) {
|
|
1004
|
+
const usageKey = event.data.apiCallId ?? event.id;
|
|
1005
|
+
if (turn.usageKeys.has(usageKey))
|
|
1006
|
+
return;
|
|
1007
|
+
const usage = normalizeProviderCallUsage(event.data);
|
|
1008
|
+
if (!usage)
|
|
1009
|
+
return;
|
|
1010
|
+
turn.usageKeys.add(usageKey);
|
|
1011
|
+
turn.usage = addUsage(turn.usage, usage);
|
|
1012
|
+
}
|
|
1013
|
+
deliverAccumulatedTurnUsage(turn) {
|
|
1014
|
+
if (!turn.usage || turn.usageDelivered)
|
|
1015
|
+
return;
|
|
1016
|
+
turn.usageDelivered = true;
|
|
1017
|
+
turn.onUsage?.({ ...turn.usage });
|
|
1018
|
+
}
|
|
1019
|
+
recordBackgroundUsage(agentId, event) {
|
|
1020
|
+
const owner = this.backgroundUsageOwners.get(agentId);
|
|
1021
|
+
if (!owner)
|
|
1022
|
+
return;
|
|
1023
|
+
const usageKey = event.data.apiCallId ?? event.id;
|
|
1024
|
+
if (owner.usageKeys.has(usageKey))
|
|
1025
|
+
return;
|
|
1026
|
+
const usage = normalizeProviderCallUsage(event.data);
|
|
1027
|
+
if (!usage)
|
|
1028
|
+
return;
|
|
1029
|
+
owner.usageKeys.add(usageKey);
|
|
1030
|
+
owner.onUsage?.(usage);
|
|
1031
|
+
this.refreshMissingBackgroundTerminalGrace(agentId);
|
|
1032
|
+
}
|
|
1033
|
+
settleBackgroundUsageOwner(agentId, succeeded) {
|
|
1034
|
+
const owner = this.backgroundUsageOwners.get(agentId);
|
|
1035
|
+
if (!owner)
|
|
1036
|
+
return;
|
|
1037
|
+
const timer = owner.missingTerminalTimers.get(agentId);
|
|
1038
|
+
if (timer)
|
|
1039
|
+
clearTimeout(timer);
|
|
1040
|
+
owner.missingTerminalTimers.delete(agentId);
|
|
1041
|
+
owner.succeeded &&= succeeded;
|
|
1042
|
+
owner.agentIds.delete(agentId);
|
|
1043
|
+
this.backgroundUsageOwners.delete(agentId);
|
|
1044
|
+
if (owner.agentIds.size === 0) {
|
|
1045
|
+
this.settleBackgroundUsageGroup(owner, owner.succeeded);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
settleBackgroundUsageGroup(owner, succeeded) {
|
|
1049
|
+
for (const timer of owner.missingTerminalTimers.values()) {
|
|
1050
|
+
clearTimeout(timer);
|
|
1051
|
+
}
|
|
1052
|
+
owner.missingTerminalTimers.clear();
|
|
1053
|
+
for (const agentId of owner.agentIds) {
|
|
1054
|
+
if (this.backgroundUsageOwners.get(agentId) === owner) {
|
|
1055
|
+
this.backgroundUsageOwners.delete(agentId);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
owner.agentIds.clear();
|
|
1059
|
+
owner.resolve(succeeded && owner.succeeded);
|
|
1060
|
+
}
|
|
1061
|
+
updateBackgroundUsageRegistryState(agentId, active) {
|
|
1062
|
+
const owner = this.backgroundUsageOwners.get(agentId);
|
|
1063
|
+
if (!owner)
|
|
1064
|
+
return;
|
|
1065
|
+
const existing = owner.missingTerminalTimers.get(agentId);
|
|
1066
|
+
if (active) {
|
|
1067
|
+
if (existing)
|
|
1068
|
+
clearTimeout(existing);
|
|
1069
|
+
owner.missingTerminalTimers.delete(agentId);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
this.refreshMissingBackgroundTerminalGrace(agentId);
|
|
1073
|
+
}
|
|
1074
|
+
refreshMissingBackgroundTerminalGrace(agentId) {
|
|
1075
|
+
const owner = this.backgroundUsageOwners.get(agentId);
|
|
1076
|
+
if (!owner)
|
|
1077
|
+
return;
|
|
1078
|
+
const registryRun = this.reportedBackgroundTasks.get(agentId);
|
|
1079
|
+
if (registryRun?.state === 'running' || registryRun?.state === 'waiting') {
|
|
1080
|
+
this.updateBackgroundUsageRegistryState(agentId, true);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const existing = owner.missingTerminalTimers.get(agentId);
|
|
1084
|
+
if (existing)
|
|
1085
|
+
clearTimeout(existing);
|
|
1086
|
+
owner.missingTerminalTimers.set(agentId, setTimeout(() => {
|
|
1087
|
+
this.settleBackgroundUsageOwner(agentId, false);
|
|
1088
|
+
}, this.backgroundUsageOwnerTimeoutMs));
|
|
1089
|
+
}
|
|
926
1090
|
async interruptAttachedTurn(turn) {
|
|
927
1091
|
try {
|
|
928
1092
|
const result = await this.session.rpc.interruptMainTurn({ flushQueued: false });
|
|
@@ -935,6 +1099,7 @@ export class CopilotSdkSessionAdapter {
|
|
|
935
1099
|
return;
|
|
936
1100
|
}
|
|
937
1101
|
this.activeTurn = undefined;
|
|
1102
|
+
this.deliverAccumulatedTurnUsage(turn);
|
|
938
1103
|
turn.reject(normalizeError(error));
|
|
939
1104
|
}
|
|
940
1105
|
}
|
|
@@ -946,7 +1111,58 @@ export class CopilotSdkSessionAdapter {
|
|
|
946
1111
|
this.activeTurn = undefined;
|
|
947
1112
|
const stopReason = aborted || turn.aborted ? 'cancelled' : 'end_turn';
|
|
948
1113
|
this.emitUpdate(stopUpdate(stopReason));
|
|
949
|
-
turn.
|
|
1114
|
+
if (!turn.backgroundSucceeded) {
|
|
1115
|
+
turn.onExecutionContinuation?.(Promise.resolve(false));
|
|
1116
|
+
}
|
|
1117
|
+
if (turn.backgroundAgentIds.size > 0 &&
|
|
1118
|
+
turn.onExecutionContinuation) {
|
|
1119
|
+
let resolveContinuation;
|
|
1120
|
+
const completion = new Promise((resolve) => {
|
|
1121
|
+
resolveContinuation = resolve;
|
|
1122
|
+
});
|
|
1123
|
+
const owner = {
|
|
1124
|
+
agentIds: new Set(turn.backgroundAgentIds),
|
|
1125
|
+
usageKeys: new Set(turn.usageKeys),
|
|
1126
|
+
onUsage: turn.onUsage,
|
|
1127
|
+
succeeded: true,
|
|
1128
|
+
missingTerminalTimers: new Map(),
|
|
1129
|
+
resolve: resolveContinuation,
|
|
1130
|
+
};
|
|
1131
|
+
for (const agentId of owner.agentIds) {
|
|
1132
|
+
const previous = this.backgroundUsageOwners.get(agentId);
|
|
1133
|
+
if (previous) {
|
|
1134
|
+
const previousTimer = previous.missingTerminalTimers.get(agentId);
|
|
1135
|
+
if (previousTimer)
|
|
1136
|
+
clearTimeout(previousTimer);
|
|
1137
|
+
previous.missingTerminalTimers.delete(agentId);
|
|
1138
|
+
previous.agentIds.delete(agentId);
|
|
1139
|
+
previous.succeeded = false;
|
|
1140
|
+
if (previous.agentIds.size === 0) {
|
|
1141
|
+
this.settleBackgroundUsageGroup(previous, false);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
this.backgroundUsageOwners.set(agentId, owner);
|
|
1145
|
+
this.refreshMissingBackgroundTerminalGrace(agentId);
|
|
1146
|
+
}
|
|
1147
|
+
turn.onExecutionContinuation(completion);
|
|
1148
|
+
}
|
|
1149
|
+
turn.resolve({
|
|
1150
|
+
stopReason,
|
|
1151
|
+
...(turn.usage && !turn.usageDelivered
|
|
1152
|
+
? {
|
|
1153
|
+
usage: {
|
|
1154
|
+
inputTokens: turn.usage.inputTokens,
|
|
1155
|
+
outputTokens: turn.usage.outputTokens,
|
|
1156
|
+
totalTokens: turn.usage.inputTokens +
|
|
1157
|
+
turn.usage.outputTokens +
|
|
1158
|
+
turn.usage.cacheReadTokens +
|
|
1159
|
+
turn.usage.cacheWriteTokens,
|
|
1160
|
+
cachedReadTokens: turn.usage.cacheReadTokens,
|
|
1161
|
+
cachedWriteTokens: turn.usage.cacheWriteTokens,
|
|
1162
|
+
},
|
|
1163
|
+
}
|
|
1164
|
+
: {}),
|
|
1165
|
+
});
|
|
950
1166
|
}
|
|
951
1167
|
resolveEventTurnId(event) {
|
|
952
1168
|
switch (event.type) {
|
|
@@ -956,6 +1172,7 @@ export class CopilotSdkSessionAdapter {
|
|
|
956
1172
|
case 'tool.execution_start':
|
|
957
1173
|
case 'tool.execution_complete':
|
|
958
1174
|
return event.data.turnId ?? this.currentRootTurnId;
|
|
1175
|
+
case 'assistant.usage':
|
|
959
1176
|
case 'assistant.idle':
|
|
960
1177
|
return this.currentRootTurnId ?? this.lastRootTurnId;
|
|
961
1178
|
default:
|
|
@@ -59,6 +59,10 @@ class CliBackedProviderV2 {
|
|
|
59
59
|
async generateReply(input, options) {
|
|
60
60
|
const text = await this.cli.generateReply(input.turn, {
|
|
61
61
|
onProgress: createAwaitingUserProgressHandler(options?.onProgress),
|
|
62
|
+
...(options?.onUsage ? { onUsage: options.onUsage } : {}),
|
|
63
|
+
...(options?.onExecutionContinuation
|
|
64
|
+
? { onExecutionContinuation: options.onExecutionContinuation }
|
|
65
|
+
: {}),
|
|
62
66
|
});
|
|
63
67
|
// Read the session AFTER the turn: a first turn on a channel opens the
|
|
64
68
|
// session as it runs, so before this point there is nothing to report.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProviderTokenUsage } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* ACP prompt usage defines input and cached tokens as separate counters.
|
|
4
|
+
* Provider totals, context occupancy, and thought tokens have no equivalent in
|
|
5
|
+
* the execution ledger and are deliberately not projected.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeAcpPromptUsage(value: unknown): ProviderTokenUsage | undefined;
|
|
8
|
+
export declare function normalizeProviderCallUsage(value: unknown): ProviderTokenUsage | undefined;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function tokenCount(value, optional = false) {
|
|
2
|
+
if (value == null && optional)
|
|
3
|
+
return 0;
|
|
4
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
5
|
+
return undefined;
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* ACP prompt usage defines input and cached tokens as separate counters.
|
|
10
|
+
* Provider totals, context occupancy, and thought tokens have no equivalent in
|
|
11
|
+
* the execution ledger and are deliberately not projected.
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeAcpPromptUsage(value) {
|
|
14
|
+
if (!value || typeof value !== 'object')
|
|
15
|
+
return undefined;
|
|
16
|
+
const usage = value;
|
|
17
|
+
const inputTokens = tokenCount(usage.inputTokens);
|
|
18
|
+
const outputTokens = tokenCount(usage.outputTokens);
|
|
19
|
+
const cacheReadTokens = tokenCount(usage.cachedReadTokens, true);
|
|
20
|
+
const cacheWriteTokens = tokenCount(usage.cachedWriteTokens, true);
|
|
21
|
+
if (inputTokens === undefined ||
|
|
22
|
+
outputTokens === undefined ||
|
|
23
|
+
cacheReadTokens === undefined ||
|
|
24
|
+
cacheWriteTokens === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens };
|
|
28
|
+
}
|
|
29
|
+
export function normalizeProviderCallUsage(value) {
|
|
30
|
+
if (!value || typeof value !== 'object')
|
|
31
|
+
return undefined;
|
|
32
|
+
const usage = value;
|
|
33
|
+
const reported = [
|
|
34
|
+
usage.inputTokens,
|
|
35
|
+
usage.outputTokens,
|
|
36
|
+
usage.cacheReadTokens,
|
|
37
|
+
usage.cacheWriteTokens,
|
|
38
|
+
];
|
|
39
|
+
if (reported.every((tokens) => tokens == null))
|
|
40
|
+
return undefined;
|
|
41
|
+
const inputTokens = tokenCount(usage.inputTokens, true);
|
|
42
|
+
const outputTokens = tokenCount(usage.outputTokens, true);
|
|
43
|
+
const cacheReadTokens = tokenCount(usage.cacheReadTokens, true);
|
|
44
|
+
const cacheWriteTokens = tokenCount(usage.cacheWriteTokens, true);
|
|
45
|
+
if (inputTokens === undefined ||
|
|
46
|
+
outputTokens === undefined ||
|
|
47
|
+
cacheReadTokens === undefined ||
|
|
48
|
+
cacheWriteTokens === undefined) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens };
|
|
52
|
+
}
|
package/dist/state-paths.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare function resolveLocalConfigAgentStateRoot(rootPath: string, agent
|
|
|
23
23
|
export declare function resolveSharedProtocolKickoffDecisionPath(stateRootDir: string, channelId: string, anchorMessageId: string): string;
|
|
24
24
|
export declare function resolveSharedProtocolStatusPath(stateRootDir: string, channelId: string, anchorMessageId: string): string;
|
|
25
25
|
export declare function resolveConnectionsStatePath(stateRootDir: string): string;
|
|
26
|
+
export declare function resolveExecutionTelemetryOutboxPath(stateRootDir: string): string;
|
|
27
|
+
export declare function resolveExecutionDeliveryJournalPath(stateRootDir: string): string;
|
|
26
28
|
export declare function resolveAttentionStatePath(stateRootDir: string, channelId: string): string;
|
|
27
29
|
export declare function resolveCompactionStatePath(stateRootDir: string, channelId: string): string;
|
|
28
30
|
export declare function resolveAuthorizationAuditPath(stateRootDir: string): string;
|
package/dist/state-paths.js
CHANGED
|
@@ -138,6 +138,12 @@ export function resolveSharedProtocolStatusPath(stateRootDir, channelId, anchorM
|
|
|
138
138
|
export function resolveConnectionsStatePath(stateRootDir) {
|
|
139
139
|
return join(stateRootDir, 'connections-state.sqlite');
|
|
140
140
|
}
|
|
141
|
+
export function resolveExecutionTelemetryOutboxPath(stateRootDir) {
|
|
142
|
+
return join(stateRootDir, 'execution-telemetry-outbox.json');
|
|
143
|
+
}
|
|
144
|
+
export function resolveExecutionDeliveryJournalPath(stateRootDir) {
|
|
145
|
+
return join(stateRootDir, 'execution-delivery-journal.json');
|
|
146
|
+
}
|
|
141
147
|
export function resolveAttentionStatePath(stateRootDir, channelId) {
|
|
142
148
|
return join(stateRootDir, 'attention-state', `${encodeSegment(channelId)}.json`);
|
|
143
149
|
}
|