effect-agent 0.1.0-beta.152 → 0.1.0-beta.153

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 (131) hide show
  1. package/dist/{Admin-CKIdJoP8.d.mts → Admin-BjmupLN3.d.mts} +36 -17
  2. package/dist/{Agent-Cjd_ZveM.d.mts → Agent-BCyrGaWg.d.mts} +2 -2
  3. package/dist/{AgentPolicy-BqDoSXO6.d.mts → AgentPolicy-CnXWWwdg.d.mts} +5 -1
  4. package/dist/{AgentRuntime-sXpKcyxu.d.mts → AgentRuntime-BAjT5gXl.d.mts} +10 -10
  5. package/dist/{AgentRuntime-D8U9gl9Q.mjs → AgentRuntime-IF_1GoUf.mjs} +54 -11
  6. package/dist/AgentRuntime-IF_1GoUf.mjs.map +1 -0
  7. package/dist/{ContextCompactor-WwSIG_Z2.d.mts → ContextCompactor-CPlStZRc.d.mts} +2 -2
  8. package/dist/{Digest-U69rDHuO.d.mts → Digest-CaJ2RIsF.d.mts} +2 -2
  9. package/dist/{DurableAgentRuntime-DQFV26SB.mjs → DurableAgentRuntime-BQzYkFVy.mjs} +185 -88
  10. package/dist/DurableAgentRuntime-BQzYkFVy.mjs.map +1 -0
  11. package/dist/{EventSource-CMbXXmwe.d.mts → EventSource-C8CKwQy5.d.mts} +3 -3
  12. package/dist/InMemory.d.mts +1 -1
  13. package/dist/{Messaging-ccQURKCf.d.mts → Messaging-B1zumorg.d.mts} +3 -3
  14. package/dist/{MessagingHost-D04wE-7W.d.mts → MessagingHost-B4jqlJmx.d.mts} +3 -3
  15. package/dist/{Output-CFD1CPfU.d.mts → Output-WL7JexCm.d.mts} +2 -2
  16. package/dist/{PreparedInputAdmission-Bt6rgtVu.d.mts → PreparedInputAdmission-CU1PzUVD.d.mts} +4 -4
  17. package/dist/{Records-B5GFWCpn.d.mts → Records-BrEpibum.d.mts} +17 -6
  18. package/dist/{RunEvent-DIrzkjLr.d.mts → RunEvent-DV_llBcq.d.mts} +18 -4
  19. package/dist/{RunJournal-Gd83HYqG.mjs → RunJournal-ixFRCyW3.mjs} +29 -7
  20. package/dist/RunJournal-ixFRCyW3.mjs.map +1 -0
  21. package/dist/{RunOptions-Bv9NpkPZ.d.mts → RunOptions-CkRYnlJa.d.mts} +22 -8
  22. package/dist/{RunPolicyUsage-ClZxyR46.d.mts → RunPolicyUsage-CUudVtg_.d.mts} +3 -1
  23. package/dist/{Schedule-CtYREFnz.d.mts → Schedule-B_z7PMg-.d.mts} +2 -2
  24. package/dist/{Subagent-hG9lJ7tJ.mjs → Subagent-CmFtZaeI.mjs} +2 -2
  25. package/dist/{Subagent-hG9lJ7tJ.mjs.map → Subagent-CmFtZaeI.mjs.map} +1 -1
  26. package/dist/{Subagent-VHrANXFy.d.mts → Subagent-Dld4W1BY.d.mts} +7 -7
  27. package/dist/{SubagentHost-C-4A7tA2.d.mts → SubagentHost--HBuY8fF.d.mts} +4 -4
  28. package/dist/{Subscription-BJAzYNw1.d.mts → Subscription-BomcQ9_W.d.mts} +5 -5
  29. package/dist/{SubscriptionInput-CkBJt0AS.d.mts → SubscriptionInput-gpZSR9vB.d.mts} +3 -3
  30. package/dist/{ThreadHistory-tS1KwxHR.d.mts → ThreadHistory-ZEu_ja6L.d.mts} +2 -2
  31. package/dist/{Worker-fwWkBg12.d.mts → Worker-2r2zGDkh.d.mts} +2 -2
  32. package/dist/{WorkerAdmission-CJ_oWRuS.mjs → WorkerAdmission-CNbm4o_x.mjs} +3 -3
  33. package/dist/{WorkerAdmission-CJ_oWRuS.mjs.map → WorkerAdmission-CNbm4o_x.mjs.map} +1 -1
  34. package/dist/{agent-registration-CKNsrpwo.mjs → agent-registration-U3kmV7nW.mjs} +18 -18
  35. package/dist/agent-registration-U3kmV7nW.mjs.map +1 -0
  36. package/dist/capabilities/Commands.d.mts +1 -1
  37. package/dist/capabilities/Messaging.d.mts +1 -1
  38. package/dist/capabilities/Redaction.d.mts +1 -1
  39. package/dist/capabilities/RunHooks.d.mts +1 -1
  40. package/dist/capabilities/Subagent.d.mts +2 -2
  41. package/dist/capabilities/Subagent.mjs +1 -1
  42. package/dist/core/Agent.d.mts +1 -1
  43. package/dist/core/AgentPolicy.d.mts +1 -1
  44. package/dist/core/AgentPolicy.mjs +2 -0
  45. package/dist/core/AgentPolicy.mjs.map +1 -1
  46. package/dist/core/RunEvent.d.mts +2 -2
  47. package/dist/core/RunEvent.mjs +10 -1
  48. package/dist/core/RunEvent.mjs.map +1 -1
  49. package/dist/core/RunPolicyUsage.d.mts +1 -1
  50. package/dist/core/RunPolicyUsage.mjs +6 -1
  51. package/dist/core/RunPolicyUsage.mjs.map +1 -1
  52. package/dist/core/Worker.d.mts +1 -1
  53. package/dist/durable/ActivityStore.d.mts +1 -1
  54. package/dist/durable/Admin.d.mts +1 -1
  55. package/dist/durable/AgentRegistration.d.mts +2 -2
  56. package/dist/durable/AgentRegistration.mjs +3 -2
  57. package/dist/durable/AgentRegistration.mjs.map +1 -1
  58. package/dist/durable/Certification.d.mts +1 -1
  59. package/dist/durable/CommittedActivity.d.mts +3 -3
  60. package/dist/durable/Digest.d.mts +1 -1
  61. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  62. package/dist/durable/DurableAgentRuntime.mjs +2 -2
  63. package/dist/durable/EventSource.d.mts +1 -1
  64. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  65. package/dist/durable/LifecyclePublication.d.mts +1 -1
  66. package/dist/durable/MessageDelivery.d.mts +5 -5
  67. package/dist/durable/MessageDeliveryStoreConformance.d.mts +24 -24
  68. package/dist/durable/PersistentHistory.d.mts +2 -2
  69. package/dist/durable/PersistentHistory.mjs +1 -1
  70. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  71. package/dist/durable/Records.d.mts +2 -2
  72. package/dist/durable/Records.mjs +16 -1
  73. package/dist/durable/Records.mjs.map +1 -1
  74. package/dist/durable/Recovery.d.mts +1 -1
  75. package/dist/durable/RunJournal.d.mts +1 -1
  76. package/dist/durable/RunJournal.mjs +1 -1
  77. package/dist/durable/Schedule.d.mts +1 -1
  78. package/dist/durable/ScheduleStoreConformance.d.mts +1 -1
  79. package/dist/durable/ScheduleTransition.d.mts +1 -1
  80. package/dist/durable/Scheduling.d.mts +3 -3
  81. package/dist/durable/SubmissionLedger.d.mts +1 -1
  82. package/dist/durable/SubmissionLedgerConformance.d.mts +2 -2
  83. package/dist/durable/SubmissionLedgerConformance.mjs +1 -1
  84. package/dist/durable/SubmissionStatus.d.mts +1 -1
  85. package/dist/durable/Subscription.d.mts +1 -1
  86. package/dist/durable/SubscriptionInput.d.mts +1 -1
  87. package/dist/durable/SubscriptionInput.mjs +1 -1
  88. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  89. package/dist/durable/SubscriptionTools.d.mts +4 -4
  90. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  91. package/dist/durable/Subscriptions.d.mts +6 -6
  92. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  93. package/dist/durable/ThreadContextHistoryProjection.d.mts +1 -1
  94. package/dist/durable/ThreadInvariants.d.mts +2 -2
  95. package/dist/durable/ThreadInvariants.mjs +1 -1
  96. package/dist/durable/ThreadProjection.d.mts +1 -1
  97. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  98. package/dist/durable/ThreadStore.d.mts +1 -1
  99. package/dist/durable/ThreadStore.mjs +1 -1
  100. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  101. package/dist/durable/ThreadStoreConformance.mjs +1 -1
  102. package/dist/durable/WorkerAdmission.d.mts +3 -3
  103. package/dist/durable/WorkerAdmission.mjs +1 -1
  104. package/dist/durable/WorkerHost.d.mts +5 -5
  105. package/dist/engine/AgentRuntime.d.mts +1 -1
  106. package/dist/engine/AgentRuntime.mjs +1 -1
  107. package/dist/engine/Compaction.d.mts +1 -1
  108. package/dist/engine/ContextCompactor.d.mts +1 -1
  109. package/dist/engine/MessagingHost.d.mts +1 -1
  110. package/dist/engine/Output.d.mts +1 -1
  111. package/dist/engine/RunOptions.d.mts +1 -1
  112. package/dist/engine/RunOptions.mjs.map +1 -1
  113. package/dist/engine/SubagentHost.d.mts +1 -1
  114. package/dist/engine/ThreadHistory.d.mts +1 -1
  115. package/dist/index.d.mts +22 -22
  116. package/dist/index.mjs +5 -5
  117. package/package.json +1 -1
  118. package/src/core/AgentPolicy.ts +2 -0
  119. package/src/core/RunEvent.ts +9 -0
  120. package/src/core/RunPolicyUsage.ts +2 -0
  121. package/src/durable/AgentRegistration.ts +2 -0
  122. package/src/durable/DurableAgentRuntime.ts +320 -137
  123. package/src/durable/Records.ts +12 -0
  124. package/src/durable/RunJournal.ts +88 -27
  125. package/src/durable/internal/agent-registration.ts +58 -35
  126. package/src/engine/RunOptions.ts +17 -2
  127. package/src/engine/internal/agent-runtime.ts +115 -8
  128. package/dist/AgentRuntime-D8U9gl9Q.mjs.map +0 -1
  129. package/dist/DurableAgentRuntime-DQFV26SB.mjs.map +0 -1
  130. package/dist/RunJournal-Gd83HYqG.mjs.map +0 -1
  131. package/dist/agent-registration-CKNsrpwo.mjs.map +0 -1
@@ -498,6 +498,17 @@ export class ModelResponseInterrupted extends Schema.TaggedClass<ModelResponseIn
498
498
  reason: BoundedText,
499
499
  }) {}
500
500
 
501
+ /** Disposable call cancellation: no response or Tool declaration is canonical. */
502
+ export class ModelCallAborted extends Schema.TaggedClass<ModelCallAborted>()("ModelCallAborted", {
503
+ runId: RunId,
504
+ turnId: TurnId,
505
+ turn: TurnNumber,
506
+ restart: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 2 })),
507
+ reason: Schema.Literal("joined-input"),
508
+ modelUsage: Schema.Array(ModelCallUsage),
509
+ unobservedModelCalls: Schema.Natural,
510
+ }) {}
511
+
501
512
  /**
502
513
  * One engine-native compaction committed before the pre-Turn view changes (RUN-026).
503
514
  * `coversThrough` is a Thread record sequence: the projection
@@ -1020,6 +1031,7 @@ export const CanonicalRecordPayload = Schema.Union([
1020
1031
  ToolApprovalRequested,
1021
1032
  ToolApprovalDecided,
1022
1033
  ModelResponseInterrupted,
1034
+ ModelCallAborted,
1023
1035
  CompactionCreated,
1024
1036
  RunFailed,
1025
1037
  RunCompleted,
@@ -1,4 +1,4 @@
1
- import { type Crypto, Effect, Schema, Stream, type DateTime } from "effect";
1
+ import { type Crypto, Effect, Predicate, Schema, Stream, type DateTime } from "effect";
2
2
  import { Prompt } from "effect/unstable/ai";
3
3
 
4
4
  import { ThreadId, RunId, ToolCallId, TurnId, type SubmissionId } from "../core/Identifiers.ts";
@@ -330,14 +330,34 @@ export const childIdempotencyKeyFor = (
330
330
  toolCallId: ToolCallId,
331
331
  ): IdempotencyKey => decodeIdempotencyKey(`subagent:${parentRunId}:${toolCallId}`);
332
332
 
333
- const decodePromptMessages = Effect.fn("RunJournal.decodePromptMessages")(
334
- (messages: PersistedJson): Effect.Effect<Prompt.Prompt, RunJournalError> =>
335
- Schema.decodeUnknownEffect(Prompt.Prompt)(messages).pipe(
336
- Effect.mapError((cause) =>
337
- journalError("Canonical messages are not Schema-encoded Prompt messages", cause),
338
- ),
333
+ const decodePersistedPrompt = (
334
+ messages: PersistedJson,
335
+ ): Effect.Effect<Prompt.Prompt, RunJournalError> =>
336
+ Schema.decodeUnknownEffect(Prompt.Prompt)(messages).pipe(
337
+ Effect.mapError((cause) =>
338
+ journalError("Canonical messages are not Schema-encoded Prompt messages", cause),
339
339
  ),
340
- );
340
+ );
341
+
342
+ // Canonical payloads are immutable. Weak keys let retained storage rows own the lifetime
343
+ // while repeated projections of an advancing tail reuse their validated Prompt values.
344
+ const decodedMessages = new WeakMap<object, Prompt.Prompt>();
345
+
346
+ const decodePromptMessages = (messages: PersistedJson) =>
347
+ Effect.suspend(() => {
348
+ if (!Predicate.isObject(messages)) return decodePersistedPrompt(messages);
349
+ const cached = decodedMessages.get(messages);
350
+
351
+ return cached === undefined
352
+ ? decodePersistedPrompt(messages).pipe(
353
+ Effect.tap((prompt) =>
354
+ Effect.sync(() => {
355
+ decodedMessages.set(messages, prompt);
356
+ }),
357
+ ),
358
+ )
359
+ : Effect.succeed(cached);
360
+ });
341
361
 
342
362
  interface PendingSettledTool {
343
363
  readonly record: ToolCallSettled;
@@ -345,24 +365,24 @@ interface PendingSettledTool {
345
365
  readonly cleared: boolean;
346
366
  }
347
367
 
348
- const toolMessageFromSettled = Effect.fn("RunJournal.toolMessageFromSettled")(
349
- (settled: ReadonlyArray<PendingSettledTool>): Effect.Effect<Prompt.Message, RunJournalError> =>
350
- Effect.try({
351
- try: () =>
352
- Prompt.makeMessage("tool", {
353
- content: settled.map(({ record, cleared }) =>
354
- Prompt.makePart("tool-result", {
355
- id: record.toolCallId,
356
- name: record.toolName,
357
- result: cleared ? CLEARED_TOOL_RESULT : record.result,
358
- isFailure: record.isFailure,
359
- providerExecuted: false,
360
- }),
361
- ),
362
- }),
363
- catch: (cause) => journalError("Unable to rebuild Tool message from ToolCallSettled", cause),
364
- }),
365
- );
368
+ const toolMessageFromSettled = (
369
+ settled: ReadonlyArray<PendingSettledTool>,
370
+ ): Effect.Effect<Prompt.Message, RunJournalError> =>
371
+ Effect.try({
372
+ try: () =>
373
+ Prompt.makeMessage("tool", {
374
+ content: settled.map(({ record, cleared }) =>
375
+ Prompt.makePart("tool-result", {
376
+ id: record.toolCallId,
377
+ name: record.toolName,
378
+ result: cleared ? CLEARED_TOOL_RESULT : record.result,
379
+ isFailure: record.isFailure,
380
+ providerExecuted: false,
381
+ }),
382
+ ),
383
+ }),
384
+ catch: (cause) => journalError("Unable to rebuild Tool message from ToolCallSettled", cause),
385
+ });
366
386
 
367
387
  /**
368
388
  * Pure canonical projection of one Run's durable execution state.
@@ -525,6 +545,7 @@ const PROMPT_TRANSPARENT_TAGS: ReadonlySet<string> = new Set([
525
545
  "ToolApprovalRequested",
526
546
  "ToolApprovalDecided",
527
547
  "ModelResponseInterrupted",
548
+ "ModelCallAborted",
528
549
  "SubagentRequested",
529
550
  "SubagentStarted",
530
551
  "SubagentJoined",
@@ -892,6 +913,7 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
892
913
  programmaticToolCalls: seed?.policyUsage.programmaticToolCalls ?? 0,
893
914
  consecutiveToolFailures: seed?.policyUsage.consecutiveToolFailures ?? 0,
894
915
  finalizationUsed: seed?.policyUsage.finalizationUsed ?? false,
916
+ modelRestarts: seed?.policyUsage.modelRestarts ?? 0,
895
917
  };
896
918
 
897
919
  const accountResponse = Effect.fn("RunJournal.accountResponse")(function* (
@@ -1020,7 +1042,7 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
1020
1042
 
1021
1043
  let pendingToolOrder = new Map<string, number>();
1022
1044
 
1023
- const flushTools = Effect.fn("RunJournal.flushTools")(function* (
1045
+ const flushTools = Effect.fnUntraced(function* (
1024
1046
  current: FoldState,
1025
1047
  ): Effect.fn.Return<FoldState, RunJournalError> {
1026
1048
  if (current.pendingTools.length === 0) return current;
@@ -1068,6 +1090,45 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
1068
1090
  policyUsage.programmaticToolCalls = payload.programmaticToolCalls;
1069
1091
  policyUsage.finalizationUsed = payload.finalizationUsed;
1070
1092
  }
1093
+ if (payload._tag === "ModelCallAborted" && payload.runId === ownerRunId) {
1094
+ if (seed !== undefined && envelope.sequence <= seed.throughSequence) return;
1095
+ if (payload.restart !== policyUsage.modelRestarts + 1)
1096
+ return yield* journalError("Model restart reservations must advance once");
1097
+ policyUsage.modelRestarts = payload.restart;
1098
+
1099
+ const summary = yield* summarizeModelUsage(payload.modelUsage).pipe(
1100
+ Effect.mapError((cause) =>
1101
+ journalError("Aborted model usage exceeds accounting bounds", cause),
1102
+ ),
1103
+ );
1104
+
1105
+ usage.modelCalls = yield* addProjectedUsage(
1106
+ "modelCalls",
1107
+ usage.modelCalls,
1108
+ summary.modelCalls,
1109
+ );
1110
+ usage.inputTokens = yield* addProjectedUsage(
1111
+ "inputTokens",
1112
+ usage.inputTokens,
1113
+ summary.inputTokens.total,
1114
+ );
1115
+ usage.outputTokens = yield* addProjectedUsage(
1116
+ "outputTokens",
1117
+ usage.outputTokens,
1118
+ summary.outputTokens.total,
1119
+ );
1120
+ usage.costMicrousd = yield* addProjectedUsage(
1121
+ "costMicrousd",
1122
+ usage.costMicrousd,
1123
+ summary.costMicrousd,
1124
+ );
1125
+ usage.modelUsage.push(...payload.modelUsage);
1126
+ unobservedModelCalls = yield* addProjectedUsage(
1127
+ "unobservedModelCalls",
1128
+ unobservedModelCalls,
1129
+ payload.unobservedModelCalls,
1130
+ );
1131
+ }
1071
1132
  if (PROMPT_TRANSPARENT_TAGS.has(payload._tag)) return;
1072
1133
  // The compaction record governs the fold (pre-scan) and contributes no
1073
1134
  // message of its own; records at or below the summarize bound render as
@@ -39,7 +39,7 @@ import { digestDefinitions, digestJson, DigestError } from "../Digest.ts";
39
39
  import type { DurableWorkerFailure, DurableWorkerRequirements } from "../DurableAgentRuntime.ts";
40
40
  import type { DefinitionDigestInput, PersistedJson } from "../Records.ts";
41
41
  import { DefinitionDigests, ReplayContract } from "../Records.ts";
42
- import type { Claim, Settlement } from "../SubmissionLedger.ts";
42
+ import type { Claim, Settlement, SubmissionSnapshot } from "../SubmissionLedger.ts";
43
43
 
44
44
  /** No unique current executable is registered for the stable Agent identity. */
45
45
  export class BindingUnavailable extends Schema.TaggedError<BindingUnavailable>()(
@@ -49,6 +49,29 @@ export class BindingUnavailable extends Schema.TaggedError<BindingUnavailable>()
49
49
 
50
50
  export type DurableBindingFailure = BindingUnavailable;
51
51
 
52
+ /**
53
+ * Submission execution/recovery routing for stable identities shared by current Definitions.
54
+ * Worker declarations and peer-messaging endpoints still require unique Agent identities.
55
+ */
56
+ export interface BindingSelection {
57
+ /** Change when routing changes, so hosts can retry parked work without rewriting admission. */
58
+ readonly key: string;
59
+ /**
60
+ * Select an exact registered Definition using the canonical Submission and authoritative host
61
+ * state. Undefined retains unique-identity resolution. Never grant authority or mutate here;
62
+ * input decoding, replay contracts and current Tool authorization still apply after selection.
63
+ */
64
+ readonly select: (
65
+ submission: SubmissionSnapshot,
66
+ ) => Effect.Effect<Agent.AnyDefinition | undefined, DurableWorkerFailure | BindingUnavailable>;
67
+ }
68
+
69
+ /** Captured at runtime construction; callers cannot replace routing during an Attempt. */
70
+ export const CurrentBindingSelection = Context.Reference<BindingSelection | undefined>(
71
+ "@effect-agent/thread/CurrentBindingSelection",
72
+ { defaultValue: () => undefined },
73
+ );
74
+
52
75
  /** Exact immutable admission, lineage and delivery evidence; never an executable-code gate. */
53
76
  export const definitionDigestsEqual = (
54
77
  left: DefinitionDigests,
@@ -358,50 +381,50 @@ export const DurableWorkerBinding = {
358
381
  /** INTERNAL identity-only capture retained for the legacy direct worker path. */
359
382
  export const makeLegacyWorkerBinding = capture;
360
383
 
361
- export const resolveWorkerBinding = (
384
+ export const resolveWorkerBinding = Effect.fnUntraced(function* (
362
385
  bindings: ReadonlyArray<ResolvedBinding>,
363
- agentId: AgentId,
364
- ): Effect.Effect<ResolvedBinding, BindingUnavailable> => {
365
- const registered = bindings.filter((binding) => binding.agentId === agentId);
386
+ submission: SubmissionSnapshot,
387
+ ) {
388
+ const selection = yield* CurrentBindingSelection;
389
+ const definition = selection === undefined ? undefined : yield* selection.select(submission);
390
+
391
+ const registered = bindings.filter(
392
+ (binding) =>
393
+ binding.agentId === submission.agentId &&
394
+ (definition === undefined || Object.is(binding.definition, definition)),
395
+ );
396
+
366
397
  const binding = registered[0];
367
398
 
368
- return registered.length === 1 && binding !== undefined
369
- ? Effect.succeed(binding)
370
- : Effect.fail(
371
- BindingUnavailable.make({
372
- agentId,
373
- message: `Exactly one current Agent Binding must be registered for ${agentId}`,
374
- }),
375
- );
376
- };
399
+ if (registered.length !== 1 || binding === undefined)
400
+ return yield* BindingUnavailable.make({
401
+ agentId: submission.agentId,
402
+ message: "Exactly one current Agent Binding must match the accepted Submission",
403
+ });
404
+
405
+ return binding;
406
+ });
377
407
 
378
- /** Resolve authoring-time admission through the one exact host registration, never by identity alone. */
408
+ /** Resolve authoring-time admission through exact host registration, never by identity alone. */
379
409
  export const resolveDefinitionBinding = (
380
410
  bindings: ReadonlyArray<ResolvedBinding>,
381
411
  definition: Pick<Agent.AnyDefinition, "id">,
382
412
  ): Effect.Effect<ResolvedBinding, BindingUnavailable> => {
383
- const candidates = bindings.filter((binding) => binding.agentId === definition.id);
384
- const binding = candidates[0];
413
+ const candidates = bindings.filter(
414
+ (binding) => binding.agentId === definition.id && Object.is(binding.definition, definition),
415
+ );
385
416
 
386
- if (candidates.length !== 1 || binding === undefined) {
387
- return Effect.fail(
388
- BindingUnavailable.make({
389
- agentId: definition.id,
390
- message: "Registered admission requires exactly one binding for the Agent identity",
391
- }),
392
- );
393
- }
394
-
395
- if (!Object.is(binding.definition, definition)) {
396
- return Effect.fail(
397
- BindingUnavailable.make({
398
- agentId: definition.id,
399
- message: "Registered admission requires the exact Agent Definition used in registration",
400
- }),
401
- );
402
- }
417
+ const binding = candidates[0];
403
418
 
404
- return Effect.succeed(binding);
419
+ return candidates.length === 1 && binding !== undefined
420
+ ? Effect.succeed(binding)
421
+ : Effect.fail(
422
+ BindingUnavailable.make({
423
+ agentId: definition.id,
424
+ message:
425
+ "Registered admission requires exactly one registration of the exact Agent Definition",
426
+ }),
427
+ );
405
428
  };
406
429
 
407
430
  /** Application versions and a model Layer, supplied directly or through an existing Binding. */
@@ -152,10 +152,17 @@ export interface RunInputCommand {
152
152
  * Dependency-neutral input seam used by the interpreter.
153
153
  *
154
154
  * A capability adapter owns queue bounds and Scope finalization. The engine calls
155
- * `start` before the first drain, `drain` only at safe Turn seams, and `end`
155
+ * `start` before the first drain, `drain` only at safe Turn seams (including after
156
+ * cancellation of a disposable model call), and `end`
156
157
  * exactly once when the Run leaves its Scope.
157
158
  */
158
159
  export interface RunInputHook<Error = never, Requirements = never> {
160
+ /**
161
+ * Wait for an eligible join without appending it to history. Must register before checking
162
+ * authority and retain any claimed input for `drain`, even when this wait is interrupted.
163
+ * Used only with `restartOnJoinedInput`; each wait belongs to the model stream's Scope.
164
+ */
165
+ readonly awaitJoin?: Effect.Effect<void, Error, Requirements> | undefined;
159
166
  readonly start?: (() => Effect.Effect<void, Error, Requirements>) | undefined;
160
167
  readonly drain: (
161
168
  policy: CommandDrainPolicy,
@@ -577,6 +584,14 @@ export class AgentUpdateAcceptance extends Context.Service<
577
584
  * the ephemeral runtime always has.
578
585
  */
579
586
  export interface RunDurabilityHook<Error = never, Requirements = never> {
587
+ /** Persist replacement count and staged usage after cancellation, before starting its successor. */
588
+ readonly commitModelRestart?:
589
+ | ((restart: {
590
+ readonly turn: number;
591
+ readonly turnId: TurnId;
592
+ readonly restart: number;
593
+ }) => Effect.Effect<void, Error, Requirements>)
594
+ | undefined;
580
595
  /** Stage the exact request declarations for the existing canonical response append. */
581
596
  readonly noteToolExposure?:
582
597
  | ((turn: number, snapshot: Snapshot) => Effect.Effect<void, Error, Requirements>)
@@ -948,7 +963,7 @@ export interface RunOptions<HookError = never, HookRequirements = never> {
948
963
  /** Initial or canonically restored run-scoped native selection. */
949
964
  readonly toolSelection?: Selection | undefined;
950
965
  /**
951
- * Host preparation boundary before each new model Turn, including its context preparation
966
+ * Host preparation boundary before each new or replacement model call, including context preparation
952
967
  * and compaction calls. The preceding Tool batch and history advance have finished. A resumed
953
968
  * canonical Tool batch bypasses this hook until it continues to a new Turn. This hook does not
954
969
  * reset the Run deadline or change prompt protection, and is not automatically inherited by spawned children.
@@ -78,6 +78,7 @@ import {
78
78
  BudgetWarning,
79
79
  CompactionPerformed,
80
80
  ModelStarted,
81
+ ModelRestarted,
81
82
  ReasoningDelta,
82
83
  RunCompleted,
83
84
  RunFailed,
@@ -555,6 +556,7 @@ interface RunContext {
555
556
  readonly durationDeadlineMillis: number;
556
557
  history: Prompt.Prompt;
557
558
  modelCalls: number;
559
+ modelRestarts: number;
558
560
  usageStatus: typeof UsageCompleteness.Type;
559
561
  pricingStatus: typeof UsageCompleteness.Type;
560
562
  unobservedModelCalls: number;
@@ -1430,7 +1432,15 @@ const decodeResumeUsage = Effect.fn("AgentRuntime.decodeResumeUsage")((input: un
1430
1432
  };
1431
1433
 
1432
1434
  const optional = Object.fromEntries(
1433
- (["usageStatus", "pricingStatus", "unobservedModelCalls", "children"] as const)
1435
+ (
1436
+ [
1437
+ "usageStatus",
1438
+ "pricingStatus",
1439
+ "unobservedModelCalls",
1440
+ "children",
1441
+ "modelRestarts",
1442
+ ] as const
1443
+ )
1434
1444
  .map((key) => [key, read(key, true)])
1435
1445
  .filter(([, value]) => value !== undefined),
1436
1446
  );
@@ -1893,7 +1903,11 @@ const agentTelemetryAttributes = (context: RunContext) => ({
1893
1903
  });
1894
1904
 
1895
1905
  /** Label Effect AI's existing span, preserving provider usage, sampling and host async context. */
1896
- const modelTelemetryTracer = Effect.fnUntraced(function* (context: RunContext, turnId?: TurnId) {
1906
+ const modelTelemetryTracer = Effect.fnUntraced(function* (
1907
+ context: RunContext,
1908
+ turnId?: TurnId,
1909
+ onSpan?: (span: Tracer.Span) => void,
1910
+ ) {
1897
1911
  const delegate = yield* Tracer.Tracer;
1898
1912
  const model = yield* Model.ModelName;
1899
1913
  const provider = yield* Model.ProviderName;
@@ -1904,6 +1918,8 @@ const modelTelemetryTracer = Effect.fnUntraced(function* (context: RunContext, t
1904
1918
 
1905
1919
  const span = delegate.span({ ...options, name: `chat ${model}` });
1906
1920
 
1921
+ onSpan?.(span);
1922
+
1907
1923
  const attributes = {
1908
1924
  ...agentTelemetryAttributes(context),
1909
1925
  "gen_ai.operation.name": "chat",
@@ -5343,6 +5359,7 @@ function decodeRunDisposition<DispositionSchema extends Schema.Top>(
5343
5359
  /** Internal control output consumed by the driver before RunEvent publication. */
5344
5360
  interface NextTurn {
5345
5361
  readonly _tag: "NextTurn";
5362
+ readonly restarting?: boolean;
5346
5363
  readonly prompt: Prompt.Prompt;
5347
5364
  readonly turn: number;
5348
5365
  readonly toolCalls: number;
@@ -5388,6 +5405,7 @@ const makeTurn = <
5388
5405
  turn: number,
5389
5406
  priorToolCalls: number,
5390
5407
  options: RunOptions<HookError, HookRequirements>,
5408
+ restarting = false,
5391
5409
  ): Stream.Stream<
5392
5410
  TurnOutput,
5393
5411
  AgentRuntimeFailure<typeof agent, HookError, InstructionError>,
@@ -5416,7 +5434,7 @@ const makeTurn = <
5416
5434
  }),
5417
5435
  );
5418
5436
  }
5419
- if (context.finalizationUsed) {
5437
+ if (context.finalizationUsed && !restarting) {
5420
5438
  return failRunEventStream(
5421
5439
  AgentPolicyError.make({
5422
5440
  limit:
@@ -5591,6 +5609,9 @@ const makeTurn = <
5591
5609
  }
5592
5610
  }
5593
5611
 
5612
+ let restartRequested = false;
5613
+ let activeModelSpan: Tracer.Span | undefined;
5614
+
5594
5615
  const trace: TurnTrace = {
5595
5616
  responsePartCount: 0,
5596
5617
  responsePartBytes: 0,
@@ -6206,6 +6227,10 @@ const makeTurn = <
6206
6227
  );
6207
6228
  });
6208
6229
 
6230
+ let failedUsageCause:
6231
+ | Cause.Cause<AgentRuntimeFailure<typeof agent, HookError, InstructionError>>
6232
+ | undefined;
6233
+
6209
6234
  // Failure accounting observes the already-selected failure; a secondary
6210
6235
  // budget/estimator failure must not replace that native outcome.
6211
6236
  const retainFailedUsage = Effect.fn("AgentRuntime.retainFailedUsage")(function* () {
@@ -6214,7 +6239,9 @@ const makeTurn = <
6214
6239
  if (trace.usage === undefined) {
6215
6240
  return yield* noteIncompleteUsage(context, turn);
6216
6241
  }
6217
- yield* consumeTurnUsage(0).pipe(Effect.exit);
6242
+ const exit = yield* consumeTurnUsage(0).pipe(Effect.exit);
6243
+
6244
+ if (Exit.isFailure(exit)) failedUsageCause = exit.cause;
6218
6245
  });
6219
6246
 
6220
6247
  const attempt = (basis: Prompt.Prompt) =>
@@ -6337,7 +6364,9 @@ const makeTurn = <
6337
6364
  : stream,
6338
6365
  Stream.provideServiceEffect(
6339
6366
  Tracer.Tracer,
6340
- modelTelemetryTracer(context, turnId),
6367
+ modelTelemetryTracer(context, turnId, (span) => {
6368
+ activeModelSpan = span;
6369
+ }),
6341
6370
  ),
6342
6371
  Stream.onStart(
6343
6372
  Effect.sync(() => {
@@ -6365,7 +6394,9 @@ const makeTurn = <
6365
6394
  ),
6366
6395
  ),
6367
6396
  Stream.flatMap(Stream.fromIterable),
6368
- Stream.tapCause(() => retainFailedUsage()),
6397
+ Stream.onExit((exit) =>
6398
+ Exit.isFailure(exit) ? retainFailedUsage() : Effect.void,
6399
+ ),
6369
6400
  ),
6370
6401
  ),
6371
6402
  );
@@ -7059,10 +7090,83 @@ const makeTurn = <
7059
7090
  }),
7060
7091
  );
7061
7092
 
7093
+ // Only the model stream is cancellable. It resolves no application Tools and
7094
+ // closes its waiter before continuation can commit a response or start a Handler.
7095
+ // Provider-defined Tools may execute remotely before reporting a part: fail closed
7096
+ // for the whole call when any are exposed.
7097
+ const restartSignal =
7098
+ policy.restartOnJoinedInput === true &&
7099
+ context.modelRestarts < 2 &&
7100
+ (options.durability === undefined || options.durability.commitModelRestart !== undefined) &&
7101
+ !Object.values(modelToolkit.tools).some(Tool.isProviderDefined)
7102
+ ? options.input?.awaitJoin
7103
+ : undefined;
7104
+
7105
+ const disposableResponse =
7106
+ restartSignal !== undefined
7107
+ ? response.pipe(
7108
+ Stream.interruptWhen(
7109
+ restartSignal.pipe(
7110
+ Effect.tap(() =>
7111
+ Effect.sync(() => {
7112
+ restartRequested = true;
7113
+ activeModelSpan?.attribute("effect_agent.model.outcome", "aborted");
7114
+ activeModelSpan?.attribute("effect_agent.model.abort_reason", "joined-input");
7115
+ }),
7116
+ ),
7117
+ ),
7118
+ ),
7119
+ )
7120
+ : response;
7121
+
7122
+ const afterResponse = Stream.unwrap(
7123
+ Effect.gen(function* () {
7124
+ if (!restartRequested)
7125
+ return continuation.pipe(Stream.tapCause(() => retainFailedUsage()));
7126
+
7127
+ const discarded = ModelRestarted.make({
7128
+ ...(yield* eventBase(context)),
7129
+ turnId,
7130
+ turn,
7131
+ reason: "joined-input",
7132
+ });
7133
+
7134
+ // Publish invalidation before any fallible persistence, input rendering or budget check.
7135
+ return Stream.succeed(discarded).pipe(
7136
+ Stream.concat(
7137
+ Stream.unwrap(
7138
+ Effect.gen(function* () {
7139
+ yield* retainFailedUsage();
7140
+ context.modelRestarts++;
7141
+ if (options.durability?.commitModelRestart !== undefined)
7142
+ yield* options.durability.commitModelRestart({
7143
+ turn,
7144
+ turnId,
7145
+ restart: context.modelRestarts,
7146
+ });
7147
+ if (failedUsageCause !== undefined)
7148
+ return yield* Effect.failCause(failedUsageCause);
7149
+ const inputs = yield* drainInputs(context, options);
7150
+ const nextPrompt = yield* appendInputs(context, prompt, inputs, options);
7151
+
7152
+ return Stream.succeed<TurnOutput>({
7153
+ _tag: "NextTurn",
7154
+ prompt: nextPrompt,
7155
+ turn,
7156
+ toolCalls: priorToolCalls,
7157
+ restarting: true,
7158
+ });
7159
+ }),
7160
+ ),
7161
+ ),
7162
+ );
7163
+ }),
7164
+ );
7165
+
7062
7166
  const events = Stream.fromIterable(preEvents).pipe(
7063
7167
  Stream.concat(started),
7064
- Stream.concat(response),
7065
- Stream.concat(continuation.pipe(Stream.tapCause(() => retainFailedUsage()))),
7168
+ Stream.concat(disposableResponse),
7169
+ Stream.concat(afterResponse),
7066
7170
  );
7067
7171
 
7068
7172
  return modelServices === undefined ? events : Stream.provideContext(events, modelServices);
@@ -8144,6 +8248,7 @@ function streamWithCompletion<
8144
8248
  // canonical response records so token budgets and the compaction
8145
8249
  // trigger keep accounting across ownership changes.
8146
8250
  modelCalls: resumeUsage?.modelCalls ?? 0,
8251
+ modelRestarts: resumeUsage?.modelRestarts ?? 0,
8147
8252
  usageStatus:
8148
8253
  resumeUsage?.usageStatus ?? (resumeUsage === undefined ? "complete" : "unknown"),
8149
8254
  pricingStatus:
@@ -8379,6 +8484,7 @@ function streamWithCompletion<
8379
8484
  readonly turn: number;
8380
8485
  readonly toolCalls: number;
8381
8486
  readonly resume?: RunTurnResume;
8487
+ readonly restarting?: boolean;
8382
8488
  }
8383
8489
  | undefined;
8384
8490
 
@@ -8425,6 +8531,7 @@ function streamWithCompletion<
8425
8531
  request.turn,
8426
8532
  request.toolCalls,
8427
8533
  options,
8534
+ request.restarting,
8428
8535
  )
8429
8536
  : makeResumeTurn(
8430
8537
  agent,