@borgee/agents-host 0.2.74 → 0.2.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -24
- package/dist/agents-host.d.ts +6 -4
- package/dist/agents-host.js +127 -16
- package/dist/background-runs.d.ts +24 -0
- package/dist/background-runs.js +184 -0
- package/dist/chat/chat-control-plane.d.ts +7 -1
- package/dist/chat/sdk-chat-control-plane.d.ts +7 -2
- package/dist/chat/sdk-chat-control-plane.js +22 -0
- package/dist/context/claude-file-brief.js +3 -3
- package/dist/context/injection.d.ts +18 -12
- package/dist/context/injection.js +53 -53
- package/dist/context/prompt.js +10 -10
- package/dist/context/resolved-working-folder.d.ts +3 -0
- package/dist/context/resolved-working-folder.js +106 -0
- package/dist/context/turn-preparation.js +1 -1
- package/dist/managed-daemon.js +30 -30
- package/dist/plugin-sdk.js +315 -29
- package/dist/plugin-sdk.js.map +3 -3
- package/dist/policy/authorization-audit.d.ts +1 -1
- package/dist/policy/copilot-permission.d.ts +9 -0
- package/dist/policy/copilot-permission.js +120 -1
- package/dist/progress-to-activity.d.ts +1 -1
- package/dist/progress-to-activity.js +1 -0
- package/dist/providers/claude/adapter.d.ts +2 -1
- package/dist/providers/claude/adapter.js +36 -1
- package/dist/providers/claude/background-run-observer.d.ts +32 -0
- package/dist/providers/claude/background-run-observer.js +381 -0
- package/dist/providers/claude/cli-client.d.ts +12 -0
- package/dist/providers/claude/cli-client.js +453 -72
- package/dist/providers/claude/foreground-handoff.d.ts +4 -0
- package/dist/providers/claude/foreground-handoff.js +45 -0
- package/dist/providers/claude/task-cancellation-protocol.d.ts +14 -0
- package/dist/providers/claude/task-cancellation-protocol.js +21 -0
- package/dist/providers/codex/adapter.d.ts +1 -1
- package/dist/providers/codex/adapter.js +7 -0
- package/dist/providers/codex/cli-client.js +8 -7
- package/dist/providers/codex/project-doc.js +6 -6
- package/dist/providers/copilot/activity-metadata.d.ts +3 -0
- package/dist/providers/copilot/activity-metadata.js +19 -0
- package/dist/providers/copilot/adapter.d.ts +6 -2
- package/dist/providers/copilot/adapter.js +27 -1
- package/dist/providers/copilot/cli-client.d.ts +37 -10
- package/dist/providers/copilot/cli-client.js +756 -122
- package/dist/providers/copilot/sdk-session.d.ts +149 -0
- package/dist/providers/copilot/sdk-session.js +981 -0
- package/dist/providers/create-provider.js +33 -0
- package/dist/providers/provider-adapter.d.ts +24 -1
- package/dist/providers/provider-adapter.js +17 -0
- package/dist/types.d.ts +40 -11
- package/dist/vendor/claude-agent-acp/LICENSE +191 -0
- package/dist/vendor/claude-agent-acp/NOTICE +8 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts +1017 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.js +6305 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-foreground-handoff-bridge.js +322 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-cancellation-bridge.js +101 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-lifecycle-bridge.js +58 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts +130 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.js +304 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts +3 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/index.js +75 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts +6 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/lib.js +5 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts +68 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/settings.js +185 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts +102 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/tools.js +1000 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts +16 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/utils.js +81 -0
- package/dist/vendor/claude-agent-acp/package.json +85 -0
- package/package.json +15 -10
- package/skills/borgee-agent/references/task-properties.md +3 -3
- package/skills/borgee-agent/scripts/borgee-agent.mjs +1 -1
- package/skills/borgee-agent/scripts/borgee-agent.py +1 -1
- package/dist/context/resolved-workspace.d.ts +0 -3
- package/dist/context/resolved-workspace.js +0 -106
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DebugLogger } from '../debug.js';
|
|
2
2
|
import type { InternalPolicyMode } from '../compatibility-gates.js';
|
|
3
|
-
export type AuthorizationAuditSurface = 'copilot-acp-permission' | 'localhost-gateway';
|
|
3
|
+
export type AuthorizationAuditSurface = 'copilot-acp-permission' | 'copilot-sdk-permission' | 'localhost-gateway';
|
|
4
4
|
export interface AuthorizationAuditRecord {
|
|
5
5
|
timestamp: string;
|
|
6
6
|
surface: AuthorizationAuditSurface;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { RequestPermissionRequest, RequestPermissionResponse } from '@agentclientprotocol/sdk';
|
|
2
|
+
import type { PermissionRequest as CopilotSdkPermissionRequest, PermissionRequestResult as CopilotSdkPermissionRequestResult } from '@github/copilot-sdk';
|
|
2
3
|
import type { DebugLogger } from '../debug.js';
|
|
3
4
|
import type { InternalPolicyMode } from '../compatibility-gates.js';
|
|
4
5
|
import type { AuthorizationAuditSinkLike } from './authorization-audit.js';
|
|
6
|
+
import type { AuthorizationAuditSurface } from './authorization-audit.js';
|
|
5
7
|
export interface CopilotPermissionPolicyContext {
|
|
6
8
|
gateEnabled: boolean;
|
|
7
9
|
policyMode: InternalPolicyMode;
|
|
@@ -10,7 +12,14 @@ export interface CopilotPermissionPolicyContext {
|
|
|
10
12
|
auditSink?: AuthorizationAuditSinkLike;
|
|
11
13
|
agentId?: string;
|
|
12
14
|
channelId?: string;
|
|
15
|
+
sessionBound?: boolean;
|
|
13
16
|
toolPermissionMode?: 'default' | 'deny-all';
|
|
17
|
+
auditSurface?: AuthorizationAuditSurface;
|
|
18
|
+
}
|
|
19
|
+
export interface CopilotSdkPermissionPolicyContext extends Omit<CopilotPermissionPolicyContext, 'params'> {
|
|
20
|
+
request: CopilotSdkPermissionRequest;
|
|
21
|
+
sessionId: string;
|
|
14
22
|
}
|
|
15
23
|
export declare function resolveCopilotPermissionResponse(context: CopilotPermissionPolicyContext): RequestPermissionResponse;
|
|
24
|
+
export declare function resolveCopilotSdkPermissionResponse(context: CopilotSdkPermissionPolicyContext): CopilotSdkPermissionRequestResult;
|
|
16
25
|
export declare function createLegacyMissingAllowOptionError(): Error;
|
|
@@ -1,5 +1,8 @@
|
|
|
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
|
+
const SDK_APPROVE_ONCE_OPTION_ID = 'sdk-approve-once';
|
|
5
|
+
const SDK_REJECT_OPTION_ID = 'sdk-reject';
|
|
3
6
|
function legacySelectPermissionOption(options) {
|
|
4
7
|
const option = options.find((candidate) => candidate.kind === 'allow_always')
|
|
5
8
|
?? options.find((candidate) => candidate.kind === 'allow_once');
|
|
@@ -71,7 +74,7 @@ function decisionToResponse(decision) {
|
|
|
71
74
|
function appendAuditRecord(context, effectiveDecision, reason, policyDecision) {
|
|
72
75
|
context.auditSink?.append({
|
|
73
76
|
timestamp: new Date().toISOString(),
|
|
74
|
-
surface: 'copilot-acp-permission',
|
|
77
|
+
surface: context.auditSurface ?? 'copilot-acp-permission',
|
|
75
78
|
policyMode: context.policyMode,
|
|
76
79
|
effectiveOutcome: effectiveDecision?.outcome ?? 'error',
|
|
77
80
|
reason,
|
|
@@ -84,6 +87,30 @@ function appendAuditRecord(context, effectiveDecision, reason, policyDecision) {
|
|
|
84
87
|
});
|
|
85
88
|
}
|
|
86
89
|
export function resolveCopilotPermissionResponse(context) {
|
|
90
|
+
if (context.sessionBound === false) {
|
|
91
|
+
const rejectOption = resolveRejectOption(context.params.options);
|
|
92
|
+
const unboundDecision = rejectOption
|
|
93
|
+
? {
|
|
94
|
+
outcome: 'selected',
|
|
95
|
+
optionId: rejectOption.optionId,
|
|
96
|
+
optionKind: rejectOption.kind,
|
|
97
|
+
reason: 'unbound-session',
|
|
98
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
99
|
+
}
|
|
100
|
+
: {
|
|
101
|
+
outcome: 'cancelled',
|
|
102
|
+
reason: 'unbound-session',
|
|
103
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
104
|
+
};
|
|
105
|
+
context.logger.error('Copilot ACP permission request rejected for an unbound session', {
|
|
106
|
+
agentId: context.agentId,
|
|
107
|
+
sessionId: context.params.sessionId,
|
|
108
|
+
toolKind: unboundDecision.toolKind,
|
|
109
|
+
reason: unboundDecision.reason,
|
|
110
|
+
});
|
|
111
|
+
appendAuditRecord(context, unboundDecision, unboundDecision.reason, unboundDecision);
|
|
112
|
+
return decisionToResponse(unboundDecision);
|
|
113
|
+
}
|
|
87
114
|
if (context.toolPermissionMode === 'deny-all') {
|
|
88
115
|
const rejectOption = resolveRejectOption(context.params.options);
|
|
89
116
|
const deniedDecision = rejectOption
|
|
@@ -206,6 +233,98 @@ export function resolveCopilotPermissionResponse(context) {
|
|
|
206
233
|
return decisionToResponse(failedClosedDecision);
|
|
207
234
|
}
|
|
208
235
|
}
|
|
236
|
+
function sdkPermissionToolKind(request) {
|
|
237
|
+
switch (request.kind) {
|
|
238
|
+
case 'read':
|
|
239
|
+
return 'read';
|
|
240
|
+
case 'write':
|
|
241
|
+
return 'edit';
|
|
242
|
+
case 'shell':
|
|
243
|
+
return 'execute';
|
|
244
|
+
case 'url':
|
|
245
|
+
return 'fetch';
|
|
246
|
+
default:
|
|
247
|
+
return 'other';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function sdkPermissionTitle(request) {
|
|
251
|
+
if ('fullCommandText' in request && typeof request.fullCommandText === 'string') {
|
|
252
|
+
return request.fullCommandText;
|
|
253
|
+
}
|
|
254
|
+
if ('fileName' in request && typeof request.fileName === 'string') {
|
|
255
|
+
return request.fileName;
|
|
256
|
+
}
|
|
257
|
+
if ('toolName' in request && typeof request.toolName === 'string') {
|
|
258
|
+
return request.toolName;
|
|
259
|
+
}
|
|
260
|
+
return request.kind;
|
|
261
|
+
}
|
|
262
|
+
export function resolveCopilotSdkPermissionResponse(context) {
|
|
263
|
+
if (context.request.managedApprovalRequired === true) {
|
|
264
|
+
context.auditSink?.append({
|
|
265
|
+
timestamp: new Date().toISOString(),
|
|
266
|
+
surface: context.auditSurface ?? 'copilot-sdk-permission',
|
|
267
|
+
policyMode: context.policyMode,
|
|
268
|
+
effectiveOutcome: 'rejected',
|
|
269
|
+
reason: 'managed-approval-required',
|
|
270
|
+
agentId: context.agentId,
|
|
271
|
+
channelId: context.channelId,
|
|
272
|
+
selectedOptionKind: 'reject_once',
|
|
273
|
+
policyOutcome: 'selected',
|
|
274
|
+
policySelectedOptionKind: 'reject_once',
|
|
275
|
+
toolKind: sdkPermissionToolKind(context.request),
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
kind: 'reject',
|
|
279
|
+
feedback: 'Managed policy requires interactive approval, which this hosted runtime cannot provide.',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const options = [
|
|
283
|
+
{
|
|
284
|
+
optionId: SDK_APPROVE_FOR_SESSION_OPTION_ID,
|
|
285
|
+
name: 'Allow for this session',
|
|
286
|
+
kind: 'allow_always',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
optionId: SDK_APPROVE_ONCE_OPTION_ID,
|
|
290
|
+
name: 'Allow once',
|
|
291
|
+
kind: 'allow_once',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
optionId: SDK_REJECT_OPTION_ID,
|
|
295
|
+
name: 'Reject',
|
|
296
|
+
kind: 'reject_once',
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
const response = resolveCopilotPermissionResponse({
|
|
300
|
+
...context,
|
|
301
|
+
params: {
|
|
302
|
+
sessionId: context.sessionId,
|
|
303
|
+
toolCall: {
|
|
304
|
+
toolCallId: requestToolCallId(context.request),
|
|
305
|
+
title: sdkPermissionTitle(context.request),
|
|
306
|
+
kind: sdkPermissionToolKind(context.request),
|
|
307
|
+
},
|
|
308
|
+
options,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
if (response.outcome.outcome !== 'selected') {
|
|
312
|
+
return { kind: 'reject' };
|
|
313
|
+
}
|
|
314
|
+
switch (response.outcome.optionId) {
|
|
315
|
+
case SDK_APPROVE_FOR_SESSION_OPTION_ID:
|
|
316
|
+
return { kind: 'approve-for-session' };
|
|
317
|
+
case SDK_APPROVE_ONCE_OPTION_ID:
|
|
318
|
+
return { kind: 'approve-once' };
|
|
319
|
+
default:
|
|
320
|
+
return { kind: 'reject' };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function requestToolCallId(request) {
|
|
324
|
+
return 'toolCallId' in request && typeof request.toolCallId === 'string'
|
|
325
|
+
? request.toolCallId
|
|
326
|
+
: 'copilot-sdk-permission';
|
|
327
|
+
}
|
|
209
328
|
export function createLegacyMissingAllowOptionError() {
|
|
210
329
|
return new Error(LEGACY_MISSING_ALLOW_OPTION_MESSAGE);
|
|
211
330
|
}
|
|
@@ -2,7 +2,7 @@ import type { AgentActivity } from './plugin-sdk.js';
|
|
|
2
2
|
import type { ProviderProgressUpdate } from './types.js';
|
|
3
3
|
/** Everything a provider reports. The turn boundary is the reporter's own and never arrives here. */
|
|
4
4
|
export type ReportedActivity = Exclude<AgentActivity, {
|
|
5
|
-
shape: 'turn';
|
|
5
|
+
shape: 'turn' | 'background_run';
|
|
6
6
|
}>;
|
|
7
7
|
/**
|
|
8
8
|
* Restates one progress update in the rail's own vocabulary.
|
|
@@ -7,7 +7,8 @@ export declare class ClaudeProviderAdapter implements ProviderAdapter {
|
|
|
7
7
|
private readonly cli;
|
|
8
8
|
private readonly turnPreparer;
|
|
9
9
|
readonly capabilities: import("../provider-adapter.js").ProviderCapabilities;
|
|
10
|
-
|
|
10
|
+
readonly cancelBackgroundRun?: (channelId: string, providerRunId: string) => Promise<boolean>;
|
|
11
|
+
constructor(cli: ClaudeCliClient, turnPreparer: Pick<ProviderTurnPreparer, 'prepare'>);
|
|
11
12
|
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
12
13
|
cancelTurn(channelId: string): Promise<boolean>;
|
|
13
14
|
dispose(): Promise<void>;
|
|
@@ -7,14 +7,49 @@ export const CLAUDE_HOSTED_PROVIDER_CAPABILITIES = createHostedProviderCapabilit
|
|
|
7
7
|
reason: 'real-media-delivered',
|
|
8
8
|
delivery: ['blob'],
|
|
9
9
|
},
|
|
10
|
+
progressUpdates: {
|
|
11
|
+
support: 'supported',
|
|
12
|
+
},
|
|
13
|
+
backgroundRuns: {
|
|
14
|
+
lifecycle: 'supported',
|
|
15
|
+
targetedCancellation: 'supported',
|
|
16
|
+
},
|
|
17
|
+
foregroundHandoff: {
|
|
18
|
+
support: 'not-supported',
|
|
19
|
+
},
|
|
10
20
|
});
|
|
11
21
|
export class ClaudeProviderAdapter {
|
|
12
22
|
cli;
|
|
13
23
|
turnPreparer;
|
|
14
|
-
capabilities
|
|
24
|
+
capabilities;
|
|
25
|
+
cancelBackgroundRun;
|
|
15
26
|
constructor(cli, turnPreparer) {
|
|
16
27
|
this.cli = cli;
|
|
17
28
|
this.turnPreparer = turnPreparer;
|
|
29
|
+
const targetedCancellationSupported = cli.targetedBackgroundRunCancellationConfigured;
|
|
30
|
+
this.capabilities = targetedCancellationSupported
|
|
31
|
+
? CLAUDE_HOSTED_PROVIDER_CAPABILITIES
|
|
32
|
+
: createHostedProviderCapabilities({
|
|
33
|
+
imageInputTransport: {
|
|
34
|
+
source: 'transport-derived',
|
|
35
|
+
support: 'supported',
|
|
36
|
+
reason: 'real-media-delivered',
|
|
37
|
+
delivery: ['blob'],
|
|
38
|
+
},
|
|
39
|
+
progressUpdates: {
|
|
40
|
+
support: 'supported',
|
|
41
|
+
},
|
|
42
|
+
backgroundRuns: {
|
|
43
|
+
lifecycle: 'supported',
|
|
44
|
+
targetedCancellation: 'not-supported',
|
|
45
|
+
},
|
|
46
|
+
foregroundHandoff: {
|
|
47
|
+
support: 'not-supported',
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (targetedCancellationSupported) {
|
|
51
|
+
this.cancelBackgroundRun = (channelId, providerRunId) => (this.cli.cancelBackgroundRun(channelId, providerRunId));
|
|
52
|
+
}
|
|
18
53
|
}
|
|
19
54
|
async generateReply(input, options) {
|
|
20
55
|
const preparedTurn = await this.turnPreparer.prepare(input);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ProviderGenerateOptions } from '../../types.js';
|
|
2
|
+
export declare class ClaudeBackgroundRunObserver {
|
|
3
|
+
private onProgress?;
|
|
4
|
+
private readonly limits;
|
|
5
|
+
private readonly tasks;
|
|
6
|
+
private readonly emitted;
|
|
7
|
+
private backgroundTasksActive;
|
|
8
|
+
constructor(onProgress?: ProviderGenerateOptions['onProgress'], limits?: {
|
|
9
|
+
terminalRetentionMs?: number;
|
|
10
|
+
maxTrackedTasks?: number;
|
|
11
|
+
onBackgroundTasksActiveChanged?: (active: boolean) => void;
|
|
12
|
+
targetedCancellationSupported?: boolean;
|
|
13
|
+
});
|
|
14
|
+
setOnProgress(onProgress?: ProviderGenerateOptions['onProgress']): void;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
fail(reason?: string): void;
|
|
17
|
+
consume(value: unknown): boolean;
|
|
18
|
+
private getOrCreate;
|
|
19
|
+
private mapTaskStatus;
|
|
20
|
+
private isConcreteTerminal;
|
|
21
|
+
private scheduleMissing;
|
|
22
|
+
private cancelMissing;
|
|
23
|
+
private scheduleRetention;
|
|
24
|
+
private cancelRetention;
|
|
25
|
+
private touch;
|
|
26
|
+
private enforceLimit;
|
|
27
|
+
private isTerminal;
|
|
28
|
+
private deleteTask;
|
|
29
|
+
private publish;
|
|
30
|
+
private reconcileBackgroundTasksActive;
|
|
31
|
+
private setBackgroundTasksActive;
|
|
32
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
const MISSING_TASK_GRACE_MS = 250;
|
|
2
|
+
const TERMINAL_TASK_RETENTION_MS = 30_000;
|
|
3
|
+
const MAX_TRACKED_TASKS = 256;
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null;
|
|
6
|
+
}
|
|
7
|
+
function readOptionalString(record, key) {
|
|
8
|
+
const value = record[key];
|
|
9
|
+
return typeof value === 'string' ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function parseClaudeTaskLifecycleUpdate(value) {
|
|
12
|
+
if (!isRecord(value) ||
|
|
13
|
+
value.sessionUpdate !== 'session_info_update' ||
|
|
14
|
+
!isRecord(value._meta) ||
|
|
15
|
+
!isRecord(value._meta.claudeCode) ||
|
|
16
|
+
!isRecord(value._meta.claudeCode.taskLifecycle)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return parseClaudeTaskLifecycleRecord(value._meta.claudeCode.taskLifecycle);
|
|
20
|
+
}
|
|
21
|
+
export class ClaudeBackgroundRunObserver {
|
|
22
|
+
onProgress;
|
|
23
|
+
limits;
|
|
24
|
+
tasks = new Map();
|
|
25
|
+
emitted = new Map();
|
|
26
|
+
backgroundTasksActive = false;
|
|
27
|
+
constructor(onProgress, limits = {}) {
|
|
28
|
+
this.onProgress = onProgress;
|
|
29
|
+
this.limits = limits;
|
|
30
|
+
}
|
|
31
|
+
setOnProgress(onProgress) {
|
|
32
|
+
if (onProgress) {
|
|
33
|
+
this.onProgress = onProgress;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
dispose() {
|
|
37
|
+
for (const taskId of [...this.tasks.keys()]) {
|
|
38
|
+
this.deleteTask(taskId);
|
|
39
|
+
}
|
|
40
|
+
this.emitted.clear();
|
|
41
|
+
this.onProgress = undefined;
|
|
42
|
+
this.setBackgroundTasksActive(false);
|
|
43
|
+
}
|
|
44
|
+
fail(reason = 'Claude background run state is unknown because the provider session ended unexpectedly') {
|
|
45
|
+
for (const [taskId, record] of this.tasks) {
|
|
46
|
+
if (!record.background || this.isTerminal(record.state)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
this.cancelMissing(record);
|
|
50
|
+
record.state = 'unknown';
|
|
51
|
+
record.reason = reason;
|
|
52
|
+
this.publish(taskId, record);
|
|
53
|
+
}
|
|
54
|
+
this.dispose();
|
|
55
|
+
}
|
|
56
|
+
consume(value) {
|
|
57
|
+
const update = parseClaudeTaskLifecycleUpdate(value);
|
|
58
|
+
if (!update) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (update.event === 'background_tasks_changed') {
|
|
62
|
+
const liveTaskIds = new Set(update.tasks.map((task) => task.taskId));
|
|
63
|
+
for (const [taskId, record] of this.tasks) {
|
|
64
|
+
if (record.background && !this.isConcreteTerminal(record.state)) {
|
|
65
|
+
if (liveTaskIds.has(taskId)) {
|
|
66
|
+
this.cancelMissing(record);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.scheduleMissing(taskId, record);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const task of update.tasks) {
|
|
74
|
+
const record = this.getOrCreate(task.taskId, task.description);
|
|
75
|
+
record.label = task.description;
|
|
76
|
+
record.background = true;
|
|
77
|
+
this.cancelMissing(record);
|
|
78
|
+
if (record.state === 'unknown') {
|
|
79
|
+
record.state = 'running';
|
|
80
|
+
record.reason = undefined;
|
|
81
|
+
this.cancelRetention(record);
|
|
82
|
+
}
|
|
83
|
+
this.touch(task.taskId, record);
|
|
84
|
+
this.publish(task.taskId, record);
|
|
85
|
+
}
|
|
86
|
+
this.enforceLimit();
|
|
87
|
+
this.reconcileBackgroundTasksActive();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const record = this.getOrCreate(update.taskId, update.event === 'started' ? update.description : 'Claude background task');
|
|
91
|
+
switch (update.event) {
|
|
92
|
+
case 'started':
|
|
93
|
+
record.label = update.description;
|
|
94
|
+
if (update.toolUseId) {
|
|
95
|
+
record.parentActivityId = update.toolUseId;
|
|
96
|
+
}
|
|
97
|
+
this.cancelRetention(record);
|
|
98
|
+
this.touch(update.taskId, record);
|
|
99
|
+
if (record.background) {
|
|
100
|
+
this.publish(update.taskId, record);
|
|
101
|
+
}
|
|
102
|
+
this.enforceLimit();
|
|
103
|
+
this.reconcileBackgroundTasksActive();
|
|
104
|
+
return true;
|
|
105
|
+
case 'updated':
|
|
106
|
+
if (update.patch.description) {
|
|
107
|
+
record.label = update.patch.description;
|
|
108
|
+
}
|
|
109
|
+
if (update.patch.is_backgrounded === true) {
|
|
110
|
+
record.background = true;
|
|
111
|
+
}
|
|
112
|
+
if (update.patch.status) {
|
|
113
|
+
const wasUnknown = record.state === 'unknown';
|
|
114
|
+
record.state = this.mapTaskStatus(update.patch.status);
|
|
115
|
+
this.cancelMissing(record);
|
|
116
|
+
if (wasUnknown && !this.isConcreteTerminal(record.state)) {
|
|
117
|
+
record.reason = undefined;
|
|
118
|
+
}
|
|
119
|
+
if (this.isConcreteTerminal(record.state)) {
|
|
120
|
+
this.scheduleRetention(update.taskId, record);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.cancelRetention(record);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (update.patch.error) {
|
|
127
|
+
record.reason = update.patch.error;
|
|
128
|
+
}
|
|
129
|
+
if (record.background) {
|
|
130
|
+
this.publish(update.taskId, record);
|
|
131
|
+
}
|
|
132
|
+
this.touch(update.taskId, record);
|
|
133
|
+
this.enforceLimit();
|
|
134
|
+
this.reconcileBackgroundTasksActive();
|
|
135
|
+
return true;
|
|
136
|
+
case 'notification':
|
|
137
|
+
if (update.toolUseId) {
|
|
138
|
+
record.parentActivityId = update.toolUseId;
|
|
139
|
+
}
|
|
140
|
+
record.state =
|
|
141
|
+
update.status === 'completed'
|
|
142
|
+
? 'completed'
|
|
143
|
+
: update.status === 'failed'
|
|
144
|
+
? 'failed'
|
|
145
|
+
: 'cancelled';
|
|
146
|
+
this.cancelMissing(record);
|
|
147
|
+
if (update.status !== 'completed' && update.summary.trim().length > 0) {
|
|
148
|
+
record.reason = update.summary;
|
|
149
|
+
}
|
|
150
|
+
this.scheduleRetention(update.taskId, record);
|
|
151
|
+
if (record.background) {
|
|
152
|
+
this.publish(update.taskId, record);
|
|
153
|
+
}
|
|
154
|
+
this.touch(update.taskId, record);
|
|
155
|
+
this.enforceLimit();
|
|
156
|
+
this.reconcileBackgroundTasksActive();
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
getOrCreate(taskId, label) {
|
|
161
|
+
const existing = this.tasks.get(taskId);
|
|
162
|
+
if (existing) {
|
|
163
|
+
this.touch(taskId, existing);
|
|
164
|
+
return existing;
|
|
165
|
+
}
|
|
166
|
+
const record = {
|
|
167
|
+
label,
|
|
168
|
+
background: false,
|
|
169
|
+
missingFromSnapshot: false,
|
|
170
|
+
state: 'running',
|
|
171
|
+
};
|
|
172
|
+
this.tasks.set(taskId, record);
|
|
173
|
+
return record;
|
|
174
|
+
}
|
|
175
|
+
mapTaskStatus(status) {
|
|
176
|
+
switch (status) {
|
|
177
|
+
case 'pending':
|
|
178
|
+
case 'running':
|
|
179
|
+
return 'running';
|
|
180
|
+
case 'paused':
|
|
181
|
+
return 'waiting';
|
|
182
|
+
case 'completed':
|
|
183
|
+
return 'completed';
|
|
184
|
+
case 'failed':
|
|
185
|
+
return 'failed';
|
|
186
|
+
case 'killed':
|
|
187
|
+
return 'cancelled';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
isConcreteTerminal(state) {
|
|
191
|
+
return state === 'completed' || state === 'failed' || state === 'cancelled';
|
|
192
|
+
}
|
|
193
|
+
scheduleMissing(taskId, record) {
|
|
194
|
+
if (record.missingFromSnapshot) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
record.missingFromSnapshot = true;
|
|
198
|
+
record.missingTimer = setTimeout(() => {
|
|
199
|
+
record.missingTimer = undefined;
|
|
200
|
+
if (!record.missingFromSnapshot || this.isConcreteTerminal(record.state)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
record.state = 'unknown';
|
|
204
|
+
record.reason =
|
|
205
|
+
'Provider background task disappeared before a terminal lifecycle event was observed';
|
|
206
|
+
this.publish(taskId, record);
|
|
207
|
+
this.reconcileBackgroundTasksActive();
|
|
208
|
+
this.scheduleRetention(taskId, record);
|
|
209
|
+
}, MISSING_TASK_GRACE_MS);
|
|
210
|
+
}
|
|
211
|
+
cancelMissing(record) {
|
|
212
|
+
record.missingFromSnapshot = false;
|
|
213
|
+
if (record.missingTimer) {
|
|
214
|
+
clearTimeout(record.missingTimer);
|
|
215
|
+
record.missingTimer = undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
scheduleRetention(taskId, record) {
|
|
219
|
+
this.cancelRetention(record);
|
|
220
|
+
record.retentionTimer = setTimeout(() => {
|
|
221
|
+
if (this.tasks.get(taskId) === record) {
|
|
222
|
+
this.deleteTask(taskId);
|
|
223
|
+
}
|
|
224
|
+
}, this.limits.terminalRetentionMs ?? TERMINAL_TASK_RETENTION_MS);
|
|
225
|
+
}
|
|
226
|
+
cancelRetention(record) {
|
|
227
|
+
if (record.retentionTimer) {
|
|
228
|
+
clearTimeout(record.retentionTimer);
|
|
229
|
+
record.retentionTimer = undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
touch(taskId, record) {
|
|
233
|
+
this.tasks.delete(taskId);
|
|
234
|
+
this.tasks.set(taskId, record);
|
|
235
|
+
}
|
|
236
|
+
enforceLimit() {
|
|
237
|
+
const limit = this.limits.maxTrackedTasks ?? MAX_TRACKED_TASKS;
|
|
238
|
+
const countsAsHistory = (record) => !record.background || this.isTerminal(record.state);
|
|
239
|
+
let historySize = [...this.tasks.values()].filter(countsAsHistory).length;
|
|
240
|
+
while (historySize > limit) {
|
|
241
|
+
const candidate = [...this.tasks].find(([, record]) => countsAsHistory(record) && !record.retentionTimer);
|
|
242
|
+
if (!candidate) {
|
|
243
|
+
// Live identities and terminal enrichment windows are required state.
|
|
244
|
+
// Their timers converge the temporary history excess after the window.
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.deleteTask(candidate[0]);
|
|
248
|
+
historySize -= 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
isTerminal(state) {
|
|
252
|
+
return this.isConcreteTerminal(state) || state === 'unknown';
|
|
253
|
+
}
|
|
254
|
+
deleteTask(taskId) {
|
|
255
|
+
const record = this.tasks.get(taskId);
|
|
256
|
+
if (!record) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.cancelMissing(record);
|
|
260
|
+
this.cancelRetention(record);
|
|
261
|
+
this.tasks.delete(taskId);
|
|
262
|
+
this.emitted.delete(taskId);
|
|
263
|
+
this.reconcileBackgroundTasksActive();
|
|
264
|
+
}
|
|
265
|
+
publish(taskId, record) {
|
|
266
|
+
if (!this.onProgress) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const run = {
|
|
270
|
+
providerRunId: taskId,
|
|
271
|
+
executionId: taskId,
|
|
272
|
+
label: record.label,
|
|
273
|
+
state: record.state,
|
|
274
|
+
...(record.parentActivityId ? { parentActivityId: record.parentActivityId } : {}),
|
|
275
|
+
...(record.reason ? { reason: record.reason } : {}),
|
|
276
|
+
stopSupported: this.limits.targetedCancellationSupported === true,
|
|
277
|
+
};
|
|
278
|
+
const fingerprint = JSON.stringify(run);
|
|
279
|
+
if (this.emitted.get(taskId) === fingerprint) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.emitted.set(taskId, fingerprint);
|
|
283
|
+
this.onProgress({ type: 'background_run', run });
|
|
284
|
+
}
|
|
285
|
+
reconcileBackgroundTasksActive() {
|
|
286
|
+
this.setBackgroundTasksActive([...this.tasks.values()].some((record) => record.background && !this.isTerminal(record.state)));
|
|
287
|
+
}
|
|
288
|
+
setBackgroundTasksActive(active) {
|
|
289
|
+
if (this.backgroundTasksActive === active) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.backgroundTasksActive = active;
|
|
293
|
+
this.limits.onBackgroundTasksActiveChanged?.(active);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function parseClaudeTaskLifecycleRecord(lifecycle) {
|
|
297
|
+
const event = lifecycle.event;
|
|
298
|
+
if (event === 'background_tasks_changed') {
|
|
299
|
+
if (!Array.isArray(lifecycle.tasks)) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const tasks = lifecycle.tasks.flatMap((task) => {
|
|
303
|
+
if (!isRecord(task)) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
const taskId = readOptionalString(task, 'taskId');
|
|
307
|
+
const taskType = readOptionalString(task, 'taskType');
|
|
308
|
+
const description = readOptionalString(task, 'description');
|
|
309
|
+
return taskId && taskType && description ? [{ taskId, taskType, description }] : [];
|
|
310
|
+
});
|
|
311
|
+
return tasks.length === lifecycle.tasks.length ? { event, tasks } : null;
|
|
312
|
+
}
|
|
313
|
+
const taskId = readOptionalString(lifecycle, 'taskId');
|
|
314
|
+
if (!taskId) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (event === 'started') {
|
|
318
|
+
const description = readOptionalString(lifecycle, 'description');
|
|
319
|
+
if (!description) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const toolUseId = readOptionalString(lifecycle, 'toolUseId');
|
|
323
|
+
const subagentType = readOptionalString(lifecycle, 'subagentType');
|
|
324
|
+
const taskType = readOptionalString(lifecycle, 'taskType');
|
|
325
|
+
return {
|
|
326
|
+
event,
|
|
327
|
+
taskId,
|
|
328
|
+
description,
|
|
329
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
330
|
+
...(subagentType ? { subagentType } : {}),
|
|
331
|
+
...(taskType ? { taskType } : {}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (event === 'updated') {
|
|
335
|
+
if (!isRecord(lifecycle.patch)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
const status = readOptionalString(lifecycle.patch, 'status');
|
|
339
|
+
const validStatuses = new Set([
|
|
340
|
+
'pending',
|
|
341
|
+
'running',
|
|
342
|
+
'completed',
|
|
343
|
+
'failed',
|
|
344
|
+
'killed',
|
|
345
|
+
'paused',
|
|
346
|
+
]);
|
|
347
|
+
if (status !== undefined && !validStatuses.has(status)) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const description = readOptionalString(lifecycle.patch, 'description');
|
|
351
|
+
const error = readOptionalString(lifecycle.patch, 'error');
|
|
352
|
+
return {
|
|
353
|
+
event,
|
|
354
|
+
taskId,
|
|
355
|
+
patch: {
|
|
356
|
+
...(status ? { status: status } : {}),
|
|
357
|
+
...(description ? { description } : {}),
|
|
358
|
+
...(error ? { error } : {}),
|
|
359
|
+
...(typeof lifecycle.patch.is_backgrounded === 'boolean'
|
|
360
|
+
? { is_backgrounded: lifecycle.patch.is_backgrounded }
|
|
361
|
+
: {}),
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (event === 'notification') {
|
|
366
|
+
const status = readOptionalString(lifecycle, 'status');
|
|
367
|
+
const summary = readOptionalString(lifecycle, 'summary');
|
|
368
|
+
if (!summary || (status !== 'completed' && status !== 'failed' && status !== 'stopped')) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const toolUseId = readOptionalString(lifecycle, 'toolUseId');
|
|
372
|
+
return {
|
|
373
|
+
event,
|
|
374
|
+
taskId,
|
|
375
|
+
status,
|
|
376
|
+
summary,
|
|
377
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|