@zhivex-ai/core 0.17.0 → 0.19.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.
Files changed (83) hide show
  1. package/dist/agent-handoff.d.ts +2 -0
  2. package/dist/agent-handoff.d.ts.map +1 -1
  3. package/dist/agent-handoff.js +8 -3
  4. package/dist/agent-handoff.js.map +1 -1
  5. package/dist/agent-state.d.ts +6 -0
  6. package/dist/agent-state.d.ts.map +1 -0
  7. package/dist/agent-state.js +374 -0
  8. package/dist/agent-state.js.map +1 -0
  9. package/dist/agent-store.d.ts +5 -3
  10. package/dist/agent-store.d.ts.map +1 -1
  11. package/dist/agent-store.js +1027 -145
  12. package/dist/agent-store.js.map +1 -1
  13. package/dist/agent.d.ts +1 -0
  14. package/dist/agent.d.ts.map +1 -1
  15. package/dist/agent.js +698 -229
  16. package/dist/agent.js.map +1 -1
  17. package/dist/api-stability.d.ts.map +1 -1
  18. package/dist/api-stability.js +10 -0
  19. package/dist/api-stability.js.map +1 -1
  20. package/dist/artifact.d.ts.map +1 -1
  21. package/dist/artifact.js +2 -1
  22. package/dist/artifact.js.map +1 -1
  23. package/dist/audio.d.ts +2 -1
  24. package/dist/audio.d.ts.map +1 -1
  25. package/dist/audio.js +25 -0
  26. package/dist/audio.js.map +1 -1
  27. package/dist/bounded-broadcast.d.ts +45 -0
  28. package/dist/bounded-broadcast.d.ts.map +1 -0
  29. package/dist/bounded-broadcast.js +163 -0
  30. package/dist/bounded-broadcast.js.map +1 -0
  31. package/dist/catalog.d.ts.map +1 -1
  32. package/dist/catalog.js +119 -14
  33. package/dist/catalog.js.map +1 -1
  34. package/dist/generate-object.d.ts.map +1 -1
  35. package/dist/generate-object.js +26 -88
  36. package/dist/generate-object.js.map +1 -1
  37. package/dist/generate-text.d.ts +9 -1
  38. package/dist/generate-text.d.ts.map +1 -1
  39. package/dist/generate-text.js +118 -55
  40. package/dist/generate-text.js.map +1 -1
  41. package/dist/google.d.ts +12 -1
  42. package/dist/google.d.ts.map +1 -1
  43. package/dist/google.js +27 -0
  44. package/dist/google.js.map +1 -1
  45. package/dist/index.d.ts +6 -4
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +4 -3
  48. package/dist/index.js.map +1 -1
  49. package/dist/live-agent.d.ts.map +1 -1
  50. package/dist/live-agent.js +31 -54
  51. package/dist/live-agent.js.map +1 -1
  52. package/dist/messages.d.ts +4 -1
  53. package/dist/messages.d.ts.map +1 -1
  54. package/dist/messages.js.map +1 -1
  55. package/dist/realtime.d.ts.map +1 -1
  56. package/dist/realtime.js +39 -55
  57. package/dist/realtime.js.map +1 -1
  58. package/dist/response.js +1 -1
  59. package/dist/response.js.map +1 -1
  60. package/dist/runner.d.ts.map +1 -1
  61. package/dist/runner.js +2 -1
  62. package/dist/runner.js.map +1 -1
  63. package/dist/safety-policy.d.ts +41 -1
  64. package/dist/safety-policy.d.ts.map +1 -1
  65. package/dist/safety-policy.js +114 -8
  66. package/dist/safety-policy.js.map +1 -1
  67. package/dist/secure-id.d.ts +2 -0
  68. package/dist/secure-id.d.ts.map +1 -0
  69. package/dist/secure-id.js +3 -0
  70. package/dist/secure-id.js.map +1 -0
  71. package/dist/types.d.ts +289 -8
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/ui.d.ts.map +1 -1
  74. package/dist/ui.js +2 -1
  75. package/dist/ui.js.map +1 -1
  76. package/dist/workflow.d.ts.map +1 -1
  77. package/dist/workflow.js +2 -1
  78. package/dist/workflow.js.map +1 -1
  79. package/package.json +1 -1
  80. package/dist/durable-store-migration.d.ts +0 -26
  81. package/dist/durable-store-migration.d.ts.map +0 -1
  82. package/dist/durable-store-migration.js +0 -158
  83. package/dist/durable-store-migration.js.map +0 -1
package/dist/agent.js CHANGED
@@ -1,14 +1,21 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { createAgentApprovalMessage, getAgentApprovalRequests } from "./agent-approval.js";
2
3
  import { createAgentHandoffMessage } from "./agent-handoff.js";
3
- import { GuardrailTriggeredError, ValidationError } from "./errors.js";
4
- import { generateText, normalizeMessages, streamText } from "./generate-text.js";
5
- import { serializeJsonValue } from "./messages.js";
4
+ import { AGENT_RUN_STATE_SCHEMA_VERSION, normalizeAgentRunState } from "./agent-state.js";
5
+ import { BoundedReplayBroadcast } from "./bounded-broadcast.js";
6
+ import { ConflictError, GuardrailTriggeredError, ValidationError } from "./errors.js";
7
+ import { aggregateTokenUsage, generateText, getGenerateTextStepTiming, normalizeMessages, streamText } from "./generate-text.js";
8
+ import { isCallableToolDefinition, serializeJsonValue } from "./messages.js";
6
9
  import { mergeAbortSignals } from "./runtime.js";
10
+ import { evaluateAgentBudgetPreflight, getAgentBudgetStatus } from "./safety-policy.js";
11
+ import { createSecureId } from "./secure-id.js";
7
12
  import { toToolSet } from "./tool-registry.js";
8
13
  import { z } from "zod";
9
- const randomId = (prefix) => `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
10
- const AGENT_RUN_STATE_SCHEMA_VERSION = 1;
14
+ const randomId = createSecureId;
11
15
  const AGENT_GROUP_FAIL_FAST_ABORT_MESSAGE = "Agent group member aborted after fail-fast.";
16
+ const DEFAULT_AGENT_LEASE_TTL_MS = 30_000;
17
+ const DEFAULT_AGENT_CANCELLATION_POLL_MS = 1_000;
18
+ const DEFAULT_AGENT_MAX_STATE_BYTES = 4 * 1024 * 1024;
12
19
  class AgentPolicyTimeoutError extends Error {
13
20
  constructor(timeoutMs) {
14
21
  super(`Agent run timed out after ${timeoutMs}ms.`);
@@ -20,8 +27,9 @@ const joinInstructions = (...parts) => {
20
27
  return content.length ? content.join("\n\n") : undefined;
21
28
  };
22
29
  const hasToolCalls = (messages) => messages.some((message) => message.parts.some((part) => part.type === "tool-call"));
23
- const snapshotRequest = (request) => ({
24
- messages: request.messages,
30
+ const snapshotRequest = (request, messageOffset = 0, messages = request.messages) => ({
31
+ messageOffset,
32
+ messages,
25
33
  toolChoice: request.toolChoice,
26
34
  toolExecution: request.toolExecution,
27
35
  temperature: request.temperature,
@@ -42,18 +50,23 @@ const snapshotResponse = (response) => ({
42
50
  const countToolCalls = (messages) => messages.reduce((total, message) => total + message.parts.filter((part) => part.type === "tool-call").length, 0);
43
51
  const mapSteps = (steps, offset, toolResults) => {
44
52
  let toolResultCursor = 0;
53
+ let previousMessageCount = 0;
45
54
  return steps.map((step, index) => {
46
55
  const response = snapshotResponse(step.response);
47
56
  const toolCallCount = countToolCalls(response.messages);
48
57
  const stepToolResults = toolResults.slice(toolResultCursor, toolResultCursor + toolCallCount);
49
58
  toolResultCursor += toolCallCount;
50
- const finishedAt = Date.now();
59
+ const timing = getGenerateTextStepTiming(step.request);
60
+ const finishedAt = timing?.finishedAt ?? Date.now();
61
+ const messageOffset = index === 0 ? 0 : previousMessageCount;
62
+ const incrementalMessages = step.request.messages.slice(messageOffset);
63
+ previousMessageCount = step.request.messages.length;
51
64
  return {
52
65
  index: offset + index + 1,
53
66
  status: "completed",
54
- startedAt: finishedAt,
67
+ startedAt: timing?.startedAt ?? finishedAt,
55
68
  finishedAt,
56
- request: snapshotRequest(step.request),
69
+ request: snapshotRequest(step.request, messageOffset, incrementalMessages),
57
70
  response,
58
71
  toolResults: stepToolResults
59
72
  };
@@ -75,17 +88,15 @@ const toOutput = (state) => ({
75
88
  state,
76
89
  error: state.error
77
90
  });
78
- const normalizeRunState = (state) => ({
79
- ...state,
80
- schemaVersion: AGENT_RUN_STATE_SCHEMA_VERSION
81
- });
82
91
  const normalizeApprovalStatus = (status) => status === "suspended" ? "waiting_approval" : status;
83
- const cloneState = (state) => JSON.parse(JSON.stringify(normalizeRunState(state)));
84
- const createBaseState = (provider, modelId, initialMessages, maxSteps, metadata, agentId, runId, handoff, parentRunId, idempotencyKey) => {
92
+ const cloneState = (state) => JSON.parse(JSON.stringify(normalizeAgentRunState(state)));
93
+ const createBaseState = (provider, modelId, initialMessages, maxSteps, metadata, agentId, runId, handoff, parentRunId, idempotencyKey, scope) => {
85
94
  const startedAt = Date.now();
86
95
  return {
87
96
  schemaVersion: AGENT_RUN_STATE_SCHEMA_VERSION,
97
+ revision: 0,
88
98
  runId,
99
+ scope,
89
100
  idempotencyKey,
90
101
  agentId,
91
102
  parentRunId: parentRunId ?? handoff?.fromRunId,
@@ -115,6 +126,15 @@ const ensureValidStateInput = (input) => {
115
126
  if (input.prompt !== undefined || input.messages !== undefined || input.system !== undefined || input.handoff !== undefined) {
116
127
  throw new ValidationError('Pass either "state" or a fresh "prompt"/"messages" input, but not both.');
117
128
  }
129
+ const stateScope = input.state?.scope;
130
+ const inputScope = input.scope;
131
+ if (stateScope &&
132
+ inputScope &&
133
+ (stateScope.tenantId !== inputScope.tenantId ||
134
+ stateScope.userId !== inputScope.userId ||
135
+ stateScope.namespace !== inputScope.namespace)) {
136
+ throw new ValidationError('The provided agent state belongs to a different tenant/user scope.');
137
+ }
118
138
  };
119
139
  const ensureValidIdempotencyInput = (input, store) => {
120
140
  if (!input.idempotencyKey) {
@@ -123,8 +143,46 @@ const ensureValidIdempotencyInput = (input, store) => {
123
143
  if (!store) {
124
144
  throw new ValidationError('The "idempotencyKey" option requires an agent run "store".');
125
145
  }
126
- if (!store.findByIdempotencyKey) {
127
- throw new ValidationError('The agent run "store" must implement "findByIdempotencyKey()" to use "idempotencyKey".');
146
+ if (!store.claimIdempotencyKey) {
147
+ throw new ValidationError('The agent run "store" must implement "claimIdempotencyKey()" to use "idempotencyKey" safely.');
148
+ }
149
+ };
150
+ const ensureValidScope = (scope) => {
151
+ if (!scope)
152
+ return;
153
+ if (typeof scope.tenantId !== "string" || scope.tenantId.length === 0) {
154
+ throw new ValidationError('Agent scope "tenantId" must be a non-empty string.');
155
+ }
156
+ for (const field of ["userId", "namespace"]) {
157
+ if (scope[field] !== undefined && (typeof scope[field] !== "string" || scope[field].length === 0)) {
158
+ throw new ValidationError(`Agent scope "${field}" must be a non-empty string when provided.`);
159
+ }
160
+ }
161
+ };
162
+ const invokeOperationalHook = async (agent, source, operation, runId, callback, fallback) => {
163
+ if (!callback) {
164
+ return fallback;
165
+ }
166
+ try {
167
+ return await callback();
168
+ }
169
+ catch (error) {
170
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
171
+ try {
172
+ await agent.hookFailurePolicy?.onError?.({
173
+ source,
174
+ operation,
175
+ runId,
176
+ error: normalizedError
177
+ });
178
+ }
179
+ catch {
180
+ // Reporting an observer failure must never recursively fail the run.
181
+ }
182
+ if (agent.hookFailurePolicy?.[source] === "fail") {
183
+ throw normalizedError;
184
+ }
185
+ return fallback;
128
186
  }
129
187
  };
130
188
  const injectContextMessages = (messages, extraMessages) => {
@@ -146,13 +204,14 @@ const prepareFreshMessages = async (agent, input, runId) => {
146
204
  ? [createAgentHandoffMessage(input.handoff), ...input.handoff.contextMessages.filter((message) => message.role !== "system")]
147
205
  : [];
148
206
  messages = injectContextMessages(messages, handoffMessages);
149
- const memoryMessages = agent.memory
150
- ? await agent.memory.load({
207
+ const memoryMessages = await invokeOperationalHook(agent, "memory", "load", runId, agent.memory
208
+ ? () => agent.memory.load({
151
209
  runId,
152
210
  agentId: agent.id,
211
+ scope: input.scope ?? input.handoff?.scope,
153
212
  metadata: cloneMetadata(agent.metadata, input.metadata)
154
213
  })
155
- : [];
214
+ : undefined, []);
156
215
  messages = injectContextMessages(messages, memoryMessages);
157
216
  return {
158
217
  messages,
@@ -215,13 +274,13 @@ const finalizeState = (state, result, newSteps, newToolResults) => {
215
274
  state.outputText = result.text;
216
275
  state.finishReason = result.finishReason;
217
276
  state.providerFinishReason = result.providerFinishReason;
218
- state.usage = result.usage;
277
+ state.usage = aggregateTokenUsage([state.usage, result.usage]);
219
278
  state.pendingApprovals = pendingApprovals;
220
279
  state.updatedAt = Date.now();
221
280
  return toOutput(state);
222
281
  };
223
282
  const emitTelemetryEvent = async (agent, event) => {
224
- await agent.onTelemetryEvent?.(event);
283
+ await invokeOperationalHook(agent, "telemetry", event.type, event.runId, agent.onTelemetryEvent ? () => agent.onTelemetryEvent(event) : undefined, undefined);
225
284
  };
226
285
  const subAgentToolInputSchema = z.object({
227
286
  prompt: z.string().min(1),
@@ -273,6 +332,7 @@ export const createSubAgentTool = (options) => {
273
332
  prompt: input.prompt,
274
333
  system: joinInstructions(options.system, input.system),
275
334
  parentRunId: options.parentRunId,
335
+ scope: options.scope,
276
336
  maxSteps: options.maxSteps,
277
337
  metadata: cloneMetadata(options.metadata, childMetadata)
278
338
  });
@@ -311,21 +371,52 @@ export const createSubAgentTool = (options) => {
311
371
  }
312
372
  };
313
373
  };
314
- const persistState = async (agent, state) => {
374
+ const saveStateWithRevision = async (store, state) => {
375
+ const expectedRevision = state.revision ?? 0;
376
+ const nextRevision = expectedRevision + 1;
377
+ const nextState = { ...state, revision: nextRevision };
378
+ await store.save(cloneState(nextState), { expectedRevision });
379
+ state.revision = nextRevision;
380
+ };
381
+ const claimAgentExecution = async (agent, state) => {
382
+ state.status = "running";
383
+ state.updatedAt = Date.now();
384
+ assertStateSize(agent, state);
385
+ if (agent.store) {
386
+ await saveStateWithRevision(agent.store, state);
387
+ }
388
+ };
389
+ const assertStateSize = (agent, state, policy) => {
390
+ const limit = policy?.maxStateBytes ?? agent.policy?.maxStateBytes ?? DEFAULT_AGENT_MAX_STATE_BYTES;
391
+ if (!Number.isSafeInteger(limit) || limit < 1) {
392
+ throw new ValidationError('Agent policy "maxStateBytes" must be a positive integer.');
393
+ }
394
+ const bytes = new TextEncoder().encode(JSON.stringify(state)).byteLength;
395
+ if (bytes > limit) {
396
+ throw new ValidationError(`Agent run state is ${bytes} bytes and exceeds maxStateBytes=${limit}. Offload large tool outputs to artifacts or raise the explicit limit.`);
397
+ }
398
+ };
399
+ const persistState = async (agent, state, policy) => {
315
400
  state.updatedAt = Date.now();
316
- await agent.store?.save(cloneState(state));
401
+ assertStateSize(agent, state, policy);
402
+ if (agent.store) {
403
+ await saveStateWithRevision(agent.store, state);
404
+ }
317
405
  await emitTelemetryEvent(agent, {
318
406
  type: "state-saved",
319
407
  runId: state.runId,
320
408
  agentId: state.agentId,
321
409
  status: state.status
322
410
  });
323
- await agent.memory?.save?.({
324
- runId: state.runId,
325
- agentId: state.agentId,
326
- state: cloneState(state),
327
- metadata: state.metadata
328
- });
411
+ await invokeOperationalHook(agent, "memory", "save", state.runId, agent.memory?.save
412
+ ? () => agent.memory.save({
413
+ runId: state.runId,
414
+ agentId: state.agentId,
415
+ scope: state.scope,
416
+ state: cloneState(state),
417
+ metadata: state.metadata
418
+ })
419
+ : undefined, undefined);
329
420
  };
330
421
  const approvalsFromEvents = (messages) => getAgentApprovalRequests(messages);
331
422
  const emitFinalizedStepTelemetry = async (agent, state, steps) => {
@@ -388,20 +479,34 @@ const runGuardrails = async (agent, state, stage, guardrails, requestFactory) =>
388
479
  };
389
480
  const resolveContext = async (agent, input) => {
390
481
  ensureValidIdempotencyInput(input, agent.store);
391
- let loadedState = input.state ? normalizeRunState(input.state) : undefined;
482
+ const inputScope = input.scope ?? input.handoff?.scope;
483
+ ensureValidScope(inputScope);
484
+ let loadedState = input.state ? normalizeAgentRunState(input.state) : undefined;
392
485
  let loadedByIdempotencyKey = false;
393
- if (!loadedState && input.idempotencyKey) {
394
- loadedState = await agent.store?.findByIdempotencyKey?.(input.idempotencyKey);
486
+ if (!loadedState && input.runId && agent.store) {
487
+ loadedState = await agent.store.load(input.runId, inputScope);
395
488
  if (loadedState) {
396
- loadedState = normalizeRunState(loadedState);
397
- loadedByIdempotencyKey = true;
489
+ loadedState = normalizeAgentRunState(loadedState);
398
490
  }
399
491
  }
400
- if (!loadedState && input.runId && agent.store) {
401
- loadedState = await agent.store.load(input.runId);
402
- if (loadedState) {
403
- loadedState = normalizeRunState(loadedState);
492
+ if (!loadedState && input.idempotencyKey) {
493
+ const runId = input.runId ?? randomId("run");
494
+ const maxSteps = Math.max(1, input.maxSteps ?? agent.maxSteps ?? 1);
495
+ const metadata = cloneMetadata(agent.metadata, input.metadata, input.handoff?.metadata);
496
+ const prepared = await prepareFreshMessages(agent, input, runId);
497
+ const candidate = createBaseState(agent.model.provider, agent.model.modelId, prepared.messages, maxSteps, metadata, agent.id, runId, input.handoff, input.parentRunId, input.idempotencyKey, inputScope);
498
+ const claim = await agent.store.claimIdempotencyKey(candidate);
499
+ if (claim.claimed) {
500
+ return {
501
+ state: normalizeAgentRunState(claim.state),
502
+ messages: prepared.messages,
503
+ remainingSteps: maxSteps,
504
+ memoryMessages: prepared.memoryMessages,
505
+ fresh: true
506
+ };
404
507
  }
508
+ loadedState = normalizeAgentRunState(claim.state);
509
+ loadedByIdempotencyKey = true;
405
510
  }
406
511
  const normalizedInput = loadedState && loadedByIdempotencyKey
407
512
  ? { ...input, prompt: undefined, messages: undefined, system: undefined, handoff: undefined, state: loadedState }
@@ -418,6 +523,7 @@ const resolveContext = async (agent, input) => {
418
523
  ...loadedState,
419
524
  schemaVersion: AGENT_RUN_STATE_SCHEMA_VERSION,
420
525
  idempotencyKey: loadedState.idempotencyKey ?? input.idempotencyKey,
526
+ scope: loadedState.scope ?? inputScope,
421
527
  agentId: loadedState.agentId ?? agent.id,
422
528
  parentRunId: loadedState.parentRunId ?? input.parentRunId,
423
529
  provider: agent.model.provider,
@@ -430,17 +536,105 @@ const resolveContext = async (agent, input) => {
430
536
  },
431
537
  messages: resumed.messages,
432
538
  remainingSteps: Math.max(0, maxSteps - loadedState.currentStep),
433
- memoryMessages: []
539
+ memoryMessages: [],
540
+ fresh: false
434
541
  };
435
542
  }
436
543
  const runId = input.runId ?? randomId("run");
437
544
  const maxSteps = Math.max(1, input.maxSteps ?? agent.maxSteps ?? 1);
438
545
  const prepared = await prepareFreshMessages(agent, input, runId);
439
546
  return {
440
- state: createBaseState(agent.model.provider, agent.model.modelId, prepared.messages, maxSteps, metadata, agent.id, runId, input.handoff, input.parentRunId, input.idempotencyKey),
547
+ state: createBaseState(agent.model.provider, agent.model.modelId, prepared.messages, maxSteps, metadata, agent.id, runId, input.handoff, input.parentRunId, input.idempotencyKey, inputScope),
441
548
  messages: prepared.messages,
442
549
  remainingSteps: maxSteps,
443
- memoryMessages: prepared.memoryMessages
550
+ memoryMessages: prepared.memoryMessages,
551
+ fresh: true
552
+ };
553
+ };
554
+ const canonicalJson = (value) => {
555
+ if (value === null || typeof value !== "object") {
556
+ return JSON.stringify(value);
557
+ }
558
+ if (Array.isArray(value)) {
559
+ return `[${value.map(canonicalJson).join(",")}]`;
560
+ }
561
+ return `{${Object.keys(value)
562
+ .sort()
563
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
564
+ .join(",")}}`;
565
+ };
566
+ const durableToolCallId = (runId, step, providerToolCallId, toolName, input) => `tool_${createHash("sha256")
567
+ .update(`${runId}\0${step}\0${providerToolCallId}\0${toolName}\0${canonicalJson(input)}`)
568
+ .digest("hex")}`;
569
+ const wrapToolWithJournal = (agent, state, tool) => {
570
+ const store = agent.store;
571
+ if (!store?.claimToolExecution || !store.loadToolExecution || !store.completeToolExecution) {
572
+ return tool;
573
+ }
574
+ return {
575
+ ...tool,
576
+ execute: async (input, context) => {
577
+ if (!context) {
578
+ throw new ValidationError(`Durable tool "${tool.name}" requires an execution context.`);
579
+ }
580
+ const serializedInput = serializeJsonValue(input);
581
+ const step = context.step;
582
+ const toolCallId = durableToolCallId(state.runId, step, context.toolCall.id, tool.name, serializedInput);
583
+ const idempotencyKey = `${state.runId}:${toolCallId}`;
584
+ const now = Date.now();
585
+ const candidate = {
586
+ runId: state.runId,
587
+ scope: state.scope,
588
+ toolCallId,
589
+ toolName: tool.name,
590
+ status: "pending",
591
+ idempotencyKey,
592
+ revision: 0,
593
+ input: serializedInput,
594
+ updatedAt: now
595
+ };
596
+ const claim = await store.claimToolExecution(candidate);
597
+ if (!claim.claimed) {
598
+ if (claim.entry.status === "completed") {
599
+ return claim.entry.output ?? null;
600
+ }
601
+ if (claim.entry.status === "failed") {
602
+ throw new Error(claim.entry.error?.message ?? `Tool "${tool.name}" previously failed.`);
603
+ }
604
+ throw new ConflictError(`Tool "${tool.name}" has an indeterminate durable execution. Reconcile idempotency key "${claim.entry.idempotencyKey}" before retrying.`);
605
+ }
606
+ try {
607
+ const output = serializeJsonValue(await tool.execute(input, {
608
+ ...context,
609
+ runId: state.runId,
610
+ idempotencyKey
611
+ }));
612
+ await store.completeToolExecution({
613
+ ...claim.entry,
614
+ status: "completed",
615
+ output,
616
+ completedAt: Date.now(),
617
+ updatedAt: Date.now()
618
+ }, { expectedRevision: claim.entry.revision });
619
+ return output;
620
+ }
621
+ catch (error) {
622
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
623
+ try {
624
+ await store.completeToolExecution({
625
+ ...claim.entry,
626
+ status: "failed",
627
+ error: { message: normalizedError.message },
628
+ completedAt: Date.now(),
629
+ updatedAt: Date.now()
630
+ }, { expectedRevision: claim.entry.revision });
631
+ }
632
+ catch {
633
+ // The original error is more useful; a running journal row blocks unsafe replay.
634
+ }
635
+ throw normalizedError;
636
+ }
637
+ }
444
638
  };
445
639
  };
446
640
  const createGenerateOptions = (agent, state, input, messages, maxSteps, abortSignal = input.abortSignal) => {
@@ -450,6 +644,7 @@ const createGenerateOptions = (agent, state, input, messages, maxSteps, abortSig
450
644
  ...subagent,
451
645
  parentRunId: state.runId,
452
646
  parentAgentId: state.agentId,
647
+ scope: state.scope,
453
648
  onStart: async ({ toolName, childAgentId }) => {
454
649
  await emitTelemetryEvent(agent, {
455
650
  type: "subagent-start",
@@ -474,7 +669,24 @@ const createGenerateOptions = (agent, state, input, messages, maxSteps, abortSig
474
669
  }
475
670
  tools[subagentTool.name] = subagentTool;
476
671
  }
672
+ for (const [name, tool] of Object.entries(tools)) {
673
+ if (isCallableToolDefinition(tool)) {
674
+ tools[name] = wrapToolWithJournal(agent, state, tool);
675
+ }
676
+ }
477
677
  const finalTools = Object.keys(tools).length ? tools : undefined;
678
+ const budget = input.policy?.budget ?? agent.policy?.budget;
679
+ const runPolicy = resolveRunPolicy(agent, input);
680
+ let checkpointState = cloneState(state);
681
+ let reservedToolCalls = 0;
682
+ const requestedMaxTokens = input.maxTokens ?? agent.maxTokens;
683
+ const budgetStatus = budget ? getAgentBudgetStatus(state, budget) : undefined;
684
+ const tokenCeilings = [
685
+ requestedMaxTokens,
686
+ budgetStatus?.remaining.outputTokens,
687
+ budgetStatus?.remaining.totalTokens
688
+ ].filter((value) => value !== undefined);
689
+ const maxTokens = tokenCeilings.length ? Math.min(...tokenCeilings) : undefined;
478
690
  return {
479
691
  model: agent.model,
480
692
  messages,
@@ -485,9 +697,95 @@ const createGenerateOptions = (agent, state, input, messages, maxSteps, abortSig
485
697
  onToolApprovalDecision: async (event) => {
486
698
  await emitToolApprovalTelemetry(agent, state, event);
487
699
  },
700
+ onBeforeModelStep: ({ step }) => {
701
+ if (!budget)
702
+ return;
703
+ const trigger = evaluateAgentBudgetPreflight(state, budget, {
704
+ operation: "model",
705
+ requiredSteps: Math.max(1, step - state.currentStep),
706
+ requestedOutputTokens: maxTokens
707
+ });
708
+ if (trigger) {
709
+ throw new GuardrailTriggeredError("input", trigger.reason ?? "Agent model budget preflight failed.", {
710
+ metadata: trigger.metadata
711
+ });
712
+ }
713
+ },
714
+ onModelStep: async ({ request, response, step, toolCalls }) => {
715
+ if (!agent.store)
716
+ return;
717
+ const responseSnapshot = snapshotResponse(response);
718
+ const approvals = getAgentApprovalRequests(responseSnapshot.messages);
719
+ const requestOffset = Math.min(checkpointState.messages.length, request.messages.length);
720
+ const timing = getGenerateTextStepTiming(request);
721
+ const finishedAt = timing?.finishedAt ?? Date.now();
722
+ const checkpointStep = {
723
+ index: step,
724
+ status: approvals.length ? "waiting_approval" : "completed",
725
+ startedAt: timing?.startedAt ?? finishedAt,
726
+ finishedAt,
727
+ request: snapshotRequest(request, requestOffset, request.messages.slice(requestOffset)),
728
+ response: responseSnapshot,
729
+ toolResults: []
730
+ };
731
+ checkpointState = {
732
+ ...checkpointState,
733
+ status: approvals.length ? "waiting_approval" : toolCalls.length ? "running" : "completed",
734
+ messages: [...request.messages, ...responseSnapshot.messages],
735
+ steps: [...checkpointState.steps.filter((existing) => existing.index !== step), checkpointStep],
736
+ currentStep: step,
737
+ outputText: response.text ?? checkpointState.outputText,
738
+ finishReason: response.finishReason,
739
+ providerFinishReason: response.providerFinishReason,
740
+ usage: aggregateTokenUsage([checkpointState.usage, response.usage]),
741
+ pendingApprovals: approvals,
742
+ error: undefined,
743
+ updatedAt: Date.now()
744
+ };
745
+ await persistState(agent, checkpointState, runPolicy);
746
+ state.revision = checkpointState.revision;
747
+ },
748
+ onToolExecutionComplete: async ({ toolResults }) => {
749
+ if (!agent.store)
750
+ return;
751
+ const lastStep = checkpointState.steps.at(-1);
752
+ if (lastStep) {
753
+ lastStep.toolResults = [...lastStep.toolResults, ...toolResults];
754
+ }
755
+ checkpointState = {
756
+ ...checkpointState,
757
+ status: "running",
758
+ messages: [
759
+ ...checkpointState.messages,
760
+ ...toolResults.map((toolResult) => ({
761
+ role: "tool",
762
+ parts: [{ type: "tool-result", toolResult }]
763
+ }))
764
+ ],
765
+ toolResults: [...checkpointState.toolResults, ...toolResults],
766
+ updatedAt: Date.now()
767
+ };
768
+ await persistState(agent, checkpointState, runPolicy);
769
+ state.revision = checkpointState.revision;
770
+ },
771
+ stepOffset: state.currentStep,
772
+ onBeforeToolExecution: ({ toolCalls }) => {
773
+ if (!budget)
774
+ return;
775
+ reservedToolCalls += toolCalls.length;
776
+ const trigger = evaluateAgentBudgetPreflight(state, budget, {
777
+ operation: "tool",
778
+ requiredToolCalls: reservedToolCalls
779
+ });
780
+ if (trigger) {
781
+ throw new GuardrailTriggeredError("input", trigger.reason ?? "Agent tool budget preflight failed.", {
782
+ metadata: trigger.metadata
783
+ });
784
+ }
785
+ },
488
786
  maxSteps,
489
787
  temperature: input.temperature ?? agent.temperature,
490
- maxTokens: input.maxTokens ?? agent.maxTokens,
788
+ maxTokens,
491
789
  reasoning: input.reasoning ?? agent.reasoning,
492
790
  providerOptions: input.providerOptions ?? agent.providerOptions,
493
791
  abortSignal,
@@ -565,6 +863,89 @@ const createAgentAbortContext = (inputAbortSignal, policy) => {
565
863
  isTimedOut: () => timedOut
566
864
  };
567
865
  };
866
+ const acquireAgentExecutionLease = async (agent, state, policy) => {
867
+ const store = agent.store;
868
+ if (policy?.leaseMode === "disabled" || !store?.acquireLease || !store.renewLease || !store.releaseLease) {
869
+ return {
870
+ supported: false,
871
+ cancelledState: () => undefined,
872
+ leaseLost: () => false,
873
+ release: async () => undefined
874
+ };
875
+ }
876
+ const ttlMs = policy?.leaseTtlMs ?? DEFAULT_AGENT_LEASE_TTL_MS;
877
+ const heartbeatMs = policy?.heartbeatMs ?? Math.max(250, Math.floor(ttlMs / 3));
878
+ const cancellationPollMs = policy?.cancellationPollMs ?? DEFAULT_AGENT_CANCELLATION_POLL_MS;
879
+ for (const [name, value] of [
880
+ ["leaseTtlMs", ttlMs],
881
+ ["heartbeatMs", heartbeatMs],
882
+ ["cancellationPollMs", cancellationPollMs]
883
+ ]) {
884
+ if (!Number.isSafeInteger(value) || value < 1) {
885
+ throw new ValidationError(`Agent policy "${name}" must be a positive integer.`);
886
+ }
887
+ }
888
+ if (heartbeatMs >= ttlMs) {
889
+ throw new ValidationError('Agent policy "heartbeatMs" must be less than "leaseTtlMs".');
890
+ }
891
+ const ownerId = randomId("worker");
892
+ const lease = await store.acquireLease(state.runId, { ownerId, ttlMs }, state.scope);
893
+ if (!lease) {
894
+ return undefined;
895
+ }
896
+ const controller = new AbortController();
897
+ let cancelled;
898
+ let lost = false;
899
+ let stopped = false;
900
+ let monitoring = false;
901
+ let lastHeartbeat = Date.now();
902
+ let lastCancellationPoll = 0;
903
+ const intervalMs = Math.max(25, Math.min(heartbeatMs, cancellationPollMs));
904
+ const timer = setInterval(async () => {
905
+ if (stopped || monitoring)
906
+ return;
907
+ monitoring = true;
908
+ const now = Date.now();
909
+ try {
910
+ if (now - lastHeartbeat >= heartbeatMs) {
911
+ const renewed = await store.renewLease?.(state.runId, { ownerId, ttlMs }, state.scope);
912
+ if (!renewed) {
913
+ lost = true;
914
+ controller.abort(new ConflictError(`Agent run "${state.runId}" lost its worker lease.`));
915
+ return;
916
+ }
917
+ lastHeartbeat = now;
918
+ }
919
+ if (now - lastCancellationPoll >= cancellationPollMs) {
920
+ const latest = await store.load(state.runId, state.scope);
921
+ lastCancellationPoll = now;
922
+ if (latest?.status === "cancel_requested" || latest?.status === "cancelled") {
923
+ cancelled = normalizeAgentRunState(latest);
924
+ controller.abort(new Error(latest.cancellationReason ?? "Agent run was cancelled."));
925
+ }
926
+ }
927
+ }
928
+ catch (error) {
929
+ lost = true;
930
+ controller.abort(error);
931
+ }
932
+ finally {
933
+ monitoring = false;
934
+ }
935
+ }, intervalMs);
936
+ timer.unref?.();
937
+ return {
938
+ supported: true,
939
+ signal: controller.signal,
940
+ cancelledState: () => cancelled,
941
+ leaseLost: () => lost,
942
+ release: async () => {
943
+ stopped = true;
944
+ clearInterval(timer);
945
+ await store.releaseLease?.(state.runId, ownerId, state.scope);
946
+ }
947
+ };
948
+ };
568
949
  const emitRunStartTelemetry = async (agent, state, memoryMessages, approvals) => {
569
950
  await emitTelemetryEvent(agent, {
570
951
  type: "run-start",
@@ -632,6 +1013,7 @@ export class Agent {
632
1013
  store;
633
1014
  memory;
634
1015
  onTelemetryEvent;
1016
+ hookFailurePolicy;
635
1017
  constructor(definition) {
636
1018
  Object.assign(this, createAgent(definition));
637
1019
  this.model = definition.model;
@@ -656,7 +1038,8 @@ export class Agent {
656
1038
  metadata: this.metadata,
657
1039
  store: this.store,
658
1040
  memory: this.memory,
659
- onTelemetryEvent: this.onTelemetryEvent
1041
+ onTelemetryEvent: this.onTelemetryEvent,
1042
+ hookFailurePolicy: this.hookFailurePolicy
660
1043
  });
661
1044
  }
662
1045
  run(input = {}) {
@@ -765,13 +1148,13 @@ export const runAgentGroup = async (agents, input = {}) => {
765
1148
  };
766
1149
  };
767
1150
  export const cancelAgentRun = async (store, runId, options = {}) => {
768
- const loadedState = await store.load(runId);
1151
+ const loadedState = await store.load(runId, options.scope);
769
1152
  if (!loadedState) {
770
1153
  return undefined;
771
1154
  }
772
1155
  const cancelledAt = Date.now();
773
1156
  const status = options.mode === "final" ? "cancelled" : "cancel_requested";
774
- const state = normalizeRunState({
1157
+ const state = normalizeAgentRunState({
775
1158
  ...loadedState,
776
1159
  status,
777
1160
  cancelledAt,
@@ -779,7 +1162,7 @@ export const cancelAgentRun = async (store, runId, options = {}) => {
779
1162
  updatedAt: cancelledAt,
780
1163
  error: undefined
781
1164
  });
782
- await store.save(cloneState(state));
1165
+ await saveStateWithRevision(store, state);
783
1166
  return cloneState(state);
784
1167
  };
785
1168
  export const cancelAgentRunTree = async (store, runId, options = {}) => {
@@ -788,7 +1171,7 @@ export const cancelAgentRunTree = async (store, runId, options = {}) => {
788
1171
  }
789
1172
  const cancelledAt = Date.now();
790
1173
  const status = options.mode === "final" ? "cancelled" : "cancel_requested";
791
- const cancelState = (state) => normalizeRunState({
1174
+ const cancelState = (state) => normalizeAgentRunState({
792
1175
  ...state,
793
1176
  status,
794
1177
  cancelledAt,
@@ -796,7 +1179,7 @@ export const cancelAgentRunTree = async (store, runId, options = {}) => {
796
1179
  updatedAt: cancelledAt,
797
1180
  error: undefined
798
1181
  });
799
- const parent = await store.load(runId);
1182
+ const parent = await store.load(runId, options.scope);
800
1183
  if (!parent) {
801
1184
  return {
802
1185
  parent: undefined,
@@ -806,7 +1189,7 @@ export const cancelAgentRunTree = async (store, runId, options = {}) => {
806
1189
  const visited = new Set([runId]);
807
1190
  const children = [];
808
1191
  const collectChildren = async (parentRunId) => {
809
- const directChildren = await store.findByParentRunId?.(parentRunId);
1192
+ const directChildren = await store.findByParentRunId?.(parentRunId, options.scope);
810
1193
  for (const child of directChildren ?? []) {
811
1194
  if (visited.has(child.runId)) {
812
1195
  continue;
@@ -819,9 +1202,9 @@ export const cancelAgentRunTree = async (store, runId, options = {}) => {
819
1202
  await collectChildren(runId);
820
1203
  const cancelledParent = cancelState(parent);
821
1204
  const cancelledChildren = children.map(cancelState);
822
- await store.save(cloneState(cancelledParent));
1205
+ await saveStateWithRevision(store, cancelledParent);
823
1206
  for (const child of cancelledChildren) {
824
- await store.save(cloneState(child));
1207
+ await saveStateWithRevision(store, child);
825
1208
  }
826
1209
  return {
827
1210
  parent: cloneState(cancelledParent),
@@ -830,8 +1213,8 @@ export const cancelAgentRunTree = async (store, runId, options = {}) => {
830
1213
  };
831
1214
  export const runAgent = async (agent, input = {}) => {
832
1215
  const context = await resolveContext(agent, input);
833
- await emitRunStartTelemetry(agent, context.state, context.memoryMessages, input.approvals);
834
1216
  const currentStatus = normalizeApprovalStatus(context.state.status);
1217
+ const policy = resolveRunPolicy(agent, input);
835
1218
  if (currentStatus === "completed" ||
836
1219
  currentStatus === "cancelled" ||
837
1220
  currentStatus === "cancel_requested" ||
@@ -843,34 +1226,83 @@ export const runAgent = async (agent, input = {}) => {
843
1226
  context.state.status = currentStatus;
844
1227
  return toOutput(context.state);
845
1228
  }
1229
+ const supportsLeases = Boolean(agent.store?.acquireLease && agent.store.renewLease && agent.store.releaseLease);
1230
+ if (!context.fresh && currentStatus === "running" && !supportsLeases) {
1231
+ return toOutput(context.state);
1232
+ }
1233
+ const freshRequiresExistingClaim = context.fresh && Boolean(context.state.idempotencyKey);
1234
+ if (context.fresh && !freshRequiresExistingClaim) {
1235
+ await claimAgentExecution(agent, context.state);
1236
+ }
1237
+ const executionLease = await acquireAgentExecutionLease(agent, context.state, policy);
1238
+ if (!executionLease) {
1239
+ if (input.state) {
1240
+ throw new ConflictError(`Agent run "${context.state.runId}" is already owned by another worker.`);
1241
+ }
1242
+ const activeState = await agent.store?.load(context.state.runId, context.state.scope);
1243
+ return toOutput(activeState ? normalizeAgentRunState(activeState) : context.state);
1244
+ }
1245
+ try {
1246
+ if (!context.fresh || freshRequiresExistingClaim) {
1247
+ await claimAgentExecution(agent, context.state);
1248
+ }
1249
+ await emitRunStartTelemetry(agent, context.state, context.memoryMessages, input.approvals);
1250
+ }
1251
+ catch (error) {
1252
+ await executionLease.release();
1253
+ throw error;
1254
+ }
846
1255
  if (context.remainingSteps === 0) {
847
1256
  const state = createFailedState(context.state, "Agent exhausted maxSteps before reaching a terminal response.");
848
- await persistState(agent, state);
1257
+ await persistState(agent, state, policy);
849
1258
  await emitRunFinishTelemetry(agent, state);
1259
+ await executionLease.release();
850
1260
  return toOutput(state);
851
1261
  }
852
- const inputGuardrail = await runGuardrails(agent, context.state, "input", agent.inputGuardrails, () => ({
853
- runId: context.state.runId,
854
- agentId: context.state.agentId,
855
- messages: context.messages,
856
- metadata: context.state.metadata
857
- }));
1262
+ let inputGuardrail;
1263
+ try {
1264
+ inputGuardrail = await runGuardrails(agent, context.state, "input", agent.inputGuardrails, () => ({
1265
+ runId: context.state.runId,
1266
+ agentId: context.state.agentId,
1267
+ state: cloneState(context.state),
1268
+ messages: context.messages,
1269
+ metadata: context.state.metadata
1270
+ }));
1271
+ }
1272
+ catch (error) {
1273
+ await executionLease.release();
1274
+ throw error;
1275
+ }
858
1276
  if (inputGuardrail) {
859
1277
  const failedState = applyGuardrailFailure(context.state, "input", inputGuardrail);
860
- await persistState(agent, failedState);
1278
+ await persistState(agent, failedState, policy);
861
1279
  await emitRunFinishTelemetry(agent, failedState);
1280
+ await executionLease.release();
862
1281
  return toOutput(failedState);
863
1282
  }
864
- await emitTelemetryEvent(agent, {
865
- type: "step-start",
866
- runId: context.state.runId,
867
- agentId: context.state.agentId,
868
- stepIndex: context.state.currentStep + 1
869
- });
870
- const policy = resolveRunPolicy(agent, input);
871
- const abortContext = createAgentAbortContext(input.abortSignal, policy);
1283
+ try {
1284
+ await emitTelemetryEvent(agent, {
1285
+ type: "step-start",
1286
+ runId: context.state.runId,
1287
+ agentId: context.state.agentId,
1288
+ stepIndex: context.state.currentStep + 1
1289
+ });
1290
+ }
1291
+ catch (error) {
1292
+ await executionLease.release();
1293
+ throw error;
1294
+ }
1295
+ const abortContext = createAgentAbortContext(mergeAbortSignals(input.abortSignal, executionLease.signal), policy);
872
1296
  try {
873
1297
  const result = await withAgentPolicyTimeout(generateText(createGenerateOptions(agent, context.state, input, context.messages, context.remainingSteps, abortContext.signal)), abortContext);
1298
+ const cancelled = executionLease.cancelledState();
1299
+ if (cancelled) {
1300
+ await emitRunFinishTelemetry(agent, cancelled);
1301
+ return toOutput(cancelled);
1302
+ }
1303
+ if (executionLease.leaseLost()) {
1304
+ throw new ConflictError(`Agent run "${context.state.runId}" lost its worker lease.`);
1305
+ }
874
1306
  const newSteps = mapSteps(result.steps, context.state.currentStep, result.toolResults);
875
1307
  let output = finalizeState(context.state, result, newSteps, result.toolResults);
876
1308
  const outputGuardrail = await runGuardrails(agent, output.state, "output", agent.outputGuardrails, () => ({
@@ -885,71 +1317,66 @@ export const runAgent = async (agent, input = {}) => {
885
1317
  }
886
1318
  await emitFinalizedStepTelemetry(agent, output.state, newSteps);
887
1319
  await emitApprovalTelemetry(agent, output.state, approvalsFromEvents(newSteps.flatMap((step) => step.response?.messages ?? [])));
888
- await persistState(agent, output.state);
1320
+ await persistState(agent, output.state, policy);
889
1321
  await emitRunFinishTelemetry(agent, output.state);
890
1322
  return output;
891
1323
  }
892
1324
  catch (error) {
1325
+ const cancelled = executionLease.cancelledState();
1326
+ if (cancelled) {
1327
+ await emitRunFinishTelemetry(agent, cancelled);
1328
+ return toOutput(cancelled);
1329
+ }
1330
+ if (executionLease.leaseLost()) {
1331
+ throw new ConflictError(`Agent run "${context.state.runId}" lost its worker lease.`);
1332
+ }
893
1333
  if (error instanceof AgentPolicyTimeoutError || abortContext.isTimedOut()) {
894
1334
  const status = policy?.onTimeout === "cancel-requested" ? "cancel_requested" : "timed_out";
895
1335
  const message = error instanceof Error ? error.message : `Agent run timed out after ${policy?.timeoutMs}ms.`;
896
- const timedOutState = createTerminalState(context.state, status, message);
897
- await persistState(agent, timedOutState);
1336
+ const durableState = agent.store
1337
+ ? normalizeAgentRunState((await agent.store.load(context.state.runId, context.state.scope)) ?? context.state)
1338
+ : context.state;
1339
+ const timedOutState = createTerminalState(durableState, status, message);
1340
+ await persistState(agent, timedOutState, policy);
898
1341
  await emitRunFinishTelemetry(agent, timedOutState);
899
1342
  return toOutput(timedOutState);
900
1343
  }
901
- const failedState = createFailedState(context.state, error instanceof Error ? error.message : String(error));
902
- await persistState(agent, failedState);
1344
+ const durableState = agent.store
1345
+ ? normalizeAgentRunState((await agent.store.load(context.state.runId, context.state.scope)) ?? context.state)
1346
+ : context.state;
1347
+ const failedState = createFailedState(durableState, error instanceof Error ? error.message : String(error));
1348
+ await persistState(agent, failedState, policy);
903
1349
  await emitRunFinishTelemetry(agent, failedState);
904
1350
  throw error;
905
1351
  }
1352
+ finally {
1353
+ await executionLease.release();
1354
+ }
906
1355
  };
907
1356
  export const streamAgent = (agent, input = {}) => {
908
- const subscribers = new Set();
909
- const history = [];
910
- let done = false;
911
- const publish = (value) => {
912
- history.push(value);
913
- for (const subscriber of subscribers) {
914
- subscriber(value);
915
- }
916
- if (value.done) {
917
- done = true;
918
- }
919
- };
920
- const createEventStream = async function* () {
921
- let cursor = 0;
922
- while (true) {
923
- while (cursor < history.length) {
924
- const item = history[cursor];
925
- cursor += 1;
926
- if (item.done) {
927
- return;
928
- }
929
- yield item.value;
930
- }
931
- if (done) {
932
- return;
933
- }
934
- await new Promise((resolve) => {
935
- const subscriber = (value) => {
936
- subscribers.delete(subscriber);
937
- resolve(value);
938
- };
939
- subscribers.add(subscriber);
940
- });
941
- }
942
- };
1357
+ const policy = resolveRunPolicy(agent, input);
1358
+ const broadcast = new BoundedReplayBroadcast({
1359
+ maxHistory: policy?.maxStreamEvents ?? 4096
1360
+ });
1361
+ const publish = (event, terminal = false) => broadcast.publish(event, { terminal });
1362
+ let activeLease;
943
1363
  const runner = (async () => {
944
1364
  const context = await resolveContext(agent, input);
945
- await emitRunStartTelemetry(agent, context.state, context.memoryMessages, input.approvals);
946
1365
  const currentStatus = normalizeApprovalStatus(context.state.status);
1366
+ const supportsLeases = Boolean(agent.store?.acquireLease && agent.store.renewLease && agent.store.releaseLease);
1367
+ if (!context.fresh && currentStatus === "running" && !supportsLeases) {
1368
+ broadcast.close();
1369
+ return {
1370
+ output: toOutput(context.state),
1371
+ textStream: emptyAsyncIterable()
1372
+ };
1373
+ }
947
1374
  if (currentStatus === "completed" ||
948
1375
  currentStatus === "cancelled" ||
949
1376
  currentStatus === "cancel_requested" ||
950
1377
  currentStatus === "timed_out") {
951
1378
  context.state.status = currentStatus;
952
- publish({ done: true, value: undefined });
1379
+ broadcast.close();
953
1380
  return {
954
1381
  output: toOutput(context.state),
955
1382
  textStream: emptyAsyncIterable()
@@ -957,78 +1384,100 @@ export const streamAgent = (agent, input = {}) => {
957
1384
  }
958
1385
  if (currentStatus === "waiting_approval" && context.state.pendingApprovals.length > 0 && !input.approvals?.length) {
959
1386
  context.state.status = currentStatus;
960
- publish({ done: true, value: undefined });
1387
+ broadcast.close();
961
1388
  return {
962
1389
  output: toOutput(context.state),
963
1390
  textStream: emptyAsyncIterable()
964
1391
  };
965
1392
  }
1393
+ const freshRequiresExistingClaim = context.fresh && Boolean(context.state.idempotencyKey);
1394
+ if (context.fresh && !freshRequiresExistingClaim) {
1395
+ await claimAgentExecution(agent, context.state);
1396
+ }
1397
+ const executionLease = await acquireAgentExecutionLease(agent, context.state, policy);
1398
+ if (!executionLease) {
1399
+ if (input.state) {
1400
+ throw new ConflictError(`Agent run "${context.state.runId}" is already owned by another worker.`);
1401
+ }
1402
+ const activeState = await agent.store?.load(context.state.runId, context.state.scope);
1403
+ broadcast.close();
1404
+ return {
1405
+ output: toOutput(activeState ? normalizeAgentRunState(activeState) : context.state),
1406
+ textStream: emptyAsyncIterable()
1407
+ };
1408
+ }
1409
+ activeLease = executionLease;
1410
+ try {
1411
+ if (!context.fresh || freshRequiresExistingClaim) {
1412
+ await claimAgentExecution(agent, context.state);
1413
+ }
1414
+ await emitRunStartTelemetry(agent, context.state, context.memoryMessages, input.approvals);
1415
+ }
1416
+ catch (error) {
1417
+ await executionLease.release();
1418
+ throw error;
1419
+ }
966
1420
  if (context.remainingSteps === 0) {
967
1421
  const state = createFailedState(context.state, "Agent exhausted maxSteps before reaching a terminal response.");
968
- await persistState(agent, state);
1422
+ await persistState(agent, state, policy);
969
1423
  await emitRunFinishTelemetry(agent, state);
970
- publish({ done: true, value: undefined });
1424
+ await executionLease.release();
1425
+ broadcast.close();
971
1426
  return {
972
1427
  output: toOutput(state),
973
1428
  textStream: emptyAsyncIterable()
974
1429
  };
975
1430
  }
976
- const inputGuardrail = await runGuardrails(agent, context.state, "input", agent.inputGuardrails, () => ({
977
- runId: context.state.runId,
978
- agentId: context.state.agentId,
979
- messages: context.messages,
980
- metadata: context.state.metadata
981
- }));
1431
+ let inputGuardrail;
1432
+ try {
1433
+ inputGuardrail = await runGuardrails(agent, context.state, "input", agent.inputGuardrails, () => ({
1434
+ runId: context.state.runId,
1435
+ agentId: context.state.agentId,
1436
+ state: cloneState(context.state),
1437
+ messages: context.messages,
1438
+ metadata: context.state.metadata
1439
+ }));
1440
+ }
1441
+ catch (error) {
1442
+ await executionLease.release();
1443
+ throw error;
1444
+ }
982
1445
  if (inputGuardrail) {
983
1446
  const failedState = applyGuardrailFailure(context.state, "input", inputGuardrail);
984
- await persistState(agent, failedState);
1447
+ await persistState(agent, failedState, policy);
985
1448
  await emitRunFinishTelemetry(agent, failedState);
986
- publish({
987
- done: false,
988
- value: {
989
- type: "error",
990
- error: new GuardrailTriggeredError("input", failedState.error?.message ?? "Agent input guardrail triggered.", {
991
- metadata: inputGuardrail.metadata
992
- })
993
- }
994
- });
995
- publish({
996
- done: false,
997
- value: {
998
- type: "agent-run-finish",
999
- status: failedState.status,
1000
- state: failedState
1001
- }
1002
- });
1003
- publish({ done: true, value: undefined });
1449
+ await publish({
1450
+ type: "error",
1451
+ error: new GuardrailTriggeredError("input", failedState.error?.message ?? "Agent input guardrail triggered.", {
1452
+ metadata: inputGuardrail.metadata
1453
+ })
1454
+ }, true);
1455
+ await publish({
1456
+ type: "agent-run-finish",
1457
+ status: failedState.status,
1458
+ state: failedState
1459
+ }, true);
1460
+ broadcast.close();
1461
+ await executionLease.release();
1004
1462
  return {
1005
1463
  output: toOutput(failedState),
1006
1464
  textStream: emptyAsyncIterable()
1007
1465
  };
1008
1466
  }
1009
- publish({
1010
- done: false,
1011
- value: {
1012
- type: "agent-run-start",
1013
- currentStep: context.state.currentStep + 1,
1014
- maxSteps: context.state.maxSteps
1015
- }
1467
+ await publish({
1468
+ type: "agent-run-start",
1469
+ currentStep: context.state.currentStep + 1,
1470
+ maxSteps: context.state.maxSteps
1016
1471
  });
1017
1472
  for (const approval of input.approvals ?? []) {
1018
- publish({
1019
- done: false,
1020
- value: {
1021
- type: "agent-approval-resolved",
1022
- approval
1023
- }
1473
+ await publish({
1474
+ type: "agent-approval-resolved",
1475
+ approval
1024
1476
  });
1025
1477
  }
1026
- publish({
1027
- done: false,
1028
- value: {
1029
- type: "agent-step-start",
1030
- stepIndex: context.state.currentStep + 1
1031
- }
1478
+ await publish({
1479
+ type: "agent-step-start",
1480
+ stepIndex: context.state.currentStep + 1
1032
1481
  });
1033
1482
  await emitTelemetryEvent(agent, {
1034
1483
  type: "step-start",
@@ -1036,13 +1485,12 @@ export const streamAgent = (agent, input = {}) => {
1036
1485
  agentId: context.state.agentId,
1037
1486
  stepIndex: context.state.currentStep + 1
1038
1487
  });
1039
- const policy = resolveRunPolicy(agent, input);
1040
- const abortContext = createAgentAbortContext(input.abortSignal, policy);
1488
+ const abortContext = createAgentAbortContext(mergeAbortSignals(input.abortSignal, executionLease.signal), policy);
1041
1489
  const streamResult = streamText(createGenerateOptions(agent, context.state, input, context.messages, context.remainingSteps, abortContext.signal));
1042
1490
  const approvalRequests = [];
1043
1491
  const eventRelay = (async () => {
1044
1492
  for await (const event of streamResult.eventStream) {
1045
- publish({ done: false, value: event });
1493
+ await publish(event);
1046
1494
  if (event.type === "provider-data" &&
1047
1495
  typeof event.data === "object" &&
1048
1496
  event.data !== null &&
@@ -1060,12 +1508,9 @@ export const streamAgent = (agent, input = {}) => {
1060
1508
  rawData: event.data
1061
1509
  };
1062
1510
  approvalRequests.push(approval);
1063
- publish({
1064
- done: false,
1065
- value: {
1066
- type: "agent-approval-request",
1067
- approval
1068
- }
1511
+ await publish({
1512
+ type: "agent-approval-request",
1513
+ approval
1069
1514
  });
1070
1515
  await emitTelemetryEvent(agent, {
1071
1516
  type: "approval-request",
@@ -1079,6 +1524,22 @@ export const streamAgent = (agent, input = {}) => {
1079
1524
  const output = (async () => {
1080
1525
  try {
1081
1526
  const final = await withAgentPolicyTimeout(eventRelay.then(() => streamResult.collect()), abortContext);
1527
+ const cancelled = executionLease.cancelledState();
1528
+ if (cancelled) {
1529
+ await emitRunFinishTelemetry(agent, cancelled);
1530
+ await publish({
1531
+ type: "agent-run-finish",
1532
+ status: cancelled.status,
1533
+ state: cancelled
1534
+ }, true);
1535
+ broadcast.close();
1536
+ return toOutput(cancelled);
1537
+ }
1538
+ if (executionLease.leaseLost()) {
1539
+ const conflict = new ConflictError(`Agent run "${context.state.runId}" lost its worker lease.`);
1540
+ broadcast.fail(conflict);
1541
+ throw conflict;
1542
+ }
1082
1543
  const newSteps = mapSteps(final.steps, context.state.currentStep, final.toolResults);
1083
1544
  let result = finalizeState(context.state, final, newSteps, final.toolResults);
1084
1545
  const outputGuardrail = await runGuardrails(agent, result.state, "output", agent.outputGuardrails, () => ({
@@ -1090,94 +1551,102 @@ export const streamAgent = (agent, input = {}) => {
1090
1551
  }));
1091
1552
  if (outputGuardrail) {
1092
1553
  result = toOutput(applyGuardrailFailure(result.state, "output", outputGuardrail));
1093
- publish({
1094
- done: false,
1095
- value: {
1096
- type: "error",
1097
- error: new GuardrailTriggeredError("output", result.state.error?.message ?? "Agent output guardrail triggered.", { metadata: outputGuardrail.metadata })
1098
- }
1099
- });
1554
+ await publish({
1555
+ type: "error",
1556
+ error: new GuardrailTriggeredError("output", result.state.error?.message ?? "Agent output guardrail triggered.", { metadata: outputGuardrail.metadata })
1557
+ }, true);
1100
1558
  }
1101
1559
  for (const step of newSteps) {
1102
- publish({
1103
- done: false,
1104
- value: {
1105
- type: "agent-step-finish",
1106
- step
1107
- }
1560
+ await publish({
1561
+ type: "agent-step-finish",
1562
+ step
1108
1563
  });
1109
1564
  }
1110
1565
  await emitFinalizedStepTelemetry(agent, result.state, newSteps);
1111
1566
  if (!approvalRequests.length) {
1112
1567
  await emitApprovalTelemetry(agent, result.state, approvalsFromEvents(newSteps.flatMap((step) => step.response?.messages ?? [])));
1113
1568
  }
1114
- await persistState(agent, result.state);
1569
+ await persistState(agent, result.state, policy);
1115
1570
  await emitRunFinishTelemetry(agent, result.state);
1116
- publish({
1117
- done: false,
1118
- value: {
1119
- type: "agent-run-finish",
1120
- status: result.status,
1121
- state: result.state
1122
- }
1123
- });
1124
- publish({ done: true, value: undefined });
1571
+ await publish({
1572
+ type: "agent-run-finish",
1573
+ status: result.status,
1574
+ state: result.state
1575
+ }, true);
1576
+ broadcast.close();
1125
1577
  return result;
1126
1578
  }
1127
1579
  catch (error) {
1580
+ const cancelled = executionLease.cancelledState();
1581
+ if (cancelled) {
1582
+ await emitRunFinishTelemetry(agent, cancelled);
1583
+ await publish({
1584
+ type: "agent-run-finish",
1585
+ status: cancelled.status,
1586
+ state: cancelled
1587
+ }, true);
1588
+ broadcast.close();
1589
+ return toOutput(cancelled);
1590
+ }
1591
+ if (executionLease.leaseLost()) {
1592
+ const conflict = new ConflictError(`Agent run "${context.state.runId}" lost its worker lease.`);
1593
+ broadcast.fail(conflict);
1594
+ throw conflict;
1595
+ }
1128
1596
  if (error instanceof AgentPolicyTimeoutError || abortContext.isTimedOut()) {
1129
1597
  const status = policy?.onTimeout === "cancel-requested" ? "cancel_requested" : "timed_out";
1130
1598
  const message = error instanceof Error ? error.message : `Agent run timed out after ${policy?.timeoutMs}ms.`;
1131
- const timedOutState = createTerminalState(context.state, status, message);
1132
- await persistState(agent, timedOutState);
1599
+ const durableState = agent.store
1600
+ ? normalizeAgentRunState((await agent.store.load(context.state.runId, context.state.scope)) ?? context.state)
1601
+ : context.state;
1602
+ const timedOutState = createTerminalState(durableState, status, message);
1603
+ await persistState(agent, timedOutState, policy);
1133
1604
  await emitRunFinishTelemetry(agent, timedOutState);
1134
- publish({
1135
- done: false,
1136
- value: {
1137
- type: "error",
1138
- error: new AgentPolicyTimeoutError(policy?.timeoutMs ?? 0)
1139
- }
1140
- });
1141
- publish({
1142
- done: false,
1143
- value: {
1144
- type: "agent-run-finish",
1145
- status: timedOutState.status,
1146
- state: timedOutState
1147
- }
1148
- });
1149
- publish({ done: true, value: undefined });
1605
+ await publish({
1606
+ type: "error",
1607
+ error: new AgentPolicyTimeoutError(policy?.timeoutMs ?? 0)
1608
+ }, true);
1609
+ await publish({
1610
+ type: "agent-run-finish",
1611
+ status: timedOutState.status,
1612
+ state: timedOutState
1613
+ }, true);
1614
+ broadcast.close();
1150
1615
  return toOutput(timedOutState);
1151
1616
  }
1152
- const failedState = createFailedState(context.state, error instanceof Error ? error.message : String(error));
1153
- await persistState(agent, failedState);
1617
+ const durableState = agent.store
1618
+ ? normalizeAgentRunState((await agent.store.load(context.state.runId, context.state.scope)) ?? context.state)
1619
+ : context.state;
1620
+ const failedState = createFailedState(durableState, error instanceof Error ? error.message : String(error));
1621
+ await persistState(agent, failedState, policy);
1154
1622
  await emitRunFinishTelemetry(agent, failedState);
1155
- publish({
1156
- done: false,
1157
- value: {
1158
- type: "error",
1159
- error: error instanceof Error ? error : new Error(String(error))
1160
- }
1161
- });
1162
- publish({
1163
- done: false,
1164
- value: {
1165
- type: "agent-run-finish",
1166
- status: failedState.status,
1167
- state: failedState
1168
- }
1169
- });
1170
- publish({ done: true, value: undefined });
1623
+ await publish({
1624
+ type: "error",
1625
+ error: error instanceof Error ? error : new Error(String(error))
1626
+ }, true);
1627
+ await publish({
1628
+ type: "agent-run-finish",
1629
+ status: failedState.status,
1630
+ state: failedState
1631
+ }, true);
1632
+ broadcast.close();
1171
1633
  throw error;
1172
1634
  }
1635
+ finally {
1636
+ await executionLease.release();
1637
+ }
1173
1638
  })();
1174
1639
  return {
1175
1640
  output,
1176
1641
  textStream: streamResult.textStream
1177
1642
  };
1178
- })();
1643
+ })().catch(async (error) => {
1644
+ await activeLease?.release();
1645
+ broadcast.fail(error);
1646
+ throw error;
1647
+ });
1179
1648
  return {
1180
- eventStream: createEventStream(),
1649
+ eventStream: broadcast.stream(),
1181
1650
  textStream: (async function* () {
1182
1651
  const started = await runner;
1183
1652
  for await (const chunk of started.textStream) {