@nifrajs/coding-agent 3.2.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 (211) hide show
  1. package/EXTENSIONS.md +25 -0
  2. package/PERFORMANCE.md +19 -0
  3. package/README.md +69 -0
  4. package/REGISTRY.md +15 -0
  5. package/SECURITY.md +32 -0
  6. package/dist/approvals.d.ts +70 -0
  7. package/dist/approvals.d.ts.map +1 -0
  8. package/dist/approvals.js +174 -0
  9. package/dist/approvals.js.map +1 -0
  10. package/dist/capabilities.d.ts +20 -0
  11. package/dist/capabilities.d.ts.map +1 -0
  12. package/dist/capabilities.js +49 -0
  13. package/dist/capabilities.js.map +1 -0
  14. package/dist/cli.d.ts +24 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +380 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/context.d.ts +17 -0
  19. package/dist/context.d.ts.map +1 -0
  20. package/dist/context.js +66 -0
  21. package/dist/context.js.map +1 -0
  22. package/dist/deployment-adapters.d.ts +40 -0
  23. package/dist/deployment-adapters.d.ts.map +1 -0
  24. package/dist/deployment-adapters.js +147 -0
  25. package/dist/deployment-adapters.js.map +1 -0
  26. package/dist/diff.d.ts +17 -0
  27. package/dist/diff.d.ts.map +1 -0
  28. package/dist/diff.js +61 -0
  29. package/dist/diff.js.map +1 -0
  30. package/dist/errors.d.ts +9 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +35 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/extensions.d.ts +104 -0
  35. package/dist/extensions.d.ts.map +1 -0
  36. package/dist/extensions.js +284 -0
  37. package/dist/extensions.js.map +1 -0
  38. package/dist/handoffs.d.ts +97 -0
  39. package/dist/handoffs.d.ts.map +1 -0
  40. package/dist/handoffs.js +199 -0
  41. package/dist/handoffs.js.map +1 -0
  42. package/dist/healing.d.ts +56 -0
  43. package/dist/healing.d.ts.map +1 -0
  44. package/dist/healing.js +54 -0
  45. package/dist/healing.js.map +1 -0
  46. package/dist/host.d.ts +61 -0
  47. package/dist/host.d.ts.map +1 -0
  48. package/dist/host.js +369 -0
  49. package/dist/host.js.map +1 -0
  50. package/dist/index.d.ts +26 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +26 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/isolated-worker.d.ts +2 -0
  55. package/dist/isolated-worker.d.ts.map +1 -0
  56. package/dist/isolated-worker.js +155 -0
  57. package/dist/isolated-worker.js.map +1 -0
  58. package/dist/isolated.d.ts +52 -0
  59. package/dist/isolated.d.ts.map +1 -0
  60. package/dist/isolated.js +293 -0
  61. package/dist/isolated.js.map +1 -0
  62. package/dist/native.d.ts +92 -0
  63. package/dist/native.d.ts.map +1 -0
  64. package/dist/native.js +339 -0
  65. package/dist/native.js.map +1 -0
  66. package/dist/orchestration/artifact-port.d.ts +30 -0
  67. package/dist/orchestration/artifact-port.d.ts.map +1 -0
  68. package/dist/orchestration/artifact-port.js +59 -0
  69. package/dist/orchestration/artifact-port.js.map +1 -0
  70. package/dist/orchestration/catalog.d.ts +71 -0
  71. package/dist/orchestration/catalog.d.ts.map +1 -0
  72. package/dist/orchestration/catalog.js +46 -0
  73. package/dist/orchestration/catalog.js.map +1 -0
  74. package/dist/orchestration/compile.d.ts +59 -0
  75. package/dist/orchestration/compile.d.ts.map +1 -0
  76. package/dist/orchestration/compile.js +268 -0
  77. package/dist/orchestration/compile.js.map +1 -0
  78. package/dist/orchestration/dispatch.d.ts +115 -0
  79. package/dist/orchestration/dispatch.d.ts.map +1 -0
  80. package/dist/orchestration/dispatch.js +220 -0
  81. package/dist/orchestration/dispatch.js.map +1 -0
  82. package/dist/orchestration/durable-jobs.d.ts +35 -0
  83. package/dist/orchestration/durable-jobs.d.ts.map +1 -0
  84. package/dist/orchestration/durable-jobs.js +273 -0
  85. package/dist/orchestration/durable-jobs.js.map +1 -0
  86. package/dist/orchestration/effect-key.d.ts +23 -0
  87. package/dist/orchestration/effect-key.d.ts.map +1 -0
  88. package/dist/orchestration/effect-key.js +24 -0
  89. package/dist/orchestration/effect-key.js.map +1 -0
  90. package/dist/orchestration/evidence-store.d.ts +75 -0
  91. package/dist/orchestration/evidence-store.d.ts.map +1 -0
  92. package/dist/orchestration/evidence-store.js +183 -0
  93. package/dist/orchestration/evidence-store.js.map +1 -0
  94. package/dist/orchestration/hash.d.ts +12 -0
  95. package/dist/orchestration/hash.d.ts.map +1 -0
  96. package/dist/orchestration/hash.js +43 -0
  97. package/dist/orchestration/hash.js.map +1 -0
  98. package/dist/orchestration/host.d.ts +121 -0
  99. package/dist/orchestration/host.d.ts.map +1 -0
  100. package/dist/orchestration/host.js +334 -0
  101. package/dist/orchestration/host.js.map +1 -0
  102. package/dist/orchestration/index.d.ts +20 -0
  103. package/dist/orchestration/index.d.ts.map +1 -0
  104. package/dist/orchestration/index.js +19 -0
  105. package/dist/orchestration/index.js.map +1 -0
  106. package/dist/orchestration/policy.d.ts +63 -0
  107. package/dist/orchestration/policy.d.ts.map +1 -0
  108. package/dist/orchestration/policy.js +80 -0
  109. package/dist/orchestration/policy.js.map +1 -0
  110. package/dist/orchestration/recovery.d.ts +87 -0
  111. package/dist/orchestration/recovery.d.ts.map +1 -0
  112. package/dist/orchestration/recovery.js +197 -0
  113. package/dist/orchestration/recovery.js.map +1 -0
  114. package/dist/orchestration/tracer.d.ts +31 -0
  115. package/dist/orchestration/tracer.d.ts.map +1 -0
  116. package/dist/orchestration/tracer.js +141 -0
  117. package/dist/orchestration/tracer.js.map +1 -0
  118. package/dist/plans.d.ts +35 -0
  119. package/dist/plans.d.ts.map +1 -0
  120. package/dist/plans.js +63 -0
  121. package/dist/plans.js.map +1 -0
  122. package/dist/presets.d.ts +10 -0
  123. package/dist/presets.d.ts.map +1 -0
  124. package/dist/presets.js +70 -0
  125. package/dist/presets.js.map +1 -0
  126. package/dist/process.d.ts +6 -0
  127. package/dist/process.d.ts.map +1 -0
  128. package/dist/process.js +42 -0
  129. package/dist/process.js.map +1 -0
  130. package/dist/registry.d.ts +37 -0
  131. package/dist/registry.d.ts.map +1 -0
  132. package/dist/registry.js +51 -0
  133. package/dist/registry.js.map +1 -0
  134. package/dist/replay.d.ts +24 -0
  135. package/dist/replay.d.ts.map +1 -0
  136. package/dist/replay.js +154 -0
  137. package/dist/replay.js.map +1 -0
  138. package/dist/rpc.d.ts +51 -0
  139. package/dist/rpc.d.ts.map +1 -0
  140. package/dist/rpc.js +412 -0
  141. package/dist/rpc.js.map +1 -0
  142. package/dist/session-migration.d.ts +59 -0
  143. package/dist/session-migration.d.ts.map +1 -0
  144. package/dist/session-migration.js +315 -0
  145. package/dist/session-migration.js.map +1 -0
  146. package/dist/sessions.d.ts +74 -0
  147. package/dist/sessions.d.ts.map +1 -0
  148. package/dist/sessions.js +222 -0
  149. package/dist/sessions.js.map +1 -0
  150. package/dist/subagents.d.ts +51 -0
  151. package/dist/subagents.d.ts.map +1 -0
  152. package/dist/subagents.js +141 -0
  153. package/dist/subagents.js.map +1 -0
  154. package/dist/tools.d.ts +13 -0
  155. package/dist/tools.d.ts.map +1 -0
  156. package/dist/tools.js +37 -0
  157. package/dist/tools.js.map +1 -0
  158. package/dist/ui.d.ts +42 -0
  159. package/dist/ui.d.ts.map +1 -0
  160. package/dist/ui.js +93 -0
  161. package/dist/ui.js.map +1 -0
  162. package/dist/verification.d.ts +34 -0
  163. package/dist/verification.d.ts.map +1 -0
  164. package/dist/verification.js +91 -0
  165. package/dist/verification.js.map +1 -0
  166. package/dist/workflows.d.ts +98 -0
  167. package/dist/workflows.d.ts.map +1 -0
  168. package/dist/workflows.js +174 -0
  169. package/dist/workflows.js.map +1 -0
  170. package/package.json +84 -0
  171. package/src/approvals.ts +249 -0
  172. package/src/capabilities.ts +74 -0
  173. package/src/cli.ts +405 -0
  174. package/src/context.ts +81 -0
  175. package/src/deployment-adapters.ts +181 -0
  176. package/src/diff.ts +80 -0
  177. package/src/errors.ts +46 -0
  178. package/src/extensions.ts +402 -0
  179. package/src/handoffs.ts +299 -0
  180. package/src/healing.ts +93 -0
  181. package/src/host.ts +465 -0
  182. package/src/index.ts +165 -0
  183. package/src/isolated-worker.ts +177 -0
  184. package/src/isolated.ts +344 -0
  185. package/src/native.ts +478 -0
  186. package/src/orchestration/artifact-port.ts +75 -0
  187. package/src/orchestration/catalog.ts +102 -0
  188. package/src/orchestration/compile.ts +373 -0
  189. package/src/orchestration/dispatch.ts +383 -0
  190. package/src/orchestration/durable-jobs.ts +374 -0
  191. package/src/orchestration/effect-key.ts +35 -0
  192. package/src/orchestration/evidence-store.ts +233 -0
  193. package/src/orchestration/hash.ts +43 -0
  194. package/src/orchestration/host.ts +445 -0
  195. package/src/orchestration/index.ts +131 -0
  196. package/src/orchestration/policy.ts +116 -0
  197. package/src/orchestration/recovery.ts +271 -0
  198. package/src/orchestration/tracer.ts +177 -0
  199. package/src/plans.ts +105 -0
  200. package/src/presets.ts +90 -0
  201. package/src/process.ts +47 -0
  202. package/src/registry.ts +80 -0
  203. package/src/replay.ts +190 -0
  204. package/src/rpc.ts +598 -0
  205. package/src/session-migration.ts +401 -0
  206. package/src/sessions.ts +303 -0
  207. package/src/subagents.ts +210 -0
  208. package/src/tools.ts +52 -0
  209. package/src/ui.ts +137 -0
  210. package/src/verification.ts +133 -0
  211. package/src/workflows.ts +279 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Evidence-only dispatch contracts for at-least-once run execution.
3
+ *
4
+ * These values deliberately contain no job payload, tenant state, credentials, retention policy,
5
+ * error message, or tool/model data. A durable operated adapter may implement the ports, but the
6
+ * public reference adapters are disposable and keep only identifiers, digests, counters, and
7
+ * lifecycle evidence. Delivery is at-least-once; callers make effects idempotent with the key
8
+ * derived below and prove completion before a recovered worker skips the effect.
9
+ */
10
+
11
+ import { sha256HexOf } from "./hash.ts"
12
+
13
+ export const RUN_DISPATCH_VERSION = 1 as const
14
+ export const DISPATCH_JOB_NAME = "nifra.agent.run-node"
15
+ export const MAX_DISPATCH_STRING = 256
16
+ export const MAX_DISPATCH_ATTEMPTS = 16
17
+
18
+ export type DispatchBoundary = "before-effect" | "after-effect"
19
+ export type DispatchState =
20
+ | "queued"
21
+ | "leased"
22
+ | "checkpointed"
23
+ | "retrying"
24
+ | "succeeded"
25
+ | "failed"
26
+ | "cancelled"
27
+ | "dead-lettered"
28
+
29
+ export type DispatchResultCode =
30
+ | "accepted"
31
+ | "stale_lease"
32
+ | "cancelled"
33
+ | "terminal"
34
+ | "invalid_checkpoint"
35
+ | "idempotency_required"
36
+
37
+ export interface InjectedClock {
38
+ readonly now: () => number
39
+ readonly sleep?: (milliseconds: number) => Promise<void>
40
+ }
41
+
42
+ export const systemClock: InjectedClock = Object.freeze({ now: () => Date.now() })
43
+
44
+ /** A queued node identity. It is safe to persist and safe to show in evidence. */
45
+ export interface RunDispatch {
46
+ readonly version: typeof RUN_DISPATCH_VERSION
47
+ readonly dispatchId: string
48
+ readonly runId: string
49
+ readonly planDigest: string
50
+ readonly nodeId: string
51
+ readonly maxAttempts: number
52
+ readonly notBefore: number
53
+ }
54
+
55
+ /** A lease with a generation. A completion from any older generation is rejected. */
56
+ export interface RunLease {
57
+ readonly version: typeof RUN_DISPATCH_VERSION
58
+ readonly leaseId: string
59
+ readonly dispatch: RunDispatch
60
+ /** Logical attempt is stable across duplicate delivery while the same attempt is unresolved. */
61
+ readonly attempt: number
62
+ readonly generation: number
63
+ readonly leasedAt: number
64
+ readonly leaseUntil: number
65
+ readonly scheduleToken: string
66
+ readonly idempotencyKey: string
67
+ }
68
+
69
+ /** A safe boundary checkpoint. It never contains a result or effect payload. */
70
+ export interface RunCheckpoint {
71
+ readonly version: typeof RUN_DISPATCH_VERSION
72
+ readonly dispatchId: string
73
+ readonly runId: string
74
+ readonly nodeId: string
75
+ readonly attempt: number
76
+ readonly generation: number
77
+ readonly boundary: DispatchBoundary
78
+ readonly idempotencyKey: string
79
+ readonly at: number
80
+ readonly scheduleToken: string
81
+ }
82
+
83
+ /** A terminal or transitional record suitable for a public evidence sink. */
84
+ export interface RunDispatchEvidence {
85
+ readonly version: typeof RUN_DISPATCH_VERSION
86
+ readonly dispatchId: string
87
+ readonly runId: string
88
+ readonly nodeId: string
89
+ readonly state: DispatchState
90
+ readonly attempt: number
91
+ readonly generation: number
92
+ readonly at: number
93
+ readonly scheduleToken: string
94
+ readonly idempotencyKey: string
95
+ readonly code?: string
96
+ }
97
+
98
+ export interface DispatchWriteResult {
99
+ readonly ok: boolean
100
+ readonly code: DispatchResultCode
101
+ }
102
+
103
+ export interface DispatchInspection {
104
+ readonly dispatch: RunDispatch
105
+ readonly state: DispatchState
106
+ readonly attempt: number
107
+ readonly generation: number
108
+ readonly checkpoint?: RunCheckpoint
109
+ readonly evidence: readonly RunDispatchEvidence[]
110
+ }
111
+
112
+ export interface RunDispatchStore {
113
+ enqueue(dispatch: RunDispatch): void | Promise<void>
114
+ lease(now: number, limit: number, leaseMs: number): RunLease[] | Promise<RunLease[]>
115
+ checkpoint(
116
+ lease: RunLease,
117
+ checkpoint: RunCheckpoint,
118
+ ): DispatchWriteResult | Promise<DispatchWriteResult>
119
+ complete(
120
+ lease: RunLease,
121
+ evidence?: RunDispatchEvidence,
122
+ ): DispatchWriteResult | Promise<DispatchWriteResult>
123
+ retry(
124
+ lease: RunLease,
125
+ runAt: number,
126
+ code: string,
127
+ ): DispatchWriteResult | Promise<DispatchWriteResult>
128
+ deadLetter(lease: RunLease, code: string): DispatchWriteResult | Promise<DispatchWriteResult>
129
+ cancel(runId: string, code?: string): number | Promise<number>
130
+ inspect(
131
+ dispatchId: string,
132
+ ): DispatchInspection | undefined | Promise<DispatchInspection | undefined>
133
+ }
134
+
135
+ /**
136
+ * Optional operated-depth handoff. The context is intentionally opaque: the public package does not
137
+ * model tenants, rows, RLS policy text, retention periods, credentials, workers, or fleet topology.
138
+ * A private adapter supplies those controls and still implements the evidence-only store port above.
139
+ */
140
+ export interface DurableDispatchAdapter<OpaqueContext = unknown> extends RunDispatchStore {
141
+ authorize(
142
+ context: OpaqueContext,
143
+ operation: "enqueue" | "lease" | "inspect" | "settle",
144
+ ): boolean | Promise<boolean>
145
+ enforceDataLayerPolicy(
146
+ context: OpaqueContext,
147
+ operation: "read" | "write",
148
+ ): boolean | Promise<boolean>
149
+ retainEvidence(context: OpaqueContext, evidence: RunDispatchEvidence): void | Promise<void>
150
+ reconcile(context: OpaqueContext, dispatchId: string): void | Promise<void>
151
+ workerHealth(
152
+ context: OpaqueContext,
153
+ ): "ready" | "draining" | "unavailable" | Promise<"ready" | "draining" | "unavailable">
154
+ }
155
+
156
+ const DIGEST = /^[0-9a-f]{64}$/
157
+ const TOKEN = /^[A-Za-z0-9._:-]{1,256}$/
158
+ const FORBIDDEN = new Set([
159
+ "prompt",
160
+ "message",
161
+ "text",
162
+ "input",
163
+ "output",
164
+ "arguments",
165
+ "body",
166
+ "response",
167
+ "secret",
168
+ "credential",
169
+ "diagnostic",
170
+ "stack",
171
+ "content",
172
+ "transcript",
173
+ "artifact",
174
+ ])
175
+
176
+ function record(value: unknown, where: string): Record<string, unknown> {
177
+ if (typeof value !== "object" || value === null || Array.isArray(value))
178
+ throw new TypeError(`${where} must be an object`)
179
+ return value as Record<string, unknown>
180
+ }
181
+
182
+ function rejectKeys(
183
+ value: Record<string, unknown>,
184
+ allowed: ReadonlySet<string>,
185
+ where: string,
186
+ ): void {
187
+ for (const key of Object.keys(value)) {
188
+ if (FORBIDDEN.has(key.toLowerCase()))
189
+ throw new TypeError(`${where} contains forbidden field '${key}'`)
190
+ if (!allowed.has(key)) throw new TypeError(`${where} contains unknown field '${key}'`)
191
+ }
192
+ }
193
+
194
+ function boundedString(value: unknown, name: string): string {
195
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_DISPATCH_STRING)
196
+ throw new TypeError(`${name} must be a non-empty bounded string`)
197
+ return value
198
+ }
199
+
200
+ function digest(value: unknown, name: string): string {
201
+ const text = boundedString(value, name)
202
+ if (!DIGEST.test(text)) throw new TypeError(`${name} must be a lowercase sha256 digest`)
203
+ return text
204
+ }
205
+
206
+ function token(value: unknown, name: string): string {
207
+ const text = boundedString(value, name)
208
+ if (!TOKEN.test(text)) throw new TypeError(`${name} must be an opaque schedule token`)
209
+ return text
210
+ }
211
+
212
+ function nonNegative(value: unknown, name: string): number {
213
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
214
+ throw new TypeError(`${name} must be a non-negative safe integer`)
215
+ return value
216
+ }
217
+
218
+ function attempts(value: unknown): number {
219
+ const result = nonNegative(value, "maxAttempts")
220
+ if (result < 1 || result > MAX_DISPATCH_ATTEMPTS)
221
+ throw new RangeError(`maxAttempts must be between 1 and ${MAX_DISPATCH_ATTEMPTS}`)
222
+ return result
223
+ }
224
+
225
+ const DISPATCH_KEYS = new Set([
226
+ "version",
227
+ "dispatchId",
228
+ "runId",
229
+ "planDigest",
230
+ "nodeId",
231
+ "maxAttempts",
232
+ "notBefore",
233
+ ])
234
+
235
+ /** Parse a dispatch identity and reject content or undeclared fields. */
236
+ export function parseRunDispatch(value: unknown): RunDispatch {
237
+ const input = record(value, "run dispatch")
238
+ rejectKeys(input, DISPATCH_KEYS, "run dispatch")
239
+ if (input.version !== RUN_DISPATCH_VERSION)
240
+ throw new TypeError("run dispatch.version is unsupported")
241
+ return Object.freeze({
242
+ version: RUN_DISPATCH_VERSION,
243
+ dispatchId: token(input.dispatchId, "dispatchId"),
244
+ runId: token(input.runId, "runId"),
245
+ planDigest: digest(input.planDigest, "planDigest"),
246
+ nodeId: token(input.nodeId, "nodeId"),
247
+ maxAttempts: attempts(input.maxAttempts),
248
+ notBefore: nonNegative(input.notBefore, "notBefore"),
249
+ })
250
+ }
251
+
252
+ const LEASE_KEYS = new Set([
253
+ "version",
254
+ "leaseId",
255
+ "dispatch",
256
+ "attempt",
257
+ "generation",
258
+ "leasedAt",
259
+ "leaseUntil",
260
+ "scheduleToken",
261
+ "idempotencyKey",
262
+ ])
263
+
264
+ export function parseRunLease(value: unknown): RunLease {
265
+ const input = record(value, "run lease")
266
+ rejectKeys(input, LEASE_KEYS, "run lease")
267
+ if (input.version !== RUN_DISPATCH_VERSION)
268
+ throw new TypeError("run lease.version is unsupported")
269
+ const attempt = attempts(input.attempt)
270
+ const generation = nonNegative(input.generation, "generation")
271
+ return Object.freeze({
272
+ version: RUN_DISPATCH_VERSION,
273
+ leaseId: token(input.leaseId, "leaseId"),
274
+ dispatch: parseRunDispatch(input.dispatch),
275
+ attempt,
276
+ generation,
277
+ leasedAt: nonNegative(input.leasedAt, "leasedAt"),
278
+ leaseUntil: nonNegative(input.leaseUntil, "leaseUntil"),
279
+ scheduleToken: token(input.scheduleToken, "scheduleToken"),
280
+ idempotencyKey: token(input.idempotencyKey, "idempotencyKey"),
281
+ })
282
+ }
283
+
284
+ const CHECKPOINT_KEYS = new Set([
285
+ "version",
286
+ "dispatchId",
287
+ "runId",
288
+ "nodeId",
289
+ "attempt",
290
+ "generation",
291
+ "boundary",
292
+ "idempotencyKey",
293
+ "at",
294
+ "scheduleToken",
295
+ ])
296
+
297
+ export function parseRunCheckpoint(value: unknown): RunCheckpoint {
298
+ const input = record(value, "run checkpoint")
299
+ rejectKeys(input, CHECKPOINT_KEYS, "run checkpoint")
300
+ if (input.version !== RUN_DISPATCH_VERSION)
301
+ throw new TypeError("run checkpoint.version is unsupported")
302
+ const boundary = input.boundary
303
+ if (boundary !== "before-effect" && boundary !== "after-effect")
304
+ throw new TypeError("run checkpoint.boundary is invalid")
305
+ return Object.freeze({
306
+ version: RUN_DISPATCH_VERSION,
307
+ dispatchId: token(input.dispatchId, "dispatchId"),
308
+ runId: token(input.runId, "runId"),
309
+ nodeId: token(input.nodeId, "nodeId"),
310
+ attempt: attempts(input.attempt),
311
+ generation: nonNegative(input.generation, "generation"),
312
+ boundary,
313
+ idempotencyKey: token(input.idempotencyKey, "idempotencyKey"),
314
+ at: nonNegative(input.at, "at"),
315
+ scheduleToken: token(input.scheduleToken, "scheduleToken"),
316
+ })
317
+ }
318
+
319
+ const EVIDENCE_KEYS = new Set([
320
+ "version",
321
+ "dispatchId",
322
+ "runId",
323
+ "nodeId",
324
+ "state",
325
+ "attempt",
326
+ "generation",
327
+ "at",
328
+ "scheduleToken",
329
+ "idempotencyKey",
330
+ "code",
331
+ ])
332
+
333
+ const STATES: ReadonlySet<string> = new Set([
334
+ "queued",
335
+ "leased",
336
+ "checkpointed",
337
+ "retrying",
338
+ "succeeded",
339
+ "failed",
340
+ "cancelled",
341
+ "dead-lettered",
342
+ ])
343
+
344
+ export function parseRunDispatchEvidence(value: unknown): RunDispatchEvidence {
345
+ const input = record(value, "run dispatch evidence")
346
+ rejectKeys(input, EVIDENCE_KEYS, "run dispatch evidence")
347
+ if (input.version !== RUN_DISPATCH_VERSION)
348
+ throw new TypeError("run dispatch evidence.version is unsupported")
349
+ if (typeof input.state !== "string" || !STATES.has(input.state))
350
+ throw new TypeError("run dispatch evidence.state is invalid")
351
+ return Object.freeze({
352
+ version: RUN_DISPATCH_VERSION,
353
+ dispatchId: token(input.dispatchId, "dispatchId"),
354
+ runId: token(input.runId, "runId"),
355
+ nodeId: token(input.nodeId, "nodeId"),
356
+ state: input.state as DispatchState,
357
+ attempt: attempts(input.attempt),
358
+ generation: nonNegative(input.generation, "generation"),
359
+ at: nonNegative(input.at, "at"),
360
+ scheduleToken: token(input.scheduleToken, "scheduleToken"),
361
+ idempotencyKey: token(input.idempotencyKey, "idempotencyKey"),
362
+ ...(input.code === undefined ? {} : { code: token(input.code, "code") }),
363
+ })
364
+ }
365
+
366
+ /** Stable key for one logical node attempt. Duplicate delivery uses the same boundary number. */
367
+ export function deriveRunIdempotencyKey(
368
+ planDigest: string,
369
+ runId: string,
370
+ nodeId: string,
371
+ logicalAttemptBoundary: number,
372
+ ): Promise<string> {
373
+ const plan = digest(planDigest, "planDigest")
374
+ const run = token(runId, "runId")
375
+ const node = token(nodeId, "nodeId")
376
+ const attempt = attempts(logicalAttemptBoundary)
377
+ return sha256HexOf(`${plan}\u0000${run}\u0000${node}\u0000${attempt}`)
378
+ }
379
+
380
+ /** Create a parsed dispatch identity before handing it to any adapter. */
381
+ export function createRunDispatch(input: Omit<RunDispatch, "version">): RunDispatch {
382
+ return parseRunDispatch({ version: RUN_DISPATCH_VERSION, ...input })
383
+ }