@borgee/agents-host 0.2.33 → 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/README.md +28 -7
- package/dist/agents-host.d.ts +19 -0
- package/dist/agents-host.js +163 -44
- package/dist/chat/chat-control-plane.d.ts +9 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +10 -1
- package/dist/chat/sdk-chat-control-plane.js +9 -0
- package/dist/cli-args.d.ts +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/compatibility-gates.d.ts +1 -0
- package/dist/compatibility-gates.js +2 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +21 -3
- package/dist/context/injection.d.ts +5 -3
- package/dist/context/injection.js +45 -29
- package/dist/context/main-session-delegation.d.ts +1 -0
- package/dist/context/main-session-delegation.js +6 -0
- package/dist/context/prompt.js +25 -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 +75 -1
- package/dist/hosted-turn-content.d.ts +15 -0
- package/dist/hosted-turn-content.js +50 -0
- package/dist/local-config.js +10 -1
- package/dist/managed-daemon.d.ts +3 -2
- package/dist/managed-daemon.js +82 -29
- package/dist/plugin-sdk.js +58 -1
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/gateway-authorization.d.ts +18 -3
- package/dist/policy/gateway-authorization.js +33 -1
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +13 -1
- package/dist/providers/claude/cli-client.d.ts +36 -3
- package/dist/providers/claude/cli-client.js +225 -37
- package/dist/providers/codex/adapter.d.ts +3 -1
- package/dist/providers/codex/adapter.js +13 -1
- package/dist/providers/codex/cli-client.d.ts +35 -3
- package/dist/providers/codex/cli-client.js +212 -31
- package/dist/providers/codex/project-doc.js +16 -29
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +13 -1
- package/dist/providers/copilot/cli-client.d.ts +34 -2
- package/dist/providers/copilot/cli-client.js +196 -18
- package/dist/providers/create-provider.d.ts +1 -1
- package/dist/providers/create-provider.js +30 -14
- package/dist/providers/idle-backend-shutdown.d.ts +16 -0
- package/dist/providers/idle-backend-shutdown.js +53 -0
- 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 +40 -2
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +133 -35
- 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 -507
- package/skills/borgee-agent/borgee-agent.py +0 -438
|
@@ -6,8 +6,19 @@ 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';
|
|
10
|
+
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
9
11
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
10
12
|
const DEFAULT_IDLE_SESSION_TTL_MS = 2 * 24 * 60 * 60 * 1000;
|
|
13
|
+
const IDLE_BACKEND_STOPPED_MESSAGE = 'Codex ACP backend stopped after idle timeout';
|
|
14
|
+
/**
|
|
15
|
+
* A fatal teardown has callers waiting on it, so it force-kills quickly. An
|
|
16
|
+
* idle recycle has nobody waiting, and the CLIs unlink their on-disk session
|
|
17
|
+
* locks during SIGTERM cleanup — a lock left behind by SIGKILL makes the next
|
|
18
|
+
* restore believe the session is still in use (github/copilot-cli#3255). So the
|
|
19
|
+
* idle path trades latency nobody observes for a clean exit.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_IDLE_SHUTDOWN_GRACE_PERIOD_MS = 5_000;
|
|
11
22
|
const DEFAULT_SHUTDOWN_GRACE_PERIOD_MS = 250;
|
|
12
23
|
const DEFAULT_SHUTDOWN_FORCE_KILL_WAIT_MS = 250;
|
|
13
24
|
const DEFAULT_SESSION_CAPABILITIES = {
|
|
@@ -17,8 +28,6 @@ const DEFAULT_SESSION_CAPABILITIES = {
|
|
|
17
28
|
const DEFAULT_CODEX_COMMAND = 'codex-acp';
|
|
18
29
|
const DEFAULT_CODEX_ARGS = [];
|
|
19
30
|
const CODEX_CONTEXT_ROOT_DIRNAME = 'codex-context';
|
|
20
|
-
const PROJECTED_GATEWAY_AUTH_FILENAME = '.localhost-gateway-auth.json';
|
|
21
|
-
const PROJECTED_GATEWAY_AUTH_PREFIX = '.localhost-gateway-auth.';
|
|
22
31
|
const PROJECTION_UNAVAILABLE_PLACEHOLDER = '[codex-projection-unavailable]';
|
|
23
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';
|
|
24
33
|
const require = createRequire(import.meta.url);
|
|
@@ -30,6 +39,8 @@ const DEFAULT_RUNTIME = {
|
|
|
30
39
|
protocolVersion: PROTOCOL_VERSION,
|
|
31
40
|
cwd: process.cwd(),
|
|
32
41
|
idleSessionTtlMs: DEFAULT_IDLE_SESSION_TTL_MS,
|
|
42
|
+
idleBackendShutdownMs: IDLE_BACKEND_SHUTDOWN_DISABLED_MS,
|
|
43
|
+
idleShutdownGracePeriodMs: DEFAULT_IDLE_SHUTDOWN_GRACE_PERIOD_MS,
|
|
33
44
|
shutdownGracePeriodMs: DEFAULT_SHUTDOWN_GRACE_PERIOD_MS,
|
|
34
45
|
shutdownForceKillWaitMs: DEFAULT_SHUTDOWN_FORCE_KILL_WAIT_MS,
|
|
35
46
|
readFile: async (path, options) => {
|
|
@@ -47,6 +58,7 @@ const DEFAULT_RUNTIME = {
|
|
|
47
58
|
unlink: async (path) => {
|
|
48
59
|
await fs.unlink(path);
|
|
49
60
|
},
|
|
61
|
+
fetch: async (input, init) => fetch(input, init),
|
|
50
62
|
};
|
|
51
63
|
function hasVisibleText(value) {
|
|
52
64
|
return typeof value === 'string' && value.trim().length > 0;
|
|
@@ -169,7 +181,7 @@ class CodexProgressCollector {
|
|
|
169
181
|
this.onProgress({ text });
|
|
170
182
|
}
|
|
171
183
|
}
|
|
172
|
-
function createDeferredTurn(channelId,
|
|
184
|
+
function createDeferredTurn(channelId, preparedTurn, sessionPersistence, options) {
|
|
173
185
|
let settled = false;
|
|
174
186
|
let resolvePromise;
|
|
175
187
|
let rejectPromise;
|
|
@@ -179,8 +191,7 @@ function createDeferredTurn(channelId, prompt, promptContext, sessionPersistence
|
|
|
179
191
|
});
|
|
180
192
|
return {
|
|
181
193
|
channelId,
|
|
182
|
-
|
|
183
|
-
promptContext,
|
|
194
|
+
preparedTurn,
|
|
184
195
|
sessionPersistence,
|
|
185
196
|
options,
|
|
186
197
|
promise,
|
|
@@ -198,6 +209,28 @@ function createDeferredTurn(channelId, prompt, promptContext, sessionPersistence
|
|
|
198
209
|
},
|
|
199
210
|
};
|
|
200
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
|
+
}
|
|
201
234
|
function normalizeError(error) {
|
|
202
235
|
return error instanceof Error ? error : new Error(String(error));
|
|
203
236
|
}
|
|
@@ -284,8 +317,17 @@ export class CodexCliClient {
|
|
|
284
317
|
sessionStore;
|
|
285
318
|
resolveSessionStoreAgentId;
|
|
286
319
|
logger;
|
|
320
|
+
imageInputConfig;
|
|
287
321
|
runtime;
|
|
288
322
|
channels = new Map();
|
|
323
|
+
/**
|
|
324
|
+
* The provider session currently bound to channelId, or undefined before a
|
|
325
|
+
* turn has opened one. Surfaced so agents-host can record which session
|
|
326
|
+
* worked a task without the agent being asked to report its own id.
|
|
327
|
+
*/
|
|
328
|
+
sessionIdForChannel(channelId) {
|
|
329
|
+
return this.channels.get(channelId)?.session?.sessionId;
|
|
330
|
+
}
|
|
289
331
|
persistedSessions = new Map();
|
|
290
332
|
closingSessions = new WeakSet();
|
|
291
333
|
pendingSessionStarts = new Set();
|
|
@@ -298,7 +340,13 @@ export class CodexCliClient {
|
|
|
298
340
|
startPromise;
|
|
299
341
|
shutdownPromise;
|
|
300
342
|
childExitPromise;
|
|
301
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Bumped on every spawn so the `exit` / `connection.closed` handlers of a
|
|
345
|
+
* backend we deliberately recycled stay silent instead of latching a fatal
|
|
346
|
+
* error onto the client that already owns its successor.
|
|
347
|
+
*/
|
|
348
|
+
backendGeneration = 0;
|
|
349
|
+
idleShutdownPromise;
|
|
302
350
|
fatalError = null;
|
|
303
351
|
disposing = false;
|
|
304
352
|
backendClosed = false;
|
|
@@ -306,13 +354,25 @@ export class CodexCliClient {
|
|
|
306
354
|
sessionStoreLoadPromise = null;
|
|
307
355
|
sessionStoreWriteQueue = Promise.resolve();
|
|
308
356
|
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
309
|
-
|
|
357
|
+
idleBackendShutdown;
|
|
358
|
+
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), imageInputConfig = {}) {
|
|
310
359
|
this.command = command;
|
|
311
360
|
this.args = args;
|
|
312
361
|
this.sessionStore = sessionStore;
|
|
313
362
|
this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
|
|
314
363
|
this.logger = logger;
|
|
364
|
+
this.imageInputConfig = imageInputConfig;
|
|
315
365
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
366
|
+
this.idleBackendShutdown = new IdleBackendShutdownScheduler({
|
|
367
|
+
idleShutdownMs: this.runtime.idleBackendShutdownMs,
|
|
368
|
+
isIdle: () => this.isBackendIdle(),
|
|
369
|
+
shutdown: () => this.shutdownIdleBackend(),
|
|
370
|
+
onShutdownFailed: (error) => {
|
|
371
|
+
this.logger.error('failed to stop the idle Codex ACP backend', {
|
|
372
|
+
error: summarizeError(error),
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
});
|
|
316
376
|
this.fatalPromise = new Promise((_, reject) => {
|
|
317
377
|
this.rejectFatalPromise = reject;
|
|
318
378
|
});
|
|
@@ -322,19 +382,27 @@ export class CodexCliClient {
|
|
|
322
382
|
if (this.fatalError) {
|
|
323
383
|
throw this.fatalError;
|
|
324
384
|
}
|
|
325
|
-
const
|
|
385
|
+
const preparedTurnBase = typeof channelIdOrTurn === 'string'
|
|
326
386
|
? {
|
|
327
387
|
channelId: channelIdOrTurn,
|
|
388
|
+
incomingContent: '',
|
|
389
|
+
incomingParts: [],
|
|
328
390
|
prompt: typeof promptOrOptions === 'string' ? promptOrOptions : '',
|
|
329
391
|
}
|
|
330
392
|
: channelIdOrTurn;
|
|
393
|
+
const preparedTurn = {
|
|
394
|
+
...preparedTurnBase,
|
|
395
|
+
incomingContent: preparedTurnBase.incomingContent ?? '',
|
|
396
|
+
incomingParts: preparedTurnBase.incomingParts ?? [],
|
|
397
|
+
};
|
|
331
398
|
const options = typeof channelIdOrTurn === 'string'
|
|
332
399
|
? maybeOptions
|
|
333
400
|
: promptOrOptions;
|
|
334
401
|
const sessionRouting = resolveSessionRouting(preparedTurn);
|
|
335
402
|
const state = this.getOrCreateChannelState(sessionRouting.key, preparedTurn.channelId, sessionRouting.persistence);
|
|
336
403
|
this.clearIdleTimer(state);
|
|
337
|
-
|
|
404
|
+
this.idleBackendShutdown.cancel();
|
|
405
|
+
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn, sessionRouting.persistence, options);
|
|
338
406
|
state.queue.push(turn);
|
|
339
407
|
this.processChannelQueue(sessionRouting.key, state);
|
|
340
408
|
return turn.promise;
|
|
@@ -342,12 +410,17 @@ export class CodexCliClient {
|
|
|
342
410
|
async dispose() {
|
|
343
411
|
const error = new Error('Codex ACP backend stopped');
|
|
344
412
|
this.disposing = true;
|
|
413
|
+
this.idleBackendShutdown.cancel();
|
|
345
414
|
this.logger?.debug('stopping Codex ACP backend');
|
|
346
415
|
const closed = this.connection?.closed ?? Promise.resolve();
|
|
347
416
|
this.failAll(error);
|
|
348
417
|
let thrown;
|
|
349
418
|
try {
|
|
350
|
-
await Promise.all([
|
|
419
|
+
await Promise.all([
|
|
420
|
+
closed,
|
|
421
|
+
this.idleShutdownPromise ?? Promise.resolve(),
|
|
422
|
+
this.shutdownPromise ?? Promise.resolve(),
|
|
423
|
+
]);
|
|
351
424
|
}
|
|
352
425
|
catch (disposeError) {
|
|
353
426
|
thrown = disposeError;
|
|
@@ -368,6 +441,12 @@ export class CodexCliClient {
|
|
|
368
441
|
if (this.fatalError) {
|
|
369
442
|
throw this.fatalError;
|
|
370
443
|
}
|
|
444
|
+
if (this.idleShutdownPromise) {
|
|
445
|
+
await this.idleShutdownPromise;
|
|
446
|
+
if (this.fatalError) {
|
|
447
|
+
throw this.fatalError;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
371
450
|
if (!this.startPromise) {
|
|
372
451
|
this.startPromise = this.startBackend();
|
|
373
452
|
}
|
|
@@ -380,6 +459,8 @@ export class CodexCliClient {
|
|
|
380
459
|
}
|
|
381
460
|
}
|
|
382
461
|
async startBackend() {
|
|
462
|
+
const generation = ++this.backendGeneration;
|
|
463
|
+
const ownsCurrentBackend = () => (this.backendGeneration === generation && !this.disposing);
|
|
383
464
|
const launch = resolveAdapterLaunch(this.command, this.args);
|
|
384
465
|
this.logger?.debug('starting Codex ACP process', {
|
|
385
466
|
command: launch.command,
|
|
@@ -396,13 +477,20 @@ export class CodexCliClient {
|
|
|
396
477
|
});
|
|
397
478
|
child.stderr.resume();
|
|
398
479
|
this.logger?.debug('spawned Codex ACP process', { pid: child.pid });
|
|
480
|
+
let resolveChildExit;
|
|
481
|
+
this.childExitPromise = new Promise((resolve) => {
|
|
482
|
+
resolveChildExit = resolve;
|
|
483
|
+
});
|
|
399
484
|
child.once('error', (error) => {
|
|
400
485
|
this.logger?.debugError('Codex ACP process error', error);
|
|
486
|
+
if (!ownsCurrentBackend()) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
401
489
|
this.failAll(new Error(`Codex ACP process error: ${normalizeError(error).message}`));
|
|
402
490
|
});
|
|
403
491
|
child.once('exit', (code, signal) => {
|
|
404
|
-
|
|
405
|
-
if (
|
|
492
|
+
resolveChildExit();
|
|
493
|
+
if (!ownsCurrentBackend()) {
|
|
406
494
|
this.logger?.debug('Codex ACP process exited during shutdown', { code, signal });
|
|
407
495
|
return;
|
|
408
496
|
}
|
|
@@ -410,9 +498,6 @@ export class CodexCliClient {
|
|
|
410
498
|
const suffix = signal ? `signal ${signal}` : `code ${String(code ?? 'unknown')}`;
|
|
411
499
|
this.failAll(new Error(`Codex ACP process exited unexpectedly (${suffix})`));
|
|
412
500
|
});
|
|
413
|
-
this.childExitPromise = new Promise((resolveChildExit) => {
|
|
414
|
-
this.resolveChildExit = resolveChildExit;
|
|
415
|
-
});
|
|
416
501
|
const output = Writable.toWeb(child.stdin);
|
|
417
502
|
const input = Readable.toWeb(child.stdout);
|
|
418
503
|
const stream = this.runtime.ndJsonStream(output, input);
|
|
@@ -422,7 +507,7 @@ export class CodexCliClient {
|
|
|
422
507
|
const connection = app.connect(stream);
|
|
423
508
|
this.connection = connection;
|
|
424
509
|
void connection.closed.then(() => {
|
|
425
|
-
if (
|
|
510
|
+
if (ownsCurrentBackend()) {
|
|
426
511
|
this.logger?.debugError('Codex ACP connection closed unexpectedly');
|
|
427
512
|
this.failAll(new Error('Codex ACP connection closed unexpectedly'));
|
|
428
513
|
}
|
|
@@ -481,8 +566,8 @@ export class CodexCliClient {
|
|
|
481
566
|
}
|
|
482
567
|
state.activeTurn = turn;
|
|
483
568
|
try {
|
|
484
|
-
state.cwd = await this.resolveSessionCwd(turn.promptContext);
|
|
485
|
-
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);
|
|
486
571
|
state.additionalDirectories = this.resolveSessionAdditionalDirectories(projectedPromptContext);
|
|
487
572
|
state.visibilityKey = this.resolveSessionVisibilityKey(projectedPromptContext);
|
|
488
573
|
await this.ensureStarted();
|
|
@@ -490,7 +575,8 @@ export class CodexCliClient {
|
|
|
490
575
|
const session = await this.getOrCreateSession(channelId, state);
|
|
491
576
|
let reply;
|
|
492
577
|
try {
|
|
493
|
-
|
|
578
|
+
const promptInput = await this.buildPromptInput(turn.preparedTurn, projectedPromptContext?.promptContext);
|
|
579
|
+
reply = await this.runTurn(session, promptInput, turn.options);
|
|
494
580
|
}
|
|
495
581
|
catch (error) {
|
|
496
582
|
if (isSessionTainted(error)) {
|
|
@@ -512,6 +598,7 @@ export class CodexCliClient {
|
|
|
512
598
|
finally {
|
|
513
599
|
state.processing = false;
|
|
514
600
|
this.reconcileIdleChannelState(channelId, state);
|
|
601
|
+
this.idleBackendShutdown.reconcile();
|
|
515
602
|
}
|
|
516
603
|
})();
|
|
517
604
|
}
|
|
@@ -684,7 +771,7 @@ export class CodexCliClient {
|
|
|
684
771
|
return JSON.stringify({
|
|
685
772
|
directoryPath: projectedPromptContext.directoryPath,
|
|
686
773
|
channelContextPayloadPath: projectedPromptContext.promptContext.channelContextPayloadPath,
|
|
687
|
-
|
|
774
|
+
gatewayCredentialPath: projectedPromptContext.promptContext.gatewayCredentialPath ?? null,
|
|
688
775
|
});
|
|
689
776
|
}
|
|
690
777
|
resolveProjectedContextDirectory(payloadPath) {
|
|
@@ -699,8 +786,8 @@ export class CodexCliClient {
|
|
|
699
786
|
return {
|
|
700
787
|
...promptContext,
|
|
701
788
|
channelContextPayloadPath: join(projectedDirectoryPath, basename(payloadPath)),
|
|
702
|
-
|
|
703
|
-
? join(projectedDirectoryPath, basename(promptContext.
|
|
789
|
+
gatewayCredentialPath: promptContext.gatewayCredentialPath
|
|
790
|
+
? join(projectedDirectoryPath, basename(promptContext.gatewayCredentialPath))
|
|
704
791
|
: undefined,
|
|
705
792
|
};
|
|
706
793
|
}
|
|
@@ -711,12 +798,11 @@ export class CodexCliClient {
|
|
|
711
798
|
mode: 0o600,
|
|
712
799
|
});
|
|
713
800
|
}
|
|
714
|
-
async
|
|
801
|
+
async pruneProjectedGatewayCredentials(directoryPath, keepPath) {
|
|
715
802
|
const keepFilename = keepPath ? basename(keepPath) : null;
|
|
716
803
|
const entries = await this.runtime.readdir(directoryPath);
|
|
717
804
|
await Promise.all(entries
|
|
718
|
-
.filter((entry) => entry
|
|
719
|
-
|| (entry.startsWith(PROJECTED_GATEWAY_AUTH_PREFIX) && entry.endsWith('.json')))
|
|
805
|
+
.filter((entry) => isGatewayCredentialSidecarBasename(entry))
|
|
720
806
|
.filter((entry) => entry !== keepFilename)
|
|
721
807
|
.map(async (entry) => {
|
|
722
808
|
await this.runtime.unlink(join(directoryPath, entry)).catch((error) => {
|
|
@@ -739,10 +825,10 @@ export class CodexCliClient {
|
|
|
739
825
|
}
|
|
740
826
|
try {
|
|
741
827
|
await this.runtime.mkdir(projectedDirectoryPath, { recursive: true, mode: 0o700 });
|
|
742
|
-
await this.
|
|
828
|
+
await this.pruneProjectedGatewayCredentials(projectedDirectoryPath, projectedPromptContext.gatewayCredentialPath);
|
|
743
829
|
await this.copyProjectedFile(payloadPath, projectedPromptContext.channelContextPayloadPath);
|
|
744
|
-
if (resolvedPromptContext.
|
|
745
|
-
await this.copyProjectedFile(resolvedPromptContext.
|
|
830
|
+
if (resolvedPromptContext.gatewayCredentialPath && projectedPromptContext.gatewayCredentialPath) {
|
|
831
|
+
await this.copyProjectedFile(resolvedPromptContext.gatewayCredentialPath, projectedPromptContext.gatewayCredentialPath);
|
|
746
832
|
}
|
|
747
833
|
const content = buildCodexProjectDocument(projectedPromptContext);
|
|
748
834
|
assertCodexProjectDocumentSize(content);
|
|
@@ -771,7 +857,7 @@ export class CodexCliClient {
|
|
|
771
857
|
let rewritten = prompt;
|
|
772
858
|
const replacements = [
|
|
773
859
|
[sourceContext.channelContextPayloadPath, targetContext?.channelContextPayloadPath],
|
|
774
|
-
[sourceContext.
|
|
860
|
+
[sourceContext.gatewayCredentialPath, targetContext?.gatewayCredentialPath],
|
|
775
861
|
];
|
|
776
862
|
for (const [sourcePath, targetPath] of replacements) {
|
|
777
863
|
if (!sourcePath || sourcePath === targetPath) {
|
|
@@ -781,6 +867,40 @@ export class CodexCliClient {
|
|
|
781
867
|
}
|
|
782
868
|
return rewritten;
|
|
783
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
|
+
}
|
|
784
904
|
async runTurn(session, prompt, options) {
|
|
785
905
|
const promptPromise = this.raceWithFatal(session.prompt(prompt));
|
|
786
906
|
const promptFailure = new Promise((_, reject) => {
|
|
@@ -856,6 +976,7 @@ export class CodexCliClient {
|
|
|
856
976
|
}
|
|
857
977
|
this.fatalError = error;
|
|
858
978
|
this.rejectFatalPromise(error);
|
|
979
|
+
this.idleBackendShutdown.cancel();
|
|
859
980
|
for (const [channelId, state] of this.channels.entries()) {
|
|
860
981
|
this.clearIdleTimer(state);
|
|
861
982
|
state.activeTurn?.reject(error);
|
|
@@ -1073,6 +1194,66 @@ export class CodexCliClient {
|
|
|
1073
1194
|
return;
|
|
1074
1195
|
}
|
|
1075
1196
|
this.backendClosed = true;
|
|
1197
|
+
await this.closeBackendProcess(error, this.runtime.shutdownGracePeriodMs);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Backend teardown without the fatal latch: the client stays usable and
|
|
1201
|
+
* `ensureStarted()` spawns a replacement process on the next turn. Only safe
|
|
1202
|
+
* while `isBackendIdle()` holds, because every channel's live ACP session is
|
|
1203
|
+
* dropped here and no turn is ever replayed onto its successor.
|
|
1204
|
+
*/
|
|
1205
|
+
async shutdownIdleBackend() {
|
|
1206
|
+
// Detach the running backend before any signal reaches it, so its own exit
|
|
1207
|
+
// and connection-closed handlers read this teardown as deliberate instead
|
|
1208
|
+
// of latching a fatal error on a client that stays usable.
|
|
1209
|
+
this.backendGeneration += 1;
|
|
1210
|
+
const error = new Error(IDLE_BACKEND_STOPPED_MESSAGE);
|
|
1211
|
+
const sessions = [];
|
|
1212
|
+
for (const [channelId, state] of [...this.channels.entries()]) {
|
|
1213
|
+
this.clearIdleTimer(state);
|
|
1214
|
+
if (state.session) {
|
|
1215
|
+
sessions.push(state.session);
|
|
1216
|
+
}
|
|
1217
|
+
state.session = undefined;
|
|
1218
|
+
state.sessionCwd = undefined;
|
|
1219
|
+
state.sessionAdditionalDirectories = undefined;
|
|
1220
|
+
state.sessionVisibilityKey = undefined;
|
|
1221
|
+
this.channels.delete(channelId);
|
|
1222
|
+
}
|
|
1223
|
+
this.logger?.debug('stopping idle Codex ACP backend', {
|
|
1224
|
+
channels: sessions.length,
|
|
1225
|
+
idleShutdownMs: this.runtime.idleBackendShutdownMs,
|
|
1226
|
+
});
|
|
1227
|
+
for (const session of sessions) {
|
|
1228
|
+
this.closeSession(session);
|
|
1229
|
+
}
|
|
1230
|
+
const idleShutdownPromise = this.closeBackendProcess(error, this.runtime.idleShutdownGracePeriodMs).finally(() => {
|
|
1231
|
+
this.startPromise = undefined;
|
|
1232
|
+
this.childExitPromise = undefined;
|
|
1233
|
+
this.sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
1234
|
+
this.idleShutdownPromise = undefined;
|
|
1235
|
+
});
|
|
1236
|
+
this.idleShutdownPromise = idleShutdownPromise;
|
|
1237
|
+
await idleShutdownPromise;
|
|
1238
|
+
}
|
|
1239
|
+
isBackendIdle() {
|
|
1240
|
+
if (this.disposing || this.fatalError || this.backendClosed || this.idleShutdownPromise) {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
if (!this.child) {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
if (this.pendingSessionStarts.size > 0) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
for (const state of this.channels.values()) {
|
|
1250
|
+
if (state.activeTurn || state.queue.length > 0 || state.sessionPromise || state.processing) {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
async closeBackendProcess(error, gracePeriodMs) {
|
|
1076
1257
|
const connection = this.connection;
|
|
1077
1258
|
const child = this.child;
|
|
1078
1259
|
await this.waitForPendingSessionStarts();
|
|
@@ -1084,9 +1265,9 @@ export class CodexCliClient {
|
|
|
1084
1265
|
return;
|
|
1085
1266
|
}
|
|
1086
1267
|
const childExitPromise = this.childExitPromise ?? Promise.resolve();
|
|
1087
|
-
this.logger?.debug('sending SIGTERM to Codex ACP process');
|
|
1268
|
+
this.logger?.debug('sending SIGTERM to Codex ACP process', { gracePeriodMs });
|
|
1088
1269
|
child.kill('SIGTERM');
|
|
1089
|
-
const exitedAfterTerm = await this.waitForChildExit(childExitPromise,
|
|
1270
|
+
const exitedAfterTerm = await this.waitForChildExit(childExitPromise, gracePeriodMs);
|
|
1090
1271
|
if (exitedAfterTerm) {
|
|
1091
1272
|
this.logger?.debug('Codex ACP process exited after SIGTERM');
|
|
1092
1273
|
this.child = undefined;
|
|
@@ -1,35 +1,27 @@
|
|
|
1
|
-
import { basename } from 'node:path';
|
|
2
1
|
import { buildAttentionSummaryLines } from '../../context/attention.js';
|
|
3
2
|
import { buildCollaborationCapabilityDeclarationSummaryLines, buildMissedCollaborationDiagnosticSummaryLines, } from '../../context/collaboration-capabilities-diagnostics.js';
|
|
4
3
|
import { buildCollaborationOutcomeSummaryLines } from '../../context/collaboration-outcome.js';
|
|
4
|
+
import { MAIN_SESSION_DELEGATION_LINES } from '../../context/main-session-delegation.js';
|
|
5
|
+
import { buildSkillManualLines } from '../../context/skill-manual.js';
|
|
5
6
|
import { buildTaskThreadCollaborationSummaryLines } from '../../context/task-thread-collaboration.js';
|
|
6
7
|
const PROJECT_DOC_MAX_BYTES = 32 * 1024;
|
|
8
|
+
// Every command the CLI carries needs the gateway credential file, so the capability is only
|
|
9
|
+
// worth naming in a workspace that also names one; otherwise the model is handed an invocation
|
|
10
|
+
// template it cannot fill.
|
|
7
11
|
function buildSkillRuntimeLines(context) {
|
|
8
|
-
if (!context?.skillRuntime || !context.
|
|
12
|
+
if (!context?.skillRuntime || !context.gatewayCredentialPath) {
|
|
9
13
|
return [];
|
|
10
14
|
}
|
|
11
|
-
return
|
|
12
|
-
'Read-only local skill runtime bootstrap is available in this workspace.',
|
|
13
|
-
`Context payload: ${context.channelContextPayloadPath}`,
|
|
14
|
-
`Skill guide: ${context.skillRuntime.skillMarkdownPath}`,
|
|
15
|
-
`Node CLI: node ${context.skillRuntime.nodeCliPath} --context ${context.channelContextPayloadPath} --print-bootstrap`,
|
|
16
|
-
`Python CLI: python3 ${context.skillRuntime.pythonCliPath} --context ${context.channelContextPayloadPath} --print-bootstrap`,
|
|
17
|
-
'These CLIs are local-only, may only access the loopback gateway surfaced by the host, and must not mutate files or the local environment.',
|
|
18
|
-
];
|
|
15
|
+
return buildSkillManualLines(context.skillRuntime);
|
|
19
16
|
}
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
// The credential file is the whole handoff: it carries the gateway's base URL along with the
|
|
18
|
+
// channel id and the token, and the CLI reads all three out of it. Naming the base URL separately
|
|
19
|
+
// would only invite a hand-rolled request that carries the token off the CLI's one path.
|
|
20
|
+
function buildGatewayCredentialLines(context) {
|
|
21
|
+
if (!context?.localhostGateway || !context.gatewayCredentialPath) {
|
|
22
22
|
return [];
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
'A loopback-only localhost gateway may be available for this channel.',
|
|
26
|
-
`Gateway base URL: ${context.localhostGateway.baseUrl}`,
|
|
27
|
-
'Use the packaged local CLI with the context payload above for gateway access.',
|
|
28
|
-
];
|
|
29
|
-
if (context.gatewayAuthPath) {
|
|
30
|
-
lines.push(`Per-turn gateway auth sidecar: ${context.gatewayAuthPath}`, 'If a later turn prompt advertises a different auth sidecar path, prefer the prompt-visible path for that turn.');
|
|
31
|
-
}
|
|
32
|
-
return lines;
|
|
24
|
+
return [`Gateway credential file: ${context.gatewayCredentialPath}`];
|
|
33
25
|
}
|
|
34
26
|
export function buildCodexProjectDocument(context) {
|
|
35
27
|
const lines = [
|
|
@@ -37,13 +29,8 @@ export function buildCodexProjectDocument(context) {
|
|
|
37
29
|
'',
|
|
38
30
|
'This workspace belongs to one Borgee channel session hosted by agents-host.',
|
|
39
31
|
'Keep visible replies concise, do not claim actions you did not perform, and prefer the per-turn prompt when it provides newer turn-local details.',
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
'',
|
|
43
|
-
`Context payload file: ${context.channelContextPayloadPath}`,
|
|
44
|
-
`Workspace anchor: ${basename(context.channelContextPayloadPath)}`,
|
|
45
|
-
]
|
|
46
|
-
: []),
|
|
32
|
+
'',
|
|
33
|
+
...MAIN_SESSION_DELEGATION_LINES,
|
|
47
34
|
...(context?.taskWorkspace
|
|
48
35
|
? [
|
|
49
36
|
'',
|
|
@@ -76,7 +63,7 @@ export function buildCodexProjectDocument(context) {
|
|
|
76
63
|
return skillRuntimeLines.length > 0 ? ['', ...skillRuntimeLines] : [];
|
|
77
64
|
})(),
|
|
78
65
|
...(() => {
|
|
79
|
-
const gatewayLines =
|
|
66
|
+
const gatewayLines = buildGatewayCredentialLines(context);
|
|
80
67
|
return gatewayLines.length > 0 ? ['', ...gatewayLines] : [];
|
|
81
68
|
})(),
|
|
82
69
|
];
|
|
@@ -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 { CopilotCliClient } from './cli-client.js';
|
|
5
|
+
export declare const COPILOT_HOSTED_PROVIDER_CAPABILITIES: import("../provider-adapter.js").ProviderCapabilities;
|
|
5
6
|
export declare class CopilotProviderAdapter implements ProviderAdapter {
|
|
6
7
|
private readonly cli;
|
|
7
8
|
private readonly turnPreparer;
|
|
9
|
+
readonly capabilities: import("../provider-adapter.js").ProviderCapabilities;
|
|
8
10
|
constructor(cli: CopilotCliClient, 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 COPILOT_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 CopilotProviderAdapter {
|
|
3
12
|
cli;
|
|
4
13
|
turnPreparer;
|
|
14
|
+
capabilities = COPILOT_HOSTED_PROVIDER_CAPABILITIES;
|
|
5
15
|
constructor(cli, turnPreparer) {
|
|
6
16
|
this.cli = cli;
|
|
7
17
|
this.turnPreparer = turnPreparer;
|
|
@@ -11,7 +21,9 @@ export class CopilotProviderAdapter {
|
|
|
11
21
|
const text = await this.cli.generateReply(preparedTurn, {
|
|
12
22
|
onProgress: createAwaitingUserProgressHandler(options?.onProgress),
|
|
13
23
|
});
|
|
14
|
-
|
|
24
|
+
// Read the session AFTER the turn: a first turn on a channel opens the
|
|
25
|
+
// session as it runs, so before this point there is nothing to report.
|
|
26
|
+
return { ...parseProviderReply(text), sessionId: this.cli.sessionIdForChannel(input.channelId) };
|
|
15
27
|
}
|
|
16
28
|
async dispose() {
|
|
17
29
|
await this.cli.dispose();
|