@codeworksh/aikit 0.4.0 → 0.4.1-dev.20260530133433

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