@demlik/tea 0.1.0 → 0.1.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 (243) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/index.d.ts +907 -0
  3. package/dist/agent/index.js +20 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/authed-call/index.d.ts +119 -0
  6. package/dist/authed-call/index.js +148 -0
  7. package/dist/authed-call/index.js.map +1 -0
  8. package/dist/await-terminal/index.d.ts +99 -0
  9. package/dist/await-terminal/index.js +75 -0
  10. package/dist/await-terminal/index.js.map +1 -0
  11. package/dist/batch-window/index.d.ts +259 -0
  12. package/dist/batch-window/index.js +43 -0
  13. package/dist/batch-window/index.js.map +1 -0
  14. package/dist/cache/index.d.ts +2 -2
  15. package/dist/cache/index.js +2 -50
  16. package/dist/cache/index.js.map +1 -1
  17. package/dist/chunk-27N2SPTN.js +222 -0
  18. package/dist/chunk-27N2SPTN.js.map +1 -0
  19. package/dist/chunk-2YOE5PIS.js +45 -0
  20. package/dist/chunk-2YOE5PIS.js.map +1 -0
  21. package/dist/chunk-3545BOYL.js +65 -0
  22. package/dist/chunk-3545BOYL.js.map +1 -0
  23. package/dist/chunk-3XJWC7VW.js +61 -0
  24. package/dist/chunk-3XJWC7VW.js.map +1 -0
  25. package/dist/chunk-5DEQ5I3U.js +132 -0
  26. package/dist/chunk-5DEQ5I3U.js.map +1 -0
  27. package/dist/chunk-5VNWZADP.js +49 -0
  28. package/dist/chunk-5VNWZADP.js.map +1 -0
  29. package/dist/chunk-6RGBKODV.js +268 -0
  30. package/dist/chunk-6RGBKODV.js.map +1 -0
  31. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  32. package/dist/chunk-7FYQGDMF.js.map +1 -0
  33. package/dist/chunk-7W4LMPUW.js +186 -0
  34. package/dist/chunk-7W4LMPUW.js.map +1 -0
  35. package/dist/chunk-BYQUGDD3.js +16 -0
  36. package/dist/chunk-BYQUGDD3.js.map +1 -0
  37. package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
  38. package/dist/chunk-DILMKOZ7.js.map +1 -0
  39. package/dist/chunk-EHIX7NYZ.js +93 -0
  40. package/dist/chunk-EHIX7NYZ.js.map +1 -0
  41. package/dist/chunk-HCEVHFJ7.js +54 -0
  42. package/dist/chunk-HCEVHFJ7.js.map +1 -0
  43. package/dist/chunk-HJO6VNQI.js +15 -0
  44. package/dist/chunk-HJO6VNQI.js.map +1 -0
  45. package/dist/chunk-IN33Q4VJ.js +52 -0
  46. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  47. package/dist/chunk-J6527VRQ.js +56 -0
  48. package/dist/chunk-J6527VRQ.js.map +1 -0
  49. package/dist/chunk-JELSZN25.js +47 -0
  50. package/dist/chunk-JELSZN25.js.map +1 -0
  51. package/dist/chunk-K575PT4S.js +58 -0
  52. package/dist/chunk-K575PT4S.js.map +1 -0
  53. package/dist/chunk-K7Q23EQ4.js +20 -0
  54. package/dist/chunk-K7Q23EQ4.js.map +1 -0
  55. package/dist/chunk-LEC4U6CX.js +91 -0
  56. package/dist/chunk-LEC4U6CX.js.map +1 -0
  57. package/dist/{chunk-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
  58. package/dist/chunk-LP7VQBRG.js.map +1 -0
  59. package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
  60. package/dist/chunk-MMH7CW5K.js.map +1 -0
  61. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  62. package/dist/chunk-NSDFA6M6.js.map +1 -0
  63. package/dist/chunk-ON24RKSE.js +60 -0
  64. package/dist/chunk-ON24RKSE.js.map +1 -0
  65. package/dist/chunk-PS23H6KQ.js +87 -0
  66. package/dist/chunk-PS23H6KQ.js.map +1 -0
  67. package/dist/chunk-QLA76GIT.js +162 -0
  68. package/dist/chunk-QLA76GIT.js.map +1 -0
  69. package/dist/chunk-S3FQN4SL.js +506 -0
  70. package/dist/chunk-S3FQN4SL.js.map +1 -0
  71. package/dist/chunk-SC3FAFZN.js +53 -0
  72. package/dist/chunk-SC3FAFZN.js.map +1 -0
  73. package/dist/chunk-SINXE47Y.js +55 -0
  74. package/dist/chunk-SINXE47Y.js.map +1 -0
  75. package/dist/chunk-SQTCS44W.js +33 -0
  76. package/dist/chunk-SQTCS44W.js.map +1 -0
  77. package/dist/chunk-TGG26XMK.js +52 -0
  78. package/dist/chunk-TGG26XMK.js.map +1 -0
  79. package/dist/chunk-UV6UWGEO.js +147 -0
  80. package/dist/chunk-UV6UWGEO.js.map +1 -0
  81. package/dist/chunk-UYWUVEP6.js +158 -0
  82. package/dist/chunk-UYWUVEP6.js.map +1 -0
  83. package/dist/chunk-VLGARN6H.js +202 -0
  84. package/dist/chunk-VLGARN6H.js.map +1 -0
  85. package/dist/circuit-breaker/index.js +1 -51
  86. package/dist/circuit-breaker/index.js.map +1 -1
  87. package/dist/core-BoB6t7dB.d.ts +188 -0
  88. package/dist/deadline/index.d.ts +24 -5
  89. package/dist/deadline/index.js +4 -18
  90. package/dist/deadline/index.js.map +1 -1
  91. package/dist/debounce/index.d.ts +6 -0
  92. package/dist/debounce/index.js +1 -54
  93. package/dist/debounce/index.js.map +1 -1
  94. package/dist/devtools/index.d.ts +1 -1
  95. package/dist/devtools/index.js +18 -6
  96. package/dist/devtools/index.js.map +1 -1
  97. package/dist/do/index.d.ts +1723 -92
  98. package/dist/do/index.js +673 -31
  99. package/dist/do/index.js.map +1 -1
  100. package/dist/durable-effects-CO9Uh971.d.ts +180 -0
  101. package/dist/extension/index.d.ts +31 -13
  102. package/dist/extension/index.js +5 -3
  103. package/dist/extension/index.js.map +1 -1
  104. package/dist/extension/react.js +8 -3
  105. package/dist/extension/react.js.map +1 -1
  106. package/dist/extension/subs/index.d.ts +2 -2
  107. package/dist/extension/subs/index.js.map +1 -1
  108. package/dist/extension/test-utils.js +11 -3
  109. package/dist/extension/test-utils.js.map +1 -1
  110. package/dist/fan-out/index.d.ts +239 -0
  111. package/dist/fan-out/index.js +5 -0
  112. package/dist/fan-out/index.js.map +1 -0
  113. package/dist/idempotency/adapter.d.ts +62 -0
  114. package/dist/idempotency/adapter.js +4 -0
  115. package/dist/idempotency/adapter.js.map +1 -0
  116. package/dist/idempotency/index.js +1 -56
  117. package/dist/idempotency/index.js.map +1 -1
  118. package/dist/idempotent-intake/index.d.ts +186 -0
  119. package/dist/idempotent-intake/index.js +7 -0
  120. package/dist/idempotent-intake/index.js.map +1 -0
  121. package/dist/index.d.ts +341 -191
  122. package/dist/index.js +2 -1
  123. package/dist/llm-call/index.d.ts +309 -0
  124. package/dist/llm-call/index.js +14 -0
  125. package/dist/llm-call/index.js.map +1 -0
  126. package/dist/machine-viz/index.d.ts +4 -4
  127. package/dist/machine-viz/index.js +5 -9
  128. package/dist/machine-viz/index.js.map +1 -1
  129. package/dist/mem/index.d.ts +1 -0
  130. package/dist/monitored-run/index.d.ts +253 -0
  131. package/dist/monitored-run/index.js +10 -0
  132. package/dist/monitored-run/index.js.map +1 -0
  133. package/dist/node/index.d.ts +5 -5
  134. package/dist/node/index.js.map +1 -1
  135. package/dist/paginated-walk/index.d.ts +234 -0
  136. package/dist/paginated-walk/index.js +15 -0
  137. package/dist/paginated-walk/index.js.map +1 -0
  138. package/dist/paginator/index.d.ts +271 -0
  139. package/dist/paginator/index.js +3 -0
  140. package/dist/paginator/index.js.map +1 -0
  141. package/dist/parity/index.d.ts +92 -0
  142. package/dist/parity/index.js +78 -0
  143. package/dist/parity/index.js.map +1 -0
  144. package/dist/pbt/arbitraries/index.d.ts +1 -1
  145. package/dist/pbt/arbitraries/index.js +1 -1
  146. package/dist/pbt/index.d.ts +15 -8
  147. package/dist/pbt/index.js +6 -11
  148. package/dist/pbt/index.js.map +1 -1
  149. package/dist/pbt/runners/index.d.ts +1 -1
  150. package/dist/pbt/runners/index.js +3 -2
  151. package/dist/poller/index.d.ts +288 -0
  152. package/dist/poller/index.js +79 -0
  153. package/dist/poller/index.js.map +1 -0
  154. package/dist/prediction/index.d.ts +130 -0
  155. package/dist/prediction/index.js +4 -0
  156. package/dist/prediction/index.js.map +1 -0
  157. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  158. package/dist/pure/index.d.ts +2 -0
  159. package/dist/pure/index.js +4 -0
  160. package/dist/pure/index.js.map +1 -0
  161. package/dist/raft/demo.d.ts +105 -0
  162. package/dist/raft/demo.js +172 -0
  163. package/dist/raft/demo.js.map +1 -0
  164. package/dist/raft/index.d.ts +548 -0
  165. package/dist/raft/index.js +7 -0
  166. package/dist/raft/index.js.map +1 -0
  167. package/dist/raft/sim.d.ts +222 -0
  168. package/dist/raft/sim.js +8 -0
  169. package/dist/raft/sim.js.map +1 -0
  170. package/dist/raft/timeline.d.ts +46 -0
  171. package/dist/raft/timeline.js +62 -0
  172. package/dist/raft/timeline.js.map +1 -0
  173. package/dist/rate-limit/index.js +1 -43
  174. package/dist/rate-limit/index.js.map +1 -1
  175. package/dist/react/index.d.ts +2 -1
  176. package/dist/react/index.js +43 -20
  177. package/dist/react/index.js.map +1 -1
  178. package/dist/reconciler/index.d.ts +192 -0
  179. package/dist/reconciler/index.js +166 -0
  180. package/dist/reconciler/index.js.map +1 -0
  181. package/dist/recorder/index.d.ts +7 -3
  182. package/dist/recorder/index.js +1 -155
  183. package/dist/recorder/index.js.map +1 -1
  184. package/dist/resilient-call/index.d.ts +294 -0
  185. package/dist/resilient-call/index.js +13 -0
  186. package/dist/resilient-call/index.js.map +1 -0
  187. package/dist/retry-backoff/index.d.ts +42 -5
  188. package/dist/retry-backoff/index.js +1 -35
  189. package/dist/retry-backoff/index.js.map +1 -1
  190. package/dist/retry-to-success/index.d.ts +109 -0
  191. package/dist/retry-to-success/index.js +35 -0
  192. package/dist/retry-to-success/index.js.map +1 -0
  193. package/dist/saga/index.d.ts +238 -0
  194. package/dist/saga/index.js +150 -0
  195. package/dist/saga/index.js.map +1 -0
  196. package/dist/snapshot/index.d.ts +334 -0
  197. package/dist/snapshot/index.js +5 -0
  198. package/dist/snapshot/index.js.map +1 -0
  199. package/dist/subs/index.d.ts +48 -10
  200. package/dist/subs/index.js +1 -1
  201. package/dist/testing/index.d.ts +107 -2
  202. package/dist/testing/index.js +204 -6
  203. package/dist/testing/index.js.map +1 -1
  204. package/dist/throttle/index.d.ts +9 -0
  205. package/dist/throttle/index.js +1 -52
  206. package/dist/throttle/index.js.map +1 -1
  207. package/dist/throttled-input/index.d.ts +380 -0
  208. package/dist/throttled-input/index.js +88 -0
  209. package/dist/throttled-input/index.js.map +1 -0
  210. package/dist/token-refresh/index.d.ts +215 -0
  211. package/dist/token-refresh/index.js +6 -0
  212. package/dist/token-refresh/index.js.map +1 -0
  213. package/dist/trace-replay/index.d.ts +11 -2
  214. package/dist/trace-replay/index.js +3 -55
  215. package/dist/trace-replay/index.js.map +1 -1
  216. package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
  217. package/dist/with-deadline/index.d.ts +196 -0
  218. package/dist/with-deadline/index.js +111 -0
  219. package/dist/with-deadline/index.js.map +1 -0
  220. package/dist/with-resilience/index.d.ts +205 -0
  221. package/dist/with-resilience/index.js +251 -0
  222. package/dist/with-resilience/index.js.map +1 -0
  223. package/dist/with-telemetry/index.d.ts +149 -0
  224. package/dist/with-telemetry/index.js +70 -0
  225. package/dist/with-telemetry/index.js.map +1 -0
  226. package/dist/work-queue/adapter.d.ts +82 -0
  227. package/dist/work-queue/adapter.js +4 -0
  228. package/dist/work-queue/adapter.js.map +1 -0
  229. package/dist/work-queue/index.d.ts +25 -11
  230. package/dist/work-queue/index.js +1 -46
  231. package/dist/work-queue/index.js.map +1 -1
  232. package/dist/work-queue/ops.d.ts +69 -0
  233. package/dist/work-queue/ops.js +3 -0
  234. package/dist/work-queue/ops.js.map +1 -0
  235. package/dist/workflow/index.d.ts +450 -0
  236. package/dist/workflow/index.js +225 -0
  237. package/dist/workflow/index.js.map +1 -0
  238. package/package.json +168 -25
  239. package/dist/chunk-CMG2KMWX.js.map +0 -1
  240. package/dist/chunk-PYBQREFU.js.map +0 -1
  241. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  242. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  243. package/dist/chunk-ZRUST6BK.js.map +0 -1
@@ -0,0 +1,309 @@
1
+ import { C as Cmd } from '../core-BoB6t7dB.js';
2
+ import { SucceedMsg, FailMsg, RunCmd, ResilientState } from '../resilient-call/index.js';
3
+ import { RetryPolicy } from '../retry-backoff/index.js';
4
+ import { DeadlineExceeded, DeadlineSub } from '../deadline/index.js';
5
+ export { deadlineSub, subscribeDeadline } from '../deadline/index.js';
6
+ import '../cache/index.js';
7
+ import '../types-B-8bcAp8.js';
8
+ import '../circuit-breaker/index.js';
9
+ import '../protocol-B5YmJr0d.js';
10
+ import '../rate-limit/index.js';
11
+
12
+ /**
13
+ * @demlik/tea/llm-call — `resilient-call` + structured-output parse + a typed
14
+ * failure variant, around a purpose-discriminated LLM invocation.
15
+ *
16
+ * This is the ~60-line `call_llm` handler from the audit-agent seed
17
+ * (`interpret.ts` / `effects.ts`) collapsed into the uniform L2 knob: a config
18
+ * object, the resilient-call slice it inherits, and one interpret handler that
19
+ * assembles messages, binds the structured-output schema for the call's
20
+ * `purpose`, invokes the model (retry composed from `../resilient-call`, NOT
21
+ * reinvented), parses the output, and RETURNS the enriched resilient settle Msg
22
+ * (`resilient_ok` / `resilient_err`) so it re-enters the host reducer and drives
23
+ * the inherited succeed/fail → backoff → onTimer loop.
24
+ *
25
+ * ## What it adds over `../resilient-call`
26
+ *
27
+ * - **Two DI ports** (the seed's two seams):
28
+ * 1. `model: (modelId) => LLM` — the model factory. Tests pass a fake so
29
+ * the handler runs end-to-end without touching a real provider; the
30
+ * production worker wires `createChatModel(env, …)`.
31
+ * 2. `loadMessages: Loader` — the SDK / message loader. The seed lazy-
32
+ * imports `@langchain/core/messages` (a top-level `import type` blows
33
+ * up the workers test runner); the loader keeps that import lazy and
34
+ * stubbable. It produces the `BaseMessage[]` for a given call.
35
+ * - **Structured-output parse** — each `purpose` maps to a `Schema` in
36
+ * `config.schemas`. The handler binds `model.withStructuredOutput(schema)`
37
+ * so the invoke resolves to a typed object; a `Schema.parse` that throws
38
+ * becomes a `resilient_err` carrying an `LlmErr` (a parse failure is a
39
+ * failure, not a stall — "errors are data"). `withStructuredOutput` is the
40
+ * seed's brain-only path; the fake model in tests returns the typed object
41
+ * directly.
42
+ * - **A typed failure variant** — every failure (model throw, timeout-via-
43
+ * retry-exhaustion, schema parse) is surfaced as the `LlmErr` carried on the
44
+ * enriched `resilient_err` settle Msg, tagged with the `purpose` so the
45
+ * consumer's reducer routes per-stage. The seed dispatched
46
+ * `llm_failed{purpose}`; the consumer rebuilds that shape in its reducer arm
47
+ * from `m.error: LlmErr`.
48
+ *
49
+ * ## Inheriting resilient-call by composition (not reinvention)
50
+ *
51
+ * The retry / backoff lives in `../resilient-call`: `createLlmCall` builds a
52
+ * `createResilientCall` knob over the model-invoke port and DELEGATES `init`,
53
+ * `attempt`, `succeed`, `fail`, `onTimer`, `subs` to it verbatim — the slice is
54
+ * literally resilient-call's slice (the spec's "retry lives here"). There is no
55
+ * second backoff implementation here. The handler runs the invoke through the
56
+ * resilient-call handler so a transient model failure backs off exactly as a
57
+ * resilient HTTP call would; the `purpose`-branch + structured parse wrap the
58
+ * port the resilient handler drives.
59
+ *
60
+ * ## The two non-negotiables (canon, inherited)
61
+ *
62
+ * - **Durable** — the slice is resilient-call's plain-data slice (a Model
63
+ * field). Input carried on the `resilient_run` Cmd is the plain
64
+ * `LlmCall` request (purpose + modelId + payload) — no closures.
65
+ * - **Replayable** — every transition is a resilient-call verb; the handler
66
+ * is the only impurity, and the one clock read is `Date.now()` at the
67
+ * effect boundary (inside the handler), exactly as the seed stamped its
68
+ * `llm_responded` / `llm_failed` Msgs.
69
+ *
70
+ * ## Typical wiring
71
+ *
72
+ * const llm = createLlmCall<MyPurpose, MyOutputs>({
73
+ * model: (id) => createChatModel(env, id),
74
+ * schemas: { plan: planSchema, report: reportSchema },
75
+ * retry: defaultRetryPolicy,
76
+ * loadMessages: defaultMessagesLoader,
77
+ * });
78
+ *
79
+ * // in the machine:
80
+ * init: () => [{ resilience: llm.init() }, []],
81
+ * update: {
82
+ * call_llm: (s, m) => lift(s, llm.attempt(s.resilience, m.input, m.at)),
83
+ * // `resilient_ok` / `resilient_err` RE-ENTER from `handlers` (the settle
84
+ * // Msg the interpret handler returns). Run the inherited verb FIRST so the
85
+ * // succeed/fail → backoff → onTimer loop advances, THEN fold the enriched
86
+ * // payload (`m.result: LlmOk` / `m.error: LlmErr`) into the host's own
87
+ * // state — the enrichment lives in the reducer arm, not the handler.
88
+ * resilient_ok: (s, m) => {
89
+ * const [slice, cmds] = llm.succeed(s.resilience, m.key, m);
90
+ * return [{ ...s, resilience: slice, output: m.result.output }, cmds];
91
+ * },
92
+ * resilient_err: (s, m) => {
93
+ * const [slice, cmds] = llm.fail(s.resilience, m.key, m);
94
+ * return [{ ...s, resilience: slice, failure: m.error }, cmds];
95
+ * },
96
+ * deadline_exceeded: (s, m) => lift(s, llm.onTimer(s.resilience, m)),
97
+ * },
98
+ * subscriptions: (s) => llm.subs(s.resilience),
99
+ * subscribe: { deadline: subscribeDeadline },
100
+ * interpret: llm.handlers(),
101
+ */
102
+
103
+ /**
104
+ * The minimal structured-output schema contract: `parse(unknown) => T`, the
105
+ * zod-style call the handler uses to validate the model's output before it
106
+ * settles `resilient_ok`. A throwing `parse` (the zod contract on invalid
107
+ * input) propagates out of `invokeOne` and is caught by the resilient handler,
108
+ * which settles `resilient_err` (enriched to `LlmErr`) — a parse failure is a
109
+ * failure, never a silent pass. Structural so a real `z.ZodType<T>` satisfies
110
+ * it without an import.
111
+ */
112
+ interface Schema<T> {
113
+ /** Validate + narrow `value` to `T`, or throw on mismatch (the zod contract). */
114
+ parse(value: unknown): T;
115
+ }
116
+ /**
117
+ * The minimal chat-model contract every model the handler talks to must
118
+ * satisfy — the seed's `InjectableChatModel`, trimmed to the one operation
119
+ * llm-call drives for brain-only stages:
120
+ *
121
+ * `withStructuredOutput(schema)` → a runnable whose `invoke(messages)`
122
+ * resolves to a typed object matching `schema`.
123
+ *
124
+ * `BaseChatModel` from `@langchain/core` is the runtime type; this surface
125
+ * names only what llm-call calls so a test fake can ignore the rest. Generic
126
+ * over the message shape `Msg` so a consumer's loader and model agree on it
127
+ * without llm-call inspecting messages.
128
+ */
129
+ interface Llm<Msg> {
130
+ withStructuredOutput<T>(schema: Schema<T>): {
131
+ invoke(messages: readonly Msg[]): Promise<T>;
132
+ };
133
+ }
134
+ /**
135
+ * Build the `Msg[]` the handler hands to the bound model for a given call. The
136
+ * SDK / message-assembly seam — the seed's `buildBaseMessages` + the lazy
137
+ * `loadMessages` loader rolled into one injected port. Async because the seed
138
+ * lazy-imports the SDK (a top-level `import type` of the langchain messages
139
+ * package explodes the workers test runner). Receives the full `LlmCall` so it
140
+ * can branch on `purpose` exactly as the seed's `buildBaseMessages` did.
141
+ */
142
+ type MessageLoader<P extends string, Msg> = (call: LlmCall<P>) => Promise<readonly Msg[]>;
143
+ /**
144
+ * The model factory — the first DI port. `(modelId) => Llm`. Tests pass a fake
145
+ * builder; production wires `createChatModel(env, getModelConfig(id))`. `null`
146
+ * means "the host's default model" (the seed's `string | null`).
147
+ */
148
+ type ModelFactory<Msg> = (modelId: string | null) => Llm<Msg>;
149
+ /**
150
+ * The llm-call knob. `model` and `schemas` are the load-bearing pair — the
151
+ * model factory DI port and the per-purpose structured-output targets; the
152
+ * rest is optional, exactly the resilient-call "omit a brick → omit its gate"
153
+ * story for `retry`:
154
+ *
155
+ * - `model` — DI port 1: the model factory.
156
+ * - `schemas` — one `Schema` per `Purpose`. The handler binds
157
+ * `schemas[call.purpose]` as the structured-output target.
158
+ * - `retry` — backoff policy, composed straight into `../resilient-
159
+ * call`. Omit it and a model failure is terminal (no
160
+ * backoff), exactly as resilient-call with no retry brick.
161
+ * - `loadMessages` — DI port 2: the SDK / message loader. Omit it and the
162
+ * handler invokes the bound model with `[]` (a degenerate
163
+ * but valid call) — provide it to assemble real messages.
164
+ *
165
+ * `P` is the purpose union (e.g. `"plan" | "report"`); `O` maps each purpose to
166
+ * its parsed output type; `Msg` is the model's message shape (threaded through
167
+ * the loader + model so llm-call never inspects a message).
168
+ */
169
+ interface LlmCallConfig<P extends string, O extends Record<P, unknown>, Msg = unknown> {
170
+ /** DI port 1 — the model factory. `(modelId) => Llm`. */
171
+ readonly model: ModelFactory<Msg>;
172
+ /** One structured-output schema per purpose; the parse target the handler binds. */
173
+ readonly schemas: {
174
+ readonly [K in P]: Schema<O[K]>;
175
+ };
176
+ /** Backoff policy, composed into `../resilient-call`. Omit → no backoff. */
177
+ readonly retry?: RetryPolicy;
178
+ /** DI port 2 — the SDK / message loader. Omit → the handler invokes with `[]`. */
179
+ readonly loadMessages?: MessageLoader<P, Msg>;
180
+ }
181
+ /**
182
+ * One LLM call request — the resilient-call `input` for this knob, carried on
183
+ * the `resilient_run` Cmd as plain data (no closures, invariant 3). Mirrors the
184
+ * seed's purpose-discriminated `CallLlmCmd`: a `purpose` selecting the schema +
185
+ * prompt assembly, the `model` id, and the opaque per-purpose `payload` the
186
+ * loader consumes.
187
+ *
188
+ * `key` defaults to `purpose` when the consumer calls `attempt(s, purpose, …)`,
189
+ * so one in-flight call per purpose is tracked under the resilient-call slice —
190
+ * the common single-call-per-stage shape. A consumer that fans out many calls
191
+ * of one purpose passes a distinct `key`.
192
+ */
193
+ interface LlmCall<P extends string> {
194
+ /** The stage / schema selector — drives both `schemas[purpose]` and message assembly. */
195
+ readonly purpose: P;
196
+ /** The model id to invoke; `null` = the host's default model. */
197
+ readonly model: string | null;
198
+ /** The per-purpose prompt payload the `MessageLoader` consumes. Opaque to the knob. */
199
+ readonly payload: unknown;
200
+ }
201
+ /** The parsed, typed success carried on the `resilient_ok` settle Msg, tagged with its purpose. */
202
+ interface LlmOk<P extends string, O extends Record<P, unknown>> {
203
+ readonly key: string;
204
+ readonly purpose: P;
205
+ readonly output: O[P];
206
+ }
207
+ /** The typed failure variant — every failure path surfaces this, tagged by purpose. */
208
+ interface LlmErr<P extends string> {
209
+ readonly key: string;
210
+ readonly purpose: P;
211
+ /** A human-readable cause (model throw, retry-exhaustion, or schema parse). */
212
+ readonly reason: string;
213
+ /** The original error, carried untouched for the consumer to inspect. */
214
+ readonly error: unknown;
215
+ }
216
+ /**
217
+ * The settle Msgs llm-call's handler RETURNS from `interpret` so the substrate
218
+ * enqueues them as follow-up Msgs (re-entry) into the host reducer — exactly as
219
+ * `../resilient-call` does. They are the resilient-call settle Msgs with the
220
+ * payloads ENRICHED to llm-call's typed variants: `result` is the parsed,
221
+ * purpose-tagged `LlmOk`; `error` is the typed `LlmErr` (purpose + reason +
222
+ * raw). The host wires `resilient_ok` → `succeed` and `resilient_err` → `fail`
223
+ * reducer arms (the doc-comment wiring), and folds the enriched payload into its
224
+ * own state there — llm-call never forces a Msg vocabulary on the host, and the
225
+ * settle Msg drives the inherited succeed/fail → backoff → onTimer loop instead
226
+ * of bypassing it.
227
+ */
228
+ type LlmSucceedMsg<P extends string, O extends Record<P, unknown>> = SucceedMsg<LlmOk<P, O>>;
229
+ type LlmFailMsg<P extends string> = Omit<FailMsg, "error"> & {
230
+ readonly error: LlmErr<P>;
231
+ };
232
+ /** The retry / deadline timer Msg — `DeadlineExceeded`, inherited from resilient-call. */
233
+ type LlmTimerMsg = DeadlineExceeded;
234
+ /**
235
+ * The ports the LEGACY detached `handlers(ports)` form takes. Two outbound Msg
236
+ * builders the handler hands the typed Ok / Err; the consumer returns its own
237
+ * Msg (or `undefined`). KEPT only for `../agent`, which still inherits the
238
+ * detached shape; that path does NOT drive the retry loop (it dispatches the
239
+ * consumer's Msg directly and never re-enters the resilient settle Msg). New
240
+ * consumers use the no-arg `handlers()` form, which RETURNS the settle Msg for
241
+ * re-entry and drives the inherited succeed/fail → backoff → onTimer loop.
242
+ * `../agent` carries the same retry-loop gap and is fixed separately.
243
+ *
244
+ * The builders return `M | undefined` — `undefined` is the "dispatch nothing"
245
+ * sentinel the detached form checks (`!== undefined`), spelled as `undefined`
246
+ * rather than `void` so the union is unambiguous (a `void` member of a union is
247
+ * confusing and reads as "may return anything").
248
+ */
249
+ interface LlmCallPorts<P extends string, O extends Record<P, unknown>, M> {
250
+ /** Build the Msg dispatched on a parsed success. Return `undefined` to dispatch nothing. */
251
+ readonly onOk: (ok: LlmOk<P, O>) => M | undefined;
252
+ /** Build the Msg dispatched on any failure (throw / retry-exhausted / parse). */
253
+ readonly onErr: (err: LlmErr<P>) => M | undefined;
254
+ }
255
+ /**
256
+ * The effect Cmd the knob emits: run the LLM call for `key` with `input`. The
257
+ * `input` is the plain `LlmCall` request — the handler reads `purpose` /
258
+ * `model` / `payload` off it (invariant 3: Cmds are data).
259
+ *
260
+ * It is `../resilient-call`'s `RunCmd` specialized to the `LlmCall` input — NOT a
261
+ * re-declared shape — so the resilient verbs' return tuples (`[State, RunCmd[]]`)
262
+ * are this exact type and thread through `attempt` / `succeed` / `fail` /
263
+ * `onTimer` with no cast.
264
+ */
265
+ type LlmRunCmd<P extends string> = RunCmd<LlmCall<P>>;
266
+ /**
267
+ * Build an llm-call knob from `config`. `rng` is injected for the inherited
268
+ * retry jitter (pass a fixed `() => 0.5` in tests to pin backoff; defaults to
269
+ * `Math.random`, read only at the resilient-call verb boundary).
270
+ *
271
+ * Returns the uniform L2 knob contract. The slice + verbs (`init`, `attempt`,
272
+ * `succeed`, `fail`, `onTimer`, `subs`) are DELEGATED to a `../resilient-call`
273
+ * knob built over the structured-output model-invoke port — no second backoff
274
+ * here. `handlers` is the one new piece: the purpose-branching, schema-parsing
275
+ * wrapper that returns the enriched resilient settle Msg for re-entry.
276
+ *
277
+ * `P` is the purpose union, `O` the purpose→output map, `Msg` the model's
278
+ * message shape.
279
+ */
280
+ declare function createLlmCall<P extends string, O extends Record<P, unknown>, Msg = unknown>(config: LlmCallConfig<P, O, Msg>, rng?: () => number): {
281
+ init: () => ResilientState<LlmCall<P>, LlmOk<P, O>>;
282
+ attempt: (s: ResilientState<LlmCall<P>, LlmOk<P, O>>, input: LlmCall<P>, at: number, key?: string) => readonly [ResilientState<LlmCall<P>, LlmOk<P, O>>, readonly LlmRunCmd<P>[]];
283
+ succeed: (s: ResilientState<LlmCall<P>, LlmOk<P, O>>, key: string, msg: LlmSucceedMsg<P, O>) => readonly [ResilientState<LlmCall<P>, LlmOk<P, O>>, readonly LlmRunCmd<P>[]];
284
+ fail: (s: ResilientState<LlmCall<P>, LlmOk<P, O>>, key: string, msg: LlmFailMsg<P>) => readonly [ResilientState<LlmCall<P>, LlmOk<P, O>>, readonly LlmRunCmd<P>[]];
285
+ onTimer: (s: ResilientState<LlmCall<P>, LlmOk<P, O>>, msg: LlmTimerMsg) => readonly [ResilientState<LlmCall<P>, LlmOk<P, O>>, readonly LlmRunCmd<P>[]];
286
+ subs: (s: ResilientState<LlmCall<P>, LlmOk<P, O>>) => readonly DeadlineSub[];
287
+ handlers: {
288
+ (): {
289
+ resilient_run: (cmd: LlmRunCmd<P>) => Promise<LlmSucceedMsg<P, O> | LlmFailMsg<P>>;
290
+ };
291
+ <M>(ports: LlmCallPorts<P, O, M>): {
292
+ resilient_run: (cmd: LlmRunCmd<P>, ctx: {
293
+ waitUntil(p: Promise<unknown>): void;
294
+ dispatch(msg: M): unknown;
295
+ }) => void;
296
+ };
297
+ };
298
+ invokeOne: (input: LlmCall<P>) => Promise<LlmOk<P, O>>;
299
+ };
300
+ /**
301
+ * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the
302
+ * slice lives at `state.resilience` — the same convenience `../resilient-call`
303
+ * ships, re-typed for the llm-call slice so consumers wire one import. Pure.
304
+ */
305
+ declare function liftLlmCall<S extends {
306
+ resilience: ResilientState<LlmCall<P>, LlmOk<P, O>>;
307
+ }, P extends string, O extends Record<P, unknown>, C extends Cmd>(state: S, result: readonly [ResilientState<LlmCall<P>, LlmOk<P, O>>, readonly C[]]): readonly [S, readonly C[]];
308
+
309
+ export { DeadlineExceeded, DeadlineSub, type Llm, type LlmCall, type LlmCallConfig, type LlmCallPorts, type LlmErr, type LlmFailMsg, type LlmOk, type LlmRunCmd, type LlmSucceedMsg, type LlmTimerMsg, type MessageLoader, type ModelFactory, ResilientState, type Schema, createLlmCall, liftLlmCall };
@@ -0,0 +1,14 @@
1
+ export { createLlmCall, liftLlmCall } from '../chunk-PS23H6KQ.js';
2
+ import '../chunk-27N2SPTN.js';
3
+ import '../chunk-JELSZN25.js';
4
+ export { deadlineSub, subscribeDeadline } from '../chunk-K7Q23EQ4.js';
5
+ import '../chunk-5VNWZADP.js';
6
+ import '../chunk-2YOE5PIS.js';
7
+ import '../chunk-SC3FAFZN.js';
8
+ import '../chunk-IN33Q4VJ.js';
9
+ import '../chunk-NSDFA6M6.js';
10
+ import '../chunk-2YFLOCWX.js';
11
+ import '../chunk-MMH7CW5K.js';
12
+ import '../chunk-QLA76GIT.js';
13
+ //# sourceMappingURL=index.js.map
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -1,4 +1,4 @@
1
- import { Cmd, Sub, Machine } from '../index.js';
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-BoB6t7dB.js';
2
2
 
3
3
  /**
4
4
  * @demlik/tea/machine-viz — turn a `Machine` into a Mermaid diagram string.
@@ -14,9 +14,9 @@ import { Cmd, Sub, Machine } from '../index.js';
14
14
  * back to a STRUCTURAL self-edge annotated with `?` — visually distinct from a
15
15
  * resolved edge and documented in the diagram's legend.
16
16
  *
17
- * Form detection mirrors `run`/`replay` (the `updateMode` logic): inspect the
18
- * first own key of `update`; a value that is a function ⇒ Reducer form, a value
19
- * that is a record ⇒ Transitions form. We do NOT add a runtime tag to Machine.
17
+ * Form detection is `formOf(machine)` — the same reader `run`/`replay` use,
18
+ * honoring the authoritative `__form` tag `defineMachine` stamps (falling back
19
+ * to the structural heuristic for machines built without it; #275).
20
20
  *
21
21
  * What we MUST NOT do (these are the effectful paths, off-limits here):
22
22
  * - call `interpret[*]` handlers (perform I/O)
@@ -1,3 +1,6 @@
1
+ import '../chunk-MMH7CW5K.js';
2
+ import { formOf } from '../chunk-QLA76GIT.js';
3
+
1
4
  // src/machine-viz/index.ts
2
5
  function safeId(raw) {
3
6
  const cleaned = raw.replace(/[^A-Za-z0-9_]/g, "_");
@@ -7,19 +10,12 @@ function safeId(raw) {
7
10
  function safeLabel(raw) {
8
11
  return raw.replace(/["\n\r;:]/g, " ").trim();
9
12
  }
10
- function detectMode(update) {
11
- const keys = Object.keys(update);
12
- const firstKey = keys[0];
13
- if (firstKey === void 0) return "reducer";
14
- const firstValue = update[firstKey];
15
- return typeof firstValue === "function" ? "reducer" : "transitions";
16
- }
17
13
  function toMermaid(machine, opts = {}) {
18
14
  const direction = opts.direction ?? "TB";
19
15
  const stateSamples = opts.samples?.states ?? {};
20
16
  const msgSamples = opts.samples?.msgs ?? {};
21
17
  const hasCtx = opts.samples !== void 0 && "ctx" in opts.samples;
22
- const mode = detectMode(machine.update);
18
+ const mode = formOf(machine);
23
19
  const lines = [];
24
20
  if (opts.title !== void 0) {
25
21
  lines.push("---", `title: ${opts.title}`, "---");
@@ -93,7 +89,7 @@ function emitTransitions(machine, lines, ctx) {
93
89
  }
94
90
  }
95
91
  }
96
- lines.push(" note right of " + safeId(stateTypes[0] ?? "_empty"));
92
+ lines.push(` note right of ${safeId(stateTypes[0] ?? "_empty")}`);
97
93
  lines.push(" Transitions-form machine.");
98
94
  lines.push(" Edge a --> b : msg = RESOLVED (cell executed with samples).");
99
95
  if (anyStructural) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/machine-viz/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/machine-viz — turn a `Machine` into a Mermaid diagram string.\n *\n * PURE. No deps, no AST parsing, no file I/O. The function reads only the\n * `Machine` value it is handed plus optional sample states/msgs/ctx.\n *\n * The trick: TEA update cells are PURE (invariant 2 — they return\n * `[nextState, cmds]` as data, no effects). So we can SAFELY EXECUTE a cell to\n * discover its target state — exactly what `replay` does. That gives us REAL\n * edges (`red --> green : go`) without parsing source. When no sample is\n * provided for a `(state, msg)` pair we cannot resolve the target, so we fall\n * back to a STRUCTURAL self-edge annotated with `?` — visually distinct from a\n * resolved edge and documented in the diagram's legend.\n *\n * Form detection mirrors `run`/`replay` (the `updateMode` logic): inspect the\n * first own key of `update`; a value that is a function ⇒ Reducer form, a value\n * that is a record ⇒ Transitions form. We do NOT add a runtime tag to Machine.\n *\n * What we MUST NOT do (these are the effectful paths, off-limits here):\n * - call `interpret[*]` handlers (perform I/O)\n * - call `subscribe[*]` handlers (install listeners)\n * - call `run` / the runtime\n * We MAY call `init`, `update[*]` cells, and `subscriptions(state)` — all pure\n * by the invariants (same set `replay` is allowed to touch).\n */\n\nimport type { Cmd, Machine, Sub } from \"../index\";\n\n// === Public options ===\n\n/**\n * Options controlling diagram emission.\n *\n * `samples` is the lever that turns STRUCTURAL edges into RESOLVED edges. For a\n * Transitions machine, providing a sample state for `stateType` AND a sample\n * msg for `msgType` lets us execute `update[stateType][msgType](state, msg)` and\n * read the real target `next.type`. Sample maps are keyed by the discriminant\n * string (`state.type` / `msg.type`).\n *\n * `samples.ctx` is the argument to `init(null, ctx)` — provide it to draw the\n * `[*] -->` initial-state edge. Omitted ⇒ no init edge (annotated as such),\n * because `init`'s signature requires a ctx and we will not invent one.\n */\nexport interface MachineVizOptions<S, M> {\n samples?: {\n /** Sample state per `state.type` — unlocks RESOLVED edges + sub annotations. */\n states?: Partial<Record<string, S>>;\n /** Sample msg per `msg.type` — unlocks RESOLVED edges. */\n msgs?: Partial<Record<string, M>>;\n /** Ctx for `init(null, ctx)` — unlocks the `[*] -->` initial-state edge. */\n ctx?: unknown;\n };\n /** Mermaid layout direction. Default `\"TB\"` (top-to-bottom). */\n direction?: \"TB\" | \"LR\";\n /** Optional diagram title, emitted as a Mermaid front-matter `title:` block. */\n title?: string;\n}\n\n// === Mermaid label escaping ===\n//\n// Mermaid state ids must be identifier-safe; arbitrary `type` strings (which\n// CAN contain `:`, spaces, `-`, etc.) are not. We sanitize the id used as the\n// node identifier and carry the human-readable original as a quoted label via\n// `stateId : \"Original\"` when they differ. Edge transition labels (after the\n// `:`) are sanitized to avoid breaking the arrow syntax.\n\n/** Sanitize a discriminant string into a Mermaid-safe node identifier. */\nfunction safeId(raw: string): string {\n // Replace any run of non-identifier chars with `_`; prefix a leading digit so\n // the id is always a valid Mermaid token. Empty ⇒ a stable placeholder.\n const cleaned = raw.replace(/[^A-Za-z0-9_]/g, \"_\");\n if (cleaned === \"\") return \"_empty\";\n return /^[0-9]/.test(cleaned) ? `s_${cleaned}` : cleaned;\n}\n\n/** Sanitize text used as a Mermaid edge/transition label (after the `:`). */\nfunction safeLabel(raw: string): string {\n // Strip the chars that terminate or confuse a transition label: newlines,\n // the `:` that separates label from target, and Mermaid's `\"`/`;` delimiters.\n return raw.replace(/[\"\\n\\r;:]/g, \" \").trim();\n}\n\n// === Form detection (mirrors run/replay updateMode) ===\n\ntype UpdateMode = \"reducer\" | \"transitions\";\n\n/**\n * Detect Reducer-vs-Transitions form from the value shape — identical logic to\n * `run`/`replay`'s `updateMode`. Inspect the first own key: a function value ⇒\n * Reducer (flat `{ [msg.type]: fn }`), a record value ⇒ Transitions (nested\n * `{ [state.type]: { [msg.type]: fn } }`). Empty object ⇒ M is `never`; the\n * mode is moot, default `\"reducer\"`.\n */\nfunction detectMode(update: unknown): UpdateMode {\n const keys = Object.keys(update as object);\n const firstKey = keys[0];\n if (firstKey === undefined) return \"reducer\";\n const firstValue = (update as Record<string, unknown>)[firstKey];\n return typeof firstValue === \"function\" ? \"reducer\" : \"transitions\";\n}\n\n// === Edge model ===\n\ntype Edge =\n // Resolved: we executed the cell with samples and read the real target.\n | { kind: \"resolved\"; from: string; to: string; msg: string }\n // Structural: no samples for the pair → target unknown, drawn as a self-loop.\n | { kind: \"structural\"; from: string; msg: string };\n\n// === toMermaid ===\n\n/**\n * Render a `Machine` as a Mermaid `stateDiagram-v2` string.\n *\n * - **Transitions form** (`update` is a record-of-records): nodes are the\n * `state.type` keys; each `(stateType × msgType)` cell becomes an edge. With\n * samples for both, the edge is RESOLVED by executing the (pure) cell. Without\n * samples it is STRUCTURAL — a `from --> from : msg?` self-loop (the `?`\n * suffix marks \"target unresolved\"). A legend note documents the distinction.\n * The `[*] -->` init edge is drawn only when `samples.ctx` is provided.\n *\n * - **Reducer form** (`update` values are functions): there is no static phase\n * graph (state is not a discriminated union over phases). We emit a single\n * node listing the handled `msg.type`s and an honest note saying so. No edges\n * are invented.\n *\n * Output is deterministic for the same input (keys iterated in insertion order,\n * no clocks, no randomness).\n */\nexport function toMermaid<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n // `NoInfer` pins S/M to the `machine` argument so sample object literals in\n // `opts` are CHECKED against the machine's State/Msg unions rather than\n // driving inference (which would widen `{ type: \"open\" }` to `{ type: string }`\n // and collide with the machine's discriminated union).\n opts: MachineVizOptions<NoInfer<S>, NoInfer<M>> = {},\n): string {\n const direction = opts.direction ?? \"TB\";\n const stateSamples = opts.samples?.states ?? {};\n const msgSamples = opts.samples?.msgs ?? {};\n const hasCtx = opts.samples !== undefined && \"ctx\" in opts.samples;\n\n const mode = detectMode(machine.update);\n const lines: string[] = [];\n\n // Front matter — Mermaid reads a YAML `title:` block before the diagram.\n if (opts.title !== undefined) {\n lines.push(\"---\", `title: ${opts.title}`, \"---\");\n }\n lines.push(\"stateDiagram-v2\");\n lines.push(` direction ${direction}`);\n\n if (mode === \"reducer\") {\n emitReducer(machine, lines);\n } else {\n emitTransitions(machine, lines, {\n stateSamples,\n msgSamples,\n hasCtx,\n ctx: opts.samples?.ctx,\n });\n }\n\n // Subscriptions annotation — pure call to `subscriptions(sampleState)` per\n // sampled state. Skipped entirely when no state samples are provided (we\n // refuse to invent a state to feed it).\n emitSubscriptions(machine, lines, stateSamples);\n\n return lines.join(\"\\n\");\n}\n\n// === Reducer-form emission ===\n\n/**\n * Reducer form has no phase graph. Emit one node listing the handled msg types\n * plus an honest note. The msg set is the `update` record's keys (the Reducer\n * mapped type guarantees one cell per Msg variant).\n */\nfunction emitReducer(machine: { update: unknown }, lines: string[]): void {\n const msgTypes = Object.keys(machine.update as object);\n lines.push(\" state ReducerMachine {\");\n if (msgTypes.length === 0) {\n lines.push(\" [*] --> NoMsgs\");\n lines.push(' NoMsgs : \"(no msg variants — M is never)\"');\n } else {\n for (const m of msgTypes) {\n lines.push(` ${safeId(m)} : ${safeLabel(m)}`);\n }\n }\n lines.push(\" }\");\n // The note IS the design honesty: no edges, because reducer-form state is not\n // a discriminated union over phases — there is no static target to draw.\n lines.push(\" note right of ReducerMachine\");\n lines.push(\" Reducer-form machine: flat dispatch keyed by msg.type.\");\n lines.push(\n \" No phase graph — state is not a discriminated union over phases.\",\n );\n lines.push(` Handled msg types: ${msgTypes.length}`);\n lines.push(\" end note\");\n}\n\n// === Transitions-form emission ===\n\ninterface TransitionsCtx<S, M> {\n stateSamples: Partial<Record<string, S>>;\n msgSamples: Partial<Record<string, M>>;\n hasCtx: boolean;\n ctx: unknown;\n}\n\n/**\n * Transitions form: walk the `update[stateType][msgType]` table. Execute the\n * (pure) cell when both samples exist → RESOLVED edge; otherwise → STRUCTURAL\n * self-loop. Draw the `[*] -->` init edge only when a ctx sample is given.\n */\nfunction emitTransitions<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n lines: string[],\n ctx: TransitionsCtx<S, M>,\n): void {\n type CellFn = (state: S, msg: M) => readonly [S, readonly C[]];\n type Table = Record<string, Record<string, CellFn>>;\n const table = machine.update as unknown as Table;\n const stateTypes = Object.keys(table);\n\n // Declare every node up front so even unreachable phases render.\n for (const st of stateTypes) {\n lines.push(` ${safeId(st)}`);\n }\n\n // Init edge — only resolvable with a ctx sample. `init(null, ctx)` is pure\n // (invariant 2: fresh-boot branch returns a starting state). Read `next.type`.\n if (ctx.hasCtx) {\n const [initState] = machine.init(null, ctx.ctx as Ctx);\n const initType = (initState as unknown as { type: string }).type;\n lines.push(` [*] --> ${safeId(initType)}`);\n } else {\n lines.push(\n \" %% init edge omitted: no samples.ctx provided to run init(null, ctx)\",\n );\n }\n\n let anyStructural = false;\n for (const st of stateTypes) {\n const row = table[st];\n if (row === undefined) continue;\n const msgTypes = Object.keys(row);\n for (const mt of msgTypes) {\n const edge = resolveEdge(row, st, mt, ctx);\n if (edge.kind === \"resolved\") {\n lines.push(\n ` ${safeId(edge.from)} --> ${safeId(edge.to)} : ${safeLabel(edge.msg)}`,\n );\n } else {\n anyStructural = true;\n // Structural self-loop with a `?` suffix — target unresolved. Visually\n // distinct from a resolved edge (self-loop + trailing `?`).\n lines.push(\n ` ${safeId(edge.from)} --> ${safeId(edge.from)} : ${safeLabel(edge.msg)}?`,\n );\n }\n }\n }\n\n // Legend — document the resolved-vs-structural distinction in the output.\n lines.push(\" note right of \" + safeId(stateTypes[0] ?? \"_empty\"));\n lines.push(\" Transitions-form machine.\");\n lines.push(\" Edge a --> b : msg = RESOLVED (cell executed with samples).\");\n if (anyStructural) {\n lines.push(\n \" Self-loop a --> a : msg? = STRUCTURAL (no sample; target unresolved).\",\n );\n }\n lines.push(\" end note\");\n}\n\n/**\n * Resolve a single `(stateType, msgType)` cell into an Edge. With both a sample\n * state and a sample msg, EXECUTE the pure cell to read the real target. Any\n * missing sample → structural. A thrown cell (e.g. a sample shaped wrong for\n * the cell) degrades to structural rather than crashing the whole diagram —\n * the diagram is a best-effort visualization, not a correctness gate.\n */\nfunction resolveEdge<S, M extends { type: string }, C extends Cmd>(\n row: Record<string, (state: S, msg: M) => readonly [S, readonly C[]]>,\n stateType: string,\n msgType: string,\n ctx: {\n stateSamples: Partial<Record<string, S>>;\n msgSamples: Partial<Record<string, M>>;\n },\n): Edge {\n const sampleState = ctx.stateSamples[stateType];\n const sampleMsg = ctx.msgSamples[msgType];\n const cell = row[msgType];\n if (\n sampleState === undefined ||\n sampleMsg === undefined ||\n cell === undefined\n ) {\n return { kind: \"structural\", from: stateType, msg: msgType };\n }\n try {\n const [next] = cell(sampleState, sampleMsg);\n const to = (next as unknown as { type: string }).type;\n return { kind: \"resolved\", from: stateType, to, msg: msgType };\n } catch {\n // Mismatched sample → cannot resolve. Fall back to structural.\n return { kind: \"structural\", from: stateType, msg: msgType };\n }\n}\n\n// === Subscriptions annotation ===\n\n/**\n * For each sampled state, call the (pure) `subscriptions(state)` description\n * function and annotate which Sub types are active in that state as a Mermaid\n * note. We call `subscriptions` (the description) but NEVER `subscribe[*]` (the\n * effectful installer) — same boundary `replay` honors. Skipped when no state\n * samples are provided or the machine declares no `subscriptions`.\n */\nfunction emitSubscriptions<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n lines: string[],\n stateSamples: Partial<Record<string, S>>,\n): void {\n if (machine.subscriptions === undefined) return;\n const stateTypes = Object.keys(stateSamples);\n if (stateTypes.length === 0) return;\n\n for (const st of stateTypes) {\n const sample = stateSamples[st];\n if (sample === undefined) continue;\n const subs = machine.subscriptions(sample);\n const subTypes = subs.map((s) => s.type);\n const body =\n subTypes.length === 0\n ? \"(no active subs)\"\n : `subs: ${subTypes.map(safeLabel).join(\", \")}`;\n lines.push(` note left of ${safeId(st)}`);\n lines.push(` ${body}`);\n lines.push(\" end note\");\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/machine-viz/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/machine-viz — turn a `Machine` into a Mermaid diagram string.\n *\n * PURE. No deps, no AST parsing, no file I/O. The function reads only the\n * `Machine` value it is handed plus optional sample states/msgs/ctx.\n *\n * The trick: TEA update cells are PURE (invariant 2 — they return\n * `[nextState, cmds]` as data, no effects). So we can SAFELY EXECUTE a cell to\n * discover its target state — exactly what `replay` does. That gives us REAL\n * edges (`red --> green : go`) without parsing source. When no sample is\n * provided for a `(state, msg)` pair we cannot resolve the target, so we fall\n * back to a STRUCTURAL self-edge annotated with `?` — visually distinct from a\n * resolved edge and documented in the diagram's legend.\n *\n * Form detection is `formOf(machine)` — the same reader `run`/`replay` use,\n * honoring the authoritative `__form` tag `defineMachine` stamps (falling back\n * to the structural heuristic for machines built without it; #275).\n *\n * What we MUST NOT do (these are the effectful paths, off-limits here):\n * - call `interpret[*]` handlers (perform I/O)\n * - call `subscribe[*]` handlers (install listeners)\n * - call `run` / the runtime\n * We MAY call `init`, `update[*]` cells, and `subscriptions(state)` — all pure\n * by the invariants (same set `replay` is allowed to touch).\n */\n\nimport type { Cmd, Machine, Sub } from \"../index\";\nimport { formOf } from \"../index\";\n\n// === Public options ===\n\n/**\n * Options controlling diagram emission.\n *\n * `samples` is the lever that turns STRUCTURAL edges into RESOLVED edges. For a\n * Transitions machine, providing a sample state for `stateType` AND a sample\n * msg for `msgType` lets us execute `update[stateType][msgType](state, msg)` and\n * read the real target `next.type`. Sample maps are keyed by the discriminant\n * string (`state.type` / `msg.type`).\n *\n * `samples.ctx` is the argument to `init(null, ctx)` — provide it to draw the\n * `[*] -->` initial-state edge. Omitted ⇒ no init edge (annotated as such),\n * because `init`'s signature requires a ctx and we will not invent one.\n */\nexport interface MachineVizOptions<S, M> {\n samples?: {\n /** Sample state per `state.type` — unlocks RESOLVED edges + sub annotations. */\n states?: Partial<Record<string, S>>;\n /** Sample msg per `msg.type` — unlocks RESOLVED edges. */\n msgs?: Partial<Record<string, M>>;\n /** Ctx for `init(null, ctx)` — unlocks the `[*] -->` initial-state edge. */\n ctx?: unknown;\n };\n /** Mermaid layout direction. Default `\"TB\"` (top-to-bottom). */\n direction?: \"TB\" | \"LR\";\n /** Optional diagram title, emitted as a Mermaid front-matter `title:` block. */\n title?: string;\n}\n\n// === Mermaid label escaping ===\n//\n// Mermaid state ids must be identifier-safe; arbitrary `type` strings (which\n// CAN contain `:`, spaces, `-`, etc.) are not. We sanitize the id used as the\n// node identifier and carry the human-readable original as a quoted label via\n// `stateId : \"Original\"` when they differ. Edge transition labels (after the\n// `:`) are sanitized to avoid breaking the arrow syntax.\n\n/** Sanitize a discriminant string into a Mermaid-safe node identifier. */\nfunction safeId(raw: string): string {\n // Replace any run of non-identifier chars with `_`; prefix a leading digit so\n // the id is always a valid Mermaid token. Empty ⇒ a stable placeholder.\n const cleaned = raw.replace(/[^A-Za-z0-9_]/g, \"_\");\n if (cleaned === \"\") return \"_empty\";\n return /^[0-9]/.test(cleaned) ? `s_${cleaned}` : cleaned;\n}\n\n/** Sanitize text used as a Mermaid edge/transition label (after the `:`). */\nfunction safeLabel(raw: string): string {\n // Strip the chars that terminate or confuse a transition label: newlines,\n // the `:` that separates label from target, and Mermaid's `\"`/`;` delimiters.\n return raw.replace(/[\"\\n\\r;:]/g, \" \").trim();\n}\n\n// === Edge model ===\n\ntype Edge =\n // Resolved: we executed the cell with samples and read the real target.\n | { kind: \"resolved\"; from: string; to: string; msg: string }\n // Structural: no samples for the pair → target unknown, drawn as a self-loop.\n | { kind: \"structural\"; from: string; msg: string };\n\n// === toMermaid ===\n\n/**\n * Render a `Machine` as a Mermaid `stateDiagram-v2` string.\n *\n * - **Transitions form** (`update` is a record-of-records): nodes are the\n * `state.type` keys; each `(stateType × msgType)` cell becomes an edge. With\n * samples for both, the edge is RESOLVED by executing the (pure) cell. Without\n * samples it is STRUCTURAL — a `from --> from : msg?` self-loop (the `?`\n * suffix marks \"target unresolved\"). A legend note documents the distinction.\n * The `[*] -->` init edge is drawn only when `samples.ctx` is provided.\n *\n * - **Reducer form** (`update` values are functions): there is no static phase\n * graph (state is not a discriminated union over phases). We emit a single\n * node listing the handled `msg.type`s and an honest note saying so. No edges\n * are invented.\n *\n * Output is deterministic for the same input (keys iterated in insertion order,\n * no clocks, no randomness).\n */\nexport function toMermaid<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n // `NoInfer` pins S/M to the `machine` argument so sample object literals in\n // `opts` are CHECKED against the machine's State/Msg unions rather than\n // driving inference (which would widen `{ type: \"open\" }` to `{ type: string }`\n // and collide with the machine's discriminated union).\n opts: MachineVizOptions<NoInfer<S>, NoInfer<M>> = {},\n): string {\n const direction = opts.direction ?? \"TB\";\n const stateSamples = opts.samples?.states ?? {};\n const msgSamples = opts.samples?.msgs ?? {};\n const hasCtx = opts.samples !== undefined && \"ctx\" in opts.samples;\n\n const mode = formOf(machine);\n const lines: string[] = [];\n\n // Front matter — Mermaid reads a YAML `title:` block before the diagram.\n if (opts.title !== undefined) {\n lines.push(\"---\", `title: ${opts.title}`, \"---\");\n }\n lines.push(\"stateDiagram-v2\");\n lines.push(` direction ${direction}`);\n\n if (mode === \"reducer\") {\n emitReducer(machine, lines);\n } else {\n emitTransitions(machine, lines, {\n stateSamples,\n msgSamples,\n hasCtx,\n ctx: opts.samples?.ctx,\n });\n }\n\n // Subscriptions annotation — pure call to `subscriptions(sampleState)` per\n // sampled state. Skipped entirely when no state samples are provided (we\n // refuse to invent a state to feed it).\n emitSubscriptions(machine, lines, stateSamples);\n\n return lines.join(\"\\n\");\n}\n\n// === Reducer-form emission ===\n\n/**\n * Reducer form has no phase graph. Emit one node listing the handled msg types\n * plus an honest note. The msg set is the `update` record's keys (the Reducer\n * mapped type guarantees one cell per Msg variant).\n */\nfunction emitReducer(machine: { update: unknown }, lines: string[]): void {\n const msgTypes = Object.keys(machine.update as object);\n lines.push(\" state ReducerMachine {\");\n if (msgTypes.length === 0) {\n lines.push(\" [*] --> NoMsgs\");\n lines.push(' NoMsgs : \"(no msg variants — M is never)\"');\n } else {\n for (const m of msgTypes) {\n lines.push(` ${safeId(m)} : ${safeLabel(m)}`);\n }\n }\n lines.push(\" }\");\n // The note IS the design honesty: no edges, because reducer-form state is not\n // a discriminated union over phases — there is no static target to draw.\n lines.push(\" note right of ReducerMachine\");\n lines.push(\" Reducer-form machine: flat dispatch keyed by msg.type.\");\n lines.push(\n \" No phase graph — state is not a discriminated union over phases.\",\n );\n lines.push(` Handled msg types: ${msgTypes.length}`);\n lines.push(\" end note\");\n}\n\n// === Transitions-form emission ===\n\ninterface TransitionsCtx<S, M> {\n stateSamples: Partial<Record<string, S>>;\n msgSamples: Partial<Record<string, M>>;\n hasCtx: boolean;\n ctx: unknown;\n}\n\n/**\n * Transitions form: walk the `update[stateType][msgType]` table. Execute the\n * (pure) cell when both samples exist → RESOLVED edge; otherwise → STRUCTURAL\n * self-loop. Draw the `[*] -->` init edge only when a ctx sample is given.\n */\nfunction emitTransitions<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n lines: string[],\n ctx: TransitionsCtx<S, M>,\n): void {\n type CellFn = (state: S, msg: M) => readonly [S, readonly C[]];\n type Table = Record<string, Record<string, CellFn>>;\n const table = machine.update as unknown as Table;\n const stateTypes = Object.keys(table);\n\n // Declare every node up front so even unreachable phases render.\n for (const st of stateTypes) {\n lines.push(` ${safeId(st)}`);\n }\n\n // Init edge — only resolvable with a ctx sample. `init(null, ctx)` is pure\n // (invariant 2: fresh-boot branch returns a starting state). Read `next.type`.\n if (ctx.hasCtx) {\n const [initState] = machine.init(null, ctx.ctx as Ctx);\n const initType = (initState as unknown as { type: string }).type;\n lines.push(` [*] --> ${safeId(initType)}`);\n } else {\n lines.push(\n \" %% init edge omitted: no samples.ctx provided to run init(null, ctx)\",\n );\n }\n\n let anyStructural = false;\n for (const st of stateTypes) {\n const row = table[st];\n if (row === undefined) continue;\n const msgTypes = Object.keys(row);\n for (const mt of msgTypes) {\n const edge = resolveEdge(row, st, mt, ctx);\n if (edge.kind === \"resolved\") {\n lines.push(\n ` ${safeId(edge.from)} --> ${safeId(edge.to)} : ${safeLabel(edge.msg)}`,\n );\n } else {\n anyStructural = true;\n // Structural self-loop with a `?` suffix — target unresolved. Visually\n // distinct from a resolved edge (self-loop + trailing `?`).\n lines.push(\n ` ${safeId(edge.from)} --> ${safeId(edge.from)} : ${safeLabel(edge.msg)}?`,\n );\n }\n }\n }\n\n // Legend — document the resolved-vs-structural distinction in the output.\n lines.push(` note right of ${safeId(stateTypes[0] ?? \"_empty\")}`);\n lines.push(\" Transitions-form machine.\");\n lines.push(\" Edge a --> b : msg = RESOLVED (cell executed with samples).\");\n if (anyStructural) {\n lines.push(\n \" Self-loop a --> a : msg? = STRUCTURAL (no sample; target unresolved).\",\n );\n }\n lines.push(\" end note\");\n}\n\n/**\n * Resolve a single `(stateType, msgType)` cell into an Edge. With both a sample\n * state and a sample msg, EXECUTE the pure cell to read the real target. Any\n * missing sample → structural. A thrown cell (e.g. a sample shaped wrong for\n * the cell) degrades to structural rather than crashing the whole diagram —\n * the diagram is a best-effort visualization, not a correctness gate.\n */\nfunction resolveEdge<S, M extends { type: string }, C extends Cmd>(\n row: Record<string, (state: S, msg: M) => readonly [S, readonly C[]]>,\n stateType: string,\n msgType: string,\n ctx: {\n stateSamples: Partial<Record<string, S>>;\n msgSamples: Partial<Record<string, M>>;\n },\n): Edge {\n const sampleState = ctx.stateSamples[stateType];\n const sampleMsg = ctx.msgSamples[msgType];\n const cell = row[msgType];\n if (\n sampleState === undefined ||\n sampleMsg === undefined ||\n cell === undefined\n ) {\n return { kind: \"structural\", from: stateType, msg: msgType };\n }\n try {\n const [next] = cell(sampleState, sampleMsg);\n const to = (next as unknown as { type: string }).type;\n return { kind: \"resolved\", from: stateType, to, msg: msgType };\n } catch {\n // Mismatched sample → cannot resolve. Fall back to structural.\n return { kind: \"structural\", from: stateType, msg: msgType };\n }\n}\n\n// === Subscriptions annotation ===\n\n/**\n * For each sampled state, call the (pure) `subscriptions(state)` description\n * function and annotate which Sub types are active in that state as a Mermaid\n * note. We call `subscriptions` (the description) but NEVER `subscribe[*]` (the\n * effectful installer) — same boundary `replay` honors. Skipped when no state\n * samples are provided or the machine declares no `subscriptions`.\n */\nfunction emitSubscriptions<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n lines: string[],\n stateSamples: Partial<Record<string, S>>,\n): void {\n if (machine.subscriptions === undefined) return;\n const stateTypes = Object.keys(stateSamples);\n if (stateTypes.length === 0) return;\n\n for (const st of stateTypes) {\n const sample = stateSamples[st];\n if (sample === undefined) continue;\n const subs = machine.subscriptions(sample);\n const subTypes = subs.map((s) => s.type);\n const body =\n subTypes.length === 0\n ? \"(no active subs)\"\n : `subs: ${subTypes.map(safeLabel).join(\", \")}`;\n lines.push(` note left of ${safeId(st)}`);\n lines.push(` ${body}`);\n lines.push(\" end note\");\n }\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { Store } from '../index.js';
2
+ import '../core-BoB6t7dB.js';
2
3
 
3
4
  /**
4
5
  * @demlik/tea/mem — in-memory `Store<S>` adapter for `@demlik/tea`.