@borgee/agents-host 0.2.35 → 0.2.44
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 +2 -0
- package/dist/agents-host.js +111 -44
- package/dist/chat/sdk-chat-control-plane.js +3 -0
- package/dist/context/injection.d.ts +5 -3
- package/dist/context/injection.js +45 -29
- package/dist/context/prompt.js +23 -35
- package/dist/context/skill-manual.d.ts +13 -0
- package/dist/context/skill-manual.js +18 -0
- package/dist/context/turn-preparation.js +13 -4
- package/dist/gateway/localhost-gateway.js +16 -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 +77 -29
- 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 +11 -2
- package/dist/providers/claude/cli-client.js +98 -27
- 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 +85 -21
- package/dist/providers/codex/project-doc.js +13 -29
- 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 +71 -8
- package/dist/providers/create-provider.d.ts +1 -1
- package/dist/providers/create-provider.js +16 -5
- package/dist/providers/provider-adapter.d.ts +35 -0
- package/dist/providers/provider-adapter.js +44 -1
- package/dist/state-paths.d.ts +9 -1
- package/dist/state-paths.js +22 -3
- package/dist/types.d.ts +33 -2
- package/package.json +1 -1
- package/skills/borgee-agent/SKILL.md +119 -38
- package/skills/borgee-agent/references/errors.md +38 -0
- package/skills/borgee-agent/references/task-properties.md +30 -0
- package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
- package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
- package/skills/borgee-agent/borgee-agent.mjs +0 -562
- package/skills/borgee-agent/borgee-agent.py +0 -469
|
@@ -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 { PreparedPromptContext, PreparedProviderTurnInput, ProviderGenerateOptions } from '../../types.js';
|
|
5
5
|
import type { ClaudeChannelSessionStore } from './session-store.js';
|
|
6
6
|
interface ClaudeAcpRuntime {
|
|
7
7
|
spawn: typeof spawn;
|
|
@@ -9,12 +9,18 @@ interface ClaudeAcpRuntime {
|
|
|
9
9
|
ndJsonStream: typeof ndJsonStream;
|
|
10
10
|
methods: typeof methods;
|
|
11
11
|
protocolVersion: typeof PROTOCOL_VERSION;
|
|
12
|
+
fetch: typeof fetch;
|
|
12
13
|
cwd: string;
|
|
13
14
|
idleBackendShutdownMs: number;
|
|
14
15
|
idleShutdownGracePeriodMs: number;
|
|
15
16
|
shutdownGracePeriodMs: number;
|
|
16
17
|
shutdownForceKillWaitMs: number;
|
|
17
18
|
}
|
|
19
|
+
interface ClaudeHostedImageInputConfig {
|
|
20
|
+
borgeeBaseUrl?: string;
|
|
21
|
+
agentApiKey?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function buildClaudeSessionSystemPrompt(promptContext?: PreparedPromptContext): string | undefined;
|
|
18
24
|
/**
|
|
19
25
|
* Persistent ACP-backed client for the Claude ACP adapter.
|
|
20
26
|
*
|
|
@@ -29,6 +35,7 @@ export declare class ClaudeCliClient {
|
|
|
29
35
|
private readonly sessionStore?;
|
|
30
36
|
private readonly resolveSessionStoreAgentId;
|
|
31
37
|
private readonly logger;
|
|
38
|
+
private readonly imageInputConfig;
|
|
32
39
|
private readonly runtime;
|
|
33
40
|
private readonly channels;
|
|
34
41
|
/**
|
|
@@ -65,7 +72,7 @@ export declare class ClaudeCliClient {
|
|
|
65
72
|
private sessionCapabilities;
|
|
66
73
|
private readonly idleBackendShutdown;
|
|
67
74
|
private childStderr;
|
|
68
|
-
constructor(command: string, args?: string[], runtimeOverrides?: Partial<ClaudeAcpRuntime>, sessionStore?: ClaudeChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined, logger?: DebugLogger);
|
|
75
|
+
constructor(command: string, args?: string[], runtimeOverrides?: Partial<ClaudeAcpRuntime>, sessionStore?: ClaudeChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined, logger?: DebugLogger, imageInputConfig?: ClaudeHostedImageInputConfig);
|
|
69
76
|
generateReply(turn: PreparedProviderTurnInput, options?: ProviderGenerateOptions): Promise<string>;
|
|
70
77
|
generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
|
|
71
78
|
dispose(): Promise<void>;
|
|
@@ -110,5 +117,7 @@ export declare class ClaudeCliClient {
|
|
|
110
117
|
private waitForPendingSessionStarts;
|
|
111
118
|
private waitForPendingSessionCloses;
|
|
112
119
|
private waitForChildExit;
|
|
120
|
+
private buildPromptInput;
|
|
121
|
+
private fetchImageBlock;
|
|
113
122
|
}
|
|
114
123
|
export {};
|
|
@@ -6,6 +6,7 @@ import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaud
|
|
|
6
6
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
7
7
|
import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
|
|
8
8
|
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
9
|
+
import { buildSkillManualLines } from '../../context/skill-manual.js';
|
|
9
10
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
10
11
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
11
12
|
const IDLE_BACKEND_STOPPED_MESSAGE = 'Claude ACP backend stopped after idle timeout';
|
|
@@ -30,6 +31,7 @@ const DEFAULT_RUNTIME = {
|
|
|
30
31
|
ndJsonStream,
|
|
31
32
|
methods,
|
|
32
33
|
protocolVersion: PROTOCOL_VERSION,
|
|
34
|
+
fetch,
|
|
33
35
|
cwd: process.cwd(),
|
|
34
36
|
idleBackendShutdownMs: IDLE_BACKEND_SHUTDOWN_DISABLED_MS,
|
|
35
37
|
idleShutdownGracePeriodMs: DEFAULT_IDLE_SHUTDOWN_GRACE_PERIOD_MS,
|
|
@@ -42,6 +44,25 @@ function hasVisibleText(value) {
|
|
|
42
44
|
function normalizeError(error) {
|
|
43
45
|
return error instanceof Error ? error : new Error(String(error));
|
|
44
46
|
}
|
|
47
|
+
function isAbsoluteHttpUrl(value) {
|
|
48
|
+
return value.startsWith('http://') || value.startsWith('https://');
|
|
49
|
+
}
|
|
50
|
+
function resolveHostedAttachmentUrl(rawUrl, borgeeBaseUrl) {
|
|
51
|
+
if (isAbsoluteHttpUrl(rawUrl)) {
|
|
52
|
+
return rawUrl;
|
|
53
|
+
}
|
|
54
|
+
const base = borgeeBaseUrl.replace(/\/$/, '');
|
|
55
|
+
const path = rawUrl.startsWith('/') ? rawUrl : `/${rawUrl}`;
|
|
56
|
+
return `${base}${path}`;
|
|
57
|
+
}
|
|
58
|
+
function isSameOrigin(left, right) {
|
|
59
|
+
try {
|
|
60
|
+
return new URL(left).origin === new URL(right).origin;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
45
66
|
function parsePersistedSessionRecord(rawValue) {
|
|
46
67
|
const trimmed = rawValue.trim();
|
|
47
68
|
if (trimmed.startsWith('{')) {
|
|
@@ -168,7 +189,7 @@ class ClaudeProgressCollector {
|
|
|
168
189
|
this.onProgress({ text });
|
|
169
190
|
}
|
|
170
191
|
}
|
|
171
|
-
function createDeferredTurn(channelId,
|
|
192
|
+
function createDeferredTurn(channelId, preparedTurn, sessionPersistence, options) {
|
|
172
193
|
let settled = false;
|
|
173
194
|
let resolvePromise;
|
|
174
195
|
let rejectPromise;
|
|
@@ -178,9 +199,8 @@ function createDeferredTurn(channelId, prompt, sessionPersistence, promptContext
|
|
|
178
199
|
});
|
|
179
200
|
return {
|
|
180
201
|
channelId,
|
|
181
|
-
|
|
202
|
+
preparedTurn,
|
|
182
203
|
sessionPersistence,
|
|
183
|
-
promptContext,
|
|
184
204
|
options,
|
|
185
205
|
promise,
|
|
186
206
|
resolve(value) {
|
|
@@ -204,6 +224,9 @@ function resolveSessionRouting(turn) {
|
|
|
204
224
|
persistence: turn.providerSessionRouting?.persistence ?? 'persistent',
|
|
205
225
|
};
|
|
206
226
|
}
|
|
227
|
+
function asImagePart(part) {
|
|
228
|
+
return part.type === 'image' ? part : null;
|
|
229
|
+
}
|
|
207
230
|
function asObject(value) {
|
|
208
231
|
return typeof value === 'object' && value !== null ? value : null;
|
|
209
232
|
}
|
|
@@ -267,25 +290,18 @@ function resolveClaudeLaunch(command, args, baseCwd) {
|
|
|
267
290
|
args: args.map((arg) => (isRelativePathLike(arg) ? resolve(baseCwd, arg) : arg)),
|
|
268
291
|
};
|
|
269
292
|
}
|
|
270
|
-
function buildClaudeSessionSystemPrompt(promptContext) {
|
|
293
|
+
export function buildClaudeSessionSystemPrompt(promptContext) {
|
|
271
294
|
if (!promptContext) {
|
|
272
295
|
return undefined;
|
|
273
296
|
}
|
|
274
297
|
const lines = [];
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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`);
|
|
298
|
+
// Every command the CLI carries needs the gateway credential file, so the session names it only
|
|
299
|
+
// when one exists; otherwise the model is handed an invocation template it cannot fill.
|
|
300
|
+
if (promptContext.skillRuntime && promptContext.gatewayCredentialPath) {
|
|
301
|
+
lines.push(...buildSkillManualLines(promptContext.skillRuntime));
|
|
283
302
|
}
|
|
284
|
-
if (promptContext.localhostGateway && promptContext.
|
|
285
|
-
lines.push(`Gateway
|
|
286
|
-
if (promptContext.localhostGateway.baseUrl) {
|
|
287
|
-
lines.push(`Loopback gateway base URL: ${promptContext.localhostGateway.baseUrl}`);
|
|
288
|
-
}
|
|
303
|
+
if (promptContext.localhostGateway && promptContext.gatewayCredentialPath) {
|
|
304
|
+
lines.push(`Gateway credential file for this session: ${promptContext.gatewayCredentialPath}`);
|
|
289
305
|
}
|
|
290
306
|
if (promptContext.taskWorkspace) {
|
|
291
307
|
lines.push(`Task-scoped writable workspace root: ${promptContext.taskWorkspace.rootPath}.`);
|
|
@@ -312,20 +328,32 @@ function resolveSessionAdditionalDirectories(promptContext) {
|
|
|
312
328
|
if (promptContext?.channelContextPayloadPath) {
|
|
313
329
|
directories.add(dirname(promptContext.channelContextPayloadPath));
|
|
314
330
|
}
|
|
315
|
-
if (promptContext?.
|
|
316
|
-
directories.add(dirname(promptContext.
|
|
331
|
+
if (promptContext?.gatewayCredentialPath) {
|
|
332
|
+
directories.add(dirname(promptContext.gatewayCredentialPath));
|
|
317
333
|
}
|
|
318
334
|
if (promptContext?.skillRuntime?.skillDirectoryPath) {
|
|
319
335
|
directories.add(promptContext.skillRuntime.skillDirectoryPath);
|
|
320
336
|
}
|
|
321
337
|
return directories.size > 0 ? [...directories].sort((left, right) => left.localeCompare(right)) : undefined;
|
|
322
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* A session carries its scope from `session/new`: the directories it may read, plus the append that
|
|
341
|
+
* names the CLI manual and this channel's gateway credential file. The credential file sits in the
|
|
342
|
+
* channel context directory the payload already contributes, so its absence is invisible in the
|
|
343
|
+
* directory set alone — without the path itself in the key, a session opened while the credential
|
|
344
|
+
* write failed keeps an append that never names the CLI for as long as the session lives.
|
|
345
|
+
*/
|
|
323
346
|
function resolveSessionVisibilityKey(promptContext) {
|
|
324
347
|
const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
|
|
325
348
|
if (!additionalDirectories) {
|
|
326
349
|
return undefined;
|
|
327
350
|
}
|
|
328
|
-
return JSON.stringify({
|
|
351
|
+
return JSON.stringify({
|
|
352
|
+
additionalDirectories,
|
|
353
|
+
...(promptContext?.gatewayCredentialPath
|
|
354
|
+
? { gatewayCredentialPath: promptContext.gatewayCredentialPath }
|
|
355
|
+
: {}),
|
|
356
|
+
});
|
|
329
357
|
}
|
|
330
358
|
function buildSessionRequest(cwd, promptContext, additionalDirectories) {
|
|
331
359
|
const meta = buildClaudeSessionMeta(promptContext);
|
|
@@ -369,6 +397,7 @@ export class ClaudeCliClient {
|
|
|
369
397
|
sessionStore;
|
|
370
398
|
resolveSessionStoreAgentId;
|
|
371
399
|
logger;
|
|
400
|
+
imageInputConfig;
|
|
372
401
|
runtime;
|
|
373
402
|
channels = new Map();
|
|
374
403
|
/**
|
|
@@ -407,12 +436,13 @@ export class ClaudeCliClient {
|
|
|
407
436
|
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
408
437
|
idleBackendShutdown;
|
|
409
438
|
childStderr = '';
|
|
410
|
-
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger()) {
|
|
439
|
+
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), imageInputConfig = {}) {
|
|
411
440
|
this.command = command;
|
|
412
441
|
this.args = args;
|
|
413
442
|
this.sessionStore = sessionStore;
|
|
414
443
|
this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
|
|
415
444
|
this.logger = logger;
|
|
445
|
+
this.imageInputConfig = imageInputConfig;
|
|
416
446
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
417
447
|
assertClaudeCommandCompatibility(command, args, 'Claude provider runtime');
|
|
418
448
|
this.idleBackendShutdown = new IdleBackendShutdownScheduler({
|
|
@@ -434,19 +464,26 @@ export class ClaudeCliClient {
|
|
|
434
464
|
if (this.fatalError) {
|
|
435
465
|
throw this.fatalError;
|
|
436
466
|
}
|
|
437
|
-
const
|
|
467
|
+
const preparedTurnBase = typeof channelIdOrTurn === 'string'
|
|
438
468
|
? {
|
|
439
469
|
channelId: channelIdOrTurn,
|
|
470
|
+
incomingContent: '',
|
|
471
|
+
incomingParts: [],
|
|
440
472
|
prompt: typeof promptOrOptions === 'string' ? promptOrOptions : '',
|
|
441
473
|
}
|
|
442
474
|
: channelIdOrTurn;
|
|
475
|
+
const preparedTurn = {
|
|
476
|
+
...preparedTurnBase,
|
|
477
|
+
incomingContent: preparedTurnBase.incomingContent ?? '',
|
|
478
|
+
incomingParts: preparedTurnBase.incomingParts ?? [],
|
|
479
|
+
};
|
|
443
480
|
const options = typeof channelIdOrTurn === 'string'
|
|
444
481
|
? maybeOptions
|
|
445
482
|
: promptOrOptions;
|
|
446
483
|
const sessionRouting = resolveSessionRouting(preparedTurn);
|
|
447
484
|
const state = this.getOrCreateChannelState(sessionRouting.key, preparedTurn.channelId, sessionRouting.persistence);
|
|
448
485
|
this.idleBackendShutdown.cancel();
|
|
449
|
-
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn
|
|
486
|
+
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn, sessionRouting.persistence, options);
|
|
450
487
|
state.queue.push(turn);
|
|
451
488
|
this.processChannelQueue(sessionRouting.key, state);
|
|
452
489
|
return turn.promise;
|
|
@@ -618,14 +655,15 @@ export class ClaudeCliClient {
|
|
|
618
655
|
}
|
|
619
656
|
state.activeTurn = turn;
|
|
620
657
|
try {
|
|
621
|
-
state.cwd = this.resolveSessionCwd(turn.promptContext);
|
|
622
|
-
state.visibilityKey = resolveSessionVisibilityKey(turn.promptContext);
|
|
658
|
+
state.cwd = this.resolveSessionCwd(turn.preparedTurn.promptContext);
|
|
659
|
+
state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext);
|
|
623
660
|
await this.ensureStarted();
|
|
624
661
|
await this.recycleSessionIfScopeChanged(channelId, state);
|
|
625
|
-
const session = await this.getOrCreateSession(channelId, state, turn.promptContext);
|
|
662
|
+
const session = await this.getOrCreateSession(channelId, state, turn.preparedTurn.promptContext);
|
|
626
663
|
let reply;
|
|
627
664
|
try {
|
|
628
|
-
|
|
665
|
+
const promptInput = await this.buildPromptInput(turn.preparedTurn);
|
|
666
|
+
reply = await this.runTurn(session, promptInput, turn.options);
|
|
629
667
|
}
|
|
630
668
|
catch (error) {
|
|
631
669
|
if (isSessionTainted(error)) {
|
|
@@ -1199,4 +1237,37 @@ export class ClaudeCliClient {
|
|
|
1199
1237
|
}
|
|
1200
1238
|
}
|
|
1201
1239
|
}
|
|
1240
|
+
async buildPromptInput(turn) {
|
|
1241
|
+
const imageParts = turn.incomingParts.map(asImagePart).filter((part) => part !== null);
|
|
1242
|
+
if (imageParts.length === 0) {
|
|
1243
|
+
return turn.prompt;
|
|
1244
|
+
}
|
|
1245
|
+
const imageBlocks = await Promise.all(imageParts.map(async (part) => this.fetchImageBlock(part.attachment.url, part.attachment.contentType)));
|
|
1246
|
+
return [
|
|
1247
|
+
{ type: 'text', text: turn.prompt },
|
|
1248
|
+
...imageBlocks,
|
|
1249
|
+
];
|
|
1250
|
+
}
|
|
1251
|
+
async fetchImageBlock(rawUrl, contentType) {
|
|
1252
|
+
const baseUrl = this.imageInputConfig.borgeeBaseUrl?.trim();
|
|
1253
|
+
const agentApiKey = this.imageInputConfig.agentApiKey?.trim();
|
|
1254
|
+
if (!baseUrl || !agentApiKey) {
|
|
1255
|
+
throw new Error('Claude image input requires Borgee base URL and agent API key');
|
|
1256
|
+
}
|
|
1257
|
+
const resolvedUrl = resolveHostedAttachmentUrl(rawUrl, baseUrl);
|
|
1258
|
+
if (!isSameOrigin(resolvedUrl, baseUrl)) {
|
|
1259
|
+
throw new Error(`Claude image input only supports Borgee-origin attachments: ${resolvedUrl}`);
|
|
1260
|
+
}
|
|
1261
|
+
const init = { headers: { Authorization: `Bearer ${agentApiKey}` } };
|
|
1262
|
+
const response = await this.runtime.fetch(resolvedUrl, init);
|
|
1263
|
+
if (!response.ok) {
|
|
1264
|
+
throw new Error(`Claude image fetch failed with ${response.status} ${response.statusText}`.trim());
|
|
1265
|
+
}
|
|
1266
|
+
const blob = Buffer.from(await response.arrayBuffer()).toString('base64');
|
|
1267
|
+
return {
|
|
1268
|
+
type: 'image',
|
|
1269
|
+
mimeType: contentType,
|
|
1270
|
+
data: blob,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1202
1273
|
}
|
|
@@ -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;
|
|
@@ -28,6 +28,11 @@ interface CodexAcpRuntime {
|
|
|
28
28
|
mode?: number;
|
|
29
29
|
}): Promise<void>;
|
|
30
30
|
unlink(path: string): Promise<void>;
|
|
31
|
+
fetch(input: string | URL, init?: RequestInit): Promise<Response>;
|
|
32
|
+
}
|
|
33
|
+
interface CodexHostedImageInputConfig {
|
|
34
|
+
borgeeBaseUrl?: string;
|
|
35
|
+
agentApiKey?: string;
|
|
31
36
|
}
|
|
32
37
|
export declare class CodexCliClient {
|
|
33
38
|
private readonly command;
|
|
@@ -35,6 +40,7 @@ export declare class CodexCliClient {
|
|
|
35
40
|
private readonly sessionStore?;
|
|
36
41
|
private readonly resolveSessionStoreAgentId;
|
|
37
42
|
private readonly logger;
|
|
43
|
+
private readonly imageInputConfig;
|
|
38
44
|
private readonly runtime;
|
|
39
45
|
private readonly channels;
|
|
40
46
|
/**
|
|
@@ -70,7 +76,7 @@ export declare class CodexCliClient {
|
|
|
70
76
|
private sessionStoreWriteQueue;
|
|
71
77
|
private sessionCapabilities;
|
|
72
78
|
private readonly idleBackendShutdown;
|
|
73
|
-
constructor(command: string, args?: string[], runtimeOverrides?: Partial<CodexAcpRuntime>, sessionStore?: CodexChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined, logger?: DebugLogger);
|
|
79
|
+
constructor(command: string, args?: string[], runtimeOverrides?: Partial<CodexAcpRuntime>, sessionStore?: CodexChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined, logger?: DebugLogger, imageInputConfig?: CodexHostedImageInputConfig);
|
|
74
80
|
generateReply(turn: PreparedProviderTurnInput, options?: ProviderGenerateOptions): Promise<string>;
|
|
75
81
|
generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
|
|
76
82
|
dispose(): Promise<void>;
|
|
@@ -90,9 +96,11 @@ export declare class CodexCliClient {
|
|
|
90
96
|
private resolveProjectedContextDirectory;
|
|
91
97
|
private buildProjectedPromptContext;
|
|
92
98
|
private copyProjectedFile;
|
|
93
|
-
private
|
|
99
|
+
private pruneProjectedGatewayCredentials;
|
|
94
100
|
private refreshProjectedPromptContextBestEffort;
|
|
95
101
|
private rewritePromptContextPaths;
|
|
102
|
+
private buildPromptInput;
|
|
103
|
+
private fetchImageBlock;
|
|
96
104
|
private runTurn;
|
|
97
105
|
private raceWithFatal;
|
|
98
106
|
private invalidateSession;
|
|
@@ -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 { assertCodexProjectDocumentSize, buildCodexProjectDocument } from './project-doc.js';
|
|
9
|
+
import { isGatewayCredentialSidecarBasename } from '../../context/injection.js';
|
|
9
10
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
10
11
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
11
12
|
const DEFAULT_IDLE_SESSION_TTL_MS = 2 * 24 * 60 * 60 * 1000;
|
|
@@ -27,8 +28,6 @@ const DEFAULT_SESSION_CAPABILITIES = {
|
|
|
27
28
|
const DEFAULT_CODEX_COMMAND = 'codex-acp';
|
|
28
29
|
const DEFAULT_CODEX_ARGS = [];
|
|
29
30
|
const CODEX_CONTEXT_ROOT_DIRNAME = 'codex-context';
|
|
30
|
-
const PROJECTED_GATEWAY_AUTH_FILENAME = '.localhost-gateway-auth.json';
|
|
31
|
-
const PROJECTED_GATEWAY_AUTH_PREFIX = '.localhost-gateway-auth.';
|
|
32
31
|
const PROJECTION_UNAVAILABLE_PLACEHOLDER = '[codex-projection-unavailable]';
|
|
33
32
|
const QUEUED_TURN_DROPPED_MESSAGE = 'Codex ACP session was reset after a failed turn; queued turns were dropped instead of replaying them on a fresh session';
|
|
34
33
|
const require = createRequire(import.meta.url);
|
|
@@ -59,6 +58,7 @@ const DEFAULT_RUNTIME = {
|
|
|
59
58
|
unlink: async (path) => {
|
|
60
59
|
await fs.unlink(path);
|
|
61
60
|
},
|
|
61
|
+
fetch: async (input, init) => fetch(input, init),
|
|
62
62
|
};
|
|
63
63
|
function hasVisibleText(value) {
|
|
64
64
|
return typeof value === 'string' && value.trim().length > 0;
|
|
@@ -181,7 +181,7 @@ class CodexProgressCollector {
|
|
|
181
181
|
this.onProgress({ text });
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
-
function createDeferredTurn(channelId,
|
|
184
|
+
function createDeferredTurn(channelId, preparedTurn, sessionPersistence, options) {
|
|
185
185
|
let settled = false;
|
|
186
186
|
let resolvePromise;
|
|
187
187
|
let rejectPromise;
|
|
@@ -191,8 +191,7 @@ function createDeferredTurn(channelId, prompt, promptContext, sessionPersistence
|
|
|
191
191
|
});
|
|
192
192
|
return {
|
|
193
193
|
channelId,
|
|
194
|
-
|
|
195
|
-
promptContext,
|
|
194
|
+
preparedTurn,
|
|
196
195
|
sessionPersistence,
|
|
197
196
|
options,
|
|
198
197
|
promise,
|
|
@@ -210,6 +209,28 @@ function createDeferredTurn(channelId, prompt, promptContext, sessionPersistence
|
|
|
210
209
|
},
|
|
211
210
|
};
|
|
212
211
|
}
|
|
212
|
+
function isAbsoluteHttpUrl(value) {
|
|
213
|
+
return value.startsWith('http://') || value.startsWith('https://');
|
|
214
|
+
}
|
|
215
|
+
function resolveHostedAttachmentUrl(rawUrl, borgeeBaseUrl) {
|
|
216
|
+
if (isAbsoluteHttpUrl(rawUrl)) {
|
|
217
|
+
return rawUrl;
|
|
218
|
+
}
|
|
219
|
+
const base = borgeeBaseUrl.replace(/\/$/, '');
|
|
220
|
+
const path = rawUrl.startsWith('/') ? rawUrl : `/${rawUrl}`;
|
|
221
|
+
return `${base}${path}`;
|
|
222
|
+
}
|
|
223
|
+
function isSameOrigin(left, right) {
|
|
224
|
+
try {
|
|
225
|
+
return new URL(left).origin === new URL(right).origin;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function asImagePart(part) {
|
|
232
|
+
return part.type === 'image' ? part : null;
|
|
233
|
+
}
|
|
213
234
|
function normalizeError(error) {
|
|
214
235
|
return error instanceof Error ? error : new Error(String(error));
|
|
215
236
|
}
|
|
@@ -296,6 +317,7 @@ export class CodexCliClient {
|
|
|
296
317
|
sessionStore;
|
|
297
318
|
resolveSessionStoreAgentId;
|
|
298
319
|
logger;
|
|
320
|
+
imageInputConfig;
|
|
299
321
|
runtime;
|
|
300
322
|
channels = new Map();
|
|
301
323
|
/**
|
|
@@ -333,12 +355,13 @@ export class CodexCliClient {
|
|
|
333
355
|
sessionStoreWriteQueue = Promise.resolve();
|
|
334
356
|
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
335
357
|
idleBackendShutdown;
|
|
336
|
-
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger()) {
|
|
358
|
+
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), imageInputConfig = {}) {
|
|
337
359
|
this.command = command;
|
|
338
360
|
this.args = args;
|
|
339
361
|
this.sessionStore = sessionStore;
|
|
340
362
|
this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
|
|
341
363
|
this.logger = logger;
|
|
364
|
+
this.imageInputConfig = imageInputConfig;
|
|
342
365
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
343
366
|
this.idleBackendShutdown = new IdleBackendShutdownScheduler({
|
|
344
367
|
idleShutdownMs: this.runtime.idleBackendShutdownMs,
|
|
@@ -359,12 +382,19 @@ export class CodexCliClient {
|
|
|
359
382
|
if (this.fatalError) {
|
|
360
383
|
throw this.fatalError;
|
|
361
384
|
}
|
|
362
|
-
const
|
|
385
|
+
const preparedTurnBase = typeof channelIdOrTurn === 'string'
|
|
363
386
|
? {
|
|
364
387
|
channelId: channelIdOrTurn,
|
|
388
|
+
incomingContent: '',
|
|
389
|
+
incomingParts: [],
|
|
365
390
|
prompt: typeof promptOrOptions === 'string' ? promptOrOptions : '',
|
|
366
391
|
}
|
|
367
392
|
: channelIdOrTurn;
|
|
393
|
+
const preparedTurn = {
|
|
394
|
+
...preparedTurnBase,
|
|
395
|
+
incomingContent: preparedTurnBase.incomingContent ?? '',
|
|
396
|
+
incomingParts: preparedTurnBase.incomingParts ?? [],
|
|
397
|
+
};
|
|
368
398
|
const options = typeof channelIdOrTurn === 'string'
|
|
369
399
|
? maybeOptions
|
|
370
400
|
: promptOrOptions;
|
|
@@ -372,7 +402,7 @@ export class CodexCliClient {
|
|
|
372
402
|
const state = this.getOrCreateChannelState(sessionRouting.key, preparedTurn.channelId, sessionRouting.persistence);
|
|
373
403
|
this.clearIdleTimer(state);
|
|
374
404
|
this.idleBackendShutdown.cancel();
|
|
375
|
-
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn
|
|
405
|
+
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn, sessionRouting.persistence, options);
|
|
376
406
|
state.queue.push(turn);
|
|
377
407
|
this.processChannelQueue(sessionRouting.key, state);
|
|
378
408
|
return turn.promise;
|
|
@@ -536,8 +566,8 @@ export class CodexCliClient {
|
|
|
536
566
|
}
|
|
537
567
|
state.activeTurn = turn;
|
|
538
568
|
try {
|
|
539
|
-
state.cwd = await this.resolveSessionCwd(turn.promptContext);
|
|
540
|
-
const projectedPromptContext = await this.refreshProjectedPromptContextBestEffort(turn.channelId, turn.promptContext);
|
|
569
|
+
state.cwd = await this.resolveSessionCwd(turn.preparedTurn.promptContext);
|
|
570
|
+
const projectedPromptContext = await this.refreshProjectedPromptContextBestEffort(turn.channelId, turn.preparedTurn.promptContext);
|
|
541
571
|
state.additionalDirectories = this.resolveSessionAdditionalDirectories(projectedPromptContext);
|
|
542
572
|
state.visibilityKey = this.resolveSessionVisibilityKey(projectedPromptContext);
|
|
543
573
|
await this.ensureStarted();
|
|
@@ -545,7 +575,8 @@ export class CodexCliClient {
|
|
|
545
575
|
const session = await this.getOrCreateSession(channelId, state);
|
|
546
576
|
let reply;
|
|
547
577
|
try {
|
|
548
|
-
|
|
578
|
+
const promptInput = await this.buildPromptInput(turn.preparedTurn, projectedPromptContext?.promptContext);
|
|
579
|
+
reply = await this.runTurn(session, promptInput, turn.options);
|
|
549
580
|
}
|
|
550
581
|
catch (error) {
|
|
551
582
|
if (isSessionTainted(error)) {
|
|
@@ -740,7 +771,7 @@ export class CodexCliClient {
|
|
|
740
771
|
return JSON.stringify({
|
|
741
772
|
directoryPath: projectedPromptContext.directoryPath,
|
|
742
773
|
channelContextPayloadPath: projectedPromptContext.promptContext.channelContextPayloadPath,
|
|
743
|
-
|
|
774
|
+
gatewayCredentialPath: projectedPromptContext.promptContext.gatewayCredentialPath ?? null,
|
|
744
775
|
});
|
|
745
776
|
}
|
|
746
777
|
resolveProjectedContextDirectory(payloadPath) {
|
|
@@ -755,8 +786,8 @@ export class CodexCliClient {
|
|
|
755
786
|
return {
|
|
756
787
|
...promptContext,
|
|
757
788
|
channelContextPayloadPath: join(projectedDirectoryPath, basename(payloadPath)),
|
|
758
|
-
|
|
759
|
-
? join(projectedDirectoryPath, basename(promptContext.
|
|
789
|
+
gatewayCredentialPath: promptContext.gatewayCredentialPath
|
|
790
|
+
? join(projectedDirectoryPath, basename(promptContext.gatewayCredentialPath))
|
|
760
791
|
: undefined,
|
|
761
792
|
};
|
|
762
793
|
}
|
|
@@ -767,12 +798,11 @@ export class CodexCliClient {
|
|
|
767
798
|
mode: 0o600,
|
|
768
799
|
});
|
|
769
800
|
}
|
|
770
|
-
async
|
|
801
|
+
async pruneProjectedGatewayCredentials(directoryPath, keepPath) {
|
|
771
802
|
const keepFilename = keepPath ? basename(keepPath) : null;
|
|
772
803
|
const entries = await this.runtime.readdir(directoryPath);
|
|
773
804
|
await Promise.all(entries
|
|
774
|
-
.filter((entry) => entry
|
|
775
|
-
|| (entry.startsWith(PROJECTED_GATEWAY_AUTH_PREFIX) && entry.endsWith('.json')))
|
|
805
|
+
.filter((entry) => isGatewayCredentialSidecarBasename(entry))
|
|
776
806
|
.filter((entry) => entry !== keepFilename)
|
|
777
807
|
.map(async (entry) => {
|
|
778
808
|
await this.runtime.unlink(join(directoryPath, entry)).catch((error) => {
|
|
@@ -795,10 +825,10 @@ export class CodexCliClient {
|
|
|
795
825
|
}
|
|
796
826
|
try {
|
|
797
827
|
await this.runtime.mkdir(projectedDirectoryPath, { recursive: true, mode: 0o700 });
|
|
798
|
-
await this.
|
|
828
|
+
await this.pruneProjectedGatewayCredentials(projectedDirectoryPath, projectedPromptContext.gatewayCredentialPath);
|
|
799
829
|
await this.copyProjectedFile(payloadPath, projectedPromptContext.channelContextPayloadPath);
|
|
800
|
-
if (resolvedPromptContext.
|
|
801
|
-
await this.copyProjectedFile(resolvedPromptContext.
|
|
830
|
+
if (resolvedPromptContext.gatewayCredentialPath && projectedPromptContext.gatewayCredentialPath) {
|
|
831
|
+
await this.copyProjectedFile(resolvedPromptContext.gatewayCredentialPath, projectedPromptContext.gatewayCredentialPath);
|
|
802
832
|
}
|
|
803
833
|
const content = buildCodexProjectDocument(projectedPromptContext);
|
|
804
834
|
assertCodexProjectDocumentSize(content);
|
|
@@ -827,7 +857,7 @@ export class CodexCliClient {
|
|
|
827
857
|
let rewritten = prompt;
|
|
828
858
|
const replacements = [
|
|
829
859
|
[sourceContext.channelContextPayloadPath, targetContext?.channelContextPayloadPath],
|
|
830
|
-
[sourceContext.
|
|
860
|
+
[sourceContext.gatewayCredentialPath, targetContext?.gatewayCredentialPath],
|
|
831
861
|
];
|
|
832
862
|
for (const [sourcePath, targetPath] of replacements) {
|
|
833
863
|
if (!sourcePath || sourcePath === targetPath) {
|
|
@@ -837,6 +867,40 @@ export class CodexCliClient {
|
|
|
837
867
|
}
|
|
838
868
|
return rewritten;
|
|
839
869
|
}
|
|
870
|
+
async buildPromptInput(turn, projectedPromptContext) {
|
|
871
|
+
const prompt = this.rewritePromptContextPaths(turn.prompt, turn.promptContext, projectedPromptContext);
|
|
872
|
+
const imageParts = turn.incomingParts.map(asImagePart).filter((part) => part !== null);
|
|
873
|
+
if (imageParts.length === 0) {
|
|
874
|
+
return prompt;
|
|
875
|
+
}
|
|
876
|
+
const imageBlocks = await Promise.all(imageParts.map(async (part) => this.fetchImageBlock(part.attachment.url, part.attachment.contentType)));
|
|
877
|
+
return [
|
|
878
|
+
{ type: 'text', text: prompt },
|
|
879
|
+
...imageBlocks,
|
|
880
|
+
];
|
|
881
|
+
}
|
|
882
|
+
async fetchImageBlock(rawUrl, contentType) {
|
|
883
|
+
const baseUrl = this.imageInputConfig.borgeeBaseUrl?.trim();
|
|
884
|
+
const agentApiKey = this.imageInputConfig.agentApiKey?.trim();
|
|
885
|
+
if (!baseUrl || !agentApiKey) {
|
|
886
|
+
throw new Error('Codex image input requires Borgee base URL and agent API key');
|
|
887
|
+
}
|
|
888
|
+
const resolvedUrl = resolveHostedAttachmentUrl(rawUrl, baseUrl);
|
|
889
|
+
if (!isSameOrigin(resolvedUrl, baseUrl)) {
|
|
890
|
+
throw new Error(`Codex image input only supports Borgee-origin attachments: ${resolvedUrl}`);
|
|
891
|
+
}
|
|
892
|
+
const init = { headers: { Authorization: `Bearer ${agentApiKey}` } };
|
|
893
|
+
const response = await this.runtime.fetch(resolvedUrl, init);
|
|
894
|
+
if (!response.ok) {
|
|
895
|
+
throw new Error(`Codex image fetch failed with ${response.status} ${response.statusText}`.trim());
|
|
896
|
+
}
|
|
897
|
+
const blob = Buffer.from(await response.arrayBuffer()).toString('base64');
|
|
898
|
+
return {
|
|
899
|
+
type: 'image',
|
|
900
|
+
mimeType: contentType,
|
|
901
|
+
data: blob,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
840
904
|
async runTurn(session, prompt, options) {
|
|
841
905
|
const promptPromise = this.raceWithFatal(session.prompt(prompt));
|
|
842
906
|
const promptFailure = new Promise((_, reject) => {
|