@borgee/agents-host 0.1.6 → 0.1.7

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.
@@ -7,6 +7,7 @@ interface CopilotAcpRuntime {
7
7
  methods: typeof methods;
8
8
  protocolVersion: typeof PROTOCOL_VERSION;
9
9
  cwd: string;
10
+ idleSessionTtlMs: number;
10
11
  shutdownGracePeriodMs: number;
11
12
  shutdownForceKillWaitMs: number;
12
13
  }
@@ -50,6 +51,9 @@ export declare class CopilotCliClient {
50
51
  private invalidateSession;
51
52
  private closeSession;
52
53
  private rejectQueuedTurnsAfterSessionTaint;
54
+ private clearIdleTimer;
55
+ private reconcileIdleChannelState;
56
+ private evictIdleChannel;
53
57
  private shutdownBackend;
54
58
  private waitForPendingSessionStarts;
55
59
  private waitForPendingSessionCloses;
@@ -2,6 +2,7 @@ import { Readable, Writable } from 'node:stream';
2
2
  import spawn from 'cross-spawn';
3
3
  import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
4
4
  const SESSION_TAINTED_ERRORS = new WeakSet();
5
+ const DEFAULT_IDLE_SESSION_TTL_MS = 2 * 24 * 60 * 60 * 1000;
5
6
  const DEFAULT_SHUTDOWN_GRACE_PERIOD_MS = 250;
6
7
  const DEFAULT_SHUTDOWN_FORCE_KILL_WAIT_MS = 250;
7
8
  const QUEUED_TURN_DROPPED_MESSAGE = 'Copilot ACP session was reset after a failed turn; queued turns were dropped instead of replaying them on a fresh session';
@@ -12,6 +13,7 @@ const DEFAULT_RUNTIME = {
12
13
  methods,
13
14
  protocolVersion: PROTOCOL_VERSION,
14
15
  cwd: process.cwd(),
16
+ idleSessionTtlMs: DEFAULT_IDLE_SESSION_TTL_MS,
15
17
  shutdownGracePeriodMs: DEFAULT_SHUTDOWN_GRACE_PERIOD_MS,
16
18
  shutdownForceKillWaitMs: DEFAULT_SHUTDOWN_FORCE_KILL_WAIT_MS,
17
19
  };
@@ -98,6 +100,7 @@ export class CopilotCliClient {
98
100
  throw this.fatalError;
99
101
  }
100
102
  const state = this.getOrCreateChannelState(channelId);
103
+ this.clearIdleTimer(state);
101
104
  const turn = createDeferredTurn(prompt);
102
105
  state.queue.push(turn);
103
106
  this.processChannelQueue(channelId, state);
@@ -182,6 +185,7 @@ export class CopilotCliClient {
182
185
  let state = this.channels.get(channelId);
183
186
  if (!state) {
184
187
  state = {
188
+ idleTimerGeneration: 0,
185
189
  processing: false,
186
190
  queue: [],
187
191
  };
@@ -228,6 +232,7 @@ export class CopilotCliClient {
228
232
  }
229
233
  finally {
230
234
  state.processing = false;
235
+ this.reconcileIdleChannelState(channelId, state);
231
236
  }
232
237
  })();
233
238
  }
@@ -274,9 +279,6 @@ export class CopilotCliClient {
274
279
  if (state.sessionPromise === sessionPromise) {
275
280
  state.sessionPromise = undefined;
276
281
  }
277
- if (!state.session && state.queue.length === 0 && !state.activeTurn) {
278
- this.channels.delete(channelId);
279
- }
280
282
  }
281
283
  }
282
284
  async runTurn(session, prompt) {
@@ -332,6 +334,7 @@ export class CopilotCliClient {
332
334
  this.fatalError = error;
333
335
  this.rejectFatalPromise(error);
334
336
  for (const [channelId, state] of this.channels.entries()) {
337
+ this.clearIdleTimer(state);
335
338
  state.activeTurn?.reject(error);
336
339
  for (const turn of state.queue) {
337
340
  turn.reject(error);
@@ -351,6 +354,7 @@ export class CopilotCliClient {
351
354
  this.shutdownPromise = this.shutdownBackend(error);
352
355
  }
353
356
  invalidateSession(state, session) {
357
+ this.clearIdleTimer(state);
354
358
  if (state.session === session) {
355
359
  state.session = undefined;
356
360
  }
@@ -393,6 +397,55 @@ export class CopilotCliClient {
393
397
  }
394
398
  state.queue.length = 0;
395
399
  }
400
+ clearIdleTimer(state) {
401
+ state.idleTimerGeneration += 1;
402
+ if (!state.idleTimer) {
403
+ return;
404
+ }
405
+ clearTimeout(state.idleTimer);
406
+ state.idleTimer = undefined;
407
+ }
408
+ reconcileIdleChannelState(channelId, state) {
409
+ if (this.channels.get(channelId) !== state) {
410
+ return;
411
+ }
412
+ if (this.disposing || this.fatalError || this.backendClosed) {
413
+ this.clearIdleTimer(state);
414
+ return;
415
+ }
416
+ if (state.activeTurn || state.queue.length > 0 || state.sessionPromise || state.processing) {
417
+ this.clearIdleTimer(state);
418
+ return;
419
+ }
420
+ if (!state.session) {
421
+ this.clearIdleTimer(state);
422
+ this.channels.delete(channelId);
423
+ return;
424
+ }
425
+ this.clearIdleTimer(state);
426
+ const generation = state.idleTimerGeneration;
427
+ state.idleTimer = setTimeout(() => {
428
+ this.evictIdleChannel(channelId, state, generation);
429
+ }, this.runtime.idleSessionTtlMs);
430
+ }
431
+ evictIdleChannel(channelId, state, generation) {
432
+ if (this.disposing || this.fatalError || this.backendClosed) {
433
+ return;
434
+ }
435
+ if (this.channels.get(channelId) !== state || state.idleTimerGeneration !== generation) {
436
+ return;
437
+ }
438
+ state.idleTimer = undefined;
439
+ if (state.activeTurn || state.queue.length > 0 || state.sessionPromise || state.processing) {
440
+ return;
441
+ }
442
+ const session = state.session;
443
+ state.session = undefined;
444
+ if (session) {
445
+ this.closeSession(session);
446
+ }
447
+ this.channels.delete(channelId);
448
+ }
396
449
  async shutdownBackend(error) {
397
450
  if (this.backendClosed) {
398
451
  return;
@@ -9,7 +9,9 @@ export function createProvider(config) {
9
9
  return new ClaudeProviderAdapter(cli);
10
10
  }
11
11
  case 'copilot': {
12
- const cli = new CopilotCliClient(config.copilotCommand, config.copilotArgs);
12
+ const cli = new CopilotCliClient(config.copilotCommand, config.copilotArgs, {
13
+ idleSessionTtlMs: config.copilotSessionTtlMinutes * 60 * 1000,
14
+ });
13
15
  return new CopilotProviderAdapter(cli);
14
16
  }
15
17
  default:
package/dist/run.d.ts CHANGED
@@ -1,7 +1,27 @@
1
- /**
2
- * Loads config from the environment, starts an `AgentsHost`, and wires up
3
- * graceful shutdown on SIGINT/SIGTERM. Shared by the plain env-var entry
4
- * point (`index.ts`) and the CLI entry point (`cli.ts`), which sets the
5
- * relevant env vars from argv before delegating here.
6
- */
7
- export declare function runMain(): Promise<void>;
1
+ import { parseGenerateConfigSpec, resolveLocalConfigLayout } from './local-config.js';
2
+ import type { LocalConfigGenerateResult, LocalConfigGenerateSpec, LocalConfigSnapshot } from './types.js';
3
+ export interface RunMainOptions {
4
+ configPath?: string;
5
+ }
6
+ export interface ValidateLocalConfigDeps {
7
+ loadSnapshot?: (configPath: string) => Promise<LocalConfigSnapshot>;
8
+ logger?: Pick<Console, 'log'>;
9
+ }
10
+ export interface PrintLocalConfigLayoutDeps {
11
+ resolveLayout?: (rootPath: string) => ReturnType<typeof resolveLocalConfigLayout>;
12
+ writer?: Pick<Console, 'log'>;
13
+ }
14
+ export interface DescribeLocalConfigDeps {
15
+ loadSpec?: (configPath: string) => Promise<LocalConfigGenerateSpec>;
16
+ writer?: Pick<Console, 'log'>;
17
+ }
18
+ export interface GenerateLocalConfigDeps {
19
+ parseSpec?: (value: unknown, sourceLabel: string) => ReturnType<typeof parseGenerateConfigSpec>;
20
+ materialize?: (rootPath: string, spec: ReturnType<typeof parseGenerateConfigSpec>) => Promise<LocalConfigGenerateResult>;
21
+ writer?: Pick<Console, 'log'>;
22
+ }
23
+ export declare function validateLocalConfig(configPath: string, deps?: ValidateLocalConfigDeps): Promise<void>;
24
+ export declare function printLocalConfigLayout(rootPath: string, deps?: PrintLocalConfigLayoutDeps): Promise<void>;
25
+ export declare function describeLocalConfig(configPath: string, deps?: DescribeLocalConfigDeps): Promise<void>;
26
+ export declare function generateLocalConfig(rootPath: string, specJson: string, sourceLabelOrDeps?: string | GenerateLocalConfigDeps, deps?: GenerateLocalConfigDeps): Promise<void>;
27
+ export declare function runMain(options?: RunMainOptions): Promise<void>;
package/dist/run.js CHANGED
@@ -1,14 +1,51 @@
1
- import { loadConfigFromEnv } from './config.js';
2
1
  import { AgentsHost } from './agents-host.js';
3
- /**
4
- * Loads config from the environment, starts an `AgentsHost`, and wires up
5
- * graceful shutdown on SIGINT/SIGTERM. Shared by the plain env-var entry
6
- * point (`index.ts`) and the CLI entry point (`cli.ts`), which sets the
7
- * relevant env vars from argv before delegating here.
8
- */
9
- export async function runMain() {
10
- const config = loadConfigFromEnv();
11
- const host = new AgentsHost(config);
2
+ import { AgentsHostSupervisor } from './agents-host-supervisor.js';
3
+ import { loadConfigFromEnv } from './config.js';
4
+ import { loadLocalConfigGenerateSpec, loadLocalConfigSnapshot, materializeLocalConfig, parseGenerateConfigSpec, resolveLocalConfigLayout, } from './local-config.js';
5
+ export async function validateLocalConfig(configPath, deps = {}) {
6
+ const loadSnapshot = deps.loadSnapshot ?? loadLocalConfigSnapshot;
7
+ const logger = deps.logger ?? console;
8
+ const snapshot = await loadSnapshot(configPath);
9
+ logger.log('[agents-host] local-config validation OK');
10
+ logger.log(`[agents-host] host config: ${snapshot.hostConfigPath}`);
11
+ logger.log(`[agents-host] agents dir: ${snapshot.agentsDir}`);
12
+ logger.log(`[agents-host] managed agents: ${snapshot.agents.length}`);
13
+ for (const agent of snapshot.agents) {
14
+ logger.log(`[agents-host] managed agent key=${agent.key} provider=${agent.config.agent.provider} sourcePath=${agent.sourcePath}`);
15
+ }
16
+ }
17
+ export async function printLocalConfigLayout(rootPath, deps = {}) {
18
+ const resolveLayout = deps.resolveLayout ?? resolveLocalConfigLayout;
19
+ const writer = deps.writer ?? console;
20
+ writer.log(JSON.stringify(resolveLayout(rootPath), null, 2));
21
+ }
22
+ export async function describeLocalConfig(configPath, deps = {}) {
23
+ const loadSpec = deps.loadSpec ?? loadLocalConfigGenerateSpec;
24
+ const writer = deps.writer ?? console;
25
+ writer.log(JSON.stringify(await loadSpec(configPath), null, 2));
26
+ }
27
+ export async function generateLocalConfig(rootPath, specJson, sourceLabelOrDeps = '--spec-json', deps = {}) {
28
+ const sourceLabel = typeof sourceLabelOrDeps === 'string' ? sourceLabelOrDeps : '--spec-json';
29
+ const resolvedDeps = typeof sourceLabelOrDeps === 'string' ? deps : sourceLabelOrDeps;
30
+ const parseSpec = resolvedDeps.parseSpec ?? parseGenerateConfigSpec;
31
+ const materialize = resolvedDeps.materialize ?? materializeLocalConfig;
32
+ const writer = resolvedDeps.writer ?? console;
33
+ let parsedJson;
34
+ try {
35
+ parsedJson = JSON.parse(specJson);
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ throw new Error(`Invalid ${sourceLabel}: ${message}`);
40
+ }
41
+ const spec = parseSpec(parsedJson, sourceLabel);
42
+ const result = await materialize(rootPath, spec);
43
+ writer.log(JSON.stringify(result, null, 2));
44
+ }
45
+ export async function runMain(options = {}) {
46
+ const host = options.configPath
47
+ ? new AgentsHostSupervisor(options.configPath)
48
+ : new AgentsHost(loadConfigFromEnv());
12
49
  const shutdown = (signal) => {
13
50
  console.log(`[agents-host] received ${signal}, shutting down`);
14
51
  host
@@ -19,4 +56,5 @@ export async function runMain() {
19
56
  process.on('SIGINT', () => shutdown('SIGINT'));
20
57
  process.on('SIGTERM', () => shutdown('SIGTERM'));
21
58
  await host.start();
59
+ console.log('[agents-host] ready');
22
60
  }
package/dist/types.d.ts CHANGED
@@ -4,15 +4,11 @@ export interface ProviderCommandConfig {
4
4
  claudeArgs: string[];
5
5
  copilotCommand: string;
6
6
  copilotArgs: string[];
7
+ copilotSessionTtlMinutes: number;
7
8
  }
8
9
  export interface ProviderRuntimeConfig extends ProviderCommandConfig {
9
10
  provider: ProviderKind;
10
11
  }
11
- /**
12
- * Single hosted-agent configuration. The minimal agents host supports exactly
13
- * one Borgee agent per process — running multiple agents means running
14
- * multiple processes with different env vars (see README).
15
- */
16
12
  export interface HostedAgentConfig {
17
13
  agentApiKey: string;
18
14
  agentName: string;
@@ -22,6 +18,66 @@ export interface AgentsHostConfig extends ProviderCommandConfig {
22
18
  borgeeBaseUrl: string;
23
19
  agent: HostedAgentConfig;
24
20
  }
21
+ export interface LocalHostConfigFile {
22
+ borgeeBaseUrl: string;
23
+ agentsDir?: string;
24
+ defaults?: Partial<ProviderCommandConfig>;
25
+ }
26
+ export interface LocalHostGenerateConfigFile {
27
+ borgeeBaseUrl: string;
28
+ defaults?: Partial<ProviderCommandConfig>;
29
+ }
30
+ export interface LocalAgentConfigFile extends Partial<ProviderCommandConfig> {
31
+ key: string;
32
+ name: string;
33
+ apiKey: string;
34
+ provider: ProviderKind;
35
+ enabled?: boolean;
36
+ }
37
+ export interface ManagedAgentConfigSnapshot {
38
+ key: string;
39
+ sourcePath: string;
40
+ config: AgentsHostConfig;
41
+ }
42
+ export interface LocalConfigSnapshot {
43
+ /** Stable caller-facing paths, suitable for validation output and publication watches. */
44
+ hostConfigPath: string;
45
+ hostConfigDir: string;
46
+ agentsDir: string;
47
+ /** Managed stable root when the snapshot comes from generate-config publication. */
48
+ managedRootPath?: string;
49
+ /** Resolved paths pinned before config files are read. */
50
+ resolvedHostConfigPath: string;
51
+ resolvedHostConfigDir: string;
52
+ resolvedAgentsDir: string;
53
+ agents: ManagedAgentConfigSnapshot[];
54
+ }
55
+ export interface LocalConfigGenerateSpec {
56
+ host: LocalHostGenerateConfigFile;
57
+ agents: LocalAgentConfigFile[];
58
+ }
59
+ export interface LocalConfigGenerateWarning {
60
+ code: 'PRUNE_SUPERSEDED_GENERATIONS_FAILED';
61
+ message: string;
62
+ }
63
+ export interface LocalConfigGenerateResult {
64
+ root: string;
65
+ hostConfigPath: string;
66
+ agentsDir: string;
67
+ generatedAgents: Array<{
68
+ key: string;
69
+ path: string;
70
+ provider: ProviderKind;
71
+ enabled: boolean;
72
+ }>;
73
+ /**
74
+ * Agent config paths removed from the active authoritative set. Publication
75
+ * switches the whole set atomically rather than deleting these paths first.
76
+ */
77
+ prunedAgentConfigPaths: string[];
78
+ /** Non-fatal maintenance problems encountered after publication. */
79
+ warnings: LocalConfigGenerateWarning[];
80
+ }
25
81
  export interface ChannelMessageEvent {
26
82
  type?: string;
27
83
  channel_id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@borgee/agents-host",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,6 +23,7 @@
23
23
  "dependencies": {
24
24
  "@agentclientprotocol/sdk": "^1.2.1",
25
25
  "cross-spawn": "^7.0.6",
26
+ "yaml": "^2.8.1",
26
27
  "@borgee/plugin-sdk": "0.1.1"
27
28
  },
28
29
  "devDependencies": {
@@ -43,6 +44,7 @@
43
44
  "build": "tsc",
44
45
  "typecheck": "tsc --noEmit",
45
46
  "pretest": "pnpm --filter @borgee/plugin-sdk build",
46
- "test": "vitest run"
47
+ "test": "vitest run --testTimeout=10000",
48
+ "pretypecheck": "pnpm --filter @borgee/plugin-sdk build"
47
49
  }
48
50
  }