@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/README.md +678 -0
- package/dist/index.d.ts +407 -0
- package/dist/index.js +1136 -0
- package/package.json +62 -0
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
|
+
}
|