@borgee/agents-host 0.2.0 → 0.2.1
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 +11 -5
- package/dist/agents-host.d.ts +2 -0
- package/dist/agents-host.js +40 -9
- package/dist/chat/chat-control-plane.d.ts +1 -1
- package/dist/chat/sdk-chat-control-plane.d.ts +3 -3
- package/dist/chat/sdk-chat-control-plane.js +9 -17
- package/dist/cli.js +0 -0
- package/dist/config.js +2 -0
- package/dist/durable-cursor-store.d.ts +5 -0
- package/dist/durable-cursor-store.js +7 -0
- package/dist/local-config.js +8 -2
- package/dist/providers/claude/cli-client.d.ts +17 -1
- package/dist/providers/claude/cli-client.js +154 -6
- package/dist/providers/claude/session-store.d.ts +14 -0
- package/dist/providers/claude/session-store.js +103 -0
- package/dist/providers/copilot/cli-client.d.ts +19 -1
- package/dist/providers/copilot/cli-client.js +180 -3
- package/dist/providers/copilot/session-store.d.ts +14 -0
- package/dist/providers/copilot/session-store.js +97 -0
- package/dist/providers/create-provider.js +9 -2
- package/dist/state-paths.d.ts +6 -0
- package/dist/state-paths.js +46 -0
- package/dist/types.d.ts +3 -0
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -315,13 +315,19 @@ Each Borgee channel is mapped 1:1 to a provider-native session while it stays
|
|
|
315
315
|
active:
|
|
316
316
|
|
|
317
317
|
- **Claude**: first turn for a channel uses `--session-id <uuid>`; every turn
|
|
318
|
-
after uses `-r/--resume <uuid>`.
|
|
318
|
+
after uses `-r/--resume <uuid>`. The host persists the channel→session map in
|
|
319
|
+
its state root, serializes cross-channel map rewrites, and retries once with a
|
|
320
|
+
fresh session immediately if Claude reports a stale `--resume` target.
|
|
319
321
|
- **Copilot**: one persistent `copilot --acp` subprocess is shared by a single
|
|
320
322
|
`AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are
|
|
321
|
-
serialized; different channels keep isolated ACP sessions.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
serialized; different channels keep isolated ACP sessions. The host persists
|
|
324
|
+
the channel→ACP-session map in its state root and restores same-host
|
|
325
|
+
continuity with ACP `session/resume` when available, otherwise `session/load`
|
|
326
|
+
plus local replay cleanup on runtimes that advertise only load support.
|
|
327
|
+
|
|
328
|
+
There is no separate transcript/history store on our side. Claude and Copilot
|
|
329
|
+
only persist the native session ids they should try to resume later; cross-host
|
|
330
|
+
continuity still does not exist in this runtime.
|
|
325
331
|
|
|
326
332
|
## Testing
|
|
327
333
|
|
package/dist/agents-host.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare class AgentsHost {
|
|
|
27
27
|
private readonly progressChannelQueues;
|
|
28
28
|
private readonly progressDrafts;
|
|
29
29
|
private selfAgentId;
|
|
30
|
+
private selfAgentIdPromise;
|
|
30
31
|
private started;
|
|
31
32
|
private controlPlaneClosed;
|
|
32
33
|
constructor(config: AgentsHostConfig, deps?: {
|
|
@@ -39,4 +40,5 @@ export declare class AgentsHost {
|
|
|
39
40
|
private trackActiveTurn;
|
|
40
41
|
private enqueueProgressChannelTurn;
|
|
41
42
|
private ensureProgressDraft;
|
|
43
|
+
private ensureSelfAgentId;
|
|
42
44
|
}
|
package/dist/agents-host.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { chmod, mkdir } from 'node:fs/promises';
|
|
1
2
|
import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
|
|
3
|
+
import { createDurableCursorStore } from './durable-cursor-store.js';
|
|
2
4
|
import { createProvider } from './providers/create-provider.js';
|
|
5
|
+
const PRIVATE_STATE_ROOT_MODE = 0o700;
|
|
3
6
|
const STREAM_PROGRESS_EDIT_THROTTLE_MS = 150;
|
|
7
|
+
async function ensurePrivateStateRoot(path) {
|
|
8
|
+
await mkdir(path, { recursive: true, mode: PRIVATE_STATE_ROOT_MODE });
|
|
9
|
+
await chmod(path, PRIVATE_STATE_ROOT_MODE);
|
|
10
|
+
}
|
|
4
11
|
function hasVisibleText(value) {
|
|
5
12
|
return value.trim().length > 0;
|
|
6
13
|
}
|
|
@@ -128,29 +135,37 @@ export class AgentsHost {
|
|
|
128
135
|
progressChannelQueues = new Map();
|
|
129
136
|
progressDrafts = new Map();
|
|
130
137
|
selfAgentId = null;
|
|
138
|
+
selfAgentIdPromise = null;
|
|
131
139
|
started = false;
|
|
132
140
|
controlPlaneClosed = false;
|
|
133
141
|
constructor(config, deps) {
|
|
134
142
|
this.config = config;
|
|
135
143
|
this.provider = deps?.provider ?? createProvider({
|
|
136
144
|
provider: config.agent.provider,
|
|
145
|
+
stateRootDir: config.stateRootDir,
|
|
146
|
+
resolveStableAgentId: () => this.selfAgentId ?? undefined,
|
|
137
147
|
claudeCommand: config.claudeCommand,
|
|
138
148
|
claudeArgs: config.claudeArgs,
|
|
139
149
|
copilotCommand: config.copilotCommand,
|
|
140
150
|
copilotArgs: config.copilotArgs,
|
|
141
151
|
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
142
152
|
});
|
|
143
|
-
this.borgee = deps?.borgee ?? new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
this.borgee = deps?.borgee ?? new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined, {
|
|
154
|
+
cursorStore: createDurableCursorStore({
|
|
155
|
+
stateRootDir: config.stateRootDir,
|
|
156
|
+
}),
|
|
157
|
+
// Lets the server know which runtime/provider actually connected
|
|
158
|
+
// (internal/bpp ConnectHandler → users.last_connected_plugin_id), so
|
|
159
|
+
// the web UI can show real state instead of a client-side guess.
|
|
160
|
+
pluginId: `agents-host:${config.agent.provider}`,
|
|
161
|
+
});
|
|
148
162
|
}
|
|
149
163
|
async start() {
|
|
150
164
|
if (this.started)
|
|
151
165
|
return;
|
|
152
166
|
this.started = true;
|
|
153
167
|
this.controlPlaneClosed = false;
|
|
168
|
+
await ensurePrivateStateRoot(this.config.stateRootDir);
|
|
154
169
|
await this.borgee.connect((message) => {
|
|
155
170
|
if (!this.started) {
|
|
156
171
|
return;
|
|
@@ -159,11 +174,11 @@ export class AgentsHost {
|
|
|
159
174
|
? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleMessage(message))
|
|
160
175
|
: this.handleMessage(message);
|
|
161
176
|
this.trackActiveTurn(task);
|
|
177
|
+
return task;
|
|
162
178
|
});
|
|
163
|
-
const
|
|
164
|
-
this.selfAgentId = me.id;
|
|
179
|
+
const agentId = await this.ensureSelfAgentId();
|
|
165
180
|
console.log('[agents-host] connected', {
|
|
166
|
-
agentId
|
|
181
|
+
agentId,
|
|
167
182
|
agentName: this.config.agent.agentName,
|
|
168
183
|
provider: this.config.agent.provider,
|
|
169
184
|
});
|
|
@@ -187,8 +202,9 @@ export class AgentsHost {
|
|
|
187
202
|
agentName: this.config.agent.agentName,
|
|
188
203
|
channelId: msg.channel_id,
|
|
189
204
|
});
|
|
205
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
190
206
|
const authorId = String(msg.user_id ?? msg.sender_id ?? 'unknown');
|
|
191
|
-
if (
|
|
207
|
+
if (authorId === selfAgentId) {
|
|
192
208
|
return;
|
|
193
209
|
}
|
|
194
210
|
const content = String(msg.content ?? msg.body ?? '').trim();
|
|
@@ -276,4 +292,19 @@ export class AgentsHost {
|
|
|
276
292
|
}
|
|
277
293
|
return draft;
|
|
278
294
|
}
|
|
295
|
+
async ensureSelfAgentId() {
|
|
296
|
+
if (this.selfAgentId) {
|
|
297
|
+
return this.selfAgentId;
|
|
298
|
+
}
|
|
299
|
+
if (!this.selfAgentIdPromise) {
|
|
300
|
+
this.selfAgentIdPromise = this.borgee.getMe().then((me) => {
|
|
301
|
+
this.selfAgentId = me.id;
|
|
302
|
+
return me.id;
|
|
303
|
+
}).catch((error) => {
|
|
304
|
+
this.selfAgentIdPromise = null;
|
|
305
|
+
throw error;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return this.selfAgentIdPromise;
|
|
309
|
+
}
|
|
279
310
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChannelMessageEvent, MeResponseUser, PostedMessage } from '../types.js';
|
|
2
2
|
export interface ChatControlPlane {
|
|
3
|
-
connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
|
|
3
|
+
connect(onMessage: (message: ChannelMessageEvent) => void | Promise<void>): Promise<void>;
|
|
4
4
|
close(): Promise<void>;
|
|
5
5
|
postMessage(channelId: string, content: string): Promise<PostedMessage>;
|
|
6
6
|
editMessage(messageId: string, content: string): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import type { ChannelMessageEvent, MeResponseUser, PostedMessage } from '../type
|
|
|
3
3
|
import type { ChatControlPlane } from './chat-control-plane.js';
|
|
4
4
|
type PluginClientLike = Pick<BorgeePluginClient, 'agentId' | 'close' | 'connect' | 'deleteMessage' | 'editMessage' | 'getMe' | 'on' | 'sendMessage' | 'startTyping'>;
|
|
5
5
|
type PluginClientFactory = (options: BorgeePluginOptions) => PluginClientLike;
|
|
6
|
+
type SdkChatControlPlaneOptions = Pick<BorgeePluginOptions, 'cursorStore' | 'pluginId'>;
|
|
6
7
|
/**
|
|
7
8
|
* Thin adapter over `@borgee/plugin-sdk` (the same BPP/`/ws/plugin` SDK used by
|
|
8
9
|
* the OpenClaw plugin) that implements the minimal `ChatControlPlane` surface
|
|
@@ -10,12 +11,11 @@ type PluginClientFactory = (options: BorgeePluginOptions) => PluginClientLike;
|
|
|
10
11
|
*/
|
|
11
12
|
export declare class SdkChatControlPlane implements ChatControlPlane {
|
|
12
13
|
private readonly client;
|
|
13
|
-
private pendingMessages;
|
|
14
14
|
private unsubscribe;
|
|
15
15
|
private connected;
|
|
16
16
|
private me;
|
|
17
|
-
constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory,
|
|
18
|
-
connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
|
|
17
|
+
constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory, options?: SdkChatControlPlaneOptions);
|
|
18
|
+
connect(onMessage: (message: ChannelMessageEvent) => void | Promise<void>): Promise<void>;
|
|
19
19
|
close(): Promise<void>;
|
|
20
20
|
postMessage(channelId: string, content: string): Promise<PostedMessage>;
|
|
21
21
|
editMessage(messageId: string, content: string): Promise<void>;
|
|
@@ -6,43 +6,35 @@ import { createBorgeePlugin, } from '@borgee/plugin-sdk';
|
|
|
6
6
|
*/
|
|
7
7
|
export class SdkChatControlPlane {
|
|
8
8
|
client;
|
|
9
|
-
pendingMessages = [];
|
|
10
9
|
unsubscribe = null;
|
|
11
10
|
connected = false;
|
|
12
11
|
me = null;
|
|
13
|
-
constructor(baseUrl, apiKey, createClient = createBorgeePlugin,
|
|
14
|
-
this.client = createClient({ baseUrl, apiKey,
|
|
12
|
+
constructor(baseUrl, apiKey, createClient = createBorgeePlugin, options = {}) {
|
|
13
|
+
this.client = createClient({ baseUrl, apiKey, ...options });
|
|
15
14
|
}
|
|
16
15
|
async connect(onMessage) {
|
|
17
|
-
this.unsubscribe = this.client.on('message', (event) => {
|
|
18
|
-
|
|
19
|
-
if (!this.connected) {
|
|
20
|
-
this.pendingMessages.push(message);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
onMessage(message);
|
|
16
|
+
this.unsubscribe = this.client.on('message', async (event) => {
|
|
17
|
+
await onMessage(mapInboundToChannelMessage(event));
|
|
24
18
|
});
|
|
25
19
|
try {
|
|
26
|
-
|
|
20
|
+
// The SDK resolves connect after session.resume_ack, before every replay
|
|
21
|
+
// frame necessarily runs. Mark the consumer ready first so replay ACKs
|
|
22
|
+
// wait for the real handler instead of acknowledging an in-memory buffer.
|
|
27
23
|
this.connected = true;
|
|
24
|
+
await this.client.connect();
|
|
28
25
|
if (this.client.agentId) {
|
|
29
26
|
this.me = { id: this.client.agentId };
|
|
30
27
|
}
|
|
31
|
-
for (const message of this.pendingMessages) {
|
|
32
|
-
onMessage(message);
|
|
33
|
-
}
|
|
34
|
-
this.pendingMessages = [];
|
|
35
28
|
}
|
|
36
29
|
catch (error) {
|
|
30
|
+
this.connected = false;
|
|
37
31
|
this.unsubscribe?.();
|
|
38
32
|
this.unsubscribe = null;
|
|
39
|
-
this.pendingMessages = [];
|
|
40
33
|
throw error;
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
async close() {
|
|
44
37
|
this.connected = false;
|
|
45
|
-
this.pendingMessages = [];
|
|
46
38
|
this.unsubscribe?.();
|
|
47
39
|
this.unsubscribe = null;
|
|
48
40
|
await this.client.close();
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveSingleAgentStateRoot } from './state-paths.js';
|
|
1
2
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
2
3
|
export const MAX_COPILOT_SESSION_TTL_MINUTES = MAX_TIMER_DELAY_MS / 60_000;
|
|
3
4
|
export const DEFAULT_COPILOT_SESSION_TTL_MINUTES = 2 * 24 * 60;
|
|
@@ -74,6 +75,7 @@ export function loadConfigFromEnv(env = process.env) {
|
|
|
74
75
|
const provider = resolveProvider(envOr('RUNTIME_PROVIDER', 'claude', env), 'Unsupported RUNTIME_PROVIDER');
|
|
75
76
|
return {
|
|
76
77
|
borgeeBaseUrl: requireEnv('BORGEE_BASE_URL', env),
|
|
78
|
+
stateRootDir: resolveSingleAgentStateRoot(env),
|
|
77
79
|
...resolveProviderCommandConfig({
|
|
78
80
|
claudeCommand: envOr('CLAUDE_COMMAND', DEFAULT_PROVIDER_COMMAND_CONFIG.claudeCommand, env),
|
|
79
81
|
claudeArgs: parseArgs(envOr('CLAUDE_ARGS', DEFAULT_PROVIDER_COMMAND_CONFIG.claudeArgs.join(' '), env)),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FileCursorStore } from '@borgee/plugin-sdk';
|
|
2
|
+
import { resolveAgentCursorPath } from './state-paths.js';
|
|
3
|
+
export function createDurableCursorStore(options) {
|
|
4
|
+
return new FileCursorStore({
|
|
5
|
+
resolvePath: (agentId) => resolveAgentCursorPath(options.stateRootDir, agentId),
|
|
6
|
+
});
|
|
7
|
+
}
|
package/dist/local-config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { parseDocument, stringify } from 'yaml';
|
|
5
5
|
import { optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
|
|
6
|
+
import { resolveLocalConfigAgentStateRoot } from './state-paths.js';
|
|
6
7
|
const SUPPORTED_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
|
|
7
8
|
export const DEFAULT_LOCAL_HOST_CONFIG_FILENAME = 'agents-host.yaml';
|
|
8
9
|
export const DEFAULT_LOCAL_AGENTS_DIRNAME = 'agents';
|
|
@@ -131,6 +132,9 @@ function parseHostConfigFile(hostConfigPath, value) {
|
|
|
131
132
|
function toGeneratedAgentConfigFilename(key) {
|
|
132
133
|
return `${encodeURIComponent(key)}.yaml`;
|
|
133
134
|
}
|
|
135
|
+
function toCaseInsensitiveFilesystemIdentity(path) {
|
|
136
|
+
return path.toLowerCase();
|
|
137
|
+
}
|
|
134
138
|
function parseGenerateHostConfigFile(sourceLabel, value) {
|
|
135
139
|
if (value.agentsDir !== undefined) {
|
|
136
140
|
throw new Error(`Invalid generate-config host spec in ${sourceLabel}: agentsDir is not supported; generate-config always writes the canonical default layout`);
|
|
@@ -573,13 +577,14 @@ function renderAgentConfigYaml(agent) {
|
|
|
573
577
|
}
|
|
574
578
|
return stringify(config);
|
|
575
579
|
}
|
|
576
|
-
function buildManagedAgentSnapshot(host, sourcePath, agent) {
|
|
580
|
+
function buildManagedAgentSnapshot(host, stateRootBaseDir, sourcePath, agent) {
|
|
577
581
|
const providerConfig = resolveProviderCommandConfig({
|
|
578
582
|
...host.defaults,
|
|
579
583
|
...parseProviderCommandOverrides(agent, `agent config ${sourcePath}`),
|
|
580
584
|
});
|
|
581
585
|
const config = {
|
|
582
586
|
borgeeBaseUrl: host.borgeeBaseUrl,
|
|
587
|
+
stateRootDir: resolveLocalConfigAgentStateRoot(stateRootBaseDir, agent.key),
|
|
583
588
|
...providerConfig,
|
|
584
589
|
agent: {
|
|
585
590
|
agentApiKey: agent.apiKey,
|
|
@@ -610,6 +615,7 @@ export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
|
|
|
610
615
|
const resolvedHostConfigDir = dirname(resolvedHostConfigPath);
|
|
611
616
|
const hostConfigRecord = await loadParsedDocument(fileSystem, resolvedHostConfigPath, 'host config');
|
|
612
617
|
const hostConfig = parseHostConfigFile(absoluteHostConfigPath, hostConfigRecord);
|
|
618
|
+
const stateRootBaseDir = managedGeneration.managedRootPath ?? hostConfigDir;
|
|
613
619
|
const agentsDir = toAgentsDir(absoluteHostConfigPath, hostConfig.agentsDir);
|
|
614
620
|
const resolvedAgentsDir = resolve(fileSystem.realPath
|
|
615
621
|
? await fileSystem.realPath(toAgentsDir(resolvedHostConfigPath, hostConfig.agentsDir))
|
|
@@ -643,7 +649,7 @@ export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
|
|
|
643
649
|
resolvedAgentsDir,
|
|
644
650
|
agents: parsedAgents
|
|
645
651
|
.filter((entry) => entry.config.enabled !== false)
|
|
646
|
-
.map((entry) => buildManagedAgentSnapshot(hostConfig, entry.sourcePath, entry.config)),
|
|
652
|
+
.map((entry) => buildManagedAgentSnapshot(hostConfig, stateRootBaseDir, entry.sourcePath, entry.config)),
|
|
647
653
|
};
|
|
648
654
|
}
|
|
649
655
|
finally {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import type { ProviderGenerateOptions } from '../../types.js';
|
|
3
|
+
import type { ClaudeChannelSessionStore } from './session-store.js';
|
|
3
4
|
interface ClaudeCliRuntime {
|
|
4
5
|
spawn: typeof spawn;
|
|
5
6
|
}
|
|
@@ -14,15 +15,30 @@ interface ClaudeCliRuntime {
|
|
|
14
15
|
export declare class ClaudeCliClient {
|
|
15
16
|
private readonly command;
|
|
16
17
|
private readonly args;
|
|
18
|
+
private readonly sessionStore?;
|
|
19
|
+
private readonly resolveSessionStoreAgentId;
|
|
17
20
|
private readonly runtime;
|
|
18
21
|
private readonly channels;
|
|
22
|
+
private readonly persistedSessions;
|
|
23
|
+
private loadedSessionStoreAgentId;
|
|
19
24
|
private stopped;
|
|
20
|
-
|
|
25
|
+
private sessionStoreLoadPromise;
|
|
26
|
+
private sessionStoreWriteQueue;
|
|
27
|
+
constructor(command: string, args: string[], runtimeOverrides?: Partial<ClaudeCliRuntime>, sessionStore?: ClaudeChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined);
|
|
21
28
|
generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
|
|
22
29
|
dispose(): Promise<void>;
|
|
23
30
|
private getOrCreateChannelState;
|
|
24
31
|
private processChannelQueue;
|
|
25
32
|
private runTurn;
|
|
33
|
+
private runTurnAttempt;
|
|
34
|
+
private currentSessionStoreAgentId;
|
|
35
|
+
private ensureSessionStoreLoaded;
|
|
36
|
+
private hydrateChannelState;
|
|
37
|
+
private persistSession;
|
|
38
|
+
private persistSessionBestEffort;
|
|
39
|
+
private resetPersistedSession;
|
|
40
|
+
private resetPersistedSessionBestEffort;
|
|
41
|
+
private enqueueSessionStoreWrite;
|
|
26
42
|
private run;
|
|
27
43
|
}
|
|
28
44
|
export {};
|
|
@@ -4,6 +4,10 @@ const DEFAULT_RUNTIME = {
|
|
|
4
4
|
spawn,
|
|
5
5
|
};
|
|
6
6
|
const STREAM_JSON_ARGS = ['--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
7
|
+
function isStaleResumeFailure(error) {
|
|
8
|
+
const message = normalizeError(error).message.toLowerCase();
|
|
9
|
+
return message.includes('session not found') || message.includes('cannot resume');
|
|
10
|
+
}
|
|
7
11
|
function normalizeError(error) {
|
|
8
12
|
return error instanceof Error ? error : new Error(String(error));
|
|
9
13
|
}
|
|
@@ -157,12 +161,20 @@ class ClaudeStreamCollector {
|
|
|
157
161
|
export class ClaudeCliClient {
|
|
158
162
|
command;
|
|
159
163
|
args;
|
|
164
|
+
sessionStore;
|
|
165
|
+
resolveSessionStoreAgentId;
|
|
160
166
|
runtime;
|
|
161
167
|
channels = new Map();
|
|
168
|
+
persistedSessions = new Map();
|
|
169
|
+
loadedSessionStoreAgentId = null;
|
|
162
170
|
stopped = false;
|
|
163
|
-
|
|
171
|
+
sessionStoreLoadPromise = null;
|
|
172
|
+
sessionStoreWriteQueue = Promise.resolve();
|
|
173
|
+
constructor(command, args, runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined) {
|
|
164
174
|
this.command = command;
|
|
165
175
|
this.args = args;
|
|
176
|
+
this.sessionStore = sessionStore;
|
|
177
|
+
this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
|
|
166
178
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
167
179
|
}
|
|
168
180
|
async generateReply(channelId, prompt, options) {
|
|
@@ -195,6 +207,7 @@ export class ClaudeCliClient {
|
|
|
195
207
|
state = {
|
|
196
208
|
processing: false,
|
|
197
209
|
queue: [],
|
|
210
|
+
sessionHydrated: false,
|
|
198
211
|
sessionEstablished: false,
|
|
199
212
|
};
|
|
200
213
|
this.channels.set(channelId, state);
|
|
@@ -209,13 +222,26 @@ export class ClaudeCliClient {
|
|
|
209
222
|
void (async () => {
|
|
210
223
|
try {
|
|
211
224
|
while (!this.stopped) {
|
|
225
|
+
const queuedTurn = state.queue[0];
|
|
226
|
+
if (!queuedTurn) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
if (await this.ensureSessionStoreLoaded()) {
|
|
231
|
+
this.hydrateChannelState(channelId, state);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
state.queue.shift()?.reject(error);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
212
238
|
const turn = state.queue.shift();
|
|
213
239
|
if (!turn) {
|
|
214
240
|
return;
|
|
215
241
|
}
|
|
216
242
|
state.activeTurn = turn;
|
|
217
243
|
try {
|
|
218
|
-
const text = await this.runTurn(state, turn);
|
|
244
|
+
const text = await this.runTurn(channelId, state, turn);
|
|
219
245
|
turn.resolve(text);
|
|
220
246
|
}
|
|
221
247
|
catch (error) {
|
|
@@ -234,16 +260,138 @@ export class ClaudeCliClient {
|
|
|
234
260
|
}
|
|
235
261
|
})();
|
|
236
262
|
}
|
|
237
|
-
async runTurn(state, turn) {
|
|
263
|
+
async runTurn(channelId, state, turn) {
|
|
264
|
+
return this.runTurnAttempt(channelId, state, turn, true);
|
|
265
|
+
}
|
|
266
|
+
async runTurnAttempt(channelId, state, turn, allowFreshRetryAfterStaleResume) {
|
|
238
267
|
if (this.stopped) {
|
|
239
268
|
throw new Error('Claude CLI backend stopped');
|
|
240
269
|
}
|
|
241
270
|
const sessionId = state.sessionEstablished ? state.sessionId : randomUUID();
|
|
242
271
|
const sessionArgs = state.sessionEstablished ? ['--resume', sessionId] : ['--session-id', sessionId];
|
|
243
|
-
|
|
244
|
-
|
|
272
|
+
try {
|
|
273
|
+
const text = await this.run(state, [...this.args, ...sessionArgs, ...STREAM_JSON_ARGS], turn.prompt, turn.options);
|
|
274
|
+
state.sessionId = sessionId;
|
|
275
|
+
state.sessionEstablished = true;
|
|
276
|
+
await this.persistSessionBestEffort(channelId, sessionId);
|
|
277
|
+
return text;
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (state.sessionEstablished && isStaleResumeFailure(error)) {
|
|
281
|
+
await this.resetPersistedSessionBestEffort(channelId, state);
|
|
282
|
+
if (allowFreshRetryAfterStaleResume) {
|
|
283
|
+
return this.runTurnAttempt(channelId, state, turn, false);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
currentSessionStoreAgentId() {
|
|
290
|
+
const agentId = this.resolveSessionStoreAgentId()?.trim();
|
|
291
|
+
return agentId && agentId.length > 0 ? agentId : null;
|
|
292
|
+
}
|
|
293
|
+
async ensureSessionStoreLoaded() {
|
|
294
|
+
if (!this.sessionStore) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
const agentId = this.currentSessionStoreAgentId();
|
|
298
|
+
if (!agentId) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
if (this.loadedSessionStoreAgentId && this.loadedSessionStoreAgentId !== agentId) {
|
|
302
|
+
this.loadedSessionStoreAgentId = null;
|
|
303
|
+
this.sessionStoreLoadPromise = null;
|
|
304
|
+
this.persistedSessions.clear();
|
|
305
|
+
for (const state of this.channels.values()) {
|
|
306
|
+
state.sessionHydrated = false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!this.sessionStoreLoadPromise) {
|
|
310
|
+
this.sessionStoreLoadPromise = (async () => {
|
|
311
|
+
try {
|
|
312
|
+
const stored = await this.sessionStore.load(agentId);
|
|
313
|
+
this.persistedSessions.clear();
|
|
314
|
+
for (const [channelId, sessionId] of Object.entries(stored)) {
|
|
315
|
+
this.persistedSessions.set(channelId, sessionId);
|
|
316
|
+
}
|
|
317
|
+
this.loadedSessionStoreAgentId = agentId;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
this.sessionStoreLoadPromise = null;
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
})();
|
|
324
|
+
}
|
|
325
|
+
await this.sessionStoreLoadPromise;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
hydrateChannelState(channelId, state) {
|
|
329
|
+
if (state.sessionHydrated) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
state.sessionHydrated = true;
|
|
333
|
+
const persistedSessionId = this.persistedSessions.get(channelId);
|
|
334
|
+
if (!persistedSessionId) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
state.sessionId = persistedSessionId;
|
|
245
338
|
state.sessionEstablished = true;
|
|
246
|
-
|
|
339
|
+
}
|
|
340
|
+
async persistSession(channelId, sessionId) {
|
|
341
|
+
if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await this.enqueueSessionStoreWrite(async () => {
|
|
345
|
+
if (this.persistedSessions.get(channelId) === sessionId) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.persistedSessions.set(channelId, sessionId);
|
|
349
|
+
await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries(this.persistedSessions));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async persistSessionBestEffort(channelId, sessionId) {
|
|
353
|
+
try {
|
|
354
|
+
await this.persistSession(channelId, sessionId);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error('[agents-host] failed to persist Claude session map; keeping reply delivery', {
|
|
358
|
+
agentId: this.loadedSessionStoreAgentId,
|
|
359
|
+
channelId,
|
|
360
|
+
sessionId,
|
|
361
|
+
error,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async resetPersistedSession(channelId, state) {
|
|
366
|
+
state.sessionEstablished = false;
|
|
367
|
+
state.sessionId = undefined;
|
|
368
|
+
if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
await this.enqueueSessionStoreWrite(async () => {
|
|
372
|
+
if (!this.persistedSessions.has(channelId)) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
this.persistedSessions.delete(channelId);
|
|
376
|
+
await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries(this.persistedSessions));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
async resetPersistedSessionBestEffort(channelId, state) {
|
|
380
|
+
try {
|
|
381
|
+
await this.resetPersistedSession(channelId, state);
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
console.error('[agents-host] failed to clear stale Claude session map; retrying in-memory only', {
|
|
385
|
+
agentId: this.loadedSessionStoreAgentId,
|
|
386
|
+
channelId,
|
|
387
|
+
error,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async enqueueSessionStoreWrite(writeOperation) {
|
|
392
|
+
const queuedWrite = this.sessionStoreWriteQueue.then(writeOperation);
|
|
393
|
+
this.sessionStoreWriteQueue = queuedWrite.catch(() => undefined);
|
|
394
|
+
await queuedWrite;
|
|
247
395
|
}
|
|
248
396
|
async run(state, args, prompt, options) {
|
|
249
397
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ClaudeChannelSessionStore {
|
|
2
|
+
load(agentId: string): Promise<Record<string, string>>;
|
|
3
|
+
save(agentId: string, sessions: Record<string, string>): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface FileClaudeChannelSessionStoreOptions {
|
|
6
|
+
resolvePath(agentId: string): string;
|
|
7
|
+
}
|
|
8
|
+
export declare class FileClaudeChannelSessionStore implements ClaudeChannelSessionStore {
|
|
9
|
+
private readonly options;
|
|
10
|
+
constructor(options: FileClaudeChannelSessionStoreOptions);
|
|
11
|
+
private loadFromPath;
|
|
12
|
+
load(agentId: string): Promise<Record<string, string>>;
|
|
13
|
+
save(agentId: string, sessions: Record<string, string>): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
async function syncDirectory(path) {
|
|
5
|
+
const directoryHandle = await open(path, 'r');
|
|
6
|
+
try {
|
|
7
|
+
await directoryHandle.sync();
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
await directoryHandle.close();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function normalizePersistedSessions(raw, filePath) {
|
|
14
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
15
|
+
throw new Error(`invalid Claude session file: ${filePath}`);
|
|
16
|
+
}
|
|
17
|
+
const entries = Object.entries(raw);
|
|
18
|
+
const sessions = {};
|
|
19
|
+
for (const [channelId, sessionId] of entries) {
|
|
20
|
+
if (typeof sessionId !== 'string' || sessionId.trim().length === 0) {
|
|
21
|
+
throw new Error(`invalid Claude session file: ${filePath}`);
|
|
22
|
+
}
|
|
23
|
+
sessions[channelId] = sessionId;
|
|
24
|
+
}
|
|
25
|
+
return sessions;
|
|
26
|
+
}
|
|
27
|
+
export class FileClaudeChannelSessionStore {
|
|
28
|
+
options;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
}
|
|
32
|
+
async loadFromPath(filePath) {
|
|
33
|
+
let raw;
|
|
34
|
+
try {
|
|
35
|
+
raw = await readFile(filePath, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
return normalizePersistedSessions(JSON.parse(raw), filePath);
|
|
41
|
+
}
|
|
42
|
+
async load(agentId) {
|
|
43
|
+
const filePath = this.options.resolvePath(agentId);
|
|
44
|
+
try {
|
|
45
|
+
return await this.loadFromPath(filePath);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code !== 'ENOENT') {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async save(agentId, sessions) {
|
|
55
|
+
const filePath = this.options.resolvePath(agentId);
|
|
56
|
+
const parentPath = dirname(filePath);
|
|
57
|
+
await mkdir(parentPath, { recursive: true, mode: 0o700 });
|
|
58
|
+
const entries = Object.entries(sessions).sort(([left], [right]) => left.localeCompare(right));
|
|
59
|
+
if (entries.length === 0) {
|
|
60
|
+
try {
|
|
61
|
+
await unlink(filePath);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== 'ENOENT') {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await syncDirectory(parentPath);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
73
|
+
let temporaryHandle;
|
|
74
|
+
try {
|
|
75
|
+
temporaryHandle = await open(temporaryPath, 'wx', 0o600);
|
|
76
|
+
await temporaryHandle.writeFile(JSON.stringify(Object.fromEntries(entries)), {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
});
|
|
79
|
+
await temporaryHandle.sync();
|
|
80
|
+
await temporaryHandle.close();
|
|
81
|
+
temporaryHandle = undefined;
|
|
82
|
+
await rename(temporaryPath, filePath);
|
|
83
|
+
await syncDirectory(parentPath);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (temporaryHandle) {
|
|
87
|
+
try {
|
|
88
|
+
await temporaryHandle.close();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Preserve the original durability failure.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await unlink(temporaryPath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Cleanup is best effort.
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream } from '@agentclientprotocol/sdk';
|
|
3
3
|
import type { ProviderGenerateOptions } from '../../types.js';
|
|
4
|
+
import type { CopilotChannelSessionStore } from './session-store.js';
|
|
4
5
|
interface CopilotAcpRuntime {
|
|
5
6
|
spawn: typeof spawn;
|
|
6
7
|
client: typeof client;
|
|
@@ -22,8 +23,11 @@ interface CopilotAcpRuntime {
|
|
|
22
23
|
*/
|
|
23
24
|
export declare class CopilotCliClient {
|
|
24
25
|
private readonly command;
|
|
26
|
+
private readonly sessionStore?;
|
|
27
|
+
private readonly resolveSessionStoreAgentId;
|
|
25
28
|
private readonly runtime;
|
|
26
29
|
private readonly channels;
|
|
30
|
+
private readonly persistedSessions;
|
|
27
31
|
private readonly closingSessions;
|
|
28
32
|
private readonly pendingSessionStarts;
|
|
29
33
|
private readonly pendingSessionCloses;
|
|
@@ -38,7 +42,11 @@ export declare class CopilotCliClient {
|
|
|
38
42
|
private fatalError;
|
|
39
43
|
private disposing;
|
|
40
44
|
private backendClosed;
|
|
41
|
-
|
|
45
|
+
private loadedSessionStoreAgentId;
|
|
46
|
+
private sessionStoreLoadPromise;
|
|
47
|
+
private sessionStoreWriteQueue;
|
|
48
|
+
private sessionCapabilities;
|
|
49
|
+
constructor(command: string, _ignoredArgs?: string[], runtimeOverrides?: Partial<CopilotAcpRuntime>, sessionStore?: CopilotChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined);
|
|
42
50
|
generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
|
|
43
51
|
dispose(): Promise<void>;
|
|
44
52
|
private ensureStarted;
|
|
@@ -46,6 +54,10 @@ export declare class CopilotCliClient {
|
|
|
46
54
|
private getOrCreateChannelState;
|
|
47
55
|
private processChannelQueue;
|
|
48
56
|
private getOrCreateSession;
|
|
57
|
+
private startFreshSession;
|
|
58
|
+
private restoreOrCreateSession;
|
|
59
|
+
private restoreSession;
|
|
60
|
+
private clearBufferedSessionReplay;
|
|
49
61
|
private runTurn;
|
|
50
62
|
private raceWithFatal;
|
|
51
63
|
private failAll;
|
|
@@ -54,6 +66,12 @@ export declare class CopilotCliClient {
|
|
|
54
66
|
private rejectQueuedTurnsAfterSessionTaint;
|
|
55
67
|
private clearIdleTimer;
|
|
56
68
|
private reconcileIdleChannelState;
|
|
69
|
+
private ensureSessionStoreLoaded;
|
|
70
|
+
private persistSession;
|
|
71
|
+
private persistSessionBestEffort;
|
|
72
|
+
private clearPersistedSession;
|
|
73
|
+
private clearPersistedSessionBestEffort;
|
|
74
|
+
private flushSessionStore;
|
|
57
75
|
private evictIdleChannel;
|
|
58
76
|
private shutdownBackend;
|
|
59
77
|
private waitForPendingSessionStarts;
|
|
@@ -17,6 +17,10 @@ const DEFAULT_RUNTIME = {
|
|
|
17
17
|
shutdownGracePeriodMs: DEFAULT_SHUTDOWN_GRACE_PERIOD_MS,
|
|
18
18
|
shutdownForceKillWaitMs: DEFAULT_SHUTDOWN_FORCE_KILL_WAIT_MS,
|
|
19
19
|
};
|
|
20
|
+
const DEFAULT_SESSION_CAPABILITIES = {
|
|
21
|
+
loadSession: false,
|
|
22
|
+
resumeSession: false,
|
|
23
|
+
};
|
|
20
24
|
function hasVisibleText(value) {
|
|
21
25
|
return typeof value === 'string' && value.trim().length > 0;
|
|
22
26
|
}
|
|
@@ -127,6 +131,24 @@ function createDeferredTurn(prompt, options) {
|
|
|
127
131
|
function normalizeError(error) {
|
|
128
132
|
return error instanceof Error ? error : new Error(String(error));
|
|
129
133
|
}
|
|
134
|
+
function asObject(value) {
|
|
135
|
+
return typeof value === 'object' && value !== null ? value : null;
|
|
136
|
+
}
|
|
137
|
+
function isStaleRestoreFailure(error) {
|
|
138
|
+
const message = normalizeError(error).message.toLowerCase();
|
|
139
|
+
return message.includes('session not found')
|
|
140
|
+
|| message.includes('unknown session')
|
|
141
|
+
|| message.includes('cannot resume')
|
|
142
|
+
|| message.includes('not found');
|
|
143
|
+
}
|
|
144
|
+
function readSessionCapabilities(response) {
|
|
145
|
+
const agentCapabilities = asObject(asObject(response)?.agentCapabilities);
|
|
146
|
+
const sessionCapabilities = asObject(agentCapabilities?.sessionCapabilities ?? agentCapabilities?.session);
|
|
147
|
+
return {
|
|
148
|
+
loadSession: agentCapabilities?.loadSession === true,
|
|
149
|
+
resumeSession: asObject(sessionCapabilities?.resume) !== null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
130
152
|
function markSessionTainted(error) {
|
|
131
153
|
const normalized = normalizeError(error);
|
|
132
154
|
SESSION_TAINTED_ERRORS.add(normalized);
|
|
@@ -153,8 +175,11 @@ function selectPermissionOption(options) {
|
|
|
153
175
|
*/
|
|
154
176
|
export class CopilotCliClient {
|
|
155
177
|
command;
|
|
178
|
+
sessionStore;
|
|
179
|
+
resolveSessionStoreAgentId;
|
|
156
180
|
runtime;
|
|
157
181
|
channels = new Map();
|
|
182
|
+
persistedSessions = new Map();
|
|
158
183
|
closingSessions = new WeakSet();
|
|
159
184
|
pendingSessionStarts = new Set();
|
|
160
185
|
pendingSessionCloses = new Set();
|
|
@@ -169,8 +194,14 @@ export class CopilotCliClient {
|
|
|
169
194
|
fatalError = null;
|
|
170
195
|
disposing = false;
|
|
171
196
|
backendClosed = false;
|
|
172
|
-
|
|
197
|
+
loadedSessionStoreAgentId = null;
|
|
198
|
+
sessionStoreLoadPromise = null;
|
|
199
|
+
sessionStoreWriteQueue = Promise.resolve();
|
|
200
|
+
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
201
|
+
constructor(command, _ignoredArgs = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined) {
|
|
173
202
|
this.command = command;
|
|
203
|
+
this.sessionStore = sessionStore;
|
|
204
|
+
this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
|
|
174
205
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
175
206
|
this.fatalPromise = new Promise((_, reject) => {
|
|
176
207
|
this.rejectFatalPromise = reject;
|
|
@@ -248,7 +279,7 @@ export class CopilotCliClient {
|
|
|
248
279
|
}
|
|
249
280
|
});
|
|
250
281
|
try {
|
|
251
|
-
await connection.agent.request(this.runtime.methods.agent.initialize, {
|
|
282
|
+
const initializeResponse = await connection.agent.request(this.runtime.methods.agent.initialize, {
|
|
252
283
|
protocolVersion: this.runtime.protocolVersion,
|
|
253
284
|
clientCapabilities: {},
|
|
254
285
|
clientInfo: {
|
|
@@ -256,6 +287,7 @@ export class CopilotCliClient {
|
|
|
256
287
|
version: '0.1.6',
|
|
257
288
|
},
|
|
258
289
|
});
|
|
290
|
+
this.sessionCapabilities = readSessionCapabilities(initializeResponse);
|
|
259
291
|
}
|
|
260
292
|
catch (error) {
|
|
261
293
|
const normalized = new Error(`Copilot ACP initialize failed: ${normalizeError(error).message}`);
|
|
@@ -328,7 +360,11 @@ export class CopilotCliClient {
|
|
|
328
360
|
if (!this.connection) {
|
|
329
361
|
throw new Error('Copilot ACP connection is not available');
|
|
330
362
|
}
|
|
331
|
-
|
|
363
|
+
await this.ensureSessionStoreLoaded();
|
|
364
|
+
const persistedSessionId = this.persistedSessions.get(channelId);
|
|
365
|
+
const sessionPromise = persistedSessionId
|
|
366
|
+
? this.restoreOrCreateSession(channelId, persistedSessionId)
|
|
367
|
+
: this.startFreshSession(channelId);
|
|
332
368
|
this.pendingSessionStarts.add(sessionPromise);
|
|
333
369
|
state.sessionPromise = sessionPromise;
|
|
334
370
|
let sessionAdopted = false;
|
|
@@ -363,6 +399,69 @@ export class CopilotCliClient {
|
|
|
363
399
|
}
|
|
364
400
|
}
|
|
365
401
|
}
|
|
402
|
+
async startFreshSession(channelId) {
|
|
403
|
+
if (!this.connection) {
|
|
404
|
+
throw new Error('Copilot ACP connection is not available');
|
|
405
|
+
}
|
|
406
|
+
const session = await this.connection.agent.buildSession(this.runtime.cwd).start();
|
|
407
|
+
await this.persistSessionBestEffort(channelId, session.sessionId);
|
|
408
|
+
return session;
|
|
409
|
+
}
|
|
410
|
+
async restoreOrCreateSession(channelId, sessionId) {
|
|
411
|
+
if (!this.sessionCapabilities.resumeSession && !this.sessionCapabilities.loadSession) {
|
|
412
|
+
return this.startFreshSession(channelId);
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
return await this.restoreSession(sessionId);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (!isStaleRestoreFailure(error)) {
|
|
419
|
+
throw normalizeError(error);
|
|
420
|
+
}
|
|
421
|
+
await this.clearPersistedSessionBestEffort(channelId);
|
|
422
|
+
return this.startFreshSession(channelId);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async restoreSession(sessionId) {
|
|
426
|
+
if (!this.connection) {
|
|
427
|
+
throw new Error('Copilot ACP connection is not available');
|
|
428
|
+
}
|
|
429
|
+
const agent = this.connection.agent;
|
|
430
|
+
if (typeof agent.attachSession !== 'function') {
|
|
431
|
+
throw new Error('Copilot ACP SDK does not expose session attachment helpers');
|
|
432
|
+
}
|
|
433
|
+
const session = agent.attachSession({ sessionId });
|
|
434
|
+
try {
|
|
435
|
+
if (this.sessionCapabilities.resumeSession) {
|
|
436
|
+
await this.connection.agent.request(this.runtime.methods.agent.session.resume, {
|
|
437
|
+
sessionId,
|
|
438
|
+
cwd: this.runtime.cwd,
|
|
439
|
+
mcpServers: [],
|
|
440
|
+
});
|
|
441
|
+
return session;
|
|
442
|
+
}
|
|
443
|
+
if (this.sessionCapabilities.loadSession) {
|
|
444
|
+
await this.connection.agent.request(this.runtime.methods.agent.session.load, {
|
|
445
|
+
sessionId,
|
|
446
|
+
cwd: this.runtime.cwd,
|
|
447
|
+
mcpServers: [],
|
|
448
|
+
});
|
|
449
|
+
this.clearBufferedSessionReplay(session);
|
|
450
|
+
return session;
|
|
451
|
+
}
|
|
452
|
+
throw new Error('Copilot ACP agent does not advertise session restore capabilities');
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
session.dispose();
|
|
456
|
+
throw normalizeError(error);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
clearBufferedSessionReplay(session) {
|
|
460
|
+
const updates = session.updates;
|
|
461
|
+
if (updates && Array.isArray(updates.values)) {
|
|
462
|
+
updates.values = [];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
366
465
|
async runTurn(session, prompt, options) {
|
|
367
466
|
const promptPromise = this.raceWithFatal(session.prompt(prompt));
|
|
368
467
|
const promptFailure = new Promise((_, reject) => {
|
|
@@ -438,6 +537,12 @@ export class CopilotCliClient {
|
|
|
438
537
|
state.session = undefined;
|
|
439
538
|
}
|
|
440
539
|
this.closeSession(session);
|
|
540
|
+
for (const [channelId, candidate] of this.channels.entries()) {
|
|
541
|
+
if (candidate === state) {
|
|
542
|
+
void this.clearPersistedSessionBestEffort(channelId);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
441
546
|
}
|
|
442
547
|
closeSession(session) {
|
|
443
548
|
if (this.closingSessions.has(session)) {
|
|
@@ -507,6 +612,78 @@ export class CopilotCliClient {
|
|
|
507
612
|
this.evictIdleChannel(channelId, state, generation);
|
|
508
613
|
}, this.runtime.idleSessionTtlMs);
|
|
509
614
|
}
|
|
615
|
+
async ensureSessionStoreLoaded() {
|
|
616
|
+
if (!this.sessionStore) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const agentId = this.resolveSessionStoreAgentId()?.trim();
|
|
620
|
+
if (!agentId) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (this.loadedSessionStoreAgentId === agentId) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (!this.sessionStoreLoadPromise) {
|
|
627
|
+
this.sessionStoreLoadPromise = (async () => {
|
|
628
|
+
const loaded = await this.sessionStore.load(agentId);
|
|
629
|
+
this.persistedSessions.clear();
|
|
630
|
+
for (const [channelId, sessionId] of Object.entries(loaded)) {
|
|
631
|
+
this.persistedSessions.set(channelId, sessionId);
|
|
632
|
+
}
|
|
633
|
+
this.loadedSessionStoreAgentId = agentId;
|
|
634
|
+
})().finally(() => {
|
|
635
|
+
this.sessionStoreLoadPromise = null;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
await this.sessionStoreLoadPromise;
|
|
639
|
+
}
|
|
640
|
+
async persistSession(channelId, sessionId) {
|
|
641
|
+
await this.ensureSessionStoreLoaded();
|
|
642
|
+
this.persistedSessions.set(channelId, sessionId);
|
|
643
|
+
await this.flushSessionStore();
|
|
644
|
+
}
|
|
645
|
+
async persistSessionBestEffort(channelId, sessionId) {
|
|
646
|
+
try {
|
|
647
|
+
await this.persistSession(channelId, sessionId);
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
console.error('[agents-host] failed to persist Copilot session map; keeping reply delivery', {
|
|
651
|
+
agentId: this.loadedSessionStoreAgentId,
|
|
652
|
+
channelId,
|
|
653
|
+
sessionId,
|
|
654
|
+
error,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async clearPersistedSession(channelId) {
|
|
659
|
+
await this.ensureSessionStoreLoaded();
|
|
660
|
+
if (!this.persistedSessions.delete(channelId)) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
await this.flushSessionStore();
|
|
664
|
+
}
|
|
665
|
+
async clearPersistedSessionBestEffort(channelId) {
|
|
666
|
+
try {
|
|
667
|
+
await this.clearPersistedSession(channelId);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
console.error('[agents-host] failed to clear Copilot session map; retrying in-memory only', {
|
|
671
|
+
agentId: this.loadedSessionStoreAgentId,
|
|
672
|
+
channelId,
|
|
673
|
+
error,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async flushSessionStore() {
|
|
678
|
+
if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const agentId = this.loadedSessionStoreAgentId;
|
|
682
|
+
const snapshot = Object.fromEntries(this.persistedSessions.entries());
|
|
683
|
+
const write = this.sessionStoreWriteQueue.then(() => this.sessionStore.save(agentId, snapshot));
|
|
684
|
+
this.sessionStoreWriteQueue = write.catch(() => { });
|
|
685
|
+
await write;
|
|
686
|
+
}
|
|
510
687
|
evictIdleChannel(channelId, state, generation) {
|
|
511
688
|
if (this.disposing || this.fatalError || this.backendClosed) {
|
|
512
689
|
return;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CopilotChannelSessionStore {
|
|
2
|
+
load(agentId: string): Promise<Record<string, string>>;
|
|
3
|
+
save(agentId: string, sessions: Record<string, string>): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface FileCopilotChannelSessionStoreOptions {
|
|
6
|
+
resolvePath(agentId: string): string;
|
|
7
|
+
}
|
|
8
|
+
export declare class FileCopilotChannelSessionStore implements CopilotChannelSessionStore {
|
|
9
|
+
private readonly options;
|
|
10
|
+
constructor(options: FileCopilotChannelSessionStoreOptions);
|
|
11
|
+
private loadFromPath;
|
|
12
|
+
load(agentId: string): Promise<Record<string, string>>;
|
|
13
|
+
save(agentId: string, sessions: Record<string, string>): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
async function syncDirectory(path) {
|
|
5
|
+
const directoryHandle = await open(path, 'r');
|
|
6
|
+
try {
|
|
7
|
+
await directoryHandle.sync();
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
await directoryHandle.close();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function normalizePersistedSessions(raw, filePath) {
|
|
14
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
15
|
+
throw new Error(`invalid Copilot session file: ${filePath}`);
|
|
16
|
+
}
|
|
17
|
+
const entries = Object.entries(raw);
|
|
18
|
+
const sessions = {};
|
|
19
|
+
for (const [channelId, sessionId] of entries) {
|
|
20
|
+
if (typeof sessionId !== 'string' || sessionId.trim().length === 0) {
|
|
21
|
+
throw new Error(`invalid Copilot session file: ${filePath}`);
|
|
22
|
+
}
|
|
23
|
+
sessions[channelId] = sessionId;
|
|
24
|
+
}
|
|
25
|
+
return sessions;
|
|
26
|
+
}
|
|
27
|
+
export class FileCopilotChannelSessionStore {
|
|
28
|
+
options;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
}
|
|
32
|
+
async loadFromPath(filePath) {
|
|
33
|
+
const raw = await readFile(filePath, 'utf8');
|
|
34
|
+
return normalizePersistedSessions(JSON.parse(raw), filePath);
|
|
35
|
+
}
|
|
36
|
+
async load(agentId) {
|
|
37
|
+
const filePath = this.options.resolvePath(agentId);
|
|
38
|
+
try {
|
|
39
|
+
return await this.loadFromPath(filePath);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code !== 'ENOENT') {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async save(agentId, sessions) {
|
|
49
|
+
const filePath = this.options.resolvePath(agentId);
|
|
50
|
+
const parentPath = dirname(filePath);
|
|
51
|
+
await mkdir(parentPath, { recursive: true, mode: 0o700 });
|
|
52
|
+
const entries = Object.entries(sessions).sort(([left], [right]) => left.localeCompare(right));
|
|
53
|
+
if (entries.length === 0) {
|
|
54
|
+
try {
|
|
55
|
+
await unlink(filePath);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code !== 'ENOENT') {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await syncDirectory(parentPath);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
67
|
+
let temporaryHandle;
|
|
68
|
+
try {
|
|
69
|
+
temporaryHandle = await open(temporaryPath, 'wx', 0o600);
|
|
70
|
+
await temporaryHandle.writeFile(JSON.stringify(Object.fromEntries(entries)), {
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
});
|
|
73
|
+
await temporaryHandle.sync();
|
|
74
|
+
await temporaryHandle.close();
|
|
75
|
+
temporaryHandle = undefined;
|
|
76
|
+
await rename(temporaryPath, filePath);
|
|
77
|
+
await syncDirectory(parentPath);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (temporaryHandle) {
|
|
81
|
+
try {
|
|
82
|
+
await temporaryHandle.close();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Preserve the original durability failure.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await unlink(temporaryPath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Cleanup is best effort.
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { ClaudeCliClient } from './claude/cli-client.js';
|
|
2
2
|
import { ClaudeProviderAdapter } from './claude/adapter.js';
|
|
3
|
+
import { FileClaudeChannelSessionStore } from './claude/session-store.js';
|
|
3
4
|
import { CopilotCliClient } from './copilot/cli-client.js';
|
|
4
5
|
import { CopilotProviderAdapter } from './copilot/adapter.js';
|
|
6
|
+
import { FileCopilotChannelSessionStore } from './copilot/session-store.js';
|
|
7
|
+
import { resolveClaudeSessionMapPath, resolveCopilotSessionMapPath } from '../state-paths.js';
|
|
5
8
|
export function createProvider(config) {
|
|
6
9
|
switch (config.provider) {
|
|
7
10
|
case 'claude': {
|
|
8
|
-
const cli = new ClaudeCliClient(config.claudeCommand, config.claudeArgs
|
|
11
|
+
const cli = new ClaudeCliClient(config.claudeCommand, config.claudeArgs, {}, new FileClaudeChannelSessionStore({
|
|
12
|
+
resolvePath: (agentId) => resolveClaudeSessionMapPath(config.stateRootDir, agentId),
|
|
13
|
+
}), config.resolveStableAgentId);
|
|
9
14
|
return new ClaudeProviderAdapter(cli);
|
|
10
15
|
}
|
|
11
16
|
case 'copilot': {
|
|
12
17
|
const cli = new CopilotCliClient(config.copilotCommand, config.copilotArgs, {
|
|
13
18
|
idleSessionTtlMs: config.copilotSessionTtlMinutes * 60 * 1000,
|
|
14
|
-
}
|
|
19
|
+
}, new FileCopilotChannelSessionStore({
|
|
20
|
+
resolvePath: (agentId) => resolveCopilotSessionMapPath(config.stateRootDir, agentId),
|
|
21
|
+
}), config.resolveStableAgentId);
|
|
15
22
|
return new CopilotProviderAdapter(cli);
|
|
16
23
|
}
|
|
17
24
|
default:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function resolveSingleAgentStateRoot(env?: NodeJS.ProcessEnv, resolvedHomeDir?: string): string;
|
|
2
|
+
export declare function resolveManagedStateRoot(rootPath: string): string;
|
|
3
|
+
export declare function resolveLocalConfigAgentStateRoot(rootPath: string, agentKey: string): string;
|
|
4
|
+
export declare function resolveAgentCursorPath(stateRootDir: string, agentId: string): string;
|
|
5
|
+
export declare function resolveClaudeSessionMapPath(stateRootDir: string, agentId: string): string;
|
|
6
|
+
export declare function resolveCopilotSessionMapPath(stateRootDir: string, agentId: string): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
const SINGLE_AGENT_HOME_ROOT = '.borgee';
|
|
5
|
+
const AGENTS_HOST_ROOT = 'agents-host';
|
|
6
|
+
const SINGLE_AGENT_NAMESPACE = 'single-agent';
|
|
7
|
+
const MANAGED_STATE_DIRNAME = '.state';
|
|
8
|
+
const STATE_ROOT_LABEL_MAX_LENGTH = 48;
|
|
9
|
+
function encodeSegment(value) {
|
|
10
|
+
return encodeURIComponent(value);
|
|
11
|
+
}
|
|
12
|
+
function sanitizeStateRootLabel(agentKey) {
|
|
13
|
+
const sanitized = encodeSegment(agentKey)
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/%/g, '-')
|
|
16
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
17
|
+
.replace(/-+/g, '-')
|
|
18
|
+
.replace(/^-|-$/g, '')
|
|
19
|
+
.slice(0, STATE_ROOT_LABEL_MAX_LENGTH);
|
|
20
|
+
return sanitized.length > 0 ? sanitized : 'agent';
|
|
21
|
+
}
|
|
22
|
+
function hashStateRootKey(agentKey) {
|
|
23
|
+
return createHash('sha256').update(agentKey).digest('hex').slice(0, 12);
|
|
24
|
+
}
|
|
25
|
+
export function resolveSingleAgentStateRoot(env = process.env, resolvedHomeDir = homedir()) {
|
|
26
|
+
const home = env.HOME?.trim() || resolvedHomeDir.trim();
|
|
27
|
+
if (!home) {
|
|
28
|
+
throw new Error('Unable to resolve a user home directory for agents-host state');
|
|
29
|
+
}
|
|
30
|
+
return join(home, SINGLE_AGENT_HOME_ROOT, AGENTS_HOST_ROOT, SINGLE_AGENT_NAMESPACE);
|
|
31
|
+
}
|
|
32
|
+
export function resolveManagedStateRoot(rootPath) {
|
|
33
|
+
return join(resolve(rootPath), MANAGED_STATE_DIRNAME);
|
|
34
|
+
}
|
|
35
|
+
export function resolveLocalConfigAgentStateRoot(rootPath, agentKey) {
|
|
36
|
+
return join(resolveManagedStateRoot(rootPath), `${sanitizeStateRootLabel(agentKey)}-${hashStateRootKey(agentKey)}`);
|
|
37
|
+
}
|
|
38
|
+
export function resolveAgentCursorPath(stateRootDir, agentId) {
|
|
39
|
+
return join(stateRootDir, `bpp-cursor-${encodeSegment(agentId)}.json`);
|
|
40
|
+
}
|
|
41
|
+
export function resolveClaudeSessionMapPath(stateRootDir, agentId) {
|
|
42
|
+
return join(stateRootDir, `claude-channel-sessions-${encodeSegment(agentId)}.json`);
|
|
43
|
+
}
|
|
44
|
+
export function resolveCopilotSessionMapPath(stateRootDir, agentId) {
|
|
45
|
+
return join(stateRootDir, `copilot-channel-sessions-${encodeSegment(agentId)}.json`);
|
|
46
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface ProviderCommandConfig {
|
|
|
8
8
|
}
|
|
9
9
|
export interface ProviderRuntimeConfig extends ProviderCommandConfig {
|
|
10
10
|
provider: ProviderKind;
|
|
11
|
+
stateRootDir: string;
|
|
12
|
+
resolveStableAgentId?: () => string | undefined;
|
|
11
13
|
}
|
|
12
14
|
export interface HostedAgentConfig {
|
|
13
15
|
agentApiKey: string;
|
|
@@ -16,6 +18,7 @@ export interface HostedAgentConfig {
|
|
|
16
18
|
}
|
|
17
19
|
export interface AgentsHostConfig extends ProviderCommandConfig {
|
|
18
20
|
borgeeBaseUrl: string;
|
|
21
|
+
stateRootDir: string;
|
|
19
22
|
agent: HostedAgentConfig;
|
|
20
23
|
}
|
|
21
24
|
export interface LocalHostConfigFile {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borgee/agents-host",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"description": "Minimal local agents host: connects a local Claude/Copilot CLI to a Borgee agent over @borgee/plugin-sdk",
|
|
10
10
|
"bin": {
|
|
11
|
-
"agents-host": "
|
|
11
|
+
"agents-host": "dist/cli.js"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
@@ -20,19 +20,6 @@
|
|
|
20
20
|
"directory": "packages/agents-host"
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"@agentclientprotocol/sdk": "^1.2.1",
|
|
25
|
-
"cross-spawn": "^7.0.6",
|
|
26
|
-
"yaml": "^2.8.1",
|
|
27
|
-
"@borgee/plugin-sdk": "0.1.2"
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@types/cross-spawn": "^6.0.6",
|
|
31
|
-
"@types/node": "^24.3.0",
|
|
32
|
-
"tsx": "^4.20.5",
|
|
33
|
-
"typescript": "^5.9.3",
|
|
34
|
-
"vitest": "^4.1.5"
|
|
35
|
-
},
|
|
36
23
|
"scripts": {
|
|
37
24
|
"predev": "pnpm --filter @borgee/plugin-sdk build",
|
|
38
25
|
"dev": "tsx src/index.ts",
|
|
@@ -46,5 +33,18 @@
|
|
|
46
33
|
"pretest": "pnpm --filter @borgee/plugin-sdk build",
|
|
47
34
|
"test": "vitest run --testTimeout=10000",
|
|
48
35
|
"pretypecheck": "pnpm --filter @borgee/plugin-sdk build"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@agentclientprotocol/sdk": "^1.2.1",
|
|
39
|
+
"@borgee/plugin-sdk": "0.1.4",
|
|
40
|
+
"cross-spawn": "^7.0.6",
|
|
41
|
+
"yaml": "^2.8.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/cross-spawn": "^6.0.6",
|
|
45
|
+
"@types/node": "^24.3.0",
|
|
46
|
+
"tsx": "^4.20.5",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"vitest": "^4.1.5"
|
|
49
49
|
}
|
|
50
|
-
}
|
|
50
|
+
}
|