@humain/humain-code-contracts 0.3.2

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 (51) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +92 -0
  3. package/dist/index.d.ts +224 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +218 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/orchestration.d.ts +327 -0
  8. package/dist/orchestration.d.ts.map +1 -0
  9. package/dist/orchestration.js +307 -0
  10. package/dist/orchestration.js.map +1 -0
  11. package/dist/rpc.d.ts +63 -0
  12. package/dist/rpc.d.ts.map +1 -0
  13. package/dist/rpc.js +147 -0
  14. package/dist/rpc.js.map +1 -0
  15. package/dist/subagent-progress.d.ts +106 -0
  16. package/dist/subagent-progress.d.ts.map +1 -0
  17. package/dist/subagent-progress.js +440 -0
  18. package/dist/subagent-progress.js.map +1 -0
  19. package/dist/testing.d.ts +390 -0
  20. package/dist/testing.d.ts.map +1 -0
  21. package/dist/testing.js +431 -0
  22. package/dist/testing.js.map +1 -0
  23. package/dist/trace-api.d.ts +293 -0
  24. package/dist/trace-api.d.ts.map +1 -0
  25. package/dist/trace-api.js +202 -0
  26. package/dist/trace-api.js.map +1 -0
  27. package/dist/trace-artifact.d.ts +94 -0
  28. package/dist/trace-artifact.d.ts.map +1 -0
  29. package/dist/trace-artifact.js +168 -0
  30. package/dist/trace-artifact.js.map +1 -0
  31. package/dist/trace-canonical.d.ts +23 -0
  32. package/dist/trace-canonical.d.ts.map +1 -0
  33. package/dist/trace-canonical.js +240 -0
  34. package/dist/trace-canonical.js.map +1 -0
  35. package/dist/trace-export.d.ts +94 -0
  36. package/dist/trace-export.d.ts.map +1 -0
  37. package/dist/trace-export.js +374 -0
  38. package/dist/trace-export.js.map +1 -0
  39. package/dist/trace-http.d.ts +1294 -0
  40. package/dist/trace-http.d.ts.map +1 -0
  41. package/dist/trace-http.js +1481 -0
  42. package/dist/trace-http.js.map +1 -0
  43. package/dist/trace-payload.d.ts +425 -0
  44. package/dist/trace-payload.d.ts.map +1 -0
  45. package/dist/trace-payload.js +1519 -0
  46. package/dist/trace-payload.js.map +1 -0
  47. package/dist/trace.d.ts +257 -0
  48. package/dist/trace.d.ts.map +1 -0
  49. package/dist/trace.js +1014 -0
  50. package/dist/trace.js.map +1 -0
  51. package/package.json +64 -0
@@ -0,0 +1,1519 @@
1
+ import { Type } from "typebox";
2
+ import { DH_COVERAGE_REASONS, DH_COVERAGE_STATES, DH_EVENT_KINDS, DH_OUTCOMES, DhAgentIdSchema, DhArtifactIdSchema, DhAttemptIdSchema, DhEventIdSchema, resolveDhEffectiveTerminals, } from "./trace.js";
3
+ import { DhContractError } from "./trace-canonical.js";
4
+ import { DH_CONTROL_ACTIONS, DH_CONTROL_REJECT_REASONS, DH_CONTROL_STATES, dhStrictDtoParser } from "./trace-http.js";
5
+ /*
6
+ * C0 typed payload contracts. `payload.schema` carries a `dh.payload.<name>/1` id; the registry maps
7
+ * (event kind, payload schema id) to a strict parser. The event envelope stays `dh.event/1`: payloads
8
+ * remain JSON objects bounded by assertJsonPayload, and these parsers only tighten them.
9
+ *
10
+ * Rules
11
+ * - Event kinds that existed before this freeze (DH_LEGACY_FREEFORM_PAYLOAD_KINDS) may still carry a
12
+ * legacy freeform payload WITHOUT `schema`, so current CLI/Flow producer output stays valid. If such a
13
+ * payload has `schema`, it must be a registered id for that kind (no typo can masquerade as legacy).
14
+ * - Kinds added by this freeze (DH_PAYLOAD_REQUIRED_KINDS) have no legacy shape: `schema` is required.
15
+ * - Field names already emitted by the CLI (callId, toolName, stopReason, nonTextBlockCount, childAgentId,
16
+ * spawnedByEventId, nextAttemptNumber, delayMs, ...) keep their spelling.
17
+ * - Coverage and loss stay in the envelope `fields`; payload schemas never duplicate them, they only
18
+ * require that an absent value has explicit loss coverage.
19
+ * - Raw file paths never appear in typed payloads: a file is referenced by artifact.
20
+ * - This module is loaded through the trace.ts <-> trace-http.ts import cycle: NOTHING here reads an
21
+ * imported value at module top level. Schemas and parsers are built lazily on first use.
22
+ */
23
+ export const DH_PAYLOAD_SCHEMA_IDS = Object.freeze({
24
+ agentRelation: "dh.payload.agent-relation/1",
25
+ agentSpawn: "dh.payload.agent-spawn/1",
26
+ amendment: "dh.payload.amendment/1",
27
+ attemptLifecycle: "dh.payload.attempt-lifecycle/1",
28
+ captureHealth: "dh.payload.capture-health/1",
29
+ continuationDecision: "dh.payload.continuation-decision/1",
30
+ controlCorrelation: "dh.payload.control-correlation/1",
31
+ fileEvidence: "dh.payload.file-evidence/1",
32
+ flowDecision: "dh.payload.flow-decision/1",
33
+ localCancellation: "dh.payload.local-cancellation/1",
34
+ modelResponse: "dh.payload.model-response/1",
35
+ modelSubmission: "dh.payload.model-submission/1",
36
+ toolCall: "dh.payload.tool-call/1",
37
+ toolResult: "dh.payload.tool-result/1",
38
+ usage: "dh.payload.usage/1",
39
+ });
40
+ /** Kinds that existed before typed payloads. They may keep a freeform payload without `schema`. */
41
+ export const DH_LEGACY_FREEFORM_PAYLOAD_KINDS = Object.freeze([
42
+ "generation.started",
43
+ "generation.finished",
44
+ "agent.spawned",
45
+ "attempt.started",
46
+ "attempt.finished",
47
+ "attempt.retry_scheduled",
48
+ "model.request",
49
+ "model.response",
50
+ "tool.call",
51
+ "tool.result",
52
+ "file.change",
53
+ "orchestration.decision",
54
+ "cancellation.requested",
55
+ "cancellation.outcome",
56
+ "usage.reported",
57
+ ]);
58
+ /** Kinds introduced with typed payloads; `payload.schema` is mandatory. */
59
+ export const DH_PAYLOAD_REQUIRED_KINDS = Object.freeze([
60
+ "capture.health",
61
+ "control.correlation",
62
+ "event.amendment",
63
+ ]);
64
+ /**
65
+ * Continuation decisions recorded on `orchestration.decision` (dh.payload.continuation-decision/1).
66
+ * `consumed` is the only consumption event type: a control `continue` is `applied` only when the addressed
67
+ * target recorded `continuation.consumed` for that request (admitted, pending and rejected are not consumption).
68
+ */
69
+ export const DH_CONTINUATION_DECISIONS = [
70
+ "continuation.pending",
71
+ "continuation.admitted",
72
+ "continuation.rejected",
73
+ "continuation.consumed",
74
+ ];
75
+ /** Delivery channel of a continuation. `queue-steering` is never a live continuation of a running child. */
76
+ export const DH_CONTINUATION_CHANNELS = [
77
+ "live-child",
78
+ "native-descendant",
79
+ "run-root",
80
+ "queue-steering",
81
+ "internal",
82
+ ];
83
+ /** Named observations from the existing local process-cancellation seam; never arbitrary diagnostic text. */
84
+ export const DH_LOCAL_CANCELLATION_DIAGNOSTICS = [
85
+ "cancelled_before_spawn",
86
+ "logical_cancellation_observed",
87
+ "process_closed_after_signal",
88
+ ];
89
+ const IDS = DH_PAYLOAD_SCHEMA_IDS;
90
+ /** Which event kinds each payload schema id may appear on. */
91
+ const REGISTRATIONS = [
92
+ { id: IDS.modelSubmission, kinds: ["model.request"] },
93
+ { id: IDS.modelResponse, kinds: ["model.response"] },
94
+ { id: IDS.toolCall, kinds: ["tool.call"] },
95
+ { id: IDS.toolResult, kinds: ["tool.result"] },
96
+ { id: IDS.fileEvidence, kinds: ["file.change"] },
97
+ { id: IDS.agentSpawn, kinds: ["agent.spawned"] },
98
+ { id: IDS.agentRelation, kinds: ["agent.spawned"] },
99
+ { id: IDS.attemptLifecycle, kinds: ["attempt.started", "attempt.finished", "attempt.retry_scheduled"] },
100
+ { id: IDS.flowDecision, kinds: ["orchestration.decision"] },
101
+ { id: IDS.continuationDecision, kinds: ["orchestration.decision"] },
102
+ { id: IDS.usage, kinds: ["usage.reported"] },
103
+ { id: IDS.captureHealth, kinds: ["capture.health"] },
104
+ { id: IDS.controlCorrelation, kinds: ["control.correlation", "cancellation.requested", "cancellation.outcome"] },
105
+ { id: IDS.localCancellation, kinds: ["cancellation.requested", "cancellation.outcome"] },
106
+ { id: IDS.amendment, kinds: ["event.amendment"] },
107
+ ];
108
+ /** Payload schema ids accepted on a kind (empty for kinds that only carry legacy freeform payloads). */
109
+ export function dhPayloadSchemaIdsForKind(kind) {
110
+ return REGISTRATIONS.filter((entry) => entry.kinds.includes(kind)).map((entry) => entry.id);
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // Shared schema pieces (only `typebox` values at top level; everything else is lazy)
114
+ // ---------------------------------------------------------------------------
115
+ const strict = (properties) => Type.Object(properties, { additionalProperties: false });
116
+ const count = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
117
+ const positive = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
118
+ const digest = Type.String({ pattern: "^[a-f0-9]{64}$" });
119
+ const text = Type.String({ minLength: 1, maxLength: 256 });
120
+ /** Opaque producer-side identifier (request, submission, call, logical task). */
121
+ const ident = Type.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$" });
122
+ const code = Type.String({ pattern: "^[a-z][a-z0-9_.-]{0,63}$", maxLength: 64 });
123
+ const revision = Type.String({ pattern: "^[a-f0-9]{40}(?:[a-f0-9]{24})?$" });
124
+ const requestId = Type.String({ pattern: "^ctl_[A-Za-z0-9_-]{8,128}$", maxLength: 132 });
125
+ const FIELD_PATH = "^[A-Za-z][A-Za-z0-9_-]*(?:\\.[A-Za-z0-9_-]+)*$";
126
+ const FORBIDDEN_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
127
+ const LOSS_STATES = new Set(["unavailable", "redacted", "truncated"]);
128
+ const MAX_LIST = 64;
129
+ const artifactRef = () => strict({ artifactId: DhArtifactIdSchema, sha256: digest, sizeBytes: Type.Optional(count) });
130
+ const coverageGap = () => strict({
131
+ path: Type.String({ minLength: 1, maxLength: 256, pattern: FIELD_PATH }),
132
+ state: Type.Enum(DH_COVERAGE_STATES),
133
+ reason: Type.Optional(Type.Enum(DH_COVERAGE_REASONS)),
134
+ });
135
+ const usageOrigin = () => Type.Union([
136
+ strict({ owner: Type.Literal("submission"), submissionId: ident }),
137
+ strict({ owner: Type.Literal("attempt"), attemptId: DhAttemptIdSchema }),
138
+ strict({ owner: Type.Literal("flow-aggregate"), inclusive: Type.Literal(true) }),
139
+ ]);
140
+ /** Unknown is explicit (`state: unknown`); an absent count is unknown, never zero; price is known-with-provenance or unknown. */
141
+ const measurement = () => Type.Union([
142
+ strict({ state: Type.Literal("unknown") }),
143
+ strict({
144
+ state: Type.Literal("reported"),
145
+ input: Type.Optional(count),
146
+ output: Type.Optional(count),
147
+ cacheRead: Type.Optional(count),
148
+ cacheWrite: Type.Optional(count),
149
+ reasoning: Type.Optional(count),
150
+ totalTokens: Type.Optional(count),
151
+ durationMs: Type.Optional(count),
152
+ cost: Type.Union([
153
+ strict({ state: Type.Literal("unknown") }),
154
+ strict({
155
+ state: Type.Literal("known"),
156
+ amount: Type.Number({ minimum: 0 }),
157
+ currency: Type.String({ pattern: "^[A-Z]{3}$" }),
158
+ priceProvenance: text,
159
+ }),
160
+ ]),
161
+ }),
162
+ ]);
163
+ const snapshot = () => Type.Union([
164
+ strict({ state: Type.Literal("observed"), sha256: digest, sizeBytes: count }),
165
+ strict({
166
+ state: Type.Literal("unavailable"),
167
+ reason: Type.Enum(["missing", "denied", "oversized", "binary", "unsupported", "read-failed"]),
168
+ }),
169
+ ]);
170
+ // ---------------------------------------------------------------------------
171
+ // Check helpers
172
+ // ---------------------------------------------------------------------------
173
+ function bad(id, reason) {
174
+ throw new DhContractError("schema_invalid", `Invalid ${id}: ${reason}`);
175
+ }
176
+ function listed(event, artifactId) {
177
+ return (event.artifacts ?? []).includes(artifactId);
178
+ }
179
+ function assertListed(id, event, artifactId) {
180
+ if (!listed(event, artifactId))
181
+ bad(id, "artifact is not listed in event artifacts");
182
+ }
183
+ function assertLossCovered(id, event, path) {
184
+ const coverage = event.fields[path];
185
+ if (coverage === undefined || !LOSS_STATES.has(coverage.state)) {
186
+ bad(id, "absent field requires unavailable, redacted or truncated coverage");
187
+ }
188
+ }
189
+ function assertEventAttempt(id, event, attemptId) {
190
+ if (event.attemptId === undefined)
191
+ bad(id, "attemptId is required on the event");
192
+ if (event.attemptId !== attemptId)
193
+ bad(id, "attemptId does not match the event attemptId");
194
+ }
195
+ function assertGaps(id, gaps) {
196
+ for (const gap of gaps) {
197
+ if (gap.state === "captured" || gap.reason === undefined) {
198
+ bad(id, "coverage entries must describe a loss state and give a reason");
199
+ }
200
+ if (gap.path.split(".").some((segment) => FORBIDDEN_SEGMENTS.has(segment))) {
201
+ bad(id, "coverage path is not a valid field path");
202
+ }
203
+ }
204
+ }
205
+ function assertMeasurement(id, value) {
206
+ if (value.state !== "reported")
207
+ return;
208
+ const tokens = [value.input, value.output, value.cacheRead, value.cacheWrite, value.reasoning, value.totalTokens];
209
+ if (tokens.every((token) => token === undefined) && value.cost.state !== "known") {
210
+ bad(id, "measurement reports no token count and no known cost; use state unknown");
211
+ }
212
+ }
213
+ /**
214
+ * `semantic` sees the payload only; `envelope` also sees the event envelope. Both run after the shared
215
+ * strict DTO parser (plain-JSON wire gate, schema check, clone, re-check).
216
+ */
217
+ function define(id, schema, semantic, envelope) {
218
+ const parse = dhStrictDtoParser(id, schema, semantic);
219
+ return {
220
+ check: (event) => {
221
+ const payload = parse(event.payload);
222
+ envelope?.(payload, event);
223
+ },
224
+ };
225
+ }
226
+ // -- model submission / response ---------------------------------------------------------------
227
+ function modelSubmissionSchema() {
228
+ return strict({
229
+ schema: Type.Literal(IDS.modelSubmission),
230
+ requestId: ident,
231
+ submissionId: ident,
232
+ attemptId: DhAttemptIdSchema,
233
+ transportRetryIndex: Type.Integer({ minimum: 0, maximum: 1000 }),
234
+ provider: text,
235
+ api: text,
236
+ model: text,
237
+ operation: Type.Enum(["chat", "image", "classifier", "auxiliary"]),
238
+ purpose: Type.Optional(Type.Enum(["chat", "compaction", "branch-summary", "cache-warming"])),
239
+ transport: Type.Enum(["http", "websocket"]),
240
+ representation: Type.Enum(["json-body", "json-frame"]),
241
+ adapterVersion: Type.Optional(text),
242
+ requestArtifact: Type.Optional(artifactRef()),
243
+ });
244
+ }
245
+ function modelResponseSchema() {
246
+ return strict({
247
+ schema: Type.Literal(IDS.modelResponse),
248
+ submissionId: ident,
249
+ attemptId: DhAttemptIdSchema,
250
+ stopReason: Type.Enum(["pending", "stop", "length", "toolUse", "error", "aborted", "deferred"]),
251
+ provider: Type.Optional(text),
252
+ api: Type.Optional(text),
253
+ model: Type.Optional(text),
254
+ responseModel: Type.Optional(text),
255
+ toolCallCount: Type.Optional(count),
256
+ text: Type.Optional(Type.String({ maxLength: 65536 })),
257
+ reasoning: Type.Optional(Type.String({ maxLength: 65536 })),
258
+ errorMessage: Type.Optional(Type.String({ maxLength: 4096 })),
259
+ responseArtifact: Type.Optional(artifactRef()),
260
+ usage: Type.Optional(strict({ origin: usageOrigin(), measurement: measurement() })),
261
+ });
262
+ }
263
+ // -- tool calls --------------------------------------------------------------------------------
264
+ /**
265
+ * The typed call carries no free `arguments` object: the shared DTO parser scans every nested key for
266
+ * instant-named fields (`startedAt`, `occurredAt`, ...), so arbitrary tool arguments would be rejected on a
267
+ * name collision. Arguments travel as a bounded redacted JSON-text preview and/or a complete artifact; a
268
+ * truncated preview is marked in the envelope `fields` at `payload.argumentsPreview`.
269
+ */
270
+ function toolCallSchema() {
271
+ return strict({
272
+ schema: Type.Literal(IDS.toolCall),
273
+ callId: ident,
274
+ toolName: text,
275
+ argumentsPreview: Type.Optional(Type.String({ maxLength: 4096 })),
276
+ argumentsArtifact: Type.Optional(artifactRef()),
277
+ /** Provenance of a nested call: the enclosing call's id and (when recorded) its tool.call event. */
278
+ parentCallId: Type.Optional(ident),
279
+ parentEventId: Type.Optional(DhEventIdSchema),
280
+ });
281
+ }
282
+ // -- tool results ------------------------------------------------------------------------------
283
+ function toolResultSchema() {
284
+ return strict({
285
+ schema: Type.Literal(IDS.toolResult),
286
+ callId: ident,
287
+ toolName: text,
288
+ stage: Type.Enum(["original", "final"]),
289
+ source: Type.Enum(["executed", "blocked", "synthetic"]),
290
+ isError: Type.Boolean(),
291
+ modified: Type.Optional(Type.Boolean()),
292
+ originalResultEventId: Type.Optional(DhEventIdSchema),
293
+ parentCallId: Type.Optional(ident),
294
+ output: Type.Optional(Type.String({ maxLength: 65536 })),
295
+ outputArtifact: Type.Optional(artifactRef()),
296
+ nonTextBlockCount: count,
297
+ nonTextArtifacts: Type.Optional(Type.Array(artifactRef(), { maxItems: MAX_LIST })),
298
+ childAgentId: Type.Optional(DhAgentIdSchema),
299
+ childAgentIds: Type.Optional(Type.Array(DhAgentIdSchema, { maxItems: MAX_LIST, uniqueItems: true })),
300
+ });
301
+ }
302
+ // -- file evidence -----------------------------------------------------------------------------
303
+ function fileEvidenceSchema() {
304
+ return strict({
305
+ schema: Type.Literal(IDS.fileEvidence),
306
+ callId: ident,
307
+ toolName: text,
308
+ operation: Type.Enum(["write", "edit", "delete"]),
309
+ outcome: Type.Enum(["succeeded", "failed", "cancelled"]),
310
+ intent: strict({
311
+ contentSha256: Type.Optional(digest),
312
+ contentBytes: Type.Optional(count),
313
+ patchSha256: Type.Optional(digest),
314
+ }),
315
+ observed: strict({ before: snapshot(), after: snapshot(), effectObserved: Type.Boolean() }),
316
+ pathRef: artifactRef(),
317
+ evidence: Type.Optional(Type.Array(strict({
318
+ artifactId: DhArtifactIdSchema,
319
+ sha256: digest,
320
+ sizeBytes: Type.Optional(count),
321
+ role: Type.Enum(["diff", "snapshot"]),
322
+ }), { maxItems: 8 })),
323
+ worktree: Type.Optional(strict({
324
+ mode: Type.Enum(["shared", "isolated", "degraded-to-shared"]),
325
+ baseRevision: Type.Optional(revision),
326
+ endRevision: Type.Optional(revision),
327
+ baseObserved: Type.Boolean(),
328
+ })),
329
+ });
330
+ }
331
+ // -- lifecycle ---------------------------------------------------------------------------------
332
+ function agentSpawnSchema() {
333
+ return strict({
334
+ schema: Type.Literal(IDS.agentSpawn),
335
+ via: Type.Enum(["subagent", "flow"]),
336
+ spawnedByEventId: Type.Optional(DhEventIdSchema),
337
+ launchedByToolCallEventId: Type.Optional(DhEventIdSchema),
338
+ command: Type.Optional(strict({ name: text, decisionEventId: Type.Optional(DhEventIdSchema) })),
339
+ logicalTaskId: Type.Optional(ident),
340
+ nativeLaunchId: Type.Optional(ident),
341
+ });
342
+ }
343
+ /** Cross-agent relation (native Code retry): a NEW agent linked to its source, never a same-agent retryOfAttemptId. */
344
+ function agentRelationSchema() {
345
+ return strict({
346
+ schema: Type.Literal(IDS.agentRelation),
347
+ via: Type.Literal("native-code"),
348
+ relation: Type.Literal("native-retry"),
349
+ sourceAgentId: DhAgentIdSchema,
350
+ sourceAttemptId: DhAttemptIdSchema,
351
+ reasonCode: Type.Optional(code),
352
+ });
353
+ }
354
+ const PHASE_KIND = {
355
+ started: "attempt.started",
356
+ finished: "attempt.finished",
357
+ "retry-scheduled": "attempt.retry_scheduled",
358
+ };
359
+ function attemptLifecycleSchema() {
360
+ return strict({
361
+ schema: Type.Literal(IDS.attemptLifecycle),
362
+ phase: Type.Enum(["started", "finished", "retry-scheduled"]),
363
+ agentId: DhAgentIdSchema,
364
+ attemptId: DhAttemptIdSchema,
365
+ attemptNumber: Type.Optional(positive),
366
+ retryOfAttemptId: Type.Optional(DhAttemptIdSchema),
367
+ retryReason: Type.Optional(code),
368
+ nextAttemptNumber: Type.Optional(positive),
369
+ delayMs: Type.Optional(count),
370
+ spawnOutcome: Type.Optional(Type.Enum(["spawned", "failed-to-spawn", "unknown"])),
371
+ terminationConfirmed: Type.Optional(Type.Boolean()),
372
+ });
373
+ }
374
+ // -- flow decisions ----------------------------------------------------------------------------
375
+ function flowDecisionSchema() {
376
+ return strict({
377
+ schema: Type.Literal(IDS.flowDecision),
378
+ decision: Type.Enum(["schedule", "delegate", "dependency-not-launched", "dependency-released", "retry"]),
379
+ issuer: Type.Literal("flow"),
380
+ via: Type.Literal("flow"),
381
+ mode: Type.Enum(["single", "parallel", "chain", "hierarchy"]),
382
+ /** A command launch has no tool call: the two variants share no event-id field. */
383
+ provenance: Type.Union([
384
+ strict({ source: Type.Literal("tool-call"), toolCallEventId: DhEventIdSchema }),
385
+ strict({ source: Type.Literal("command"), command: text, commandEventId: Type.Optional(DhEventIdSchema) }),
386
+ strict({ source: Type.Literal("internal") }),
387
+ ]),
388
+ logicalTaskId: Type.Optional(ident),
389
+ dependencies: Type.Optional(Type.Array(ident, { maxItems: MAX_LIST })),
390
+ causeEventId: Type.Optional(DhEventIdSchema),
391
+ childAgentId: Type.Optional(DhAgentIdSchema),
392
+ failedAttemptId: Type.Optional(DhAttemptIdSchema),
393
+ nextAttemptNumber: Type.Optional(positive),
394
+ delayMs: Type.Optional(count),
395
+ wave: Type.Optional(count),
396
+ order: Type.Optional(count),
397
+ policyVersion: Type.Optional(text),
398
+ reasonCode: Type.Optional(code),
399
+ });
400
+ }
401
+ /**
402
+ * A continuation decision about one addressed target (agent + attempt). `pending` and `consumed` are recorded
403
+ * by the target itself; `admitted` and `rejected` may be recorded by the admitting authority. `requestId`
404
+ * links a decision to a control `continue` request; a continuation that no request caused names none. Message
405
+ * text never appears here: it is content, and would also be free text inside a typed payload.
406
+ */
407
+ function continuationDecisionSchema() {
408
+ return strict({
409
+ schema: Type.Literal(IDS.continuationDecision),
410
+ decision: Type.Enum(DH_CONTINUATION_DECISIONS),
411
+ target: strict({ agentId: DhAgentIdSchema, attemptId: DhAttemptIdSchema }),
412
+ requestId: Type.Optional(requestId),
413
+ channel: Type.Enum(DH_CONTINUATION_CHANNELS),
414
+ reason: Type.Optional(Type.Enum(DH_CONTROL_REJECT_REASONS)),
415
+ });
416
+ }
417
+ // -- usage, health, control, amendment ---------------------------------------------------------
418
+ function usageSchema() {
419
+ return strict({
420
+ schema: Type.Literal(IDS.usage),
421
+ usageId: ident,
422
+ origin: usageOrigin(),
423
+ measurement: measurement(),
424
+ });
425
+ }
426
+ function captureHealthSchema() {
427
+ return strict({
428
+ schema: Type.Literal(IDS.captureHealth),
429
+ status: Type.Enum(["healthy", "degraded", "failed", "recovered"]),
430
+ condition: Type.Enum([
431
+ "none",
432
+ "dropped",
433
+ "write_failed",
434
+ "redaction_failed",
435
+ "unsupported_adapter",
436
+ "flush_timeout",
437
+ "lost_observer",
438
+ "recovery",
439
+ ]),
440
+ droppedEvents: Type.Optional(count),
441
+ droppedArtifacts: Type.Optional(count),
442
+ adapter: Type.Optional(strict({ name: text, version: text })),
443
+ affectedKinds: Type.Optional(Type.Array(Type.Enum(DH_EVENT_KINDS), { maxItems: 32, uniqueItems: true })),
444
+ coverageGaps: Type.Optional(Type.Array(coverageGap(), { maxItems: 32 })),
445
+ sequenceRange: Type.Optional(strict({ from: count, to: count })),
446
+ });
447
+ }
448
+ function controlCorrelationSchema() {
449
+ return strict({
450
+ schema: Type.Literal(IDS.controlCorrelation),
451
+ requestId,
452
+ action: Type.Enum(DH_CONTROL_ACTIONS),
453
+ phase: Type.Enum(["requested", "observed"]),
454
+ state: Type.Enum(DH_CONTROL_STATES),
455
+ reason: Type.Optional(Type.Enum(DH_CONTROL_REJECT_REASONS)),
456
+ terminationConfirmed: Type.Optional(Type.Boolean()),
457
+ observed: Type.Optional(strict({
458
+ startedAgentId: Type.Optional(DhAgentIdSchema),
459
+ startedAttemptId: Type.Optional(DhAttemptIdSchema),
460
+ consumingEventId: Type.Optional(DhEventIdSchema),
461
+ })),
462
+ });
463
+ }
464
+ /** Local cancellation is not a Code control request: there is deliberately no requestId. */
465
+ function localCancellationSchema() {
466
+ return strict({
467
+ schema: Type.Literal(IDS.localCancellation),
468
+ requester: Type.Literal("local"),
469
+ scope: Type.Enum(["selected", "whole-run"]),
470
+ phase: Type.Enum(["requested", "observed"]),
471
+ state: Type.Enum(["requested", "applied", "rejected", "unsupported", "unknown"]),
472
+ reason: Type.Optional(Type.Enum(DH_CONTROL_REJECT_REASONS)),
473
+ diagnosticCode: Type.Optional(Type.Enum(DH_LOCAL_CANCELLATION_DIAGNOSTICS)),
474
+ terminationConfirmed: Type.Optional(Type.Boolean()),
475
+ exitCode: Type.Optional(Type.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER })),
476
+ });
477
+ }
478
+ function amendmentSchema() {
479
+ return strict({
480
+ schema: Type.Literal(IDS.amendment),
481
+ amendsEventId: DhEventIdSchema,
482
+ amendsKind: Type.Enum(DH_EVENT_KINDS),
483
+ relation: Type.Enum(["supersedes", "resolves-unknown"]),
484
+ reasonCode: code,
485
+ coverage: Type.Optional(Type.Array(coverageGap(), { minItems: 1, maxItems: 32 })),
486
+ replacement: Type.Optional(strict({ outcome: Type.Enum(DH_OUTCOMES), failureCode: Type.Optional(code) })),
487
+ });
488
+ }
489
+ // ---------------------------------------------------------------------------
490
+ // Registry
491
+ // ---------------------------------------------------------------------------
492
+ function buildParsers() {
493
+ const submission = define(IDS.modelSubmission, modelSubmissionSchema, undefined, (payload, event) => {
494
+ const id = IDS.modelSubmission;
495
+ assertEventAttempt(id, event, payload.attemptId);
496
+ if (payload.requestArtifact !== undefined) {
497
+ assertListed(id, event, payload.requestArtifact.artifactId);
498
+ }
499
+ else
500
+ assertLossCovered(id, event, "payload.requestArtifact");
501
+ });
502
+ const response = define(IDS.modelResponse, modelResponseSchema, (payload) => {
503
+ if (payload.usage !== undefined)
504
+ assertMeasurement(IDS.modelResponse, payload.usage.measurement);
505
+ }, (payload, event) => {
506
+ const id = IDS.modelResponse;
507
+ assertEventAttempt(id, event, payload.attemptId);
508
+ const origin = payload.usage?.origin;
509
+ if (origin !== undefined && (origin.owner !== "submission" || origin.submissionId !== payload.submissionId)) {
510
+ bad(id, "usage origin must be the submission this response answers");
511
+ }
512
+ if (payload.responseArtifact !== undefined)
513
+ assertListed(id, event, payload.responseArtifact.artifactId);
514
+ });
515
+ const toolCall = define(IDS.toolCall, toolCallSchema, (payload) => {
516
+ const id = IDS.toolCall;
517
+ if (payload.parentEventId !== undefined && payload.parentCallId === undefined) {
518
+ bad(id, "parentEventId requires parentCallId");
519
+ }
520
+ if (payload.parentCallId === payload.callId)
521
+ bad(id, "a call cannot be its own parent");
522
+ }, (payload, event) => {
523
+ const id = IDS.toolCall;
524
+ if (payload.argumentsArtifact !== undefined) {
525
+ assertListed(id, event, payload.argumentsArtifact.artifactId);
526
+ }
527
+ else if (payload.argumentsPreview === undefined)
528
+ assertLossCovered(id, event, "payload.argumentsArtifact");
529
+ });
530
+ const toolResult = define(IDS.toolResult, toolResultSchema, (payload) => {
531
+ const id = IDS.toolResult;
532
+ if (payload.stage === "original") {
533
+ if (payload.originalResultEventId !== undefined) {
534
+ bad(id, "originalResultEventId is only allowed on a final result");
535
+ }
536
+ if (payload.modified !== undefined)
537
+ bad(id, "modified is only allowed on a final result");
538
+ }
539
+ else if (payload.originalResultEventId === undefined) {
540
+ bad(id, "originalResultEventId is required on a final result");
541
+ }
542
+ if (payload.childAgentId !== undefined && payload.childAgentIds !== undefined) {
543
+ bad(id, "use childAgentId or childAgentIds, not both");
544
+ }
545
+ if ((payload.nonTextArtifacts?.length ?? 0) > 0 && payload.nonTextBlockCount === 0) {
546
+ bad(id, "nontext artifacts require nonTextBlockCount above zero");
547
+ }
548
+ }, (payload, event) => {
549
+ const id = IDS.toolResult;
550
+ if (payload.stage === "original") {
551
+ // The untouched executed result keeps the real execution outcome, independent of later hooks.
552
+ if (event.outcome === undefined)
553
+ bad(id, "outcome is required on an original tool result");
554
+ if (event.outcome === "succeeded" && payload.isError)
555
+ bad(id, "isError contradicts outcome succeeded");
556
+ if (event.outcome === "failed" && !payload.isError)
557
+ bad(id, "isError contradicts outcome failed");
558
+ }
559
+ if (payload.outputArtifact !== undefined)
560
+ assertListed(id, event, payload.outputArtifact.artifactId);
561
+ if (payload.nonTextBlockCount > 0) {
562
+ const artifacts = payload.nonTextArtifacts ?? [];
563
+ for (const ref of artifacts)
564
+ assertListed(id, event, ref.artifactId);
565
+ if (artifacts.length === 0) {
566
+ const coverage = event.fields["payload.nonTextContent"];
567
+ if (coverage === undefined || !LOSS_STATES.has(coverage.state)) {
568
+ bad(id, "nontext content has neither artifacts nor loss coverage at payload.nonTextContent");
569
+ }
570
+ }
571
+ }
572
+ });
573
+ const file = define(IDS.fileEvidence, fileEvidenceSchema, (payload) => {
574
+ const id = IDS.fileEvidence;
575
+ const { before, after, effectObserved } = payload.observed;
576
+ if (effectObserved && after.state !== "observed") {
577
+ bad(id, "effectObserved requires an observed after snapshot");
578
+ }
579
+ if (before.state === "observed" && after.state === "observed") {
580
+ const changed = before.sha256 !== after.sha256;
581
+ if (changed !== effectObserved)
582
+ bad(id, "effectObserved must match the snapshot change when both snapshots are observed");
583
+ }
584
+ const tree = payload.worktree;
585
+ if (tree !== undefined) {
586
+ if (!tree.baseObserved && tree.baseRevision !== undefined) {
587
+ bad(id, "baseRevision must not be recorded when the base was not observed");
588
+ }
589
+ if (tree.mode === "isolated" && (!tree.baseObserved || tree.baseRevision === undefined)) {
590
+ bad(id, "an isolated worktree needs an observed baseRevision");
591
+ }
592
+ }
593
+ }, (payload, event) => {
594
+ const id = IDS.fileEvidence;
595
+ assertListed(id, event, payload.pathRef.artifactId);
596
+ for (const ref of payload.evidence ?? [])
597
+ assertListed(id, event, ref.artifactId);
598
+ });
599
+ const spawn = define(IDS.agentSpawn, agentSpawnSchema, (payload) => {
600
+ const id = IDS.agentSpawn;
601
+ if (payload.via === "subagent") {
602
+ if (payload.command !== undefined)
603
+ bad(id, "a subagent spawn cannot carry command");
604
+ if (payload.launchedByToolCallEventId !== undefined) {
605
+ bad(id, "a subagent spawn uses spawnedByEventId, not launchedByToolCallEventId");
606
+ }
607
+ return;
608
+ }
609
+ if (payload.spawnedByEventId !== undefined)
610
+ bad(id, "a flow launch does not use spawnedByEventId");
611
+ if ((payload.launchedByToolCallEventId === undefined) === (payload.command === undefined)) {
612
+ bad(id, "exactly one of launchedByToolCallEventId or command is required");
613
+ }
614
+ }, (payload, event) => {
615
+ const id = IDS.agentSpawn;
616
+ if (payload.via === "flow") {
617
+ if (event.producer.issuer !== "flow")
618
+ bad(id, "a flow launch requires producer issuer flow");
619
+ }
620
+ else if (payload.spawnedByEventId === undefined)
621
+ assertLossCovered(id, event, "payload.spawnedByEventId");
622
+ });
623
+ const relation = define(IDS.agentRelation, agentRelationSchema, undefined, (payload, event) => {
624
+ const id = IDS.agentRelation;
625
+ if (payload.sourceAgentId === event.agentId) {
626
+ bad(id, "a cross-agent relation needs a sourceAgentId different from the event agentId");
627
+ }
628
+ if (event.retryOfAttemptId !== undefined) {
629
+ bad(id, "retryOfAttemptId is a same-agent link and cannot accompany a cross-agent relation");
630
+ }
631
+ if (event.producer.issuer !== "code")
632
+ bad(id, "native retry requires Code issuer");
633
+ });
634
+ const lifecycle = define(IDS.attemptLifecycle, attemptLifecycleSchema, (payload) => {
635
+ const id = IDS.attemptLifecycle;
636
+ if (payload.retryOfAttemptId !== undefined) {
637
+ if (payload.phase !== "started")
638
+ bad(id, "retryOfAttemptId is only allowed in phase started");
639
+ if (payload.retryOfAttemptId === payload.attemptId)
640
+ bad(id, "retryOfAttemptId cannot equal attemptId");
641
+ }
642
+ if (payload.phase === "retry-scheduled") {
643
+ if (payload.delayMs === undefined)
644
+ bad(id, "delayMs is required in phase retry-scheduled");
645
+ if (payload.nextAttemptNumber === undefined || payload.nextAttemptNumber < 2) {
646
+ bad(id, "nextAttemptNumber of at least 2 is required in phase retry-scheduled");
647
+ }
648
+ }
649
+ else if (payload.nextAttemptNumber !== undefined || payload.delayMs !== undefined) {
650
+ bad(id, "nextAttemptNumber and delayMs are only allowed in phase retry-scheduled");
651
+ }
652
+ if (payload.terminationConfirmed !== undefined && payload.phase !== "finished") {
653
+ bad(id, "terminationConfirmed is only allowed in phase finished");
654
+ }
655
+ if (payload.spawnOutcome !== undefined && payload.phase !== "started") {
656
+ bad(id, "spawnOutcome is only allowed in phase started");
657
+ }
658
+ }, (payload, event) => {
659
+ const id = IDS.attemptLifecycle;
660
+ if (PHASE_KIND[payload.phase] !== event.kind)
661
+ bad(id, "phase does not match event kind");
662
+ if (payload.agentId !== event.agentId)
663
+ bad(id, "agentId does not match the event agentId");
664
+ assertEventAttempt(id, event, payload.attemptId);
665
+ if (payload.attemptNumber !== undefined && payload.attemptNumber !== event.attemptNumber) {
666
+ bad(id, "attemptNumber does not match the event attemptNumber");
667
+ }
668
+ if (payload.retryOfAttemptId !== event.retryOfAttemptId) {
669
+ bad(id, "retryOfAttemptId must match the event retryOfAttemptId");
670
+ }
671
+ if (payload.phase === "finished" && event.outcome === undefined)
672
+ bad(id, "outcome is required on attempt finish");
673
+ });
674
+ const decision = define(IDS.flowDecision, flowDecisionSchema, (payload) => {
675
+ const id = IDS.flowDecision;
676
+ const kind = payload.decision;
677
+ if (payload.logicalTaskId === undefined)
678
+ bad(id, "logicalTaskId is required");
679
+ if (payload.childAgentId !== undefined && kind !== "delegate") {
680
+ bad(id, "childAgentId is only allowed on delegate");
681
+ }
682
+ const retryFields = payload.failedAttemptId !== undefined ||
683
+ payload.nextAttemptNumber !== undefined ||
684
+ payload.delayMs !== undefined;
685
+ if (retryFields && kind !== "retry")
686
+ bad(id, "failedAttemptId, nextAttemptNumber and delayMs are only allowed on retry");
687
+ const dependencies = payload.dependencies ?? [];
688
+ if (new Set(dependencies).size !== dependencies.length || dependencies.includes(payload.logicalTaskId)) {
689
+ bad(id, "dependencies must be unique logical tasks other than the task itself");
690
+ }
691
+ if (kind === "dependency-not-launched" || kind === "dependency-released") {
692
+ if (dependencies.length === 0)
693
+ bad(id, "dependency decision requires dependencies");
694
+ if (payload.causeEventId === undefined)
695
+ bad(id, "dependency decision requires causeEventId");
696
+ }
697
+ if (kind === "retry") {
698
+ if (payload.failedAttemptId === undefined)
699
+ bad(id, "retry requires failedAttemptId");
700
+ if (payload.nextAttemptNumber === undefined || payload.nextAttemptNumber < 2) {
701
+ bad(id, "retry requires nextAttemptNumber of at least 2");
702
+ }
703
+ }
704
+ }, (_payload, event) => {
705
+ if (event.producer.issuer !== "flow")
706
+ bad(IDS.flowDecision, "a flow decision requires producer issuer flow");
707
+ });
708
+ const continuation = define(IDS.continuationDecision, continuationDecisionSchema, (payload) => {
709
+ if (payload.decision === "continuation.consumed" &&
710
+ payload.channel === "queue-steering" &&
711
+ payload.requestId !== undefined)
712
+ bad(IDS.continuationDecision, "queue-steering cannot certify consumption");
713
+ if ((payload.decision === "continuation.rejected") !== (payload.reason !== undefined)) {
714
+ bad(IDS.continuationDecision, "reason is required when the decision is continuation.rejected, and only then");
715
+ }
716
+ }, (payload, event) => {
717
+ // Pending and consumed are facts the target itself observes; admission and rejection may come from the authority.
718
+ if (payload.decision !== "continuation.pending" && payload.decision !== "continuation.consumed")
719
+ return;
720
+ if (event.agentId !== payload.target.agentId || event.attemptId !== payload.target.attemptId) {
721
+ bad(IDS.continuationDecision, "pending or consumed continuation must be recorded on the addressed target agent and attempt");
722
+ }
723
+ });
724
+ const usage = define(IDS.usage, usageSchema, (payload) => assertMeasurement(IDS.usage, payload.measurement), (payload, event) => {
725
+ const id = IDS.usage;
726
+ const origin = payload.origin;
727
+ if (origin.owner === "attempt" && event.attemptId !== origin.attemptId) {
728
+ bad(id, "usage origin attemptId does not match the event attemptId");
729
+ }
730
+ if (origin.owner === "submission" && event.attemptId === undefined) {
731
+ bad(id, "usage origin needs the submission's attemptId on the event");
732
+ }
733
+ });
734
+ const health = define(IDS.captureHealth, captureHealthSchema, (payload) => {
735
+ const id = IDS.captureHealth;
736
+ if ((payload.status === "healthy") !== (payload.condition === "none")) {
737
+ bad(id, "condition none is required for, and only for, status healthy");
738
+ }
739
+ if (payload.sequenceRange !== undefined && payload.sequenceRange.from > payload.sequenceRange.to) {
740
+ bad(id, "sequenceRange.from must not exceed sequenceRange.to");
741
+ }
742
+ assertGaps(id, payload.coverageGaps ?? []);
743
+ }, undefined);
744
+ const control = define(IDS.controlCorrelation, controlCorrelationSchema, (payload) => {
745
+ const id = IDS.controlCorrelation;
746
+ if (payload.phase === "requested" && payload.state !== "requested" && payload.state !== "accepted") {
747
+ bad(id, "phase requested allows only states requested or accepted");
748
+ }
749
+ if (payload.phase === "observed" && payload.state === "requested") {
750
+ bad(id, "phase observed cannot be in state requested");
751
+ }
752
+ if ((payload.state === "rejected") !== (payload.reason !== undefined)) {
753
+ bad(id, "reason is required when state is rejected, and only then");
754
+ }
755
+ if (payload.terminationConfirmed !== undefined) {
756
+ if (payload.action !== "cancel")
757
+ bad(id, "terminationConfirmed is only valid for the cancel action");
758
+ if (payload.terminationConfirmed && payload.state !== "applied") {
759
+ bad(id, "terminationConfirmed true requires state applied");
760
+ }
761
+ }
762
+ const applied = payload.state === "applied";
763
+ if (payload.observed !== undefined && !(applied && payload.action !== "cancel")) {
764
+ bad(id, "observed is only valid for an applied retry or continue");
765
+ }
766
+ if (applied && payload.action === "retry") {
767
+ if (payload.observed?.startedAgentId === undefined || payload.observed?.startedAttemptId === undefined) {
768
+ bad(id, "an applied retry needs both observed.startedAgentId and observed.startedAttemptId");
769
+ }
770
+ }
771
+ if (applied && payload.action === "continue" && payload.observed?.consumingEventId === undefined) {
772
+ bad(id, "an applied continue needs observed.consumingEventId");
773
+ }
774
+ }, (payload, event) => {
775
+ const id = IDS.controlCorrelation;
776
+ if (event.kind === "cancellation.requested" || event.kind === "cancellation.outcome") {
777
+ if (payload.action !== "cancel")
778
+ bad(id, "cancellation event carries only the cancel action");
779
+ const phase = event.kind === "cancellation.requested" ? "requested" : "observed";
780
+ if (payload.phase !== phase)
781
+ bad(id, "phase does not match cancellation event kind");
782
+ }
783
+ // The graph links a claim to observed facts of the addressed attempt, so the claim must name it.
784
+ const claim = payload.terminationConfirmed === true || (payload.state === "applied" && payload.action !== "cancel");
785
+ if (claim && event.attemptId === undefined) {
786
+ bad(id, "a termination, retry or continue claim needs the addressed attemptId on the event");
787
+ }
788
+ });
789
+ const localCancellation = define(IDS.localCancellation, localCancellationSchema, (payload) => {
790
+ const id = IDS.localCancellation;
791
+ if (payload.phase === "requested") {
792
+ if (payload.state !== "requested" ||
793
+ payload.reason !== undefined ||
794
+ payload.diagnosticCode !== undefined ||
795
+ payload.terminationConfirmed !== undefined ||
796
+ payload.exitCode !== undefined) {
797
+ bad(id, "phase requested requires state requested and forbids reason, diagnosticCode, terminationConfirmed and exitCode");
798
+ }
799
+ return;
800
+ }
801
+ if (payload.state === "requested")
802
+ bad(id, "phase observed cannot be in state requested");
803
+ if (payload.terminationConfirmed === undefined)
804
+ bad(id, "phase observed requires terminationConfirmed");
805
+ if (payload.terminationConfirmed && payload.state !== "applied")
806
+ bad(id, "terminationConfirmed true requires state applied");
807
+ if ((payload.state === "rejected") !== (payload.reason !== undefined)) {
808
+ bad(id, "reason is required when state is rejected, and only then");
809
+ }
810
+ }, (payload, event) => {
811
+ const phase = event.kind === "cancellation.requested" ? "requested" : "observed";
812
+ if (payload.phase !== phase)
813
+ bad(IDS.localCancellation, "phase does not match cancellation event kind");
814
+ if (payload.terminationConfirmed === true && event.attemptId === undefined) {
815
+ bad(IDS.localCancellation, "a termination claim needs the addressed attemptId on the event");
816
+ }
817
+ });
818
+ const amendment = define(IDS.amendment, amendmentSchema, (payload) => {
819
+ const id = IDS.amendment;
820
+ const replacement = payload.replacement;
821
+ if (payload.relation === "supersedes") {
822
+ if (replacement !== undefined)
823
+ bad(id, "supersedes cannot carry a replacement outcome");
824
+ if (payload.coverage === undefined)
825
+ bad(id, "an amendment needs coverage or a replacement outcome");
826
+ }
827
+ else {
828
+ if (replacement === undefined)
829
+ bad(id, "resolves-unknown requires a replacement outcome");
830
+ if (replacement.outcome === "unknown")
831
+ bad(id, "resolves-unknown requires a known replacement outcome");
832
+ }
833
+ if (replacement?.failureCode !== undefined && replacement.outcome !== "failed") {
834
+ bad(id, "failureCode is only allowed when the replacement outcome is failed");
835
+ }
836
+ assertGaps(id, payload.coverage ?? []);
837
+ }, (payload, event) => {
838
+ if (payload.amendsEventId === event.eventId)
839
+ bad(IDS.amendment, "an amendment cannot amend itself");
840
+ });
841
+ return new Map([
842
+ [IDS.modelSubmission, submission],
843
+ [IDS.modelResponse, response],
844
+ [IDS.toolCall, toolCall],
845
+ [IDS.toolResult, toolResult],
846
+ [IDS.fileEvidence, file],
847
+ [IDS.agentSpawn, spawn],
848
+ [IDS.agentRelation, relation],
849
+ [IDS.attemptLifecycle, lifecycle],
850
+ [IDS.flowDecision, decision],
851
+ [IDS.continuationDecision, continuation],
852
+ [IDS.usage, usage],
853
+ [IDS.captureHealth, health],
854
+ [IDS.controlCorrelation, control],
855
+ [IDS.localCancellation, localCancellation],
856
+ [IDS.amendment, amendment],
857
+ ]);
858
+ }
859
+ let parsers;
860
+ /**
861
+ * Enforces the payload contract on one event. Legacy kinds without `payload.schema` pass; everything
862
+ * else must name a registered (kind, schema id) pair and satisfy that schema and its envelope rules.
863
+ * Called by `parseDhEvent` on the trusted clone.
864
+ */
865
+ export function assertDhEventPayload(event) {
866
+ const id = event.payload.schema;
867
+ if (id === undefined) {
868
+ if (DH_LEGACY_FREEFORM_PAYLOAD_KINDS.includes(event.kind))
869
+ return;
870
+ throw new DhContractError("schema_invalid", "Invalid dh.event/1: event kind requires a payload schema");
871
+ }
872
+ if (typeof id !== "string")
873
+ throw new DhContractError("schema_invalid", "Invalid dh.event/1: payload.schema must be a string");
874
+ const registration = REGISTRATIONS.find((entry) => entry.id === id);
875
+ if (registration === undefined)
876
+ throw new DhContractError("schema_invalid", "Invalid dh.event/1: unknown payload schema");
877
+ if (!registration.kinds.includes(event.kind)) {
878
+ throw new DhContractError("schema_invalid", "Invalid dh.event/1: payload schema is not registered for kind");
879
+ }
880
+ parsers ??= buildParsers();
881
+ parsers.get(id)?.check(event);
882
+ }
883
+ // ---------------------------------------------------------------------------
884
+ // Cross-event payload graph
885
+ // ---------------------------------------------------------------------------
886
+ function str(value) {
887
+ return typeof value === "string" ? value : undefined;
888
+ }
889
+ function obj(value) {
890
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
891
+ }
892
+ const typed = (event, id) => event.payload.schema === id;
893
+ /** Generation, agent and attempt: what a control request addresses. */
894
+ const targetKey = (event) => JSON.stringify([event.generationId, event.agentId, event.attemptId ?? null]);
895
+ /** A cancellation.outcome that observed the target terminate for a control cancel request. */
896
+ const confirmsTermination = (event) => event.kind === "cancellation.outcome" &&
897
+ typed(event, IDS.controlCorrelation) &&
898
+ event.payload.action === "cancel" &&
899
+ event.payload.phase === "observed" &&
900
+ event.payload.state === "applied" &&
901
+ event.payload.terminationConfirmed === true;
902
+ /** The only consumption event type: the addressed target recorded that it consumed the continuation. */
903
+ const consumesContinuation = (event) => event.kind === "orchestration.decision" &&
904
+ typed(event, IDS.continuationDecision) &&
905
+ event.payload.decision === "continuation.consumed";
906
+ /** Every artifact reference a typed payload carries, with the payload field that names it. */
907
+ function* artifactRefs(event) {
908
+ const { payload } = event;
909
+ for (const field of [
910
+ "requestArtifact",
911
+ "responseArtifact",
912
+ "argumentsArtifact",
913
+ "outputArtifact",
914
+ "pathRef",
915
+ ]) {
916
+ const ref = obj(payload[field]);
917
+ if (ref !== undefined)
918
+ yield [field, ref];
919
+ }
920
+ for (const field of ["nonTextArtifacts", "evidence"]) {
921
+ const list = payload[field];
922
+ if (!Array.isArray(list))
923
+ continue;
924
+ for (const item of list) {
925
+ const ref = obj(item);
926
+ if (ref !== undefined)
927
+ yield [field, ref];
928
+ }
929
+ }
930
+ }
931
+ /**
932
+ * A reference carries the sha256 (and optionally size) the producer claims for the artifact; the artifact
933
+ * metadata is the stored fact. A mismatch means the reference does not describe that artifact. Artifacts
934
+ * missing from the metadata are reported by the bundle's own link check, not here.
935
+ */
936
+ function validateArtifactRefs(events, artifacts, err) {
937
+ const byId = new Map();
938
+ for (const artifact of artifacts)
939
+ if (!byId.has(artifact.artifactId))
940
+ byId.set(artifact.artifactId, artifact);
941
+ for (const event of events) {
942
+ if (typeof event.payload.schema !== "string")
943
+ continue;
944
+ for (const [field, ref] of artifactRefs(event)) {
945
+ const id = str(ref.artifactId) ?? "";
946
+ const artifact = byId.get(id);
947
+ if (artifact === undefined)
948
+ continue;
949
+ if (ref.sha256 !== artifact.sha256)
950
+ err(event, `${field} sha256 does not match artifact metadata`);
951
+ if (typeof ref.sizeBytes === "number" && ref.sizeBytes !== artifact.sizeBytes) {
952
+ err(event, `${field} sizeBytes does not match artifact metadata`);
953
+ }
954
+ }
955
+ }
956
+ }
957
+ /**
958
+ * Cross-event rules over already-parsed events. Legacy freeform events are ignored. Returns one message per
959
+ * violation (never throws); `validateDhTraceBundle` appends these to its own errors. Nothing here rewrites or
960
+ * reinterprets an original event: amendments are separate events that point at their target.
961
+ *
962
+ * One pass builds the indexes (events by id, attempts by agent, attempt starts, relations, confirmed
963
+ * terminations); every rule after it is a Map/Set lookup, so the cost is linear in the number of events
964
+ * (no per-event scan of `events`, no per-event array copy).
965
+ */
966
+ export function validateDhPayloadGraph(events, artifacts, ownership) {
967
+ const errors = [];
968
+ const err = (_event, message) => errors.push(message);
969
+ if (artifacts !== undefined)
970
+ validateArtifactRefs(events, artifacts, err);
971
+ const byId = new Map();
972
+ const attemptsByAgent = new Map();
973
+ const attemptStarts = new Map();
974
+ const effectiveTerminals = resolveDhEffectiveTerminals(events, ownership);
975
+ const failedAttempts = new Set([...effectiveTerminals]
976
+ .filter(([, terminal]) => terminal.outcome === "failed" && !terminal.conflict && !terminal.authorityUnresolved)
977
+ .map(([id]) => id));
978
+ const confirmedFinishes = new Set();
979
+ const relationsByAgent = new Map();
980
+ const confirmedCancels = new Map();
981
+ for (const event of events) {
982
+ if (!byId.has(event.eventId))
983
+ byId.set(event.eventId, event);
984
+ let attempts = attemptsByAgent.get(event.agentId);
985
+ if (attempts === undefined) {
986
+ attempts = new Set();
987
+ attemptsByAgent.set(event.agentId, attempts);
988
+ }
989
+ if (event.attemptId !== undefined) {
990
+ attempts.add(event.attemptId);
991
+ if (event.kind === "attempt.started" && !attemptStarts.has(event.attemptId)) {
992
+ attemptStarts.set(event.attemptId, event);
993
+ }
994
+ if (event.kind === "attempt.finished") {
995
+ if (typed(event, IDS.attemptLifecycle) && event.payload.terminationConfirmed === true) {
996
+ confirmedFinishes.add(targetKey(event));
997
+ }
998
+ }
999
+ }
1000
+ if (typed(event, IDS.agentRelation) && !relationsByAgent.has(event.agentId)) {
1001
+ relationsByAgent.set(event.agentId, event);
1002
+ }
1003
+ if (confirmsTermination(event)) {
1004
+ const request = str(event.payload.requestId) ?? "";
1005
+ const targets = confirmedCancels.get(request) ?? new Set();
1006
+ targets.add(targetKey(event));
1007
+ confirmedCancels.set(request, targets);
1008
+ }
1009
+ }
1010
+ const submissions = new Map();
1011
+ const requests = new Map();
1012
+ const responses = new Map();
1013
+ for (const event of events) {
1014
+ if (typed(event, IDS.modelSubmission)) {
1015
+ const submissionId = str(event.payload.submissionId) ?? "";
1016
+ if (submissions.has(submissionId))
1017
+ err(event, "duplicate submissionId");
1018
+ else
1019
+ submissions.set(submissionId, event);
1020
+ const requestKey = str(event.payload.requestId) ?? "";
1021
+ const attemptId = str(event.payload.attemptId);
1022
+ const entry = requests.get(requestKey) ?? { attemptId, indexes: new Set() };
1023
+ if (entry.attemptId !== attemptId)
1024
+ err(event, "requestId spans attempts");
1025
+ const index = Number(event.payload.transportRetryIndex);
1026
+ if (entry.indexes.has(index))
1027
+ err(event, "duplicate transportRetryIndex for requestId");
1028
+ entry.indexes.add(index);
1029
+ requests.set(requestKey, entry);
1030
+ }
1031
+ else if (typed(event, IDS.modelResponse)) {
1032
+ const submissionId = str(event.payload.submissionId) ?? "";
1033
+ const list = responses.get(submissionId);
1034
+ if (list === undefined)
1035
+ responses.set(submissionId, [event]);
1036
+ else
1037
+ list.push(event);
1038
+ }
1039
+ }
1040
+ for (const [submissionId, list] of responses) {
1041
+ const submission = submissions.get(submissionId);
1042
+ for (const event of list) {
1043
+ if (submission === undefined) {
1044
+ err(event, "model response has no matching model submission");
1045
+ continue;
1046
+ }
1047
+ if (submission.payload.attemptId !== event.payload.attemptId) {
1048
+ err(event, "response attempt differs from the submission attempt");
1049
+ }
1050
+ if (submission.agentId !== event.agentId) {
1051
+ err(event, "response agent differs from the submission agent");
1052
+ }
1053
+ }
1054
+ for (let index = 1; index < list.length; index++) {
1055
+ const event = list[index];
1056
+ if (event !== undefined)
1057
+ err(event, "more than one model response for submission");
1058
+ }
1059
+ }
1060
+ // Each usage measurement has exactly one owner; aggregates are inclusive and never counted.
1061
+ const owned = new Map();
1062
+ const submissionUsageAttempts = new Set();
1063
+ const attemptUsage = [];
1064
+ const own = (event, key) => {
1065
+ if (owned.has(key))
1066
+ err(event, "usage is owned by more than one event (double count)");
1067
+ else
1068
+ owned.set(key, event);
1069
+ };
1070
+ for (const event of events) {
1071
+ let origin;
1072
+ if (typed(event, IDS.modelResponse))
1073
+ origin = obj(obj(event.payload.usage)?.origin);
1074
+ else if (typed(event, IDS.usage))
1075
+ origin = obj(event.payload.origin);
1076
+ if (origin === undefined)
1077
+ continue;
1078
+ if (origin.owner === "submission") {
1079
+ const submissionId = str(origin.submissionId) ?? "";
1080
+ own(event, `submission:${submissionId}`);
1081
+ if (typed(event, IDS.usage)) {
1082
+ // A response's submission link is checked above; a usage event names its submission on its own.
1083
+ const submission = submissions.get(submissionId);
1084
+ if (submission === undefined) {
1085
+ err(event, "usage.reported references no model submission");
1086
+ }
1087
+ else {
1088
+ if (submission.attemptId !== event.attemptId) {
1089
+ err(event, "usage submission belongs to a different attempt");
1090
+ }
1091
+ if (submission.agentId !== event.agentId) {
1092
+ err(event, "usage submission belongs to a different agent");
1093
+ }
1094
+ }
1095
+ }
1096
+ const attemptId = str(event.payload.attemptId) ?? event.attemptId;
1097
+ if (attemptId !== undefined)
1098
+ submissionUsageAttempts.add(attemptId);
1099
+ }
1100
+ else if (origin.owner === "attempt") {
1101
+ const attemptId = str(origin.attemptId) ?? "";
1102
+ own(event, `attempt:${attemptId}`);
1103
+ attemptUsage.push({ event, attemptId });
1104
+ }
1105
+ }
1106
+ for (const { event, attemptId } of attemptUsage) {
1107
+ if (submissionUsageAttempts.has(attemptId)) {
1108
+ err(event, "usage for attempt duplicates submission-owned usage in the same attempt (double count)");
1109
+ }
1110
+ }
1111
+ // Tool calls: a nested call names a real enclosing tool.call.
1112
+ for (const event of events) {
1113
+ if (!typed(event, IDS.toolCall))
1114
+ continue;
1115
+ const parentId = str(event.payload.parentEventId);
1116
+ if (parentId === undefined)
1117
+ continue;
1118
+ const parent = byId.get(parentId);
1119
+ if (parent?.kind !== "tool.call") {
1120
+ err(event, "parentEventId is not a tool.call in this bundle");
1121
+ continue;
1122
+ }
1123
+ if (parent.generationId !== event.generationId) {
1124
+ err(event, "parent tool.call belongs to a different generation");
1125
+ }
1126
+ const parentCallId = str(event.payload.parentCallId);
1127
+ const expected = str(parent.payload.callId);
1128
+ if (parentCallId !== expected) {
1129
+ err(event, "parentCallId differs from the parent tool.call callId");
1130
+ }
1131
+ }
1132
+ // Original and final tool results.
1133
+ const originals = new Map();
1134
+ const finalsFor = new Set();
1135
+ for (const event of events) {
1136
+ if (!typed(event, IDS.toolResult) || event.payload.stage !== "original")
1137
+ continue;
1138
+ const key = JSON.stringify([event.agentId, event.attemptId ?? null, event.payload.callId]);
1139
+ if (originals.has(key))
1140
+ err(event, "duplicate original tool.result for call");
1141
+ else
1142
+ originals.set(key, event);
1143
+ }
1144
+ for (const event of events) {
1145
+ if (!typed(event, IDS.toolResult) || event.payload.stage !== "final")
1146
+ continue;
1147
+ const targetId = str(event.payload.originalResultEventId) ?? "";
1148
+ const target = byId.get(targetId);
1149
+ if (target === undefined || target.kind !== "tool.result") {
1150
+ err(event, "originalResultEventId references no original tool.result");
1151
+ continue;
1152
+ }
1153
+ if (!typed(target, IDS.toolResult) || target.payload.stage !== "original") {
1154
+ err(event, "originalResultEventId is not an original tool.result");
1155
+ continue;
1156
+ }
1157
+ if (target.payload.callId !== event.payload.callId) {
1158
+ err(event, "final result callId differs from the original call");
1159
+ }
1160
+ else if (target.agentId !== event.agentId || target.attemptId !== event.attemptId) {
1161
+ err(event, "final result agent or attempt differs from the original");
1162
+ }
1163
+ if (finalsFor.has(targetId))
1164
+ err(event, "duplicate final tool.result for original");
1165
+ finalsFor.add(targetId);
1166
+ }
1167
+ // Flow decisions.
1168
+ const notLaunched = new Set();
1169
+ const released = new Set();
1170
+ for (const event of events) {
1171
+ if (!typed(event, IDS.flowDecision))
1172
+ continue;
1173
+ const provenance = obj(event.payload.provenance);
1174
+ const cause = str(event.payload.causeEventId);
1175
+ if (cause !== undefined && !byId.has(cause))
1176
+ err(event, "causeEventId references no event");
1177
+ if (provenance?.source === "tool-call") {
1178
+ const target = byId.get(str(provenance.toolCallEventId) ?? "");
1179
+ if (target?.kind !== "tool.call")
1180
+ err(event, "provenance toolCallEventId is not a tool.call in this bundle");
1181
+ else if (target.generationId !== event.generationId) {
1182
+ err(event, "provenance toolCallEventId is in a different generation");
1183
+ }
1184
+ }
1185
+ else if (provenance?.source === "command") {
1186
+ const commandEvent = str(provenance.commandEventId);
1187
+ if (commandEvent !== undefined && !byId.has(commandEvent)) {
1188
+ err(event, "provenance commandEventId references no event");
1189
+ }
1190
+ }
1191
+ const task = str(event.payload.logicalTaskId) ?? "";
1192
+ if (event.payload.decision === "dependency-not-launched")
1193
+ notLaunched.add(task);
1194
+ if (event.payload.decision === "dependency-released")
1195
+ released.add(task);
1196
+ if (event.payload.decision === "retry") {
1197
+ const failed = str(event.payload.failedAttemptId) ?? "";
1198
+ if (!failedAttempts.has(failed))
1199
+ err(event, "retry failedAttemptId has no failed attempt.finished");
1200
+ }
1201
+ }
1202
+ for (const event of events) {
1203
+ if (!typed(event, IDS.agentSpawn))
1204
+ continue;
1205
+ const task = str(event.payload.logicalTaskId);
1206
+ if (task !== undefined && notLaunched.has(task) && !released.has(task)) {
1207
+ err(event, "task was decided not launched and has no dependency-released decision");
1208
+ }
1209
+ // A Flow launch from a tool call names a real tool.call of the same generation made by the parent agent.
1210
+ const callId = event.payload.via === "flow" ? str(event.payload.launchedByToolCallEventId) : undefined;
1211
+ if (callId === undefined)
1212
+ continue;
1213
+ const call = byId.get(callId);
1214
+ if (call?.kind !== "tool.call") {
1215
+ err(event, "launchedByToolCallEventId is not a tool.call in this bundle");
1216
+ continue;
1217
+ }
1218
+ if (call.generationId !== event.generationId) {
1219
+ err(event, "launching tool.call belongs to a different generation");
1220
+ }
1221
+ if (event.parentAgentId === undefined)
1222
+ err(event, "a flow launch from a tool call needs parentAgentId");
1223
+ else if (call.agentId !== event.parentAgentId) {
1224
+ err(event, "launching tool.call belongs to an agent other than the parent agent");
1225
+ }
1226
+ if (call.streamId === event.streamId && call.sequence >= event.sequence) {
1227
+ err(event, "flow launch precedes its launching tool.call in the producer stream");
1228
+ }
1229
+ }
1230
+ // Full graph eligibility; a partial ingest may retain unresolved relations, never invent a failure.
1231
+ const nativeClaims = new Map();
1232
+ const latestByAgent = new Map();
1233
+ for (const start of attemptStarts.values()) {
1234
+ const latest = latestByAgent.get(start.agentId);
1235
+ if (!latest || (start.attemptNumber ?? 0) > (latest.attemptNumber ?? 0))
1236
+ latestByAgent.set(start.agentId, start);
1237
+ }
1238
+ for (const event of events) {
1239
+ if (!typed(event, IDS.agentRelation))
1240
+ continue;
1241
+ const source = str(event.payload.sourceAgentId) ?? "";
1242
+ const attempt = str(event.payload.sourceAttemptId) ?? "";
1243
+ const known = attemptsByAgent.get(source);
1244
+ if (known === undefined)
1245
+ err(event, "sourceAgentId has no events in this bundle");
1246
+ else if (!known.has(attempt))
1247
+ err(event, "sourceAttemptId is not an attempt of sourceAgentId");
1248
+ const start = attemptStarts.get(attempt);
1249
+ const terminal = effectiveTerminals.get(attempt);
1250
+ if (!start ||
1251
+ !terminal ||
1252
+ terminal.outcome !== "failed" ||
1253
+ terminal.conflict ||
1254
+ terminal.authorityUnresolved ||
1255
+ terminal.event.agentId !== source ||
1256
+ latestByAgent.get(source)?.attemptId !== attempt)
1257
+ err(event, "not_eligible: native source must be the latest finished failed attempt");
1258
+ if (event.producer.issuer !== "code" || event.agentId === source || event.retryOfAttemptId !== undefined)
1259
+ err(event, "schema_invalid: invalid native relation authority or identity");
1260
+ if (start &&
1261
+ (start.generationId !== event.generationId ||
1262
+ start.tenantScope.environmentId !== event.tenantScope.environmentId ||
1263
+ start.tenantScope.projectId !== event.tenantScope.projectId))
1264
+ err(event, "not_eligible: native source has a foreign generation or scope");
1265
+ if (terminal &&
1266
+ (terminal.event.generationId !== event.generationId ||
1267
+ terminal.event.tenantScope.environmentId !== event.tenantScope.environmentId ||
1268
+ terminal.event.tenantScope.projectId !== event.tenantScope.projectId))
1269
+ err(event, "not_eligible: native terminal has a foreign generation or scope");
1270
+ const targets = [...(attemptsByAgent.get(event.agentId) ?? [])].map((id) => attemptStarts.get(id));
1271
+ const first = targets.filter((start) => start?.attemptNumber === 1);
1272
+ const target = first.length === 1 ? first[0] : undefined;
1273
+ if (!target ||
1274
+ targets.some((start) => start?.attemptNumber === undefined) ||
1275
+ target.retryOfAttemptId !== undefined ||
1276
+ target.generationId !== event.generationId ||
1277
+ target.tenantScope.environmentId !== event.tenantScope.environmentId ||
1278
+ target.tenantScope.projectId !== event.tenantScope.projectId)
1279
+ err(event, "not_eligible: native target must start one fresh attempt at number 1");
1280
+ if (relationsByAgent.get(event.agentId) !== event || nativeClaims.has(attempt))
1281
+ err(event, "not_eligible: native source or target already claimed");
1282
+ nativeClaims.set(attempt, event.agentId);
1283
+ }
1284
+ // Amendments point at originals of the same producer and stream and never overwrite a known outcome.
1285
+ const resolved = new Set();
1286
+ for (const event of events) {
1287
+ if (!typed(event, IDS.amendment))
1288
+ continue;
1289
+ const targetId = str(event.payload.amendsEventId) ?? "";
1290
+ const target = byId.get(targetId);
1291
+ if (target === undefined) {
1292
+ err(event, "amendsEventId references no event");
1293
+ continue;
1294
+ }
1295
+ if (target.kind === "event.amendment")
1296
+ err(event, "cannot amend an amendment");
1297
+ else if (target.kind !== event.payload.amendsKind) {
1298
+ err(event, "amendsKind differs from the amended event kind");
1299
+ }
1300
+ if (target.agentId !== event.agentId)
1301
+ err(event, "amendment and amended event belong to different agents");
1302
+ if (target.generationId !== event.generationId) {
1303
+ err(event, "amendment and amended event belong to different generations");
1304
+ }
1305
+ // Producer identity is the issuer bound at authentication; version and mode are claims and are not compared.
1306
+ if (target.producer.issuer !== event.producer.issuer) {
1307
+ err(event, "amendment issuer differs from the amended event issuer");
1308
+ }
1309
+ if (target.streamId !== event.streamId) {
1310
+ err(event, "amendment stream differs from the amended event stream");
1311
+ }
1312
+ else if (target.sequence >= event.sequence) {
1313
+ err(event, "amendment precedes the amended event in the producer stream");
1314
+ }
1315
+ if (event.payload.relation === "resolves-unknown") {
1316
+ if (target.outcome !== "unknown") {
1317
+ err(event, "resolves-unknown targets an event whose outcome is not unknown");
1318
+ }
1319
+ else if (resolved.has(targetId))
1320
+ err(event, "outcome is already resolved by another amendment");
1321
+ resolved.add(targetId);
1322
+ }
1323
+ }
1324
+ // Continuation decisions address a known target attempt; a request is consumed at most once.
1325
+ const consumedRequests = new Set();
1326
+ for (const event of events) {
1327
+ if (!typed(event, IDS.continuationDecision))
1328
+ continue;
1329
+ const target = obj(event.payload.target);
1330
+ const agent = str(target?.agentId) ?? "";
1331
+ const attempt = str(target?.attemptId) ?? "";
1332
+ const known = attemptsByAgent.get(agent);
1333
+ if (known === undefined)
1334
+ err(event, "continuation target agent has no events in this bundle");
1335
+ else if (!known.has(attempt))
1336
+ err(event, "continuation target attempt is not an attempt of target agent");
1337
+ const request = str(event.payload.requestId);
1338
+ if (consumesContinuation(event) && request !== undefined) {
1339
+ if (consumedRequests.has(request))
1340
+ err(event, "duplicate continuation.consumed for request");
1341
+ consumedRequests.add(request);
1342
+ }
1343
+ }
1344
+ // One local origin per target AND scope: selected and whole-run are distinct requests.
1345
+ // Index the earliest sequence per stream so duplicate-origin diagnostics never make array order chronology.
1346
+ const localKey = (event) => JSON.stringify([targetKey(event), event.payload.scope]);
1347
+ const localRequests = new Map();
1348
+ for (const event of events) {
1349
+ if (!typed(event, IDS.localCancellation) || event.kind !== "cancellation.requested")
1350
+ continue;
1351
+ const key = localKey(event);
1352
+ let streams = localRequests.get(key);
1353
+ if (streams !== undefined)
1354
+ err(event, "duplicate local cancellation.requested for target and scope");
1355
+ else {
1356
+ streams = new Map();
1357
+ localRequests.set(key, streams);
1358
+ }
1359
+ streams.set(event.streamId, Math.min(streams.get(event.streamId) ?? event.sequence, event.sequence));
1360
+ }
1361
+ for (const event of events) {
1362
+ if (!typed(event, IDS.localCancellation) || event.kind !== "cancellation.outcome")
1363
+ continue;
1364
+ const streams = localRequests.get(localKey(event));
1365
+ if (streams === undefined) {
1366
+ err(event, "no matching local cancellation.requested for target and scope");
1367
+ }
1368
+ else {
1369
+ const sequence = streams.get(event.streamId);
1370
+ // Different streams have no shared sequence clock. Do not infer order from timestamps.
1371
+ if (sequence !== undefined && sequence >= event.sequence) {
1372
+ err(event, "observation precedes its local cancellation.requested in the producer stream");
1373
+ }
1374
+ }
1375
+ }
1376
+ // Control correlation: a claim of termination, consumption or a started retry needs observed linkage.
1377
+ for (const event of events) {
1378
+ if (!typed(event, IDS.controlCorrelation) || event.payload.state !== "applied")
1379
+ continue;
1380
+ const request = str(event.payload.requestId) ?? "";
1381
+ const observed = obj(event.payload.observed);
1382
+ if (event.payload.action === "cancel") {
1383
+ // A cancellation.outcome is itself the observation; any other event must point at one.
1384
+ if (event.payload.terminationConfirmed !== true || event.kind === "cancellation.outcome")
1385
+ continue;
1386
+ const outcome = confirmedCancels.get(request)?.has(targetKey(event)) ?? false;
1387
+ if (!outcome && !confirmedFinishes.has(targetKey(event))) {
1388
+ err(event, "terminationConfirmed is claimed without a matching observed cancellation.outcome or confirmed attempt.finished for request");
1389
+ }
1390
+ }
1391
+ else if (event.payload.action === "continue") {
1392
+ const proofError = dhAppliedControlEvidenceError({
1393
+ generationId: event.generationId,
1394
+ agentId: event.agentId,
1395
+ attemptId: event.attemptId,
1396
+ requestId: request,
1397
+ }, "continue", { consumingEventId: str(observed?.consumingEventId) }, { attemptStarts, relationsByAgent, byId });
1398
+ if (proofError)
1399
+ err(event, proofError);
1400
+ const consumingId = str(observed?.consumingEventId) ?? "";
1401
+ const consumer = byId.get(consumingId);
1402
+ if (consumer === undefined) {
1403
+ err(event, "consumingEventId references no event");
1404
+ }
1405
+ else if (!consumesContinuation(consumer)) {
1406
+ err(event, "consumingEventId is not a continuation.consumed decision");
1407
+ }
1408
+ else {
1409
+ if (consumer.generationId !== event.generationId ||
1410
+ consumer.agentId !== event.agentId ||
1411
+ consumer.attemptId !== event.attemptId) {
1412
+ err(event, "consuming event belongs to a different generation, agent or attempt than the addressed target");
1413
+ }
1414
+ const answered = str(consumer.payload.requestId);
1415
+ if (answered !== request) {
1416
+ err(event, "consuming event answers a different request");
1417
+ }
1418
+ }
1419
+ }
1420
+ else if (event.payload.action === "retry") {
1421
+ const proofError = dhAppliedControlEvidenceError(event, "retry", { startedAgentId: str(observed?.startedAgentId), startedAttemptId: str(observed?.startedAttemptId) }, { attemptStarts, relationsByAgent, byId });
1422
+ if (proofError)
1423
+ err(event, proofError);
1424
+ const startedAgent = str(observed?.startedAgentId);
1425
+ const startedAttempt = str(observed?.startedAttemptId);
1426
+ if (startedAgent !== undefined && startedAgent !== event.agentId) {
1427
+ // Native Code retry: a new agent linked to the addressed agent and attempt by a cross-agent relation.
1428
+ const relation = relationsByAgent.get(startedAgent);
1429
+ if (relation === undefined) {
1430
+ err(event, "startedAgentId has no agent.spawned relation in this bundle");
1431
+ }
1432
+ else if (relation.payload.sourceAgentId !== event.agentId ||
1433
+ relation.payload.sourceAttemptId !== event.attemptId) {
1434
+ err(event, "started agent is related to a different agent or attempt than the addressed target");
1435
+ }
1436
+ if (startedAttempt !== undefined && attemptStarts.get(startedAttempt)?.agentId !== startedAgent) {
1437
+ err(event, "startedAttemptId has no attempt.started for startedAgentId");
1438
+ }
1439
+ }
1440
+ else if (startedAttempt === undefined) {
1441
+ err(event, "a same-agent retry needs observed.startedAttemptId");
1442
+ }
1443
+ else {
1444
+ // Same agent, new attempt: the start event retries the addressed attempt.
1445
+ const start = attemptStarts.get(startedAttempt);
1446
+ if (start === undefined) {
1447
+ err(event, "startedAttemptId has no attempt.started in this bundle");
1448
+ }
1449
+ else {
1450
+ if (start.agentId !== event.agentId) {
1451
+ err(event, "started attempt belongs to a different agent");
1452
+ }
1453
+ if (start.retryOfAttemptId !== event.attemptId) {
1454
+ err(event, "started attempt does not retry the addressed attempt");
1455
+ }
1456
+ }
1457
+ }
1458
+ }
1459
+ }
1460
+ return errors;
1461
+ }
1462
+ /** Shared status/graph linkage check. Supplied records are evidence, never an allocation alone. */
1463
+ function dhAppliedControlEvidenceError(target, action, observed, index) {
1464
+ if (action === "continue") {
1465
+ const consumer = observed.consumingEventId ? index.byId.get(observed.consumingEventId) : undefined;
1466
+ if (!consumer ||
1467
+ !consumesContinuation(consumer) ||
1468
+ consumer.payload.channel === "queue-steering" ||
1469
+ consumer.generationId !== target.generationId ||
1470
+ consumer.agentId !== target.agentId ||
1471
+ consumer.attemptId !== target.attemptId ||
1472
+ consumer.payload.requestId !== target.requestId)
1473
+ return "not_eligible: no matching target consumption";
1474
+ return;
1475
+ }
1476
+ const start = observed.startedAttemptId ? index.attemptStarts.get(observed.startedAttemptId) : undefined;
1477
+ const source = target.attemptId ? index.attemptStarts.get(target.attemptId) : undefined;
1478
+ if (!observed.startedAgentId || !start || start.agentId !== observed.startedAgentId)
1479
+ return "not_eligible: no matching observed start pair";
1480
+ if (!source ||
1481
+ source.agentId !== target.agentId ||
1482
+ source.generationId !== target.generationId ||
1483
+ start.generationId !== target.generationId ||
1484
+ source.tenantScope.environmentId !== start.tenantScope.environmentId ||
1485
+ source.tenantScope.projectId !== start.tenantScope.projectId)
1486
+ return "not_eligible: observed start has wrong generation or scope";
1487
+ if (start.agentId === target.agentId) {
1488
+ if (start.retryOfAttemptId !== target.attemptId ||
1489
+ source.attemptNumber === undefined ||
1490
+ start.attemptNumber !== source.attemptNumber + 1)
1491
+ return "not_eligible: observed start has wrong retry linkage or number";
1492
+ }
1493
+ else {
1494
+ const relation = index.relationsByAgent.get(start.agentId);
1495
+ if (start.attemptNumber !== 1 ||
1496
+ start.retryOfAttemptId !== undefined ||
1497
+ !relation ||
1498
+ relation.producer.issuer !== "code" ||
1499
+ relation.generationId !== target.generationId ||
1500
+ relation.payload.sourceAgentId !== target.agentId ||
1501
+ relation.payload.sourceAttemptId !== target.attemptId ||
1502
+ relation.tenantScope.environmentId !== source.tenantScope.environmentId ||
1503
+ relation.tenantScope.projectId !== source.tenantScope.projectId)
1504
+ return "not_eligible: no admitted native relation for observed start";
1505
+ }
1506
+ }
1507
+ export function assertDhAppliedControlEvidence(target, action, observed, events) {
1508
+ const attemptStarts = new Map(), relationsByAgent = new Map(), byId = new Map();
1509
+ for (const event of events) {
1510
+ byId.set(event.eventId, event);
1511
+ if (event.kind === "attempt.started" && event.attemptId)
1512
+ attemptStarts.set(event.attemptId, event);
1513
+ if (typed(event, IDS.agentRelation))
1514
+ relationsByAgent.set(event.agentId, event);
1515
+ }
1516
+ if (dhAppliedControlEvidenceError(target, action, observed, { attemptStarts, relationsByAgent, byId }))
1517
+ throw new DhContractError("not_eligible");
1518
+ }
1519
+ //# sourceMappingURL=trace-payload.js.map