@phaseo/agent-sdk 0.1.1 → 0.1.3

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.
@@ -0,0 +1,1070 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { captureAgentRunDevtools } from "../devtools.js";
3
+ import { AgentGatewayError, toAgentGatewayErrorDetails } from "../errors.js";
4
+ class EphemeralCheckpointStore {
5
+ runs = new Map();
6
+ steps = new Map();
7
+ leases = new Map();
8
+ async createRun(run) {
9
+ this.runs.set(run.id, structuredClone(run));
10
+ this.steps.set(run.id, []);
11
+ }
12
+ async getRun(runId) {
13
+ const run = this.runs.get(runId);
14
+ return run ? structuredClone(run) : null;
15
+ }
16
+ async saveRun(run) {
17
+ this.runs.set(run.id, structuredClone(run));
18
+ }
19
+ async listRuns(args) {
20
+ const statuses = Array.isArray(args?.statuses) ? new Set(args.statuses) : null;
21
+ const runs = Array.from(this.runs.values())
22
+ .filter((run) => (args?.agentId ? run.agentId === args.agentId : true))
23
+ .filter((run) => (statuses ? statuses.has(run.status) : true))
24
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
25
+ const limited = typeof args?.limit === "number" && args.limit > 0
26
+ ? runs.slice(0, args.limit)
27
+ : runs;
28
+ return structuredClone(limited);
29
+ }
30
+ async acquireRunLease(args) {
31
+ const now = Date.now();
32
+ const existing = this.leases.get(args.runId);
33
+ const existingExpiresAt = existing ? Date.parse(existing.expiresAt) : 0;
34
+ if (existing &&
35
+ existing.owner !== args.owner &&
36
+ Number.isFinite(existingExpiresAt) &&
37
+ existingExpiresAt > now) {
38
+ return {
39
+ acquired: false,
40
+ lease: structuredClone(existing),
41
+ };
42
+ }
43
+ const lease = {
44
+ owner: args.owner,
45
+ acquiredAt: new Date(now).toISOString(),
46
+ expiresAt: new Date(now + Math.max(1, args.ttlSeconds) * 1_000).toISOString(),
47
+ };
48
+ this.leases.set(args.runId, structuredClone(lease));
49
+ return {
50
+ acquired: true,
51
+ lease: structuredClone(lease),
52
+ };
53
+ }
54
+ async renewRunLease(args) {
55
+ const now = Date.now();
56
+ const existing = this.leases.get(args.runId);
57
+ const existingExpiresAt = existing ? Date.parse(existing.expiresAt) : 0;
58
+ if (!existing ||
59
+ existing.owner !== args.owner ||
60
+ (!Number.isFinite(existingExpiresAt) || existingExpiresAt <= now)) {
61
+ return {
62
+ renewed: false,
63
+ lease: existing ? structuredClone(existing) : null,
64
+ };
65
+ }
66
+ const renewedLease = {
67
+ owner: existing.owner,
68
+ acquiredAt: existing.acquiredAt,
69
+ expiresAt: new Date(now + Math.max(1, args.ttlSeconds) * 1_000).toISOString(),
70
+ };
71
+ this.leases.set(args.runId, structuredClone(renewedLease));
72
+ return {
73
+ renewed: true,
74
+ lease: structuredClone(renewedLease),
75
+ };
76
+ }
77
+ async releaseRunLease(args) {
78
+ const existing = this.leases.get(args.runId);
79
+ if (!existing || existing.owner !== args.owner)
80
+ return false;
81
+ this.leases.delete(args.runId);
82
+ return true;
83
+ }
84
+ async appendStep(step) {
85
+ const existing = this.steps.get(step.runId) ?? [];
86
+ existing.push(structuredClone(step));
87
+ this.steps.set(step.runId, existing);
88
+ }
89
+ async saveStep(step) {
90
+ const existing = this.steps.get(step.runId) ?? [];
91
+ const index = existing.findIndex((entry) => entry.runId === step.runId && entry.index === step.index);
92
+ if (index >= 0) {
93
+ existing[index] = structuredClone(step);
94
+ }
95
+ else {
96
+ existing.push(structuredClone(step));
97
+ }
98
+ this.steps.set(step.runId, existing);
99
+ }
100
+ async listSteps(runId) {
101
+ return structuredClone(this.steps.get(runId) ?? []);
102
+ }
103
+ }
104
+ function toPromptText(input) {
105
+ if (typeof input === "string")
106
+ return input;
107
+ return JSON.stringify(input, null, 2);
108
+ }
109
+ function toPresetModelAlias(preset) {
110
+ if (typeof preset !== "string")
111
+ return undefined;
112
+ const normalized = preset.trim().replace(/^@+/, "");
113
+ return normalized.length > 0 ? `@${normalized}` : undefined;
114
+ }
115
+ function nowIso() {
116
+ return new Date().toISOString();
117
+ }
118
+ function defaultStore() {
119
+ return new EphemeralCheckpointStore();
120
+ }
121
+ const DEFAULT_LEASE_TTL_SECONDS = 300;
122
+ const DEFAULT_MODEL_RETRY_BACKOFF_MS = 250;
123
+ function resolveLeaseConfig() {
124
+ return {
125
+ owner: `lease:${randomUUID()}`,
126
+ ttlSeconds: DEFAULT_LEASE_TTL_SECONDS,
127
+ };
128
+ }
129
+ function resolveModelRetryConfig(args) {
130
+ const config = args.options.modelRetry ?? args.definition.modelRetry;
131
+ return {
132
+ maxRetries: typeof config?.maxRetries === "number" && Number.isFinite(config.maxRetries)
133
+ ? Math.max(0, Math.floor(config.maxRetries))
134
+ : 0,
135
+ backoffMs: typeof config?.backoffMs === "number" && Number.isFinite(config.backoffMs)
136
+ ? Math.max(0, Math.floor(config.backoffMs))
137
+ : DEFAULT_MODEL_RETRY_BACKOFF_MS,
138
+ };
139
+ }
140
+ function resolveToolExecutionConfig(args) {
141
+ const concurrency = args.options.toolExecution?.toolConcurrency ??
142
+ args.definition.toolExecution?.toolConcurrency;
143
+ return {
144
+ toolConcurrency: typeof concurrency === "number" && Number.isFinite(concurrency)
145
+ ? Math.max(1, Math.floor(concurrency))
146
+ : 1,
147
+ };
148
+ }
149
+ function resolveRequestedModelTarget(args) {
150
+ const explicitModel = typeof args.options.model === "string" && args.options.model.trim().length > 0
151
+ ? args.options.model.trim()
152
+ : undefined;
153
+ if (explicitModel)
154
+ return explicitModel;
155
+ const runPresetAlias = toPresetModelAlias(args.options.preset);
156
+ if (runPresetAlias)
157
+ return runPresetAlias;
158
+ const definitionModel = typeof args.definition.model === "string" && args.definition.model.trim().length > 0
159
+ ? args.definition.model.trim()
160
+ : undefined;
161
+ if (definitionModel)
162
+ return definitionModel;
163
+ return toPresetModelAlias(args.definition.preset);
164
+ }
165
+ function startLeaseHeartbeat(args) {
166
+ const intervalMs = Math.max(250, Math.floor(args.ttlSeconds * 1000 * 0.5));
167
+ let stopped = false;
168
+ let renewing = false;
169
+ let failure = null;
170
+ const tick = async () => {
171
+ if (stopped || renewing || failure)
172
+ return;
173
+ renewing = true;
174
+ try {
175
+ const result = await args.store.renewRunLease({
176
+ runId: args.runId,
177
+ owner: args.owner,
178
+ ttlSeconds: args.ttlSeconds,
179
+ });
180
+ if (!result.renewed) {
181
+ failure = new Error(`Run ${args.runId} lease could not be renewed${result.lease?.owner ? `; currently owned by ${result.lease.owner}` : ""}`);
182
+ }
183
+ }
184
+ catch (error) {
185
+ failure = error instanceof Error ? error : new Error(String(error));
186
+ }
187
+ finally {
188
+ renewing = false;
189
+ }
190
+ };
191
+ const timer = setInterval(() => {
192
+ void tick();
193
+ }, intervalMs);
194
+ return {
195
+ stop() {
196
+ stopped = true;
197
+ clearInterval(timer);
198
+ },
199
+ throwIfFailed() {
200
+ if (failure)
201
+ throw failure;
202
+ },
203
+ };
204
+ }
205
+ async function emitEvent(handler, event) {
206
+ if (!handler)
207
+ return;
208
+ await handler(event);
209
+ }
210
+ function toErrorMessage(error) {
211
+ return error instanceof Error ? error.message : String(error);
212
+ }
213
+ function toPersistedErrorDetails(error) {
214
+ return error instanceof AgentGatewayError ? toAgentGatewayErrorDetails(error) : undefined;
215
+ }
216
+ async function sleepWithSignal(delayMs, signal) {
217
+ if (delayMs <= 0)
218
+ return;
219
+ await new Promise((resolve, reject) => {
220
+ if (signal?.aborted) {
221
+ reject(signal.reason ?? new Error("Aborted"));
222
+ return;
223
+ }
224
+ const timer = setTimeout(() => {
225
+ if (signal)
226
+ signal.removeEventListener("abort", onAbort);
227
+ resolve();
228
+ }, delayMs);
229
+ const onAbort = () => {
230
+ clearTimeout(timer);
231
+ signal?.removeEventListener("abort", onAbort);
232
+ reject(signal?.reason ?? new Error("Aborted"));
233
+ };
234
+ signal?.addEventListener("abort", onAbort, { once: true });
235
+ });
236
+ }
237
+ function buildPauseRecord(request) {
238
+ return {
239
+ reason: request.reason,
240
+ payload: request.payload,
241
+ requestedAt: nowIso(),
242
+ };
243
+ }
244
+ function createToolAbortContext(args) {
245
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0
246
+ ? Math.floor(args.timeoutMs)
247
+ : null;
248
+ if (!args.parentSignal && timeoutMs == null) {
249
+ return {
250
+ signal: args.parentSignal,
251
+ cleanup: () => { },
252
+ };
253
+ }
254
+ const controller = new AbortController();
255
+ let timer = null;
256
+ const onAbort = () => {
257
+ controller.abort(args.parentSignal?.reason);
258
+ };
259
+ if (args.parentSignal) {
260
+ if (args.parentSignal.aborted) {
261
+ controller.abort(args.parentSignal.reason);
262
+ }
263
+ else {
264
+ args.parentSignal.addEventListener("abort", onAbort, { once: true });
265
+ }
266
+ }
267
+ if (timeoutMs != null) {
268
+ timer = setTimeout(() => {
269
+ controller.abort(new Error(`Tool ${args.toolName} timed out after ${timeoutMs}ms`));
270
+ }, timeoutMs);
271
+ }
272
+ return {
273
+ signal: controller.signal,
274
+ cleanup: () => {
275
+ if (timer)
276
+ clearTimeout(timer);
277
+ if (args.parentSignal) {
278
+ args.parentSignal.removeEventListener("abort", onAbort);
279
+ }
280
+ },
281
+ };
282
+ }
283
+ async function executeToolWithTimeout(args) {
284
+ const abortContext = createToolAbortContext({
285
+ parentSignal: args.runtimeContext.signal,
286
+ timeoutMs: args.tool.timeoutMs,
287
+ toolName: args.call.name,
288
+ });
289
+ try {
290
+ const maybePromise = Promise.resolve(args.tool.execute(args.call.input, {
291
+ ...args.runtimeContext,
292
+ signal: abortContext.signal,
293
+ }));
294
+ if (args.tool.timeoutMs == null || args.tool.timeoutMs <= 0) {
295
+ return await maybePromise;
296
+ }
297
+ const timeoutMessage = `Tool ${args.call.name} timed out after ${Math.floor(args.tool.timeoutMs)}ms`;
298
+ const timeoutPromise = new Promise((_, reject) => {
299
+ abortContext.signal?.addEventListener("abort", () => {
300
+ reject(abortContext.signal?.reason ?? new Error(timeoutMessage));
301
+ }, { once: true });
302
+ });
303
+ return await Promise.race([maybePromise, timeoutPromise]);
304
+ }
305
+ finally {
306
+ abortContext.cleanup();
307
+ }
308
+ }
309
+ function buildRunResult(args) {
310
+ return {
311
+ run: args.run,
312
+ steps: args.steps,
313
+ output: args.run.result,
314
+ messages: args.run.messages,
315
+ };
316
+ }
317
+ class ObservedRunCancellationError extends Error {
318
+ run;
319
+ constructor(run) {
320
+ super(run.error || `Run ${run.id} was cancelled`);
321
+ this.run = run;
322
+ this.name = "ObservedRunCancellationError";
323
+ }
324
+ }
325
+ async function throwIfRunCancelled(args) {
326
+ const latestRun = await args.store.getRun(args.runId);
327
+ if (!latestRun || latestRun.status !== "cancelled")
328
+ return;
329
+ if (args.activeStep &&
330
+ args.activeStep.status !== "checkpointed" &&
331
+ args.activeStep.status !== "failed" &&
332
+ args.activeStep.status !== "cancelled") {
333
+ args.activeStep.status = "cancelled";
334
+ args.activeStep.error = latestRun.error || "Cancelled";
335
+ args.activeStep.errorDetails = undefined;
336
+ args.activeStep.updatedAt = nowIso();
337
+ await args.store.saveStep(args.activeStep);
338
+ await emitEvent(args.onEvent, {
339
+ type: "step.cancelled",
340
+ runId: args.runId,
341
+ agentId: args.definition.id,
342
+ timestamp: nowIso(),
343
+ status: latestRun.status,
344
+ stepIndex: args.activeStep.index,
345
+ attempt: args.activeStep.modelAttempts,
346
+ requestId: args.activeStep.requestId,
347
+ nativeResponseId: args.activeStep.nativeResponseId ?? null,
348
+ provider: args.activeStep.provider,
349
+ model: args.activeStep.model,
350
+ usage: args.activeStep.usage,
351
+ responseMeta: args.activeStep.responseMeta,
352
+ error: args.activeStep.error,
353
+ });
354
+ }
355
+ await emitEvent(args.onEvent, {
356
+ type: "run.cancelled",
357
+ runId: args.runId,
358
+ agentId: args.definition.id,
359
+ timestamp: nowIso(),
360
+ status: latestRun.status,
361
+ error: latestRun.error,
362
+ });
363
+ throw new ObservedRunCancellationError(latestRun);
364
+ }
365
+ async function executeToolCalls(toolCalls, tools, runtimeContext, toolExecution, emit, runStatus) {
366
+ const { toolConcurrency } = resolveToolExecutionConfig({
367
+ definition: { id: runtimeContext.agentId, toolExecution },
368
+ options: { toolExecution },
369
+ });
370
+ const messages = new Array(toolCalls.length);
371
+ const runOneTool = async (call, index) => {
372
+ const tool = tools.find((entry) => entry.id === call.name);
373
+ if (!tool) {
374
+ await emitEvent(emit, {
375
+ type: "tool.failed",
376
+ runId: runtimeContext.runId,
377
+ agentId: runtimeContext.agentId,
378
+ timestamp: nowIso(),
379
+ status: runStatus ?? "running",
380
+ stepIndex: runtimeContext.stepIndex,
381
+ toolCallId: call.id,
382
+ toolName: call.name,
383
+ error: `Tool not found: ${call.name}`,
384
+ });
385
+ throw new Error(`Tool not found: ${call.name}`);
386
+ }
387
+ await emitEvent(emit, {
388
+ type: "tool.started",
389
+ runId: runtimeContext.runId,
390
+ agentId: runtimeContext.agentId,
391
+ timestamp: nowIso(),
392
+ status: runStatus ?? "running",
393
+ stepIndex: runtimeContext.stepIndex,
394
+ toolCallId: call.id,
395
+ toolName: call.name,
396
+ });
397
+ let output;
398
+ try {
399
+ output = await executeToolWithTimeout({
400
+ tool,
401
+ call,
402
+ runtimeContext,
403
+ });
404
+ }
405
+ catch (error) {
406
+ await emitEvent(emit, {
407
+ type: "tool.failed",
408
+ runId: runtimeContext.runId,
409
+ agentId: runtimeContext.agentId,
410
+ timestamp: nowIso(),
411
+ status: runStatus ?? "running",
412
+ stepIndex: runtimeContext.stepIndex,
413
+ toolCallId: call.id,
414
+ toolName: call.name,
415
+ error: toErrorMessage(error),
416
+ });
417
+ throw error;
418
+ }
419
+ await emitEvent(emit, {
420
+ type: "tool.completed",
421
+ runId: runtimeContext.runId,
422
+ agentId: runtimeContext.agentId,
423
+ timestamp: nowIso(),
424
+ status: runStatus ?? "running",
425
+ stepIndex: runtimeContext.stepIndex,
426
+ toolCallId: call.id,
427
+ toolName: call.name,
428
+ output,
429
+ });
430
+ messages[index] = {
431
+ role: "tool",
432
+ name: call.name,
433
+ toolCallId: call.id,
434
+ content: typeof output === "string" ? output : JSON.stringify(output, null, 2),
435
+ };
436
+ };
437
+ if (toolConcurrency <= 1 || toolCalls.length <= 1) {
438
+ for (const [index, call] of toolCalls.entries()) {
439
+ await runOneTool(call, index);
440
+ }
441
+ return messages;
442
+ }
443
+ let nextIndex = 0;
444
+ const workerCount = Math.min(toolConcurrency, toolCalls.length);
445
+ await Promise.all(Array.from({ length: workerCount }, async () => {
446
+ while (nextIndex < toolCalls.length) {
447
+ const currentIndex = nextIndex;
448
+ nextIndex += 1;
449
+ await runOneTool(toolCalls[currentIndex], currentIndex);
450
+ }
451
+ }));
452
+ return messages;
453
+ }
454
+ async function generateModelResponseWithRetries(args) {
455
+ const retryConfig = resolveModelRetryConfig({
456
+ definition: args.definition,
457
+ options: args.options,
458
+ });
459
+ const totalAttempts = retryConfig.maxRetries + 1;
460
+ const modelTarget = resolveRequestedModelTarget({
461
+ definition: args.definition,
462
+ options: args.options,
463
+ });
464
+ for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {
465
+ args.step.modelAttempts = attempt;
466
+ await emitEvent(args.options.onEvent, {
467
+ type: "model.requested",
468
+ runId: args.run.id,
469
+ agentId: args.definition.id,
470
+ timestamp: nowIso(),
471
+ status: args.run.status,
472
+ stepIndex: args.stepIndex,
473
+ attempt,
474
+ model: modelTarget,
475
+ });
476
+ try {
477
+ const response = await args.options.client.generate({
478
+ agentId: args.definition.id,
479
+ model: modelTarget,
480
+ instructions: args.definition.instructions,
481
+ messages: args.run.messages,
482
+ tools: args.tools.map((tool) => ({
483
+ id: tool.id,
484
+ description: tool.description,
485
+ parameters: tool.parameters,
486
+ })),
487
+ context: args.options.context,
488
+ signal: args.options.signal,
489
+ });
490
+ args.leaseHeartbeat.throwIfFailed();
491
+ return response;
492
+ }
493
+ catch (error) {
494
+ args.leaseHeartbeat.throwIfFailed();
495
+ if (attempt >= totalAttempts) {
496
+ await emitEvent(args.options.onEvent, {
497
+ type: "model.failed",
498
+ runId: args.run.id,
499
+ agentId: args.definition.id,
500
+ timestamp: nowIso(),
501
+ status: args.run.status,
502
+ stepIndex: args.stepIndex,
503
+ attempt,
504
+ model: modelTarget,
505
+ error: toErrorMessage(error),
506
+ errorDetails: toPersistedErrorDetails(error),
507
+ });
508
+ throw error;
509
+ }
510
+ const backoffMs = retryConfig.backoffMs * attempt;
511
+ await sleepWithSignal(backoffMs, args.options.signal);
512
+ args.leaseHeartbeat.throwIfFailed();
513
+ }
514
+ }
515
+ throw new Error("Unreachable model retry state");
516
+ }
517
+ async function resumeAgentInternal(definition, runId, options, emitRunResumedEvent) {
518
+ const store = options.store;
519
+ const run = await store.getRun(runId);
520
+ if (!run) {
521
+ throw new Error(`Run not found: ${runId}`);
522
+ }
523
+ if (run.status === "completed") {
524
+ const steps = await store.listSteps(runId);
525
+ return buildRunResult({
526
+ run: run,
527
+ steps,
528
+ });
529
+ }
530
+ if (run.status === "cancelled") {
531
+ throw new Error(run.error || `Run ${runId} was cancelled`);
532
+ }
533
+ const leaseConfig = resolveLeaseConfig();
534
+ const leaseResult = await store.acquireRunLease({
535
+ runId,
536
+ owner: leaseConfig.owner,
537
+ ttlSeconds: leaseConfig.ttlSeconds,
538
+ });
539
+ if (!leaseResult.acquired) {
540
+ throw new Error(`Run ${runId} is already leased by ${leaseResult.lease?.owner ?? "another worker"}`);
541
+ }
542
+ const leaseHeartbeat = startLeaseHeartbeat({
543
+ store,
544
+ runId,
545
+ owner: leaseConfig.owner,
546
+ ttlSeconds: leaseConfig.ttlSeconds,
547
+ });
548
+ let activeStep = null;
549
+ try {
550
+ const previousStatus = run.status;
551
+ if (run.status === "waiting_for_human" && !options.humanInput) {
552
+ throw new Error(`Run ${runId} is waiting for human input${run.pause?.reason ? `: ${run.pause.reason}` : ""}`);
553
+ }
554
+ if (options.humanInput) {
555
+ run.messages.push({ role: "user", content: options.humanInput });
556
+ run.pause = null;
557
+ }
558
+ run.status = "running";
559
+ run.updatedAt = nowIso();
560
+ await store.saveRun(run);
561
+ if (emitRunResumedEvent) {
562
+ await emitEvent(options.onEvent, {
563
+ type: "run.resumed",
564
+ runId,
565
+ agentId: definition.id,
566
+ timestamp: nowIso(),
567
+ status: run.status,
568
+ previousStatus,
569
+ });
570
+ }
571
+ const tools = definition.tools ?? [];
572
+ const maxSteps = options.maxSteps ?? definition.maxSteps ?? 12;
573
+ const steps = await store.listSteps(runId);
574
+ for (let stepIndex = run.stepCount; stepIndex < maxSteps; stepIndex += 1) {
575
+ await throwIfRunCancelled({
576
+ store,
577
+ runId,
578
+ definition,
579
+ onEvent: options.onEvent,
580
+ activeStep,
581
+ });
582
+ leaseHeartbeat.throwIfFailed();
583
+ const step = {
584
+ runId,
585
+ index: stepIndex,
586
+ status: "executing_model",
587
+ createdAt: nowIso(),
588
+ updatedAt: nowIso(),
589
+ };
590
+ activeStep = step;
591
+ await store.appendStep(step);
592
+ await emitEvent(options.onEvent, {
593
+ type: "step.started",
594
+ runId,
595
+ agentId: definition.id,
596
+ timestamp: nowIso(),
597
+ status: run.status,
598
+ stepIndex,
599
+ });
600
+ const response = await generateModelResponseWithRetries({
601
+ definition,
602
+ run,
603
+ options,
604
+ step,
605
+ stepIndex,
606
+ tools,
607
+ leaseHeartbeat,
608
+ });
609
+ await throwIfRunCancelled({
610
+ store,
611
+ runId,
612
+ definition,
613
+ onEvent: options.onEvent,
614
+ activeStep,
615
+ });
616
+ run.messages.push(response.message);
617
+ run.stepCount = stepIndex + 1;
618
+ run.updatedAt = nowIso();
619
+ step.requestId = response.requestId;
620
+ step.nativeResponseId = response.nativeResponseId ?? null;
621
+ step.provider = response.provider;
622
+ step.model =
623
+ response.model ??
624
+ resolveRequestedModelTarget({
625
+ definition,
626
+ options,
627
+ });
628
+ step.usage = response.usage;
629
+ step.toolCalls = response.message.toolCalls;
630
+ step.responseMeta = response.responseMeta;
631
+ await emitEvent(options.onEvent, {
632
+ type: "model.completed",
633
+ runId,
634
+ agentId: definition.id,
635
+ timestamp: nowIso(),
636
+ status: run.status,
637
+ stepIndex,
638
+ attempt: step.modelAttempts,
639
+ requestId: response.requestId,
640
+ nativeResponseId: response.nativeResponseId ?? null,
641
+ provider: response.provider,
642
+ model: response.model ??
643
+ resolveRequestedModelTarget({
644
+ definition,
645
+ options,
646
+ }),
647
+ usage: response.usage,
648
+ responseMeta: response.responseMeta,
649
+ });
650
+ await throwIfRunCancelled({
651
+ store,
652
+ runId,
653
+ definition,
654
+ onEvent: options.onEvent,
655
+ activeStep,
656
+ });
657
+ const parsedOutput = !response.message.toolCalls?.length && definition.parseOutput
658
+ ? definition.parseOutput(response.message.content)
659
+ : undefined;
660
+ if (definition.humanReview) {
661
+ const pauseRequest = await definition.humanReview({
662
+ runId,
663
+ agentId: definition.id,
664
+ stepIndex,
665
+ input: run.input,
666
+ context: options.context,
667
+ messages: [...run.messages],
668
+ response,
669
+ parsedOutput,
670
+ });
671
+ await throwIfRunCancelled({
672
+ store,
673
+ runId,
674
+ definition,
675
+ onEvent: options.onEvent,
676
+ activeStep,
677
+ });
678
+ if (pauseRequest) {
679
+ run.status = "waiting_for_human";
680
+ run.pause = buildPauseRecord(pauseRequest);
681
+ run.updatedAt = nowIso();
682
+ await store.saveRun(run);
683
+ step.status = "checkpointed";
684
+ step.updatedAt = nowIso();
685
+ await store.saveStep(step);
686
+ await store.saveRun(run);
687
+ await emitEvent(options.onEvent, {
688
+ type: "step.completed",
689
+ runId,
690
+ agentId: definition.id,
691
+ timestamp: nowIso(),
692
+ status: run.status,
693
+ stepIndex,
694
+ attempt: step.modelAttempts,
695
+ requestId: response.requestId,
696
+ nativeResponseId: response.nativeResponseId ?? null,
697
+ provider: response.provider,
698
+ model: response.model ??
699
+ resolveRequestedModelTarget({
700
+ definition,
701
+ options,
702
+ }),
703
+ usage: response.usage,
704
+ responseMeta: response.responseMeta,
705
+ });
706
+ await emitEvent(options.onEvent, {
707
+ type: "checkpoint.saved",
708
+ runId,
709
+ agentId: definition.id,
710
+ timestamp: nowIso(),
711
+ status: run.status,
712
+ stepIndex,
713
+ requestId: response.requestId,
714
+ nativeResponseId: response.nativeResponseId ?? null,
715
+ provider: response.provider,
716
+ model: response.model ??
717
+ resolveRequestedModelTarget({
718
+ definition,
719
+ options,
720
+ }),
721
+ usage: response.usage,
722
+ responseMeta: response.responseMeta,
723
+ });
724
+ await emitEvent(options.onEvent, {
725
+ type: "run.waiting_for_human",
726
+ runId,
727
+ agentId: definition.id,
728
+ timestamp: nowIso(),
729
+ status: run.status,
730
+ stepIndex,
731
+ pause: run.pause,
732
+ });
733
+ return buildRunResult({
734
+ run: run,
735
+ steps: await store.listSteps(runId),
736
+ });
737
+ }
738
+ }
739
+ if (Array.isArray(response.message.toolCalls) && response.message.toolCalls.length > 0) {
740
+ step.status = "executing_tools";
741
+ step.updatedAt = nowIso();
742
+ const toolMessages = await executeToolCalls(response.message.toolCalls, tools, {
743
+ runId,
744
+ agentId: definition.id,
745
+ stepIndex,
746
+ context: options.context,
747
+ signal: options.signal,
748
+ }, options.toolExecution ?? definition.toolExecution, options.onEvent, run.status);
749
+ leaseHeartbeat.throwIfFailed();
750
+ run.messages.push(...toolMessages);
751
+ await throwIfRunCancelled({
752
+ store,
753
+ runId,
754
+ definition,
755
+ onEvent: options.onEvent,
756
+ activeStep,
757
+ });
758
+ run.status = "waiting_for_tools";
759
+ run.updatedAt = nowIso();
760
+ await store.saveRun(run);
761
+ step.status = "checkpointed";
762
+ step.updatedAt = nowIso();
763
+ await store.saveStep(step);
764
+ await store.saveRun(run);
765
+ await emitEvent(options.onEvent, {
766
+ type: "step.completed",
767
+ runId,
768
+ agentId: definition.id,
769
+ timestamp: nowIso(),
770
+ status: run.status,
771
+ stepIndex,
772
+ attempt: step.modelAttempts,
773
+ requestId: response.requestId,
774
+ nativeResponseId: response.nativeResponseId ?? null,
775
+ provider: response.provider,
776
+ model: response.model ??
777
+ resolveRequestedModelTarget({
778
+ definition,
779
+ options,
780
+ }),
781
+ usage: response.usage,
782
+ responseMeta: response.responseMeta,
783
+ });
784
+ await emitEvent(options.onEvent, {
785
+ type: "checkpoint.saved",
786
+ runId,
787
+ agentId: definition.id,
788
+ timestamp: nowIso(),
789
+ status: run.status,
790
+ stepIndex,
791
+ requestId: response.requestId,
792
+ nativeResponseId: response.nativeResponseId ?? null,
793
+ provider: response.provider,
794
+ model: response.model ??
795
+ resolveRequestedModelTarget({
796
+ definition,
797
+ options,
798
+ }),
799
+ usage: response.usage,
800
+ responseMeta: response.responseMeta,
801
+ });
802
+ activeStep = null;
803
+ continue;
804
+ }
805
+ const outputText = response.message.content;
806
+ const output = parsedOutput !== undefined
807
+ ? parsedOutput
808
+ : definition.parseOutput
809
+ ? definition.parseOutput(outputText)
810
+ : outputText;
811
+ run.result = output;
812
+ run.status = "completed";
813
+ run.pause = null;
814
+ run.updatedAt = nowIso();
815
+ await store.saveRun(run);
816
+ step.status = "checkpointed";
817
+ step.updatedAt = nowIso();
818
+ await store.saveStep(step);
819
+ await store.saveRun(run);
820
+ await emitEvent(options.onEvent, {
821
+ type: "step.completed",
822
+ runId,
823
+ agentId: definition.id,
824
+ timestamp: nowIso(),
825
+ status: run.status,
826
+ stepIndex,
827
+ attempt: step.modelAttempts,
828
+ requestId: response.requestId,
829
+ nativeResponseId: response.nativeResponseId ?? null,
830
+ provider: response.provider,
831
+ model: response.model ??
832
+ resolveRequestedModelTarget({
833
+ definition,
834
+ options,
835
+ }),
836
+ usage: response.usage,
837
+ responseMeta: response.responseMeta,
838
+ });
839
+ await emitEvent(options.onEvent, {
840
+ type: "checkpoint.saved",
841
+ runId,
842
+ agentId: definition.id,
843
+ timestamp: nowIso(),
844
+ status: run.status,
845
+ stepIndex,
846
+ requestId: response.requestId,
847
+ nativeResponseId: response.nativeResponseId ?? null,
848
+ provider: response.provider,
849
+ model: response.model ??
850
+ resolveRequestedModelTarget({
851
+ definition,
852
+ options,
853
+ }),
854
+ usage: response.usage,
855
+ responseMeta: response.responseMeta,
856
+ });
857
+ await emitEvent(options.onEvent, {
858
+ type: "run.completed",
859
+ runId,
860
+ agentId: definition.id,
861
+ timestamp: nowIso(),
862
+ status: run.status,
863
+ output,
864
+ });
865
+ return buildRunResult({
866
+ run: run,
867
+ steps: await store.listSteps(runId),
868
+ });
869
+ }
870
+ run.status = "failed";
871
+ run.error = `Max steps exceeded (${maxSteps})`;
872
+ run.errorDetails = undefined;
873
+ run.updatedAt = nowIso();
874
+ await store.saveRun(run);
875
+ await emitEvent(options.onEvent, {
876
+ type: "run.failed",
877
+ runId,
878
+ agentId: definition.id,
879
+ timestamp: nowIso(),
880
+ status: run.status,
881
+ error: run.error,
882
+ errorDetails: run.errorDetails,
883
+ });
884
+ throw new Error(run.error);
885
+ }
886
+ catch (error) {
887
+ if (error instanceof ObservedRunCancellationError) {
888
+ return buildRunResult({
889
+ run: error.run,
890
+ steps: await store.listSteps(runId),
891
+ });
892
+ }
893
+ const latestRun = await store.getRun(runId);
894
+ const latestStatus = latestRun?.status;
895
+ if (latestStatus !== "completed" &&
896
+ latestStatus !== "cancelled" &&
897
+ latestStatus !== "failed") {
898
+ const failureMessage = toErrorMessage(error);
899
+ const errorDetails = toPersistedErrorDetails(error);
900
+ if (activeStep && activeStep.status !== "checkpointed" && activeStep.status !== "failed") {
901
+ activeStep.status = "failed";
902
+ activeStep.error = failureMessage;
903
+ activeStep.errorDetails = errorDetails;
904
+ activeStep.updatedAt = nowIso();
905
+ await store.saveStep(activeStep);
906
+ await emitEvent(options.onEvent, {
907
+ type: "step.failed",
908
+ runId,
909
+ agentId: definition.id,
910
+ timestamp: nowIso(),
911
+ status: "failed",
912
+ stepIndex: activeStep.index,
913
+ attempt: activeStep.modelAttempts,
914
+ requestId: activeStep.requestId,
915
+ nativeResponseId: activeStep.nativeResponseId ?? null,
916
+ provider: activeStep.provider,
917
+ model: activeStep.model,
918
+ usage: activeStep.usage,
919
+ error: failureMessage,
920
+ errorDetails,
921
+ });
922
+ }
923
+ run.status = "failed";
924
+ run.error = failureMessage;
925
+ run.errorDetails = errorDetails;
926
+ run.updatedAt = nowIso();
927
+ await store.saveRun(run);
928
+ await emitEvent(options.onEvent, {
929
+ type: "run.failed",
930
+ runId,
931
+ agentId: definition.id,
932
+ timestamp: nowIso(),
933
+ status: run.status,
934
+ error: failureMessage,
935
+ errorDetails,
936
+ });
937
+ }
938
+ throw error;
939
+ }
940
+ finally {
941
+ leaseHeartbeat.stop();
942
+ await store.releaseRunLease({
943
+ runId,
944
+ owner: leaseConfig.owner,
945
+ });
946
+ }
947
+ }
948
+ export async function runAgent(definition, options) {
949
+ const startedAt = Date.now();
950
+ const runId = randomUUID();
951
+ const createdAt = nowIso();
952
+ const store = defaultStore();
953
+ const messages = [];
954
+ if (definition.instructions) {
955
+ messages.push({ role: "system", content: definition.instructions });
956
+ }
957
+ messages.push({ role: "user", content: toPromptText(options.input) });
958
+ const run = {
959
+ id: runId,
960
+ agentId: definition.id,
961
+ status: "queued",
962
+ input: options.input,
963
+ context: options.context,
964
+ messages,
965
+ pause: null,
966
+ createdAt,
967
+ updatedAt: createdAt,
968
+ stepCount: 0,
969
+ };
970
+ await store.createRun(run);
971
+ await emitEvent(options.onEvent, {
972
+ type: "run.started",
973
+ runId,
974
+ agentId: definition.id,
975
+ timestamp: nowIso(),
976
+ status: run.status,
977
+ });
978
+ try {
979
+ const result = await resumeAgentInternal(definition, runId, {
980
+ client: options.client,
981
+ store,
982
+ context: options.context,
983
+ model: options.model,
984
+ preset: options.preset,
985
+ maxSteps: options.maxSteps,
986
+ modelRetry: options.modelRetry,
987
+ toolExecution: options.toolExecution,
988
+ signal: options.signal,
989
+ onEvent: options.onEvent,
990
+ }, false);
991
+ captureAgentRunDevtools({
992
+ type: "agent.run",
993
+ definition,
994
+ options,
995
+ startedAt,
996
+ result,
997
+ runId,
998
+ });
999
+ return result;
1000
+ }
1001
+ catch (error) {
1002
+ captureAgentRunDevtools({
1003
+ type: "agent.run",
1004
+ definition,
1005
+ options,
1006
+ startedAt,
1007
+ error,
1008
+ runId,
1009
+ });
1010
+ throw error;
1011
+ }
1012
+ }
1013
+ export async function continueAgent(definition, options) {
1014
+ const startedAt = Date.now();
1015
+ if (options.run.run.agentId !== definition.id) {
1016
+ throw new Error(`Cannot continue run ${options.run.run.id} with agent ${definition.id}; it belongs to ${options.run.run.agentId}`);
1017
+ }
1018
+ const store = defaultStore();
1019
+ await store.createRun(options.run.run);
1020
+ for (const step of options.run.steps) {
1021
+ await store.appendStep(step);
1022
+ }
1023
+ const captureOptions = {
1024
+ input: options.run.run.input,
1025
+ context: options.context === undefined
1026
+ ? options.run.run.context
1027
+ : options.context,
1028
+ model: options.model,
1029
+ preset: options.preset,
1030
+ maxSteps: options.maxSteps,
1031
+ devtools: options.devtools,
1032
+ };
1033
+ try {
1034
+ const result = await resumeAgentInternal(definition, options.run.run.id, {
1035
+ client: options.client,
1036
+ store,
1037
+ context: options.context === undefined
1038
+ ? options.run.run.context
1039
+ : options.context,
1040
+ model: options.model,
1041
+ preset: options.preset,
1042
+ maxSteps: options.maxSteps,
1043
+ modelRetry: options.modelRetry,
1044
+ toolExecution: options.toolExecution,
1045
+ signal: options.signal,
1046
+ humanInput: options.humanInput,
1047
+ onEvent: options.onEvent,
1048
+ }, true);
1049
+ captureAgentRunDevtools({
1050
+ type: "agent.continue",
1051
+ definition,
1052
+ options: captureOptions,
1053
+ startedAt,
1054
+ result,
1055
+ runId: options.run.run.id,
1056
+ });
1057
+ return result;
1058
+ }
1059
+ catch (error) {
1060
+ captureAgentRunDevtools({
1061
+ type: "agent.continue",
1062
+ definition,
1063
+ options: captureOptions,
1064
+ startedAt,
1065
+ error,
1066
+ runId: options.run.run.id,
1067
+ });
1068
+ throw error;
1069
+ }
1070
+ }