@demlik/tea 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/agent/index.d.ts +2 -2
  2. package/dist/agent/index.js +8 -8
  3. package/dist/authed-call/index.d.ts +2 -2
  4. package/dist/authed-call/index.js +5 -5
  5. package/dist/await-terminal/index.d.ts +1 -1
  6. package/dist/await-terminal/index.js +2 -2
  7. package/dist/batch-window/index.d.ts +2 -2
  8. package/dist/batch-window/index.js +3 -3
  9. package/dist/cache/index.d.ts +2 -2
  10. package/dist/{chunk-EHIX7NYZ.js → chunk-5KVHID6A.js} +4 -4
  11. package/dist/{chunk-EHIX7NYZ.js.map → chunk-5KVHID6A.js.map} +1 -1
  12. package/dist/{chunk-27N2SPTN.js → chunk-6GIGQT6C.js} +4 -4
  13. package/dist/{chunk-27N2SPTN.js.map → chunk-6GIGQT6C.js.map} +1 -1
  14. package/dist/{chunk-3XJWC7VW.js → chunk-6OUDPSSG.js} +3 -3
  15. package/dist/{chunk-3XJWC7VW.js.map → chunk-6OUDPSSG.js.map} +1 -1
  16. package/dist/{chunk-MMH7CW5K.js → chunk-6VDCTR7N.js} +24 -27
  17. package/dist/chunk-6VDCTR7N.js.map +1 -0
  18. package/dist/{chunk-QLA76GIT.js → chunk-DYRKDCJI.js} +30 -5
  19. package/dist/chunk-DYRKDCJI.js.map +1 -0
  20. package/dist/{chunk-LP7VQBRG.js → chunk-EGWTT5XJ.js} +3 -3
  21. package/dist/{chunk-LP7VQBRG.js.map → chunk-EGWTT5XJ.js.map} +1 -1
  22. package/dist/{chunk-DILMKOZ7.js → chunk-EMJTPHI7.js} +4 -4
  23. package/dist/{chunk-DILMKOZ7.js.map → chunk-EMJTPHI7.js.map} +1 -1
  24. package/dist/{chunk-5DEQ5I3U.js → chunk-FOGL3TC7.js} +3 -3
  25. package/dist/{chunk-5DEQ5I3U.js.map → chunk-FOGL3TC7.js.map} +1 -1
  26. package/dist/{chunk-SQTCS44W.js → chunk-G2U7DJHL.js} +3 -3
  27. package/dist/{chunk-SQTCS44W.js.map → chunk-G2U7DJHL.js.map} +1 -1
  28. package/dist/{chunk-ON24RKSE.js → chunk-IK4S5DJN.js} +3 -3
  29. package/dist/{chunk-ON24RKSE.js.map → chunk-IK4S5DJN.js.map} +1 -1
  30. package/dist/{chunk-PS23H6KQ.js → chunk-M6UZT7HS.js} +3 -3
  31. package/dist/{chunk-PS23H6KQ.js.map → chunk-M6UZT7HS.js.map} +1 -1
  32. package/dist/{chunk-K7Q23EQ4.js → chunk-SKAMZKTE.js} +3 -3
  33. package/dist/{chunk-K7Q23EQ4.js.map → chunk-SKAMZKTE.js.map} +1 -1
  34. package/dist/{chunk-S3FQN4SL.js → chunk-VFGKTGGC.js} +7 -7
  35. package/dist/{chunk-S3FQN4SL.js.map → chunk-VFGKTGGC.js.map} +1 -1
  36. package/dist/{chunk-7W4LMPUW.js → chunk-ZDRN3YCE.js} +4 -4
  37. package/dist/{chunk-7W4LMPUW.js.map → chunk-ZDRN3YCE.js.map} +1 -1
  38. package/dist/{core-BoB6t7dB.d.ts → core-BmlJOvxM.d.ts} +10 -1
  39. package/dist/deadline/index.d.ts +2 -2
  40. package/dist/deadline/index.js +3 -3
  41. package/dist/do/index.d.ts +451 -335
  42. package/dist/do/index.js +172 -144
  43. package/dist/do/index.js.map +1 -1
  44. package/dist/{durable-effects-CO9Uh971.d.ts → durable-effects-9LxFpmh3.d.ts} +1 -1
  45. package/dist/extension/index.d.ts +1 -1
  46. package/dist/extension/index.js +3 -3
  47. package/dist/extension/react.js +3 -3
  48. package/dist/extension/subs/index.d.ts +2 -2
  49. package/dist/fan-out/index.d.ts +1 -1
  50. package/dist/idempotent-intake/index.d.ts +1 -1
  51. package/dist/index.d.ts +20 -3
  52. package/dist/index.js +2 -2
  53. package/dist/llm-call/index.d.ts +2 -2
  54. package/dist/llm-call/index.js +5 -5
  55. package/dist/machine-viz/index.d.ts +1 -1
  56. package/dist/machine-viz/index.js +2 -2
  57. package/dist/mem/index.d.ts +1 -1
  58. package/dist/monitored-run/index.d.ts +2 -2
  59. package/dist/monitored-run/index.js +5 -5
  60. package/dist/node/index.d.ts +1 -1
  61. package/dist/paginated-walk/index.d.ts +2 -2
  62. package/dist/paginated-walk/index.js +5 -5
  63. package/dist/parity/index.d.ts +1 -1
  64. package/dist/parity/index.js +3 -3
  65. package/dist/pbt/arbitraries/index.d.ts +1 -1
  66. package/dist/pbt/index.d.ts +1 -1
  67. package/dist/pbt/index.js +3 -3
  68. package/dist/pbt/runners/index.d.ts +1 -1
  69. package/dist/pbt/runners/index.js +3 -3
  70. package/dist/poller/index.d.ts +2 -2
  71. package/dist/poller/index.js +3 -3
  72. package/dist/prediction/index.d.ts +1 -1
  73. package/dist/prediction/index.js +2 -2
  74. package/dist/pure/index.d.ts +1 -1
  75. package/dist/pure/index.js +2 -2
  76. package/dist/react/index.d.ts +1 -1
  77. package/dist/react/index.js +2 -2
  78. package/dist/reconciler/index.d.ts +2 -2
  79. package/dist/reconciler/index.js +5 -5
  80. package/dist/recorder/index.d.ts +1 -1
  81. package/dist/resilient-call/index.d.ts +2 -2
  82. package/dist/resilient-call/index.js +4 -4
  83. package/dist/saga/index.d.ts +1 -1
  84. package/dist/snapshot/index.d.ts +1 -1
  85. package/dist/snapshot/index.js +3 -3
  86. package/dist/subs/index.d.ts +2 -2
  87. package/dist/testing/index.d.ts +1 -1
  88. package/dist/testing/index.js +2 -2
  89. package/dist/throttled-input/index.d.ts +2 -2
  90. package/dist/throttled-input/index.js +4 -4
  91. package/dist/token-refresh/index.d.ts +1 -1
  92. package/dist/token-refresh/index.js +3 -3
  93. package/dist/trace-replay/index.d.ts +1 -1
  94. package/dist/trace-replay/index.js +3 -3
  95. package/dist/{types-B-8bcAp8.d.ts → types-BoUWgwSm.d.ts} +1 -1
  96. package/dist/with-deadline/index.d.ts +1 -1
  97. package/dist/with-deadline/index.js +2 -2
  98. package/dist/with-resilience/index.d.ts +2 -2
  99. package/dist/with-resilience/index.js +4 -4
  100. package/dist/with-telemetry/index.d.ts +1 -1
  101. package/dist/with-telemetry/index.js +2 -2
  102. package/dist/work-queue/adapter.d.ts +1 -1
  103. package/dist/work-queue/index.d.ts +1 -1
  104. package/dist/work-queue/ops.d.ts +1 -1
  105. package/dist/workflow/index.d.ts +3 -3
  106. package/package.json +1 -19
  107. package/dist/chunk-6RGBKODV.js +0 -268
  108. package/dist/chunk-6RGBKODV.js.map +0 -1
  109. package/dist/chunk-MMH7CW5K.js.map +0 -1
  110. package/dist/chunk-QLA76GIT.js.map +0 -1
  111. package/dist/chunk-VLGARN6H.js +0 -202
  112. package/dist/chunk-VLGARN6H.js.map +0 -1
  113. package/dist/raft/demo.d.ts +0 -105
  114. package/dist/raft/demo.js +0 -172
  115. package/dist/raft/demo.js.map +0 -1
  116. package/dist/raft/index.d.ts +0 -548
  117. package/dist/raft/index.js +0 -7
  118. package/dist/raft/index.js.map +0 -1
  119. package/dist/raft/sim.d.ts +0 -222
  120. package/dist/raft/sim.js +0 -8
  121. package/dist/raft/sim.js.map +0 -1
  122. package/dist/raft/timeline.d.ts +0 -46
  123. package/dist/raft/timeline.js +0 -62
  124. package/dist/raft/timeline.js.map +0 -1
@@ -1,15 +1,15 @@
1
1
  import { Store, Runtime, BootingRuntime } from '../index.js';
2
- import { P as PendingEffectsRecorder, b as EffectOwed, a as EffectConfirmed } from '../durable-effects-CO9Uh971.js';
3
- export { D as DeliveryId, E as EffectLedgerEvent, O as OwedEffect, d as PendingEffectsLedger, c as applyEffectEvent, e as emptyLedger, f as foldLedger, i as isOwed, p as pendingEffectsLedger, s as survivingEffects } from '../durable-effects-CO9Uh971.js';
2
+ import { P as PendingEffectsRecorder, b as EffectOwed, a as EffectConfirmed } from '../durable-effects-9LxFpmh3.js';
3
+ export { D as DeliveryId, E as EffectLedgerEvent, O as OwedEffect, d as PendingEffectsLedger, c as applyEffectEvent, e as emptyLedger, f as foldLedger, i as isOwed, p as pendingEffectsLedger, s as survivingEffects } from '../durable-effects-9LxFpmh3.js';
4
4
  import { RetryPolicy, Rng } from '../retry-backoff/index.js';
5
- import { C as Cmd, S as Sub, M as Machine } from '../core-BoB6t7dB.js';
6
- import { AgentTurn, AgentState, AgentMachineMsg, AgentEvent, AgentStatus } from '../agent/index.js';
5
+ import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
6
+ import { AgentEvent, AgentTurn, AgentState, AgentMachineMsg, AgentStatus } from '../agent/index.js';
7
7
  import '../fan-out/index.js';
8
8
  import '../work-queue/index.js';
9
9
  import '../llm-call/index.js';
10
10
  import '../resilient-call/index.js';
11
11
  import '../cache/index.js';
12
- import '../types-B-8bcAp8.js';
12
+ import '../types-BoUWgwSm.js';
13
13
  import '../circuit-breaker/index.js';
14
14
  import '../deadline/index.js';
15
15
  import '../protocol-B5YmJr0d.js';
@@ -117,6 +117,13 @@ interface NextStep<I> {
117
117
  * A `/step` response — a discriminated union on `done`:
118
118
  * - `{ done: false, step }` — execute `step`, POST its result, ask again.
119
119
  * - `{ done: true, output }` — the run is terminal; `output` is its result.
120
+ *
121
+ * This is the ORIGINAL 2-arm INLINE contract, kept byte-identical: a host that
122
+ * resolves each step inside the held request returns exactly these two shapes.
123
+ * The not-ready arm ({@link StepWorking}) is a SEPARATE, opt-in type reachable
124
+ * only through the defer-resume hook — see {@link DeferredStepResponse}. Keeping
125
+ * `StepResponse` a strict subset is what makes inline adopters unaffected: their
126
+ * `if (r.done) … else r.step` narrowing never sees a third member.
120
127
  */
121
128
  type StepResponse<I, O> = {
122
129
  readonly done: false;
@@ -125,6 +132,37 @@ type StepResponse<I, O> = {
125
132
  readonly done: true;
126
133
  readonly output: O;
127
134
  };
135
+ /**
136
+ * The NOT-READY arm — the run is computing OUT-OF-BAND under a defer-resume host
137
+ * (Binclusive ADR 0035 / prod incident #1873: a non-blocking pull carrier must
138
+ * answer a pull with an explicit "computing, poll again" instead of holding the
139
+ * request across a multi-second step). The held `/step` request returns this
140
+ * IMMEDIATELY instead of inline-awaiting `engine.resume`; the hands re-poll
141
+ * (optionally after `retryAfterMs`) until the deferred compute lands in the
142
+ * durable checkpoint and a real {@link StepResponse} step/done arm is returned.
143
+ *
144
+ * It is a first-class discriminated member (`working: true`), NOT a hollow
145
+ * `{ done: false }` with an absent/stale `step` — the not-ready state is made
146
+ * representable so it can never be confused with a real pending step.
147
+ */
148
+ interface StepWorking {
149
+ readonly done: false;
150
+ readonly working: true;
151
+ /**
152
+ * Advisory earliest re-poll delay as a hint to the hands (ms to wait before
153
+ * the next pull). Purely advisory — a host may omit it and the hands fall back
154
+ * to their own backoff curve.
155
+ */
156
+ readonly retryAfterMs?: number;
157
+ }
158
+ /**
159
+ * The 3-arm response a DEFER-RESUME host returns — the inline {@link StepResponse}
160
+ * arms PLUS {@link StepWorking}. A superset of `StepResponse`, so a 2-arm value is
161
+ * assignable to it (the widening is safe for existing transports); discriminate
162
+ * with `if (r.done)` for terminal, then `"working" in r` for the not-ready arm,
163
+ * else the step arm.
164
+ */
165
+ type DeferredStepResponse<I, O> = StepResponse<I, O> | StepWorking;
128
166
  /**
129
167
  * Mint a fresh per-run capability token: 32 random bytes, hex-encoded. Uses the
130
168
  * Web Crypto `crypto.getRandomValues` available in the Workers runtime (and in
@@ -202,6 +240,51 @@ interface StepHostConfig {
202
240
  */
203
241
  readonly giveUpAfterMs?: number;
204
242
  }
243
+ /**
244
+ * The DEFER-RESUME hook — the opt-in seam that drives `engine.resume` OUT of the
245
+ * held `/step` request. A non-blocking host (Binclusive audit carrier, ADR 0035)
246
+ * cannot inline-await a multi-second step inside the held request, so instead of
247
+ * `stepHost` calling `engine.resume` inline it hands the posted result to this
248
+ * hook to SETTLE-AND-ENQUEUE (record it durably, schedule the compute in a
249
+ * returning activation) and returns the {@link StepWorking} arm PROMPTLY. A later
250
+ * pull re-reads the durable next-step once {@link DeferResumeHook.settled} reports
251
+ * the enqueued compute has landed.
252
+ *
253
+ * Providing this hook is what flips `stepHost` from the 2-arm inline contract to
254
+ * the 3-arm deferred contract — see the `stepHost` overloads. Omit it and the
255
+ * host stays byte-identical inline.
256
+ */
257
+ interface DeferResumeHook<R> {
258
+ /**
259
+ * SETTLE-AND-ENQUEUE the posted result to resume OUT of the held request:
260
+ * durably record it and schedule the compute (e.g. `ctx.storage.setAlarm` or a
261
+ * queue) so a returning activation runs `engine.resume`. MUST return promptly —
262
+ * it never awaits the step compute. Called at most once per distinct `callId`
263
+ * (`stepHost` dedupes re-POSTs through `idempotent-intake` first), so a re-POST
264
+ * of the same step does not re-enqueue.
265
+ */
266
+ enqueue(runId: string, posted: StepResult<R>): void | Promise<void>;
267
+ /**
268
+ * Has the enqueued resume for `callId` COMPLETED and been written to the
269
+ * durable checkpoint? `false` ⇒ the held request returns {@link StepWorking}
270
+ * (poll again); `true` ⇒ `stepHost` reads the next step / terminal output off
271
+ * the now-advanced checkpoint, exactly as the inline path does.
272
+ */
273
+ settled(runId: string, callId: string): boolean | Promise<boolean>;
274
+ /**
275
+ * Optional advisory re-poll delay (ms) surfaced on the {@link StepWorking} arm
276
+ * so the hands can back off to the host's expected step latency.
277
+ */
278
+ retryAfterMs?: number;
279
+ }
280
+ /**
281
+ * `StepHostConfig` with the defer-resume hook engaged — the presence of
282
+ * `deferResume` is the type-level switch that selects the 3-arm response (see the
283
+ * `stepHost` overloads). Extends the inline config, so the alarm tuning is shared.
284
+ */
285
+ interface DeferStepHostConfig<R> extends StepHostConfig {
286
+ readonly deferResume: DeferResumeHook<R>;
287
+ }
205
288
  /**
206
289
  * A structured `/step` outcome `stepHost` returns — the response body plus the
207
290
  * HTTP status the consumer's route should send. Kept transport-agnostic (no
@@ -216,6 +299,21 @@ type StepOutcome<I, O> = {
216
299
  readonly error: string;
217
300
  };
218
301
  };
302
+ /**
303
+ * The `/step` outcome a DEFER-RESUME host returns — like {@link StepOutcome} but
304
+ * its 200 body is the 3-arm {@link DeferredStepResponse} (it can carry the
305
+ * not-ready arm). Returned only from the `deferResume` overload of `stepHost`;
306
+ * the inline overload keeps the narrower {@link StepOutcome}.
307
+ */
308
+ type DeferredStepOutcome<I, O> = {
309
+ readonly status: 200;
310
+ readonly body: DeferredStepResponse<I, O>;
311
+ } | {
312
+ readonly status: 401 | 404;
313
+ readonly body: {
314
+ readonly error: string;
315
+ };
316
+ };
219
317
  /**
220
318
  * Build the `/step` handler for a DO that drives one or more runs over the pull
221
319
  * carrier. Returns `handle(request, now)` — call it from the DO's `/step`
@@ -233,11 +331,20 @@ type StepOutcome<I, O> = {
233
331
  * @param engine the run-specific operations
234
332
  * @param config alarm tuning
235
333
  */
334
+ declare function stepHost<R, I, O>(ctx: StepCtx, engine: StepEngine<R, I, O>, config: DeferStepHostConfig<R>): {
335
+ handle(request: StepRequest<R>, now: number): Promise<DeferredStepOutcome<I, O>>;
336
+ };
236
337
  declare function stepHost<R, I, O>(ctx: StepCtx, engine: StepEngine<R, I, O>, config?: StepHostConfig): {
237
338
  handle(request: StepRequest<R>, now: number): Promise<StepOutcome<I, O>>;
238
339
  };
239
- /** The `/step` transport the hands POST through (injected; faked in tests). */
240
- type StepTransport<R, I, O> = (request: StepRequest<R>) => Promise<StepResponse<I, O>>;
340
+ /**
341
+ * The `/step` transport the hands POST through (injected; faked in tests).
342
+ * Returns the 3-arm {@link DeferredStepResponse} — a defer-resume host may answer
343
+ * with the not-ready arm, which {@link runStepLoop} re-polls. An inline host's
344
+ * 2-arm transport (`=> Promise<StepResponse>`) is assignable here (the narrower
345
+ * return widens), so existing inline adopters are unaffected.
346
+ */
347
+ type StepTransport<R, I, O> = (request: StepRequest<R>) => Promise<DeferredStepResponse<I, O>>;
241
348
  /** Execute one tool call and produce its result (the consumer's hands). */
242
349
  type ExecuteStep<I, R> = (step: NextStep<I>) => Promise<R> | R;
243
350
  /** Tuning for {@link runStepLoop}. */
@@ -284,6 +391,11 @@ type StepLoopOutcome<O> = {
284
391
  * not re-executed; its prior result is re-POSTed. Closes the "the host
285
392
  * re-handed the same step after a duplicate settle" path (the host side is
286
393
  * idempotent; the hands match it).
394
+ * - **not-ready re-poll** — a {@link StepWorking} arm (a defer-resume host
395
+ * computing the step out-of-band) is not a step: the loop re-POSTs the SAME
396
+ * `posted` after a backoff (honoring the host's advisory `retryAfterMs`),
397
+ * until a real step / done arrives. Bounded only by the absolute deadline —
398
+ * an inline host never returns this arm, so its drive is unchanged.
287
399
  */
288
400
  declare function runStepLoop<R, I, O>(token: string, runId: string, transport: StepTransport<R, I, O>, execute: ExecuteStep<I, R>, config: RunStepLoopConfig): Promise<StepLoopOutcome<O>>;
289
401
 
@@ -828,177 +940,213 @@ declare function rebuildProjection<Model, Msg extends {
828
940
  }[]): ProjectionRunner<Model, Msg, View>;
829
941
 
830
942
  /**
831
- * @demlik/tea/do — the minimum-viable Durable-Object HOST for a `@demlik/tea`
832
- * `createAgent` runtime.
833
- *
834
- * `doStore` (in `./index`) is the durability half. This file is the rest of the
835
- * boilerplate a DO consumer hand-rolls every time it drives a tea agent over a
836
- * remote transport: the deferred-tool gateway, auto-boot on rehydrate, the
837
- * command-runner WS accept, the runtime→SSE plumbing, and a clean read of a
838
- * run's terminal output. Each export below names exactly the hand-rolled piece
839
- * it deletes from the consumer.
840
- *
841
- * ## The transport model: ONE Sub type, gateway-bridged I/O
842
- *
843
- * `createAgent().toMachine()` fixes the machine's Sub type to `DeadlineSub` —
844
- * the agent owns the retry + watchdog timers and nothing else. That is the
845
- * WHOLE Sub model for a DO-hosted agent, and it is coherent because the
846
- * transport deliberately does NOT live in the Sub system:
847
- *
848
- * - Inbound WebSocket frames are bridged straight into `gateway.settle(...)`
849
- * from the DO's `webSocketMessage` lifecycle method — a direct dispatch,
850
- * not a Sub.
851
- * - The per-tool deadline is a `setTimeout` inside the gateway — a timer, not
852
- * a DO alarm.
853
- *
854
- * The deferred-tool gateway owns each tool round-trip as a Promise the interpret
855
- * cell awaits; the transport bridges results into `dispatch` from outside the
856
- * Sub system. The agent's Sub type stays exactly `DeadlineSub` — there is no
857
- * competing DO-native Sub variant to union in, and the host needs none. This is
858
- * the single transport model for `@demlik/tea/do` (#52); see
859
- * `.patterns/tea/durable-actors.md` for the host-layer north star.
943
+ * The runtime→SSE plumbing — the hub, its projection expression, and the
944
+ * semantic-event adapter. See `./host` for the transport-model rationale.
860
945
  */
861
946
 
862
947
  /**
863
- * The deferred-tool gateway. `R` is the consumer's tool-result type (what
864
- * `agent_tool_ok` carries).
865
- *
866
- * - `await(callId, send, deadlineMs)` — register the round-trip, fire `send`,
867
- * return the Promise the reply (or the deadline) settles. Idempotent per
868
- * `callId`: a second call for an in-flight id returns the SAME promise
869
- * without re-firing `send` (the boot-reconcile re-fire is a no-op at the
870
- * gateway, mirroring the agent's idempotent boot).
871
- * - `settle(callId, result)` — the transport calls this on a reply; resolves
872
- * the awaited promise. A settle for an unknown / already-settled id is a
873
- * no-op (the callId guard the consumer used to keep in its pending Map).
874
- * - `fail(callId, reason)` — settle the round-trip as an error on demand
875
- * (e.g. a transport-level close). Same unknown-id no-op.
876
- * - `inFlight()` — the callIds with an open round-trip (tests / introspection).
948
+ * A set of SSE sinks plus the plumbing to fan an event out to all of them and
949
+ * to open a `text/event-stream` Response wired to a fresh sink.
877
950
  *
878
- * The deadline → reject path is what makes "timeout is data": the tool cell's
879
- * `catch` maps the rejection to `agent_tool_err`, recorded on the conversation,
880
- * never a hang.
881
- */
882
- interface DeferredGateway<R> {
883
- await(callId: string, send: () => void, deadlineMs: number): Promise<R>;
884
- settle(callId: string, result: R): void;
885
- fail(callId: string, reason: string): void;
886
- inFlight(): readonly string[];
887
- }
888
- /**
889
- * Build a deferred-tool gateway. No clock / RNG captured at construction — the
890
- * deadline is per-call (`await(..., deadlineMs)`), so one gateway serves every
891
- * tool family with its own timeout.
951
+ * The consumer:
952
+ * 1. holds one `SseHub<E>` per DO,
953
+ * 2. calls `hub.emit(event)` from its Msg→event mapping (driven off
954
+ * `runtime.observe`), and
955
+ * 3. returns `hub.open()` from the `/sse` route.
892
956
  *
893
- * Deletes from the consumer: the `pending` Map, the `Pending` interface, the
894
- * `awaitClient` method, the `forceTimeout` plumbing, and the `deliverResult`
895
- * callId-guard body — all of it collapses to this object plus the tool cell's
896
- * `await` + try/catch.
957
+ * `register(sink)` exists for tests: subscribe a sink directly and observe the
958
+ * exact event sequence a live client would receive (a live `text/event-stream`
959
+ * fights vitest-pool-workers teardown). Deletes the consumer's `sseSinks` set,
960
+ * `openSse`, `emitSse`, and `onSseEvent`.
897
961
  */
898
- declare function deferredGateway<R>(): DeferredGateway<R>;
899
- /** A `callId`-keyed durable round-trip: maps the gateway's string id to the
900
- * ledger's monotonic delivery id so a `settle(callId)` finds the owed entry. */
901
- interface DurableDeferredGateway<R> extends DeferredGateway<R> {
902
- /** The ledger recorder this gateway feeds (for `surviving()` on activation). */
903
- readonly recorder: PendingEffectsRecorder<{
904
- callId: string;
905
- }>;
962
+ interface SseHub<E> {
963
+ emit(event: E): void;
964
+ open(): Response;
965
+ register(sink: (event: E) => void): () => void;
906
966
  }
967
+ /** Build an SSE hub for event type `E` (the consumer's domain event shape). */
968
+ declare function sseHub<E>(): SseHub<E>;
907
969
  /**
908
- * Wrap a `DeferredGateway<R>` so every round-trip is also recorded in a durable
909
- * `PendingEffectsRecorder`. The in-memory gateway is the fast path; the recorder
910
- * is the durability slice.
911
- *
912
- * - `recordOwed(callId, event)` — fires from `await`, BEFORE `send`, carrying
913
- * the `effect_owed` event the consumer must persist (then deliver).
914
- * - `recordConfirmed(callId, event)` — fires from `settle`/`fail` for a known
915
- * id, carrying the `effect_confirmed` event to persist.
916
- * - `reissue(callId, send, deadlineMs)` — re-arm a surviving round-trip on
917
- * activation. Idempotent: the gateway's own `await` returns the open promise
918
- * without re-firing `send` for an in-flight `callId`, so re-issuing an id
919
- * that is somehow still pending is a no-op (mirrors the boot-reconcile
920
- * re-fire). The effect is already owed, so no second `effect_owed` is
921
- * recorded.
970
+ * Express an {@link SseHub} as a {@link Projection}. The projection's view is
971
+ * the latest emitted SSE event (or `null` before the first); `apply` maps each
972
+ * write-model update to an event via `toEvent` and emits it through `hub.emit`,
973
+ * so the SSE stream is driven by the SAME hub `sseHub()` returns — no API
974
+ * change for the `/sse` route (`hub.open()`) or any existing caller.
922
975
  *
923
- * The effect payload the ledger stores is the `callId` (the correlation key the
924
- * consumer round-trips); the consumer maps it back to its tool-call context.
925
- */
926
- declare function durableDeferredGateway<R>(inner: DeferredGateway<R>, recorder: PendingEffectsRecorder<{
927
- callId: string;
928
- }>, hooks: {
929
- recordOwed(callId: string, event: EffectOwed<{
930
- callId: string;
931
- }>): void;
932
- recordConfirmed(callId: string, event: EffectConfirmed): void;
933
- }): DurableDeferredGateway<R>;
934
- /**
935
- * The typed cold-wake resume port `bootResume` fires through — the agent's
936
- * `AgentBootPort` (issue #60) generalized to any DO-hosted machine (issue #231).
976
+ * - `toEvent(msg, model)` returns the SSE event for this update, or `null` to
977
+ * skip it (the view stays, nothing is emitted).
978
+ * - `name`/`key` default to `"sse"`/`"main"`; override for a second SSE view.
937
979
  *
938
- * - `isResumable(state)` — true iff the rehydrated State is mid-loop and needs a
939
- * resume dispatch. False on a fresh boot, which makes `bootResume` a no-op.
940
- * - `resumeMsg(now)` — the TYPED constructor for the single resume Msg (never a
941
- * hand-built `{ type: … }` literal); the `agentBootMsg` shape lifted out so a
942
- * non-agent grain supplies its own boot Msg constructor.
980
+ * @example
981
+ * const hub = sseHub<MyEvent>();
982
+ * const registry = projectionRegistry<MyState, MyMsg>();
983
+ * registry.register(sseProjection(hub, (msg) =>
984
+ * msg?.type === "turn_done" ? { kind: "turn", ... } : null,
985
+ * ));
986
+ * driveProjections(registry, runtime); // SSE is now one projection
987
+ * // /sse route unchanged:
988
+ * return hub.open();
943
989
  */
944
- interface ResumePort<S, M extends {
990
+ declare function sseProjection<Model, Msg extends {
945
991
  type: string;
946
- }> {
947
- readonly isResumable: (state: S) => boolean;
948
- readonly resumeMsg: (now: number) => M;
949
- }
992
+ }, E>(hub: SseHub<E>, toEvent: (msg: Msg | null, model: Model) => E | null, id?: {
993
+ name?: string;
994
+ key?: string;
995
+ }): Projection<Model, Msg, E | null>;
950
996
  /**
951
- * After `runtime.ready`, derive the single resume Msg from the rehydrated State
952
- * via `port` and dispatch it exactly once — the generalized AgentBoot. On a
953
- * fresh (non-rehydrated) machine `port.isResumable` is false and this is a
954
- * no-op. Call once right after building the runtime; it standardizes the
955
- * hand-written `if (isResumable(state)) await runtime.dispatch(bootMsg(now()))`
956
- * every DO-hosted machine otherwise re-derives.
997
+ * Wire an {@link SseHub} to a runtime's semantic {@link AgentEvent} stream (#47).
998
+ * For each event `type` the runtime can emit, subscribes via `runtime.on` and
999
+ * pushes `toFrame(event)` at the hub (returning `null` skips that event). The
1000
+ * runtime MUST have been built with `run(machine, { events: agentEvents() })`,
1001
+ * which is what makes `on` deliver the `AgentEvent`s. Returns a cleanup that
1002
+ * detaches every subscription.
957
1003
  *
958
- * `await ready` is the single boot gate (issue #45): `getState()` is total only
959
- * on the booted `Runtime`, so state cannot be inspected before boot populates
960
- * it. `now` is injected (the sole boot clock read) so a test can pin it.
961
- */
962
- declare function bootResume<S, M extends {
963
- type: string;
964
- }, E extends {
965
- type: string;
966
- } = never>(booting: BootingRuntime<S, M, E>, port: ResumePort<S, M>, now?: () => number): Promise<void>;
967
- /**
968
- * True iff an agent slice loaded from storage is mid-loop (running + awaiting
969
- * tools) — the resumable case `agent_boot` re-fires.
1004
+ * This is the runtime→SSE adapter expressed over the typed event channel: no
1005
+ * `observe` firehose, no private-Msg coupling. `hub.open()` and every existing
1006
+ * `sseHub` caller are unchanged — only the SOURCE of `hub.emit` moves from a
1007
+ * hand-rolled Msg switch to `on`.
970
1008
  *
971
- * The resumability question IS "is the run suspended on tools?" — so this
972
- * delegates to the agent's own typed `status` channel (issue #49) rather than
973
- * re-deriving the private `run.phase` / `conversation.awaiting` shape by hand.
974
- * The host no longer leaks the agent's internal slice structure; a change to
975
- * `Awaiting` surfaces as a compile error in `status`, not a silent break here.
1009
+ * @example
1010
+ * const hub = sseHub<MyFrame>();
1011
+ * const runtime = await run(machine, { ctx, events: agentEvents() }).ready;
1012
+ * const off = sseFromAgentEvents(runtime, hub, (e) =>
1013
+ * e.type === "RunDone" ? { kind: "done", output: e.output } : null,
1014
+ * );
1015
+ * // /sse route unchanged:
1016
+ * return hub.open();
976
1017
  */
977
- declare function agentIsResumable<Stage, P extends string, O extends Record<P, unknown>, R>(state: AgentState<Stage, P, O, R>): boolean;
1018
+ declare function sseFromAgentEvents<R, Frame>(runtime: {
1019
+ on<K extends AgentEvent<R>["type"]>(type: K, handler: (event: Extract<AgentEvent<R>, {
1020
+ type: K;
1021
+ }>) => void): () => void;
1022
+ }, hub: SseHub<Frame>, toFrame: (event: AgentEvent<R>) => Frame | null): () => void;
1023
+
978
1024
  /**
979
- * The AGENT specialization of {@link bootResume} (issue #231): after
980
- * `runtime.ready`, self-dispatch `agent_boot` iff the rehydrated slice is
981
- * resumable, a no-op on a fresh DO. Wires the agent slice's `agentIsResumable`
982
- * predicate + the agent-owned `agentBootMsg` typed port (issue #60) into the
983
- * generalized helper, so the resume dispatch lives in ONE place — see
984
- * `bootResume`'s docblock for the boot-gate / init-purity rationale.
985
- *
986
- * `now` is injected (the host's only clock read for boot) so a test can pin it.
1025
+ * `createAgentHost` — the runtime+events+autoBoot assembly + the test seam,
1026
+ * ONCE (issue #53). The facade over the concern modules: the deferred gateway
1027
+ * (`./deferred-gateway`), the WS carrier (`./command-socket`), the SSE plumbing
1028
+ * (`./sse`), and the cold-wake resume (`./resume`). See `./host` for the
1029
+ * transport-model rationale.
987
1030
  */
988
- declare function autoBoot<Stage, P extends string, O extends Record<P, unknown>, R, E extends {
989
- type: string;
990
- } = never>(booting: BootingRuntime<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, E>, now?: () => number): Promise<void>;
1031
+
991
1032
  /**
992
- * Accept a command-runner WebSocket on the DO's `fetch`. Adds the server end to
993
- * `clients`, wires `message` → `onFrame(data)` (string frames only) and
994
- * `close` → deregister, and returns the 101 upgrade Response.
995
- *
996
- * The consumer's `onFrame` parses the frame and calls `gateway.settle(callId,
997
- * result)` — the inbound bridge. Deletes the consumer's `acceptClient` method.
1033
+ * What the consumer supplies to {@link createAgentHost} — the domain mappings,
1034
+ * nothing of the wiring. `Stage`/`P`/`O`/`R` are the agent slice's type
1035
+ * parameters (the consumer names them once); `Frame` is its SSE event shape.
1036
+ * `C`/`U`/`Ctx` are the machine's own Cmd/Sub/Ctx — inferred from
1037
+ * `buildMachine` at the `createAgentHost` call site (never name them by hand;
1038
+ * an erased `any` here cost consumers all checking on this layer — #278).
998
1039
  */
999
- declare function acceptCommandSocket(clients: Set<WebSocket>, onFrame: (data: string) => void): Response;
1040
+ interface AgentHostConfig<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame, C extends Cmd = Cmd, U extends Sub = Sub, Ctx = unknown> {
1041
+ /**
1042
+ * Build the wired agent machine. Called once per host build (per activation),
1043
+ * AFTER the previous runtime — if any — was torn down. The consumer wires its
1044
+ * per-tool interpret here (`agent.toMachine({ toolInterpret })`).
1045
+ */
1046
+ readonly buildMachine: () => Machine<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, C, U, Ctx>;
1047
+ /** The durable `Store` for the agent slice (typically `doStore(storage, parse)`). */
1048
+ readonly store: Store<AgentState<Stage, P, O, R>>;
1049
+ /** The Ctx the machine threads to its interpret cells — checked against `buildMachine`'s. */
1050
+ readonly ctx: Ctx;
1051
+ /**
1052
+ * The run-terminality predicate (#46) — what makes `result()` first-class.
1053
+ * Defaults to the agent's own terminal phases (`done` / `failed`).
1054
+ */
1055
+ readonly terminal?: (state: AgentState<Stage, P, O, R>) => boolean;
1056
+ /**
1057
+ * Map one semantic {@link AgentEvent} to the consumer's SSE frame, or `null`
1058
+ * to skip it. This is the ONLY place the consumer touches the SSE seam — the
1059
+ * host owns the subscription wiring (`sseFromAgentEvents`) and the hub.
1060
+ */
1061
+ readonly toSseFrame: (event: AgentEvent<R>) => Frame | null;
1062
+ /** Clock injected for `autoBoot` (the host's only boot clock read). */
1063
+ readonly now?: () => number;
1064
+ }
1000
1065
  /**
1001
- * Broadcast a JSON frame to every connected command-runner socket. A dead
1066
+ * The assembled host: the runtime cell + the SSE hub + the framework test seam,
1067
+ * owned ONCE. A DO holds one `AgentHost` and delegates to it; its own surface
1068
+ * shrinks to the domain (the gateway, the WS bridge, the command-send mapping).
1069
+ *
1070
+ * @typeParam Stage Pipeline stage type.
1071
+ * @typeParam P Brain-call purposes.
1072
+ * @typeParam O Per-purpose outputs (bound to {@link AgentTurn}).
1073
+ * @typeParam R Tool result type.
1074
+ * @typeParam Frame The consumer's SSE event shape.
1075
+ */
1076
+ interface AgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame> {
1077
+ /**
1078
+ * Get (or build) the booted runtime. Build-once-boot-once: the first call
1079
+ * builds the machine, wires SSE off the semantic event stream, awaits the boot
1080
+ * gate, runs the `autoBoot` re-fire for a rehydrated suspended run, and caches
1081
+ * the booted `Runtime`; subsequent calls return the cached handle. The single
1082
+ * assembly every consumer used to hand-roll in `getRuntime()`.
1083
+ */
1084
+ runtime(): Promise<Runtime<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, AgentEvent<R>>>;
1085
+ /**
1086
+ * The agent's lifecycle status (#49) — the ONE typed channel a consumer reads
1087
+ * instead of re-deriving `run.phase` / `awaiting` by hand. Ensures the runtime
1088
+ * is built + boot-reconciled first. Replaces the per-consumer `isSuspended` /
1089
+ * `runPhase` test methods (read `status().kind`).
1090
+ */
1091
+ status(): Promise<AgentStatus<Stage>>;
1092
+ /**
1093
+ * The run's terminal State (#46), or `undefined` while in flight. Read the
1094
+ * run's product off it (e.g. `result()?.output`). Replaces the per-consumer
1095
+ * `verdict` plumbing's source. Ensures the runtime is built first.
1096
+ */
1097
+ result(): Promise<AgentState<Stage, P, O, R> | undefined>;
1098
+ /**
1099
+ * The SSE hub — the route (`host.sse.open()`) AND the test seam
1100
+ * (`host.sse.register(sink)`). The host drives `hub.emit` off the semantic
1101
+ * event stream; the consumer never touches it except to open / register.
1102
+ */
1103
+ readonly sse: SseHub<Frame>;
1104
+ /**
1105
+ * Tear down the cached runtime (drain its tail via `stop()`, drop the handle)
1106
+ * so the next `runtime()` rebuilds from storage. The framework `settle` test
1107
+ * seam, owned once.
1108
+ */
1109
+ reset(): Promise<void>;
1110
+ }
1111
+ /**
1112
+ * Build an {@link AgentHost} from the consumer's domain mappings. Owns the
1113
+ * runtime+events+autoBoot assembly and the framework test seam — the wiring
1114
+ * issue #53 had every consumer re-write.
1115
+ *
1116
+ * @example
1117
+ * const host = createAgentHost<Stage, Purpose, Outputs, ClientResult, SseEvent>({
1118
+ * buildMachine: () => agent.toMachine<Ctx>({ toolInterpret }),
1119
+ * store: doStore(storage, parse),
1120
+ * ctx,
1121
+ * toSseFrame: (e) =>
1122
+ * e.type === "ToolSettled" ? { kind: "result", ...} :
1123
+ * e.type === "TurnSettled" && e.turn.toolCalls.length === 0
1124
+ * ? { kind: "verdict", verdict: e.turn.content } :
1125
+ * e.type === "RunDone" ? { kind: "phase", phase: "done" } : null,
1126
+ * });
1127
+ * // route: return host.sse.open();
1128
+ * // start: await (await host.runtime()).dispatch({ type: "agent_start", ... });
1129
+ * // test: (await host.status()).kind === "suspended"
1130
+ */
1131
+ declare function createAgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame, C extends Cmd = Cmd, U extends Sub = Sub, Ctx = unknown>(config: AgentHostConfig<Stage, P, O, R, Frame, C, U, Ctx>): AgentHost<Stage, P, O, R, Frame>;
1132
+
1133
+ /**
1134
+ * The command-runner WebSocket host + broadcast — the WS half of the
1135
+ * `@demlik/tea/do` transport, in both the resident (`acceptCommandSocket`) and
1136
+ * hibernation-aware (`acceptDurableCommandSocket`) forms. See `./host` for the
1137
+ * transport-model rationale (ONE Sub type, gateway-bridged I/O).
1138
+ */
1139
+ /**
1140
+ * Accept a command-runner WebSocket on the DO's `fetch`. Adds the server end to
1141
+ * `clients`, wires `message` → `onFrame(data)` (string frames only) and
1142
+ * `close` → deregister, and returns the 101 upgrade Response.
1143
+ *
1144
+ * The consumer's `onFrame` parses the frame and calls `gateway.settle(callId,
1145
+ * result)` — the inbound bridge. Deletes the consumer's `acceptClient` method.
1146
+ */
1147
+ declare function acceptCommandSocket(clients: Set<WebSocket>, onFrame: (data: string) => void): Response;
1148
+ /**
1149
+ * Broadcast a JSON frame to every connected command-runner socket. A dead
1002
1150
  * socket is skipped (dropped on its own `close` event). Lifts the consumer's
1003
1151
  * fan-out loop in `sendCommand`.
1004
1152
  */
@@ -1034,6 +1182,85 @@ declare function acceptDurableCommandSocket(ctx: HibernatableCtx): Response;
1034
1182
  * eviction, before any other lifecycle method has run. A dead socket is skipped.
1035
1183
  */
1036
1184
  declare function broadcastHibernatable(ctx: HibernatableCtx, frame: unknown): void;
1185
+
1186
+ /**
1187
+ * The deferred-tool gateway + its durable wrappers — the Promise-owning half of
1188
+ * the `@demlik/tea/do` transport. See `./host` for the transport-model
1189
+ * rationale (ONE Sub type, gateway-bridged I/O).
1190
+ */
1191
+
1192
+ /**
1193
+ * The deferred-tool gateway. `R` is the consumer's tool-result type (what
1194
+ * `agent_tool_ok` carries).
1195
+ *
1196
+ * - `await(callId, send, deadlineMs)` — register the round-trip, fire `send`,
1197
+ * return the Promise the reply (or the deadline) settles. Idempotent per
1198
+ * `callId`: a second call for an in-flight id returns the SAME promise
1199
+ * without re-firing `send` (the boot-reconcile re-fire is a no-op at the
1200
+ * gateway, mirroring the agent's idempotent boot).
1201
+ * - `settle(callId, result)` — the transport calls this on a reply; resolves
1202
+ * the awaited promise. A settle for an unknown / already-settled id is a
1203
+ * no-op (the callId guard the consumer used to keep in its pending Map).
1204
+ * - `fail(callId, reason)` — settle the round-trip as an error on demand
1205
+ * (e.g. a transport-level close). Same unknown-id no-op.
1206
+ * - `inFlight()` — the callIds with an open round-trip (tests / introspection).
1207
+ *
1208
+ * The deadline → reject path is what makes "timeout is data": the tool cell's
1209
+ * `catch` maps the rejection to `agent_tool_err`, recorded on the conversation,
1210
+ * never a hang.
1211
+ */
1212
+ interface DeferredGateway<R> {
1213
+ await(callId: string, send: () => void, deadlineMs: number): Promise<R>;
1214
+ settle(callId: string, result: R): void;
1215
+ fail(callId: string, reason: string): void;
1216
+ inFlight(): readonly string[];
1217
+ }
1218
+ /**
1219
+ * Build a deferred-tool gateway. No clock / RNG captured at construction — the
1220
+ * deadline is per-call (`await(..., deadlineMs)`), so one gateway serves every
1221
+ * tool family with its own timeout.
1222
+ *
1223
+ * Deletes from the consumer: the `pending` Map, the `Pending` interface, the
1224
+ * `awaitClient` method, the `forceTimeout` plumbing, and the `deliverResult`
1225
+ * callId-guard body — all of it collapses to this object plus the tool cell's
1226
+ * `await` + try/catch.
1227
+ */
1228
+ declare function deferredGateway<R>(): DeferredGateway<R>;
1229
+ /** A `callId`-keyed durable round-trip: maps the gateway's string id to the
1230
+ * ledger's monotonic delivery id so a `settle(callId)` finds the owed entry. */
1231
+ interface DurableDeferredGateway<R> extends DeferredGateway<R> {
1232
+ /** The ledger recorder this gateway feeds (for `surviving()` on activation). */
1233
+ readonly recorder: PendingEffectsRecorder<{
1234
+ callId: string;
1235
+ }>;
1236
+ }
1237
+ /**
1238
+ * Wrap a `DeferredGateway<R>` so every round-trip is also recorded in a durable
1239
+ * `PendingEffectsRecorder`. The in-memory gateway is the fast path; the recorder
1240
+ * is the durability slice.
1241
+ *
1242
+ * - `recordOwed(callId, event)` — fires from `await`, BEFORE `send`, carrying
1243
+ * the `effect_owed` event the consumer must persist (then deliver).
1244
+ * - `recordConfirmed(callId, event)` — fires from `settle`/`fail` for a known
1245
+ * id, carrying the `effect_confirmed` event to persist.
1246
+ * - `reissue(callId, send, deadlineMs)` — re-arm a surviving round-trip on
1247
+ * activation. Idempotent: the gateway's own `await` returns the open promise
1248
+ * without re-firing `send` for an in-flight `callId`, so re-issuing an id
1249
+ * that is somehow still pending is a no-op (mirrors the boot-reconcile
1250
+ * re-fire). The effect is already owed, so no second `effect_owed` is
1251
+ * recorded.
1252
+ *
1253
+ * The effect payload the ledger stores is the `callId` (the correlation key the
1254
+ * consumer round-trips); the consumer maps it back to its tool-call context.
1255
+ */
1256
+ declare function durableDeferredGateway<R>(inner: DeferredGateway<R>, recorder: PendingEffectsRecorder<{
1257
+ callId: string;
1258
+ }>, hooks: {
1259
+ recordOwed(callId: string, event: EffectOwed<{
1260
+ callId: string;
1261
+ }>): void;
1262
+ recordConfirmed(callId: string, event: EffectConfirmed): void;
1263
+ }): DurableDeferredGateway<R>;
1037
1264
  /**
1038
1265
  * The durable command carrier — a {@link DurableDeferredGateway} whose every
1039
1266
  * tool round-trip is also a durable owed effect.
@@ -1045,7 +1272,7 @@ declare function broadcastHibernatable(ctx: HibernatableCtx, frame: unknown): vo
1045
1272
  * `recordOwed`) to PERSIST before the send, and `settle`/`fail` hand it an
1046
1273
  * `effect_confirmed` (via `recordConfirmed`) to persist. On the next activation
1047
1274
  * the consumer reads its persisted ledger events back and calls
1048
- * {@link reissueSurvivingEffects} to re-fire the survivors.
1275
+ * `reissueSurvivingEffects` (see `./resume`) to re-fire the survivors.
1049
1276
  *
1050
1277
  * It extends `DurableDeferredGateway` (so it is a drop-in for the gateway the
1051
1278
  * interpret cell already awaits) and exposes the underlying `recorder` for a
@@ -1083,6 +1310,71 @@ declare function durableCommandCarrier<R>(inner: DeferredGateway<R>, recorder: P
1083
1310
  }>): void;
1084
1311
  recordConfirmed(callId: string, event: EffectConfirmed): void;
1085
1312
  }): DurableCommandCarrier<R>;
1313
+
1314
+ /**
1315
+ * Cold-wake resume — the boot/hibernation re-fire path: the generalized
1316
+ * `bootResume` port, its agent specialization `autoBoot`, and the wake-time
1317
+ * re-emit of surviving tool round-trips. See `./host` for the transport-model
1318
+ * rationale.
1319
+ */
1320
+
1321
+ /**
1322
+ * The typed cold-wake resume port `bootResume` fires through — the agent's
1323
+ * `AgentBootPort` (issue #60) generalized to any DO-hosted machine (issue #231).
1324
+ *
1325
+ * - `isResumable(state)` — true iff the rehydrated State is mid-loop and needs a
1326
+ * resume dispatch. False on a fresh boot, which makes `bootResume` a no-op.
1327
+ * - `resumeMsg(now)` — the TYPED constructor for the single resume Msg (never a
1328
+ * hand-built `{ type: … }` literal); the `agentBootMsg` shape lifted out so a
1329
+ * non-agent grain supplies its own boot Msg constructor.
1330
+ */
1331
+ interface ResumePort<S, M extends {
1332
+ type: string;
1333
+ }> {
1334
+ readonly isResumable: (state: S) => boolean;
1335
+ readonly resumeMsg: (now: number) => M;
1336
+ }
1337
+ /**
1338
+ * After `runtime.ready`, derive the single resume Msg from the rehydrated State
1339
+ * via `port` and dispatch it exactly once — the generalized AgentBoot. On a
1340
+ * fresh (non-rehydrated) machine `port.isResumable` is false and this is a
1341
+ * no-op. Call once right after building the runtime; it standardizes the
1342
+ * hand-written `if (isResumable(state)) await runtime.dispatch(bootMsg(now()))`
1343
+ * every DO-hosted machine otherwise re-derives.
1344
+ *
1345
+ * `await ready` is the single boot gate (issue #45): `getState()` is total only
1346
+ * on the booted `Runtime`, so state cannot be inspected before boot populates
1347
+ * it. `now` is injected (the sole boot clock read) so a test can pin it.
1348
+ */
1349
+ declare function bootResume<S, M extends {
1350
+ type: string;
1351
+ }, E extends {
1352
+ type: string;
1353
+ } = never>(booting: BootingRuntime<S, M, E>, port: ResumePort<S, M>, now?: () => number): Promise<void>;
1354
+ /**
1355
+ * True iff an agent slice loaded from storage is mid-loop (running + awaiting
1356
+ * tools) — the resumable case `agent_boot` re-fires.
1357
+ *
1358
+ * The resumability question IS "is the run suspended on tools?" — so this
1359
+ * delegates to the agent's own typed `status` channel (issue #49) rather than
1360
+ * re-deriving the private `run.phase` / `conversation.awaiting` shape by hand.
1361
+ * The host no longer leaks the agent's internal slice structure; a change to
1362
+ * `Awaiting` surfaces as a compile error in `status`, not a silent break here.
1363
+ */
1364
+ declare function agentIsResumable<Stage, P extends string, O extends Record<P, unknown>, R>(state: AgentState<Stage, P, O, R>): boolean;
1365
+ /**
1366
+ * The AGENT specialization of {@link bootResume} (issue #231): after
1367
+ * `runtime.ready`, self-dispatch `agent_boot` iff the rehydrated slice is
1368
+ * resumable, a no-op on a fresh DO. Wires the agent slice's `agentIsResumable`
1369
+ * predicate + the agent-owned `agentBootMsg` typed port (issue #60) into the
1370
+ * generalized helper, so the resume dispatch lives in ONE place — see
1371
+ * `bootResume`'s docblock for the boot-gate / init-purity rationale.
1372
+ *
1373
+ * `now` is injected (the host's only clock read for boot) so a test can pin it.
1374
+ */
1375
+ declare function autoBoot<Stage, P extends string, O extends Record<P, unknown>, R, E extends {
1376
+ type: string;
1377
+ } = never>(booting: BootingRuntime<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, E>, now?: () => number): Promise<void>;
1086
1378
  /**
1087
1379
  * RE-EMIT ON ACTIVATION (the #91 wake path). Given a durable carrier whose
1088
1380
  * recorder was rehydrated from the persisted ledger events (e.g.
@@ -1112,182 +1404,6 @@ declare function durableCommandCarrier<R>(inner: DeferredGateway<R>, recorder: P
1112
1404
  * they restore a suspended run's full in-flight state after a wake.
1113
1405
  */
1114
1406
  declare function reissueSurvivingEffects<R>(carrier: DurableCommandCarrier<R>, reissue: (callId: string) => void): readonly string[];
1115
- /**
1116
- * A set of SSE sinks plus the plumbing to fan an event out to all of them and
1117
- * to open a `text/event-stream` Response wired to a fresh sink.
1118
- *
1119
- * The consumer:
1120
- * 1. holds one `SseHub<E>` per DO,
1121
- * 2. calls `hub.emit(event)` from its Msg→event mapping (driven off
1122
- * `runtime.observe`), and
1123
- * 3. returns `hub.open()` from the `/sse` route.
1124
- *
1125
- * `register(sink)` exists for tests: subscribe a sink directly and observe the
1126
- * exact event sequence a live client would receive (a live `text/event-stream`
1127
- * fights vitest-pool-workers teardown). Deletes the consumer's `sseSinks` set,
1128
- * `openSse`, `emitSse`, and `onSseEvent`.
1129
- */
1130
- interface SseHub<E> {
1131
- emit(event: E): void;
1132
- open(): Response;
1133
- register(sink: (event: E) => void): () => void;
1134
- }
1135
- /** Build an SSE hub for event type `E` (the consumer's domain event shape). */
1136
- declare function sseHub<E>(): SseHub<E>;
1137
- /**
1138
- * Express an {@link SseHub} as a {@link Projection}. The projection's view is
1139
- * the latest emitted SSE event (or `null` before the first); `apply` maps each
1140
- * write-model update to an event via `toEvent` and emits it through `hub.emit`,
1141
- * so the SSE stream is driven by the SAME hub `sseHub()` returns — no API
1142
- * change for the `/sse` route (`hub.open()`) or any existing caller.
1143
- *
1144
- * - `toEvent(msg, model)` returns the SSE event for this update, or `null` to
1145
- * skip it (the view stays, nothing is emitted).
1146
- * - `name`/`key` default to `"sse"`/`"main"`; override for a second SSE view.
1147
- *
1148
- * @example
1149
- * const hub = sseHub<MyEvent>();
1150
- * const registry = projectionRegistry<MyState, MyMsg>();
1151
- * registry.register(sseProjection(hub, (msg) =>
1152
- * msg?.type === "turn_done" ? { kind: "turn", ... } : null,
1153
- * ));
1154
- * driveProjections(registry, runtime); // SSE is now one projection
1155
- * // /sse route unchanged:
1156
- * return hub.open();
1157
- */
1158
- declare function sseProjection<Model, Msg extends {
1159
- type: string;
1160
- }, E>(hub: SseHub<E>, toEvent: (msg: Msg | null, model: Model) => E | null, id?: {
1161
- name?: string;
1162
- key?: string;
1163
- }): Projection<Model, Msg, E | null>;
1164
- /**
1165
- * Wire an {@link SseHub} to a runtime's semantic {@link AgentEvent} stream (#47).
1166
- * For each event `type` the runtime can emit, subscribes via `runtime.on` and
1167
- * pushes `toFrame(event)` at the hub (returning `null` skips that event). The
1168
- * runtime MUST have been built with `run(machine, { events: agentEvents() })`,
1169
- * which is what makes `on` deliver the `AgentEvent`s. Returns a cleanup that
1170
- * detaches every subscription.
1171
- *
1172
- * This is the runtime→SSE adapter expressed over the typed event channel: no
1173
- * `observe` firehose, no private-Msg coupling. `hub.open()` and every existing
1174
- * `sseHub` caller are unchanged — only the SOURCE of `hub.emit` moves from a
1175
- * hand-rolled Msg switch to `on`.
1176
- *
1177
- * @example
1178
- * const hub = sseHub<MyFrame>();
1179
- * const runtime = await run(machine, { ctx, events: agentEvents() }).ready;
1180
- * const off = sseFromAgentEvents(runtime, hub, (e) =>
1181
- * e.type === "RunDone" ? { kind: "done", output: e.output } : null,
1182
- * );
1183
- * // /sse route unchanged:
1184
- * return hub.open();
1185
- */
1186
- declare function sseFromAgentEvents<R, Frame>(runtime: {
1187
- on<K extends AgentEvent<R>["type"]>(type: K, handler: (event: Extract<AgentEvent<R>, {
1188
- type: K;
1189
- }>) => void): () => void;
1190
- }, hub: SseHub<Frame>, toFrame: (event: AgentEvent<R>) => Frame | null): () => void;
1191
- /**
1192
- * What the consumer supplies to {@link createAgentHost} — the domain mappings,
1193
- * nothing of the wiring. `Stage`/`P`/`O`/`R` are the agent slice's type
1194
- * parameters (the consumer names them once); `Frame` is its SSE event shape.
1195
- * `C`/`U`/`Ctx` are the machine's own Cmd/Sub/Ctx — inferred from
1196
- * `buildMachine` at the `createAgentHost` call site (never name them by hand;
1197
- * an erased `any` here cost consumers all checking on this layer — #278).
1198
- */
1199
- interface AgentHostConfig<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame, C extends Cmd = Cmd, U extends Sub = Sub, Ctx = unknown> {
1200
- /**
1201
- * Build the wired agent machine. Called once per host build (per activation),
1202
- * AFTER the previous runtime — if any — was torn down. The consumer wires its
1203
- * per-tool interpret here (`agent.toMachine({ toolInterpret })`).
1204
- */
1205
- readonly buildMachine: () => Machine<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, C, U, Ctx>;
1206
- /** The durable `Store` for the agent slice (typically `doStore(storage, parse)`). */
1207
- readonly store: Store<AgentState<Stage, P, O, R>>;
1208
- /** The Ctx the machine threads to its interpret cells — checked against `buildMachine`'s. */
1209
- readonly ctx: Ctx;
1210
- /**
1211
- * The run-terminality predicate (#46) — what makes `result()` first-class.
1212
- * Defaults to the agent's own terminal phases (`done` / `failed`).
1213
- */
1214
- readonly terminal?: (state: AgentState<Stage, P, O, R>) => boolean;
1215
- /**
1216
- * Map one semantic {@link AgentEvent} to the consumer's SSE frame, or `null`
1217
- * to skip it. This is the ONLY place the consumer touches the SSE seam — the
1218
- * host owns the subscription wiring (`sseFromAgentEvents`) and the hub.
1219
- */
1220
- readonly toSseFrame: (event: AgentEvent<R>) => Frame | null;
1221
- /** Clock injected for `autoBoot` (the host's only boot clock read). */
1222
- readonly now?: () => number;
1223
- }
1224
- /**
1225
- * The assembled host: the runtime cell + the SSE hub + the framework test seam,
1226
- * owned ONCE. A DO holds one `AgentHost` and delegates to it; its own surface
1227
- * shrinks to the domain (the gateway, the WS bridge, the command-send mapping).
1228
- *
1229
- * @typeParam Stage Pipeline stage type.
1230
- * @typeParam P Brain-call purposes.
1231
- * @typeParam O Per-purpose outputs (bound to {@link AgentTurn}).
1232
- * @typeParam R Tool result type.
1233
- * @typeParam Frame The consumer's SSE event shape.
1234
- */
1235
- interface AgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame> {
1236
- /**
1237
- * Get (or build) the booted runtime. Build-once-boot-once: the first call
1238
- * builds the machine, wires SSE off the semantic event stream, awaits the boot
1239
- * gate, runs the `autoBoot` re-fire for a rehydrated suspended run, and caches
1240
- * the booted `Runtime`; subsequent calls return the cached handle. The single
1241
- * assembly every consumer used to hand-roll in `getRuntime()`.
1242
- */
1243
- runtime(): Promise<Runtime<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, AgentEvent<R>>>;
1244
- /**
1245
- * The agent's lifecycle status (#49) — the ONE typed channel a consumer reads
1246
- * instead of re-deriving `run.phase` / `awaiting` by hand. Ensures the runtime
1247
- * is built + boot-reconciled first. Replaces the per-consumer `isSuspended` /
1248
- * `runPhase` test methods (read `status().kind`).
1249
- */
1250
- status(): Promise<AgentStatus<Stage>>;
1251
- /**
1252
- * The run's terminal State (#46), or `undefined` while in flight. Read the
1253
- * run's product off it (e.g. `result()?.output`). Replaces the per-consumer
1254
- * `verdict` plumbing's source. Ensures the runtime is built first.
1255
- */
1256
- result(): Promise<AgentState<Stage, P, O, R> | undefined>;
1257
- /**
1258
- * The SSE hub — the route (`host.sse.open()`) AND the test seam
1259
- * (`host.sse.register(sink)`). The host drives `hub.emit` off the semantic
1260
- * event stream; the consumer never touches it except to open / register.
1261
- */
1262
- readonly sse: SseHub<Frame>;
1263
- /**
1264
- * Tear down the cached runtime (drain its tail via `stop()`, drop the handle)
1265
- * so the next `runtime()` rebuilds from storage. The framework `settle` test
1266
- * seam, owned once.
1267
- */
1268
- reset(): Promise<void>;
1269
- }
1270
- /**
1271
- * Build an {@link AgentHost} from the consumer's domain mappings. Owns the
1272
- * runtime+events+autoBoot assembly and the framework test seam — the wiring
1273
- * issue #53 had every consumer re-write.
1274
- *
1275
- * @example
1276
- * const host = createAgentHost<Stage, Purpose, Outputs, ClientResult, SseEvent>({
1277
- * buildMachine: () => agent.toMachine<Ctx>({ toolInterpret }),
1278
- * store: doStore(storage, parse),
1279
- * ctx,
1280
- * toSseFrame: (e) =>
1281
- * e.type === "ToolSettled" ? { kind: "result", ...} :
1282
- * e.type === "TurnSettled" && e.turn.toolCalls.length === 0
1283
- * ? { kind: "verdict", verdict: e.turn.content } :
1284
- * e.type === "RunDone" ? { kind: "phase", phase: "done" } : null,
1285
- * });
1286
- * // route: return host.sse.open();
1287
- * // start: await (await host.runtime()).dispatch({ type: "agent_start", ... });
1288
- * // test: (await host.status()).kind === "suspended"
1289
- */
1290
- declare function createAgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame, C extends Cmd = Cmd, U extends Sub = Sub, Ctx = unknown>(config: AgentHostConfig<Stage, P, O, R, Frame, C, U, Ctx>): AgentHost<Stage, P, O, R, Frame>;
1291
1407
 
1292
1408
  /**
1293
1409
  * @demlik/tea/do — the idempotent-effect brick (the receiver/effect-side dedup).
@@ -1763,4 +1879,4 @@ interface DoStoreOptions<S> {
1763
1879
  */
1764
1880
  declare function doStore<S>(storage: DurableObjectStorage, parse: (raw: unknown) => S | null, keyOrOptions?: string | DoStoreOptions<S>): Store<S>;
1765
1881
 
1766
- export { type AgentHost, type AgentHostConfig, type AlarmStorage, type AppliedEffects, type AppliedEffectsEvent, type AppliedEffectsGuard, type AttachableSocket, type BroadcastOptions, type BroadcastReport, type DeferredGateway, type DoStoreOptions, type DurableCommandCarrier, type DurableTimer, type DurableTimerConfig, type EffectApplied, EffectConfirmed, type EffectForgotten, type EffectKey, EffectOwed, type EventLogRange, type EventSourcedOptions, type EventSourcedStore, type ExecuteStep, type HibernatableCtx, type IdempotentEffect, type IdempotentOutcome, type NextStep, PendingEffectsRecorder, type PersistedEvent, type PresenceCtx, type PresenceSocket, type PresenceUpgrade, type Projection, type ProjectionId, type ProjectionRegistry, type ProjectionRunner, type ProjectionUpdate, type RegisterOptions, type ResumePort, type RunStepLoopConfig, type SseHub, type StepCtx, type StepEngine, type StepHostConfig, type StepLoopOutcome, type StepOutcome, type StepRequest, type StepResponse, type StepResult, type StepTransport, WS_READY_STATE_OPEN, acceptCommandSocket, acceptDurableCommandSocket, acceptPresenceSocket, agentIsResumable, appliedEffects, applyAppliedEvent, autoBoot, bootResume, broadcast, broadcastFrame, broadcastHibernatable, constantTimeEqual, createAgentHost, deferredGateway, doEventSourcedStore, doStore, driveProjections, durableCommandCarrier, durableDeferredGateway, durableTimer, emptyApplied, foldApplied, idempotentEffect, isApplied, mintRunToken, presenceCount, projectionIdString, projectionRegistry, rebuildProjection, registerHibernatableSocket, reissueSurvivingEffects, runProjection, runStepLoop, sseFromAgentEvents, sseHub, sseProjection, stepHost };
1882
+ export { type AgentHost, type AgentHostConfig, type AlarmStorage, type AppliedEffects, type AppliedEffectsEvent, type AppliedEffectsGuard, type AttachableSocket, type BroadcastOptions, type BroadcastReport, type DeferResumeHook, type DeferStepHostConfig, type DeferredGateway, type DeferredStepOutcome, type DeferredStepResponse, type DoStoreOptions, type DurableCommandCarrier, type DurableTimer, type DurableTimerConfig, type EffectApplied, EffectConfirmed, type EffectForgotten, type EffectKey, EffectOwed, type EventLogRange, type EventSourcedOptions, type EventSourcedStore, type ExecuteStep, type HibernatableCtx, type IdempotentEffect, type IdempotentOutcome, type NextStep, PendingEffectsRecorder, type PersistedEvent, type PresenceCtx, type PresenceSocket, type PresenceUpgrade, type Projection, type ProjectionId, type ProjectionRegistry, type ProjectionRunner, type ProjectionUpdate, type RegisterOptions, type ResumePort, type RunStepLoopConfig, type SseHub, type StepCtx, type StepEngine, type StepHostConfig, type StepLoopOutcome, type StepOutcome, type StepRequest, type StepResponse, type StepResult, type StepTransport, type StepWorking, WS_READY_STATE_OPEN, acceptCommandSocket, acceptDurableCommandSocket, acceptPresenceSocket, agentIsResumable, appliedEffects, applyAppliedEvent, autoBoot, bootResume, broadcast, broadcastFrame, broadcastHibernatable, constantTimeEqual, createAgentHost, deferredGateway, doEventSourcedStore, doStore, driveProjections, durableCommandCarrier, durableDeferredGateway, durableTimer, emptyApplied, foldApplied, idempotentEffect, isApplied, mintRunToken, presenceCount, projectionIdString, projectionRegistry, rebuildProjection, registerHibernatableSocket, reissueSurvivingEffects, runProjection, runStepLoop, sseFromAgentEvents, sseHub, sseProjection, stepHost };