@codeworksh/aikit 0.1.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 +347 -0
- package/package.json +60 -0
- package/src/agent/agent.ts +75 -0
- package/src/agent/loop.ts +786 -0
- package/src/agent.ts +11 -0
- package/src/event/event.ts +271 -0
- package/src/index.ts +10 -0
- package/src/llm.ts +29 -0
- package/src/message/message.ts +280 -0
- package/src/model/catalog.ts +95 -0
- package/src/model/model.ts +253 -0
- package/src/provider/provider.ts +43 -0
- package/src/provider/providers/anthropic/index.ts +769 -0
- package/src/provider/register.ts +14 -0
- package/src/provider/stream.ts +299 -0
- package/src/stream.ts +72 -0
- package/src/utils/eventstream.ts +95 -0
- package/src/utils/helpers.ts +24 -0
- package/src/utils/jsonparse.ts +32 -0
- package/src/utils/overflow.ts +121 -0
- package/src/utils/sanitize.ts +25 -0
- package/src/utils/validation.ts +63 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import type { Event } from "../event/event";
|
|
2
|
+
import type { Message } from "../message/message";
|
|
3
|
+
import type { Model } from "../model/model";
|
|
4
|
+
import type { Provider } from "../provider/provider";
|
|
5
|
+
import { Stream } from "../provider/stream";
|
|
6
|
+
import { EventStream } from "../utils/eventstream";
|
|
7
|
+
import { validateToolArguments } from "../utils/validation";
|
|
8
|
+
import type { Agent } from "./agent";
|
|
9
|
+
|
|
10
|
+
export namespace Loop {
|
|
11
|
+
/** Stream function - can return sync or Promise for async config lookup */
|
|
12
|
+
export type StreamFn = (
|
|
13
|
+
...args: Parameters<typeof Stream.streamSimple>
|
|
14
|
+
) => ReturnType<typeof Stream.streamSimple> | Promise<ReturnType<typeof Stream.streamSimple>>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration for how tool calls from a single assistant message are executed.
|
|
18
|
+
*
|
|
19
|
+
* - "sequential": each tool call is prepared, executed, and finalized before the next one starts.
|
|
20
|
+
* - "parallel": tool calls are prepared sequentially, then allowed tools execute concurrently.
|
|
21
|
+
* Final tool results are still emitted in assistant source order.
|
|
22
|
+
*/
|
|
23
|
+
export type ToolExecutionMode = "sequential" | "parallel";
|
|
24
|
+
|
|
25
|
+
/** Context passed to `beforeToolExecution`. */
|
|
26
|
+
export interface BeforeToolExecutionContext {
|
|
27
|
+
/** The current context at the time the tool call is prepared */
|
|
28
|
+
context: Agent.AgentContext;
|
|
29
|
+
/** The assistant message that requested the tool call. */
|
|
30
|
+
assistantMessage: Message.AssistantMessage;
|
|
31
|
+
/** The validated in-flight tool call with validated arguments */
|
|
32
|
+
toolCall: Agent.ToolCallInFlight;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Context passed to `afterToolExecution`. */
|
|
36
|
+
export interface AfterToolExecutionContext {
|
|
37
|
+
/** Current agent context at the time the tool call is finalized. */
|
|
38
|
+
context: Agent.AgentContext;
|
|
39
|
+
/** The assistant message that requested the tool call. */
|
|
40
|
+
assistantMessage: Message.AssistantMessage;
|
|
41
|
+
/** The validated in-flight tool call with validated arguments */
|
|
42
|
+
toolCall: Agent.ToolCallInFlight;
|
|
43
|
+
/** The executed tool result before any `afterToolExecution` overrides are applied. */
|
|
44
|
+
result: Agent.ToolTerminalResult<any>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result returned from `beforeToolExecution`.
|
|
49
|
+
*
|
|
50
|
+
* Returning `{ block: true }` prevents the tool from executing. The loop emits an error tool result instead.
|
|
51
|
+
* `reason` becomes the text shown in that error result. If omitted, a default blocked message is used.
|
|
52
|
+
*/
|
|
53
|
+
export interface BeforeToolCallResult<T = any> {
|
|
54
|
+
block?: boolean;
|
|
55
|
+
reason?: string;
|
|
56
|
+
details?: T;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Configuration for the agent loop. */
|
|
60
|
+
export interface Config extends Stream.SimpleOptions {
|
|
61
|
+
model: Model.Value;
|
|
62
|
+
convertToLlm: (messages: Message.Message[]) => Message.Message[] | Promise<Message.Message[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Optional transform applied to the context before `convertToLlm`.
|
|
65
|
+
*
|
|
66
|
+
* Use this for operations that work at the AgentMessage level:
|
|
67
|
+
* - Context window management (pruning old messages)
|
|
68
|
+
* - Injecting context from external sources
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* transformContext: async (messages) => {
|
|
73
|
+
* if (estimateTokens(messages) > MAX_TOKENS) {
|
|
74
|
+
* return pruneOldMessages(messages);
|
|
75
|
+
* }
|
|
76
|
+
* return messages;
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
transformContext?: (messages: Message.Message[], signal?: AbortSignal) => Promise<Message.Message[]>;
|
|
81
|
+
/**
|
|
82
|
+
* Resolves an API key dynamically for each LLM call.
|
|
83
|
+
*
|
|
84
|
+
* Useful for short-lived OAuth tokens (e.g., GitHub Copilot) that may expire
|
|
85
|
+
* during long-running tool execution phases.
|
|
86
|
+
*/
|
|
87
|
+
getApiKey?: (provider: Provider.Info) => Promise<string | undefined> | string | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Returns steering messages to inject into the conversation mid-run.
|
|
90
|
+
*
|
|
91
|
+
* Called after each tool execution to check for user interruptions.
|
|
92
|
+
* If messages are returned, remaining tool calls are skipped and
|
|
93
|
+
* these messages are added to the context before the next LLM call.
|
|
94
|
+
*
|
|
95
|
+
* Use this for "steering" the agent while it's working.
|
|
96
|
+
*/
|
|
97
|
+
getSteeringMessages?: () => Promise<Message.UserMessage[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Returns follow-up messages to process after the agent would otherwise stop.
|
|
100
|
+
*
|
|
101
|
+
* Called when the agent has no more tool calls and no steering messages.
|
|
102
|
+
* If messages are returned, they're added to the context and the agent
|
|
103
|
+
* continues with another turn.
|
|
104
|
+
*
|
|
105
|
+
* Use this for follow-up messages that should wait until the agent finishes.
|
|
106
|
+
*/
|
|
107
|
+
getFollowUpMessages?: () => Promise<Message.UserMessage[]>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tool execution mode.
|
|
111
|
+
* - "sequential": execute tool calls one by one
|
|
112
|
+
* - "parallel": preflight tool calls sequentially, then execute allowed tools concurrently
|
|
113
|
+
*
|
|
114
|
+
* Default: "parallel"
|
|
115
|
+
*/
|
|
116
|
+
toolExecution?: ToolExecutionMode;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Called before a tool is executed, after arguments have been validated.
|
|
120
|
+
*
|
|
121
|
+
* Return `{ block: true }` to prevent execution. The loop emits an error tool result instead.
|
|
122
|
+
* The hook receives the agent abort signal and is responsible for honoring it.
|
|
123
|
+
*/
|
|
124
|
+
beforeToolExecution?: (
|
|
125
|
+
context: BeforeToolExecutionContext,
|
|
126
|
+
signal?: AbortSignal,
|
|
127
|
+
) => Promise<BeforeToolCallResult | undefined>;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Called after a tool finishes executing, before final tool events are emitted.
|
|
131
|
+
*
|
|
132
|
+
* Any omitted fields keep their original values. No deep merge is performed.
|
|
133
|
+
* The hook receives the agent abort signal and is responsible for honoring it.
|
|
134
|
+
*/
|
|
135
|
+
afterToolExecution?: (
|
|
136
|
+
context: AfterToolExecutionContext,
|
|
137
|
+
signal?: AbortSignal,
|
|
138
|
+
) => Promise<Agent.ToolTerminalResult<any> | undefined>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createAgentStream(): EventStream<Event.AgentEvent, Message.Message[]> {
|
|
142
|
+
return new EventStream<Event.AgentEvent, Message.Message[]>(
|
|
143
|
+
(event: Event.AgentEvent) => event.type === "agent.end",
|
|
144
|
+
(event: Event.AgentEvent) => (event.type === "agent.end" ? event.messages : []),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type AssistantPart = Message.AssistantMessage["parts"][number];
|
|
149
|
+
|
|
150
|
+
type ToolCallPart = Message.ToolCallPendingPart & { partIndex: number }; // runtime toolcall pending part
|
|
151
|
+
|
|
152
|
+
type ToolCallPrepared =
|
|
153
|
+
| {
|
|
154
|
+
error: {
|
|
155
|
+
kind: "error" | "blocked";
|
|
156
|
+
message: string;
|
|
157
|
+
details?: any;
|
|
158
|
+
};
|
|
159
|
+
runnable: Agent.ToolCallInFlight;
|
|
160
|
+
}
|
|
161
|
+
| {
|
|
162
|
+
error?: never;
|
|
163
|
+
runnable: Agent.ToolCallInFlight;
|
|
164
|
+
tool: Agent.AnyAgentTool;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
type ToolCallRunnable = {
|
|
168
|
+
toolCall: ToolCallPart;
|
|
169
|
+
runnable: Agent.ToolCallInFlight;
|
|
170
|
+
tool: Agent.AnyAgentTool;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type ToolCallExecutionResult<T = any> =
|
|
174
|
+
| { result: Agent.ToolTerminalResult<T>; error?: never }
|
|
175
|
+
| { result?: never; error: { message: string; details?: any } };
|
|
176
|
+
|
|
177
|
+
function snapshotAssistantMessage(message: Message.AssistantMessage): Message.AssistantMessage {
|
|
178
|
+
return structuredClone(message);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function snapshotAssistantPartEvent(
|
|
182
|
+
message: Message.AssistantMessage,
|
|
183
|
+
partIndex: number,
|
|
184
|
+
): { message: Message.AssistantMessage; part: AssistantPart } {
|
|
185
|
+
const snapshot = snapshotAssistantMessage(message);
|
|
186
|
+
const part = snapshot.parts[partIndex];
|
|
187
|
+
if (!part) {
|
|
188
|
+
throw new Error(`Assistant message part at index ${partIndex} was not found`);
|
|
189
|
+
}
|
|
190
|
+
return { message: snapshot, part };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function streamAssistantResponse(
|
|
194
|
+
config: Config,
|
|
195
|
+
context: Agent.AgentContext,
|
|
196
|
+
stream: EventStream<Event.AgentEvent, Message.Message[]>,
|
|
197
|
+
streamFn?: StreamFn,
|
|
198
|
+
signal?: AbortSignal,
|
|
199
|
+
): Promise<Message.AssistantMessage> {
|
|
200
|
+
// FIXME @sanchitrk:
|
|
201
|
+
// Have something like a pipeline with Message.Message[] transformation
|
|
202
|
+
// caller will inject transfomers[](async functions) that is then invoked in order
|
|
203
|
+
|
|
204
|
+
// Apply context transform if configured (Message.Message[] → Message.Message[])
|
|
205
|
+
let messages = context.messages;
|
|
206
|
+
if (config.transformContext) {
|
|
207
|
+
messages = await config.transformContext(messages, signal);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Convert to LLM-compatible messages (Message.Message[] → Message.Message[])
|
|
211
|
+
const llmMessages = await config.convertToLlm(messages);
|
|
212
|
+
|
|
213
|
+
// Build LLM context, subset of Agent.AgentContext
|
|
214
|
+
const llmContext: Agent.AgentContext = {
|
|
215
|
+
systemPrompt: context.systemPrompt,
|
|
216
|
+
messages: llmMessages,
|
|
217
|
+
tools: context.tools,
|
|
218
|
+
};
|
|
219
|
+
const streamFunction = streamFn || Stream.streamSimple;
|
|
220
|
+
// Resolve API key (important for expiring tokens)
|
|
221
|
+
// TODO @sanchitrk:
|
|
222
|
+
// improve passing of credentials (key/oauth), provider also has resolve api key,
|
|
223
|
+
// for now its dummy implementation.
|
|
224
|
+
const resolvedApiKey =
|
|
225
|
+
(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
|
|
226
|
+
|
|
227
|
+
const events = await streamFunction(config.model, llmContext, {
|
|
228
|
+
...config,
|
|
229
|
+
apiKey: resolvedApiKey,
|
|
230
|
+
signal,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
let partialMessage: Message.AssistantMessage | null = null;
|
|
234
|
+
let started = false;
|
|
235
|
+
|
|
236
|
+
const ensureMessageStarted = (message: Message.AssistantMessage): void => {
|
|
237
|
+
if (started) return;
|
|
238
|
+
started = true;
|
|
239
|
+
stream.push({ type: "message.start", message: snapshotAssistantMessage(message) });
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const emitMessageUpdate = (message: Message.AssistantMessage): void => {
|
|
243
|
+
stream.push({ type: "message.update", message: snapshotAssistantMessage(message) });
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const emitPartStart = (message: Message.AssistantMessage, partIndex: number): void => {
|
|
247
|
+
const snapshot = snapshotAssistantPartEvent(message, partIndex);
|
|
248
|
+
stream.push({ type: "message.part.start", partIndex, ...snapshot });
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const emitPartUpdate = (message: Message.AssistantMessage, partIndex: number): void => {
|
|
252
|
+
const snapshot = snapshotAssistantPartEvent(message, partIndex);
|
|
253
|
+
stream.push({ type: "message.part.update", partIndex, ...snapshot, source: "llm" });
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const emitPartEnd = (message: Message.AssistantMessage, partIndex: number): void => {
|
|
257
|
+
const snapshot = snapshotAssistantPartEvent(message, partIndex);
|
|
258
|
+
stream.push({ type: "message.part.end", partIndex, ...snapshot });
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// NOTE: Do not mutate context directly
|
|
262
|
+
// once all the events are done return message
|
|
263
|
+
// mutate partialMessage and corresponding parts as required
|
|
264
|
+
// return the final built message form llm events
|
|
265
|
+
for await (const event of events) {
|
|
266
|
+
switch (event.type) {
|
|
267
|
+
case "start": {
|
|
268
|
+
partialMessage = event.partial;
|
|
269
|
+
ensureMessageStarted(partialMessage);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case "text.start":
|
|
273
|
+
case "thinking.start":
|
|
274
|
+
case "toolcall.start": {
|
|
275
|
+
partialMessage = event.partial;
|
|
276
|
+
ensureMessageStarted(partialMessage);
|
|
277
|
+
emitPartStart(partialMessage, event.partIndex);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "text.delta":
|
|
281
|
+
case "thinking.delta":
|
|
282
|
+
case "toolcall.delta": {
|
|
283
|
+
partialMessage = event.partial;
|
|
284
|
+
ensureMessageStarted(partialMessage);
|
|
285
|
+
emitPartUpdate(partialMessage, event.partIndex);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case "text.end":
|
|
289
|
+
case "thinking.end":
|
|
290
|
+
case "toolcall.end": {
|
|
291
|
+
partialMessage = event.partial;
|
|
292
|
+
ensureMessageStarted(partialMessage);
|
|
293
|
+
emitPartEnd(partialMessage, event.partIndex);
|
|
294
|
+
emitMessageUpdate(partialMessage);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case "done": {
|
|
298
|
+
ensureMessageStarted(event.message);
|
|
299
|
+
stream.push({ type: "message.end", message: snapshotAssistantMessage(event.message) });
|
|
300
|
+
return event.message;
|
|
301
|
+
}
|
|
302
|
+
case "error": {
|
|
303
|
+
ensureMessageStarted(event.error);
|
|
304
|
+
stream.push({ type: "message.end", message: snapshotAssistantMessage(event.error) });
|
|
305
|
+
return event.error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const finalMessage = await events.result();
|
|
311
|
+
ensureMessageStarted(finalMessage);
|
|
312
|
+
stream.push({ type: "message.end", message: snapshotAssistantMessage(finalMessage) });
|
|
313
|
+
return finalMessage;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function runLoop(
|
|
317
|
+
config: Config,
|
|
318
|
+
currentContext: Agent.AgentContext,
|
|
319
|
+
newMessages: Message.Message[],
|
|
320
|
+
stream: EventStream<Event.AgentEvent, Message.Message[]>,
|
|
321
|
+
streamFn?: StreamFn,
|
|
322
|
+
signal?: AbortSignal,
|
|
323
|
+
): Promise<void> {
|
|
324
|
+
// initial `turn.start` is already emitted
|
|
325
|
+
// avoiding duplicate `turn.start` emits
|
|
326
|
+
let firstTurn = true;
|
|
327
|
+
// Check for steering messages at start (user may have submitted while waiting)
|
|
328
|
+
let pendingMessages: Message.UserMessage[] = (await config.getSteeringMessages?.()) || [];
|
|
329
|
+
|
|
330
|
+
// Outer loop: continues when queued follow-up messages arrive after agent would stop
|
|
331
|
+
while (true) {
|
|
332
|
+
let hasMoreToolCalls = true;
|
|
333
|
+
|
|
334
|
+
// Inner loop: process tool calls and steering messages
|
|
335
|
+
// Note: turn.start for full tool calls / pending messages.
|
|
336
|
+
while (hasMoreToolCalls || pendingMessages.length > 0) {
|
|
337
|
+
// flip firstTurn, so next iterations starts emitting `turn.start` event
|
|
338
|
+
if (!firstTurn) {
|
|
339
|
+
stream.push({ type: "turn.start" });
|
|
340
|
+
} else {
|
|
341
|
+
firstTurn = false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Process pending messages (inject before next assistant response)
|
|
345
|
+
if (pendingMessages.length > 0) {
|
|
346
|
+
for (const message of pendingMessages) {
|
|
347
|
+
stream.push({ type: "message.start", message: message });
|
|
348
|
+
for (const [partIndex, part] of message.parts.entries()) {
|
|
349
|
+
stream.push({ type: "message.part.start", message: message, partIndex, part });
|
|
350
|
+
stream.push({ type: "message.part.end", message: message, partIndex, part });
|
|
351
|
+
stream.push({ type: "message.update", message: message });
|
|
352
|
+
}
|
|
353
|
+
stream.push({ type: "message.end", message: message });
|
|
354
|
+
currentContext.messages.push(message);
|
|
355
|
+
newMessages.push(message);
|
|
356
|
+
}
|
|
357
|
+
pendingMessages = [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Run llm and stream assistant response
|
|
361
|
+
const message: Message.AssistantMessage = await streamAssistantResponse(
|
|
362
|
+
config,
|
|
363
|
+
currentContext,
|
|
364
|
+
stream,
|
|
365
|
+
streamFn,
|
|
366
|
+
signal,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// terminal state
|
|
370
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
371
|
+
currentContext.messages.push(message); // append the terminal message into the context
|
|
372
|
+
newMessages.push(message); // append the terminal message
|
|
373
|
+
|
|
374
|
+
stream.push({ type: "turn.end", message });
|
|
375
|
+
stream.push({ type: "agent.end", messages: newMessages });
|
|
376
|
+
stream.end(newMessages);
|
|
377
|
+
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// check for pending tool calls
|
|
382
|
+
// use partIndex for in-place mutation for tool call result parts in message.parts
|
|
383
|
+
const toolCalls: ToolCallPart[] = message.parts
|
|
384
|
+
.map((part, index) => ({ ...part, partIndex: index }))
|
|
385
|
+
.filter((part) => part.type === "toolCall" && part.status === "pending");
|
|
386
|
+
|
|
387
|
+
hasMoreToolCalls = toolCalls.length > 0;
|
|
388
|
+
if (hasMoreToolCalls) {
|
|
389
|
+
await executeToolCalls(config, currentContext, message, toolCalls, stream, signal);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
currentContext.messages.push(message);
|
|
393
|
+
newMessages.push(message);
|
|
394
|
+
|
|
395
|
+
stream.push({ type: "turn.end", message });
|
|
396
|
+
|
|
397
|
+
//
|
|
398
|
+
// check for steering messages while turn loop was working.
|
|
399
|
+
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Agent would stop here. Check for follow-up messages.
|
|
403
|
+
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
|
|
404
|
+
if (followUpMessages.length > 0) {
|
|
405
|
+
// Set as pending so inner loop processes them
|
|
406
|
+
pendingMessages = followUpMessages;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// No more messages, exit
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
stream.push({ type: "agent.end", messages: newMessages });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function executeToolCalls(
|
|
418
|
+
config: Config,
|
|
419
|
+
currentContext: Agent.AgentContext,
|
|
420
|
+
message: Message.AssistantMessage,
|
|
421
|
+
toolCalls: ToolCallPart[],
|
|
422
|
+
stream: EventStream<Event.AgentEvent, Message.Message[]>,
|
|
423
|
+
signal?: AbortSignal,
|
|
424
|
+
): Promise<void> {
|
|
425
|
+
if (config.toolExecution === "sequential")
|
|
426
|
+
return executeToolCallsSequential(config, currentContext, message, toolCalls, stream, signal);
|
|
427
|
+
return executeToolCallsParallel(config, currentContext, message, toolCalls, stream, signal);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function executeToolCallsSequential(
|
|
431
|
+
config: Config,
|
|
432
|
+
currentContext: Agent.AgentContext,
|
|
433
|
+
message: Message.AssistantMessage,
|
|
434
|
+
toolCalls: ToolCallPart[],
|
|
435
|
+
stream: EventStream<Event.AgentEvent, Message.Message[]>,
|
|
436
|
+
signal?: AbortSignal,
|
|
437
|
+
): Promise<void> {
|
|
438
|
+
for (const toolCall of toolCalls) {
|
|
439
|
+
const toolCallInFlight: Agent.ToolCallInFlight = {
|
|
440
|
+
callID: toolCall.callID,
|
|
441
|
+
name: toolCall.name,
|
|
442
|
+
rawArgs: toolCall.arguments,
|
|
443
|
+
};
|
|
444
|
+
stream.push({
|
|
445
|
+
type: "tool.execution.start",
|
|
446
|
+
...toolCallInFlight,
|
|
447
|
+
});
|
|
448
|
+
//
|
|
449
|
+
// Prepares the tool call for execution.
|
|
450
|
+
// Invokes a pre-execution callback with the current context, allowing
|
|
451
|
+
// the callback to either continue or block (short-circuiting the execution).
|
|
452
|
+
// Mutates `message.parts` in-place to update the pending tool call with its result.
|
|
453
|
+
const prepared = await prepareToolCall(config, currentContext, message, toolCallInFlight, signal);
|
|
454
|
+
if (prepared.error) {
|
|
455
|
+
const error: Agent.ToolErrorResult<any> = {
|
|
456
|
+
status: "error",
|
|
457
|
+
result: {
|
|
458
|
+
content: [{ type: "text", text: prepared.error.message }],
|
|
459
|
+
details: prepared.error.details,
|
|
460
|
+
isError: true,
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
await emitToolCallTerminalResult(toolCall, prepared.runnable, message, stream, error);
|
|
464
|
+
} else {
|
|
465
|
+
const runnable: ToolCallRunnable = {
|
|
466
|
+
toolCall,
|
|
467
|
+
runnable: prepared.runnable,
|
|
468
|
+
tool: prepared.tool,
|
|
469
|
+
};
|
|
470
|
+
const executed = await executeRunnableToolCall(runnable, message, stream, signal);
|
|
471
|
+
if (executed.error) {
|
|
472
|
+
// tool call invocation failed with error
|
|
473
|
+
// create tool error result
|
|
474
|
+
const error: Agent.ToolErrorResult<any> = {
|
|
475
|
+
status: "error",
|
|
476
|
+
result: {
|
|
477
|
+
content: [{ type: "text", text: executed.error.message }],
|
|
478
|
+
details: executed.error.details,
|
|
479
|
+
isError: true,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
await emitToolCallTerminalResult(runnable.toolCall, runnable.runnable, message, stream, error);
|
|
483
|
+
} else {
|
|
484
|
+
// finalize results from tool invocation
|
|
485
|
+
// invokes after tool exection callback
|
|
486
|
+
await finalizeExecutedToolCall(
|
|
487
|
+
config,
|
|
488
|
+
currentContext,
|
|
489
|
+
runnable.toolCall,
|
|
490
|
+
runnable.runnable,
|
|
491
|
+
message,
|
|
492
|
+
stream,
|
|
493
|
+
executed.result,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function executeToolCallsParallel(
|
|
501
|
+
config: Config,
|
|
502
|
+
currentContext: Agent.AgentContext,
|
|
503
|
+
message: Message.AssistantMessage,
|
|
504
|
+
toolCalls: ToolCallPart[],
|
|
505
|
+
stream: EventStream<Event.AgentEvent, Message.Message[]>,
|
|
506
|
+
signal?: AbortSignal,
|
|
507
|
+
): Promise<void> {
|
|
508
|
+
const runnables: ToolCallRunnable[] = [];
|
|
509
|
+
|
|
510
|
+
for (const toolCall of toolCalls) {
|
|
511
|
+
const toolCallInFlight: Agent.ToolCallInFlight = {
|
|
512
|
+
callID: toolCall.callID,
|
|
513
|
+
name: toolCall.name,
|
|
514
|
+
rawArgs: toolCall.arguments,
|
|
515
|
+
};
|
|
516
|
+
stream.push({
|
|
517
|
+
type: "tool.execution.start",
|
|
518
|
+
...toolCallInFlight,
|
|
519
|
+
});
|
|
520
|
+
//
|
|
521
|
+
// Prepares the tool call for execution.
|
|
522
|
+
// Invokes a pre-execution callback with the current context, allowing
|
|
523
|
+
// the callback to either continue or block (short-circuiting the execution).
|
|
524
|
+
// Mutates `message.parts` in-place to update the pending tool call with its result.
|
|
525
|
+
const prepared = await prepareToolCall(config, currentContext, message, toolCallInFlight, signal);
|
|
526
|
+
if (prepared.error) {
|
|
527
|
+
const error: Agent.ToolErrorResult<any> = {
|
|
528
|
+
status: "error",
|
|
529
|
+
result: {
|
|
530
|
+
content: [{ type: "text", text: prepared.error.message }],
|
|
531
|
+
details: prepared.error.details,
|
|
532
|
+
isError: true,
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
await emitToolCallTerminalResult(toolCall, prepared.runnable, message, stream, error);
|
|
536
|
+
} else {
|
|
537
|
+
runnables.push({
|
|
538
|
+
toolCall,
|
|
539
|
+
runnable: prepared.runnable,
|
|
540
|
+
tool: prepared.tool,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const runningExecutions = runnables.map((runnable) => ({
|
|
546
|
+
runnable,
|
|
547
|
+
execution: executeRunnableToolCall(runnable, message, stream, signal),
|
|
548
|
+
}));
|
|
549
|
+
|
|
550
|
+
for (const running of runningExecutions) {
|
|
551
|
+
const executed = await running.execution;
|
|
552
|
+
const { runnable } = running;
|
|
553
|
+
if (executed.error) {
|
|
554
|
+
// tool call invocation failed with error
|
|
555
|
+
// create tool error result
|
|
556
|
+
const error: Agent.ToolErrorResult<any> = {
|
|
557
|
+
status: "error",
|
|
558
|
+
result: {
|
|
559
|
+
content: [{ type: "text", text: executed.error.message }],
|
|
560
|
+
details: executed.error.details,
|
|
561
|
+
isError: true,
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
await emitToolCallTerminalResult(runnable.toolCall, runnable.runnable, message, stream, error);
|
|
565
|
+
} else {
|
|
566
|
+
// finalize results from tool invocation
|
|
567
|
+
// invokes after tool exection callback
|
|
568
|
+
await finalizeExecutedToolCall(
|
|
569
|
+
config,
|
|
570
|
+
currentContext,
|
|
571
|
+
runnable.toolCall,
|
|
572
|
+
runnable.runnable,
|
|
573
|
+
message,
|
|
574
|
+
stream,
|
|
575
|
+
executed.result,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function prepareToolCall(
|
|
582
|
+
config: Config,
|
|
583
|
+
currentContext: Agent.AgentContext,
|
|
584
|
+
assistantMessage: Message.AssistantMessage,
|
|
585
|
+
toolCallInFlight: Agent.ToolCallInFlight,
|
|
586
|
+
signal?: AbortSignal,
|
|
587
|
+
): Promise<ToolCallPrepared> {
|
|
588
|
+
const tool = currentContext.tools?.find((t) => t.name === toolCallInFlight.name);
|
|
589
|
+
if (!tool) {
|
|
590
|
+
return {
|
|
591
|
+
error: {
|
|
592
|
+
kind: "error",
|
|
593
|
+
message: `Tool ${toolCallInFlight.name} Not Found`,
|
|
594
|
+
},
|
|
595
|
+
runnable: toolCallInFlight,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
//
|
|
601
|
+
// first validate toolcall args, before invoking callback
|
|
602
|
+
// appends validated args
|
|
603
|
+
toolCallInFlight.args = validateToolArguments(tool, toolCallInFlight);
|
|
604
|
+
if (config.beforeToolExecution) {
|
|
605
|
+
const beforeResult = await config.beforeToolExecution(
|
|
606
|
+
{
|
|
607
|
+
context: currentContext,
|
|
608
|
+
assistantMessage,
|
|
609
|
+
toolCall: toolCallInFlight,
|
|
610
|
+
},
|
|
611
|
+
signal,
|
|
612
|
+
);
|
|
613
|
+
if (beforeResult?.block) {
|
|
614
|
+
return {
|
|
615
|
+
error: {
|
|
616
|
+
kind: "blocked",
|
|
617
|
+
message: beforeResult?.reason || "Tool Execution was Blocked",
|
|
618
|
+
details: beforeResult?.details,
|
|
619
|
+
},
|
|
620
|
+
runnable: toolCallInFlight,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
runnable: toolCallInFlight,
|
|
626
|
+
tool: tool as Agent.AnyAgentTool,
|
|
627
|
+
};
|
|
628
|
+
} catch (err) {
|
|
629
|
+
return {
|
|
630
|
+
error: {
|
|
631
|
+
kind: "error",
|
|
632
|
+
message: err instanceof Error ? err.message : String(err),
|
|
633
|
+
},
|
|
634
|
+
runnable: toolCallInFlight,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function executeRunnableToolCall(
|
|
640
|
+
toolCallRunnable: ToolCallRunnable,
|
|
641
|
+
message: Message.AssistantMessage,
|
|
642
|
+
stream: EventStream<Event.AgentEvent, Message.Message[]>,
|
|
643
|
+
signal?: AbortSignal,
|
|
644
|
+
): Promise<ToolCallExecutionResult> {
|
|
645
|
+
const { toolCall, runnable, tool } = toolCallRunnable;
|
|
646
|
+
const { partIndex, ...pendingPart } = toolCall;
|
|
647
|
+
try {
|
|
648
|
+
const result = await tool.execute(runnable.callID, runnable.args, signal, (result) => {
|
|
649
|
+
const runningResult: Agent.ToolRunningResult<any> = {
|
|
650
|
+
status: "running",
|
|
651
|
+
partial: result.partial,
|
|
652
|
+
};
|
|
653
|
+
const toolExecutionUpdate: Event.ToolExecutionUpdate = {
|
|
654
|
+
type: "tool.execution.update",
|
|
655
|
+
...runnable,
|
|
656
|
+
...runningResult,
|
|
657
|
+
};
|
|
658
|
+
stream.push({ ...toolExecutionUpdate });
|
|
659
|
+
|
|
660
|
+
const runningPart: Message.ToolCallRunningPart = {
|
|
661
|
+
...pendingPart,
|
|
662
|
+
...runningResult,
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
message.parts[partIndex] = runningPart; // in-place mutate; add error part
|
|
666
|
+
|
|
667
|
+
stream.push({ type: "message.part.update", message, partIndex, part: runningPart, source: "tool" });
|
|
668
|
+
});
|
|
669
|
+
return {
|
|
670
|
+
result,
|
|
671
|
+
};
|
|
672
|
+
} catch (err) {
|
|
673
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
674
|
+
return {
|
|
675
|
+
error: {
|
|
676
|
+
message,
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function emitToolCallTerminalResult(
|
|
683
|
+
toolCall: ToolCallPart,
|
|
684
|
+
runnable: Agent.ToolCallInFlight,
|
|
685
|
+
message: Message.AssistantMessage,
|
|
686
|
+
stream: EventStream<Event.AgentEvent, Message.Message[]>,
|
|
687
|
+
result: Agent.ToolTerminalResult<any>,
|
|
688
|
+
): Promise<void> {
|
|
689
|
+
const { partIndex, ...pendingPart } = toolCall;
|
|
690
|
+
const { time } = pendingPart;
|
|
691
|
+
const toolExecutionEnd: Event.ToolExecutionEnd = {
|
|
692
|
+
type: "tool.execution.end",
|
|
693
|
+
...runnable,
|
|
694
|
+
...result,
|
|
695
|
+
};
|
|
696
|
+
stream.push({ ...toolExecutionEnd });
|
|
697
|
+
|
|
698
|
+
const part: Message.ToolCallErrorPart | Message.ToolCallCompletedPart = {
|
|
699
|
+
...pendingPart,
|
|
700
|
+
...result,
|
|
701
|
+
time: {
|
|
702
|
+
start: time.start,
|
|
703
|
+
end: Date.now(),
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
message.parts[partIndex] = part; // in-place mutate; add error part
|
|
708
|
+
|
|
709
|
+
stream.push({ type: "message.part.start", message: message, partIndex: partIndex, part: part });
|
|
710
|
+
stream.push({ type: "message.part.end", message: message, partIndex: partIndex, part: part });
|
|
711
|
+
stream.push({ type: "message.update", message: message });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function finalizeExecutedToolCall(
|
|
715
|
+
config: Config,
|
|
716
|
+
currentContext: Agent.AgentContext,
|
|
717
|
+
toolCall: ToolCallPart,
|
|
718
|
+
runnable: Agent.ToolCallInFlight,
|
|
719
|
+
message: Message.AssistantMessage,
|
|
720
|
+
stream: EventStream<Event.AgentEvent, Message.Message[]>,
|
|
721
|
+
result: Agent.ToolTerminalResult<any>,
|
|
722
|
+
signal?: AbortSignal,
|
|
723
|
+
): Promise<void> {
|
|
724
|
+
if (config.afterToolExecution) {
|
|
725
|
+
const afterResult = await config.afterToolExecution(
|
|
726
|
+
{
|
|
727
|
+
context: currentContext,
|
|
728
|
+
assistantMessage: message,
|
|
729
|
+
toolCall: runnable,
|
|
730
|
+
result,
|
|
731
|
+
},
|
|
732
|
+
signal,
|
|
733
|
+
);
|
|
734
|
+
if (afterResult) {
|
|
735
|
+
await emitToolCallTerminalResult(toolCall, runnable, message, stream, afterResult);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
await emitToolCallTerminalResult(toolCall, runnable, message, stream, result);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export function agentLoop(
|
|
743
|
+
config: Config,
|
|
744
|
+
context: Agent.AgentContext,
|
|
745
|
+
prompts: Message.UserMessage[],
|
|
746
|
+
streamFn?: StreamFn,
|
|
747
|
+
signal?: AbortSignal,
|
|
748
|
+
): EventStream<Event.AgentEvent, Message.Message[]> {
|
|
749
|
+
const stream = createAgentStream();
|
|
750
|
+
/**
|
|
751
|
+
* maintain 2 arrays to represent state, `context` the actual full state sent to the LLM,
|
|
752
|
+
* `newMessages` for intermediate current loop state.
|
|
753
|
+
* think of newMessages as subset of context.messages for the running current loop.
|
|
754
|
+
* they do move at the same speed staring with initial user prompts.
|
|
755
|
+
* */
|
|
756
|
+
(async () => {
|
|
757
|
+
void config;
|
|
758
|
+
void signal;
|
|
759
|
+
void streamFn;
|
|
760
|
+
const newMessages: Message.Message[] = [...prompts];
|
|
761
|
+
const currentContext: Agent.AgentContext = {
|
|
762
|
+
...context,
|
|
763
|
+
messages: [...context.messages, ...prompts],
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
stream.push({ type: "agent.start" });
|
|
767
|
+
stream.push({ type: "turn.start" });
|
|
768
|
+
for (const prompt of prompts) {
|
|
769
|
+
stream.push({ type: "message.start", message: prompt });
|
|
770
|
+
for (const [partIndex, part] of prompt.parts.entries()) {
|
|
771
|
+
stream.push({ type: "message.part.start", message: prompt, partIndex, part });
|
|
772
|
+
stream.push({ type: "message.part.end", message: prompt, partIndex, part });
|
|
773
|
+
}
|
|
774
|
+
stream.push({ type: "message.end", message: prompt });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
await runLoop(config, currentContext, newMessages, stream, streamFn, signal);
|
|
778
|
+
})();
|
|
779
|
+
|
|
780
|
+
return stream;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export function agentLoopContinue(): EventStream<Event.AgentEvent, Message.Message[]> {
|
|
784
|
+
return createAgentStream();
|
|
785
|
+
}
|
|
786
|
+
}
|