@nanoagent/kernel 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1136 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { bedrock } from '@ai-sdk/amazon-bedrock';
3
+ import { anthropic } from '@ai-sdk/anthropic';
4
+ import { azure } from '@ai-sdk/azure';
5
+ import { baseten } from '@ai-sdk/baseten';
6
+ import { cerebras } from '@ai-sdk/cerebras';
7
+ import { cohere } from '@ai-sdk/cohere';
8
+ import { deepinfra } from '@ai-sdk/deepinfra';
9
+ import { deepseek } from '@ai-sdk/deepseek';
10
+ import { fireworks } from '@ai-sdk/fireworks';
11
+ import { google } from '@ai-sdk/google';
12
+ import { vertex } from '@ai-sdk/google-vertex';
13
+ import { groq } from '@ai-sdk/groq';
14
+ import { mistral } from '@ai-sdk/mistral';
15
+ import { openai } from '@ai-sdk/openai';
16
+ import { perplexity } from '@ai-sdk/perplexity';
17
+ import { togetherai } from '@ai-sdk/togetherai';
18
+ import { vercel } from '@ai-sdk/vercel';
19
+ import { xai } from '@ai-sdk/xai';
20
+ import { Effect } from 'effect';
21
+ import { jsonSchema, streamText } from 'ai';
22
+ const DEFAULT_MODEL_PROVIDERS = {
23
+ openai,
24
+ anthropic,
25
+ azure,
26
+ baseten,
27
+ cerebras,
28
+ cohere,
29
+ deepinfra,
30
+ deepseek,
31
+ fireworks,
32
+ google,
33
+ gemini: google,
34
+ vertex,
35
+ 'google-vertex': vertex,
36
+ groq,
37
+ grok: xai,
38
+ mistral,
39
+ perplexity,
40
+ together: togetherai,
41
+ togetherai,
42
+ bedrock,
43
+ 'amazon-bedrock': bedrock,
44
+ vercel,
45
+ xai
46
+ };
47
+ function normalizeModelProviders(modelProviders) {
48
+ return Object.fromEntries(Object.entries(modelProviders ?? {})
49
+ .map(([provider, modelProvider]) => [
50
+ provider.trim().toLowerCase(),
51
+ modelProvider
52
+ ])
53
+ .filter(([provider]) => provider));
54
+ }
55
+ function toError(error) {
56
+ return error instanceof Error ? error : new Error(String(error));
57
+ }
58
+ function serializeError(error) {
59
+ if (error instanceof Error) {
60
+ return {
61
+ message: error.message,
62
+ name: error.name,
63
+ stack: error.stack
64
+ };
65
+ }
66
+ return { message: String(error) };
67
+ }
68
+ function fromAgentResult(evaluate) {
69
+ return Effect.flatMap(Effect.try({
70
+ try: evaluate,
71
+ catch: toError
72
+ }), result => Effect.isEffect(result)
73
+ ? result
74
+ : Effect.tryPromise({
75
+ try: () => Promise.resolve(result),
76
+ catch: toError
77
+ }));
78
+ }
79
+ function runMiddleware({ input, middleware, terminal }) {
80
+ const operation = (middleware ?? []).reduceRight((next, current) => input => fromAgentResult(() => current({
81
+ input,
82
+ next: nextInput => Effect.runPromise(next(nextInput))
83
+ })), input => fromAgentResult(() => terminal(input)));
84
+ return operation(input);
85
+ }
86
+ function checkNotAborted(signal) {
87
+ if (!signal?.aborted) {
88
+ return Effect.void;
89
+ }
90
+ return Effect.fail(signal.reason instanceof Error
91
+ ? signal.reason
92
+ : new Error('Agent run aborted.'));
93
+ }
94
+ function nowIso() {
95
+ return new Date().toISOString();
96
+ }
97
+ function durationMs(startCreatedAt, endCreatedAt) {
98
+ return Date.parse(endCreatedAt) - Date.parse(startCreatedAt);
99
+ }
100
+ function cloneUnknown(value) {
101
+ if (typeof value !== 'object' || value === null) {
102
+ return value;
103
+ }
104
+ try {
105
+ return structuredClone(value);
106
+ }
107
+ catch {
108
+ if (Array.isArray(value)) {
109
+ return value.map(item => cloneUnknown(item));
110
+ }
111
+ const prototype = Object.getPrototypeOf(value);
112
+ if (prototype === Object.prototype || prototype === null) {
113
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneUnknown(entry)]));
114
+ }
115
+ return value;
116
+ }
117
+ }
118
+ function cloneModelResult(modelResult) {
119
+ return {
120
+ ...modelResult,
121
+ response: {
122
+ ...modelResult.response,
123
+ messages: [...modelResult.response.messages]
124
+ }
125
+ };
126
+ }
127
+ function cloneTurn(turn) {
128
+ return {
129
+ turnId: turn.turnId,
130
+ turn: turn.turn,
131
+ modelArgs: turn.modelArgs ? cloneUnknown(turn.modelArgs) : undefined,
132
+ modelResult: turn.modelResult
133
+ ? cloneModelResult(turn.modelResult)
134
+ : undefined,
135
+ toolCalls: {
136
+ pending: turn.toolCalls.pending.map(cloneToolCall),
137
+ inFlight: turn.toolCalls.inFlight.map(cloneToolCall),
138
+ completed: [...turn.toolCalls.completed]
139
+ }
140
+ };
141
+ }
142
+ function cloneToolCall(toolCall) {
143
+ return {
144
+ toolCallId: toolCall.toolCallId,
145
+ toolName: toolCall.toolName,
146
+ input: cloneUnknown(toolCall.input)
147
+ };
148
+ }
149
+ function deepFreeze(value, seen = new WeakSet()) {
150
+ if (typeof value !== 'object' || value === null) {
151
+ return value;
152
+ }
153
+ const prototype = Object.getPrototypeOf(value);
154
+ if (!Array.isArray(value) &&
155
+ prototype !== Object.prototype &&
156
+ prototype !== null) {
157
+ return value;
158
+ }
159
+ if (seen.has(value)) {
160
+ return value;
161
+ }
162
+ seen.add(value);
163
+ for (const property of Reflect.ownKeys(value)) {
164
+ deepFreeze(value[property], seen);
165
+ }
166
+ return Object.freeze(value);
167
+ }
168
+ function isSkip(value) {
169
+ return (typeof value === 'object' &&
170
+ value !== null &&
171
+ 'type' in value &&
172
+ value.type === 'skip');
173
+ }
174
+ function toModelTools(tools) {
175
+ return Object.fromEntries(Object.entries(tools).map(([toolName, toolConfig]) => {
176
+ const { execute: _execute, ...definition } = toolConfig;
177
+ if (isRawJsonSchema(definition.inputSchema)) {
178
+ definition.inputSchema = jsonSchema(definition.inputSchema);
179
+ }
180
+ return [toolName, definition];
181
+ }));
182
+ }
183
+ function isRawJsonSchema(value) {
184
+ return (typeof value === 'object' &&
185
+ value !== null &&
186
+ !Array.isArray(value) &&
187
+ 'type' in value &&
188
+ !('validate' in value));
189
+ }
190
+ function resolveModel({ model, modelProviders }) {
191
+ const [rawProvider, ...modelParts] = model.split('/');
192
+ const modelName = modelParts.join('/').trim();
193
+ const provider = rawProvider?.trim().toLowerCase();
194
+ if (!provider || !modelName) {
195
+ return Effect.fail(new Error(`Invalid model "${model}". Expected "<provider>/<model-name>".`));
196
+ }
197
+ const modelProvider = modelProviders[provider];
198
+ if (!modelProvider) {
199
+ return Effect.fail(new Error(`Unsupported model provider "${provider}". Supported providers: ${Object.keys(modelProviders).join(', ')}.`));
200
+ }
201
+ return Effect.try({
202
+ try: () => modelProvider(modelName),
203
+ catch: toError
204
+ });
205
+ }
206
+ async function buildModelResult(result) {
207
+ const [response, finishReason, totalUsage, text, reasoning, reasoningText, sources, warnings, providerMetadata] = await Promise.all([
208
+ result.response,
209
+ result.finishReason,
210
+ result.totalUsage,
211
+ result.text,
212
+ result.reasoning,
213
+ result.reasoningText,
214
+ result.sources,
215
+ result.warnings,
216
+ result.providerMetadata
217
+ ]);
218
+ return {
219
+ finishReason,
220
+ response,
221
+ totalUsage,
222
+ text,
223
+ reasoning,
224
+ reasoningText,
225
+ sources,
226
+ warnings,
227
+ providerMetadata
228
+ };
229
+ }
230
+ function makeBaseArgs(state) {
231
+ const readonlyState = deepFreeze(cloneUnknown(state));
232
+ return {
233
+ context: readonlyState.context,
234
+ state: readonlyState,
235
+ runId: state.runId
236
+ };
237
+ }
238
+ function emptyToolCalls() {
239
+ return { pending: [], inFlight: [], completed: [] };
240
+ }
241
+ function persistSnapshot({ events, previousRevision, saveState, state }) {
242
+ const snapshottedState = {
243
+ ...state,
244
+ revision: previousRevision + 1,
245
+ updatedAt: nowIso()
246
+ };
247
+ const snapshottedEvents = events.map(event => 'revision' in event
248
+ ? { ...event, revision: snapshottedState.revision }
249
+ : event);
250
+ return Effect.gen(function* () {
251
+ if (saveState) {
252
+ yield* fromAgentResult(() => saveState({
253
+ state: cloneUnknown(snapshottedState),
254
+ events: snapshottedEvents
255
+ }));
256
+ }
257
+ return {
258
+ events: snapshottedEvents,
259
+ state: snapshottedState
260
+ };
261
+ });
262
+ }
263
+ function withRunning(state, phase) {
264
+ return {
265
+ ...state,
266
+ status: { type: 'running', phase }
267
+ };
268
+ }
269
+ function recoverablePhase(state) {
270
+ if (state.status.type === 'running' || state.status.type === 'paused') {
271
+ return state.status.phase;
272
+ }
273
+ if (state.status.type === 'failed') {
274
+ return state.status.phase;
275
+ }
276
+ return 'run_started';
277
+ }
278
+ function applyContext({ snapshot, hookResult }) {
279
+ if (hookResult?.context !== undefined) {
280
+ return {
281
+ ...snapshot,
282
+ context: hookResult.context
283
+ };
284
+ }
285
+ return snapshot;
286
+ }
287
+ async function snapshotPause({ pause, snapshotter, snapshot }) {
288
+ if (snapshot.status.type !== 'running') {
289
+ throw new Error('Cannot pause a run that is not running.');
290
+ }
291
+ const createdAt = nowIso();
292
+ const phase = snapshot.status.phase;
293
+ return snapshotter({
294
+ ...snapshot,
295
+ status: {
296
+ type: 'paused',
297
+ phase,
298
+ reason: pause.reason,
299
+ metadata: pause.metadata,
300
+ createdAt
301
+ }
302
+ }, [
303
+ {
304
+ type: 'pause',
305
+ runId: snapshot.runId,
306
+ revision: snapshot.revision,
307
+ createdAt,
308
+ phase,
309
+ turn: snapshot.currentTurn
310
+ ? cloneTurn(snapshot.currentTurn)
311
+ : undefined,
312
+ reason: pause.reason,
313
+ metadata: pause.metadata
314
+ }
315
+ ]);
316
+ }
317
+ function executeToolCall({ context, toolCall, messages, signal, tools }) {
318
+ const execute = tools[toolCall.toolName]?.execute;
319
+ if (!execute) {
320
+ return Effect.fail(new Error(`Tool "${toolCall.toolName}" is not executable.`));
321
+ }
322
+ const response = {
323
+ toolCallId: toolCall.toolCallId,
324
+ toolName: toolCall.toolName,
325
+ input: toolCall.input
326
+ };
327
+ return Effect.match(Effect.tryPromise({
328
+ try: async () => {
329
+ const output = await execute(toolCall.input, {
330
+ toolCallId: toolCall.toolCallId,
331
+ messages,
332
+ abortSignal: signal,
333
+ experimental_context: context
334
+ });
335
+ if (typeof output !== 'object' ||
336
+ output === null ||
337
+ !(Symbol.asyncIterator in output)) {
338
+ return output;
339
+ }
340
+ let finalOutput;
341
+ for await (const chunk of output) {
342
+ finalOutput = chunk;
343
+ }
344
+ return finalOutput;
345
+ },
346
+ catch: toError
347
+ }), {
348
+ onFailure: error => ({ ...response, error }),
349
+ onSuccess: output => ({ ...response, output })
350
+ });
351
+ }
352
+ export async function* runAgent(options) {
353
+ const tools = options.tools ?? {};
354
+ const modelProviders = {
355
+ ...DEFAULT_MODEL_PROVIDERS,
356
+ ...normalizeModelProviders(options.modelProviders)
357
+ };
358
+ let snapshot = 'status' in options.state
359
+ ? cloneUnknown(options.state)
360
+ : {
361
+ runId: options.state.runId ?? randomUUID(),
362
+ revision: 0,
363
+ status: { type: 'running', phase: 'run_started' },
364
+ context: options.state.context,
365
+ turns: [],
366
+ updatedAt: nowIso()
367
+ };
368
+ const hooks = options.hooks;
369
+ const runCreatedAt = snapshot.updatedAt;
370
+ // Caller-emitted finish from any hook. The main loop consumes this and
371
+ // transitions to status.completed at the next checkpoint.
372
+ let pendingFinish;
373
+ let pendingContinue = false;
374
+ async function* emitEvents(events) {
375
+ for (const event of events) {
376
+ if (event.type === 'pause') {
377
+ await Effect.runPromise(fromAgentResult(() => hooks.onPause?.({
378
+ ...makeBaseArgs(snapshot),
379
+ createdAt: event.createdAt,
380
+ phase: event.phase,
381
+ turn: event.turn,
382
+ reason: event.reason,
383
+ metadata: event.metadata
384
+ })));
385
+ }
386
+ else if (event.type === 'model_restarted') {
387
+ await Effect.runPromise(fromAgentResult(() => hooks.onModelRestarted?.({
388
+ ...makeBaseArgs(snapshot),
389
+ createdAt: event.createdAt,
390
+ turn: event.turn
391
+ })));
392
+ }
393
+ yield event;
394
+ }
395
+ }
396
+ const snapshotState = async (nextSnapshot, events) => {
397
+ const snapshotted = await Effect.runPromise(persistSnapshot({
398
+ events,
399
+ previousRevision: snapshot.revision,
400
+ saveState: options.saveState,
401
+ state: nextSnapshot
402
+ }));
403
+ snapshot = snapshotted.state;
404
+ return snapshotted.events;
405
+ };
406
+ function isPaused() {
407
+ return snapshot.status.type === 'paused';
408
+ }
409
+ function phaseEvent(type) {
410
+ if (type === 'run_started') {
411
+ return {
412
+ type,
413
+ runId: snapshot.runId,
414
+ revision: snapshot.revision,
415
+ createdAt: nowIso()
416
+ };
417
+ }
418
+ if (!snapshot.currentTurn) {
419
+ throw new Error(`Cannot emit ${type} without a current turn.`);
420
+ }
421
+ return {
422
+ type,
423
+ runId: snapshot.runId,
424
+ revision: snapshot.revision,
425
+ createdAt: nowIso(),
426
+ turn: cloneTurn(snapshot.currentTurn)
427
+ };
428
+ }
429
+ async function applyHookResult(hookResult, options = {}) {
430
+ const hasContextUpdate = hookResult?.context !== undefined;
431
+ snapshot = applyContext({ snapshot, hookResult });
432
+ const control = hookResult?.control;
433
+ if (control?.type === 'pause') {
434
+ const onPause = options.onPause ??
435
+ ((pause) => snapshotPause({
436
+ snapshotter: snapshotState,
437
+ pause,
438
+ snapshot
439
+ }));
440
+ return {
441
+ events: await onPause(control)
442
+ };
443
+ }
444
+ if (control?.type === 'finish' && options.onFinish !== 'ignore') {
445
+ pendingFinish = {
446
+ source: 'caller',
447
+ reason: control.reason,
448
+ metadata: control.metadata
449
+ };
450
+ return { events: [] };
451
+ }
452
+ if (control?.type === 'continue' && options.onContinue !== 'ignore') {
453
+ pendingContinue = true;
454
+ return { events: [] };
455
+ }
456
+ if (hasContextUpdate && options.persistContext) {
457
+ return { events: await snapshotState(snapshot, []) };
458
+ }
459
+ return { value: hookResult?.value };
460
+ }
461
+ async function prepareStartedTurn(prefixEvents = []) {
462
+ const currentTurn = snapshot.currentTurn;
463
+ if (!currentTurn) {
464
+ throw new Error('Cannot prepare turn without a current turn.');
465
+ }
466
+ const preparedHook = await Effect.runPromise(fromAgentResult(() => hooks.onTurnPrepared({
467
+ ...makeBaseArgs(snapshot),
468
+ createdAt: nowIso(),
469
+ turn: cloneTurn(currentTurn)
470
+ })));
471
+ const preparedResult = await applyHookResult(preparedHook);
472
+ if (preparedResult.events) {
473
+ return { events: [...prefixEvents, ...preparedResult.events] };
474
+ }
475
+ if (!preparedResult.value) {
476
+ throw new Error('onTurnPrepared must return a turn configuration.');
477
+ }
478
+ const { model: preparedModel, ...streamTextOptions } = preparedResult.value;
479
+ const modelArgs = {
480
+ ...streamTextOptions,
481
+ model: preparedModel,
482
+ toolNames: Object.keys(tools)
483
+ };
484
+ const preparedSnapshot = {
485
+ ...snapshot,
486
+ status: { type: 'running', phase: 'turn_prepared' },
487
+ currentTurn: {
488
+ ...snapshot.currentTurn,
489
+ modelArgs
490
+ }
491
+ };
492
+ snapshot = preparedSnapshot;
493
+ return {
494
+ events: [
495
+ ...prefixEvents,
496
+ ...(await snapshotState(preparedSnapshot, [
497
+ phaseEvent('turn_prepared')
498
+ ]))
499
+ ]
500
+ };
501
+ }
502
+ async function prepareTurn(turnNumber) {
503
+ const turnId = randomUUID();
504
+ const turnCreatedAt = nowIso();
505
+ const startedTurn = {
506
+ turnId,
507
+ turn: turnNumber,
508
+ toolCalls: emptyToolCalls()
509
+ };
510
+ const turnStartedSnapshot = {
511
+ ...snapshot,
512
+ status: { type: 'running', phase: 'turn_started' },
513
+ currentTurn: startedTurn
514
+ };
515
+ snapshot = turnStartedSnapshot;
516
+ const turnStartedEvents = await snapshotState(turnStartedSnapshot, [
517
+ phaseEvent('turn_started')
518
+ ]);
519
+ const turnStartedHook = await Effect.runPromise(fromAgentResult(() => hooks.onTurnStarted?.({
520
+ ...makeBaseArgs(snapshot),
521
+ createdAt: turnCreatedAt,
522
+ turn: cloneTurn(startedTurn)
523
+ })));
524
+ const turnStartedResult = await applyHookResult(turnStartedHook, {
525
+ persistContext: true
526
+ });
527
+ if ((isPaused() || pendingFinish) && turnStartedResult.events) {
528
+ return { events: [...turnStartedEvents, ...turnStartedResult.events] };
529
+ }
530
+ return prepareStartedTurn(turnStartedEvents);
531
+ }
532
+ async function* runModel() {
533
+ const currentTurn = snapshot.currentTurn;
534
+ if (!currentTurn?.modelArgs) {
535
+ throw new Error('Cannot run model without a prepared turn.');
536
+ }
537
+ const modelArgs = {
538
+ ...currentTurn.modelArgs,
539
+ toolNames: [...currentTurn.modelArgs.toolNames]
540
+ };
541
+ const modelStartedAt = nowIso();
542
+ const wasResumingFromModelStarted = snapshot.status.type === 'running' &&
543
+ snapshot.status.phase === 'model_started';
544
+ const modelStartedSnapshot = {
545
+ ...snapshot,
546
+ status: { type: 'running', phase: 'model_started' }
547
+ };
548
+ snapshot = modelStartedSnapshot;
549
+ const startedEvents = wasResumingFromModelStarted
550
+ ? [
551
+ {
552
+ type: 'model_restarted',
553
+ runId: snapshot.runId,
554
+ createdAt: nowIso(),
555
+ turn: cloneTurn(currentTurn)
556
+ }
557
+ ]
558
+ : await snapshotState(modelStartedSnapshot, [phaseEvent('model_started')]);
559
+ yield* emitEvents(startedEvents);
560
+ const modelStartedHook = await Effect.runPromise(fromAgentResult(() => hooks.onModelStarted?.({
561
+ ...makeBaseArgs(snapshot),
562
+ args: modelArgs,
563
+ createdAt: modelStartedAt,
564
+ turn: cloneTurn(currentTurn)
565
+ })));
566
+ const modelStartedResult = await applyHookResult(modelStartedHook, {
567
+ persistContext: true
568
+ });
569
+ if ((isPaused() || pendingFinish) && modelStartedResult.events) {
570
+ yield* emitEvents(modelStartedResult.events);
571
+ return;
572
+ }
573
+ const streamEvents = [];
574
+ const callModelResult = await Effect.runPromise(runMiddleware({
575
+ input: {
576
+ ...makeBaseArgs(snapshot),
577
+ args: modelArgs,
578
+ createdAt: modelStartedAt,
579
+ turn: cloneTurn(currentTurn)
580
+ },
581
+ middleware: options.middleware?.callModel,
582
+ terminal: async (input) => {
583
+ const { model: modelName, toolNames: _toolNames, ...streamTextOptions } = input.args;
584
+ const model = await Effect.runPromise(resolveModel({ model: modelName, modelProviders }));
585
+ const rawResult = await Effect.runPromise(Effect.try({
586
+ try: () => streamText({
587
+ ...streamTextOptions,
588
+ model,
589
+ tools: toModelTools(tools),
590
+ abortSignal: options.signal
591
+ }),
592
+ catch: toError
593
+ }));
594
+ const attemptStreamEvents = [];
595
+ for await (const part of rawResult.fullStream) {
596
+ await Effect.runPromise(checkNotAborted(options.signal));
597
+ attemptStreamEvents.push({
598
+ type: 'stream_part',
599
+ runId: snapshot.runId,
600
+ turnId: currentTurn.turnId,
601
+ turn: currentTurn.turn,
602
+ createdAt: nowIso(),
603
+ part
604
+ });
605
+ }
606
+ const modelResult = cloneModelResult(await buildModelResult(rawResult));
607
+ const pendingToolCalls = modelResult.finishReason === 'tool-calls'
608
+ ? (await Effect.runPromise(Effect.tryPromise({
609
+ try: () => rawResult.toolCalls,
610
+ catch: toError
611
+ }))).map(cloneToolCall)
612
+ : [];
613
+ const completedAt = nowIso();
614
+ streamEvents.push(...attemptStreamEvents);
615
+ return {
616
+ args: {
617
+ ...input.args,
618
+ toolNames: [...input.args.toolNames]
619
+ },
620
+ duration: durationMs(input.createdAt, completedAt),
621
+ pendingToolCalls,
622
+ rawResult,
623
+ result: modelResult
624
+ };
625
+ }
626
+ }));
627
+ const completedAt = nowIso();
628
+ for (const event of streamEvents) {
629
+ await Effect.runPromise(fromAgentResult(() => hooks.onStreamUpdate?.({
630
+ ...makeBaseArgs(snapshot),
631
+ createdAt: event.createdAt,
632
+ part: event.part,
633
+ turn: cloneTurn(currentTurn)
634
+ })));
635
+ yield* emitEvents([event]);
636
+ }
637
+ const modelCompletedHook = await Effect.runPromise(fromAgentResult(() => hooks.onModelCompleted?.({
638
+ ...makeBaseArgs(snapshot),
639
+ args: callModelResult.args,
640
+ createdAt: completedAt,
641
+ duration: callModelResult.duration,
642
+ result: cloneModelResult(callModelResult.result),
643
+ rawResult: callModelResult.rawResult,
644
+ turn: cloneTurn({
645
+ ...currentTurn,
646
+ modelArgs: callModelResult.args
647
+ })
648
+ })));
649
+ const modelCompletedResult = await applyHookResult(modelCompletedHook);
650
+ if (modelCompletedResult.events) {
651
+ yield* emitEvents(modelCompletedResult.events);
652
+ return;
653
+ }
654
+ const nextTurn = {
655
+ ...currentTurn,
656
+ modelArgs: callModelResult.args,
657
+ modelResult: callModelResult.result,
658
+ toolCalls: {
659
+ pending: callModelResult.pendingToolCalls,
660
+ inFlight: [],
661
+ completed: []
662
+ }
663
+ };
664
+ const modelCompletedSnapshot = {
665
+ ...snapshot,
666
+ status: { type: 'running', phase: 'model_completed' },
667
+ currentTurn: nextTurn
668
+ };
669
+ snapshot = modelCompletedSnapshot;
670
+ yield* emitEvents(await snapshotState(modelCompletedSnapshot, [
671
+ {
672
+ type: 'model_completed',
673
+ runId: snapshot.runId,
674
+ revision: snapshot.revision,
675
+ createdAt: completedAt,
676
+ duration: callModelResult.duration,
677
+ args: callModelResult.args,
678
+ result: cloneModelResult(callModelResult.result),
679
+ turn: cloneTurn(nextTurn)
680
+ }
681
+ ]));
682
+ }
683
+ async function* prepareToolCalls() {
684
+ const currentTurn = snapshot.currentTurn;
685
+ if (!currentTurn?.modelResult) {
686
+ throw new Error('Cannot prepare tool calls without model result.');
687
+ }
688
+ const modelResult = currentTurn.modelResult;
689
+ const hookResult = await Effect.runPromise(fromAgentResult(() => hooks.onToolCallsStarted?.({
690
+ ...makeBaseArgs(snapshot),
691
+ createdAt: nowIso(),
692
+ result: cloneModelResult(modelResult),
693
+ toolCalls: deepFreeze(currentTurn.toolCalls.pending.map(cloneToolCall)),
694
+ turn: cloneTurn(currentTurn)
695
+ })));
696
+ const hook = await applyHookResult(hookResult);
697
+ if (hook.events) {
698
+ yield* emitEvents(hook.events);
699
+ return;
700
+ }
701
+ const preparedToolCalls = (hook.value ?? currentTurn.toolCalls.pending).map(cloneToolCall);
702
+ const nextTurn = {
703
+ ...currentTurn,
704
+ toolCalls: {
705
+ pending: preparedToolCalls,
706
+ inFlight: [],
707
+ completed: []
708
+ }
709
+ };
710
+ const nextSnapshot = {
711
+ ...snapshot,
712
+ status: { type: 'running', phase: 'tool_calls_started' },
713
+ currentTurn: nextTurn
714
+ };
715
+ snapshot = nextSnapshot;
716
+ yield* emitEvents(await snapshotState(nextSnapshot, [phaseEvent('tool_calls_started')]));
717
+ }
718
+ async function* prepareEachToolCall() {
719
+ const currentTurn = snapshot.currentTurn;
720
+ if (!currentTurn) {
721
+ throw new Error('Cannot start tool calls without a current turn.');
722
+ }
723
+ const accepted = [];
724
+ const completed = [
725
+ ...currentTurn.toolCalls.completed
726
+ ];
727
+ const pendingToolCalls = currentTurn.toolCalls.pending;
728
+ for (const [index, originalToolCall] of pendingToolCalls.entries()) {
729
+ const hookToolCall = deepFreeze(cloneToolCall(originalToolCall));
730
+ const hookResult = await Effect.runPromise(fromAgentResult(() => hooks.onToolCallStarted?.({
731
+ ...makeBaseArgs(snapshot),
732
+ createdAt: nowIso(),
733
+ toolCall: hookToolCall,
734
+ toolCallId: hookToolCall.toolCallId,
735
+ toolName: hookToolCall.toolName,
736
+ input: hookToolCall.input,
737
+ turn: cloneTurn(snapshot.currentTurn)
738
+ })));
739
+ const hook = await applyHookResult(hookResult, {
740
+ onPause: pause => {
741
+ snapshot = {
742
+ ...snapshot,
743
+ status: { type: 'running', phase: 'tool_calls_started' },
744
+ currentTurn: {
745
+ ...snapshot.currentTurn,
746
+ toolCalls: {
747
+ pending: [
748
+ ...accepted,
749
+ originalToolCall,
750
+ ...pendingToolCalls.slice(index + 1)
751
+ ],
752
+ inFlight: [],
753
+ completed: [...completed]
754
+ }
755
+ }
756
+ };
757
+ return snapshotPause({
758
+ snapshotter: snapshotState,
759
+ pause,
760
+ snapshot
761
+ });
762
+ }
763
+ });
764
+ if (hook.events) {
765
+ yield* emitEvents(hook.events);
766
+ return;
767
+ }
768
+ if (isSkip(hook.value)) {
769
+ completed.push({
770
+ ...hook.value.result,
771
+ toolCallId: originalToolCall.toolCallId,
772
+ toolName: originalToolCall.toolName
773
+ });
774
+ }
775
+ else {
776
+ accepted.push(cloneToolCall(hook.value ?? originalToolCall));
777
+ }
778
+ const startedSnapshot = {
779
+ ...snapshot,
780
+ status: { type: 'running', phase: 'tool_call_started' },
781
+ currentTurn: {
782
+ ...snapshot.currentTurn,
783
+ toolCalls: {
784
+ pending: [...accepted],
785
+ inFlight: [],
786
+ completed: [...completed]
787
+ }
788
+ }
789
+ };
790
+ snapshot = startedSnapshot;
791
+ yield* emitEvents(await snapshotState(startedSnapshot, [phaseEvent('tool_call_started')]));
792
+ }
793
+ }
794
+ async function* executePreparedTools() {
795
+ const currentTurn = snapshot.currentTurn;
796
+ if (!currentTurn?.modelArgs) {
797
+ throw new Error('Cannot execute tools without tool snapshot.');
798
+ }
799
+ const launchedToolCalls = currentTurn.toolCalls.pending.map(cloneToolCall);
800
+ const toolMessages = 'messages' in currentTurn.modelArgs && currentTurn.modelArgs.messages
801
+ ? currentTurn.modelArgs.messages
802
+ : Array.isArray(currentTurn.modelArgs.prompt)
803
+ ? currentTurn.modelArgs.prompt
804
+ : [];
805
+ const launchedSnapshot = {
806
+ ...snapshot,
807
+ status: {
808
+ type: 'running',
809
+ phase: snapshot.status.type === 'running'
810
+ ? snapshot.status.phase
811
+ : 'tool_call_started'
812
+ },
813
+ currentTurn: {
814
+ ...currentTurn,
815
+ toolCalls: {
816
+ pending: [],
817
+ inFlight: launchedToolCalls,
818
+ completed: [...currentTurn.toolCalls.completed]
819
+ }
820
+ }
821
+ };
822
+ snapshot = launchedSnapshot;
823
+ yield* emitEvents(await snapshotState(launchedSnapshot, []));
824
+ const startedAtByToolCall = new Map(launchedToolCalls.map(toolCall => [toolCall.toolCallId, nowIso()]));
825
+ const results = await Effect.runPromise(Effect.all(launchedToolCalls.map(toolCall => runMiddleware({
826
+ input: {
827
+ context: snapshot.context,
828
+ messages: toolMessages,
829
+ ...(options.signal ? { signal: options.signal } : {}),
830
+ toolCall: cloneToolCall(toolCall),
831
+ tools
832
+ },
833
+ middleware: options.middleware?.callTool,
834
+ terminal: executeToolCall
835
+ })), { concurrency: 'unbounded' }));
836
+ for (const response of results) {
837
+ await Effect.runPromise(checkNotAborted(options.signal));
838
+ const completedAt = nowIso();
839
+ const responseResult = response.error !== undefined
840
+ ? { error: response.error }
841
+ : { output: response.output };
842
+ const duration = durationMs(startedAtByToolCall.get(response.toolCallId) ?? completedAt, completedAt);
843
+ const turnAfterResponse = {
844
+ ...snapshot.currentTurn,
845
+ toolCalls: {
846
+ pending: [],
847
+ inFlight: snapshot.currentTurn.toolCalls.inFlight.filter(toolCall => toolCall.toolCallId !== response.toolCallId),
848
+ completed: [...snapshot.currentTurn.toolCalls.completed, response]
849
+ }
850
+ };
851
+ const hookResult = await Effect.runPromise(fromAgentResult(() => hooks.onToolCallCompleted?.({
852
+ ...makeBaseArgs(snapshot),
853
+ createdAt: completedAt,
854
+ duration,
855
+ toolCallId: response.toolCallId,
856
+ toolName: response.toolName,
857
+ input: response.input,
858
+ turn: cloneTurn(turnAfterResponse),
859
+ ...responseResult
860
+ })));
861
+ const hook = await applyHookResult(hookResult);
862
+ if (hook.events) {
863
+ yield* emitEvents(hook.events);
864
+ return;
865
+ }
866
+ const completedSnapshot = {
867
+ ...snapshot,
868
+ status: { type: 'running', phase: 'tool_call_completed' },
869
+ currentTurn: turnAfterResponse
870
+ };
871
+ const completionEvent = {
872
+ type: 'tool_call_completed',
873
+ runId: snapshot.runId,
874
+ revision: snapshot.revision,
875
+ createdAt: completedAt,
876
+ duration,
877
+ turn: cloneTurn(turnAfterResponse),
878
+ toolCallId: response.toolCallId,
879
+ toolName: response.toolName,
880
+ input: response.input,
881
+ ...responseResult
882
+ };
883
+ snapshot = completedSnapshot;
884
+ yield* emitEvents(await snapshotState(completedSnapshot, [completionEvent]));
885
+ }
886
+ }
887
+ async function* completeToolCalls() {
888
+ const currentTurn = snapshot.currentTurn;
889
+ const modelResult = currentTurn?.modelResult;
890
+ if (!currentTurn || !modelResult) {
891
+ throw new Error('Cannot complete tool calls without model result.');
892
+ }
893
+ const hookResult = await Effect.runPromise(fromAgentResult(() => hooks.onToolCallsCompleted?.({
894
+ ...makeBaseArgs(snapshot),
895
+ createdAt: nowIso(),
896
+ result: cloneModelResult(modelResult),
897
+ toolCalls: currentTurn.toolCalls.completed,
898
+ turn: cloneTurn(currentTurn)
899
+ })));
900
+ const hook = await applyHookResult(hookResult);
901
+ if (hook.events) {
902
+ yield* emitEvents(hook.events);
903
+ return;
904
+ }
905
+ const completedSnapshot = {
906
+ ...snapshot,
907
+ status: { type: 'running', phase: 'tool_calls_completed' }
908
+ };
909
+ snapshot = completedSnapshot;
910
+ yield* emitEvents(await snapshotState(completedSnapshot, [
911
+ phaseEvent('tool_calls_completed')
912
+ ]));
913
+ }
914
+ async function* completeTurn() {
915
+ const currentTurn = snapshot.currentTurn;
916
+ if (!currentTurn?.modelResult) {
917
+ throw new Error('Cannot complete turn without model result.');
918
+ }
919
+ const completedAt = nowIso();
920
+ const duration = 0;
921
+ const completedTurn = cloneTurn(currentTurn);
922
+ // Record the turn before running the post-completion hook. The turn
923
+ // happened; hook decisions (pause, finish, continue) shouldn't undo it.
924
+ const completedSnapshot = {
925
+ ...snapshot,
926
+ status: { type: 'running', phase: 'turn_completed' },
927
+ turns: [...snapshot.turns, completedTurn],
928
+ currentTurn: undefined
929
+ };
930
+ snapshot = completedSnapshot;
931
+ yield* emitEvents(await snapshotState(completedSnapshot, [
932
+ {
933
+ type: 'turn_completed',
934
+ runId: snapshot.runId,
935
+ revision: snapshot.revision,
936
+ createdAt: completedAt,
937
+ duration,
938
+ turn: cloneTurn(completedTurn)
939
+ }
940
+ ]));
941
+ const hookResult = await Effect.runPromise(fromAgentResult(() => hooks.onTurnCompleted?.({
942
+ ...makeBaseArgs(snapshot),
943
+ createdAt: completedAt,
944
+ duration,
945
+ turn: cloneTurn(completedTurn)
946
+ })));
947
+ const hook = await applyHookResult(hookResult);
948
+ if (hook.events) {
949
+ yield* emitEvents(hook.events);
950
+ return;
951
+ }
952
+ }
953
+ async function* completeRun({ source, reason, metadata }) {
954
+ const completedAt = nowIso();
955
+ const duration = durationMs(runCreatedAt, completedAt);
956
+ const turns = snapshot.turns.map(cloneTurn);
957
+ const hookResult = await Effect.runPromise(fromAgentResult(() => hooks.onRunCompleted?.({
958
+ ...makeBaseArgs(snapshot),
959
+ createdAt: completedAt,
960
+ duration,
961
+ turns: turns.map(cloneTurn)
962
+ })));
963
+ // Inside completeRun, finish is meaningless — we're already finishing.
964
+ // Pause is still honored.
965
+ const hook = await applyHookResult(hookResult, {
966
+ onFinish: 'ignore',
967
+ onContinue: 'ignore'
968
+ });
969
+ if (hook.events) {
970
+ yield* emitEvents(hook.events);
971
+ return;
972
+ }
973
+ const completedSnapshot = {
974
+ ...snapshot,
975
+ status: {
976
+ type: 'completed',
977
+ source,
978
+ reason,
979
+ metadata,
980
+ createdAt: completedAt
981
+ }
982
+ };
983
+ snapshot = completedSnapshot;
984
+ yield* emitEvents(await snapshotState(completedSnapshot, [
985
+ {
986
+ type: 'run_completed',
987
+ runId: snapshot.runId,
988
+ revision: snapshot.revision,
989
+ createdAt: completedAt,
990
+ duration,
991
+ turns: turns.map(cloneTurn),
992
+ source,
993
+ reason,
994
+ metadata
995
+ }
996
+ ]));
997
+ }
998
+ async function failRun(error) {
999
+ const createdAt = nowIso();
1000
+ const hookResult = await Effect.runPromise(fromAgentResult(() => hooks.onRunFailed?.({
1001
+ ...makeBaseArgs(snapshot),
1002
+ createdAt,
1003
+ error
1004
+ })));
1005
+ snapshot = applyContext({ snapshot, hookResult });
1006
+ const failure = serializeError(error);
1007
+ const phase = recoverablePhase(snapshot);
1008
+ const failedSnapshot = {
1009
+ ...snapshot,
1010
+ status: { type: 'failed', phase, error: failure, createdAt }
1011
+ };
1012
+ snapshot = failedSnapshot;
1013
+ return snapshotState(failedSnapshot, [
1014
+ {
1015
+ type: 'run_failed',
1016
+ runId: snapshot.runId,
1017
+ revision: snapshot.revision,
1018
+ createdAt,
1019
+ phase,
1020
+ error: failure
1021
+ }
1022
+ ]);
1023
+ }
1024
+ try {
1025
+ await Effect.runPromise(checkNotAborted(options.signal));
1026
+ if (snapshot.status.type === 'paused' ||
1027
+ snapshot.status.type === 'failed') {
1028
+ const phase = snapshot.status.phase;
1029
+ const resumedSnapshot = withRunning(snapshot, phase);
1030
+ snapshot = resumedSnapshot;
1031
+ yield* emitEvents(await snapshotState(resumedSnapshot, []));
1032
+ }
1033
+ if (snapshot.revision === 0 &&
1034
+ snapshot.status.type === 'running' &&
1035
+ snapshot.status.phase === 'run_started') {
1036
+ const runStartedAt = nowIso();
1037
+ const hookResult = await Effect.runPromise(fromAgentResult(() => hooks.onRunStarted?.({
1038
+ ...makeBaseArgs(snapshot),
1039
+ createdAt: runStartedAt
1040
+ })));
1041
+ const hook = await applyHookResult(hookResult);
1042
+ if (hook.events) {
1043
+ yield* emitEvents(hook.events);
1044
+ return;
1045
+ }
1046
+ yield* emitEvents(await snapshotState(snapshot, [phaseEvent('run_started')]));
1047
+ }
1048
+ while (snapshot.status.type === 'running') {
1049
+ await Effect.runPromise(checkNotAborted(options.signal));
1050
+ if (isPaused()) {
1051
+ return;
1052
+ }
1053
+ if (pendingFinish) {
1054
+ const request = pendingFinish;
1055
+ pendingFinish = undefined;
1056
+ yield* completeRun(request);
1057
+ continue;
1058
+ }
1059
+ const phase = snapshot.status
1060
+ .phase;
1061
+ switch (phase) {
1062
+ case 'run_started':
1063
+ case 'turn_completed': {
1064
+ if (phase === 'turn_completed') {
1065
+ const lastTurn = snapshot.turns.at(-1);
1066
+ if (!lastTurn?.modelResult) {
1067
+ throw new Error('Invalid state: turn_completed without a recorded turn.');
1068
+ }
1069
+ if (lastTurn.turn >= options.maxTurns) {
1070
+ yield* completeRun({
1071
+ source: 'max_turns',
1072
+ metadata: { maxTurns: options.maxTurns }
1073
+ });
1074
+ continue;
1075
+ }
1076
+ if (pendingContinue) {
1077
+ pendingContinue = false;
1078
+ }
1079
+ else if (lastTurn.toolCalls.completed.length === 0) {
1080
+ yield* completeRun({ source: 'model_done' });
1081
+ continue;
1082
+ }
1083
+ }
1084
+ const nextTurnNumber = snapshot.turns.length + 1;
1085
+ const prepared = await prepareTurn(nextTurnNumber);
1086
+ yield* emitEvents(prepared.events);
1087
+ continue;
1088
+ }
1089
+ case 'turn_started': {
1090
+ const prepared = await prepareStartedTurn();
1091
+ yield* emitEvents(prepared.events);
1092
+ continue;
1093
+ }
1094
+ case 'turn_prepared':
1095
+ case 'model_started':
1096
+ yield* runModel();
1097
+ continue;
1098
+ case 'model_completed':
1099
+ if ((snapshot.currentTurn?.toolCalls.pending.length ?? 0) === 0) {
1100
+ yield* completeTurn();
1101
+ }
1102
+ else {
1103
+ yield* prepareToolCalls();
1104
+ }
1105
+ continue;
1106
+ case 'tool_calls_started':
1107
+ yield* prepareEachToolCall();
1108
+ continue;
1109
+ case 'tool_call_started':
1110
+ if ((snapshot.currentTurn?.toolCalls.pending.length ?? 0) > 0) {
1111
+ yield* executePreparedTools();
1112
+ }
1113
+ else {
1114
+ yield* completeToolCalls();
1115
+ }
1116
+ continue;
1117
+ case 'tool_call_completed':
1118
+ if ((snapshot.currentTurn?.toolCalls.inFlight.length ?? 0) > 0) {
1119
+ throw new Error('Cannot safely resume while tool calls are in flight.');
1120
+ }
1121
+ yield* completeToolCalls();
1122
+ continue;
1123
+ case 'tool_calls_completed':
1124
+ yield* completeTurn();
1125
+ continue;
1126
+ }
1127
+ }
1128
+ }
1129
+ catch (error) {
1130
+ if (options.signal?.aborted) {
1131
+ throw error;
1132
+ }
1133
+ yield* emitEvents(await failRun(error));
1134
+ throw error;
1135
+ }
1136
+ }