omk-agent-core 0.98.1 → 0.98.3

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 (91) hide show
  1. package/CHANGELOG.md +644 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +3 -5
  4. package/dist/agent.js.map +1 -1
  5. package/dist/effects/effect-journal.d.ts +43 -0
  6. package/dist/effects/effect-journal.d.ts.map +1 -0
  7. package/dist/effects/effect-journal.js +186 -0
  8. package/dist/effects/effect-journal.js.map +1 -0
  9. package/dist/effects/effect-recovery.d.ts +70 -0
  10. package/dist/effects/effect-recovery.d.ts.map +1 -0
  11. package/dist/effects/effect-recovery.js +120 -0
  12. package/dist/effects/effect-recovery.js.map +1 -0
  13. package/dist/effects/effect-transitions.d.ts +34 -0
  14. package/dist/effects/effect-transitions.d.ts.map +1 -0
  15. package/dist/effects/effect-transitions.js +148 -0
  16. package/dist/effects/effect-transitions.js.map +1 -0
  17. package/dist/effects/effect-types.d.ts +135 -0
  18. package/dist/effects/effect-types.d.ts.map +1 -0
  19. package/dist/effects/effect-types.js +32 -0
  20. package/dist/effects/effect-types.js.map +1 -0
  21. package/dist/harness/abort-delivery.d.ts +26 -0
  22. package/dist/harness/abort-delivery.d.ts.map +1 -0
  23. package/dist/harness/abort-delivery.js +36 -0
  24. package/dist/harness/abort-delivery.js.map +1 -0
  25. package/dist/harness/agent-harness.d.ts +38 -6
  26. package/dist/harness/agent-harness.d.ts.map +1 -1
  27. package/dist/harness/agent-harness.js +327 -325
  28. package/dist/harness/agent-harness.js.map +1 -1
  29. package/dist/harness/canonical-digest.d.ts +32 -0
  30. package/dist/harness/canonical-digest.d.ts.map +1 -0
  31. package/dist/harness/canonical-digest.js +164 -0
  32. package/dist/harness/canonical-digest.js.map +1 -0
  33. package/dist/harness/compaction/operation.d.ts +7 -0
  34. package/dist/harness/compaction/operation.d.ts.map +1 -1
  35. package/dist/harness/compaction/operation.js.map +1 -1
  36. package/dist/harness/deferred-commands.d.ts +53 -0
  37. package/dist/harness/deferred-commands.d.ts.map +1 -0
  38. package/dist/harness/deferred-commands.js +96 -0
  39. package/dist/harness/deferred-commands.js.map +1 -0
  40. package/dist/harness/harness-session.d.ts +5 -58
  41. package/dist/harness/harness-session.d.ts.map +1 -1
  42. package/dist/harness/harness-session.js +15 -18
  43. package/dist/harness/harness-session.js.map +1 -1
  44. package/dist/harness/operation-lifecycle-controller.d.ts +68 -0
  45. package/dist/harness/operation-lifecycle-controller.d.ts.map +1 -0
  46. package/dist/harness/operation-lifecycle-controller.js +199 -0
  47. package/dist/harness/operation-lifecycle-controller.js.map +1 -0
  48. package/dist/harness/operation-lifecycle-reducer.d.ts +19 -0
  49. package/dist/harness/operation-lifecycle-reducer.d.ts.map +1 -0
  50. package/dist/harness/operation-lifecycle-reducer.js +201 -0
  51. package/dist/harness/operation-lifecycle-reducer.js.map +1 -0
  52. package/dist/harness/operation-lifecycle-types.d.ts +130 -0
  53. package/dist/harness/operation-lifecycle-types.d.ts.map +1 -0
  54. package/dist/harness/operation-lifecycle-types.js +34 -0
  55. package/dist/harness/operation-lifecycle-types.js.map +1 -0
  56. package/dist/harness/operation-outcome.d.ts +79 -0
  57. package/dist/harness/operation-outcome.d.ts.map +1 -0
  58. package/dist/harness/operation-outcome.js +167 -0
  59. package/dist/harness/operation-outcome.js.map +1 -0
  60. package/dist/harness/operation-trace-divergence.d.ts +60 -0
  61. package/dist/harness/operation-trace-divergence.d.ts.map +1 -0
  62. package/dist/harness/operation-trace-divergence.js +199 -0
  63. package/dist/harness/operation-trace-divergence.js.map +1 -0
  64. package/dist/harness/operation-trace.d.ts +134 -0
  65. package/dist/harness/operation-trace.d.ts.map +1 -0
  66. package/dist/harness/operation-trace.js +161 -0
  67. package/dist/harness/operation-trace.js.map +1 -0
  68. package/dist/harness/session-write-coordinator.d.ts +76 -0
  69. package/dist/harness/session-write-coordinator.d.ts.map +1 -0
  70. package/dist/harness/session-write-coordinator.js +126 -0
  71. package/dist/harness/session-write-coordinator.js.map +1 -0
  72. package/dist/harness/subscriber-fanout.d.ts +50 -0
  73. package/dist/harness/subscriber-fanout.d.ts.map +1 -0
  74. package/dist/harness/subscriber-fanout.js +92 -0
  75. package/dist/harness/subscriber-fanout.js.map +1 -0
  76. package/dist/harness/tree-navigation.d.ts +45 -0
  77. package/dist/harness/tree-navigation.d.ts.map +1 -0
  78. package/dist/harness/tree-navigation.js +59 -0
  79. package/dist/harness/tree-navigation.js.map +1 -0
  80. package/dist/harness/types.d.ts +34 -3
  81. package/dist/harness/types.d.ts.map +1 -1
  82. package/dist/harness/types.js.map +1 -1
  83. package/dist/index.d.ts +2 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +2 -0
  86. package/dist/index.js.map +1 -1
  87. package/dist/listener-delivery.d.ts +22 -0
  88. package/dist/listener-delivery.d.ts.map +1 -0
  89. package/dist/listener-delivery.js +36 -0
  90. package/dist/listener-delivery.js.map +1 -0
  91. package/package.json +4 -3
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Pure operation-lifecycle model for AgentHarness public operations.
3
+ *
4
+ * One public operation (`prompt`, `skill`, `promptFromTemplate`, `compact`,
5
+ * `navigateTree`) may span several low-level agent attempts. This module
6
+ * declares the identity, state, command, and violation vocabulary used to
7
+ * keep that distinction provable. It imports nothing: no harness, session,
8
+ * provider, or Node types, so it stays a leaf for the import-cycle ratchet
9
+ * and is safe to export from a browser entry point.
10
+ *
11
+ * Reducer-purity note: the reducer never reads a clock, so its `settling`
12
+ * state carries `HarnessSettledAttempt` records (attempt + outcome) rather
13
+ * than wall-clock summaries. The controller, which owns the clock, renders
14
+ * public `HarnessAttemptSummary` values from those records.
15
+ */
16
+ /** Public operation kinds that own the harness at most one at a time. */
17
+ export type HarnessOperationKind = "prompt" | "skill" | "prompt_template" | "manual_compaction" | "tree_navigation";
18
+ /** Prompt-family kinds run agent attempts; structural kinds do not. */
19
+ export declare const PROMPT_FAMILY_KINDS: readonly HarnessOperationKind[];
20
+ export type HarnessOperationStage = "preparing" | "attempt_running" | "save_point" | "recovering_overflow" | "structural_running" | "committing" | "settling";
21
+ /** Correlation identity of one public operation. `sequence` is monotonic per harness instance. */
22
+ export interface HarnessOperationRef {
23
+ readonly operationId: string;
24
+ readonly sequence: number;
25
+ readonly kind: HarnessOperationKind;
26
+ readonly startedAtMs: number;
27
+ }
28
+ /** Overflow recovery is a substage of the originating prompt operation, never its own operation. */
29
+ export type HarnessAttemptReason = "initial" | "context_overflow_recovery";
30
+ export interface HarnessAttemptRef {
31
+ readonly operationId: string;
32
+ readonly attemptId: string;
33
+ readonly index: number;
34
+ readonly reason: HarnessAttemptReason;
35
+ readonly startedAtMs: number;
36
+ }
37
+ export type HarnessAttemptOutcome = "completed" | "failed" | "aborted" | "overflow";
38
+ /** Reducer-side record of a finished attempt; carries no wall-clock data beyond what the attempt ref captured. */
39
+ export interface HarnessSettledAttempt {
40
+ readonly attempt: HarnessAttemptRef;
41
+ readonly outcome: HarnessAttemptOutcome;
42
+ }
43
+ /** Public attempt summary rendered by the controller from settled records plus its own clock readings. */
44
+ export interface HarnessAttemptSummary {
45
+ readonly attemptId: string;
46
+ readonly index: number;
47
+ readonly reason: HarnessAttemptReason;
48
+ readonly outcome: HarnessAttemptOutcome;
49
+ readonly startedAtMs: number;
50
+ readonly finishedAtMs: number;
51
+ }
52
+ export type HarnessOperationOutcome = {
53
+ readonly status: "completed";
54
+ } | {
55
+ readonly status: "failed";
56
+ readonly code: string;
57
+ readonly message: string;
58
+ } | {
59
+ readonly status: "aborted";
60
+ readonly reason?: string;
61
+ } | {
62
+ readonly status: "cancelled";
63
+ readonly reason: string;
64
+ };
65
+ export type HarnessLifecycleState = {
66
+ readonly tag: "idle";
67
+ readonly lastSequence: number;
68
+ } | {
69
+ readonly tag: "active";
70
+ readonly operation: HarnessOperationRef;
71
+ readonly stage: HarnessOperationStage;
72
+ readonly attempt?: HarnessAttemptRef;
73
+ readonly attempts: readonly HarnessSettledAttempt[];
74
+ readonly abortRequested: boolean;
75
+ } | {
76
+ readonly tag: "settling";
77
+ readonly operation: HarnessOperationRef;
78
+ readonly outcome: HarnessOperationOutcome;
79
+ readonly attempts: readonly HarnessSettledAttempt[];
80
+ readonly abortRequested: boolean;
81
+ };
82
+ export type HarnessLifecycleCommand = {
83
+ readonly type: "begin";
84
+ readonly operation: HarnessOperationRef;
85
+ } | {
86
+ readonly type: "stage";
87
+ readonly operationId: string;
88
+ readonly stage: HarnessOperationStage;
89
+ } | {
90
+ readonly type: "attempt_begin";
91
+ readonly attempt: HarnessAttemptRef;
92
+ } | {
93
+ readonly type: "attempt_end";
94
+ readonly attemptId: string;
95
+ readonly outcome: HarnessAttemptOutcome;
96
+ } | {
97
+ readonly type: "abort_request";
98
+ readonly operationId: string;
99
+ } | {
100
+ readonly type: "settle_begin";
101
+ readonly operationId: string;
102
+ readonly outcome: HarnessOperationOutcome;
103
+ } | {
104
+ readonly type: "settle_finish";
105
+ readonly operationId: string;
106
+ };
107
+ export type HarnessLifecycleViolationCode = "busy" | "stale_operation" | "invalid_transition" | "attempt_mismatch" | "sequence_violation";
108
+ /**
109
+ * Illegal lifecycle transition rejected by the reducer. Extends `Error` so the
110
+ * controller can attach it as a preserved `cause` on public boundary errors.
111
+ */
112
+ export declare class HarnessLifecycleViolation extends Error {
113
+ readonly code: HarnessLifecycleViolationCode;
114
+ readonly state: HarnessLifecycleState;
115
+ readonly command: HarnessLifecycleCommand;
116
+ constructor(code: HarnessLifecycleViolationCode, message: string, state: HarnessLifecycleState, command: HarnessLifecycleCommand);
117
+ }
118
+ export type HarnessLifecycleResult<T> = {
119
+ readonly ok: true;
120
+ readonly value: T;
121
+ } | {
122
+ readonly ok: false;
123
+ readonly error: HarnessLifecycleViolation;
124
+ };
125
+ /** Clock and identity factories injected so lifecycle behavior is deterministic under test. */
126
+ export interface HarnessLifecycleDependencies {
127
+ readonly createOperationId: () => string;
128
+ readonly now: () => number;
129
+ }
130
+ //# sourceMappingURL=operation-lifecycle-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-lifecycle-types.d.ts","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure operation-lifecycle model for AgentHarness public operations.\n *\n * One public operation (`prompt`, `skill`, `promptFromTemplate`, `compact`,\n * `navigateTree`) may span several low-level agent attempts. This module\n * declares the identity, state, command, and violation vocabulary used to\n * keep that distinction provable. It imports nothing: no harness, session,\n * provider, or Node types, so it stays a leaf for the import-cycle ratchet\n * and is safe to export from a browser entry point.\n *\n * Reducer-purity note: the reducer never reads a clock, so its `settling`\n * state carries `HarnessSettledAttempt` records (attempt + outcome) rather\n * than wall-clock summaries. The controller, which owns the clock, renders\n * public `HarnessAttemptSummary` values from those records.\n */\n\n/** Public operation kinds that own the harness at most one at a time. */\nexport type HarnessOperationKind = \"prompt\" | \"skill\" | \"prompt_template\" | \"manual_compaction\" | \"tree_navigation\";\n\n/** Prompt-family kinds run agent attempts; structural kinds do not. */\nexport const PROMPT_FAMILY_KINDS: readonly HarnessOperationKind[] = [\"prompt\", \"skill\", \"prompt_template\"];\n\nexport type HarnessOperationStage =\n\t| \"preparing\"\n\t| \"attempt_running\"\n\t| \"save_point\"\n\t| \"recovering_overflow\"\n\t| \"structural_running\"\n\t| \"committing\"\n\t| \"settling\";\n\n/** Correlation identity of one public operation. `sequence` is monotonic per harness instance. */\nexport interface HarnessOperationRef {\n\treadonly operationId: string;\n\treadonly sequence: number;\n\treadonly kind: HarnessOperationKind;\n\treadonly startedAtMs: number;\n}\n\n/** Overflow recovery is a substage of the originating prompt operation, never its own operation. */\nexport type HarnessAttemptReason = \"initial\" | \"context_overflow_recovery\";\n\nexport interface HarnessAttemptRef {\n\treadonly operationId: string;\n\treadonly attemptId: string;\n\treadonly index: number;\n\treadonly reason: HarnessAttemptReason;\n\treadonly startedAtMs: number;\n}\n\nexport type HarnessAttemptOutcome = \"completed\" | \"failed\" | \"aborted\" | \"overflow\";\n\n/** Reducer-side record of a finished attempt; carries no wall-clock data beyond what the attempt ref captured. */\nexport interface HarnessSettledAttempt {\n\treadonly attempt: HarnessAttemptRef;\n\treadonly outcome: HarnessAttemptOutcome;\n}\n\n/** Public attempt summary rendered by the controller from settled records plus its own clock readings. */\nexport interface HarnessAttemptSummary {\n\treadonly attemptId: string;\n\treadonly index: number;\n\treadonly reason: HarnessAttemptReason;\n\treadonly outcome: HarnessAttemptOutcome;\n\treadonly startedAtMs: number;\n\treadonly finishedAtMs: number;\n}\n\nexport type HarnessOperationOutcome =\n\t| { readonly status: \"completed\" }\n\t| { readonly status: \"failed\"; readonly code: string; readonly message: string }\n\t| { readonly status: \"aborted\"; readonly reason?: string }\n\t| { readonly status: \"cancelled\"; readonly reason: string };\n\nexport type HarnessLifecycleState =\n\t| { readonly tag: \"idle\"; readonly lastSequence: number }\n\t| {\n\t\t\treadonly tag: \"active\";\n\t\t\treadonly operation: HarnessOperationRef;\n\t\t\treadonly stage: HarnessOperationStage;\n\t\t\treadonly attempt?: HarnessAttemptRef;\n\t\t\treadonly attempts: readonly HarnessSettledAttempt[];\n\t\t\treadonly abortRequested: boolean;\n\t }\n\t| {\n\t\t\treadonly tag: \"settling\";\n\t\t\treadonly operation: HarnessOperationRef;\n\t\t\treadonly outcome: HarnessOperationOutcome;\n\t\t\treadonly attempts: readonly HarnessSettledAttempt[];\n\t\t\treadonly abortRequested: boolean;\n\t };\n\nexport type HarnessLifecycleCommand =\n\t| { readonly type: \"begin\"; readonly operation: HarnessOperationRef }\n\t| { readonly type: \"stage\"; readonly operationId: string; readonly stage: HarnessOperationStage }\n\t| { readonly type: \"attempt_begin\"; readonly attempt: HarnessAttemptRef }\n\t| { readonly type: \"attempt_end\"; readonly attemptId: string; readonly outcome: HarnessAttemptOutcome }\n\t| { readonly type: \"abort_request\"; readonly operationId: string }\n\t| { readonly type: \"settle_begin\"; readonly operationId: string; readonly outcome: HarnessOperationOutcome }\n\t| { readonly type: \"settle_finish\"; readonly operationId: string };\n\nexport type HarnessLifecycleViolationCode =\n\t| \"busy\"\n\t| \"stale_operation\"\n\t| \"invalid_transition\"\n\t| \"attempt_mismatch\"\n\t| \"sequence_violation\";\n\n/**\n * Illegal lifecycle transition rejected by the reducer. Extends `Error` so the\n * controller can attach it as a preserved `cause` on public boundary errors.\n */\nexport class HarnessLifecycleViolation extends Error {\n\tpublic readonly code: HarnessLifecycleViolationCode;\n\tpublic readonly state: HarnessLifecycleState;\n\tpublic readonly command: HarnessLifecycleCommand;\n\n\tconstructor(\n\t\tcode: HarnessLifecycleViolationCode,\n\t\tmessage: string,\n\t\tstate: HarnessLifecycleState,\n\t\tcommand: HarnessLifecycleCommand,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"HarnessLifecycleViolation\";\n\t\tthis.code = code;\n\t\tthis.state = state;\n\t\tthis.command = command;\n\t}\n}\n\nexport type HarnessLifecycleResult<T> =\n\t| { readonly ok: true; readonly value: T }\n\t| { readonly ok: false; readonly error: HarnessLifecycleViolation };\n\n/** Clock and identity factories injected so lifecycle behavior is deterministic under test. */\nexport interface HarnessLifecycleDependencies {\n\treadonly createOperationId: () => string;\n\treadonly now: () => number;\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pure operation-lifecycle model for AgentHarness public operations.
3
+ *
4
+ * One public operation (`prompt`, `skill`, `promptFromTemplate`, `compact`,
5
+ * `navigateTree`) may span several low-level agent attempts. This module
6
+ * declares the identity, state, command, and violation vocabulary used to
7
+ * keep that distinction provable. It imports nothing: no harness, session,
8
+ * provider, or Node types, so it stays a leaf for the import-cycle ratchet
9
+ * and is safe to export from a browser entry point.
10
+ *
11
+ * Reducer-purity note: the reducer never reads a clock, so its `settling`
12
+ * state carries `HarnessSettledAttempt` records (attempt + outcome) rather
13
+ * than wall-clock summaries. The controller, which owns the clock, renders
14
+ * public `HarnessAttemptSummary` values from those records.
15
+ */
16
+ /** Prompt-family kinds run agent attempts; structural kinds do not. */
17
+ export const PROMPT_FAMILY_KINDS = ["prompt", "skill", "prompt_template"];
18
+ /**
19
+ * Illegal lifecycle transition rejected by the reducer. Extends `Error` so the
20
+ * controller can attach it as a preserved `cause` on public boundary errors.
21
+ */
22
+ export class HarnessLifecycleViolation extends Error {
23
+ code;
24
+ state;
25
+ command;
26
+ constructor(code, message, state, command) {
27
+ super(message);
28
+ this.name = "HarnessLifecycleViolation";
29
+ this.code = code;
30
+ this.state = state;
31
+ this.command = command;
32
+ }
33
+ }
34
+ //# sourceMappingURL=operation-lifecycle-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-lifecycle-types.js","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,uEAAuE;AACvE,MAAM,CAAC,MAAM,mBAAmB,GAAoC,CAAC,QAAQ,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC;AAwF3G;;;GAGG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IACnC,IAAI,CAAgC;IACpC,KAAK,CAAwB;IAC7B,OAAO,CAA0B;IAEjD,YACC,IAAmC,EACnC,OAAe,EACf,KAA4B,EAC5B,OAAgC,EAC/B;QACD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAAA,CACvB;CACD","sourcesContent":["/**\n * Pure operation-lifecycle model for AgentHarness public operations.\n *\n * One public operation (`prompt`, `skill`, `promptFromTemplate`, `compact`,\n * `navigateTree`) may span several low-level agent attempts. This module\n * declares the identity, state, command, and violation vocabulary used to\n * keep that distinction provable. It imports nothing: no harness, session,\n * provider, or Node types, so it stays a leaf for the import-cycle ratchet\n * and is safe to export from a browser entry point.\n *\n * Reducer-purity note: the reducer never reads a clock, so its `settling`\n * state carries `HarnessSettledAttempt` records (attempt + outcome) rather\n * than wall-clock summaries. The controller, which owns the clock, renders\n * public `HarnessAttemptSummary` values from those records.\n */\n\n/** Public operation kinds that own the harness at most one at a time. */\nexport type HarnessOperationKind = \"prompt\" | \"skill\" | \"prompt_template\" | \"manual_compaction\" | \"tree_navigation\";\n\n/** Prompt-family kinds run agent attempts; structural kinds do not. */\nexport const PROMPT_FAMILY_KINDS: readonly HarnessOperationKind[] = [\"prompt\", \"skill\", \"prompt_template\"];\n\nexport type HarnessOperationStage =\n\t| \"preparing\"\n\t| \"attempt_running\"\n\t| \"save_point\"\n\t| \"recovering_overflow\"\n\t| \"structural_running\"\n\t| \"committing\"\n\t| \"settling\";\n\n/** Correlation identity of one public operation. `sequence` is monotonic per harness instance. */\nexport interface HarnessOperationRef {\n\treadonly operationId: string;\n\treadonly sequence: number;\n\treadonly kind: HarnessOperationKind;\n\treadonly startedAtMs: number;\n}\n\n/** Overflow recovery is a substage of the originating prompt operation, never its own operation. */\nexport type HarnessAttemptReason = \"initial\" | \"context_overflow_recovery\";\n\nexport interface HarnessAttemptRef {\n\treadonly operationId: string;\n\treadonly attemptId: string;\n\treadonly index: number;\n\treadonly reason: HarnessAttemptReason;\n\treadonly startedAtMs: number;\n}\n\nexport type HarnessAttemptOutcome = \"completed\" | \"failed\" | \"aborted\" | \"overflow\";\n\n/** Reducer-side record of a finished attempt; carries no wall-clock data beyond what the attempt ref captured. */\nexport interface HarnessSettledAttempt {\n\treadonly attempt: HarnessAttemptRef;\n\treadonly outcome: HarnessAttemptOutcome;\n}\n\n/** Public attempt summary rendered by the controller from settled records plus its own clock readings. */\nexport interface HarnessAttemptSummary {\n\treadonly attemptId: string;\n\treadonly index: number;\n\treadonly reason: HarnessAttemptReason;\n\treadonly outcome: HarnessAttemptOutcome;\n\treadonly startedAtMs: number;\n\treadonly finishedAtMs: number;\n}\n\nexport type HarnessOperationOutcome =\n\t| { readonly status: \"completed\" }\n\t| { readonly status: \"failed\"; readonly code: string; readonly message: string }\n\t| { readonly status: \"aborted\"; readonly reason?: string }\n\t| { readonly status: \"cancelled\"; readonly reason: string };\n\nexport type HarnessLifecycleState =\n\t| { readonly tag: \"idle\"; readonly lastSequence: number }\n\t| {\n\t\t\treadonly tag: \"active\";\n\t\t\treadonly operation: HarnessOperationRef;\n\t\t\treadonly stage: HarnessOperationStage;\n\t\t\treadonly attempt?: HarnessAttemptRef;\n\t\t\treadonly attempts: readonly HarnessSettledAttempt[];\n\t\t\treadonly abortRequested: boolean;\n\t }\n\t| {\n\t\t\treadonly tag: \"settling\";\n\t\t\treadonly operation: HarnessOperationRef;\n\t\t\treadonly outcome: HarnessOperationOutcome;\n\t\t\treadonly attempts: readonly HarnessSettledAttempt[];\n\t\t\treadonly abortRequested: boolean;\n\t };\n\nexport type HarnessLifecycleCommand =\n\t| { readonly type: \"begin\"; readonly operation: HarnessOperationRef }\n\t| { readonly type: \"stage\"; readonly operationId: string; readonly stage: HarnessOperationStage }\n\t| { readonly type: \"attempt_begin\"; readonly attempt: HarnessAttemptRef }\n\t| { readonly type: \"attempt_end\"; readonly attemptId: string; readonly outcome: HarnessAttemptOutcome }\n\t| { readonly type: \"abort_request\"; readonly operationId: string }\n\t| { readonly type: \"settle_begin\"; readonly operationId: string; readonly outcome: HarnessOperationOutcome }\n\t| { readonly type: \"settle_finish\"; readonly operationId: string };\n\nexport type HarnessLifecycleViolationCode =\n\t| \"busy\"\n\t| \"stale_operation\"\n\t| \"invalid_transition\"\n\t| \"attempt_mismatch\"\n\t| \"sequence_violation\";\n\n/**\n * Illegal lifecycle transition rejected by the reducer. Extends `Error` so the\n * controller can attach it as a preserved `cause` on public boundary errors.\n */\nexport class HarnessLifecycleViolation extends Error {\n\tpublic readonly code: HarnessLifecycleViolationCode;\n\tpublic readonly state: HarnessLifecycleState;\n\tpublic readonly command: HarnessLifecycleCommand;\n\n\tconstructor(\n\t\tcode: HarnessLifecycleViolationCode,\n\t\tmessage: string,\n\t\tstate: HarnessLifecycleState,\n\t\tcommand: HarnessLifecycleCommand,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"HarnessLifecycleViolation\";\n\t\tthis.code = code;\n\t\tthis.state = state;\n\t\tthis.command = command;\n\t}\n}\n\nexport type HarnessLifecycleResult<T> =\n\t| { readonly ok: true; readonly value: T }\n\t| { readonly ok: false; readonly error: HarnessLifecycleViolation };\n\n/** Clock and identity factories injected so lifecycle behavior is deterministic under test. */\nexport interface HarnessLifecycleDependencies {\n\treadonly createOperationId: () => string;\n\treadonly now: () => number;\n}\n"]}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Pure outcome classification and error aggregation for harness operations.
3
+ *
4
+ * Everything here is a total function over already-observed results: it never
5
+ * touches lifecycle state, sessions, providers, or clocks. Keeping the rules in
6
+ * one leaf module makes the precedence auditable in isolation and keeps
7
+ * `agent-harness.ts` free of the branch-heavy classification tables.
8
+ */
9
+ import { type AssistantMessage } from "omk-ai";
10
+ import type { HarnessAttemptOutcome, HarnessOperationOutcome } from "./operation-lifecycle-types.ts";
11
+ import type { NavigateTreeResult } from "./types.ts";
12
+ import { AgentHarnessError } from "./types.ts";
13
+ /**
14
+ * True only for an error that *is* an abort, not merely an error raised while
15
+ * an abort signal happened to be up. `AgentHarnessError` has no "aborted" code,
16
+ * so an explicit abort reaches us as a subsystem error carrying code "aborted"
17
+ * or as a DOM-style `AbortError`.
18
+ */
19
+ export declare function isExplicitAbortError(error: unknown): boolean;
20
+ /** Map a subsystem failure onto the harness' stable top-level classification. */
21
+ export declare function normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError["code"]): AgentHarnessError;
22
+ /** Result-based outcome for prompt-family operations that resolve with a failure/abort assistant message. */
23
+ export declare function classifyAssistantOutcome(message: AssistantMessage): HarnessOperationOutcome | undefined;
24
+ /** Structural cancellation is a distinct, non-failure terminal outcome. */
25
+ export declare function classifyNavigateTreeOutcome(result: NavigateTreeResult): HarnessOperationOutcome;
26
+ /** A thrown attempt body is an aborted attempt only when the error itself is an abort. */
27
+ export declare function classifyAttemptFailure(error: unknown): HarnessAttemptOutcome;
28
+ /** Context overflow is a recoverable attempt outcome, not an attempt failure. */
29
+ export declare function classifyAttemptOutcome(message: AssistantMessage, contextWindow: number | undefined): HarnessAttemptOutcome;
30
+ /**
31
+ * Single outcome-precedence rule for every public operation:
32
+ *
33
+ * session persistence failure > non-abort body/hook failure >
34
+ * explicit abort > result-classified outcome > completed
35
+ *
36
+ * A raised abort signal alone never downgrades another failure to "aborted":
37
+ * only an error that *is* an abort does. Otherwise a flush failure during an
38
+ * aborted turn would settle as "aborted" while the public promise rejected
39
+ * with "session".
40
+ */
41
+ export declare function resolveOperationOutcome<T>(input: {
42
+ readonly signalAborted: boolean;
43
+ readonly result: T | undefined;
44
+ readonly bodyError: unknown;
45
+ readonly flushError: unknown;
46
+ readonly classifyResult: ((result: T) => HarnessOperationOutcome | undefined) | undefined;
47
+ readonly fallbackCode: AgentHarnessError["code"];
48
+ }): HarnessOperationOutcome;
49
+ /**
50
+ * Run boundary steps in order and collect their errors instead of stopping at
51
+ * the first. A boundary that must still report, flush, or settle after one step
52
+ * fails uses this so one failure cannot strand the rest.
53
+ */
54
+ export declare function collectStepErrors(steps: ReadonlyArray<() => Promise<void> | void>): Promise<Error[]>;
55
+ /**
56
+ * Which error a public operation rejects with, or `undefined` on success.
57
+ *
58
+ * The top-level code comes from the same `flush > body` source the recorded
59
+ * outcome uses, so `outcome.code === rejection.code` for every failed outcome;
60
+ * settlement only ever contributes a cause. Every concurrent cause stays
61
+ * reachable through one `AggregateError` in body, flush, settle order, so an
62
+ * audit can still see that, say, the body and the final flush failed together.
63
+ */
64
+ export declare function resolveOperationFailure(input: {
65
+ readonly bodyError: unknown;
66
+ readonly flushError: unknown;
67
+ readonly settleError: unknown;
68
+ readonly fallbackCode: AgentHarnessError["code"];
69
+ }): AgentHarnessError | undefined;
70
+ /**
71
+ * The error a boundary should throw after several steps may have failed, or
72
+ * `undefined` when none did. A single failure is returned untouched so its
73
+ * own classification survives; several are kept reachable through one
74
+ * `AggregateError`, classified by the first (primary) failure. This is what
75
+ * lets a failing boundary flush report *alongside* the body or listener error
76
+ * it followed instead of erasing it.
77
+ */
78
+ export declare function combineBoundaryErrors(errors: readonly unknown[], message: string, fallbackCode: AgentHarnessError["code"]): unknown;
79
+ //# sourceMappingURL=operation-outcome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-outcome.d.ts","sourceRoot":"","sources":["../../src/harness/operation-outcome.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,QAAQ,CAAC;AAClE,OAAO,KAAK,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACrG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAA8D,MAAM,YAAY,CAAC;AAE3G;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAI5D;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,GAAG,iBAAiB,CAOhH;AAED,6GAA6G;AAC7G,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,gBAAgB,GAAG,uBAAuB,GAAG,SAAS,CAMvG;AAED,2EAA2E;AAC3E,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,kBAAkB,GAAG,uBAAuB,CAE/F;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAE5E;AAED,iFAAiF;AACjF,wBAAgB,sBAAsB,CACrC,OAAO,EAAE,gBAAgB,EACzB,aAAa,EAAE,MAAM,GAAG,SAAS,GAC/B,qBAAqB,CAKvB;AA2BD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE;IACjD,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,uBAAuB,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IAC1F,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CACjD,GAAG,uBAAuB,CAa1B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAU1G;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CACjD,GAAG,iBAAiB,GAAG,SAAS,CAiBhC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,SAAS,OAAO,EAAE,EAC1B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,GACrC,OAAO,CAKT","sourcesContent":["/**\n * Pure outcome classification and error aggregation for harness operations.\n *\n * Everything here is a total function over already-observed results: it never\n * touches lifecycle state, sessions, providers, or clocks. Keeping the rules in\n * one leaf module makes the precedence auditable in isolation and keeps\n * `agent-harness.ts` free of the branch-heavy classification tables.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"omk-ai\";\nimport type { HarnessAttemptOutcome, HarnessOperationOutcome } from \"./operation-lifecycle-types.ts\";\nimport type { NavigateTreeResult } from \"./types.ts\";\nimport { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from \"./types.ts\";\n\n/**\n * True only for an error that *is* an abort, not merely an error raised while\n * an abort signal happened to be up. `AgentHarnessError` has no \"aborted\" code,\n * so an explicit abort reaches us as a subsystem error carrying code \"aborted\"\n * or as a DOM-style `AbortError`.\n */\nexport function isExplicitAbortError(error: unknown): boolean {\n\tconst cause = toError(error);\n\tif (cause.name === \"AbortError\") return true;\n\treturn (cause instanceof CompactionError || cause instanceof BranchSummaryError) && cause.code === \"aborted\";\n}\n\n/** Map a subsystem failure onto the harness' stable top-level classification. */\nexport function normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError[\"code\"]): AgentHarnessError {\n\tif (error instanceof AgentHarnessError) return error;\n\tconst cause = toError(error);\n\tif (cause instanceof SessionError) return new AgentHarnessError(\"session\", cause.message, cause);\n\tif (cause instanceof CompactionError) return new AgentHarnessError(\"compaction\", cause.message, cause);\n\tif (cause instanceof BranchSummaryError) return new AgentHarnessError(\"branch_summary\", cause.message, cause);\n\treturn new AgentHarnessError(fallbackCode, cause.message, cause);\n}\n\n/** Result-based outcome for prompt-family operations that resolve with a failure/abort assistant message. */\nexport function classifyAssistantOutcome(message: AssistantMessage): HarnessOperationOutcome | undefined {\n\tif (message.stopReason === \"aborted\") return { status: \"aborted\" };\n\tif (message.stopReason === \"error\") {\n\t\treturn { status: \"failed\", code: \"provider\", message: message.errorMessage ?? \"Provider error\" };\n\t}\n\treturn undefined;\n}\n\n/** Structural cancellation is a distinct, non-failure terminal outcome. */\nexport function classifyNavigateTreeOutcome(result: NavigateTreeResult): HarnessOperationOutcome {\n\treturn result.cancelled ? { status: \"cancelled\", reason: \"tree_navigation_cancelled\" } : { status: \"completed\" };\n}\n\n/** A thrown attempt body is an aborted attempt only when the error itself is an abort. */\nexport function classifyAttemptFailure(error: unknown): HarnessAttemptOutcome {\n\treturn isExplicitAbortError(error) ? \"aborted\" : \"failed\";\n}\n\n/** Context overflow is a recoverable attempt outcome, not an attempt failure. */\nexport function classifyAttemptOutcome(\n\tmessage: AssistantMessage,\n\tcontextWindow: number | undefined,\n): HarnessAttemptOutcome {\n\tif (message.stopReason === \"aborted\") return \"aborted\";\n\tif (isContextOverflow(message, contextWindow)) return \"overflow\";\n\tif (message.stopReason === \"error\") return \"failed\";\n\treturn \"completed\";\n}\n\n/**\n * The single classification source for a failed operation: `flush > body`.\n *\n * Session persistence outranks the body because a flush failure after a provider\n * success must never record or report a completed operation, and a flush error\n * that already carries a harness classification (e.g. an `invalid_state`\n * coordinator reentry) keeps it. Both the recorded outcome and the public\n * rejection read their top-level code from here, so the two can never disagree.\n * Settlement is not a source: it runs after the outcome is recorded, so it can\n * only add a cause and a rejection.\n */\nfunction classificationSource(input: {\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}):\n\t| { readonly stage: \"body\" | \"flush\"; readonly error: unknown; readonly fallbackCode: AgentHarnessError[\"code\"] }\n\t| undefined {\n\tif (input.flushError !== undefined) return { stage: \"flush\", error: input.flushError, fallbackCode: \"session\" };\n\tif (input.bodyError !== undefined) {\n\t\treturn { stage: \"body\", error: input.bodyError, fallbackCode: input.fallbackCode };\n\t}\n\treturn undefined;\n}\n\n/**\n * Single outcome-precedence rule for every public operation:\n *\n * session persistence failure > non-abort body/hook failure >\n * explicit abort > result-classified outcome > completed\n *\n * A raised abort signal alone never downgrades another failure to \"aborted\":\n * only an error that *is* an abort does. Otherwise a flush failure during an\n * aborted turn would settle as \"aborted\" while the public promise rejected\n * with \"session\".\n */\nexport function resolveOperationOutcome<T>(input: {\n\treadonly signalAborted: boolean;\n\treadonly result: T | undefined;\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly classifyResult: ((result: T) => HarnessOperationOutcome | undefined) | undefined;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): HarnessOperationOutcome {\n\tconst source = classificationSource(input);\n\tif (source !== undefined) {\n\t\t// Only a body failure can be an abort; a flush or settle failure is a failure\n\t\t// even when the abort signal happens to be up.\n\t\tif (source.stage === \"body\" && isExplicitAbortError(source.error)) return { status: \"aborted\" };\n\t\tconst error = normalizeHarnessError(source.error, source.fallbackCode);\n\t\treturn { status: \"failed\", code: error.code, message: error.message };\n\t}\n\treturn (\n\t\tinput.classifyResult?.(input.result as T) ??\n\t\t(input.signalAborted ? { status: \"aborted\" } : { status: \"completed\" })\n\t);\n}\n\n/**\n * Run boundary steps in order and collect their errors instead of stopping at\n * the first. A boundary that must still report, flush, or settle after one step\n * fails uses this so one failure cannot strand the rest.\n */\nexport async function collectStepErrors(steps: ReadonlyArray<() => Promise<void> | void>): Promise<Error[]> {\n\tconst errors: Error[] = [];\n\tfor (const step of steps) {\n\t\ttry {\n\t\t\tawait step();\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t}\n\treturn errors;\n}\n\n/**\n * Which error a public operation rejects with, or `undefined` on success.\n *\n * The top-level code comes from the same `flush > body` source the recorded\n * outcome uses, so `outcome.code === rejection.code` for every failed outcome;\n * settlement only ever contributes a cause. Every concurrent cause stays\n * reachable through one `AggregateError` in body, flush, settle order, so an\n * audit can still see that, say, the body and the final flush failed together.\n */\nexport function resolveOperationFailure(input: {\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly settleError: unknown;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): AgentHarnessError | undefined {\n\tconst causes = [input.bodyError, input.flushError, input.settleError].filter((error) => error !== undefined);\n\tif (causes.length === 0) return undefined;\n\tconst source = classificationSource(input) ?? {\n\t\tstage: \"settle\" as const,\n\t\terror: input.settleError,\n\t\tfallbackCode: \"hook\" as const,\n\t};\n\tconst code = normalizeHarnessError(source.error, source.fallbackCode).code;\n\tif (causes.length === 1) return normalizeHarnessError(causes[0], source.fallbackCode);\n\tconst stages = [\n\t\tinput.bodyError !== undefined ? \"body\" : undefined,\n\t\tinput.flushError !== undefined ? \"final flush\" : undefined,\n\t\tinput.settleError !== undefined ? \"settlement\" : undefined,\n\t].filter((stage) => stage !== undefined);\n\tconst cause = new AggregateError(causes.map(toError), `Operation failed (${stages.join(\", \")})`);\n\treturn new AgentHarnessError(code, cause.message, cause);\n}\n\n/**\n * The error a boundary should throw after several steps may have failed, or\n * `undefined` when none did. A single failure is returned untouched so its\n * own classification survives; several are kept reachable through one\n * `AggregateError`, classified by the first (primary) failure. This is what\n * lets a failing boundary flush report *alongside* the body or listener error\n * it followed instead of erasing it.\n */\nexport function combineBoundaryErrors(\n\terrors: readonly unknown[],\n\tmessage: string,\n\tfallbackCode: AgentHarnessError[\"code\"],\n): unknown {\n\tconst present = errors.filter((error) => error !== undefined);\n\tif (present.length <= 1) return present[0];\n\tconst cause = new AggregateError(present.map(toError), message);\n\treturn new AgentHarnessError(normalizeHarnessError(present[0], fallbackCode).code, cause.message, cause);\n}\n"]}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Pure outcome classification and error aggregation for harness operations.
3
+ *
4
+ * Everything here is a total function over already-observed results: it never
5
+ * touches lifecycle state, sessions, providers, or clocks. Keeping the rules in
6
+ * one leaf module makes the precedence auditable in isolation and keeps
7
+ * `agent-harness.ts` free of the branch-heavy classification tables.
8
+ */
9
+ import { isContextOverflow } from "omk-ai";
10
+ import { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from "./types.js";
11
+ /**
12
+ * True only for an error that *is* an abort, not merely an error raised while
13
+ * an abort signal happened to be up. `AgentHarnessError` has no "aborted" code,
14
+ * so an explicit abort reaches us as a subsystem error carrying code "aborted"
15
+ * or as a DOM-style `AbortError`.
16
+ */
17
+ export function isExplicitAbortError(error) {
18
+ const cause = toError(error);
19
+ if (cause.name === "AbortError")
20
+ return true;
21
+ return (cause instanceof CompactionError || cause instanceof BranchSummaryError) && cause.code === "aborted";
22
+ }
23
+ /** Map a subsystem failure onto the harness' stable top-level classification. */
24
+ export function normalizeHarnessError(error, fallbackCode) {
25
+ if (error instanceof AgentHarnessError)
26
+ return error;
27
+ const cause = toError(error);
28
+ if (cause instanceof SessionError)
29
+ return new AgentHarnessError("session", cause.message, cause);
30
+ if (cause instanceof CompactionError)
31
+ return new AgentHarnessError("compaction", cause.message, cause);
32
+ if (cause instanceof BranchSummaryError)
33
+ return new AgentHarnessError("branch_summary", cause.message, cause);
34
+ return new AgentHarnessError(fallbackCode, cause.message, cause);
35
+ }
36
+ /** Result-based outcome for prompt-family operations that resolve with a failure/abort assistant message. */
37
+ export function classifyAssistantOutcome(message) {
38
+ if (message.stopReason === "aborted")
39
+ return { status: "aborted" };
40
+ if (message.stopReason === "error") {
41
+ return { status: "failed", code: "provider", message: message.errorMessage ?? "Provider error" };
42
+ }
43
+ return undefined;
44
+ }
45
+ /** Structural cancellation is a distinct, non-failure terminal outcome. */
46
+ export function classifyNavigateTreeOutcome(result) {
47
+ return result.cancelled ? { status: "cancelled", reason: "tree_navigation_cancelled" } : { status: "completed" };
48
+ }
49
+ /** A thrown attempt body is an aborted attempt only when the error itself is an abort. */
50
+ export function classifyAttemptFailure(error) {
51
+ return isExplicitAbortError(error) ? "aborted" : "failed";
52
+ }
53
+ /** Context overflow is a recoverable attempt outcome, not an attempt failure. */
54
+ export function classifyAttemptOutcome(message, contextWindow) {
55
+ if (message.stopReason === "aborted")
56
+ return "aborted";
57
+ if (isContextOverflow(message, contextWindow))
58
+ return "overflow";
59
+ if (message.stopReason === "error")
60
+ return "failed";
61
+ return "completed";
62
+ }
63
+ /**
64
+ * The single classification source for a failed operation: `flush > body`.
65
+ *
66
+ * Session persistence outranks the body because a flush failure after a provider
67
+ * success must never record or report a completed operation, and a flush error
68
+ * that already carries a harness classification (e.g. an `invalid_state`
69
+ * coordinator reentry) keeps it. Both the recorded outcome and the public
70
+ * rejection read their top-level code from here, so the two can never disagree.
71
+ * Settlement is not a source: it runs after the outcome is recorded, so it can
72
+ * only add a cause and a rejection.
73
+ */
74
+ function classificationSource(input) {
75
+ if (input.flushError !== undefined)
76
+ return { stage: "flush", error: input.flushError, fallbackCode: "session" };
77
+ if (input.bodyError !== undefined) {
78
+ return { stage: "body", error: input.bodyError, fallbackCode: input.fallbackCode };
79
+ }
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Single outcome-precedence rule for every public operation:
84
+ *
85
+ * session persistence failure > non-abort body/hook failure >
86
+ * explicit abort > result-classified outcome > completed
87
+ *
88
+ * A raised abort signal alone never downgrades another failure to "aborted":
89
+ * only an error that *is* an abort does. Otherwise a flush failure during an
90
+ * aborted turn would settle as "aborted" while the public promise rejected
91
+ * with "session".
92
+ */
93
+ export function resolveOperationOutcome(input) {
94
+ const source = classificationSource(input);
95
+ if (source !== undefined) {
96
+ // Only a body failure can be an abort; a flush or settle failure is a failure
97
+ // even when the abort signal happens to be up.
98
+ if (source.stage === "body" && isExplicitAbortError(source.error))
99
+ return { status: "aborted" };
100
+ const error = normalizeHarnessError(source.error, source.fallbackCode);
101
+ return { status: "failed", code: error.code, message: error.message };
102
+ }
103
+ return (input.classifyResult?.(input.result) ??
104
+ (input.signalAborted ? { status: "aborted" } : { status: "completed" }));
105
+ }
106
+ /**
107
+ * Run boundary steps in order and collect their errors instead of stopping at
108
+ * the first. A boundary that must still report, flush, or settle after one step
109
+ * fails uses this so one failure cannot strand the rest.
110
+ */
111
+ export async function collectStepErrors(steps) {
112
+ const errors = [];
113
+ for (const step of steps) {
114
+ try {
115
+ await step();
116
+ }
117
+ catch (error) {
118
+ errors.push(toError(error));
119
+ }
120
+ }
121
+ return errors;
122
+ }
123
+ /**
124
+ * Which error a public operation rejects with, or `undefined` on success.
125
+ *
126
+ * The top-level code comes from the same `flush > body` source the recorded
127
+ * outcome uses, so `outcome.code === rejection.code` for every failed outcome;
128
+ * settlement only ever contributes a cause. Every concurrent cause stays
129
+ * reachable through one `AggregateError` in body, flush, settle order, so an
130
+ * audit can still see that, say, the body and the final flush failed together.
131
+ */
132
+ export function resolveOperationFailure(input) {
133
+ const causes = [input.bodyError, input.flushError, input.settleError].filter((error) => error !== undefined);
134
+ if (causes.length === 0)
135
+ return undefined;
136
+ const source = classificationSource(input) ?? {
137
+ stage: "settle",
138
+ error: input.settleError,
139
+ fallbackCode: "hook",
140
+ };
141
+ const code = normalizeHarnessError(source.error, source.fallbackCode).code;
142
+ if (causes.length === 1)
143
+ return normalizeHarnessError(causes[0], source.fallbackCode);
144
+ const stages = [
145
+ input.bodyError !== undefined ? "body" : undefined,
146
+ input.flushError !== undefined ? "final flush" : undefined,
147
+ input.settleError !== undefined ? "settlement" : undefined,
148
+ ].filter((stage) => stage !== undefined);
149
+ const cause = new AggregateError(causes.map(toError), `Operation failed (${stages.join(", ")})`);
150
+ return new AgentHarnessError(code, cause.message, cause);
151
+ }
152
+ /**
153
+ * The error a boundary should throw after several steps may have failed, or
154
+ * `undefined` when none did. A single failure is returned untouched so its
155
+ * own classification survives; several are kept reachable through one
156
+ * `AggregateError`, classified by the first (primary) failure. This is what
157
+ * lets a failing boundary flush report *alongside* the body or listener error
158
+ * it followed instead of erasing it.
159
+ */
160
+ export function combineBoundaryErrors(errors, message, fallbackCode) {
161
+ const present = errors.filter((error) => error !== undefined);
162
+ if (present.length <= 1)
163
+ return present[0];
164
+ const cause = new AggregateError(present.map(toError), message);
165
+ return new AgentHarnessError(normalizeHarnessError(present[0], fallbackCode).code, cause.message, cause);
166
+ }
167
+ //# sourceMappingURL=operation-outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-outcome.js","sourceRoot":"","sources":["../../src/harness/operation-outcome.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure outcome classification and error aggregation for harness operations.\n *\n * Everything here is a total function over already-observed results: it never\n * touches lifecycle state, sessions, providers, or clocks. Keeping the rules in\n * one leaf module makes the precedence auditable in isolation and keeps\n * `agent-harness.ts` free of the branch-heavy classification tables.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"omk-ai\";\nimport type { HarnessAttemptOutcome, HarnessOperationOutcome } from \"./operation-lifecycle-types.ts\";\nimport type { NavigateTreeResult } from \"./types.ts\";\nimport { AgentHarnessError, BranchSummaryError, CompactionError, SessionError, toError } from \"./types.ts\";\n\n/**\n * True only for an error that *is* an abort, not merely an error raised while\n * an abort signal happened to be up. `AgentHarnessError` has no \"aborted\" code,\n * so an explicit abort reaches us as a subsystem error carrying code \"aborted\"\n * or as a DOM-style `AbortError`.\n */\nexport function isExplicitAbortError(error: unknown): boolean {\n\tconst cause = toError(error);\n\tif (cause.name === \"AbortError\") return true;\n\treturn (cause instanceof CompactionError || cause instanceof BranchSummaryError) && cause.code === \"aborted\";\n}\n\n/** Map a subsystem failure onto the harness' stable top-level classification. */\nexport function normalizeHarnessError(error: unknown, fallbackCode: AgentHarnessError[\"code\"]): AgentHarnessError {\n\tif (error instanceof AgentHarnessError) return error;\n\tconst cause = toError(error);\n\tif (cause instanceof SessionError) return new AgentHarnessError(\"session\", cause.message, cause);\n\tif (cause instanceof CompactionError) return new AgentHarnessError(\"compaction\", cause.message, cause);\n\tif (cause instanceof BranchSummaryError) return new AgentHarnessError(\"branch_summary\", cause.message, cause);\n\treturn new AgentHarnessError(fallbackCode, cause.message, cause);\n}\n\n/** Result-based outcome for prompt-family operations that resolve with a failure/abort assistant message. */\nexport function classifyAssistantOutcome(message: AssistantMessage): HarnessOperationOutcome | undefined {\n\tif (message.stopReason === \"aborted\") return { status: \"aborted\" };\n\tif (message.stopReason === \"error\") {\n\t\treturn { status: \"failed\", code: \"provider\", message: message.errorMessage ?? \"Provider error\" };\n\t}\n\treturn undefined;\n}\n\n/** Structural cancellation is a distinct, non-failure terminal outcome. */\nexport function classifyNavigateTreeOutcome(result: NavigateTreeResult): HarnessOperationOutcome {\n\treturn result.cancelled ? { status: \"cancelled\", reason: \"tree_navigation_cancelled\" } : { status: \"completed\" };\n}\n\n/** A thrown attempt body is an aborted attempt only when the error itself is an abort. */\nexport function classifyAttemptFailure(error: unknown): HarnessAttemptOutcome {\n\treturn isExplicitAbortError(error) ? \"aborted\" : \"failed\";\n}\n\n/** Context overflow is a recoverable attempt outcome, not an attempt failure. */\nexport function classifyAttemptOutcome(\n\tmessage: AssistantMessage,\n\tcontextWindow: number | undefined,\n): HarnessAttemptOutcome {\n\tif (message.stopReason === \"aborted\") return \"aborted\";\n\tif (isContextOverflow(message, contextWindow)) return \"overflow\";\n\tif (message.stopReason === \"error\") return \"failed\";\n\treturn \"completed\";\n}\n\n/**\n * The single classification source for a failed operation: `flush > body`.\n *\n * Session persistence outranks the body because a flush failure after a provider\n * success must never record or report a completed operation, and a flush error\n * that already carries a harness classification (e.g. an `invalid_state`\n * coordinator reentry) keeps it. Both the recorded outcome and the public\n * rejection read their top-level code from here, so the two can never disagree.\n * Settlement is not a source: it runs after the outcome is recorded, so it can\n * only add a cause and a rejection.\n */\nfunction classificationSource(input: {\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}):\n\t| { readonly stage: \"body\" | \"flush\"; readonly error: unknown; readonly fallbackCode: AgentHarnessError[\"code\"] }\n\t| undefined {\n\tif (input.flushError !== undefined) return { stage: \"flush\", error: input.flushError, fallbackCode: \"session\" };\n\tif (input.bodyError !== undefined) {\n\t\treturn { stage: \"body\", error: input.bodyError, fallbackCode: input.fallbackCode };\n\t}\n\treturn undefined;\n}\n\n/**\n * Single outcome-precedence rule for every public operation:\n *\n * session persistence failure > non-abort body/hook failure >\n * explicit abort > result-classified outcome > completed\n *\n * A raised abort signal alone never downgrades another failure to \"aborted\":\n * only an error that *is* an abort does. Otherwise a flush failure during an\n * aborted turn would settle as \"aborted\" while the public promise rejected\n * with \"session\".\n */\nexport function resolveOperationOutcome<T>(input: {\n\treadonly signalAborted: boolean;\n\treadonly result: T | undefined;\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly classifyResult: ((result: T) => HarnessOperationOutcome | undefined) | undefined;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): HarnessOperationOutcome {\n\tconst source = classificationSource(input);\n\tif (source !== undefined) {\n\t\t// Only a body failure can be an abort; a flush or settle failure is a failure\n\t\t// even when the abort signal happens to be up.\n\t\tif (source.stage === \"body\" && isExplicitAbortError(source.error)) return { status: \"aborted\" };\n\t\tconst error = normalizeHarnessError(source.error, source.fallbackCode);\n\t\treturn { status: \"failed\", code: error.code, message: error.message };\n\t}\n\treturn (\n\t\tinput.classifyResult?.(input.result as T) ??\n\t\t(input.signalAborted ? { status: \"aborted\" } : { status: \"completed\" })\n\t);\n}\n\n/**\n * Run boundary steps in order and collect their errors instead of stopping at\n * the first. A boundary that must still report, flush, or settle after one step\n * fails uses this so one failure cannot strand the rest.\n */\nexport async function collectStepErrors(steps: ReadonlyArray<() => Promise<void> | void>): Promise<Error[]> {\n\tconst errors: Error[] = [];\n\tfor (const step of steps) {\n\t\ttry {\n\t\t\tawait step();\n\t\t} catch (error) {\n\t\t\terrors.push(toError(error));\n\t\t}\n\t}\n\treturn errors;\n}\n\n/**\n * Which error a public operation rejects with, or `undefined` on success.\n *\n * The top-level code comes from the same `flush > body` source the recorded\n * outcome uses, so `outcome.code === rejection.code` for every failed outcome;\n * settlement only ever contributes a cause. Every concurrent cause stays\n * reachable through one `AggregateError` in body, flush, settle order, so an\n * audit can still see that, say, the body and the final flush failed together.\n */\nexport function resolveOperationFailure(input: {\n\treadonly bodyError: unknown;\n\treadonly flushError: unknown;\n\treadonly settleError: unknown;\n\treadonly fallbackCode: AgentHarnessError[\"code\"];\n}): AgentHarnessError | undefined {\n\tconst causes = [input.bodyError, input.flushError, input.settleError].filter((error) => error !== undefined);\n\tif (causes.length === 0) return undefined;\n\tconst source = classificationSource(input) ?? {\n\t\tstage: \"settle\" as const,\n\t\terror: input.settleError,\n\t\tfallbackCode: \"hook\" as const,\n\t};\n\tconst code = normalizeHarnessError(source.error, source.fallbackCode).code;\n\tif (causes.length === 1) return normalizeHarnessError(causes[0], source.fallbackCode);\n\tconst stages = [\n\t\tinput.bodyError !== undefined ? \"body\" : undefined,\n\t\tinput.flushError !== undefined ? \"final flush\" : undefined,\n\t\tinput.settleError !== undefined ? \"settlement\" : undefined,\n\t].filter((stage) => stage !== undefined);\n\tconst cause = new AggregateError(causes.map(toError), `Operation failed (${stages.join(\", \")})`);\n\treturn new AgentHarnessError(code, cause.message, cause);\n}\n\n/**\n * The error a boundary should throw after several steps may have failed, or\n * `undefined` when none did. A single failure is returned untouched so its\n * own classification survives; several are kept reachable through one\n * `AggregateError`, classified by the first (primary) failure. This is what\n * lets a failing boundary flush report *alongside* the body or listener error\n * it followed instead of erasing it.\n */\nexport function combineBoundaryErrors(\n\terrors: readonly unknown[],\n\tmessage: string,\n\tfallbackCode: AgentHarnessError[\"code\"],\n): unknown {\n\tconst present = errors.filter((error) => error !== undefined);\n\tif (present.length <= 1) return present[0];\n\tconst cause = new AggregateError(present.map(toError), message);\n\treturn new AgentHarnessError(normalizeHarnessError(present[0], fallbackCode).code, cause.message, cause);\n}\n"]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Trace comparison for shadow-mode runtime convergence (FND-003, Stage C).
3
+ *
4
+ * While `AgentHarness` runs as a shadow reducer beside the authoritative
5
+ * `AgentSession`, every scenario yields two traces. This module reduces each
6
+ * trace to a per-operation summary and classifies every difference as a
7
+ * `blocker` (the two runtimes disagree about lifecycle semantics) or a
8
+ * `non_blocker` (they took a different path to the same committed state).
9
+ *
10
+ * Blocker dimensions follow the plan verbatim: operation count and identity,
11
+ * the attempt set and how each attempt closed, terminal status and public
12
+ * error code, accepted deferred-command IDs in order, session-write order,
13
+ * and effect uncertainty left open at settlement. Stage paths and abort
14
+ * signalling without an outcome difference are informational only.
15
+ */
16
+ import { type OperationTraceEvent, type TraceOutcome } from "./operation-trace.ts";
17
+ export type TraceDivergenceClass = "blocker" | "non_blocker";
18
+ export type TraceDivergenceDimension = "operation_count" | "operation_identity" | "attempt_set" | "terminal_status" | "public_error_code" | "accepted_commands" | "session_write_order" | "effect_uncertainty" | "stage_path" | "abort_signal";
19
+ export interface TraceDivergence {
20
+ readonly dimension: TraceDivergenceDimension;
21
+ readonly class: TraceDivergenceClass;
22
+ readonly operationIndex?: number;
23
+ readonly left: string;
24
+ readonly right: string;
25
+ }
26
+ export interface TraceComparison {
27
+ readonly equal: boolean;
28
+ readonly leftDigest: string;
29
+ readonly rightDigest: string;
30
+ readonly blockers: readonly TraceDivergence[];
31
+ readonly nonBlockers: readonly TraceDivergence[];
32
+ }
33
+ export interface TraceAttemptSummary {
34
+ readonly attemptId: string;
35
+ readonly index: number;
36
+ readonly reason: string;
37
+ readonly outcome?: string;
38
+ }
39
+ /** Everything the comparison reads about one operation, in source order. */
40
+ export interface OperationTraceSummary {
41
+ readonly operationId: string;
42
+ readonly kind: string;
43
+ readonly sequence: number;
44
+ readonly stages: readonly string[];
45
+ readonly attempts: readonly TraceAttemptSummary[];
46
+ readonly abortRequested: boolean;
47
+ readonly settlement?: TraceOutcome;
48
+ readonly settled?: TraceOutcome;
49
+ /** Effects marked uncertain and not reconciled before the operation settled. */
50
+ readonly unresolvedEffectIds: readonly string[];
51
+ }
52
+ export interface TraceSummary {
53
+ readonly operations: readonly OperationTraceSummary[];
54
+ readonly acceptedCommandIds: readonly string[];
55
+ readonly sessionWrites: readonly string[];
56
+ }
57
+ export declare function summarizeTrace(events: readonly OperationTraceEvent[]): TraceSummary;
58
+ /** Compare two traces of the same scenario. Equal digests short-circuit to an empty report. */
59
+ export declare function compareOperationTraces(left: readonly OperationTraceEvent[], right: readonly OperationTraceEvent[]): TraceComparison;
60
+ //# sourceMappingURL=operation-trace-divergence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-trace-divergence.d.ts","sourceRoot":"","sources":["../../src/harness/operation-trace-divergence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAsB,KAAK,mBAAmB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEvG,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,aAAa,CAAC;AAE7D,MAAM,MAAM,wBAAwB,GACjC,iBAAiB,GACjB,oBAAoB,GACpB,aAAa,GACb,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,qBAAqB,GACrB,oBAAoB,GACpB,YAAY,GACZ,cAAc,CAAC;AAalB,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,SAAS,EAAE,wBAAwB,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;IACrC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAClD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,UAAU,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACtD,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,GAAG,YAAY,CAuFnF;AA6CD,+FAA+F;AAC/F,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,SAAS,mBAAmB,EAAE,EACpC,KAAK,EAAE,SAAS,mBAAmB,EAAE,GACnC,eAAe,CAoCjB","sourcesContent":["/**\n * Trace comparison for shadow-mode runtime convergence (FND-003, Stage C).\n *\n * While `AgentHarness` runs as a shadow reducer beside the authoritative\n * `AgentSession`, every scenario yields two traces. This module reduces each\n * trace to a per-operation summary and classifies every difference as a\n * `blocker` (the two runtimes disagree about lifecycle semantics) or a\n * `non_blocker` (they took a different path to the same committed state).\n *\n * Blocker dimensions follow the plan verbatim: operation count and identity,\n * the attempt set and how each attempt closed, terminal status and public\n * error code, accepted deferred-command IDs in order, session-write order,\n * and effect uncertainty left open at settlement. Stage paths and abort\n * signalling without an outcome difference are informational only.\n */\n\nimport { computeTraceDigest, type OperationTraceEvent, type TraceOutcome } from \"./operation-trace.ts\";\n\nexport type TraceDivergenceClass = \"blocker\" | \"non_blocker\";\n\nexport type TraceDivergenceDimension =\n\t| \"operation_count\"\n\t| \"operation_identity\"\n\t| \"attempt_set\"\n\t| \"terminal_status\"\n\t| \"public_error_code\"\n\t| \"accepted_commands\"\n\t| \"session_write_order\"\n\t| \"effect_uncertainty\"\n\t| \"stage_path\"\n\t| \"abort_signal\";\n\nconst BLOCKER_DIMENSIONS: ReadonlySet<TraceDivergenceDimension> = new Set([\n\t\"operation_count\",\n\t\"operation_identity\",\n\t\"attempt_set\",\n\t\"terminal_status\",\n\t\"public_error_code\",\n\t\"accepted_commands\",\n\t\"session_write_order\",\n\t\"effect_uncertainty\",\n]);\n\nexport interface TraceDivergence {\n\treadonly dimension: TraceDivergenceDimension;\n\treadonly class: TraceDivergenceClass;\n\treadonly operationIndex?: number;\n\treadonly left: string;\n\treadonly right: string;\n}\n\nexport interface TraceComparison {\n\treadonly equal: boolean;\n\treadonly leftDigest: string;\n\treadonly rightDigest: string;\n\treadonly blockers: readonly TraceDivergence[];\n\treadonly nonBlockers: readonly TraceDivergence[];\n}\n\nexport interface TraceAttemptSummary {\n\treadonly attemptId: string;\n\treadonly index: number;\n\treadonly reason: string;\n\treadonly outcome?: string;\n}\n\n/** Everything the comparison reads about one operation, in source order. */\nexport interface OperationTraceSummary {\n\treadonly operationId: string;\n\treadonly kind: string;\n\treadonly sequence: number;\n\treadonly stages: readonly string[];\n\treadonly attempts: readonly TraceAttemptSummary[];\n\treadonly abortRequested: boolean;\n\treadonly settlement?: TraceOutcome;\n\treadonly settled?: TraceOutcome;\n\t/** Effects marked uncertain and not reconciled before the operation settled. */\n\treadonly unresolvedEffectIds: readonly string[];\n}\n\nexport interface TraceSummary {\n\treadonly operations: readonly OperationTraceSummary[];\n\treadonly acceptedCommandIds: readonly string[];\n\treadonly sessionWrites: readonly string[];\n}\n\ninterface MutableOperation {\n\toperationId: string;\n\tkind: string;\n\tsequence: number;\n\tstages: string[];\n\tattempts: TraceAttemptSummary[];\n\tabortRequested: boolean;\n\tsettlement?: TraceOutcome;\n\tsettled?: TraceOutcome;\n\tunresolved: Set<string>;\n}\n\nexport function summarizeTrace(events: readonly OperationTraceEvent[]): TraceSummary {\n\tconst operations: MutableOperation[] = [];\n\tconst byId = new Map<string, MutableOperation>();\n\tconst acceptedCommandIds: string[] = [];\n\tconst sessionWrites: string[] = [];\n\tfor (const event of events) {\n\t\tswitch (event.type) {\n\t\t\tcase \"operation_started\": {\n\t\t\t\tconst operation: MutableOperation = {\n\t\t\t\t\toperationId: event.operationId,\n\t\t\t\t\tkind: event.kind,\n\t\t\t\t\tsequence: event.sequence,\n\t\t\t\t\tstages: [],\n\t\t\t\t\tattempts: [],\n\t\t\t\t\tabortRequested: false,\n\t\t\t\t\tunresolved: new Set(),\n\t\t\t\t};\n\t\t\t\toperations.push(operation);\n\t\t\t\tbyId.set(event.operationId, operation);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stage_changed\":\n\t\t\t\tbyId.get(event.operationId)?.stages.push(event.stage);\n\t\t\t\tbreak;\n\t\t\tcase \"attempt_started\":\n\t\t\t\tbyId\n\t\t\t\t\t.get(event.operationId)\n\t\t\t\t\t?.attempts.push({ attemptId: event.attemptId, index: event.index, reason: event.reason });\n\t\t\t\tbreak;\n\t\t\tcase \"attempt_finished\": {\n\t\t\t\tconst operation = byId.get(event.operationId);\n\t\t\t\tif (operation === undefined) break;\n\t\t\t\toperation.attempts = operation.attempts.map((attempt) =>\n\t\t\t\t\tattempt.attemptId === event.attemptId ? { ...attempt, outcome: event.outcome } : attempt,\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"abort_requested\": {\n\t\t\t\tconst operation = byId.get(event.operationId);\n\t\t\t\tif (operation !== undefined) operation.abortRequested = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"effect_uncertain\":\n\t\t\t\tbyId.get(event.operationId)?.unresolved.add(event.effectId);\n\t\t\t\tbreak;\n\t\t\tcase \"effect_reconciled\":\n\t\t\t\tbyId.get(event.operationId)?.unresolved.delete(event.effectId);\n\t\t\t\tbreak;\n\t\t\tcase \"effect_prepared\":\n\t\t\t\tbreak;\n\t\t\tcase \"deferred_command_accepted\":\n\t\t\t\tacceptedCommandIds.push(event.commandId);\n\t\t\t\tbreak;\n\t\t\tcase \"session_write_accepted\":\n\t\t\t\tsessionWrites.push(`${event.writeSequence}:${event.writeType}`);\n\t\t\t\tbreak;\n\t\t\tcase \"settlement_started\": {\n\t\t\t\tconst operation = byId.get(event.operationId);\n\t\t\t\tif (operation !== undefined) operation.settlement = event.outcome;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"operation_settled\": {\n\t\t\t\tconst operation = byId.get(event.operationId);\n\t\t\t\tif (operation !== undefined) operation.settled = event.outcome;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tconst unknownEvent: never = event;\n\t\t\t\tthrow new TypeError(`Unknown trace event ${String((unknownEvent as { type?: unknown }).type)}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\toperations: operations.map((operation) => ({\n\t\t\toperationId: operation.operationId,\n\t\t\tkind: operation.kind,\n\t\t\tsequence: operation.sequence,\n\t\t\tstages: operation.stages,\n\t\t\tattempts: operation.attempts,\n\t\t\tabortRequested: operation.abortRequested,\n\t\t\t...(operation.settlement === undefined ? {} : { settlement: operation.settlement }),\n\t\t\t...(operation.settled === undefined ? {} : { settled: operation.settled }),\n\t\t\tunresolvedEffectIds: [...operation.unresolved].sort(compareCodeUnits),\n\t\t})),\n\t\tacceptedCommandIds,\n\t\tsessionWrites,\n\t};\n}\n\n/** Code-unit order, so the report does not depend on the host locale. */\nfunction compareCodeUnits(left: string, right: string): number {\n\tif (left === right) return 0;\n\treturn left < right ? -1 : 1;\n}\n\nfunction describeOutcome(outcome: TraceOutcome | undefined): string {\n\tif (outcome === undefined) return \"<none>\";\n\treturn outcome.status === \"failed\" ? `failed:${outcome.code}` : outcome.status;\n}\n\nfunction describeAttempts(attempts: readonly TraceAttemptSummary[]): string {\n\treturn attempts.map((attempt) => `${attempt.attemptId}/${attempt.reason}=${attempt.outcome ?? \"<open>\"}`).join(\",\");\n}\n\nfunction compareOperation(index: number, left: OperationTraceSummary, right: OperationTraceSummary): TraceDivergence[] {\n\tconst out: TraceDivergence[] = [];\n\tconst push = (dimension: TraceDivergenceDimension, leftValue: string, rightValue: string): void => {\n\t\tif (leftValue === rightValue) return;\n\t\tconst divergenceClass: TraceDivergenceClass = BLOCKER_DIMENSIONS.has(dimension) ? \"blocker\" : \"non_blocker\";\n\t\tout.push({ dimension, class: divergenceClass, operationIndex: index, left: leftValue, right: rightValue });\n\t};\n\tpush(\"operation_identity\", `${left.kind}#${left.sequence}`, `${right.kind}#${right.sequence}`);\n\tpush(\"attempt_set\", describeAttempts(left.attempts), describeAttempts(right.attempts));\n\tconst leftSettled = left.settled ?? left.settlement;\n\tconst rightSettled = right.settled ?? right.settlement;\n\tif ((leftSettled?.status ?? \"<none>\") !== (rightSettled?.status ?? \"<none>\")) {\n\t\tout.push({\n\t\t\tdimension: \"terminal_status\",\n\t\t\tclass: \"blocker\",\n\t\t\toperationIndex: index,\n\t\t\tleft: describeOutcome(leftSettled),\n\t\t\tright: describeOutcome(rightSettled),\n\t\t});\n\t} else if (leftSettled?.status === \"failed\" && rightSettled?.status === \"failed\") {\n\t\tpush(\"public_error_code\", leftSettled.code, rightSettled.code);\n\t}\n\tpush(\"effect_uncertainty\", left.unresolvedEffectIds.join(\",\"), right.unresolvedEffectIds.join(\",\"));\n\tpush(\"stage_path\", left.stages.join(\">\"), right.stages.join(\">\"));\n\tpush(\"abort_signal\", String(left.abortRequested), String(right.abortRequested));\n\treturn out;\n}\n\n/** Compare two traces of the same scenario. Equal digests short-circuit to an empty report. */\nexport function compareOperationTraces(\n\tleft: readonly OperationTraceEvent[],\n\tright: readonly OperationTraceEvent[],\n): TraceComparison {\n\tconst leftDigest = computeTraceDigest(left);\n\tconst rightDigest = computeTraceDigest(right);\n\tif (leftDigest === rightDigest) return { equal: true, leftDigest, rightDigest, blockers: [], nonBlockers: [] };\n\tconst leftSummary = summarizeTrace(left);\n\tconst rightSummary = summarizeTrace(right);\n\tconst divergences: TraceDivergence[] = [];\n\tif (leftSummary.operations.length !== rightSummary.operations.length) {\n\t\tdivergences.push({\n\t\t\tdimension: \"operation_count\",\n\t\t\tclass: \"blocker\",\n\t\t\tleft: String(leftSummary.operations.length),\n\t\t\tright: String(rightSummary.operations.length),\n\t\t});\n\t}\n\tconst shared = Math.min(leftSummary.operations.length, rightSummary.operations.length);\n\tfor (let index = 0; index < shared; index++) {\n\t\tdivergences.push(...compareOperation(index, leftSummary.operations[index], rightSummary.operations[index]));\n\t}\n\tconst leftCommands = leftSummary.acceptedCommandIds.join(\",\");\n\tconst rightCommands = rightSummary.acceptedCommandIds.join(\",\");\n\tif (leftCommands !== rightCommands) {\n\t\tdivergences.push({ dimension: \"accepted_commands\", class: \"blocker\", left: leftCommands, right: rightCommands });\n\t}\n\tconst leftWrites = leftSummary.sessionWrites.join(\",\");\n\tconst rightWrites = rightSummary.sessionWrites.join(\",\");\n\tif (leftWrites !== rightWrites) {\n\t\tdivergences.push({ dimension: \"session_write_order\", class: \"blocker\", left: leftWrites, right: rightWrites });\n\t}\n\treturn {\n\t\tequal: false,\n\t\tleftDigest,\n\t\trightDigest,\n\t\tblockers: divergences.filter((item) => item.class === \"blocker\"),\n\t\tnonBlockers: divergences.filter((item) => item.class === \"non_blocker\"),\n\t};\n}\n"]}