@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.
@@ -1,68 +1,469 @@
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 isStaleResumeFailure(error) {
8
+ const message = normalizeError(error).message.toLowerCase();
9
+ return message.includes('session not found') || message.includes('cannot resume');
10
+ }
11
+ function normalizeError(error) {
12
+ return error instanceof Error ? error : new Error(String(error));
13
+ }
14
+ function createDeferredTurn(prompt, options) {
15
+ let settled = false;
16
+ let resolvePromise;
17
+ let rejectPromise;
18
+ const promise = new Promise((resolve, reject) => {
19
+ resolvePromise = resolve;
20
+ rejectPromise = reject;
21
+ });
22
+ return {
23
+ prompt,
24
+ options,
25
+ promise,
26
+ resolve(value) {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ resolvePromise(value);
31
+ },
32
+ reject(error) {
33
+ if (settled)
34
+ return;
35
+ settled = true;
36
+ rejectPromise(normalizeError(error));
37
+ },
38
+ };
39
+ }
40
+ function asObject(value) {
41
+ return typeof value === 'object' && value !== null ? value : null;
42
+ }
43
+ function asString(value) {
44
+ return typeof value === 'string' ? value : undefined;
45
+ }
46
+ function hasVisibleText(value) {
47
+ return typeof value === 'string' && value.trim().length > 0;
48
+ }
49
+ function readTextBlocks(blocks) {
50
+ if (!Array.isArray(blocks)) {
51
+ return [];
52
+ }
53
+ const segments = [];
54
+ for (const entry of blocks) {
55
+ const block = asObject(entry);
56
+ if (!block)
57
+ continue;
58
+ if (block.type === 'text' && hasVisibleText(asString(block.text))) {
59
+ segments.push(asString(block.text));
60
+ }
61
+ }
62
+ return segments;
63
+ }
64
+ function extractTextDelta(event) {
65
+ if (event.type === 'content_block_delta') {
66
+ const delta = asObject(event.delta);
67
+ if (!delta)
68
+ return null;
69
+ const deltaType = asString(delta.type);
70
+ if (deltaType !== undefined && deltaType !== 'text_delta') {
71
+ return null;
72
+ }
73
+ return asString(delta.text) ?? null;
74
+ }
75
+ if (event.type === 'content_block_start') {
76
+ const block = asObject(event.content_block) ?? asObject(event.block);
77
+ if (!block || block.type !== 'text') {
78
+ return null;
79
+ }
80
+ return asString(block.text) ?? null;
81
+ }
82
+ return null;
83
+ }
84
+ function extractToolSummary(event) {
85
+ if (event.type === 'tool_use') {
86
+ const toolName = asString(event.name) ?? asString(event.tool_name);
87
+ return hasVisibleText(toolName) ? `Running ${toolName}…` : 'Running tool…';
88
+ }
89
+ if (event.type === 'content_block_start') {
90
+ const block = asObject(event.content_block) ?? asObject(event.block);
91
+ if (!block || block.type !== 'tool_use') {
92
+ return null;
93
+ }
94
+ const toolName = asString(block.name);
95
+ return hasVisibleText(toolName) ? `Running ${toolName}…` : 'Running tool…';
96
+ }
97
+ return null;
98
+ }
99
+ function extractFinalText(event) {
100
+ if (event.type === 'result') {
101
+ const result = asString(event.result);
102
+ if (result !== undefined) {
103
+ return result;
104
+ }
105
+ }
106
+ if (event.type === 'message' || event.type === 'assistant') {
107
+ const message = asObject(event.message) ?? event;
108
+ const joined = readTextBlocks(message.content).join('');
109
+ if (joined.length > 0) {
110
+ return joined;
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ class ClaudeStreamCollector {
116
+ onProgress;
117
+ publicText = '';
118
+ finalText = null;
119
+ lastPublished = null;
120
+ constructor(onProgress) {
121
+ this.onProgress = onProgress;
122
+ }
123
+ consume(event) {
124
+ const payload = event.type === 'stream_event' ? asObject(event.event) ?? event : event;
125
+ const textDelta = extractTextDelta(payload);
126
+ if (textDelta !== null) {
127
+ this.publicText += textDelta;
128
+ this.publish(this.publicText);
129
+ }
130
+ if (!hasVisibleText(this.publicText)) {
131
+ const toolSummary = extractToolSummary(payload);
132
+ if (toolSummary) {
133
+ this.publish(toolSummary);
134
+ }
135
+ }
136
+ const finalText = extractFinalText(event) ?? extractFinalText(payload);
137
+ if (finalText !== null) {
138
+ this.finalText = finalText;
139
+ }
140
+ }
141
+ getFinalText() {
142
+ const text = this.finalText ?? this.publicText;
143
+ return hasVisibleText(text) ? text : '';
144
+ }
145
+ publish(text) {
146
+ if (!this.onProgress || !hasVisibleText(text) || text === this.lastPublished) {
147
+ return;
148
+ }
149
+ this.lastPublished = text;
150
+ this.onProgress({ text });
151
+ }
152
+ }
3
153
  /**
4
154
  * CLI client for the Claude Code CLI (`claude`) non-interactive mode, with
5
155
  * native per-channel session continuity.
6
156
  *
7
- * Verified CLI behavior (`claude --help`):
8
- * - `--session-id <uuid>` starts a *new* conversation pinned to that UUID.
9
- * - `-r/--resume <uuid>` resumes an *existing* conversation by session ID.
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.
157
+ * Claude session memory remains entirely inside the Claude CLI. This client
158
+ * only pins one native session id per Borgee channel and serializes turns per
159
+ * channel so `--resume` is never called concurrently for the same session.
15
160
  */
16
161
  export class ClaudeCliClient {
17
162
  command;
18
163
  args;
19
- sessionIdsByChannel = new Map();
20
- constructor(command, args) {
164
+ sessionStore;
165
+ resolveSessionStoreAgentId;
166
+ runtime;
167
+ channels = new Map();
168
+ persistedSessions = new Map();
169
+ loadedSessionStoreAgentId = null;
170
+ stopped = false;
171
+ sessionStoreLoadPromise = null;
172
+ sessionStoreWriteQueue = Promise.resolve();
173
+ constructor(command, args, runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined) {
21
174
  this.command = command;
22
175
  this.args = args;
176
+ this.sessionStore = sessionStore;
177
+ this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
178
+ this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
179
+ }
180
+ async generateReply(channelId, prompt, options) {
181
+ if (this.stopped) {
182
+ throw new Error('Claude CLI backend stopped');
183
+ }
184
+ const state = this.getOrCreateChannelState(channelId);
185
+ const turn = createDeferredTurn(prompt, options);
186
+ state.queue.push(turn);
187
+ this.processChannelQueue(channelId, state);
188
+ return turn.promise;
23
189
  }
24
- async generateReply(channelId, prompt) {
25
- const existingSessionId = this.sessionIdsByChannel.get(channelId);
26
- const sessionId = existingSessionId ?? randomUUID();
27
- const sessionArgs = existingSessionId
28
- ? ['--resume', sessionId]
29
- : ['--session-id', sessionId];
30
- const text = await this.run([...this.args, ...sessionArgs], prompt);
31
- // Only remember the session once the CLI call actually succeeds, so a
32
- // failed first turn doesn't leave us permanently trying to `--resume`
33
- // a session that was never created.
34
- this.sessionIdsByChannel.set(channelId, sessionId);
35
- return text;
36
- }
37
- async run(args, prompt) {
190
+ async dispose() {
191
+ if (this.stopped)
192
+ return;
193
+ this.stopped = true;
194
+ const stoppedError = new Error('Claude CLI backend stopped');
195
+ for (const state of this.channels.values()) {
196
+ state.activeTurn?.reject(stoppedError);
197
+ for (const queuedTurn of state.queue) {
198
+ queuedTurn.reject(stoppedError);
199
+ }
200
+ state.queue = [];
201
+ state.activeChild?.kill('SIGTERM');
202
+ }
203
+ }
204
+ getOrCreateChannelState(channelId) {
205
+ let state = this.channels.get(channelId);
206
+ if (!state) {
207
+ state = {
208
+ processing: false,
209
+ queue: [],
210
+ sessionHydrated: false,
211
+ sessionEstablished: false,
212
+ };
213
+ this.channels.set(channelId, state);
214
+ }
215
+ return state;
216
+ }
217
+ processChannelQueue(channelId, state) {
218
+ if (state.processing) {
219
+ return;
220
+ }
221
+ state.processing = true;
222
+ void (async () => {
223
+ try {
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
+ }
238
+ const turn = state.queue.shift();
239
+ if (!turn) {
240
+ return;
241
+ }
242
+ state.activeTurn = turn;
243
+ try {
244
+ const text = await this.runTurn(channelId, state, turn);
245
+ turn.resolve(text);
246
+ }
247
+ catch (error) {
248
+ turn.reject(error);
249
+ }
250
+ finally {
251
+ state.activeTurn = undefined;
252
+ }
253
+ }
254
+ }
255
+ finally {
256
+ state.processing = false;
257
+ if (state.queue.length > 0 && !this.stopped) {
258
+ this.processChannelQueue(channelId, state);
259
+ }
260
+ }
261
+ })();
262
+ }
263
+ async runTurn(channelId, state, turn) {
264
+ return this.runTurnAttempt(channelId, state, turn, true);
265
+ }
266
+ async runTurnAttempt(channelId, state, turn, allowFreshRetryAfterStaleResume) {
267
+ if (this.stopped) {
268
+ throw new Error('Claude CLI backend stopped');
269
+ }
270
+ const sessionId = state.sessionEstablished ? state.sessionId : randomUUID();
271
+ const sessionArgs = state.sessionEstablished ? ['--resume', sessionId] : ['--session-id', sessionId];
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;
338
+ state.sessionEstablished = true;
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;
395
+ }
396
+ async run(state, args, prompt, options) {
38
397
  return new Promise((resolve, reject) => {
39
- const child = spawn(this.command, args, {
398
+ const child = this.runtime.spawn(this.command, args, {
40
399
  stdio: ['pipe', 'pipe', 'pipe'],
41
400
  });
42
- let stdout = '';
401
+ state.activeChild = child;
402
+ const collector = new ClaudeStreamCollector(options?.onProgress);
43
403
  let stderr = '';
404
+ let lineBuffer = '';
405
+ let settled = false;
406
+ const settleReject = (error) => {
407
+ if (settled)
408
+ return;
409
+ settled = true;
410
+ reject(normalizeError(error));
411
+ };
412
+ const settleResolve = (value) => {
413
+ if (settled)
414
+ return;
415
+ settled = true;
416
+ resolve(value);
417
+ };
44
418
  child.stdout.setEncoding('utf8');
45
419
  child.stderr.setEncoding('utf8');
46
420
  child.stdout.on('data', (chunk) => {
47
- stdout += chunk;
421
+ lineBuffer += chunk;
422
+ while (true) {
423
+ const newlineIndex = lineBuffer.indexOf('\n');
424
+ if (newlineIndex === -1) {
425
+ break;
426
+ }
427
+ const line = lineBuffer.slice(0, newlineIndex).trim();
428
+ lineBuffer = lineBuffer.slice(newlineIndex + 1);
429
+ if (!line)
430
+ continue;
431
+ try {
432
+ const event = JSON.parse(line);
433
+ collector.consume(event);
434
+ }
435
+ catch (error) {
436
+ settleReject(new Error(`Claude stream-json parse failed: ${normalizeError(error).message}`));
437
+ child.kill('SIGTERM');
438
+ return;
439
+ }
440
+ }
48
441
  });
49
442
  child.stderr.on('data', (chunk) => {
50
443
  stderr += chunk;
51
444
  });
52
- child.on('error', (error) => {
53
- reject(error);
445
+ child.once('error', (error) => {
446
+ settleReject(error);
54
447
  });
55
- child.on('close', (code) => {
56
- if (code !== 0) {
57
- reject(new Error(`Claude CLI failed with code ${code}: ${stderr.trim()}`));
58
- return;
448
+ child.once('close', (code) => {
449
+ if (state.activeChild === child) {
450
+ state.activeChild = undefined;
451
+ }
452
+ if (lineBuffer.trim().length > 0 && !settled) {
453
+ try {
454
+ collector.consume(JSON.parse(lineBuffer.trim()));
455
+ lineBuffer = '';
456
+ }
457
+ catch (error) {
458
+ settleReject(new Error(`Claude stream-json parse failed: ${normalizeError(error).message}`));
459
+ return;
460
+ }
59
461
  }
60
- const text = stdout.trim();
61
- if (!text) {
62
- reject(new Error('Claude CLI returned empty output'));
462
+ if (code !== 0) {
463
+ settleReject(new Error(`Claude CLI failed with code ${code}: ${stderr.trim()}`));
63
464
  return;
64
465
  }
65
- resolve(text);
466
+ settleResolve(collector.getFinalText());
66
467
  });
67
468
  child.stdin.write(prompt);
68
469
  child.stdin.end();
@@ -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,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() {