@borgee/agents-host 0.2.35 → 0.2.56
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 +23 -27
- package/dist/agents-host.d.ts +28 -5
- package/dist/agents-host.js +274 -236
- package/dist/chat/chat-control-plane.d.ts +3 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +4 -3
- package/dist/chat/sdk-chat-control-plane.js +6 -0
- package/dist/cli.js +1 -5
- package/dist/compatibility-gates.d.ts +4 -0
- package/dist/compatibility-gates.js +18 -1
- package/dist/context/claude-file-brief.d.ts +2 -0
- package/dist/context/claude-file-brief.js +83 -0
- package/dist/context/compaction.d.ts +20 -0
- package/dist/context/compaction.js +59 -0
- package/dist/context/injection.d.ts +44 -9
- package/dist/context/injection.js +360 -53
- package/dist/context/main-session-delegation.d.ts +1 -1
- package/dist/context/projection-strategy.d.ts +24 -0
- package/dist/context/projection-strategy.js +90 -0
- package/dist/context/prompt.d.ts +16 -1
- package/dist/context/prompt.js +475 -53
- package/dist/context/resolved-workspace.d.ts +2 -0
- package/dist/context/resolved-workspace.js +64 -0
- package/dist/context/skill-manual.d.ts +14 -0
- package/dist/context/skill-manual.js +21 -0
- package/dist/context/turn-preparation.d.ts +8 -2
- package/dist/context/turn-preparation.js +68 -17
- package/dist/gateway/localhost-gateway.js +18 -8
- package/dist/hosted-turn-content.d.ts +15 -0
- package/dist/hosted-turn-content.js +50 -0
- package/dist/managed-daemon.d.ts +3 -2
- package/dist/managed-daemon.js +198 -37
- package/dist/plugin-sdk.js +276 -359
- package/dist/plugin-sdk.js.map +4 -4
- package/dist/progress-to-activity.d.ts +16 -0
- package/dist/progress-to-activity.js +24 -0
- package/dist/projection-strategy-values.d.ts +4 -0
- package/dist/projection-strategy-values.js +28 -0
- package/dist/providers/acp-progress-collector.d.ts +44 -0
- package/dist/providers/acp-progress-collector.js +130 -0
- package/dist/providers/awaiting-user.d.ts +2 -3
- package/dist/providers/awaiting-user.js +5 -7
- package/dist/providers/claude/activity-metadata.d.ts +14 -0
- package/dist/providers/claude/activity-metadata.js +81 -0
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +10 -0
- package/dist/providers/claude/cli-client.d.ts +9 -1
- package/dist/providers/claude/cli-client.js +270 -126
- package/dist/providers/codex/adapter.d.ts +3 -1
- package/dist/providers/codex/adapter.js +10 -0
- package/dist/providers/codex/cli-client.d.ts +10 -2
- package/dist/providers/codex/cli-client.js +88 -104
- package/dist/providers/codex/project-doc.js +22 -37
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +10 -0
- package/dist/providers/copilot/cli-client.d.ts +9 -1
- package/dist/providers/copilot/cli-client.js +68 -85
- package/dist/providers/create-provider.d.ts +3 -1
- package/dist/providers/create-provider.js +36 -9
- package/dist/providers/provider-adapter.d.ts +35 -0
- package/dist/providers/provider-adapter.js +44 -1
- package/dist/state-paths.d.ts +10 -2
- package/dist/state-paths.js +25 -6
- package/dist/task-thread-resolution.d.ts +3 -2
- package/dist/types.d.ts +163 -8
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +127 -38
- package/skills/borgee-agent/references/errors.md +38 -0
- package/skills/borgee-agent/references/task-properties.md +33 -0
- package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
- package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
- package/dist/durable-cursor-store.d.ts +0 -5
- package/dist/durable-cursor-store.js +0 -7
- package/skills/borgee-agent/borgee-agent.mjs +0 -562
- package/skills/borgee-agent/borgee-agent.py +0 -469
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
4
5
|
import spawn from 'cross-spawn';
|
|
6
|
+
import { DEFAULT_PROJECTION_STRATEGY, normalizeProjectionStrategy, } from '../../projection-strategy-values.js';
|
|
5
7
|
import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaudeCompatibilityAlias, } from '../../config.js';
|
|
6
8
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
7
9
|
import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
|
|
10
|
+
import { AcpProgressCollector } from '../acp-progress-collector.js';
|
|
11
|
+
import { readClaudeActivityMetadata } from './activity-metadata.js';
|
|
8
12
|
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
13
|
+
import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
|
|
9
14
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
10
15
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
11
16
|
const IDLE_BACKEND_STOPPED_MESSAGE = 'Claude ACP backend stopped after idle timeout';
|
|
@@ -23,6 +28,7 @@ const DEFAULT_SESSION_CAPABILITIES = {
|
|
|
23
28
|
loadSession: false,
|
|
24
29
|
resumeSession: false,
|
|
25
30
|
};
|
|
31
|
+
const CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV = 'CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD';
|
|
26
32
|
const require = createRequire(import.meta.url);
|
|
27
33
|
const DEFAULT_RUNTIME = {
|
|
28
34
|
spawn,
|
|
@@ -30,6 +36,7 @@ const DEFAULT_RUNTIME = {
|
|
|
30
36
|
ndJsonStream,
|
|
31
37
|
methods,
|
|
32
38
|
protocolVersion: PROTOCOL_VERSION,
|
|
39
|
+
fetch,
|
|
33
40
|
cwd: process.cwd(),
|
|
34
41
|
idleBackendShutdownMs: IDLE_BACKEND_SHUTDOWN_DISABLED_MS,
|
|
35
42
|
idleShutdownGracePeriodMs: DEFAULT_IDLE_SHUTDOWN_GRACE_PERIOD_MS,
|
|
@@ -42,6 +49,25 @@ function hasVisibleText(value) {
|
|
|
42
49
|
function normalizeError(error) {
|
|
43
50
|
return error instanceof Error ? error : new Error(String(error));
|
|
44
51
|
}
|
|
52
|
+
function isAbsoluteHttpUrl(value) {
|
|
53
|
+
return value.startsWith('http://') || value.startsWith('https://');
|
|
54
|
+
}
|
|
55
|
+
function resolveHostedAttachmentUrl(rawUrl, borgeeBaseUrl) {
|
|
56
|
+
if (isAbsoluteHttpUrl(rawUrl)) {
|
|
57
|
+
return rawUrl;
|
|
58
|
+
}
|
|
59
|
+
const base = borgeeBaseUrl.replace(/\/$/, '');
|
|
60
|
+
const path = rawUrl.startsWith('/') ? rawUrl : `/${rawUrl}`;
|
|
61
|
+
return `${base}${path}`;
|
|
62
|
+
}
|
|
63
|
+
function isSameOrigin(left, right) {
|
|
64
|
+
try {
|
|
65
|
+
return new URL(left).origin === new URL(right).origin;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
45
71
|
function parsePersistedSessionRecord(rawValue) {
|
|
46
72
|
const trimmed = rawValue.trim();
|
|
47
73
|
if (trimmed.startsWith('{')) {
|
|
@@ -90,85 +116,98 @@ class ClaudeCliProcessError extends Error {
|
|
|
90
116
|
this.stderrLineCount = stderrLineCount;
|
|
91
117
|
}
|
|
92
118
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
case 'completed':
|
|
97
|
-
return normalizedTitle ? `Completed ${normalizedTitle}` : 'Completed tool call';
|
|
98
|
-
case 'failed':
|
|
99
|
-
return normalizedTitle ? `Failed ${normalizedTitle}` : 'Tool call failed';
|
|
100
|
-
case 'pending':
|
|
101
|
-
case 'in_progress':
|
|
102
|
-
case undefined:
|
|
103
|
-
case null:
|
|
104
|
-
return normalizedTitle ? `Running ${normalizedTitle}…` : 'Running tool…';
|
|
105
|
-
default:
|
|
106
|
-
return normalizedTitle ? `${status} ${normalizedTitle}` : status;
|
|
119
|
+
function parseCompactionBanner(text) {
|
|
120
|
+
if (/(?:^|\n\n)Compacting\.\.\.\s*$/u.test(text)) {
|
|
121
|
+
return { stage: 'started' };
|
|
107
122
|
}
|
|
123
|
+
if (/(?:^|\n\n)Compacting completed\.\s*$/u.test(text)) {
|
|
124
|
+
return { stage: 'completed' };
|
|
125
|
+
}
|
|
126
|
+
const match = /(?:^|\n\n)Compacting failed(?::\s*(.+?)|\.)\s*$/u.exec(text);
|
|
127
|
+
if (!match) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
stage: 'failed',
|
|
132
|
+
...(match?.[1] ? { failureReason: match[1].trim() } : {}),
|
|
133
|
+
};
|
|
108
134
|
}
|
|
109
|
-
|
|
110
|
-
const current = entries.find((entry) => entry.status === 'in_progress') ??
|
|
111
|
-
entries.find((entry) => entry.status === 'pending') ??
|
|
112
|
-
entries[0];
|
|
113
|
-
return hasVisibleText(current?.content) ? `Plan: ${current.content.trim()}` : null;
|
|
114
|
-
}
|
|
115
|
-
class ClaudeProgressCollector {
|
|
135
|
+
class ClaudeCompactionObserver {
|
|
116
136
|
onProgress;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
answerText = '';
|
|
138
|
+
compactionState = 'idle';
|
|
139
|
+
lastCompactionBannerKey = null;
|
|
120
140
|
constructor(onProgress) {
|
|
121
141
|
this.onProgress = onProgress;
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (update.update.content.type !== 'text') {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
this.publicText += update.update.content.text;
|
|
130
|
-
this.publish(this.publicText);
|
|
131
|
-
return;
|
|
132
|
-
case 'tool_call':
|
|
133
|
-
this.toolTitles.set(update.update.toolCallId, update.update.title);
|
|
134
|
-
this.publishFallback(formatToolProgress(update.update.title, update.update.status));
|
|
135
|
-
return;
|
|
136
|
-
case 'tool_call_update': {
|
|
137
|
-
const nextTitle = update.update.title ?? this.toolTitles.get(update.update.toolCallId);
|
|
138
|
-
if (hasVisibleText(nextTitle)) {
|
|
139
|
-
this.toolTitles.set(update.update.toolCallId, nextTitle);
|
|
140
|
-
}
|
|
141
|
-
this.publishFallback(formatToolProgress(nextTitle, update.update.status));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
case 'plan':
|
|
145
|
-
this.publishFallback(formatPlanProgress(update.update.entries.map((entry) => ({
|
|
146
|
-
content: entry.content,
|
|
147
|
-
status: entry.status,
|
|
148
|
-
}))));
|
|
149
|
-
return;
|
|
150
|
-
default:
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
getFinalText() {
|
|
155
|
-
return this.publicText.trim();
|
|
143
|
+
consumeAnswerChunk(chunk) {
|
|
144
|
+
this.answerText += chunk;
|
|
145
|
+
this.publishCompactionFromText(this.answerText);
|
|
156
146
|
}
|
|
157
|
-
|
|
158
|
-
if (
|
|
147
|
+
consumeUsageUpdate(usedTokens, contextWindowTokens) {
|
|
148
|
+
if (!this.onProgress || this.compactionState !== 'completed-awaiting-usage') {
|
|
159
149
|
return;
|
|
160
150
|
}
|
|
161
|
-
this.
|
|
151
|
+
this.compactionState = 'idle';
|
|
152
|
+
this.onProgress({
|
|
153
|
+
type: 'compaction',
|
|
154
|
+
compaction: {
|
|
155
|
+
provider: 'claude',
|
|
156
|
+
stage: 'completed',
|
|
157
|
+
usedTokens,
|
|
158
|
+
contextWindowTokens,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
162
161
|
}
|
|
163
|
-
|
|
164
|
-
if (!this.onProgress
|
|
162
|
+
publishCompactionFromText(text) {
|
|
163
|
+
if (!this.onProgress) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const banner = parseCompactionBanner(text);
|
|
167
|
+
if (!banner) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const bannerKey = `${banner.stage}:${banner.stage === 'failed' ? banner.failureReason ?? '' : ''}:${text.trimEnd().length}`;
|
|
171
|
+
if (bannerKey === this.lastCompactionBannerKey) {
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
167
|
-
this.
|
|
168
|
-
|
|
174
|
+
this.lastCompactionBannerKey = bannerKey;
|
|
175
|
+
switch (banner.stage) {
|
|
176
|
+
case 'started':
|
|
177
|
+
this.compactionState = 'active';
|
|
178
|
+
this.onProgress({
|
|
179
|
+
type: 'compaction',
|
|
180
|
+
compaction: {
|
|
181
|
+
provider: 'claude',
|
|
182
|
+
stage: 'started',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
case 'completed':
|
|
187
|
+
this.compactionState = 'completed-awaiting-usage';
|
|
188
|
+
this.onProgress({
|
|
189
|
+
type: 'compaction',
|
|
190
|
+
compaction: {
|
|
191
|
+
provider: 'claude',
|
|
192
|
+
stage: 'completed',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
case 'failed':
|
|
197
|
+
this.compactionState = 'idle';
|
|
198
|
+
this.onProgress({
|
|
199
|
+
type: 'compaction',
|
|
200
|
+
compaction: {
|
|
201
|
+
provider: 'claude',
|
|
202
|
+
stage: 'failed',
|
|
203
|
+
...(banner.failureReason ? { failureReason: banner.failureReason } : {}),
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
169
208
|
}
|
|
170
209
|
}
|
|
171
|
-
function createDeferredTurn(channelId,
|
|
210
|
+
function createDeferredTurn(channelId, preparedTurn, sessionPersistence, options) {
|
|
172
211
|
let settled = false;
|
|
173
212
|
let resolvePromise;
|
|
174
213
|
let rejectPromise;
|
|
@@ -178,9 +217,8 @@ function createDeferredTurn(channelId, prompt, sessionPersistence, promptContext
|
|
|
178
217
|
});
|
|
179
218
|
return {
|
|
180
219
|
channelId,
|
|
181
|
-
|
|
220
|
+
preparedTurn,
|
|
182
221
|
sessionPersistence,
|
|
183
|
-
promptContext,
|
|
184
222
|
options,
|
|
185
223
|
promise,
|
|
186
224
|
resolve(value) {
|
|
@@ -204,6 +242,9 @@ function resolveSessionRouting(turn) {
|
|
|
204
242
|
persistence: turn.providerSessionRouting?.persistence ?? 'persistent',
|
|
205
243
|
};
|
|
206
244
|
}
|
|
245
|
+
function asImagePart(part) {
|
|
246
|
+
return part.type === 'image' ? part : null;
|
|
247
|
+
}
|
|
207
248
|
function asObject(value) {
|
|
208
249
|
return typeof value === 'object' && value !== null ? value : null;
|
|
209
250
|
}
|
|
@@ -267,68 +308,120 @@ function resolveClaudeLaunch(command, args, baseCwd) {
|
|
|
267
308
|
args: args.map((arg) => (isRelativePathLike(arg) ? resolve(baseCwd, arg) : arg)),
|
|
268
309
|
};
|
|
269
310
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
311
|
+
function resolveClaudeSdkCompactionControl() {
|
|
312
|
+
const rawThreshold = process.env[CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV];
|
|
313
|
+
if (!hasVisibleText(rawThreshold)) {
|
|
272
314
|
return undefined;
|
|
273
315
|
}
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
lines.push(`Channel context payload: ${promptContext.channelContextPayloadPath}`);
|
|
278
|
-
}
|
|
279
|
-
if (promptContext.skillRuntime) {
|
|
280
|
-
lines.push(`Skill guide: ${promptContext.skillRuntime.skillMarkdownPath}`);
|
|
281
|
-
lines.push(`Node bootstrap CLI: node ${promptContext.skillRuntime.nodeCliPath} --context ${promptContext.channelContextPayloadPath} --print-bootstrap`);
|
|
282
|
-
lines.push(`Python bootstrap CLI: python3 ${promptContext.skillRuntime.pythonCliPath} --context ${promptContext.channelContextPayloadPath} --print-bootstrap`);
|
|
283
|
-
}
|
|
284
|
-
if (promptContext.localhostGateway && promptContext.gatewayAuthPath) {
|
|
285
|
-
lines.push(`Gateway auth sidecar for this session: ${promptContext.gatewayAuthPath}`);
|
|
286
|
-
if (promptContext.localhostGateway.baseUrl) {
|
|
287
|
-
lines.push(`Loopback gateway base URL: ${promptContext.localhostGateway.baseUrl}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (promptContext.taskWorkspace) {
|
|
291
|
-
lines.push(`Task-scoped writable workspace root: ${promptContext.taskWorkspace.rootPath}.`);
|
|
292
|
-
lines.push('This writable workspace is local to agents-host for the current task thread and does not imply that the target repository is already checked out there.');
|
|
293
|
-
}
|
|
294
|
-
if (promptContext.taskAssignmentContext?.currentTaskId) {
|
|
295
|
-
lines.push(`Current assigned task id: ${promptContext.taskAssignmentContext.currentTaskId}.`);
|
|
316
|
+
const parsedThreshold = Number(rawThreshold.trim());
|
|
317
|
+
if (!Number.isSafeInteger(parsedThreshold) || parsedThreshold <= 0) {
|
|
318
|
+
throw new Error(`Invalid ${CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV}: expected a positive integer context token threshold for the experiment-scoped Claude SDK compaction opt-in`);
|
|
296
319
|
}
|
|
297
|
-
return
|
|
320
|
+
return {
|
|
321
|
+
enabled: true,
|
|
322
|
+
context_token_threshold: parsedThreshold,
|
|
323
|
+
};
|
|
298
324
|
}
|
|
299
|
-
function buildClaudeSessionMeta(
|
|
300
|
-
const
|
|
301
|
-
if (!systemPromptAppend) {
|
|
325
|
+
function buildClaudeSessionMeta(systemPromptAppend) {
|
|
326
|
+
const sdkCompactionControl = resolveClaudeSdkCompactionControl();
|
|
327
|
+
if (!systemPromptAppend && !sdkCompactionControl) {
|
|
302
328
|
return undefined;
|
|
303
329
|
}
|
|
304
330
|
return {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
331
|
+
...(systemPromptAppend
|
|
332
|
+
? {
|
|
333
|
+
systemPrompt: {
|
|
334
|
+
append: systemPromptAppend,
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
: {}),
|
|
338
|
+
...(sdkCompactionControl
|
|
339
|
+
? {
|
|
340
|
+
claudeCode: {
|
|
341
|
+
options: {
|
|
342
|
+
compaction_control: sdkCompactionControl,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
: {}),
|
|
308
347
|
};
|
|
309
348
|
}
|
|
349
|
+
function buildFallbackClaudeSessionPrompt(agentName, promptContext, promptStrategy) {
|
|
350
|
+
if (!promptContext) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
return buildClaudeSessionPromptAppend({
|
|
354
|
+
agentName: agentName ?? 'Borgee agent',
|
|
355
|
+
promptContext,
|
|
356
|
+
promptStrategy,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function resolveProjectionStrategy(promptStrategy) {
|
|
360
|
+
return normalizeProjectionStrategy(promptStrategy) ?? DEFAULT_PROJECTION_STRATEGY;
|
|
361
|
+
}
|
|
362
|
+
function resolveTurnSessionPromptAppend(turn) {
|
|
363
|
+
const promptStrategy = resolveProjectionStrategy(turn.projectionStrategy);
|
|
364
|
+
if (promptStrategy === 'turn-full') {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
return turn.claudeSessionPromptAppend
|
|
368
|
+
?? buildFallbackClaudeSessionPrompt(turn.agentName, turn.promptContext, promptStrategy);
|
|
369
|
+
}
|
|
310
370
|
function resolveSessionAdditionalDirectories(promptContext) {
|
|
311
371
|
const directories = new Set();
|
|
312
372
|
if (promptContext?.channelContextPayloadPath) {
|
|
313
373
|
directories.add(dirname(promptContext.channelContextPayloadPath));
|
|
314
374
|
}
|
|
315
|
-
if (promptContext?.
|
|
316
|
-
directories.add(dirname(promptContext.
|
|
375
|
+
if (promptContext?.gatewayCredentialPath) {
|
|
376
|
+
directories.add(dirname(promptContext.gatewayCredentialPath));
|
|
317
377
|
}
|
|
318
378
|
if (promptContext?.skillRuntime?.skillDirectoryPath) {
|
|
319
379
|
directories.add(promptContext.skillRuntime.skillDirectoryPath);
|
|
320
380
|
}
|
|
321
381
|
return directories.size > 0 ? [...directories].sort((left, right) => left.localeCompare(right)) : undefined;
|
|
322
382
|
}
|
|
323
|
-
function
|
|
383
|
+
function hashSessionAppend(systemPromptAppend) {
|
|
384
|
+
if (!systemPromptAppend) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
return createHash('sha256').update(systemPromptAppend).digest('hex');
|
|
388
|
+
}
|
|
389
|
+
function resolveSessionVisibilityKey(promptContext, systemPromptAppend, promptStrategy) {
|
|
390
|
+
const resolvedPromptStrategy = resolveProjectionStrategy(promptStrategy);
|
|
391
|
+
const explicitPromptStrategy = resolvedPromptStrategy === DEFAULT_PROJECTION_STRATEGY && promptStrategy == null
|
|
392
|
+
? undefined
|
|
393
|
+
: resolvedPromptStrategy === DEFAULT_PROJECTION_STRATEGY
|
|
394
|
+
? undefined
|
|
395
|
+
: resolvedPromptStrategy;
|
|
324
396
|
const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
|
|
325
|
-
|
|
397
|
+
const sessionAppendHash = hashSessionAppend(systemPromptAppend);
|
|
398
|
+
const projectedBriefHash = promptContext?.claudeProjectedBriefHash;
|
|
399
|
+
if (!additionalDirectories
|
|
400
|
+
&& !sessionAppendHash
|
|
401
|
+
&& !projectedBriefHash
|
|
402
|
+
&& !promptContext?.gatewayCredentialPath
|
|
403
|
+
&& !explicitPromptStrategy) {
|
|
326
404
|
return undefined;
|
|
327
405
|
}
|
|
328
|
-
|
|
406
|
+
if (sessionAppendHash) {
|
|
407
|
+
return JSON.stringify({
|
|
408
|
+
...(additionalDirectories ? { additionalDirectories } : {}),
|
|
409
|
+
sessionAppendHash,
|
|
410
|
+
...(projectedBriefHash ? { projectedBriefHash } : {}),
|
|
411
|
+
...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return JSON.stringify({
|
|
415
|
+
...(additionalDirectories ? { additionalDirectories } : {}),
|
|
416
|
+
...(promptContext?.gatewayCredentialPath
|
|
417
|
+
? { gatewayCredentialPath: promptContext.gatewayCredentialPath }
|
|
418
|
+
: {}),
|
|
419
|
+
...(projectedBriefHash ? { projectedBriefHash } : {}),
|
|
420
|
+
...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
|
|
421
|
+
});
|
|
329
422
|
}
|
|
330
|
-
function buildSessionRequest(cwd,
|
|
331
|
-
const meta = buildClaudeSessionMeta(
|
|
423
|
+
function buildSessionRequest(cwd, systemPromptAppend, additionalDirectories) {
|
|
424
|
+
const meta = buildClaudeSessionMeta(systemPromptAppend);
|
|
332
425
|
return {
|
|
333
426
|
cwd,
|
|
334
427
|
mcpServers: [],
|
|
@@ -369,6 +462,7 @@ export class ClaudeCliClient {
|
|
|
369
462
|
sessionStore;
|
|
370
463
|
resolveSessionStoreAgentId;
|
|
371
464
|
logger;
|
|
465
|
+
imageInputConfig;
|
|
372
466
|
runtime;
|
|
373
467
|
channels = new Map();
|
|
374
468
|
/**
|
|
@@ -407,12 +501,13 @@ export class ClaudeCliClient {
|
|
|
407
501
|
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
408
502
|
idleBackendShutdown;
|
|
409
503
|
childStderr = '';
|
|
410
|
-
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger()) {
|
|
504
|
+
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), imageInputConfig = {}) {
|
|
411
505
|
this.command = command;
|
|
412
506
|
this.args = args;
|
|
413
507
|
this.sessionStore = sessionStore;
|
|
414
508
|
this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
|
|
415
509
|
this.logger = logger;
|
|
510
|
+
this.imageInputConfig = imageInputConfig;
|
|
416
511
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
417
512
|
assertClaudeCommandCompatibility(command, args, 'Claude provider runtime');
|
|
418
513
|
this.idleBackendShutdown = new IdleBackendShutdownScheduler({
|
|
@@ -434,19 +529,26 @@ export class ClaudeCliClient {
|
|
|
434
529
|
if (this.fatalError) {
|
|
435
530
|
throw this.fatalError;
|
|
436
531
|
}
|
|
437
|
-
const
|
|
532
|
+
const preparedTurnBase = typeof channelIdOrTurn === 'string'
|
|
438
533
|
? {
|
|
439
534
|
channelId: channelIdOrTurn,
|
|
535
|
+
incomingContent: '',
|
|
536
|
+
incomingParts: [],
|
|
440
537
|
prompt: typeof promptOrOptions === 'string' ? promptOrOptions : '',
|
|
441
538
|
}
|
|
442
539
|
: channelIdOrTurn;
|
|
540
|
+
const preparedTurn = {
|
|
541
|
+
...preparedTurnBase,
|
|
542
|
+
incomingContent: preparedTurnBase.incomingContent ?? '',
|
|
543
|
+
incomingParts: preparedTurnBase.incomingParts ?? [],
|
|
544
|
+
};
|
|
443
545
|
const options = typeof channelIdOrTurn === 'string'
|
|
444
546
|
? maybeOptions
|
|
445
547
|
: promptOrOptions;
|
|
446
548
|
const sessionRouting = resolveSessionRouting(preparedTurn);
|
|
447
549
|
const state = this.getOrCreateChannelState(sessionRouting.key, preparedTurn.channelId, sessionRouting.persistence);
|
|
448
550
|
this.idleBackendShutdown.cancel();
|
|
449
|
-
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn
|
|
551
|
+
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn, sessionRouting.persistence, options);
|
|
450
552
|
state.queue.push(turn);
|
|
451
553
|
this.processChannelQueue(sessionRouting.key, state);
|
|
452
554
|
return turn.promise;
|
|
@@ -618,14 +720,16 @@ export class ClaudeCliClient {
|
|
|
618
720
|
}
|
|
619
721
|
state.activeTurn = turn;
|
|
620
722
|
try {
|
|
621
|
-
state.cwd = this.resolveSessionCwd(turn.promptContext);
|
|
622
|
-
|
|
723
|
+
state.cwd = this.resolveSessionCwd(turn.preparedTurn.promptContext);
|
|
724
|
+
const sessionPromptAppend = resolveTurnSessionPromptAppend(turn.preparedTurn);
|
|
725
|
+
state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext, sessionPromptAppend, turn.preparedTurn.projectionStrategy);
|
|
623
726
|
await this.ensureStarted();
|
|
624
727
|
await this.recycleSessionIfScopeChanged(channelId, state);
|
|
625
|
-
const session = await this.getOrCreateSession(channelId, state, turn.
|
|
728
|
+
const session = await this.getOrCreateSession(channelId, state, turn.preparedTurn, sessionPromptAppend);
|
|
626
729
|
let reply;
|
|
627
730
|
try {
|
|
628
|
-
|
|
731
|
+
const promptInput = await this.buildPromptInput(turn.preparedTurn);
|
|
732
|
+
reply = await this.runTurn(session, promptInput, turn.options);
|
|
629
733
|
}
|
|
630
734
|
catch (error) {
|
|
631
735
|
if (isSessionTainted(error)) {
|
|
@@ -665,7 +769,7 @@ export class ClaudeCliClient {
|
|
|
665
769
|
}
|
|
666
770
|
})();
|
|
667
771
|
}
|
|
668
|
-
async getOrCreateSession(channelId, state,
|
|
772
|
+
async getOrCreateSession(channelId, state, turn, systemPromptAppend) {
|
|
669
773
|
if (state.session) {
|
|
670
774
|
return state.session;
|
|
671
775
|
}
|
|
@@ -679,10 +783,10 @@ export class ClaudeCliClient {
|
|
|
679
783
|
const persistedSessionId = state.sessionPersistence === 'persistent'
|
|
680
784
|
? await this.readPersistedSessionId(channelId, cwd, state.visibilityKey)
|
|
681
785
|
: undefined;
|
|
682
|
-
const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
|
|
786
|
+
const additionalDirectories = resolveSessionAdditionalDirectories(turn.promptContext);
|
|
683
787
|
const sessionPromise = persistedSessionId
|
|
684
|
-
? this.restoreOrCreateSession(channelId, persistedSessionId, cwd,
|
|
685
|
-
: this.startFreshSession(channelId, cwd,
|
|
788
|
+
? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, systemPromptAppend, additionalDirectories)
|
|
789
|
+
: this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
|
|
686
790
|
this.pendingSessionStarts.add(sessionPromise);
|
|
687
791
|
state.sessionPromise = sessionPromise;
|
|
688
792
|
let sessionAdopted = false;
|
|
@@ -721,11 +825,11 @@ export class ClaudeCliClient {
|
|
|
721
825
|
}
|
|
722
826
|
}
|
|
723
827
|
}
|
|
724
|
-
async startFreshSession(channelId, cwd,
|
|
828
|
+
async startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories) {
|
|
725
829
|
if (!this.connection) {
|
|
726
830
|
throw new Error('Claude ACP connection is not available');
|
|
727
831
|
}
|
|
728
|
-
const session = await this.connection.agent.buildSession(buildSessionRequest(cwd,
|
|
832
|
+
const session = await this.connection.agent.buildSession(buildSessionRequest(cwd, systemPromptAppend, additionalDirectories)).start();
|
|
729
833
|
this.logger?.debug('started fresh Claude ACP session', { channelId, cwd });
|
|
730
834
|
const state = this.channels.get(channelId);
|
|
731
835
|
if (state?.sessionPersistence === 'persistent') {
|
|
@@ -733,13 +837,13 @@ export class ClaudeCliClient {
|
|
|
733
837
|
}
|
|
734
838
|
return session;
|
|
735
839
|
}
|
|
736
|
-
async restoreOrCreateSession(channelId, sessionId, cwd,
|
|
840
|
+
async restoreOrCreateSession(channelId, sessionId, cwd, systemPromptAppend, additionalDirectories) {
|
|
737
841
|
if (!this.sessionCapabilities.resumeSession) {
|
|
738
842
|
this.logger?.debug('Claude ACP restore unsupported; starting fresh session', { channelId });
|
|
739
|
-
return this.startFreshSession(channelId, cwd,
|
|
843
|
+
return this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
|
|
740
844
|
}
|
|
741
845
|
try {
|
|
742
|
-
const session = await this.restoreSession(sessionId, cwd,
|
|
846
|
+
const session = await this.restoreSession(sessionId, cwd, systemPromptAppend, additionalDirectories);
|
|
743
847
|
this.logger?.debug('restored Claude ACP session', {
|
|
744
848
|
channelId,
|
|
745
849
|
restoreMethod: this.sessionCapabilities.resumeSession ? 'session/resume' : 'session/load',
|
|
@@ -752,10 +856,10 @@ export class ClaudeCliClient {
|
|
|
752
856
|
}
|
|
753
857
|
this.logger?.debug('discarded stale Claude ACP session and started fresh', { channelId });
|
|
754
858
|
await this.clearPersistedSessionBestEffort(channelId);
|
|
755
|
-
return this.startFreshSession(channelId, cwd,
|
|
859
|
+
return this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
|
|
756
860
|
}
|
|
757
861
|
}
|
|
758
|
-
async restoreSession(sessionId, cwd,
|
|
862
|
+
async restoreSession(sessionId, cwd, systemPromptAppend, additionalDirectories) {
|
|
759
863
|
if (!this.connection) {
|
|
760
864
|
throw new Error('Claude ACP connection is not available');
|
|
761
865
|
}
|
|
@@ -767,7 +871,7 @@ export class ClaudeCliClient {
|
|
|
767
871
|
try {
|
|
768
872
|
const request = {
|
|
769
873
|
sessionId,
|
|
770
|
-
...buildSessionRequest(cwd,
|
|
874
|
+
...buildSessionRequest(cwd, systemPromptAppend, additionalDirectories),
|
|
771
875
|
};
|
|
772
876
|
if (this.sessionCapabilities.resumeSession) {
|
|
773
877
|
await this.connection.agent.request(this.runtime.methods.agent.session.resume, request);
|
|
@@ -792,7 +896,7 @@ export class ClaudeCliClient {
|
|
|
792
896
|
}
|
|
793
897
|
}
|
|
794
898
|
resolveSessionCwd(promptContext) {
|
|
795
|
-
return promptContext?.
|
|
899
|
+
return promptContext?.resolvedWorkspace?.rootPath ?? this.runtime.cwd;
|
|
796
900
|
}
|
|
797
901
|
async recycleSessionIfScopeChanged(channelId, state) {
|
|
798
902
|
const session = state.session;
|
|
@@ -824,7 +928,8 @@ export class ClaudeCliClient {
|
|
|
824
928
|
const promptFailure = new Promise((_, reject) => {
|
|
825
929
|
void promptPromise.catch((error) => reject(markSessionTainted(error)));
|
|
826
930
|
});
|
|
827
|
-
const collector = new
|
|
931
|
+
const collector = new AcpProgressCollector(options?.onProgress, readClaudeActivityMetadata);
|
|
932
|
+
const compactionObserver = new ClaudeCompactionObserver(options?.onProgress);
|
|
828
933
|
for (;;) {
|
|
829
934
|
let update;
|
|
830
935
|
try {
|
|
@@ -850,6 +955,12 @@ export class ClaudeCliClient {
|
|
|
850
955
|
}
|
|
851
956
|
return output;
|
|
852
957
|
}
|
|
958
|
+
if (update.update.sessionUpdate === 'agent_message_chunk' && update.update.content.type === 'text') {
|
|
959
|
+
compactionObserver.consumeAnswerChunk(update.update.content.text);
|
|
960
|
+
}
|
|
961
|
+
if (update.update.sessionUpdate === 'usage_update') {
|
|
962
|
+
compactionObserver.consumeUsageUpdate(update.update.used, update.update.size);
|
|
963
|
+
}
|
|
853
964
|
collector.consume(update);
|
|
854
965
|
}
|
|
855
966
|
}
|
|
@@ -1199,4 +1310,37 @@ export class ClaudeCliClient {
|
|
|
1199
1310
|
}
|
|
1200
1311
|
}
|
|
1201
1312
|
}
|
|
1313
|
+
async buildPromptInput(turn) {
|
|
1314
|
+
const imageParts = turn.incomingParts.map(asImagePart).filter((part) => part !== null);
|
|
1315
|
+
if (imageParts.length === 0) {
|
|
1316
|
+
return turn.prompt;
|
|
1317
|
+
}
|
|
1318
|
+
const imageBlocks = await Promise.all(imageParts.map(async (part) => this.fetchImageBlock(part.attachment.url, part.attachment.contentType)));
|
|
1319
|
+
return [
|
|
1320
|
+
{ type: 'text', text: turn.prompt },
|
|
1321
|
+
...imageBlocks,
|
|
1322
|
+
];
|
|
1323
|
+
}
|
|
1324
|
+
async fetchImageBlock(rawUrl, contentType) {
|
|
1325
|
+
const baseUrl = this.imageInputConfig.borgeeBaseUrl?.trim();
|
|
1326
|
+
const agentApiKey = this.imageInputConfig.agentApiKey?.trim();
|
|
1327
|
+
if (!baseUrl || !agentApiKey) {
|
|
1328
|
+
throw new Error('Claude image input requires Borgee base URL and agent API key');
|
|
1329
|
+
}
|
|
1330
|
+
const resolvedUrl = resolveHostedAttachmentUrl(rawUrl, baseUrl);
|
|
1331
|
+
if (!isSameOrigin(resolvedUrl, baseUrl)) {
|
|
1332
|
+
throw new Error(`Claude image input only supports Borgee-origin attachments: ${resolvedUrl}`);
|
|
1333
|
+
}
|
|
1334
|
+
const init = { headers: { Authorization: `Bearer ${agentApiKey}` } };
|
|
1335
|
+
const response = await this.runtime.fetch(resolvedUrl, init);
|
|
1336
|
+
if (!response.ok) {
|
|
1337
|
+
throw new Error(`Claude image fetch failed with ${response.status} ${response.statusText}`.trim());
|
|
1338
|
+
}
|
|
1339
|
+
const blob = Buffer.from(await response.arrayBuffer()).toString('base64');
|
|
1340
|
+
return {
|
|
1341
|
+
type: 'image',
|
|
1342
|
+
mimeType: contentType,
|
|
1343
|
+
data: blob,
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1202
1346
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ProviderAdapter } from '../provider-adapter.js';
|
|
2
2
|
import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../../types.js';
|
|
3
3
|
import { ProviderTurnPreparer } from '../../context/turn-preparation.js';
|
|
4
4
|
import { CodexCliClient } from './cli-client.js';
|
|
5
|
+
export declare const CODEX_HOSTED_PROVIDER_CAPABILITIES: import("../provider-adapter.js").ProviderCapabilities;
|
|
5
6
|
export declare class CodexProviderAdapter implements ProviderAdapter {
|
|
6
7
|
private readonly cli;
|
|
7
8
|
private readonly turnPreparer;
|
|
9
|
+
readonly capabilities: import("../provider-adapter.js").ProviderCapabilities;
|
|
8
10
|
constructor(cli: CodexCliClient, turnPreparer: ProviderTurnPreparer);
|
|
9
11
|
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
10
12
|
dispose(): Promise<void>;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import { createHostedProviderCapabilities } from '../provider-adapter.js';
|
|
1
2
|
import { createAwaitingUserProgressHandler, parseProviderReply } from '../awaiting-user.js';
|
|
3
|
+
export const CODEX_HOSTED_PROVIDER_CAPABILITIES = createHostedProviderCapabilities({
|
|
4
|
+
imageInputTransport: {
|
|
5
|
+
source: 'transport-derived',
|
|
6
|
+
support: 'supported',
|
|
7
|
+
reason: 'real-media-delivered',
|
|
8
|
+
delivery: ['blob'],
|
|
9
|
+
},
|
|
10
|
+
});
|
|
2
11
|
export class CodexProviderAdapter {
|
|
3
12
|
cli;
|
|
4
13
|
turnPreparer;
|
|
14
|
+
capabilities = CODEX_HOSTED_PROVIDER_CAPABILITIES;
|
|
5
15
|
constructor(cli, turnPreparer) {
|
|
6
16
|
this.cli = cli;
|
|
7
17
|
this.turnPreparer = turnPreparer;
|