@borgee/agents-host 0.1.8 → 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 +12 -5
- package/dist/agents-host.d.ts +9 -0
- package/dist/agents-host.js +221 -12
- package/dist/chat/chat-control-plane.d.ts +6 -3
- package/dist/chat/sdk-chat-control-plane.d.ts +10 -6
- package/dist/chat/sdk-chat-control-plane.js +28 -18
- 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/adapter.d.ts +3 -2
- package/dist/providers/claude/adapter.js +7 -2
- package/dist/providers/claude/cli-client.d.ts +34 -11
- package/dist/providers/claude/cli-client.js +438 -37
- package/dist/providers/claude/session-store.d.ts +14 -0
- package/dist/providers/claude/session-store.js +103 -0
- package/dist/providers/copilot/adapter.d.ts +2 -2
- package/dist/providers/copilot/adapter.js +4 -2
- package/dist/providers/copilot/cli-client.d.ts +21 -2
- package/dist/providers/copilot/cli-client.js +270 -14
- 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/providers/provider-adapter.d.ts +2 -2
- package/dist/state-paths.d.ts +6 -0
- package/dist/state-paths.js +46 -0
- package/dist/types.d.ts +12 -0
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Borgee channel message
|
|
|
26
26
|
- local-config multi-agent startup
|
|
27
27
|
- one isolated `AgentsHost` per effective agent key
|
|
28
28
|
- per-channel conversation memory via each provider's native session mechanism
|
|
29
|
+
- one draft progress message per turn when a provider emits public progress
|
|
29
30
|
- hot reload for host config and agent file add / update / remove
|
|
30
31
|
|
|
31
32
|
**Out of scope:**
|
|
@@ -314,13 +315,19 @@ Each Borgee channel is mapped 1:1 to a provider-native session while it stays
|
|
|
314
315
|
active:
|
|
315
316
|
|
|
316
317
|
- **Claude**: first turn for a channel uses `--session-id <uuid>`; every turn
|
|
317
|
-
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.
|
|
318
321
|
- **Copilot**: one persistent `copilot --acp` subprocess is shared by a single
|
|
319
322
|
`AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are
|
|
320
|
-
serialized; different channels keep isolated ACP sessions.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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.
|
|
324
331
|
|
|
325
332
|
## Testing
|
|
326
333
|
|
package/dist/agents-host.d.ts
CHANGED
|
@@ -23,8 +23,13 @@ export declare class AgentsHost {
|
|
|
23
23
|
private readonly config;
|
|
24
24
|
private readonly provider;
|
|
25
25
|
private readonly borgee;
|
|
26
|
+
private readonly activeTurns;
|
|
27
|
+
private readonly progressChannelQueues;
|
|
28
|
+
private readonly progressDrafts;
|
|
26
29
|
private selfAgentId;
|
|
30
|
+
private selfAgentIdPromise;
|
|
27
31
|
private started;
|
|
32
|
+
private controlPlaneClosed;
|
|
28
33
|
constructor(config: AgentsHostConfig, deps?: {
|
|
29
34
|
borgee?: ChatControlPlane;
|
|
30
35
|
provider?: ProviderAdapter;
|
|
@@ -32,4 +37,8 @@ export declare class AgentsHost {
|
|
|
32
37
|
start(): Promise<void>;
|
|
33
38
|
stop(): Promise<void>;
|
|
34
39
|
private handleMessage;
|
|
40
|
+
private trackActiveTurn;
|
|
41
|
+
private enqueueProgressChannelTurn;
|
|
42
|
+
private ensureProgressDraft;
|
|
43
|
+
private ensureSelfAgentId;
|
|
35
44
|
}
|
package/dist/agents-host.js
CHANGED
|
@@ -1,5 +1,114 @@
|
|
|
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;
|
|
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
|
+
}
|
|
11
|
+
function hasVisibleText(value) {
|
|
12
|
+
return value.trim().length > 0;
|
|
13
|
+
}
|
|
14
|
+
function providerUsesDraftProgress(provider) {
|
|
15
|
+
return provider === 'claude' || provider === 'copilot';
|
|
16
|
+
}
|
|
17
|
+
class DraftMessageController {
|
|
18
|
+
borgee;
|
|
19
|
+
channelId;
|
|
20
|
+
canWrite;
|
|
21
|
+
messageId = null;
|
|
22
|
+
currentBody = '';
|
|
23
|
+
pendingBody = null;
|
|
24
|
+
timer = null;
|
|
25
|
+
sealed = false;
|
|
26
|
+
writeChain = Promise.resolve();
|
|
27
|
+
constructor(borgee, channelId, canWrite) {
|
|
28
|
+
this.borgee = borgee;
|
|
29
|
+
this.channelId = channelId;
|
|
30
|
+
this.canWrite = canWrite;
|
|
31
|
+
}
|
|
32
|
+
update(text) {
|
|
33
|
+
if (this.sealed || !hasVisibleText(text)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.pendingBody = text;
|
|
37
|
+
if (this.timer) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.timer = setTimeout(() => {
|
|
41
|
+
this.timer = null;
|
|
42
|
+
void this.flushPending().catch((error) => {
|
|
43
|
+
console.error('[agents-host] failed to flush draft progress:', error);
|
|
44
|
+
});
|
|
45
|
+
}, STREAM_PROGRESS_EDIT_THROTTLE_MS);
|
|
46
|
+
}
|
|
47
|
+
cancelProgress() {
|
|
48
|
+
if (this.timer) {
|
|
49
|
+
clearTimeout(this.timer);
|
|
50
|
+
this.timer = null;
|
|
51
|
+
}
|
|
52
|
+
this.sealed = true;
|
|
53
|
+
}
|
|
54
|
+
async finalize(finalText) {
|
|
55
|
+
this.sealed = true;
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearTimeout(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
if (hasVisibleText(finalText)) {
|
|
61
|
+
this.pendingBody = finalText;
|
|
62
|
+
await this.flushPending();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await this.enqueueWrite(async () => {
|
|
66
|
+
if (!this.canWrite() || !this.messageId) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await this.borgee.deleteMessage(this.messageId);
|
|
70
|
+
this.messageId = null;
|
|
71
|
+
this.currentBody = '';
|
|
72
|
+
this.pendingBody = null;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async discard() {
|
|
76
|
+
this.sealed = true;
|
|
77
|
+
if (this.timer) {
|
|
78
|
+
clearTimeout(this.timer);
|
|
79
|
+
this.timer = null;
|
|
80
|
+
}
|
|
81
|
+
this.pendingBody = null;
|
|
82
|
+
await this.enqueueWrite(async () => {
|
|
83
|
+
if (!this.canWrite() || !this.messageId) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await this.borgee.deleteMessage(this.messageId);
|
|
87
|
+
this.messageId = null;
|
|
88
|
+
this.currentBody = '';
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async flushPending() {
|
|
92
|
+
await this.enqueueWrite(async () => {
|
|
93
|
+
const body = this.pendingBody;
|
|
94
|
+
if (!this.canWrite() || !body || body === this.currentBody) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this.messageId) {
|
|
98
|
+
await this.borgee.editMessage(this.messageId, body);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const posted = await this.borgee.postMessage(this.channelId, body);
|
|
102
|
+
this.messageId = posted.messageId;
|
|
103
|
+
}
|
|
104
|
+
this.currentBody = body;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
enqueueWrite(operation) {
|
|
108
|
+
this.writeChain = this.writeChain.then(operation, operation);
|
|
109
|
+
return this.writeChain;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
3
112
|
/**
|
|
4
113
|
* Minimal single-agent agents host: connects one local Claude/Copilot CLI
|
|
5
114
|
* to exactly one Borgee agent over `@borgee/plugin-sdk` (BPP / `/ws/plugin`).
|
|
@@ -22,35 +131,54 @@ export class AgentsHost {
|
|
|
22
131
|
config;
|
|
23
132
|
provider;
|
|
24
133
|
borgee;
|
|
134
|
+
activeTurns = new Set();
|
|
135
|
+
progressChannelQueues = new Map();
|
|
136
|
+
progressDrafts = new Map();
|
|
25
137
|
selfAgentId = null;
|
|
138
|
+
selfAgentIdPromise = null;
|
|
26
139
|
started = false;
|
|
140
|
+
controlPlaneClosed = false;
|
|
27
141
|
constructor(config, deps) {
|
|
28
142
|
this.config = config;
|
|
29
143
|
this.provider = deps?.provider ?? createProvider({
|
|
30
144
|
provider: config.agent.provider,
|
|
145
|
+
stateRootDir: config.stateRootDir,
|
|
146
|
+
resolveStableAgentId: () => this.selfAgentId ?? undefined,
|
|
31
147
|
claudeCommand: config.claudeCommand,
|
|
32
148
|
claudeArgs: config.claudeArgs,
|
|
33
149
|
copilotCommand: config.copilotCommand,
|
|
34
150
|
copilotArgs: config.copilotArgs,
|
|
35
151
|
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
36
152
|
});
|
|
37
|
-
this.borgee = deps?.borgee ?? new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
});
|
|
42
162
|
}
|
|
43
163
|
async start() {
|
|
44
164
|
if (this.started)
|
|
45
165
|
return;
|
|
46
166
|
this.started = true;
|
|
167
|
+
this.controlPlaneClosed = false;
|
|
168
|
+
await ensurePrivateStateRoot(this.config.stateRootDir);
|
|
47
169
|
await this.borgee.connect((message) => {
|
|
48
|
-
|
|
170
|
+
if (!this.started) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const task = providerUsesDraftProgress(this.config.agent.provider)
|
|
174
|
+
? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleMessage(message))
|
|
175
|
+
: this.handleMessage(message);
|
|
176
|
+
this.trackActiveTurn(task);
|
|
177
|
+
return task;
|
|
49
178
|
});
|
|
50
|
-
const
|
|
51
|
-
this.selfAgentId = me.id;
|
|
179
|
+
const agentId = await this.ensureSelfAgentId();
|
|
52
180
|
console.log('[agents-host] connected', {
|
|
53
|
-
agentId
|
|
181
|
+
agentId,
|
|
54
182
|
agentName: this.config.agent.agentName,
|
|
55
183
|
provider: this.config.agent.provider,
|
|
56
184
|
});
|
|
@@ -59,8 +187,13 @@ export class AgentsHost {
|
|
|
59
187
|
if (!this.started)
|
|
60
188
|
return;
|
|
61
189
|
this.started = false;
|
|
190
|
+
for (const draft of this.progressDrafts.values()) {
|
|
191
|
+
draft.cancelProgress();
|
|
192
|
+
}
|
|
193
|
+
this.controlPlaneClosed = true;
|
|
62
194
|
await this.borgee.close();
|
|
63
195
|
await this.provider.dispose?.();
|
|
196
|
+
await Promise.allSettled([...this.activeTurns]);
|
|
64
197
|
}
|
|
65
198
|
async handleMessage(msg) {
|
|
66
199
|
console.log('[agents-host] received message', {
|
|
@@ -69,8 +202,9 @@ export class AgentsHost {
|
|
|
69
202
|
agentName: this.config.agent.agentName,
|
|
70
203
|
channelId: msg.channel_id,
|
|
71
204
|
});
|
|
205
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
72
206
|
const authorId = String(msg.user_id ?? msg.sender_id ?? 'unknown');
|
|
73
|
-
if (
|
|
207
|
+
if (authorId === selfAgentId) {
|
|
74
208
|
return;
|
|
75
209
|
}
|
|
76
210
|
const content = String(msg.content ?? msg.body ?? '').trim();
|
|
@@ -80,6 +214,8 @@ export class AgentsHost {
|
|
|
80
214
|
// messages an agent receives (per-agent + per-channel require_mention is
|
|
81
215
|
// enforced at BPP fan-out via AgentReceivesChannelMessage). If a channel
|
|
82
216
|
// message reaches this host at all, the agent is meant to handle it.
|
|
217
|
+
const stopTyping = this.borgee.startTyping(msg.channel_id);
|
|
218
|
+
const useDraftProgress = providerUsesDraftProgress(this.config.agent.provider);
|
|
83
219
|
try {
|
|
84
220
|
const reply = await this.provider.generateReply({
|
|
85
221
|
agentName: this.config.agent.agentName,
|
|
@@ -87,15 +223,88 @@ export class AgentsHost {
|
|
|
87
223
|
channelId: msg.channel_id,
|
|
88
224
|
incomingAuthorId: authorId,
|
|
89
225
|
incomingContent: content,
|
|
90
|
-
}
|
|
91
|
-
|
|
226
|
+
}, useDraftProgress
|
|
227
|
+
? {
|
|
228
|
+
onProgress: (update) => {
|
|
229
|
+
if (!this.started || this.controlPlaneClosed) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.ensureProgressDraft(msg.channel_id).update(update.text);
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
: undefined);
|
|
236
|
+
if (useDraftProgress) {
|
|
237
|
+
const draft = this.progressDrafts.get(msg.channel_id);
|
|
238
|
+
if (!this.started) {
|
|
239
|
+
await draft?.discard().catch(() => { });
|
|
240
|
+
this.progressDrafts.delete(msg.channel_id);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
await (draft ?? this.ensureProgressDraft(msg.channel_id)).finalize(reply.text);
|
|
244
|
+
this.progressDrafts.delete(msg.channel_id);
|
|
245
|
+
}
|
|
246
|
+
else if (this.started && !this.controlPlaneClosed) {
|
|
247
|
+
await this.borgee.postMessage(msg.channel_id, reply.text);
|
|
248
|
+
}
|
|
92
249
|
}
|
|
93
250
|
catch (error) {
|
|
251
|
+
if (useDraftProgress) {
|
|
252
|
+
const draft = this.progressDrafts.get(msg.channel_id);
|
|
253
|
+
if (draft) {
|
|
254
|
+
await draft.discard().catch(() => { });
|
|
255
|
+
this.progressDrafts.delete(msg.channel_id);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
94
258
|
console.error('[agents-host] failed to generate or send reply:', {
|
|
95
259
|
provider: this.config.agent.provider,
|
|
96
260
|
agentName: this.config.agent.agentName,
|
|
97
261
|
error,
|
|
98
262
|
});
|
|
99
263
|
}
|
|
264
|
+
finally {
|
|
265
|
+
stopTyping();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
trackActiveTurn(task) {
|
|
269
|
+
this.activeTurns.add(task);
|
|
270
|
+
void task.finally(() => {
|
|
271
|
+
this.activeTurns.delete(task);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
enqueueProgressChannelTurn(channelId, task) {
|
|
275
|
+
const previous = this.progressChannelQueues.get(channelId) ?? Promise.resolve();
|
|
276
|
+
const next = previous
|
|
277
|
+
.catch(() => { })
|
|
278
|
+
.then(task)
|
|
279
|
+
.finally(() => {
|
|
280
|
+
if (this.progressChannelQueues.get(channelId) === next) {
|
|
281
|
+
this.progressChannelQueues.delete(channelId);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
this.progressChannelQueues.set(channelId, next);
|
|
285
|
+
return next;
|
|
286
|
+
}
|
|
287
|
+
ensureProgressDraft(channelId) {
|
|
288
|
+
let draft = this.progressDrafts.get(channelId);
|
|
289
|
+
if (!draft) {
|
|
290
|
+
draft = new DraftMessageController(this.borgee, channelId, () => !this.controlPlaneClosed);
|
|
291
|
+
this.progressDrafts.set(channelId, draft);
|
|
292
|
+
}
|
|
293
|
+
return draft;
|
|
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;
|
|
100
309
|
}
|
|
101
310
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { ChannelMessageEvent, MeResponseUser } from '../types.js';
|
|
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
|
-
postMessage(channelId: string, content: string): Promise<
|
|
5
|
+
postMessage(channelId: string, content: string): Promise<PostedMessage>;
|
|
6
|
+
editMessage(messageId: string, content: string): Promise<void>;
|
|
7
|
+
deleteMessage(messageId: string): Promise<void>;
|
|
8
|
+
startTyping(channelId: string): () => void;
|
|
6
9
|
getMe(): Promise<MeResponseUser>;
|
|
7
10
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type BorgeePluginClient, type BorgeePluginOptions, type InboundMessageEvent } from '@borgee/plugin-sdk';
|
|
2
|
-
import type { ChannelMessageEvent, MeResponseUser } from '../types.js';
|
|
2
|
+
import type { ChannelMessageEvent, MeResponseUser, PostedMessage } from '../types.js';
|
|
3
3
|
import type { ChatControlPlane } from './chat-control-plane.js';
|
|
4
|
-
type PluginClientLike = Pick<BorgeePluginClient, 'close' | 'connect' | 'getMe' | 'on' | 'sendMessage'>;
|
|
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,13 +11,16 @@ 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
|
-
|
|
17
|
-
|
|
16
|
+
private me;
|
|
17
|
+
constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory, options?: SdkChatControlPlaneOptions);
|
|
18
|
+
connect(onMessage: (message: ChannelMessageEvent) => void | Promise<void>): Promise<void>;
|
|
18
19
|
close(): Promise<void>;
|
|
19
|
-
postMessage(channelId: string, content: string): Promise<
|
|
20
|
+
postMessage(channelId: string, content: string): Promise<PostedMessage>;
|
|
21
|
+
editMessage(messageId: string, content: string): Promise<void>;
|
|
22
|
+
deleteMessage(messageId: string): Promise<void>;
|
|
23
|
+
startTyping(channelId: string): () => void;
|
|
20
24
|
getMe(): Promise<MeResponseUser>;
|
|
21
25
|
}
|
|
22
26
|
export declare function mapInboundToChannelMessage(event: InboundMessageEvent): ChannelMessageEvent;
|
|
@@ -6,53 +6,63 @@ 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
|
-
|
|
13
|
-
|
|
11
|
+
me = null;
|
|
12
|
+
constructor(baseUrl, apiKey, createClient = createBorgeePlugin, options = {}) {
|
|
13
|
+
this.client = createClient({ baseUrl, apiKey, ...options });
|
|
14
14
|
}
|
|
15
15
|
async connect(onMessage) {
|
|
16
|
-
this.unsubscribe = this.client.on('message', (event) => {
|
|
17
|
-
|
|
18
|
-
if (!this.connected) {
|
|
19
|
-
this.pendingMessages.push(message);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
onMessage(message);
|
|
16
|
+
this.unsubscribe = this.client.on('message', async (event) => {
|
|
17
|
+
await onMessage(mapInboundToChannelMessage(event));
|
|
23
18
|
});
|
|
24
19
|
try {
|
|
25
|
-
|
|
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.
|
|
26
23
|
this.connected = true;
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
await this.client.connect();
|
|
25
|
+
if (this.client.agentId) {
|
|
26
|
+
this.me = { id: this.client.agentId };
|
|
29
27
|
}
|
|
30
|
-
this.pendingMessages = [];
|
|
31
28
|
}
|
|
32
29
|
catch (error) {
|
|
30
|
+
this.connected = false;
|
|
33
31
|
this.unsubscribe?.();
|
|
34
32
|
this.unsubscribe = null;
|
|
35
|
-
this.pendingMessages = [];
|
|
36
33
|
throw error;
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
async close() {
|
|
40
37
|
this.connected = false;
|
|
41
|
-
this.pendingMessages = [];
|
|
42
38
|
this.unsubscribe?.();
|
|
43
39
|
this.unsubscribe = null;
|
|
44
40
|
await this.client.close();
|
|
45
41
|
}
|
|
46
42
|
async postMessage(channelId, content) {
|
|
47
|
-
await this.client.sendMessage({ channelId, body: content });
|
|
43
|
+
const sent = await this.client.sendMessage({ channelId, body: content });
|
|
44
|
+
return { messageId: sent.messageId };
|
|
45
|
+
}
|
|
46
|
+
async editMessage(messageId, content) {
|
|
47
|
+
await this.client.editMessage({ messageId, body: content });
|
|
48
|
+
}
|
|
49
|
+
async deleteMessage(messageId) {
|
|
50
|
+
await this.client.deleteMessage({ messageId });
|
|
51
|
+
}
|
|
52
|
+
startTyping(channelId) {
|
|
53
|
+
return this.client.startTyping(channelId);
|
|
48
54
|
}
|
|
49
55
|
async getMe() {
|
|
56
|
+
if (this.me) {
|
|
57
|
+
return this.me;
|
|
58
|
+
}
|
|
50
59
|
const me = await this.client.getMe();
|
|
51
|
-
|
|
60
|
+
this.me = {
|
|
52
61
|
id: me.id,
|
|
53
62
|
display_name: me.displayName,
|
|
54
63
|
role: me.kind,
|
|
55
64
|
};
|
|
65
|
+
return this.me;
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
export function mapInboundToChannelMessage(event) {
|
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,8 +1,9 @@
|
|
|
1
1
|
import type { ProviderAdapter } from '../provider-adapter.js';
|
|
2
|
-
import type { ProviderInput, ProviderReply } from '../../types.js';
|
|
2
|
+
import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../../types.js';
|
|
3
3
|
import { ClaudeCliClient } from './cli-client.js';
|
|
4
4
|
export declare class ClaudeProviderAdapter implements ProviderAdapter {
|
|
5
5
|
private readonly cli;
|
|
6
6
|
constructor(cli: ClaudeCliClient);
|
|
7
|
-
generateReply(input: ProviderInput): Promise<ProviderReply>;
|
|
7
|
+
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
8
|
+
dispose(): Promise<void>;
|
|
8
9
|
}
|
|
@@ -4,7 +4,7 @@ export class ClaudeProviderAdapter {
|
|
|
4
4
|
constructor(cli) {
|
|
5
5
|
this.cli = cli;
|
|
6
6
|
}
|
|
7
|
-
async generateReply(input) {
|
|
7
|
+
async generateReply(input, options) {
|
|
8
8
|
const prompt = buildPrompt({
|
|
9
9
|
agentName: input.agentName,
|
|
10
10
|
provider: input.provider,
|
|
@@ -12,7 +12,12 @@ export class ClaudeProviderAdapter {
|
|
|
12
12
|
incomingAuthorId: input.incomingAuthorId,
|
|
13
13
|
incomingContent: input.incomingContent,
|
|
14
14
|
});
|
|
15
|
-
const text = await this.cli.generateReply(input.channelId, prompt
|
|
15
|
+
const text = await this.cli.generateReply(input.channelId, prompt, {
|
|
16
|
+
onProgress: options?.onProgress,
|
|
17
|
+
});
|
|
16
18
|
return { text };
|
|
17
19
|
}
|
|
20
|
+
async dispose() {
|
|
21
|
+
await this.cli.dispose();
|
|
22
|
+
}
|
|
18
23
|
}
|
|
@@ -1,21 +1,44 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import type { ProviderGenerateOptions } from '../../types.js';
|
|
3
|
+
import type { ClaudeChannelSessionStore } from './session-store.js';
|
|
4
|
+
interface ClaudeCliRuntime {
|
|
5
|
+
spawn: typeof spawn;
|
|
6
|
+
}
|
|
1
7
|
/**
|
|
2
8
|
* CLI client for the Claude Code CLI (`claude`) non-interactive mode, with
|
|
3
9
|
* native per-channel session continuity.
|
|
4
10
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* These are documented as distinct operations, so this client tracks which
|
|
9
|
-
* channels have already started a session and switches from `--session-id`
|
|
10
|
-
* (first turn) to `--resume` (every turn after) accordingly. No message
|
|
11
|
-
* history is kept on our side — the CLI's own session storage is the single
|
|
12
|
-
* source of truth for conversation memory.
|
|
11
|
+
* Claude session memory remains entirely inside the Claude CLI. This client
|
|
12
|
+
* only pins one native session id per Borgee channel and serializes turns per
|
|
13
|
+
* channel so `--resume` is never called concurrently for the same session.
|
|
13
14
|
*/
|
|
14
15
|
export declare class ClaudeCliClient {
|
|
15
16
|
private readonly command;
|
|
16
17
|
private readonly args;
|
|
17
|
-
private readonly
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
private readonly sessionStore?;
|
|
19
|
+
private readonly resolveSessionStoreAgentId;
|
|
20
|
+
private readonly runtime;
|
|
21
|
+
private readonly channels;
|
|
22
|
+
private readonly persistedSessions;
|
|
23
|
+
private loadedSessionStoreAgentId;
|
|
24
|
+
private stopped;
|
|
25
|
+
private sessionStoreLoadPromise;
|
|
26
|
+
private sessionStoreWriteQueue;
|
|
27
|
+
constructor(command: string, args: string[], runtimeOverrides?: Partial<ClaudeCliRuntime>, sessionStore?: ClaudeChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined);
|
|
28
|
+
generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
|
|
29
|
+
dispose(): Promise<void>;
|
|
30
|
+
private getOrCreateChannelState;
|
|
31
|
+
private processChannelQueue;
|
|
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;
|
|
20
42
|
private run;
|
|
21
43
|
}
|
|
44
|
+
export {};
|