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