@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.
- package/LICENSE +21 -0
- package/dist/agent/index.d.ts +907 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/authed-call/index.d.ts +119 -0
- package/dist/authed-call/index.js +148 -0
- package/dist/authed-call/index.js.map +1 -0
- package/dist/await-terminal/index.d.ts +99 -0
- package/dist/await-terminal/index.js +75 -0
- package/dist/await-terminal/index.js.map +1 -0
- package/dist/batch-window/index.d.ts +259 -0
- package/dist/batch-window/index.js +43 -0
- package/dist/batch-window/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -50
- package/dist/cache/index.js.map +1 -1
- package/dist/chunk-27N2SPTN.js +222 -0
- package/dist/chunk-27N2SPTN.js.map +1 -0
- package/dist/chunk-2YOE5PIS.js +45 -0
- package/dist/chunk-2YOE5PIS.js.map +1 -0
- package/dist/chunk-3545BOYL.js +65 -0
- package/dist/chunk-3545BOYL.js.map +1 -0
- package/dist/chunk-3XJWC7VW.js +61 -0
- package/dist/chunk-3XJWC7VW.js.map +1 -0
- package/dist/chunk-5DEQ5I3U.js +132 -0
- package/dist/chunk-5DEQ5I3U.js.map +1 -0
- package/dist/chunk-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.js.map +1 -0
- package/dist/chunk-6RGBKODV.js +268 -0
- package/dist/chunk-6RGBKODV.js.map +1 -0
- package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
- package/dist/chunk-7FYQGDMF.js.map +1 -0
- package/dist/chunk-7W4LMPUW.js +186 -0
- package/dist/chunk-7W4LMPUW.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
- package/dist/chunk-DILMKOZ7.js.map +1 -0
- package/dist/chunk-EHIX7NYZ.js +93 -0
- package/dist/chunk-EHIX7NYZ.js.map +1 -0
- package/dist/chunk-HCEVHFJ7.js +54 -0
- package/dist/chunk-HCEVHFJ7.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.js.map +1 -0
- package/dist/chunk-J6527VRQ.js +56 -0
- package/dist/chunk-J6527VRQ.js.map +1 -0
- package/dist/chunk-JELSZN25.js +47 -0
- package/dist/chunk-JELSZN25.js.map +1 -0
- package/dist/chunk-K575PT4S.js +58 -0
- package/dist/chunk-K575PT4S.js.map +1 -0
- package/dist/chunk-K7Q23EQ4.js +20 -0
- package/dist/chunk-K7Q23EQ4.js.map +1 -0
- package/dist/chunk-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
- package/dist/chunk-LP7VQBRG.js.map +1 -0
- package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
- package/dist/chunk-MMH7CW5K.js.map +1 -0
- package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
- package/dist/chunk-NSDFA6M6.js.map +1 -0
- package/dist/chunk-ON24RKSE.js +60 -0
- package/dist/chunk-ON24RKSE.js.map +1 -0
- package/dist/chunk-PS23H6KQ.js +87 -0
- package/dist/chunk-PS23H6KQ.js.map +1 -0
- package/dist/chunk-QLA76GIT.js +162 -0
- package/dist/chunk-QLA76GIT.js.map +1 -0
- package/dist/chunk-S3FQN4SL.js +506 -0
- package/dist/chunk-S3FQN4SL.js.map +1 -0
- package/dist/chunk-SC3FAFZN.js +53 -0
- package/dist/chunk-SC3FAFZN.js.map +1 -0
- package/dist/chunk-SINXE47Y.js +55 -0
- package/dist/chunk-SINXE47Y.js.map +1 -0
- package/dist/chunk-SQTCS44W.js +33 -0
- package/dist/chunk-SQTCS44W.js.map +1 -0
- package/dist/chunk-TGG26XMK.js +52 -0
- package/dist/chunk-TGG26XMK.js.map +1 -0
- package/dist/chunk-UV6UWGEO.js +147 -0
- package/dist/chunk-UV6UWGEO.js.map +1 -0
- package/dist/chunk-UYWUVEP6.js +158 -0
- package/dist/chunk-UYWUVEP6.js.map +1 -0
- package/dist/chunk-VLGARN6H.js +202 -0
- package/dist/chunk-VLGARN6H.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-BoB6t7dB.d.ts +188 -0
- package/dist/deadline/index.d.ts +24 -5
- package/dist/deadline/index.js +4 -18
- package/dist/deadline/index.js.map +1 -1
- package/dist/debounce/index.d.ts +6 -0
- package/dist/debounce/index.js +1 -54
- package/dist/debounce/index.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/devtools/index.js +18 -6
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +1723 -92
- package/dist/do/index.js +673 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-CO9Uh971.d.ts +180 -0
- package/dist/extension/index.d.ts +31 -13
- package/dist/extension/index.js +5 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +8 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/extension/test-utils.js +11 -3
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +239 -0
- package/dist/fan-out/index.js +5 -0
- package/dist/fan-out/index.js.map +1 -0
- package/dist/idempotency/adapter.d.ts +62 -0
- package/dist/idempotency/adapter.js +4 -0
- package/dist/idempotency/adapter.js.map +1 -0
- package/dist/idempotency/index.js +1 -56
- package/dist/idempotency/index.js.map +1 -1
- package/dist/idempotent-intake/index.d.ts +186 -0
- package/dist/idempotent-intake/index.js +7 -0
- package/dist/idempotent-intake/index.js.map +1 -0
- package/dist/index.d.ts +341 -191
- package/dist/index.js +2 -1
- package/dist/llm-call/index.d.ts +309 -0
- package/dist/llm-call/index.js +14 -0
- package/dist/llm-call/index.js.map +1 -0
- package/dist/machine-viz/index.d.ts +4 -4
- package/dist/machine-viz/index.js +5 -9
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -0
- package/dist/monitored-run/index.d.ts +253 -0
- package/dist/monitored-run/index.js +10 -0
- package/dist/monitored-run/index.js.map +1 -0
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +234 -0
- package/dist/paginated-walk/index.js +15 -0
- package/dist/paginated-walk/index.js.map +1 -0
- package/dist/paginator/index.d.ts +271 -0
- package/dist/paginator/index.js +3 -0
- package/dist/paginator/index.js.map +1 -0
- package/dist/parity/index.d.ts +92 -0
- package/dist/parity/index.js +78 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +15 -8
- package/dist/pbt/index.js +6 -11
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -2
- package/dist/poller/index.d.ts +288 -0
- package/dist/poller/index.js +79 -0
- package/dist/poller/index.js.map +1 -0
- package/dist/prediction/index.d.ts +130 -0
- package/dist/prediction/index.js +4 -0
- package/dist/prediction/index.js.map +1 -0
- package/dist/protocol-B5YmJr0d.d.ts +45 -0
- package/dist/pure/index.d.ts +2 -0
- package/dist/pure/index.js +4 -0
- package/dist/pure/index.js.map +1 -0
- package/dist/raft/demo.d.ts +105 -0
- package/dist/raft/demo.js +172 -0
- package/dist/raft/demo.js.map +1 -0
- package/dist/raft/index.d.ts +548 -0
- package/dist/raft/index.js +7 -0
- package/dist/raft/index.js.map +1 -0
- package/dist/raft/sim.d.ts +222 -0
- package/dist/raft/sim.js +8 -0
- package/dist/raft/sim.js.map +1 -0
- package/dist/raft/timeline.d.ts +46 -0
- package/dist/raft/timeline.js +62 -0
- package/dist/raft/timeline.js.map +1 -0
- package/dist/rate-limit/index.js +1 -43
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +43 -20
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +192 -0
- package/dist/reconciler/index.js +166 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/recorder/index.d.ts +7 -3
- package/dist/recorder/index.js +1 -155
- package/dist/recorder/index.js.map +1 -1
- package/dist/resilient-call/index.d.ts +294 -0
- package/dist/resilient-call/index.js +13 -0
- package/dist/resilient-call/index.js.map +1 -0
- package/dist/retry-backoff/index.d.ts +42 -5
- package/dist/retry-backoff/index.js +1 -35
- package/dist/retry-backoff/index.js.map +1 -1
- package/dist/retry-to-success/index.d.ts +109 -0
- package/dist/retry-to-success/index.js +35 -0
- package/dist/retry-to-success/index.js.map +1 -0
- package/dist/saga/index.d.ts +238 -0
- package/dist/saga/index.js +150 -0
- package/dist/saga/index.js.map +1 -0
- package/dist/snapshot/index.d.ts +334 -0
- package/dist/snapshot/index.js +5 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/subs/index.d.ts +48 -10
- package/dist/subs/index.js +1 -1
- package/dist/testing/index.d.ts +107 -2
- package/dist/testing/index.js +204 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/throttle/index.d.ts +9 -0
- package/dist/throttle/index.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +380 -0
- package/dist/throttled-input/index.js +88 -0
- package/dist/throttled-input/index.js.map +1 -0
- package/dist/token-refresh/index.d.ts +215 -0
- package/dist/token-refresh/index.js +6 -0
- package/dist/token-refresh/index.js.map +1 -0
- package/dist/trace-replay/index.d.ts +11 -2
- package/dist/trace-replay/index.js +3 -55
- package/dist/trace-replay/index.js.map +1 -1
- package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +111 -0
- package/dist/with-deadline/index.js.map +1 -0
- package/dist/with-resilience/index.d.ts +205 -0
- package/dist/with-resilience/index.js +251 -0
- package/dist/with-resilience/index.js.map +1 -0
- package/dist/with-telemetry/index.d.ts +149 -0
- package/dist/with-telemetry/index.js +70 -0
- package/dist/with-telemetry/index.js.map +1 -0
- package/dist/work-queue/adapter.d.ts +82 -0
- package/dist/work-queue/adapter.js +4 -0
- package/dist/work-queue/adapter.js.map +1 -0
- package/dist/work-queue/index.d.ts +25 -11
- package/dist/work-queue/index.js +1 -46
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +69 -0
- package/dist/work-queue/ops.js +3 -0
- package/dist/work-queue/ops.js.map +1 -0
- package/dist/workflow/index.d.ts +450 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +168 -25
- package/dist/chunk-CMG2KMWX.js.map +0 -1
- package/dist/chunk-PYBQREFU.js.map +0 -1
- package/dist/chunk-PYOCUBQJ.js.map +0 -1
- package/dist/chunk-Y5JM3UUH.js.map +0 -1
- 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 '../
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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 =
|
|
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(
|
|
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"]}
|