@avadisabelle/ava-pi-agent-core 0.64.9 → 0.65.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/dist/agent.js CHANGED
@@ -1,302 +1,369 @@
1
- /**
2
- * Agent class that uses the agent-loop directly.
3
- * No transport abstraction - calls streamSimple via the loop.
4
- */
5
- import { getModel, streamSimple, } from "@avadisabelle/ava-pi-ai";
1
+ import { streamSimple, } from "@avadisabelle/ava-pi-ai";
6
2
  import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.js";
7
- /**
8
- * Default convertToLlm: Keep only LLM-compatible messages, convert attachments.
9
- */
10
3
  function defaultConvertToLlm(messages) {
11
- return messages.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult");
4
+ return messages.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
12
5
  }
13
- export class Agent {
14
- _state = {
15
- systemPrompt: "",
16
- model: getModel("google", "gemini-2.5-flash-lite-preview-06-17"),
17
- thinkingLevel: "off",
18
- tools: [],
19
- messages: [],
6
+ const EMPTY_USAGE = {
7
+ input: 0,
8
+ output: 0,
9
+ cacheRead: 0,
10
+ cacheWrite: 0,
11
+ totalTokens: 0,
12
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
13
+ };
14
+ const DEFAULT_MODEL = {
15
+ id: "unknown",
16
+ name: "unknown",
17
+ api: "unknown",
18
+ provider: "unknown",
19
+ baseUrl: "",
20
+ reasoning: false,
21
+ input: [],
22
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
23
+ contextWindow: 0,
24
+ maxTokens: 0,
25
+ };
26
+ function createMutableAgentState(initialState) {
27
+ let tools = initialState?.tools?.slice() ?? [];
28
+ let messages = initialState?.messages?.slice() ?? [];
29
+ return {
30
+ systemPrompt: initialState?.systemPrompt ?? "",
31
+ model: initialState?.model ?? DEFAULT_MODEL,
32
+ thinkingLevel: initialState?.thinkingLevel ?? "off",
33
+ get tools() {
34
+ return tools;
35
+ },
36
+ set tools(nextTools) {
37
+ tools = nextTools.slice();
38
+ },
39
+ get messages() {
40
+ return messages;
41
+ },
42
+ set messages(nextMessages) {
43
+ messages = nextMessages.slice();
44
+ },
20
45
  isStreaming: false,
21
- streamMessage: null,
46
+ streamingMessage: undefined,
22
47
  pendingToolCalls: new Set(),
23
- error: undefined,
48
+ errorMessage: undefined,
24
49
  };
50
+ }
51
+ class PendingMessageQueue {
52
+ mode;
53
+ messages = [];
54
+ constructor(mode) {
55
+ this.mode = mode;
56
+ }
57
+ enqueue(message) {
58
+ this.messages.push(message);
59
+ }
60
+ hasItems() {
61
+ return this.messages.length > 0;
62
+ }
63
+ drain() {
64
+ if (this.mode === "all") {
65
+ const drained = this.messages.slice();
66
+ this.messages = [];
67
+ return drained;
68
+ }
69
+ const first = this.messages[0];
70
+ if (!first) {
71
+ return [];
72
+ }
73
+ this.messages = this.messages.slice(1);
74
+ return [first];
75
+ }
76
+ clear() {
77
+ this.messages = [];
78
+ }
79
+ }
80
+ /**
81
+ * Stateful wrapper around the low-level agent loop.
82
+ *
83
+ * `Agent` owns the current transcript, emits lifecycle events, executes tools,
84
+ * and exposes queueing APIs for steering and follow-up messages.
85
+ */
86
+ export class Agent {
87
+ _state;
25
88
  listeners = new Set();
26
- abortController;
89
+ steeringQueue;
90
+ followUpQueue;
27
91
  convertToLlm;
28
92
  transformContext;
29
- steeringQueue = [];
30
- followUpQueue = [];
31
- steeringMode;
32
- followUpMode;
33
93
  streamFn;
34
- _sessionId;
35
94
  getApiKey;
36
- _onPayload;
37
- runningPrompt;
38
- resolveRunningPrompt;
39
- _thinkingBudgets;
40
- _transport;
41
- _maxRetryDelayMs;
42
- _toolExecution;
43
- _beforeToolCall;
44
- _afterToolCall;
45
- constructor(opts = {}) {
46
- this._state = { ...this._state, ...opts.initialState };
47
- this.convertToLlm = opts.convertToLlm || defaultConvertToLlm;
48
- this.transformContext = opts.transformContext;
49
- this.steeringMode = opts.steeringMode || "one-at-a-time";
50
- this.followUpMode = opts.followUpMode || "one-at-a-time";
51
- this.streamFn = opts.streamFn || streamSimple;
52
- this._sessionId = opts.sessionId;
53
- this.getApiKey = opts.getApiKey;
54
- this._onPayload = opts.onPayload;
55
- this._thinkingBudgets = opts.thinkingBudgets;
56
- this._transport = opts.transport ?? "sse";
57
- this._maxRetryDelayMs = opts.maxRetryDelayMs;
58
- this._toolExecution = opts.toolExecution ?? "parallel";
59
- this._beforeToolCall = opts.beforeToolCall;
60
- this._afterToolCall = opts.afterToolCall;
61
- }
62
- /**
63
- * Get the current session ID used for provider caching.
64
- */
65
- get sessionId() {
66
- return this._sessionId;
67
- }
68
- /**
69
- * Set the session ID for provider caching.
70
- * Call this when switching sessions (new session, branch, resume).
71
- */
72
- set sessionId(value) {
73
- this._sessionId = value;
74
- }
75
- /**
76
- * Get the current thinking budgets.
77
- */
78
- get thinkingBudgets() {
79
- return this._thinkingBudgets;
80
- }
81
- /**
82
- * Set custom thinking budgets for token-based providers.
83
- */
84
- set thinkingBudgets(value) {
85
- this._thinkingBudgets = value;
86
- }
87
- /**
88
- * Get the current preferred transport.
89
- */
90
- get transport() {
91
- return this._transport;
92
- }
93
- /**
94
- * Set the preferred transport.
95
- */
96
- setTransport(value) {
97
- this._transport = value;
95
+ onPayload;
96
+ onResponse;
97
+ beforeToolCall;
98
+ afterToolCall;
99
+ activeRun;
100
+ /** Session identifier forwarded to providers for cache-aware backends. */
101
+ sessionId;
102
+ /** Optional per-level thinking token budgets forwarded to the stream function. */
103
+ thinkingBudgets;
104
+ /** Preferred transport forwarded to the stream function. */
105
+ transport;
106
+ /** Optional cap for provider-requested retry delays. */
107
+ maxRetryDelayMs;
108
+ /** Tool execution strategy for assistant messages that contain multiple tool calls. */
109
+ toolExecution;
110
+ constructor(options = {}) {
111
+ this._state = createMutableAgentState(options.initialState);
112
+ this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
113
+ this.transformContext = options.transformContext;
114
+ this.streamFn = options.streamFn ?? streamSimple;
115
+ this.getApiKey = options.getApiKey;
116
+ this.onPayload = options.onPayload;
117
+ this.onResponse = options.onResponse;
118
+ this.beforeToolCall = options.beforeToolCall;
119
+ this.afterToolCall = options.afterToolCall;
120
+ this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
121
+ this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
122
+ this.sessionId = options.sessionId;
123
+ this.thinkingBudgets = options.thinkingBudgets;
124
+ this.transport = options.transport ?? "sse";
125
+ this.maxRetryDelayMs = options.maxRetryDelayMs;
126
+ this.toolExecution = options.toolExecution ?? "parallel";
98
127
  }
99
128
  /**
100
- * Get the current max retry delay in milliseconds.
129
+ * Subscribe to agent lifecycle events.
130
+ *
131
+ * Listener promises are awaited in subscription order and are included in
132
+ * the current run's settlement. Listeners also receive the active abort
133
+ * signal for the current run.
134
+ *
135
+ * `agent_end` is the final emitted event for a run, but the agent does not
136
+ * become idle until all awaited listeners for that event have settled.
101
137
  */
102
- get maxRetryDelayMs() {
103
- return this._maxRetryDelayMs;
138
+ subscribe(listener) {
139
+ this.listeners.add(listener);
140
+ return () => this.listeners.delete(listener);
104
141
  }
105
142
  /**
106
- * Set the maximum delay to wait for server-requested retries.
107
- * Set to 0 to disable the cap.
143
+ * Current agent state.
144
+ *
145
+ * Assigning `state.tools` or `state.messages` copies the provided top-level array.
108
146
  */
109
- set maxRetryDelayMs(value) {
110
- this._maxRetryDelayMs = value;
111
- }
112
- get toolExecution() {
113
- return this._toolExecution;
114
- }
115
- setToolExecution(value) {
116
- this._toolExecution = value;
117
- }
118
- setBeforeToolCall(value) {
119
- this._beforeToolCall = value;
120
- }
121
- setAfterToolCall(value) {
122
- this._afterToolCall = value;
123
- }
124
147
  get state() {
125
148
  return this._state;
126
149
  }
127
- subscribe(fn) {
128
- this.listeners.add(fn);
129
- return () => this.listeners.delete(fn);
130
- }
131
- // State mutators
132
- setSystemPrompt(v) {
133
- this._state.systemPrompt = v;
134
- }
135
- setModel(m) {
136
- this._state.model = m;
137
- }
138
- setThinkingLevel(l) {
139
- this._state.thinkingLevel = l;
150
+ /** Controls how queued steering messages are drained. */
151
+ set steeringMode(mode) {
152
+ this.steeringQueue.mode = mode;
140
153
  }
141
- setSteeringMode(mode) {
142
- this.steeringMode = mode;
154
+ get steeringMode() {
155
+ return this.steeringQueue.mode;
143
156
  }
144
- getSteeringMode() {
145
- return this.steeringMode;
157
+ /** Controls how queued follow-up messages are drained. */
158
+ set followUpMode(mode) {
159
+ this.followUpQueue.mode = mode;
146
160
  }
147
- setFollowUpMode(mode) {
148
- this.followUpMode = mode;
161
+ get followUpMode() {
162
+ return this.followUpQueue.mode;
149
163
  }
150
- getFollowUpMode() {
151
- return this.followUpMode;
164
+ /** Queue a message to be injected after the current assistant turn finishes. */
165
+ steer(message) {
166
+ this.steeringQueue.enqueue(message);
152
167
  }
153
- setTools(t) {
154
- this._state.tools = t;
155
- }
156
- replaceMessages(ms) {
157
- this._state.messages = ms.slice();
158
- }
159
- appendMessage(m) {
160
- this._state.messages = [...this._state.messages, m];
161
- }
162
- /**
163
- * Queue a steering message while the agent is running.
164
- * Delivered after the current assistant turn finishes executing its tool calls,
165
- * before the next LLM call.
166
- */
167
- steer(m) {
168
- this.steeringQueue.push(m);
169
- }
170
- /**
171
- * Queue a follow-up message to be processed after the agent finishes.
172
- * Delivered only when agent has no more tool calls or steering messages.
173
- */
174
- followUp(m) {
175
- this.followUpQueue.push(m);
168
+ /** Queue a message to run only after the agent would otherwise stop. */
169
+ followUp(message) {
170
+ this.followUpQueue.enqueue(message);
176
171
  }
172
+ /** Remove all queued steering messages. */
177
173
  clearSteeringQueue() {
178
- this.steeringQueue = [];
174
+ this.steeringQueue.clear();
179
175
  }
176
+ /** Remove all queued follow-up messages. */
180
177
  clearFollowUpQueue() {
181
- this.followUpQueue = [];
178
+ this.followUpQueue.clear();
182
179
  }
180
+ /** Remove all queued steering and follow-up messages. */
183
181
  clearAllQueues() {
184
- this.steeringQueue = [];
185
- this.followUpQueue = [];
182
+ this.clearSteeringQueue();
183
+ this.clearFollowUpQueue();
186
184
  }
185
+ /** Returns true when either queue still contains pending messages. */
187
186
  hasQueuedMessages() {
188
- return this.steeringQueue.length > 0 || this.followUpQueue.length > 0;
189
- }
190
- dequeueSteeringMessages() {
191
- if (this.steeringMode === "one-at-a-time") {
192
- if (this.steeringQueue.length > 0) {
193
- const first = this.steeringQueue[0];
194
- this.steeringQueue = this.steeringQueue.slice(1);
195
- return [first];
196
- }
197
- return [];
198
- }
199
- const steering = this.steeringQueue.slice();
200
- this.steeringQueue = [];
201
- return steering;
202
- }
203
- dequeueFollowUpMessages() {
204
- if (this.followUpMode === "one-at-a-time") {
205
- if (this.followUpQueue.length > 0) {
206
- const first = this.followUpQueue[0];
207
- this.followUpQueue = this.followUpQueue.slice(1);
208
- return [first];
209
- }
210
- return [];
211
- }
212
- const followUp = this.followUpQueue.slice();
213
- this.followUpQueue = [];
214
- return followUp;
187
+ return this.steeringQueue.hasItems() || this.followUpQueue.hasItems();
215
188
  }
216
- clearMessages() {
217
- this._state.messages = [];
189
+ /** Active abort signal for the current run, if any. */
190
+ get signal() {
191
+ return this.activeRun?.abortController.signal;
218
192
  }
193
+ /** Abort the current run, if one is active. */
219
194
  abort() {
220
- this.abortController?.abort();
195
+ this.activeRun?.abortController.abort();
221
196
  }
197
+ /**
198
+ * Resolve when the current run and all awaited event listeners have finished.
199
+ *
200
+ * This resolves after `agent_end` listeners settle.
201
+ */
222
202
  waitForIdle() {
223
- return this.runningPrompt ?? Promise.resolve();
203
+ return this.activeRun?.promise ?? Promise.resolve();
224
204
  }
205
+ /** Clear transcript state, runtime state, and queued messages. */
225
206
  reset() {
226
207
  this._state.messages = [];
227
208
  this._state.isStreaming = false;
228
- this._state.streamMessage = null;
209
+ this._state.streamingMessage = undefined;
229
210
  this._state.pendingToolCalls = new Set();
230
- this._state.error = undefined;
231
- this.steeringQueue = [];
232
- this.followUpQueue = [];
211
+ this._state.errorMessage = undefined;
212
+ this.clearFollowUpQueue();
213
+ this.clearSteeringQueue();
233
214
  }
234
215
  async prompt(input, images) {
235
- if (this._state.isStreaming) {
216
+ if (this.activeRun) {
236
217
  throw new Error("Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.");
237
218
  }
238
- const model = this._state.model;
239
- if (!model)
240
- throw new Error("No model configured");
241
- let msgs;
242
- if (Array.isArray(input)) {
243
- msgs = input;
244
- }
245
- else if (typeof input === "string") {
246
- const content = [{ type: "text", text: input }];
247
- if (images && images.length > 0) {
248
- content.push(...images);
249
- }
250
- msgs = [
251
- {
252
- role: "user",
253
- content,
254
- timestamp: Date.now(),
255
- },
256
- ];
257
- }
258
- else {
259
- msgs = [input];
260
- }
261
- await this._runLoop(msgs);
219
+ const messages = this.normalizePromptInput(input, images);
220
+ await this.runPromptMessages(messages);
262
221
  }
263
- /**
264
- * Continue from current context (used for retries and resuming queued messages).
265
- */
222
+ /** Continue from the current transcript. The last message must be a user or tool-result message. */
266
223
  async continue() {
267
- if (this._state.isStreaming) {
224
+ if (this.activeRun) {
268
225
  throw new Error("Agent is already processing. Wait for completion before continuing.");
269
226
  }
270
- const messages = this._state.messages;
271
- if (messages.length === 0) {
227
+ const lastMessage = this._state.messages[this._state.messages.length - 1];
228
+ if (!lastMessage) {
272
229
  throw new Error("No messages to continue from");
273
230
  }
274
- if (messages[messages.length - 1].role === "assistant") {
275
- const queuedSteering = this.dequeueSteeringMessages();
231
+ if (lastMessage.role === "assistant") {
232
+ const queuedSteering = this.steeringQueue.drain();
276
233
  if (queuedSteering.length > 0) {
277
- await this._runLoop(queuedSteering, { skipInitialSteeringPoll: true });
234
+ await this.runPromptMessages(queuedSteering, { skipInitialSteeringPoll: true });
278
235
  return;
279
236
  }
280
- const queuedFollowUp = this.dequeueFollowUpMessages();
281
- if (queuedFollowUp.length > 0) {
282
- await this._runLoop(queuedFollowUp);
237
+ const queuedFollowUps = this.followUpQueue.drain();
238
+ if (queuedFollowUps.length > 0) {
239
+ await this.runPromptMessages(queuedFollowUps);
283
240
  return;
284
241
  }
285
242
  throw new Error("Cannot continue from message role: assistant");
286
243
  }
287
- await this._runLoop(undefined);
244
+ await this.runContinuation();
288
245
  }
289
- _processLoopEvent(event) {
246
+ normalizePromptInput(input, images) {
247
+ if (Array.isArray(input)) {
248
+ return input;
249
+ }
250
+ if (typeof input !== "string") {
251
+ return [input];
252
+ }
253
+ const content = [{ type: "text", text: input }];
254
+ if (images && images.length > 0) {
255
+ content.push(...images);
256
+ }
257
+ return [{ role: "user", content, timestamp: Date.now() }];
258
+ }
259
+ async runPromptMessages(messages, options = {}) {
260
+ await this.runWithLifecycle(async (signal) => {
261
+ await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFn);
262
+ });
263
+ }
264
+ async runContinuation() {
265
+ await this.runWithLifecycle(async (signal) => {
266
+ await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFn);
267
+ });
268
+ }
269
+ createContextSnapshot() {
270
+ return {
271
+ systemPrompt: this._state.systemPrompt,
272
+ messages: this._state.messages.slice(),
273
+ tools: this._state.tools.slice(),
274
+ };
275
+ }
276
+ createLoopConfig(options = {}) {
277
+ let skipInitialSteeringPoll = options.skipInitialSteeringPoll === true;
278
+ return {
279
+ model: this._state.model,
280
+ reasoning: this._state.thinkingLevel === "off" ? undefined : this._state.thinkingLevel,
281
+ sessionId: this.sessionId,
282
+ onPayload: this.onPayload,
283
+ onResponse: this.onResponse,
284
+ transport: this.transport,
285
+ thinkingBudgets: this.thinkingBudgets,
286
+ maxRetryDelayMs: this.maxRetryDelayMs,
287
+ toolExecution: this.toolExecution,
288
+ beforeToolCall: this.beforeToolCall,
289
+ afterToolCall: this.afterToolCall,
290
+ convertToLlm: this.convertToLlm,
291
+ transformContext: this.transformContext,
292
+ getApiKey: this.getApiKey,
293
+ getSteeringMessages: async () => {
294
+ if (skipInitialSteeringPoll) {
295
+ skipInitialSteeringPoll = false;
296
+ return [];
297
+ }
298
+ return this.steeringQueue.drain();
299
+ },
300
+ getFollowUpMessages: async () => this.followUpQueue.drain(),
301
+ };
302
+ }
303
+ async runWithLifecycle(executor) {
304
+ if (this.activeRun) {
305
+ throw new Error("Agent is already processing.");
306
+ }
307
+ const abortController = new AbortController();
308
+ let resolvePromise = () => { };
309
+ const promise = new Promise((resolve) => {
310
+ resolvePromise = resolve;
311
+ });
312
+ this.activeRun = { promise, resolve: resolvePromise, abortController };
313
+ this._state.isStreaming = true;
314
+ this._state.streamingMessage = undefined;
315
+ this._state.errorMessage = undefined;
316
+ try {
317
+ await executor(abortController.signal);
318
+ }
319
+ catch (error) {
320
+ await this.handleRunFailure(error, abortController.signal.aborted);
321
+ }
322
+ finally {
323
+ this.finishRun();
324
+ }
325
+ }
326
+ async handleRunFailure(error, aborted) {
327
+ const failureMessage = {
328
+ role: "assistant",
329
+ content: [{ type: "text", text: "" }],
330
+ api: this._state.model.api,
331
+ provider: this._state.model.provider,
332
+ model: this._state.model.id,
333
+ usage: EMPTY_USAGE,
334
+ stopReason: aborted ? "aborted" : "error",
335
+ errorMessage: error instanceof Error ? error.message : String(error),
336
+ timestamp: Date.now(),
337
+ };
338
+ this._state.messages.push(failureMessage);
339
+ this._state.errorMessage = failureMessage.errorMessage;
340
+ await this.processEvents({ type: "agent_end", messages: [failureMessage] });
341
+ }
342
+ finishRun() {
343
+ this._state.isStreaming = false;
344
+ this._state.streamingMessage = undefined;
345
+ this._state.pendingToolCalls = new Set();
346
+ this.activeRun?.resolve();
347
+ this.activeRun = undefined;
348
+ }
349
+ /**
350
+ * Reduce internal state for a loop event, then await listeners.
351
+ *
352
+ * `agent_end` only means no further loop events will be emitted. The run is
353
+ * considered idle later, after all awaited listeners for `agent_end` finish
354
+ * and `finishRun()` clears runtime-owned state.
355
+ */
356
+ async processEvents(event) {
290
357
  switch (event.type) {
291
358
  case "message_start":
292
- this._state.streamMessage = event.message;
359
+ this._state.streamingMessage = event.message;
293
360
  break;
294
361
  case "message_update":
295
- this._state.streamMessage = event.message;
362
+ this._state.streamingMessage = event.message;
296
363
  break;
297
364
  case "message_end":
298
- this._state.streamMessage = null;
299
- this.appendMessage(event.message);
365
+ this._state.streamingMessage = undefined;
366
+ this._state.messages.push(event.message);
300
367
  break;
301
368
  case "tool_execution_start": {
302
369
  const pendingToolCalls = new Set(this._state.pendingToolCalls);
@@ -312,106 +379,19 @@ export class Agent {
312
379
  }
313
380
  case "turn_end":
314
381
  if (event.message.role === "assistant" && event.message.errorMessage) {
315
- this._state.error = event.message.errorMessage;
382
+ this._state.errorMessage = event.message.errorMessage;
316
383
  }
317
384
  break;
318
385
  case "agent_end":
319
- this._state.isStreaming = false;
320
- this._state.streamMessage = null;
386
+ this._state.streamingMessage = undefined;
321
387
  break;
322
388
  }
323
- this.emit(event);
324
- }
325
- /**
326
- * Run the agent loop.
327
- * If messages are provided, starts a new conversation turn with those messages.
328
- * Otherwise, continues from existing context.
329
- */
330
- async _runLoop(messages, options) {
331
- const model = this._state.model;
332
- if (!model)
333
- throw new Error("No model configured");
334
- this.runningPrompt = new Promise((resolve) => {
335
- this.resolveRunningPrompt = resolve;
336
- });
337
- this.abortController = new AbortController();
338
- this._state.isStreaming = true;
339
- this._state.streamMessage = null;
340
- this._state.error = undefined;
341
- const reasoning = this._state.thinkingLevel === "off" ? undefined : this._state.thinkingLevel;
342
- const context = {
343
- systemPrompt: this._state.systemPrompt,
344
- messages: this._state.messages.slice(),
345
- tools: this._state.tools,
346
- };
347
- let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true;
348
- const config = {
349
- model,
350
- reasoning,
351
- sessionId: this._sessionId,
352
- onPayload: this._onPayload,
353
- transport: this._transport,
354
- thinkingBudgets: this._thinkingBudgets,
355
- maxRetryDelayMs: this._maxRetryDelayMs,
356
- toolExecution: this._toolExecution,
357
- beforeToolCall: this._beforeToolCall,
358
- afterToolCall: this._afterToolCall,
359
- convertToLlm: this.convertToLlm,
360
- transformContext: this.transformContext,
361
- getApiKey: this.getApiKey,
362
- getSteeringMessages: async () => {
363
- if (skipInitialSteeringPoll) {
364
- skipInitialSteeringPoll = false;
365
- return [];
366
- }
367
- return this.dequeueSteeringMessages();
368
- },
369
- getFollowUpMessages: async () => this.dequeueFollowUpMessages(),
370
- };
371
- try {
372
- if (messages) {
373
- await runAgentLoop(messages, context, config, async (event) => this._processLoopEvent(event), this.abortController.signal, this.streamFn);
374
- }
375
- else {
376
- await runAgentLoopContinue(context, config, async (event) => this._processLoopEvent(event), this.abortController.signal, this.streamFn);
377
- }
378
- }
379
- catch (err) {
380
- const errorMsg = {
381
- role: "assistant",
382
- content: [{ type: "text", text: "" }],
383
- api: model.api,
384
- provider: model.provider,
385
- model: model.id,
386
- usage: {
387
- input: 0,
388
- output: 0,
389
- cacheRead: 0,
390
- cacheWrite: 0,
391
- totalTokens: 0,
392
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
393
- },
394
- stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
395
- errorMessage: err?.message || String(err),
396
- timestamp: Date.now(),
397
- };
398
- this.appendMessage(errorMsg);
399
- this._state.error = err?.message || String(err);
400
- this.emit({ type: "agent_end", messages: [errorMsg] });
401
- }
402
- finally {
403
- this._state.isStreaming = false;
404
- this._state.streamMessage = null;
405
- this._state.pendingToolCalls = new Set();
406
- this.abortController = undefined;
407
- this.resolveRunningPrompt?.();
408
- this.runningPrompt = undefined;
409
- this.resolveRunningPrompt = undefined;
389
+ const signal = this.activeRun?.abortController.signal;
390
+ if (!signal) {
391
+ throw new Error("Agent listener invoked outside active run");
410
392
  }
411
- }
412
- emit(e) {
413
393
  for (const listener of this.listeners) {
414
- listener(e);
394
+ await listener(event, signal);
415
395
  }
416
396
  }
417
397
  }