@borgee/agents-host 0.1.8 → 0.2.0
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 +1 -0
- package/dist/agents-host.d.ts +7 -0
- package/dist/agents-host.js +181 -3
- package/dist/chat/chat-control-plane.d.ts +5 -2
- package/dist/chat/sdk-chat-control-plane.d.ts +7 -3
- package/dist/chat/sdk-chat-control-plane.js +20 -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 +18 -11
- package/dist/providers/claude/cli-client.js +288 -35
- 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 +2 -1
- package/dist/providers/copilot/cli-client.js +90 -11
- package/dist/providers/provider-adapter.d.ts +2 -2
- package/dist/types.d.ts +9 -0
- package/package.json +2 -2
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:**
|
package/dist/agents-host.d.ts
CHANGED
|
@@ -23,8 +23,12 @@ 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;
|
|
27
30
|
private started;
|
|
31
|
+
private controlPlaneClosed;
|
|
28
32
|
constructor(config: AgentsHostConfig, deps?: {
|
|
29
33
|
borgee?: ChatControlPlane;
|
|
30
34
|
provider?: ProviderAdapter;
|
|
@@ -32,4 +36,7 @@ export declare class AgentsHost {
|
|
|
32
36
|
start(): Promise<void>;
|
|
33
37
|
stop(): Promise<void>;
|
|
34
38
|
private handleMessage;
|
|
39
|
+
private trackActiveTurn;
|
|
40
|
+
private enqueueProgressChannelTurn;
|
|
41
|
+
private ensureProgressDraft;
|
|
35
42
|
}
|
package/dist/agents-host.js
CHANGED
|
@@ -1,5 +1,107 @@
|
|
|
1
1
|
import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
|
|
2
2
|
import { createProvider } from './providers/create-provider.js';
|
|
3
|
+
const STREAM_PROGRESS_EDIT_THROTTLE_MS = 150;
|
|
4
|
+
function hasVisibleText(value) {
|
|
5
|
+
return value.trim().length > 0;
|
|
6
|
+
}
|
|
7
|
+
function providerUsesDraftProgress(provider) {
|
|
8
|
+
return provider === 'claude' || provider === 'copilot';
|
|
9
|
+
}
|
|
10
|
+
class DraftMessageController {
|
|
11
|
+
borgee;
|
|
12
|
+
channelId;
|
|
13
|
+
canWrite;
|
|
14
|
+
messageId = null;
|
|
15
|
+
currentBody = '';
|
|
16
|
+
pendingBody = null;
|
|
17
|
+
timer = null;
|
|
18
|
+
sealed = false;
|
|
19
|
+
writeChain = Promise.resolve();
|
|
20
|
+
constructor(borgee, channelId, canWrite) {
|
|
21
|
+
this.borgee = borgee;
|
|
22
|
+
this.channelId = channelId;
|
|
23
|
+
this.canWrite = canWrite;
|
|
24
|
+
}
|
|
25
|
+
update(text) {
|
|
26
|
+
if (this.sealed || !hasVisibleText(text)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.pendingBody = text;
|
|
30
|
+
if (this.timer) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.timer = setTimeout(() => {
|
|
34
|
+
this.timer = null;
|
|
35
|
+
void this.flushPending().catch((error) => {
|
|
36
|
+
console.error('[agents-host] failed to flush draft progress:', error);
|
|
37
|
+
});
|
|
38
|
+
}, STREAM_PROGRESS_EDIT_THROTTLE_MS);
|
|
39
|
+
}
|
|
40
|
+
cancelProgress() {
|
|
41
|
+
if (this.timer) {
|
|
42
|
+
clearTimeout(this.timer);
|
|
43
|
+
this.timer = null;
|
|
44
|
+
}
|
|
45
|
+
this.sealed = true;
|
|
46
|
+
}
|
|
47
|
+
async finalize(finalText) {
|
|
48
|
+
this.sealed = true;
|
|
49
|
+
if (this.timer) {
|
|
50
|
+
clearTimeout(this.timer);
|
|
51
|
+
this.timer = null;
|
|
52
|
+
}
|
|
53
|
+
if (hasVisibleText(finalText)) {
|
|
54
|
+
this.pendingBody = finalText;
|
|
55
|
+
await this.flushPending();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await this.enqueueWrite(async () => {
|
|
59
|
+
if (!this.canWrite() || !this.messageId) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await this.borgee.deleteMessage(this.messageId);
|
|
63
|
+
this.messageId = null;
|
|
64
|
+
this.currentBody = '';
|
|
65
|
+
this.pendingBody = null;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async discard() {
|
|
69
|
+
this.sealed = true;
|
|
70
|
+
if (this.timer) {
|
|
71
|
+
clearTimeout(this.timer);
|
|
72
|
+
this.timer = null;
|
|
73
|
+
}
|
|
74
|
+
this.pendingBody = null;
|
|
75
|
+
await this.enqueueWrite(async () => {
|
|
76
|
+
if (!this.canWrite() || !this.messageId) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await this.borgee.deleteMessage(this.messageId);
|
|
80
|
+
this.messageId = null;
|
|
81
|
+
this.currentBody = '';
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async flushPending() {
|
|
85
|
+
await this.enqueueWrite(async () => {
|
|
86
|
+
const body = this.pendingBody;
|
|
87
|
+
if (!this.canWrite() || !body || body === this.currentBody) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (this.messageId) {
|
|
91
|
+
await this.borgee.editMessage(this.messageId, body);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const posted = await this.borgee.postMessage(this.channelId, body);
|
|
95
|
+
this.messageId = posted.messageId;
|
|
96
|
+
}
|
|
97
|
+
this.currentBody = body;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
enqueueWrite(operation) {
|
|
101
|
+
this.writeChain = this.writeChain.then(operation, operation);
|
|
102
|
+
return this.writeChain;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
3
105
|
/**
|
|
4
106
|
* Minimal single-agent agents host: connects one local Claude/Copilot CLI
|
|
5
107
|
* to exactly one Borgee agent over `@borgee/plugin-sdk` (BPP / `/ws/plugin`).
|
|
@@ -22,8 +124,12 @@ export class AgentsHost {
|
|
|
22
124
|
config;
|
|
23
125
|
provider;
|
|
24
126
|
borgee;
|
|
127
|
+
activeTurns = new Set();
|
|
128
|
+
progressChannelQueues = new Map();
|
|
129
|
+
progressDrafts = new Map();
|
|
25
130
|
selfAgentId = null;
|
|
26
131
|
started = false;
|
|
132
|
+
controlPlaneClosed = false;
|
|
27
133
|
constructor(config, deps) {
|
|
28
134
|
this.config = config;
|
|
29
135
|
this.provider = deps?.provider ?? createProvider({
|
|
@@ -44,8 +150,15 @@ export class AgentsHost {
|
|
|
44
150
|
if (this.started)
|
|
45
151
|
return;
|
|
46
152
|
this.started = true;
|
|
153
|
+
this.controlPlaneClosed = false;
|
|
47
154
|
await this.borgee.connect((message) => {
|
|
48
|
-
|
|
155
|
+
if (!this.started) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const task = providerUsesDraftProgress(this.config.agent.provider)
|
|
159
|
+
? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleMessage(message))
|
|
160
|
+
: this.handleMessage(message);
|
|
161
|
+
this.trackActiveTurn(task);
|
|
49
162
|
});
|
|
50
163
|
const me = await this.borgee.getMe();
|
|
51
164
|
this.selfAgentId = me.id;
|
|
@@ -59,8 +172,13 @@ export class AgentsHost {
|
|
|
59
172
|
if (!this.started)
|
|
60
173
|
return;
|
|
61
174
|
this.started = false;
|
|
175
|
+
for (const draft of this.progressDrafts.values()) {
|
|
176
|
+
draft.cancelProgress();
|
|
177
|
+
}
|
|
178
|
+
this.controlPlaneClosed = true;
|
|
62
179
|
await this.borgee.close();
|
|
63
180
|
await this.provider.dispose?.();
|
|
181
|
+
await Promise.allSettled([...this.activeTurns]);
|
|
64
182
|
}
|
|
65
183
|
async handleMessage(msg) {
|
|
66
184
|
console.log('[agents-host] received message', {
|
|
@@ -80,6 +198,8 @@ export class AgentsHost {
|
|
|
80
198
|
// messages an agent receives (per-agent + per-channel require_mention is
|
|
81
199
|
// enforced at BPP fan-out via AgentReceivesChannelMessage). If a channel
|
|
82
200
|
// message reaches this host at all, the agent is meant to handle it.
|
|
201
|
+
const stopTyping = this.borgee.startTyping(msg.channel_id);
|
|
202
|
+
const useDraftProgress = providerUsesDraftProgress(this.config.agent.provider);
|
|
83
203
|
try {
|
|
84
204
|
const reply = await this.provider.generateReply({
|
|
85
205
|
agentName: this.config.agent.agentName,
|
|
@@ -87,15 +207,73 @@ export class AgentsHost {
|
|
|
87
207
|
channelId: msg.channel_id,
|
|
88
208
|
incomingAuthorId: authorId,
|
|
89
209
|
incomingContent: content,
|
|
90
|
-
}
|
|
91
|
-
|
|
210
|
+
}, useDraftProgress
|
|
211
|
+
? {
|
|
212
|
+
onProgress: (update) => {
|
|
213
|
+
if (!this.started || this.controlPlaneClosed) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.ensureProgressDraft(msg.channel_id).update(update.text);
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
: undefined);
|
|
220
|
+
if (useDraftProgress) {
|
|
221
|
+
const draft = this.progressDrafts.get(msg.channel_id);
|
|
222
|
+
if (!this.started) {
|
|
223
|
+
await draft?.discard().catch(() => { });
|
|
224
|
+
this.progressDrafts.delete(msg.channel_id);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await (draft ?? this.ensureProgressDraft(msg.channel_id)).finalize(reply.text);
|
|
228
|
+
this.progressDrafts.delete(msg.channel_id);
|
|
229
|
+
}
|
|
230
|
+
else if (this.started && !this.controlPlaneClosed) {
|
|
231
|
+
await this.borgee.postMessage(msg.channel_id, reply.text);
|
|
232
|
+
}
|
|
92
233
|
}
|
|
93
234
|
catch (error) {
|
|
235
|
+
if (useDraftProgress) {
|
|
236
|
+
const draft = this.progressDrafts.get(msg.channel_id);
|
|
237
|
+
if (draft) {
|
|
238
|
+
await draft.discard().catch(() => { });
|
|
239
|
+
this.progressDrafts.delete(msg.channel_id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
94
242
|
console.error('[agents-host] failed to generate or send reply:', {
|
|
95
243
|
provider: this.config.agent.provider,
|
|
96
244
|
agentName: this.config.agent.agentName,
|
|
97
245
|
error,
|
|
98
246
|
});
|
|
99
247
|
}
|
|
248
|
+
finally {
|
|
249
|
+
stopTyping();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
trackActiveTurn(task) {
|
|
253
|
+
this.activeTurns.add(task);
|
|
254
|
+
void task.finally(() => {
|
|
255
|
+
this.activeTurns.delete(task);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
enqueueProgressChannelTurn(channelId, task) {
|
|
259
|
+
const previous = this.progressChannelQueues.get(channelId) ?? Promise.resolve();
|
|
260
|
+
const next = previous
|
|
261
|
+
.catch(() => { })
|
|
262
|
+
.then(task)
|
|
263
|
+
.finally(() => {
|
|
264
|
+
if (this.progressChannelQueues.get(channelId) === next) {
|
|
265
|
+
this.progressChannelQueues.delete(channelId);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
this.progressChannelQueues.set(channelId, next);
|
|
269
|
+
return next;
|
|
270
|
+
}
|
|
271
|
+
ensureProgressDraft(channelId) {
|
|
272
|
+
let draft = this.progressDrafts.get(channelId);
|
|
273
|
+
if (!draft) {
|
|
274
|
+
draft = new DraftMessageController(this.borgee, channelId, () => !this.controlPlaneClosed);
|
|
275
|
+
this.progressDrafts.set(channelId, draft);
|
|
276
|
+
}
|
|
277
|
+
return draft;
|
|
100
278
|
}
|
|
101
279
|
}
|
|
@@ -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
3
|
connect(onMessage: (message: ChannelMessageEvent) => 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,7 +1,7 @@
|
|
|
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
6
|
/**
|
|
7
7
|
* Thin adapter over `@borgee/plugin-sdk` (the same BPP/`/ws/plugin` SDK used by
|
|
@@ -13,10 +13,14 @@ export declare class SdkChatControlPlane implements ChatControlPlane {
|
|
|
13
13
|
private pendingMessages;
|
|
14
14
|
private unsubscribe;
|
|
15
15
|
private connected;
|
|
16
|
+
private me;
|
|
16
17
|
constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory, pluginId?: string);
|
|
17
18
|
connect(onMessage: (message: ChannelMessageEvent) => 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;
|
|
@@ -9,6 +9,7 @@ export class SdkChatControlPlane {
|
|
|
9
9
|
pendingMessages = [];
|
|
10
10
|
unsubscribe = null;
|
|
11
11
|
connected = false;
|
|
12
|
+
me = null;
|
|
12
13
|
constructor(baseUrl, apiKey, createClient = createBorgeePlugin, pluginId) {
|
|
13
14
|
this.client = createClient({ baseUrl, apiKey, pluginId });
|
|
14
15
|
}
|
|
@@ -24,6 +25,9 @@ export class SdkChatControlPlane {
|
|
|
24
25
|
try {
|
|
25
26
|
await this.client.connect();
|
|
26
27
|
this.connected = true;
|
|
28
|
+
if (this.client.agentId) {
|
|
29
|
+
this.me = { id: this.client.agentId };
|
|
30
|
+
}
|
|
27
31
|
for (const message of this.pendingMessages) {
|
|
28
32
|
onMessage(message);
|
|
29
33
|
}
|
|
@@ -44,15 +48,29 @@ export class SdkChatControlPlane {
|
|
|
44
48
|
await this.client.close();
|
|
45
49
|
}
|
|
46
50
|
async postMessage(channelId, content) {
|
|
47
|
-
await this.client.sendMessage({ channelId, body: content });
|
|
51
|
+
const sent = await this.client.sendMessage({ channelId, body: content });
|
|
52
|
+
return { messageId: sent.messageId };
|
|
53
|
+
}
|
|
54
|
+
async editMessage(messageId, content) {
|
|
55
|
+
await this.client.editMessage({ messageId, body: content });
|
|
56
|
+
}
|
|
57
|
+
async deleteMessage(messageId) {
|
|
58
|
+
await this.client.deleteMessage({ messageId });
|
|
59
|
+
}
|
|
60
|
+
startTyping(channelId) {
|
|
61
|
+
return this.client.startTyping(channelId);
|
|
48
62
|
}
|
|
49
63
|
async getMe() {
|
|
64
|
+
if (this.me) {
|
|
65
|
+
return this.me;
|
|
66
|
+
}
|
|
50
67
|
const me = await this.client.getMe();
|
|
51
|
-
|
|
68
|
+
this.me = {
|
|
52
69
|
id: me.id,
|
|
53
70
|
display_name: me.displayName,
|
|
54
71
|
role: me.kind,
|
|
55
72
|
};
|
|
73
|
+
return this.me;
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
76
|
export function mapInboundToChannelMessage(event) {
|
|
@@ -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,28 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import type { ProviderGenerateOptions } from '../../types.js';
|
|
3
|
+
interface ClaudeCliRuntime {
|
|
4
|
+
spawn: typeof spawn;
|
|
5
|
+
}
|
|
1
6
|
/**
|
|
2
7
|
* CLI client for the Claude Code CLI (`claude`) non-interactive mode, with
|
|
3
8
|
* native per-channel session continuity.
|
|
4
9
|
*
|
|
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.
|
|
10
|
+
* Claude session memory remains entirely inside the Claude CLI. This client
|
|
11
|
+
* only pins one native session id per Borgee channel and serializes turns per
|
|
12
|
+
* channel so `--resume` is never called concurrently for the same session.
|
|
13
13
|
*/
|
|
14
14
|
export declare class ClaudeCliClient {
|
|
15
15
|
private readonly command;
|
|
16
16
|
private readonly args;
|
|
17
|
-
private readonly
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
private readonly runtime;
|
|
18
|
+
private readonly channels;
|
|
19
|
+
private stopped;
|
|
20
|
+
constructor(command: string, args: string[], runtimeOverrides?: Partial<ClaudeCliRuntime>);
|
|
21
|
+
generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
|
|
22
|
+
dispose(): Promise<void>;
|
|
23
|
+
private getOrCreateChannelState;
|
|
24
|
+
private processChannelQueue;
|
|
25
|
+
private runTurn;
|
|
20
26
|
private run;
|
|
21
27
|
}
|
|
28
|
+
export {};
|
|
@@ -1,68 +1,321 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import spawn from 'cross-spawn';
|
|
3
|
+
const DEFAULT_RUNTIME = {
|
|
4
|
+
spawn,
|
|
5
|
+
};
|
|
6
|
+
const STREAM_JSON_ARGS = ['--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
7
|
+
function normalizeError(error) {
|
|
8
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
9
|
+
}
|
|
10
|
+
function createDeferredTurn(prompt, options) {
|
|
11
|
+
let settled = false;
|
|
12
|
+
let resolvePromise;
|
|
13
|
+
let rejectPromise;
|
|
14
|
+
const promise = new Promise((resolve, reject) => {
|
|
15
|
+
resolvePromise = resolve;
|
|
16
|
+
rejectPromise = reject;
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
prompt,
|
|
20
|
+
options,
|
|
21
|
+
promise,
|
|
22
|
+
resolve(value) {
|
|
23
|
+
if (settled)
|
|
24
|
+
return;
|
|
25
|
+
settled = true;
|
|
26
|
+
resolvePromise(value);
|
|
27
|
+
},
|
|
28
|
+
reject(error) {
|
|
29
|
+
if (settled)
|
|
30
|
+
return;
|
|
31
|
+
settled = true;
|
|
32
|
+
rejectPromise(normalizeError(error));
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function asObject(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null ? value : null;
|
|
38
|
+
}
|
|
39
|
+
function asString(value) {
|
|
40
|
+
return typeof value === 'string' ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
function hasVisibleText(value) {
|
|
43
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
44
|
+
}
|
|
45
|
+
function readTextBlocks(blocks) {
|
|
46
|
+
if (!Array.isArray(blocks)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const segments = [];
|
|
50
|
+
for (const entry of blocks) {
|
|
51
|
+
const block = asObject(entry);
|
|
52
|
+
if (!block)
|
|
53
|
+
continue;
|
|
54
|
+
if (block.type === 'text' && hasVisibleText(asString(block.text))) {
|
|
55
|
+
segments.push(asString(block.text));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return segments;
|
|
59
|
+
}
|
|
60
|
+
function extractTextDelta(event) {
|
|
61
|
+
if (event.type === 'content_block_delta') {
|
|
62
|
+
const delta = asObject(event.delta);
|
|
63
|
+
if (!delta)
|
|
64
|
+
return null;
|
|
65
|
+
const deltaType = asString(delta.type);
|
|
66
|
+
if (deltaType !== undefined && deltaType !== 'text_delta') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return asString(delta.text) ?? null;
|
|
70
|
+
}
|
|
71
|
+
if (event.type === 'content_block_start') {
|
|
72
|
+
const block = asObject(event.content_block) ?? asObject(event.block);
|
|
73
|
+
if (!block || block.type !== 'text') {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return asString(block.text) ?? null;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function extractToolSummary(event) {
|
|
81
|
+
if (event.type === 'tool_use') {
|
|
82
|
+
const toolName = asString(event.name) ?? asString(event.tool_name);
|
|
83
|
+
return hasVisibleText(toolName) ? `Running ${toolName}…` : 'Running tool…';
|
|
84
|
+
}
|
|
85
|
+
if (event.type === 'content_block_start') {
|
|
86
|
+
const block = asObject(event.content_block) ?? asObject(event.block);
|
|
87
|
+
if (!block || block.type !== 'tool_use') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const toolName = asString(block.name);
|
|
91
|
+
return hasVisibleText(toolName) ? `Running ${toolName}…` : 'Running tool…';
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function extractFinalText(event) {
|
|
96
|
+
if (event.type === 'result') {
|
|
97
|
+
const result = asString(event.result);
|
|
98
|
+
if (result !== undefined) {
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (event.type === 'message' || event.type === 'assistant') {
|
|
103
|
+
const message = asObject(event.message) ?? event;
|
|
104
|
+
const joined = readTextBlocks(message.content).join('');
|
|
105
|
+
if (joined.length > 0) {
|
|
106
|
+
return joined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
class ClaudeStreamCollector {
|
|
112
|
+
onProgress;
|
|
113
|
+
publicText = '';
|
|
114
|
+
finalText = null;
|
|
115
|
+
lastPublished = null;
|
|
116
|
+
constructor(onProgress) {
|
|
117
|
+
this.onProgress = onProgress;
|
|
118
|
+
}
|
|
119
|
+
consume(event) {
|
|
120
|
+
const payload = event.type === 'stream_event' ? asObject(event.event) ?? event : event;
|
|
121
|
+
const textDelta = extractTextDelta(payload);
|
|
122
|
+
if (textDelta !== null) {
|
|
123
|
+
this.publicText += textDelta;
|
|
124
|
+
this.publish(this.publicText);
|
|
125
|
+
}
|
|
126
|
+
if (!hasVisibleText(this.publicText)) {
|
|
127
|
+
const toolSummary = extractToolSummary(payload);
|
|
128
|
+
if (toolSummary) {
|
|
129
|
+
this.publish(toolSummary);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const finalText = extractFinalText(event) ?? extractFinalText(payload);
|
|
133
|
+
if (finalText !== null) {
|
|
134
|
+
this.finalText = finalText;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
getFinalText() {
|
|
138
|
+
const text = this.finalText ?? this.publicText;
|
|
139
|
+
return hasVisibleText(text) ? text : '';
|
|
140
|
+
}
|
|
141
|
+
publish(text) {
|
|
142
|
+
if (!this.onProgress || !hasVisibleText(text) || text === this.lastPublished) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.lastPublished = text;
|
|
146
|
+
this.onProgress({ text });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
3
149
|
/**
|
|
4
150
|
* CLI client for the Claude Code CLI (`claude`) non-interactive mode, with
|
|
5
151
|
* native per-channel session continuity.
|
|
6
152
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* These are documented as distinct operations, so this client tracks which
|
|
11
|
-
* channels have already started a session and switches from `--session-id`
|
|
12
|
-
* (first turn) to `--resume` (every turn after) accordingly. No message
|
|
13
|
-
* history is kept on our side — the CLI's own session storage is the single
|
|
14
|
-
* source of truth for conversation memory.
|
|
153
|
+
* Claude session memory remains entirely inside the Claude CLI. This client
|
|
154
|
+
* only pins one native session id per Borgee channel and serializes turns per
|
|
155
|
+
* channel so `--resume` is never called concurrently for the same session.
|
|
15
156
|
*/
|
|
16
157
|
export class ClaudeCliClient {
|
|
17
158
|
command;
|
|
18
159
|
args;
|
|
19
|
-
|
|
20
|
-
|
|
160
|
+
runtime;
|
|
161
|
+
channels = new Map();
|
|
162
|
+
stopped = false;
|
|
163
|
+
constructor(command, args, runtimeOverrides = {}) {
|
|
21
164
|
this.command = command;
|
|
22
165
|
this.args = args;
|
|
166
|
+
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
167
|
+
}
|
|
168
|
+
async generateReply(channelId, prompt, options) {
|
|
169
|
+
if (this.stopped) {
|
|
170
|
+
throw new Error('Claude CLI backend stopped');
|
|
171
|
+
}
|
|
172
|
+
const state = this.getOrCreateChannelState(channelId);
|
|
173
|
+
const turn = createDeferredTurn(prompt, options);
|
|
174
|
+
state.queue.push(turn);
|
|
175
|
+
this.processChannelQueue(channelId, state);
|
|
176
|
+
return turn.promise;
|
|
23
177
|
}
|
|
24
|
-
async
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
178
|
+
async dispose() {
|
|
179
|
+
if (this.stopped)
|
|
180
|
+
return;
|
|
181
|
+
this.stopped = true;
|
|
182
|
+
const stoppedError = new Error('Claude CLI backend stopped');
|
|
183
|
+
for (const state of this.channels.values()) {
|
|
184
|
+
state.activeTurn?.reject(stoppedError);
|
|
185
|
+
for (const queuedTurn of state.queue) {
|
|
186
|
+
queuedTurn.reject(stoppedError);
|
|
187
|
+
}
|
|
188
|
+
state.queue = [];
|
|
189
|
+
state.activeChild?.kill('SIGTERM');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
getOrCreateChannelState(channelId) {
|
|
193
|
+
let state = this.channels.get(channelId);
|
|
194
|
+
if (!state) {
|
|
195
|
+
state = {
|
|
196
|
+
processing: false,
|
|
197
|
+
queue: [],
|
|
198
|
+
sessionEstablished: false,
|
|
199
|
+
};
|
|
200
|
+
this.channels.set(channelId, state);
|
|
201
|
+
}
|
|
202
|
+
return state;
|
|
203
|
+
}
|
|
204
|
+
processChannelQueue(channelId, state) {
|
|
205
|
+
if (state.processing) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
state.processing = true;
|
|
209
|
+
void (async () => {
|
|
210
|
+
try {
|
|
211
|
+
while (!this.stopped) {
|
|
212
|
+
const turn = state.queue.shift();
|
|
213
|
+
if (!turn) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
state.activeTurn = turn;
|
|
217
|
+
try {
|
|
218
|
+
const text = await this.runTurn(state, turn);
|
|
219
|
+
turn.resolve(text);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
turn.reject(error);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
state.activeTurn = undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
state.processing = false;
|
|
231
|
+
if (state.queue.length > 0 && !this.stopped) {
|
|
232
|
+
this.processChannelQueue(channelId, state);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
}
|
|
237
|
+
async runTurn(state, turn) {
|
|
238
|
+
if (this.stopped) {
|
|
239
|
+
throw new Error('Claude CLI backend stopped');
|
|
240
|
+
}
|
|
241
|
+
const sessionId = state.sessionEstablished ? state.sessionId : randomUUID();
|
|
242
|
+
const sessionArgs = state.sessionEstablished ? ['--resume', sessionId] : ['--session-id', sessionId];
|
|
243
|
+
const text = await this.run(state, [...this.args, ...sessionArgs, ...STREAM_JSON_ARGS], turn.prompt, turn.options);
|
|
244
|
+
state.sessionId = sessionId;
|
|
245
|
+
state.sessionEstablished = true;
|
|
35
246
|
return text;
|
|
36
247
|
}
|
|
37
|
-
async run(args, prompt) {
|
|
248
|
+
async run(state, args, prompt, options) {
|
|
38
249
|
return new Promise((resolve, reject) => {
|
|
39
|
-
const child = spawn(this.command, args, {
|
|
250
|
+
const child = this.runtime.spawn(this.command, args, {
|
|
40
251
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
41
252
|
});
|
|
42
|
-
|
|
253
|
+
state.activeChild = child;
|
|
254
|
+
const collector = new ClaudeStreamCollector(options?.onProgress);
|
|
43
255
|
let stderr = '';
|
|
256
|
+
let lineBuffer = '';
|
|
257
|
+
let settled = false;
|
|
258
|
+
const settleReject = (error) => {
|
|
259
|
+
if (settled)
|
|
260
|
+
return;
|
|
261
|
+
settled = true;
|
|
262
|
+
reject(normalizeError(error));
|
|
263
|
+
};
|
|
264
|
+
const settleResolve = (value) => {
|
|
265
|
+
if (settled)
|
|
266
|
+
return;
|
|
267
|
+
settled = true;
|
|
268
|
+
resolve(value);
|
|
269
|
+
};
|
|
44
270
|
child.stdout.setEncoding('utf8');
|
|
45
271
|
child.stderr.setEncoding('utf8');
|
|
46
272
|
child.stdout.on('data', (chunk) => {
|
|
47
|
-
|
|
273
|
+
lineBuffer += chunk;
|
|
274
|
+
while (true) {
|
|
275
|
+
const newlineIndex = lineBuffer.indexOf('\n');
|
|
276
|
+
if (newlineIndex === -1) {
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
const line = lineBuffer.slice(0, newlineIndex).trim();
|
|
280
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
281
|
+
if (!line)
|
|
282
|
+
continue;
|
|
283
|
+
try {
|
|
284
|
+
const event = JSON.parse(line);
|
|
285
|
+
collector.consume(event);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
settleReject(new Error(`Claude stream-json parse failed: ${normalizeError(error).message}`));
|
|
289
|
+
child.kill('SIGTERM');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
48
293
|
});
|
|
49
294
|
child.stderr.on('data', (chunk) => {
|
|
50
295
|
stderr += chunk;
|
|
51
296
|
});
|
|
52
|
-
child.
|
|
53
|
-
|
|
297
|
+
child.once('error', (error) => {
|
|
298
|
+
settleReject(error);
|
|
54
299
|
});
|
|
55
|
-
child.
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
return;
|
|
300
|
+
child.once('close', (code) => {
|
|
301
|
+
if (state.activeChild === child) {
|
|
302
|
+
state.activeChild = undefined;
|
|
59
303
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
304
|
+
if (lineBuffer.trim().length > 0 && !settled) {
|
|
305
|
+
try {
|
|
306
|
+
collector.consume(JSON.parse(lineBuffer.trim()));
|
|
307
|
+
lineBuffer = '';
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
settleReject(new Error(`Claude stream-json parse failed: ${normalizeError(error).message}`));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (code !== 0) {
|
|
315
|
+
settleReject(new Error(`Claude CLI failed with code ${code}: ${stderr.trim()}`));
|
|
63
316
|
return;
|
|
64
317
|
}
|
|
65
|
-
|
|
318
|
+
settleResolve(collector.getFinalText());
|
|
66
319
|
});
|
|
67
320
|
child.stdin.write(prompt);
|
|
68
321
|
child.stdin.end();
|
|
@@ -1,9 +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 { CopilotCliClient } from './cli-client.js';
|
|
4
4
|
export declare class CopilotProviderAdapter implements ProviderAdapter {
|
|
5
5
|
private readonly cli;
|
|
6
6
|
constructor(cli: CopilotCliClient);
|
|
7
|
-
generateReply(input: ProviderInput): Promise<ProviderReply>;
|
|
7
|
+
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
8
8
|
dispose(): Promise<void>;
|
|
9
9
|
}
|
|
@@ -4,7 +4,7 @@ export class CopilotProviderAdapter {
|
|
|
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,9 @@ export class CopilotProviderAdapter {
|
|
|
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
|
}
|
|
18
20
|
async dispose() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream } from '@agentclientprotocol/sdk';
|
|
3
|
+
import type { ProviderGenerateOptions } from '../../types.js';
|
|
3
4
|
interface CopilotAcpRuntime {
|
|
4
5
|
spawn: typeof spawn;
|
|
5
6
|
client: typeof client;
|
|
@@ -38,7 +39,7 @@ export declare class CopilotCliClient {
|
|
|
38
39
|
private disposing;
|
|
39
40
|
private backendClosed;
|
|
40
41
|
constructor(command: string, _ignoredArgs?: string[], runtimeOverrides?: Partial<CopilotAcpRuntime>);
|
|
41
|
-
generateReply(channelId: string, prompt: string): Promise<string>;
|
|
42
|
+
generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
|
|
42
43
|
dispose(): Promise<void>;
|
|
43
44
|
private ensureStarted;
|
|
44
45
|
private startBackend;
|
|
@@ -17,7 +17,88 @@ const DEFAULT_RUNTIME = {
|
|
|
17
17
|
shutdownGracePeriodMs: DEFAULT_SHUTDOWN_GRACE_PERIOD_MS,
|
|
18
18
|
shutdownForceKillWaitMs: DEFAULT_SHUTDOWN_FORCE_KILL_WAIT_MS,
|
|
19
19
|
};
|
|
20
|
-
function
|
|
20
|
+
function hasVisibleText(value) {
|
|
21
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
22
|
+
}
|
|
23
|
+
function formatToolProgress(title, status) {
|
|
24
|
+
const normalizedTitle = hasVisibleText(title) ? title.trim() : null;
|
|
25
|
+
switch (status) {
|
|
26
|
+
case 'completed':
|
|
27
|
+
return normalizedTitle ? `Completed ${normalizedTitle}` : 'Completed tool call';
|
|
28
|
+
case 'failed':
|
|
29
|
+
return normalizedTitle ? `Failed ${normalizedTitle}` : 'Tool call failed';
|
|
30
|
+
case 'pending':
|
|
31
|
+
case 'in_progress':
|
|
32
|
+
case undefined:
|
|
33
|
+
case null:
|
|
34
|
+
return normalizedTitle ? `Running ${normalizedTitle}…` : 'Running tool…';
|
|
35
|
+
default:
|
|
36
|
+
return normalizedTitle ? `${status} ${normalizedTitle}` : status;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function formatPlanProgress(entries) {
|
|
40
|
+
const current = entries.find((entry) => entry.status === 'in_progress') ??
|
|
41
|
+
entries.find((entry) => entry.status === 'pending') ??
|
|
42
|
+
entries[0];
|
|
43
|
+
return hasVisibleText(current?.content) ? `Plan: ${current.content.trim()}` : null;
|
|
44
|
+
}
|
|
45
|
+
class CopilotProgressCollector {
|
|
46
|
+
onProgress;
|
|
47
|
+
publicText = '';
|
|
48
|
+
lastPublished = null;
|
|
49
|
+
toolTitles = new Map();
|
|
50
|
+
constructor(onProgress) {
|
|
51
|
+
this.onProgress = onProgress;
|
|
52
|
+
}
|
|
53
|
+
consume(update) {
|
|
54
|
+
switch (update.update.sessionUpdate) {
|
|
55
|
+
case 'agent_message_chunk':
|
|
56
|
+
if (update.update.content.type !== 'text') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.publicText += update.update.content.text;
|
|
60
|
+
this.publish(this.publicText);
|
|
61
|
+
return;
|
|
62
|
+
case 'tool_call':
|
|
63
|
+
this.toolTitles.set(update.update.toolCallId, update.update.title);
|
|
64
|
+
this.publishFallback(formatToolProgress(update.update.title, update.update.status));
|
|
65
|
+
return;
|
|
66
|
+
case 'tool_call_update': {
|
|
67
|
+
const nextTitle = update.update.title ?? this.toolTitles.get(update.update.toolCallId);
|
|
68
|
+
if (hasVisibleText(nextTitle)) {
|
|
69
|
+
this.toolTitles.set(update.update.toolCallId, nextTitle);
|
|
70
|
+
}
|
|
71
|
+
this.publishFallback(formatToolProgress(nextTitle, update.update.status));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
case 'plan':
|
|
75
|
+
this.publishFallback(formatPlanProgress(update.update.entries.map((entry) => ({
|
|
76
|
+
content: entry.content,
|
|
77
|
+
status: entry.status,
|
|
78
|
+
}))));
|
|
79
|
+
return;
|
|
80
|
+
default:
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
getFinalText() {
|
|
85
|
+
return this.publicText.trim();
|
|
86
|
+
}
|
|
87
|
+
publishFallback(text) {
|
|
88
|
+
if (hasVisibleText(this.publicText)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.publish(text);
|
|
92
|
+
}
|
|
93
|
+
publish(text) {
|
|
94
|
+
if (!this.onProgress || !hasVisibleText(text) || text === this.lastPublished) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.lastPublished = text;
|
|
98
|
+
this.onProgress({ text });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function createDeferredTurn(prompt, options) {
|
|
21
102
|
let settled = false;
|
|
22
103
|
let resolvePromise;
|
|
23
104
|
let rejectPromise;
|
|
@@ -27,6 +108,7 @@ function createDeferredTurn(prompt) {
|
|
|
27
108
|
});
|
|
28
109
|
return {
|
|
29
110
|
prompt,
|
|
111
|
+
options,
|
|
30
112
|
promise,
|
|
31
113
|
resolve(value) {
|
|
32
114
|
if (settled)
|
|
@@ -95,13 +177,13 @@ export class CopilotCliClient {
|
|
|
95
177
|
});
|
|
96
178
|
void this.fatalPromise.catch(() => { });
|
|
97
179
|
}
|
|
98
|
-
async generateReply(channelId, prompt) {
|
|
180
|
+
async generateReply(channelId, prompt, options) {
|
|
99
181
|
if (this.fatalError) {
|
|
100
182
|
throw this.fatalError;
|
|
101
183
|
}
|
|
102
184
|
const state = this.getOrCreateChannelState(channelId);
|
|
103
185
|
this.clearIdleTimer(state);
|
|
104
|
-
const turn = createDeferredTurn(prompt);
|
|
186
|
+
const turn = createDeferredTurn(prompt, options);
|
|
105
187
|
state.queue.push(turn);
|
|
106
188
|
this.processChannelQueue(channelId, state);
|
|
107
189
|
return turn.promise;
|
|
@@ -211,7 +293,7 @@ export class CopilotCliClient {
|
|
|
211
293
|
const session = await this.getOrCreateSession(channelId, state);
|
|
212
294
|
let reply;
|
|
213
295
|
try {
|
|
214
|
-
reply = await this.runTurn(session, turn.prompt);
|
|
296
|
+
reply = await this.runTurn(session, turn.prompt, turn.options);
|
|
215
297
|
}
|
|
216
298
|
catch (error) {
|
|
217
299
|
if (isSessionTainted(error)) {
|
|
@@ -281,12 +363,12 @@ export class CopilotCliClient {
|
|
|
281
363
|
}
|
|
282
364
|
}
|
|
283
365
|
}
|
|
284
|
-
async runTurn(session, prompt) {
|
|
366
|
+
async runTurn(session, prompt, options) {
|
|
285
367
|
const promptPromise = this.raceWithFatal(session.prompt(prompt));
|
|
286
368
|
const promptFailure = new Promise((_, reject) => {
|
|
287
369
|
void promptPromise.catch((error) => reject(markSessionTainted(error)));
|
|
288
370
|
});
|
|
289
|
-
|
|
371
|
+
const collector = new CopilotProgressCollector(options?.onProgress);
|
|
290
372
|
for (;;) {
|
|
291
373
|
let update;
|
|
292
374
|
try {
|
|
@@ -309,16 +391,13 @@ export class CopilotCliClient {
|
|
|
309
391
|
if (response.stopReason !== 'end_turn') {
|
|
310
392
|
throw new Error(`Copilot ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
311
393
|
}
|
|
312
|
-
const output =
|
|
394
|
+
const output = collector.getFinalText();
|
|
313
395
|
if (!output) {
|
|
314
396
|
throw new Error('Copilot ACP returned empty output');
|
|
315
397
|
}
|
|
316
398
|
return output;
|
|
317
399
|
}
|
|
318
|
-
|
|
319
|
-
update.update.content.type === 'text') {
|
|
320
|
-
text += update.update.content.text;
|
|
321
|
-
}
|
|
400
|
+
collector.consume(update);
|
|
322
401
|
}
|
|
323
402
|
}
|
|
324
403
|
async raceWithFatal(promise) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ProviderInput, ProviderReply } from '../types.js';
|
|
1
|
+
import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../types.js';
|
|
2
2
|
export interface ProviderAdapter {
|
|
3
|
-
generateReply(input: ProviderInput): Promise<ProviderReply>;
|
|
3
|
+
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
4
4
|
dispose?(): Promise<void>;
|
|
5
5
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -106,3 +106,12 @@ export interface ProviderInput {
|
|
|
106
106
|
export interface ProviderReply {
|
|
107
107
|
text: string;
|
|
108
108
|
}
|
|
109
|
+
export interface PostedMessage {
|
|
110
|
+
messageId: string;
|
|
111
|
+
}
|
|
112
|
+
export interface ProviderProgressUpdate {
|
|
113
|
+
text: string;
|
|
114
|
+
}
|
|
115
|
+
export interface ProviderGenerateOptions {
|
|
116
|
+
onProgress?: (update: ProviderProgressUpdate) => void;
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borgee/agents-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@agentclientprotocol/sdk": "^1.2.1",
|
|
25
25
|
"cross-spawn": "^7.0.6",
|
|
26
26
|
"yaml": "^2.8.1",
|
|
27
|
-
"@borgee/plugin-sdk": "0.1.
|
|
27
|
+
"@borgee/plugin-sdk": "0.1.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/cross-spawn": "^6.0.6",
|