@oh-my-pi/pi-agent-core 3.15.1 → 3.20.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 +14 -9
- package/package.json +3 -3
- package/src/agent-loop.ts +120 -92
- package/src/agent.ts +101 -33
- package/src/types.ts +20 -9
package/README.md
CHANGED
|
@@ -224,26 +224,31 @@ const unsubscribe = agent.subscribe((event) => {
|
|
|
224
224
|
unsubscribe();
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
-
##
|
|
227
|
+
## Steering & Follow-up
|
|
228
228
|
|
|
229
|
-
Queue messages to inject during tool execution (
|
|
229
|
+
Queue messages to inject during tool execution (steering) or after the agent would otherwise stop (follow-up):
|
|
230
230
|
|
|
231
231
|
```typescript
|
|
232
|
-
agent.
|
|
232
|
+
agent.setSteeringMode("one-at-a-time");
|
|
233
|
+
agent.setInterruptMode("immediate");
|
|
233
234
|
|
|
234
235
|
// While agent is running tools
|
|
235
|
-
agent.
|
|
236
|
+
agent.steer({
|
|
236
237
|
role: "user",
|
|
237
238
|
content: "Stop! Do this instead.",
|
|
238
239
|
timestamp: Date.now(),
|
|
239
240
|
});
|
|
240
|
-
```
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
// Queue a follow-up to run after the current turn completes
|
|
243
|
+
agent.followUp({
|
|
244
|
+
role: "user",
|
|
245
|
+
content: "After that, summarize the changes.",
|
|
246
|
+
timestamp: Date.now(),
|
|
247
|
+
});
|
|
248
|
+
```
|
|
243
249
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
3. LLM responds to the interruption
|
|
250
|
+
Steering messages are checked after each tool call by default. Set `interruptMode` to `"wait"` to defer
|
|
251
|
+
steering until the current turn completes.
|
|
247
252
|
|
|
248
253
|
## Custom Message Types
|
|
249
254
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-agent-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"test": "vitest --run"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@oh-my-pi/pi-ai": "3.
|
|
17
|
-
"@oh-my-pi/pi-tui": "3.
|
|
16
|
+
"@oh-my-pi/pi-ai": "3.20.0",
|
|
17
|
+
"@oh-my-pi/pi-tui": "3.20.0"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"ai",
|
package/src/agent-loop.ts
CHANGED
|
@@ -109,73 +109,90 @@ async function runLoop(
|
|
|
109
109
|
stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
110
110
|
streamFn?: StreamFn,
|
|
111
111
|
): Promise<void> {
|
|
112
|
-
let hasMoreToolCalls = true;
|
|
113
112
|
let firstTurn = true;
|
|
114
|
-
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
// Check for steering messages at start (user may have typed while waiting)
|
|
114
|
+
let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];
|
|
115
|
+
|
|
116
|
+
// Outer loop: continues when queued follow-up messages arrive after agent would stop
|
|
117
|
+
while (true) {
|
|
118
|
+
let hasMoreToolCalls = true;
|
|
119
|
+
let steeringAfterTools: AgentMessage[] | null = null;
|
|
120
|
+
|
|
121
|
+
// Inner loop: process tool calls and steering messages
|
|
122
|
+
while (hasMoreToolCalls || pendingMessages.length > 0) {
|
|
123
|
+
if (!firstTurn) {
|
|
124
|
+
stream.push({ type: "turn_start" });
|
|
125
|
+
} else {
|
|
126
|
+
firstTurn = false;
|
|
127
|
+
}
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
// Process pending messages (inject before next assistant response)
|
|
130
|
+
if (pendingMessages.length > 0) {
|
|
131
|
+
for (const message of pendingMessages) {
|
|
132
|
+
stream.push({ type: "message_start", message });
|
|
133
|
+
stream.push({ type: "message_end", message });
|
|
134
|
+
currentContext.messages.push(message);
|
|
135
|
+
newMessages.push(message);
|
|
136
|
+
}
|
|
137
|
+
pendingMessages = [];
|
|
131
138
|
}
|
|
132
|
-
queuedMessages = [];
|
|
133
|
-
}
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
// Stream assistant response
|
|
141
|
+
const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
|
|
142
|
+
newMessages.push(message);
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
145
|
+
stream.push({ type: "turn_end", message, toolResults: [] });
|
|
146
|
+
stream.push({ type: "agent_end", messages: newMessages });
|
|
147
|
+
stream.end(newMessages);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
// Check for tool calls
|
|
152
|
+
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
153
|
+
hasMoreToolCalls = toolCalls.length > 0;
|
|
154
|
+
|
|
155
|
+
const toolResults: ToolResultMessage[] = [];
|
|
156
|
+
if (hasMoreToolCalls) {
|
|
157
|
+
const toolExecution = await executeToolCalls(
|
|
158
|
+
currentContext.tools,
|
|
159
|
+
message,
|
|
160
|
+
signal,
|
|
161
|
+
stream,
|
|
162
|
+
config.getSteeringMessages,
|
|
163
|
+
config.getToolContext,
|
|
164
|
+
config.interruptMode,
|
|
165
|
+
);
|
|
166
|
+
toolResults.push(...toolExecution.toolResults);
|
|
167
|
+
steeringAfterTools = toolExecution.steeringMessages ?? null;
|
|
168
|
+
|
|
169
|
+
for (const result of toolResults) {
|
|
170
|
+
currentContext.messages.push(result);
|
|
171
|
+
newMessages.push(result);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
if (hasMoreToolCalls) {
|
|
152
|
-
const toolExecution = await executeToolCalls(
|
|
153
|
-
currentContext.tools,
|
|
154
|
-
message,
|
|
155
|
-
signal,
|
|
156
|
-
stream,
|
|
157
|
-
config.getQueuedMessages,
|
|
158
|
-
config.getToolContext,
|
|
159
|
-
config.interruptMode,
|
|
160
|
-
);
|
|
161
|
-
toolResults.push(...toolExecution.toolResults);
|
|
162
|
-
queuedAfterTools = toolExecution.queuedMessages ?? null;
|
|
175
|
+
stream.push({ type: "turn_end", message, toolResults });
|
|
163
176
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
// Get steering messages after turn completes
|
|
178
|
+
if (steeringAfterTools && steeringAfterTools.length > 0) {
|
|
179
|
+
pendingMessages = steeringAfterTools;
|
|
180
|
+
steeringAfterTools = null;
|
|
181
|
+
} else {
|
|
182
|
+
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
167
183
|
}
|
|
168
184
|
}
|
|
169
185
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
} else {
|
|
177
|
-
queuedMessages = (await config.getQueuedMessages?.()) || [];
|
|
186
|
+
// Agent would stop here. Check for follow-up messages.
|
|
187
|
+
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
|
|
188
|
+
if (followUpMessages.length > 0) {
|
|
189
|
+
// Set as pending so inner loop processes them
|
|
190
|
+
pendingMessages = followUpMessages;
|
|
191
|
+
continue;
|
|
178
192
|
}
|
|
193
|
+
|
|
194
|
+
// No more messages, exit
|
|
195
|
+
break;
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
stream.push({ type: "agent_end", messages: newMessages });
|
|
@@ -225,9 +242,35 @@ async function streamAssistantResponse(
|
|
|
225
242
|
let addedPartial = false;
|
|
226
243
|
|
|
227
244
|
for await (const event of response) {
|
|
228
|
-
// Check abort
|
|
245
|
+
// Check for abort signal before processing each event
|
|
229
246
|
if (signal?.aborted) {
|
|
230
|
-
|
|
247
|
+
const abortedMessage: AssistantMessage = partialMessage
|
|
248
|
+
? { ...partialMessage, stopReason: "aborted" }
|
|
249
|
+
: {
|
|
250
|
+
role: "assistant",
|
|
251
|
+
content: [],
|
|
252
|
+
api: config.model.api,
|
|
253
|
+
provider: config.model.provider,
|
|
254
|
+
model: config.model.id,
|
|
255
|
+
usage: {
|
|
256
|
+
input: 0,
|
|
257
|
+
output: 0,
|
|
258
|
+
cacheRead: 0,
|
|
259
|
+
cacheWrite: 0,
|
|
260
|
+
totalTokens: 0,
|
|
261
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
262
|
+
},
|
|
263
|
+
stopReason: "aborted",
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
};
|
|
266
|
+
if (addedPartial) {
|
|
267
|
+
context.messages[context.messages.length - 1] = abortedMessage;
|
|
268
|
+
} else {
|
|
269
|
+
context.messages.push(abortedMessage);
|
|
270
|
+
stream.push({ type: "message_start", message: { ...abortedMessage } });
|
|
271
|
+
}
|
|
272
|
+
stream.push({ type: "message_end", message: abortedMessage });
|
|
273
|
+
return abortedMessage;
|
|
231
274
|
}
|
|
232
275
|
|
|
233
276
|
switch (event.type) {
|
|
@@ -273,22 +316,6 @@ async function streamAssistantResponse(
|
|
|
273
316
|
return finalMessage;
|
|
274
317
|
}
|
|
275
318
|
}
|
|
276
|
-
|
|
277
|
-
// Check abort after processing - allows handlers to abort mid-stream
|
|
278
|
-
if (signal?.aborted) {
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// If we broke out due to abort, return an aborted message
|
|
284
|
-
if (signal?.aborted && partialMessage) {
|
|
285
|
-
const abortedMessage: AssistantMessage = {
|
|
286
|
-
...partialMessage,
|
|
287
|
-
stopReason: "aborted",
|
|
288
|
-
};
|
|
289
|
-
context.messages[context.messages.length - 1] = abortedMessage;
|
|
290
|
-
stream.push({ type: "message_end", message: abortedMessage });
|
|
291
|
-
return abortedMessage;
|
|
292
319
|
}
|
|
293
320
|
|
|
294
321
|
return await response.result();
|
|
@@ -302,13 +329,14 @@ async function executeToolCalls(
|
|
|
302
329
|
assistantMessage: AssistantMessage,
|
|
303
330
|
signal: AbortSignal | undefined,
|
|
304
331
|
stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
305
|
-
|
|
332
|
+
getSteeringMessages?: AgentLoopConfig["getSteeringMessages"],
|
|
306
333
|
getToolContext?: AgentLoopConfig["getToolContext"],
|
|
307
|
-
interruptMode
|
|
308
|
-
): Promise<{ toolResults: ToolResultMessage[];
|
|
334
|
+
interruptMode: AgentLoopConfig["interruptMode"] = "immediate",
|
|
335
|
+
): Promise<{ toolResults: ToolResultMessage[]; steeringMessages?: AgentMessage[] }> {
|
|
309
336
|
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
310
337
|
const results: ToolResultMessage[] = [];
|
|
311
|
-
let
|
|
338
|
+
let steeringMessages: AgentMessage[] | undefined;
|
|
339
|
+
const shouldInterruptImmediately = interruptMode !== "wait";
|
|
312
340
|
|
|
313
341
|
for (let index = 0; index < toolCalls.length; index++) {
|
|
314
342
|
const toolCall = toolCalls[index];
|
|
@@ -322,17 +350,14 @@ async function executeToolCalls(
|
|
|
322
350
|
});
|
|
323
351
|
|
|
324
352
|
let result: AgentToolResult<any>;
|
|
353
|
+
let isError = false;
|
|
325
354
|
|
|
326
|
-
const details: { toolCallId: string; toolName: string; isError?: boolean } = {
|
|
327
|
-
toolCallId: toolCall.id,
|
|
328
|
-
toolName: toolCall.name,
|
|
329
|
-
};
|
|
330
355
|
try {
|
|
331
356
|
if (!tool) throw new Error(`Tool ${toolCall.name} not found`);
|
|
332
357
|
|
|
333
358
|
const validatedArgs = validateToolArguments(tool, toolCall);
|
|
334
|
-
const toolContext = getToolContext?.();
|
|
335
359
|
|
|
360
|
+
const toolContext = getToolContext ? getToolContext() : undefined;
|
|
336
361
|
result = await tool.execute(
|
|
337
362
|
toolCall.id,
|
|
338
363
|
validatedArgs,
|
|
@@ -353,33 +378,36 @@ async function executeToolCalls(
|
|
|
353
378
|
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
354
379
|
details: {},
|
|
355
380
|
};
|
|
356
|
-
|
|
381
|
+
isError = true;
|
|
357
382
|
}
|
|
358
383
|
|
|
359
384
|
stream.push({
|
|
360
385
|
type: "tool_execution_end",
|
|
386
|
+
toolCallId: toolCall.id,
|
|
387
|
+
toolName: toolCall.name,
|
|
361
388
|
result,
|
|
362
|
-
|
|
389
|
+
isError,
|
|
363
390
|
});
|
|
364
391
|
|
|
365
392
|
const toolResultMessage: ToolResultMessage = {
|
|
366
393
|
role: "toolResult",
|
|
394
|
+
toolCallId: toolCall.id,
|
|
395
|
+
toolName: toolCall.name,
|
|
367
396
|
content: result.content,
|
|
368
397
|
details: result.details,
|
|
398
|
+
isError,
|
|
369
399
|
timestamp: Date.now(),
|
|
370
|
-
...details,
|
|
371
400
|
};
|
|
372
401
|
|
|
373
402
|
results.push(toolResultMessage);
|
|
374
403
|
stream.push({ type: "message_start", message: toolResultMessage });
|
|
375
404
|
stream.push({ type: "message_end", message: toolResultMessage });
|
|
376
405
|
|
|
377
|
-
// Check for
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
queuedMessages = queued;
|
|
406
|
+
// Check for steering messages - skip remaining tools if user interrupted
|
|
407
|
+
if (shouldInterruptImmediately && getSteeringMessages) {
|
|
408
|
+
const steering = await getSteeringMessages();
|
|
409
|
+
if (steering.length > 0) {
|
|
410
|
+
steeringMessages = steering;
|
|
383
411
|
const remainingCalls = toolCalls.slice(index + 1);
|
|
384
412
|
for (const skipped of remainingCalls) {
|
|
385
413
|
results.push(skipToolCall(skipped, stream));
|
|
@@ -389,7 +417,7 @@ async function executeToolCalls(
|
|
|
389
417
|
}
|
|
390
418
|
}
|
|
391
419
|
|
|
392
|
-
return { toolResults: results,
|
|
420
|
+
return { toolResults: results, steeringMessages };
|
|
393
421
|
}
|
|
394
422
|
|
|
395
423
|
function skipToolCall(
|
package/src/agent.ts
CHANGED
|
@@ -48,13 +48,19 @@ export interface AgentOptions {
|
|
|
48
48
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* Steering mode: "all" = send all steering messages at once, "one-at-a-time" = one per turn
|
|
52
52
|
*/
|
|
53
|
-
|
|
53
|
+
steeringMode?: "all" | "one-at-a-time";
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
|
|
56
|
+
* Follow-up mode: "all" = send all follow-up messages at once, "one-at-a-time" = one per turn
|
|
57
|
+
*/
|
|
58
|
+
followUpMode?: "all" | "one-at-a-time";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* When to interrupt tool execution for steering messages.
|
|
62
|
+
* - "immediate": check after each tool call (default)
|
|
63
|
+
* - "wait": defer steering until the current turn completes
|
|
58
64
|
*/
|
|
59
65
|
interruptMode?: "immediate" | "wait";
|
|
60
66
|
|
|
@@ -71,6 +77,7 @@ export interface AgentOptions {
|
|
|
71
77
|
|
|
72
78
|
/**
|
|
73
79
|
* Provides tool execution context, resolved per tool call.
|
|
80
|
+
* Use for late-bound UI or session state access.
|
|
74
81
|
*/
|
|
75
82
|
getToolContext?: () => AgentToolContext | undefined;
|
|
76
83
|
}
|
|
@@ -92,8 +99,10 @@ export class Agent {
|
|
|
92
99
|
private abortController?: AbortController;
|
|
93
100
|
private convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
94
101
|
private transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
95
|
-
private
|
|
96
|
-
private
|
|
102
|
+
private steeringQueue: AgentMessage[] = [];
|
|
103
|
+
private followUpQueue: AgentMessage[] = [];
|
|
104
|
+
private steeringMode: "all" | "one-at-a-time";
|
|
105
|
+
private followUpMode: "all" | "one-at-a-time";
|
|
97
106
|
private interruptMode: "immediate" | "wait";
|
|
98
107
|
public streamFn: StreamFn;
|
|
99
108
|
public getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
|
@@ -105,7 +114,8 @@ export class Agent {
|
|
|
105
114
|
this._state = { ...this._state, ...opts.initialState };
|
|
106
115
|
this.convertToLlm = opts.convertToLlm || defaultConvertToLlm;
|
|
107
116
|
this.transformContext = opts.transformContext;
|
|
108
|
-
this.
|
|
117
|
+
this.steeringMode = opts.steeringMode || "one-at-a-time";
|
|
118
|
+
this.followUpMode = opts.followUpMode || "one-at-a-time";
|
|
109
119
|
this.interruptMode = opts.interruptMode || "immediate";
|
|
110
120
|
this.streamFn = opts.streamFn || streamSimple;
|
|
111
121
|
this.getApiKey = opts.getApiKey;
|
|
@@ -134,12 +144,20 @@ export class Agent {
|
|
|
134
144
|
this._state.thinkingLevel = l;
|
|
135
145
|
}
|
|
136
146
|
|
|
137
|
-
|
|
138
|
-
this.
|
|
147
|
+
setSteeringMode(mode: "all" | "one-at-a-time") {
|
|
148
|
+
this.steeringMode = mode;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getSteeringMode(): "all" | "one-at-a-time" {
|
|
152
|
+
return this.steeringMode;
|
|
139
153
|
}
|
|
140
154
|
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
setFollowUpMode(mode: "all" | "one-at-a-time") {
|
|
156
|
+
this.followUpMode = mode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getFollowUpMode(): "all" | "one-at-a-time" {
|
|
160
|
+
return this.followUpMode;
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
setInterruptMode(mode: "immediate" | "wait") {
|
|
@@ -162,24 +180,49 @@ export class Agent {
|
|
|
162
180
|
this._state.messages = [...this._state.messages, m];
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
|
|
166
|
-
this.
|
|
183
|
+
popMessage(): AgentMessage | undefined {
|
|
184
|
+
const messages = this._state.messages.slice(0, -1);
|
|
185
|
+
const removed = this._state.messages.at(-1);
|
|
186
|
+
this._state.messages = messages;
|
|
187
|
+
|
|
188
|
+
if (removed && this._state.streamMessage === removed) {
|
|
189
|
+
this._state.streamMessage = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return removed;
|
|
167
193
|
}
|
|
168
194
|
|
|
169
|
-
|
|
170
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Queue a steering message to interrupt the agent mid-run.
|
|
197
|
+
* Delivered after current tool execution, skips remaining tools.
|
|
198
|
+
*/
|
|
199
|
+
steer(m: AgentMessage) {
|
|
200
|
+
this.steeringQueue.push(m);
|
|
171
201
|
}
|
|
172
202
|
|
|
173
|
-
|
|
174
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Queue a follow-up message to be processed after the agent finishes.
|
|
205
|
+
* Delivered only when agent has no more tool calls or steering messages.
|
|
206
|
+
*/
|
|
207
|
+
followUp(m: AgentMessage) {
|
|
208
|
+
this.followUpQueue.push(m);
|
|
175
209
|
}
|
|
176
210
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
clearSteeringQueue() {
|
|
212
|
+
this.steeringQueue = [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
clearFollowUpQueue() {
|
|
216
|
+
this.followUpQueue = [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
clearAllQueues() {
|
|
220
|
+
this.steeringQueue = [];
|
|
221
|
+
this.followUpQueue = [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
clearMessages() {
|
|
225
|
+
this._state.messages = [];
|
|
183
226
|
}
|
|
184
227
|
|
|
185
228
|
abort() {
|
|
@@ -196,13 +239,20 @@ export class Agent {
|
|
|
196
239
|
this._state.streamMessage = null;
|
|
197
240
|
this._state.pendingToolCalls = new Set<string>();
|
|
198
241
|
this._state.error = undefined;
|
|
199
|
-
this.
|
|
242
|
+
this.steeringQueue = [];
|
|
243
|
+
this.followUpQueue = [];
|
|
200
244
|
}
|
|
201
245
|
|
|
202
246
|
/** Send a prompt with an AgentMessage */
|
|
203
247
|
async prompt(message: AgentMessage | AgentMessage[]): Promise<void>;
|
|
204
248
|
async prompt(input: string, images?: ImageContent[]): Promise<void>;
|
|
205
249
|
async prompt(input: string | AgentMessage | AgentMessage[], images?: ImageContent[]) {
|
|
250
|
+
if (this._state.isStreaming) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
"Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
206
256
|
const model = this._state.model;
|
|
207
257
|
if (!model) throw new Error("No model configured");
|
|
208
258
|
|
|
@@ -231,6 +281,10 @@ export class Agent {
|
|
|
231
281
|
|
|
232
282
|
/** Continue from current context (for retry after overflow) */
|
|
233
283
|
async continue() {
|
|
284
|
+
if (this._state.isStreaming) {
|
|
285
|
+
throw new Error("Agent is already processing. Wait for completion before continuing.");
|
|
286
|
+
}
|
|
287
|
+
|
|
234
288
|
const messages = this._state.messages;
|
|
235
289
|
if (messages.length === 0) {
|
|
236
290
|
throw new Error("No messages to continue from");
|
|
@@ -276,23 +330,37 @@ export class Agent {
|
|
|
276
330
|
const config: AgentLoopConfig = {
|
|
277
331
|
model,
|
|
278
332
|
reasoning,
|
|
333
|
+
interruptMode: this.interruptMode,
|
|
279
334
|
convertToLlm: this.convertToLlm,
|
|
280
335
|
transformContext: this.transformContext,
|
|
281
336
|
getApiKey: this.getApiKey,
|
|
282
337
|
getToolContext: this.getToolContext,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
338
|
+
getSteeringMessages: async () => {
|
|
339
|
+
if (this.steeringMode === "one-at-a-time") {
|
|
340
|
+
if (this.steeringQueue.length > 0) {
|
|
341
|
+
const first = this.steeringQueue[0];
|
|
342
|
+
this.steeringQueue = this.steeringQueue.slice(1);
|
|
343
|
+
return [first];
|
|
344
|
+
}
|
|
345
|
+
return [];
|
|
346
|
+
} else {
|
|
347
|
+
const steering = this.steeringQueue.slice();
|
|
348
|
+
this.steeringQueue = [];
|
|
349
|
+
return steering;
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
getFollowUpMessages: async () => {
|
|
353
|
+
if (this.followUpMode === "one-at-a-time") {
|
|
354
|
+
if (this.followUpQueue.length > 0) {
|
|
355
|
+
const first = this.followUpQueue[0];
|
|
356
|
+
this.followUpQueue = this.followUpQueue.slice(1);
|
|
289
357
|
return [first];
|
|
290
358
|
}
|
|
291
359
|
return [];
|
|
292
360
|
} else {
|
|
293
|
-
const
|
|
294
|
-
this.
|
|
295
|
-
return
|
|
361
|
+
const followUp = this.followUpQueue.slice();
|
|
362
|
+
this.followUpQueue = [];
|
|
363
|
+
return followUp;
|
|
296
364
|
}
|
|
297
365
|
},
|
|
298
366
|
};
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,13 @@ export type StreamFn = (
|
|
|
22
22
|
export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
23
23
|
model: Model<any>;
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* When to interrupt tool execution for steering messages.
|
|
27
|
+
* - "immediate": check after each tool call (default)
|
|
28
|
+
* - "wait": defer steering until the current turn completes
|
|
29
|
+
*/
|
|
30
|
+
interruptMode?: "immediate" | "wait";
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.
|
|
27
34
|
*
|
|
@@ -32,7 +39,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
32
39
|
* @example
|
|
33
40
|
* ```typescript
|
|
34
41
|
* convertToLlm: (messages) => messages.flatMap(m => {
|
|
35
|
-
* if (m.role === "
|
|
42
|
+
* if (m.role === "custom") {
|
|
36
43
|
* // Convert custom message to user message
|
|
37
44
|
* return [{ role: "user", content: m.content, timestamp: m.timestamp }];
|
|
38
45
|
* }
|
|
@@ -75,20 +82,22 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
75
82
|
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
|
76
83
|
|
|
77
84
|
/**
|
|
78
|
-
* Returns
|
|
85
|
+
* Returns steering messages to inject into the conversation mid-run.
|
|
79
86
|
*
|
|
80
|
-
* Called after each
|
|
81
|
-
* If messages are returned,
|
|
87
|
+
* Called after each tool execution to check for user interruptions unless interruptMode is "wait".
|
|
88
|
+
* If messages are returned, remaining tool calls are skipped and
|
|
89
|
+
* these messages are added to the context before the next LLM call.
|
|
82
90
|
*/
|
|
83
|
-
|
|
91
|
+
getSteeringMessages?: () => Promise<AgentMessage[]>;
|
|
84
92
|
|
|
85
93
|
/**
|
|
86
|
-
*
|
|
94
|
+
* Returns follow-up messages to process after the agent would otherwise stop.
|
|
87
95
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
96
|
+
* Called when the agent has no more tool calls and no steering messages.
|
|
97
|
+
* If messages are returned, they're added to the context and the agent
|
|
98
|
+
* continues with another turn.
|
|
90
99
|
*/
|
|
91
|
-
|
|
100
|
+
getFollowUpMessages?: () => Promise<AgentMessage[]>;
|
|
92
101
|
|
|
93
102
|
/**
|
|
94
103
|
* Provides tool execution context, resolved per tool call.
|
|
@@ -159,6 +168,8 @@ export interface RenderResultOptions {
|
|
|
159
168
|
expanded: boolean;
|
|
160
169
|
/** Whether this is a partial/streaming result */
|
|
161
170
|
isPartial: boolean;
|
|
171
|
+
/** Current spinner frame index for animated elements (optional) */
|
|
172
|
+
spinnerFrame?: number;
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
/**
|