effect-agent 0.1.0-beta.95 → 0.1.0-beta.97

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 (124) hide show
  1. package/dist/{AgentRuntime-CufqQ4JM.d.mts → AgentRuntime-B64362Id.d.mts} +2 -2
  2. package/dist/AgentRuntime-Cdvqw7l_.mjs.map +1 -1
  3. package/dist/{DurableAgentRuntime-BaGsWDHO.mjs → DurableAgentRuntime-DbUvZHDZ.mjs} +82 -66
  4. package/dist/DurableAgentRuntime-DbUvZHDZ.mjs.map +1 -0
  5. package/dist/DurableFailpoint-DQJet828.d.mts +41 -0
  6. package/dist/{EventSource-DVfX1GRH.d.mts → EventSource-D6PAAEyK.d.mts} +3 -3
  7. package/dist/{PreparedInputAdmission-DoWaMC2R.d.mts → PreparedInputAdmission-omMbwd--.d.mts} +4 -4
  8. package/dist/{Records-CsEsSW2o.mjs → Records-BAutSSnJ.mjs} +18 -5
  9. package/dist/Records-BAutSSnJ.mjs.map +1 -0
  10. package/dist/{Records-B84j3Jq_.d.mts → Records-CV49DgQK.d.mts} +90 -16
  11. package/dist/{RunJournal-yYpUlTOE.mjs → RunJournal-DRRRZmG7.mjs} +8 -3
  12. package/dist/RunJournal-DRRRZmG7.mjs.map +1 -0
  13. package/dist/{RunOptions-CE-G3niT.d.mts → RunOptions-Clj8cLsp.d.mts} +4 -4
  14. package/dist/{Schedule-DCYBfhgT.d.mts → Schedule-DePbGfgd.d.mts} +2 -2
  15. package/dist/{Scheduling-CUuYc3-D.mjs → Scheduling-C3QpYn0W.mjs} +2 -2
  16. package/dist/{Scheduling-CUuYc3-D.mjs.map → Scheduling-C3QpYn0W.mjs.map} +1 -1
  17. package/dist/{Subagent-DscbtbNY.d.mts → Subagent-Bm1dRecN.d.mts} +3 -3
  18. package/dist/{Subscription-C8IHdmFk.d.mts → Subscription-BR_8Cxh3.d.mts} +3 -3
  19. package/dist/{SubscriptionInput-DhM3jUW2.d.mts → SubscriptionInput-B95sQbuk.d.mts} +3 -3
  20. package/dist/agent-registration-CXGDwvJH.mjs +379 -0
  21. package/dist/agent-registration-CXGDwvJH.mjs.map +1 -0
  22. package/dist/capabilities/Commands.d.mts +1 -1
  23. package/dist/capabilities/RunHooks.d.mts +1 -1
  24. package/dist/capabilities/Subagent.d.mts +1 -1
  25. package/dist/durable/ActivityStore.d.mts +1 -1
  26. package/dist/durable/ActivityStore.mjs +1 -1
  27. package/dist/durable/Admin.d.mts +1 -1
  28. package/dist/durable/Admin.mjs +1 -1
  29. package/dist/durable/AgentRegistration.d.mts +2 -2
  30. package/dist/durable/AgentRegistration.mjs +7 -3
  31. package/dist/durable/AgentRegistration.mjs.map +1 -1
  32. package/dist/durable/Certification.d.mts +2 -2
  33. package/dist/durable/CommittedActivity.d.mts +1 -1
  34. package/dist/durable/CommittedActivity.mjs +1 -1
  35. package/dist/durable/Digest.d.mts +1 -1
  36. package/dist/durable/Digest.mjs +1 -1
  37. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  38. package/dist/durable/DurableAgentRuntime.mjs +1 -1
  39. package/dist/durable/DurableFailpoint.d.mts +1 -1
  40. package/dist/durable/DurableFailpoint.mjs +2 -0
  41. package/dist/durable/DurableFailpoint.mjs.map +1 -1
  42. package/dist/durable/DurableFailpointTestControl.d.mts +1 -1
  43. package/dist/durable/EventSource.d.mts +1 -1
  44. package/dist/durable/EventSource.mjs +1 -1
  45. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  46. package/dist/durable/GitHubWorkflowSource.mjs +1 -1
  47. package/dist/durable/MessageDelivery.d.mts +3 -3
  48. package/dist/durable/MessageDelivery.mjs +1 -1
  49. package/dist/durable/MessageDeliveryStoreConformance.d.mts +39 -39
  50. package/dist/durable/MessageDeliveryStoreConformance.mjs +1 -1
  51. package/dist/durable/PersistentHistory.d.mts +1 -1
  52. package/dist/durable/PersistentHistory.mjs +2 -2
  53. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  54. package/dist/durable/Records.d.mts +2 -2
  55. package/dist/durable/Records.mjs +2 -2
  56. package/dist/durable/Recovery.d.mts +1 -1
  57. package/dist/durable/Recovery.mjs +1 -1
  58. package/dist/durable/RunJournal.d.mts +2 -2
  59. package/dist/durable/RunJournal.mjs +2 -2
  60. package/dist/durable/Schedule.d.mts +1 -1
  61. package/dist/durable/Schedule.mjs +1 -1
  62. package/dist/durable/ScheduleStoreConformance.d.mts +1 -1
  63. package/dist/durable/ScheduleStoreConformance.mjs +1 -1
  64. package/dist/durable/ScheduleTransition.d.mts +1 -1
  65. package/dist/durable/ScheduleTransition.mjs +1 -1
  66. package/dist/durable/Scheduling.d.mts +2 -2
  67. package/dist/durable/Scheduling.mjs +1 -1
  68. package/dist/durable/SqlStorageV2Upgrade.d.mts +2 -2
  69. package/dist/durable/SqlStorageV2Upgrade.mjs +1 -1
  70. package/dist/durable/SqlSubscriptionStore.d.mts +2 -2
  71. package/dist/durable/SqlSubscriptionStore.mjs +1 -1
  72. package/dist/durable/SubmissionLedger.d.mts +1 -1
  73. package/dist/durable/SubmissionLedger.mjs +1 -1
  74. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  75. package/dist/durable/SubmissionLedgerConformance.mjs +1 -1
  76. package/dist/durable/SubmissionStatus.d.mts +1 -1
  77. package/dist/durable/Subscription.d.mts +1 -1
  78. package/dist/durable/Subscription.mjs +1 -1
  79. package/dist/durable/SubscriptionInput.d.mts +1 -1
  80. package/dist/durable/SubscriptionInput.mjs +2 -2
  81. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  82. package/dist/durable/SubscriptionStoreConformance.mjs +1 -1
  83. package/dist/durable/SubscriptionTools.d.mts +3 -3
  84. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  85. package/dist/durable/SubscriptionTransition.mjs +1 -1
  86. package/dist/durable/Subscriptions.d.mts +5 -5
  87. package/dist/durable/Subscriptions.mjs +1 -1
  88. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  89. package/dist/durable/ThreadContextHistory.mjs +1 -1
  90. package/dist/durable/ThreadContextHistoryProjection.d.mts +1 -1
  91. package/dist/durable/ThreadContextHistoryProjection.mjs +1 -1
  92. package/dist/durable/ThreadInvariants.d.mts +1 -1
  93. package/dist/durable/ThreadInvariants.mjs +1 -1
  94. package/dist/durable/ThreadProjection.d.mts +1 -1
  95. package/dist/durable/ThreadProjection.mjs +1 -1
  96. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  97. package/dist/durable/ThreadStore.d.mts +1 -1
  98. package/dist/durable/ThreadStore.mjs +1 -1
  99. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  100. package/dist/durable/ThreadStoreConformance.mjs +1 -1
  101. package/dist/durable/ToolReconciler.mjs +1 -1
  102. package/dist/durable/WorkerHost.d.mts +1 -1
  103. package/dist/engine/AgentRuntime.d.mts +1 -1
  104. package/dist/engine/RunOptions.d.mts +1 -1
  105. package/dist/engine/RunOptions.mjs.map +1 -1
  106. package/dist/index.d.mts +10 -10
  107. package/dist/index.mjs +4 -4
  108. package/package.json +1 -1
  109. package/src/durable/AgentRegistration.ts +5 -0
  110. package/src/durable/DurableAgentRuntime.ts +155 -84
  111. package/src/durable/DurableFailpoint.ts +2 -0
  112. package/src/durable/Records.ts +19 -2
  113. package/src/durable/RunJournal.ts +6 -0
  114. package/src/durable/internal/agent-registration.ts +427 -14
  115. package/src/durable/internal/messaging-host.ts +3 -6
  116. package/src/durable/internal/worker-host.ts +114 -88
  117. package/src/engine/RunOptions.ts +3 -3
  118. package/src/engine/internal/agent-runtime.ts +3 -4
  119. package/dist/DurableAgentRuntime-BaGsWDHO.mjs.map +0 -1
  120. package/dist/DurableFailpoint-Oinuuedm.d.mts +0 -41
  121. package/dist/Records-CsEsSW2o.mjs.map +0 -1
  122. package/dist/RunJournal-yYpUlTOE.mjs.map +0 -1
  123. package/dist/agent-registration-CWDMOAZA.mjs +0 -172
  124. package/dist/agent-registration-CWDMOAZA.mjs.map +0 -1
@@ -1,4 +1,13 @@
1
- import { type Crypto, type Option, type Scope, Context, Effect, Layer, Schema } from "effect";
1
+ import {
2
+ type Crypto,
3
+ type Option,
4
+ type Scope,
5
+ Array,
6
+ Context,
7
+ Effect,
8
+ Layer,
9
+ Schema,
10
+ } from "effect";
2
11
  import { Tool } from "effect/unstable/ai";
3
12
 
4
13
  import type * as Agent from "../../core/Agent.ts";
@@ -9,6 +18,7 @@ import {
9
18
  type RunDispositionDeclaration,
10
19
  } from "../../core/Agent.ts";
11
20
  import { type ThreadId, AgentId } from "../../core/Identifiers.ts";
21
+ import { getToolExecutionKind } from "../../core/SubagentContract.ts";
12
22
  import {
13
23
  AdditionalToolCatalog,
14
24
  DiscoveryTool,
@@ -17,14 +27,20 @@ import {
17
27
  ToolNamespace,
18
28
  } from "../../core/ToolExposure.ts";
19
29
  import { type RuntimeBinding } from "../../engine/AgentRuntime.ts";
30
+ import { getToolExecutionClass } from "../../engine/DurableStep.ts";
20
31
  import {
21
32
  BackgroundReporting,
22
33
  WorkerReportPreparationFailure,
23
34
  type WorkerReporting,
24
35
  } from "../../engine/SubagentHost.ts";
25
- import { digestDefinitions, DigestError } from "../Digest.ts";
36
+ import { digestDefinitions, digestJson, DigestError } from "../Digest.ts";
26
37
  import type { DurableWorkerFailure, DurableWorkerRequirements } from "../DurableAgentRuntime.ts";
27
- import type { DefinitionDigestInput, DefinitionDigests } from "../Records.ts";
38
+ import {
39
+ DefinitionDigestInput,
40
+ DefinitionDigests,
41
+ ReplayContract,
42
+ PersistedJson,
43
+ } from "../Records.ts";
28
44
  import type { Claim, Settlement } from "../SubmissionLedger.ts";
29
45
 
30
46
  /**
@@ -41,10 +57,9 @@ export class BindingUnavailable extends Schema.TaggedError<BindingUnavailable>()
41
57
  ) {}
42
58
 
43
59
  /**
44
- * A Binding is registered for the identity but its exact stored
45
- * definition/model/tool digests do not match the claimed head's stored
46
- * digests byte-for-byte. Fail closed (SUB-023): missing or different code is
47
- * a typed compatibility failure, never a silent substitution.
60
+ * A Binding is registered for the identity but neither the exact admission fingerprints
61
+ * nor its declared replay contract support the retained pending operations. Refusal leaves
62
+ * the original Submission pending until compatible code is available.
48
63
  */
49
64
  export class BindingDigestMismatch extends Schema.TaggedError<BindingDigestMismatch>()(
50
65
  "BindingDigestMismatch",
@@ -62,7 +77,370 @@ export const definitionDigestsEqual = (
62
77
  left: DefinitionDigests,
63
78
  right: DefinitionDigests,
64
79
  ): boolean =>
65
- left.agent === right.agent && left.model === right.model && left.tools === right.tools;
80
+ left.agent === right.agent &&
81
+ left.model === right.model &&
82
+ left.tools === right.tools &&
83
+ Schema.toEquivalence(Schema.UndefinedOr(ReplayContract))(left.replay, right.replay);
84
+
85
+ /**
86
+ * Host-owned semantic versions, independent of deployment and tool inventory. Change a version
87
+ * when the meaning of an operation, its durable Step names or external idempotency keys changes.
88
+ * Codecs and execution class are included automatically; JSON Schema alone cannot prove semantics.
89
+ */
90
+ export interface ReplayVersions {
91
+ readonly agent: PersistedJson;
92
+ readonly tools: Readonly<Record<string, PersistedJson>>;
93
+ }
94
+
95
+ /**
96
+ * Data exported from a definition, never an old executable Binding. A host may supply an audited
97
+ * manifest from the original release for work admitted before replay contracts were retained.
98
+ * Its original declarations must hash to that admission; a bare old digest is not a manifest.
99
+ */
100
+ const ToolReplayDeclaration = Schema.Struct({
101
+ version: PersistedJson,
102
+ parameters: PersistedJson,
103
+ success: PersistedJson,
104
+ failure: PersistedJson,
105
+ failureMode: Schema.String,
106
+ needsApproval: Schema.Union([Schema.Boolean, Schema.Literal("dynamic")]),
107
+ executionClass: Schema.String,
108
+ executionKind: Schema.String,
109
+ });
110
+
111
+ const AgentReplayDeclaration = Schema.Struct({
112
+ version: PersistedJson,
113
+ id: AgentId,
114
+ input: PersistedJson,
115
+ output: PersistedJson,
116
+ updates: Schema.NullOr(PersistedJson),
117
+ completion: Schema.NullOr(Schema.Struct({ tool: Schema.String, required: Schema.Boolean })),
118
+ completionFromTools: Schema.Array(Schema.String),
119
+ disposition: Schema.NullOr(PersistedJson),
120
+ });
121
+
122
+ export class BindingManifest extends Schema.Class<BindingManifest>("BindingManifest")({
123
+ agentId: AgentId,
124
+ definitions: DefinitionDigestInput,
125
+ replay: ReplayContract,
126
+ agentContract: Schema.optionalKey(AgentReplayDeclaration),
127
+ toolContracts: Schema.optionalKey(Schema.Record(Schema.String, ToolReplayDeclaration)),
128
+ }) {}
129
+
130
+ export const makeBindingManifest = Effect.fn("AgentRegistration.makeBindingManifest")(function* (
131
+ definition: Agent.AnyDefinition,
132
+ definitions: DefinitionDigestInput,
133
+ versions: ReplayVersions,
134
+ ) {
135
+ const jsonSchema = (schema: Schema.Top) =>
136
+ Schema.decodeUnknownSync(Schema.Json)(Tool.getJsonSchemaFromSchema(schema));
137
+
138
+ const contracts = yield* Effect.try({
139
+ try: () => {
140
+ const tools = Object.values(definition.toolkit.tools);
141
+
142
+ if (tools.some((tool) => !Object.hasOwn(versions.tools, tool.name)))
143
+ throw new Error("Every Tool needs an explicit replay semantic version");
144
+
145
+ return {
146
+ agent: {
147
+ version: versions.agent,
148
+ id: definition.id,
149
+ input: jsonSchema(definition.input),
150
+ output: jsonSchema(definition.output),
151
+ updates: definition.updates === undefined ? null : jsonSchema(definition.updates),
152
+ completion:
153
+ definition.completion === undefined
154
+ ? null
155
+ : {
156
+ tool: definition.completion.tool,
157
+ required: definition.completion.required ?? false,
158
+ },
159
+ completionFromTools:
160
+ definition.completionFromTools?.map((entry) => entry.tool).sort() ?? [],
161
+ disposition:
162
+ definition.runDisposition === undefined
163
+ ? null
164
+ : jsonSchema(definition.runDisposition.schema),
165
+ },
166
+ tools: tools.map((tool) => ({
167
+ name: tool.name,
168
+ contract: {
169
+ version: versions.tools[tool.name]!,
170
+ parameters: jsonSchema(tool.parametersSchema),
171
+ success: jsonSchema(tool.successSchema),
172
+ failure: jsonSchema(tool.failureSchema),
173
+ failureMode: tool.failureMode,
174
+ needsApproval:
175
+ typeof tool.needsApproval === "function" ? "dynamic" : (tool.needsApproval ?? false),
176
+ executionClass: getToolExecutionClass(tool),
177
+ executionKind: getToolExecutionKind(tool.annotations),
178
+ },
179
+ })),
180
+ };
181
+ },
182
+ catch: () =>
183
+ DigestError.make({
184
+ message:
185
+ "Replay contracts require serializable codecs and an explicit semantic version for every Tool",
186
+ }),
187
+ });
188
+
189
+ const agent = yield* digestJson(contracts.agent);
190
+ const agentBehavior = yield* digestJson({ ...contracts.agent, input: null });
191
+
192
+ const tools = yield* Effect.forEach(contracts.tools, ({ name, contract }) =>
193
+ digestJson(contract).pipe(Effect.map((digest) => [name, digest] as const)),
194
+ );
195
+
196
+ return BindingManifest.make({
197
+ agentId: definition.id,
198
+ definitions,
199
+ replay: ReplayContract.make({ agent, agentBehavior, tools: Object.fromEntries(tools) }),
200
+ agentContract: contracts.agent,
201
+ toolContracts: Object.fromEntries(
202
+ contracts.tools.map(({ name, contract }) => [name, contract]),
203
+ ),
204
+ });
205
+ });
206
+
207
+ const jsonEqual = Schema.toEquivalence(PersistedJson);
208
+
209
+ // JSON Schema generation can widen object openness without changing an Effect codec.
210
+ // Prove only this widening; all other codec changes still require an exact contract.
211
+ const schemasCompatible = (previous: PersistedJson, current: PersistedJson): boolean => {
212
+ if (jsonEqual(previous, current)) return true;
213
+
214
+ const hasNegativeSchema = (value: PersistedJson): boolean => {
215
+ if (Array.isArray<PersistedJson>(value)) return value.some(hasNegativeSchema);
216
+ if (value === null || typeof value !== "object") return false;
217
+
218
+ return Object.entries(value).some(
219
+ ([key, child]) =>
220
+ key === "oneOf" ||
221
+ key === "maxContains" ||
222
+ key === "unevaluatedProperties" ||
223
+ key === "unevaluatedItems" ||
224
+ key === "if" ||
225
+ key === "then" ||
226
+ key === "else" ||
227
+ key === "$dynamicRef" ||
228
+ key === "$recursiveRef" ||
229
+ (key === "not" && !jsonEqual(child, {})) ||
230
+ hasNegativeSchema(child),
231
+ );
232
+ };
233
+
234
+ if (hasNegativeSchema(previous) || hasNegativeSchema(current)) return false;
235
+
236
+ const compare = (before: PersistedJson, after: PersistedJson): boolean => {
237
+ if (jsonEqual(before, after)) return true;
238
+ if (
239
+ before === null ||
240
+ typeof before !== "object" ||
241
+ Array.isArray<PersistedJson>(before) ||
242
+ after === null ||
243
+ typeof after !== "object" ||
244
+ Array.isArray<PersistedJson>(after)
245
+ )
246
+ return false;
247
+ if (Object.keys(before).length !== Object.keys(after).length) return false;
248
+
249
+ return Object.entries(before).every(([key, value]) => {
250
+ if (!Object.hasOwn(after, key)) return false;
251
+ const next = after[key]!;
252
+
253
+ if (jsonEqual(value, next)) return true;
254
+ if (key === "additionalProperties") return value === false && next === true;
255
+ if (key === "items") return compare(value, next);
256
+ if (
257
+ (key === "anyOf" || key === "allOf") &&
258
+ Array.isArray<PersistedJson>(value) &&
259
+ Array.isArray<PersistedJson>(next)
260
+ )
261
+ return (
262
+ value.length === next.length &&
263
+ value.every((entry, index) => compare(entry, next[index]!))
264
+ );
265
+ if (
266
+ (key === "properties" || key === "$defs" || key === "definitions") &&
267
+ value !== null &&
268
+ typeof value === "object" &&
269
+ !Array.isArray<PersistedJson>(value) &&
270
+ next !== null &&
271
+ typeof next === "object" &&
272
+ !Array.isArray<PersistedJson>(next)
273
+ )
274
+ return (
275
+ Object.keys(value).length === Object.keys(next).length &&
276
+ Object.entries(value).every(
277
+ ([name, schema]) => Object.hasOwn(next, name) && compare(schema, next[name]!),
278
+ )
279
+ );
280
+
281
+ return false;
282
+ });
283
+ };
284
+
285
+ return compare(previous, current);
286
+ };
287
+
288
+ /** Compile audited declaration data once, without acquiring an executable or rewriting receipts.
289
+ * Full input schemas stay in registration metadata, never in each admission's replay contract. */
290
+ export const compileBindingManifest = Effect.fn("AgentRegistration.compileBindingManifest")(
291
+ function* (
292
+ definition: Agent.AnyDefinition,
293
+ definitions: DefinitionDigestInput,
294
+ versions: ReplayVersions,
295
+ retained: ReadonlyArray<BindingManifest> = [],
296
+ ): Effect.fn.Return<
297
+ Pick<ResolvedBinding, "digests" | "manifest" | "retainedManifests" | "acceptsInput">,
298
+ DigestError,
299
+ Crypto.Crypto
300
+ > {
301
+ const manifest = yield* makeBindingManifest(definition, definitions, versions);
302
+
303
+ const validContract = Effect.fnUntraced(function* (entry: BindingManifest) {
304
+ if (entry.agentContract === undefined) return undefined;
305
+ if (
306
+ entry.agentContract.id !== entry.agentId ||
307
+ (yield* digestJson(entry.agentContract)) !== entry.replay.agent ||
308
+ (entry.replay.agentBehavior !== undefined &&
309
+ (yield* digestJson({ ...entry.agentContract, input: null })) !==
310
+ entry.replay.agentBehavior)
311
+ )
312
+ return yield* DigestError.make({
313
+ message: "A manifest's Agent contract does not match its replay digest",
314
+ });
315
+
316
+ return entry.agentContract;
317
+ });
318
+
319
+ const current = manifest.agentContract!;
320
+ const acceptsInput = Schema.is(Schema.toEncoded(definition.input));
321
+
322
+ const validateTools = Effect.fnUntraced(function* (entry: BindingManifest) {
323
+ for (const [name, contract] of Object.entries(entry.toolContracts ?? {}))
324
+ if ((yield* digestJson(contract)) !== entry.replay.tools[name])
325
+ return yield* DigestError.make({
326
+ message: "A manifest's Tool contract does not match its replay digest",
327
+ });
328
+ });
329
+
330
+ const digests = yield* digestDefinitions(manifest.definitions);
331
+
332
+ const retainedManifests = yield* Effect.forEach(
333
+ retained,
334
+ Effect.fnUntraced(function* (entry) {
335
+ if (entry.agentId !== manifest.agentId)
336
+ return yield* DigestError.make({
337
+ message: "A retained manifest belongs to another Agent",
338
+ });
339
+ const previous = yield* validContract(entry);
340
+
341
+ yield* validateTools(entry);
342
+
343
+ const compatible =
344
+ previous !== undefined &&
345
+ jsonEqual(
346
+ { ...previous, input: null, output: null, updates: null, disposition: null },
347
+ { ...current, input: null, output: null, updates: null, disposition: null },
348
+ ) &&
349
+ schemasCompatible(previous.output, current.output) &&
350
+ schemasCompatible(previous.updates, current.updates) &&
351
+ schemasCompatible(previous.disposition, current.disposition);
352
+
353
+ const compatibleTools: Record<string, string> = {};
354
+
355
+ for (const [name, oldTool] of Object.entries(entry.toolContracts ?? {})) {
356
+ const newTool = manifest.toolContracts?.[name];
357
+
358
+ if (
359
+ newTool !== undefined &&
360
+ jsonEqual(
361
+ { ...oldTool, parameters: null, success: null, failure: null },
362
+ { ...newTool, parameters: null, success: null, failure: null },
363
+ ) &&
364
+ schemasCompatible(oldTool.parameters, newTool.parameters) &&
365
+ schemasCompatible(oldTool.success, newTool.success) &&
366
+ schemasCompatible(oldTool.failure, newTool.failure)
367
+ )
368
+ compatibleTools[name] = manifest.replay.tools[name]!;
369
+ }
370
+
371
+ return {
372
+ digests: yield* digestDefinitions(entry.definitions),
373
+ replay: entry.replay,
374
+ compatibleTools,
375
+ ...(compatible ? { compatibleAgent: manifest.replay.agent } : {}),
376
+ };
377
+ }),
378
+ );
379
+
380
+ return {
381
+ digests: DefinitionDigests.make({ ...digests, replay: manifest.replay }),
382
+ manifest,
383
+ retainedManifests,
384
+ acceptsInput: (input: PersistedJson) => {
385
+ // Async checks and defective guards cannot establish synchronous replay proof.
386
+ try {
387
+ return acceptsInput(input);
388
+ } catch {
389
+ return false;
390
+ }
391
+ },
392
+ };
393
+ },
394
+ );
395
+
396
+ const replaySupports = (
397
+ current: ReplayContract,
398
+ retained: ReplayContract,
399
+ requiredTools?: ReadonlyArray<string>,
400
+ inputCompatible = false,
401
+ compatibleTools: Readonly<Record<string, string>> = {},
402
+ ): boolean =>
403
+ (current.agent === retained.agent || inputCompatible) &&
404
+ (requiredTools ?? Object.keys(retained.tools)).every(
405
+ (name) =>
406
+ retained.tools[name] !== undefined &&
407
+ (current.tools[name] === retained.tools[name] ||
408
+ (current.tools[name] !== undefined && compatibleTools[name] === current.tools[name])),
409
+ );
410
+
411
+ /** Resolve executable code only. Admission, delivery, lineage and receipt comparisons stay exact. */
412
+ export const bindingSupports = (
413
+ binding: Pick<ResolvedBinding, "digests" | "retainedManifests" | "acceptsInput">,
414
+ digests: DefinitionDigests,
415
+ requiredTools?: ReadonlyArray<string>,
416
+ input?: PersistedJson,
417
+ ): boolean => {
418
+ if (definitionDigestsEqual(binding.digests, digests)) return true;
419
+
420
+ const manifest = binding.retainedManifests?.find(
421
+ (entry) =>
422
+ definitionDigestsEqual(entry.digests, digests) ||
423
+ definitionDigestsEqual({ ...entry.digests, replay: entry.replay }, digests),
424
+ );
425
+
426
+ const retained = digests.replay ?? manifest?.replay;
427
+
428
+ return (
429
+ binding.digests.replay !== undefined &&
430
+ retained !== undefined &&
431
+ replaySupports(
432
+ binding.digests.replay,
433
+ retained,
434
+ requiredTools,
435
+ input !== undefined &&
436
+ binding.acceptsInput?.(input) === true &&
437
+ ((binding.digests.replay.agentBehavior !== undefined &&
438
+ binding.digests.replay.agentBehavior === retained.agentBehavior) ||
439
+ manifest?.compatibleAgent === binding.digests.replay.agent),
440
+ manifest?.compatibleTools,
441
+ )
442
+ );
443
+ };
66
444
 
67
445
  type ExecutableDefinition = Agent.AnyDefinition & {
68
446
  readonly instructions: InstructionSource<never, unknown, unknown>;
@@ -194,6 +572,16 @@ const backgroundReports = (definition: Agent.AnyDefinition) => [
194
572
  /** One exact executable registration used by durable claim-time resolution. */
195
573
  export interface ResolvedBinding extends CapturedBinding {
196
574
  readonly digests: DefinitionDigests;
575
+ readonly manifest?: BindingManifest;
576
+ /** Current encoded input guard, acquired without executable services. */
577
+ readonly acceptsInput?: (input: PersistedJson) => boolean;
578
+ readonly retainedManifests?: ReadonlyArray<{
579
+ readonly digests: DefinitionDigests;
580
+ readonly replay: ReplayContract;
581
+ readonly compatibleTools?: Readonly<Record<string, string>>;
582
+ /** Original declarations prove unchanged semantics and output/completion contracts. */
583
+ readonly compatibleAgent?: string;
584
+ }>;
197
585
  }
198
586
 
199
587
  /** Trusted claim identity for resolving per-Attempt application services. Not model input. */
@@ -291,6 +679,8 @@ export const resolveWorkerBinding = (
291
679
  bindings: ReadonlyArray<ResolvedBinding>,
292
680
  agentId: AgentId,
293
681
  digests: DefinitionDigests,
682
+ requiredTools?: ReadonlyArray<string>,
683
+ input?: PersistedJson,
294
684
  ): Effect.Effect<ResolvedBinding, DurableBindingFailure> => {
295
685
  const registered = bindings.filter((binding) => binding.agentId === agentId);
296
686
 
@@ -304,16 +694,22 @@ export const resolveWorkerBinding = (
304
694
  }
305
695
  const exact = registered.find((binding) => definitionDigestsEqual(binding.digests, digests));
306
696
 
307
- if (exact === undefined) {
697
+ const compatible =
698
+ exact ??
699
+ (registered.length === 1 && bindingSupports(registered[0]!, digests, requiredTools, input)
700
+ ? registered[0]
701
+ : undefined);
702
+
703
+ if (compatible === undefined) {
308
704
  return Effect.fail(
309
705
  BindingDigestMismatch.make({
310
706
  agentId,
311
- message: `The registered Binding for ${agentId} does not match the stored definition digests exactly; missing or different code fails closed (SUB-023)`,
707
+ message: `No registered Binding proves the retained replay contracts for ${agentId}; the original request and receipts remain pending`,
312
708
  }),
313
709
  );
314
710
  }
315
711
 
316
- return Effect.succeed(exact);
712
+ return Effect.succeed(compatible);
317
713
  };
318
714
 
319
715
  /** Resolve authoring-time admission through the one exact host registration, never by identity alone. */
@@ -354,6 +750,11 @@ export type AgentRegistration<A extends ExecutableAgentBinding = ExecutableAgent
354
750
  readonly definitions: DefinitionDigestInput;
355
751
  }
356
752
  ) & {
753
+ /** Opt into contract-checked deployment continuity. Omit to require an exact registration. */
754
+ readonly continuity?: {
755
+ readonly versions: ReplayVersions;
756
+ readonly retainedManifests?: ReadonlyArray<BindingManifest>;
757
+ };
357
758
  /**
358
759
  * Source-owned reports capture services outside attemptLayer and open a fresh Scope per
359
760
  * preparation. Version changes with this registration. Missing exact code and preparation
@@ -483,8 +884,8 @@ const compileRegistration = <Entry extends AgentRegistration>(
483
884
  try: () => registrationDefinitions(entry),
484
885
  catch: () =>
485
886
  DigestError.make({ message: "Agent update schema has no serializable wire contract" }),
486
- }).pipe(Effect.flatMap(digestDefinitions)),
487
- (digests) =>
887
+ }),
888
+ (definitions) =>
488
889
  Effect.gen(function* () {
489
890
  const binding = yield* capture(
490
891
  entry.model === undefined ? entry.agent : { definition: entry.agent, model: entry.model },
@@ -496,7 +897,19 @@ const compileRegistration = <Entry extends AgentRegistration>(
496
897
  ...backgroundReports(binding.definition),
497
898
  ]);
498
899
 
499
- return { ...binding, digests, reporting };
900
+ if (entry.continuity === undefined)
901
+ return { ...binding, digests: yield* digestDefinitions(definitions), reporting };
902
+
903
+ return {
904
+ ...binding,
905
+ ...(yield* compileBindingManifest(
906
+ binding.definition,
907
+ definitions,
908
+ entry.continuity.versions,
909
+ entry.continuity.retainedManifests,
910
+ )),
911
+ reporting,
912
+ };
500
913
  }),
501
914
  // The erased descriptor accepts arbitrary provided services. Restore the concrete entry's
502
915
  // Exclude<worker requirements, provided services> | layer requirements at this collection seam.
@@ -52,6 +52,7 @@ import {
52
52
  } from "../ThreadStore.ts";
53
53
  import {
54
54
  definitionDigestsEqual,
55
+ bindingSupports,
55
56
  resolveDefinitionBinding,
56
57
  type ResolvedBinding,
57
58
  } from "./agent-registration.ts";
@@ -102,11 +103,7 @@ export const makeMessagingRuntime = Effect.fn("MessagingHost.make")(function* (
102
103
 
103
104
  if (created?._tag !== "ThreadCreated") return yield* failure("context", "not-found");
104
105
 
105
- const matching = deps.bindings.filter(
106
- (binding) =>
107
- binding.agentId === created.agentId &&
108
- definitionDigestsEqual(binding.digests, created.definitions),
109
- );
106
+ const matching = deps.bindings.filter((binding) => binding.agentId === created.agentId);
110
107
 
111
108
  if (matching.length !== 1) return yield* failure("context", "binding-mismatch");
112
109
 
@@ -310,7 +307,7 @@ export const makeMessagingRuntime = Effect.fn("MessagingHost.make")(function* (
310
307
  !Schema.is(MessageAdmission)(metadata) ||
311
308
  metadata.peerName !== request.name ||
312
309
  saved.envelope.agentId !== target.agentId ||
313
- !definitionDigestsEqual(saved.envelope.definitions, target.digests) ||
310
+ !bindingSupports(target, saved.envelope.definitions, undefined, saved.envelope.input) ||
314
311
  !sameJson(saved.envelope.input, input) ||
315
312
  (inReplyTo === undefined || metadata.inReplyTo === undefined
316
313
  ? inReplyTo !== metadata.inReplyTo