stitchkit 0.64.0 → 0.65.1

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/dist/testing.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  AgentMessageSchema,
3
3
  AgentRunSchema
4
- } from "./index-b1k33127.js";
4
+ } from "./index-sbkyvacf.js";
5
5
  import {
6
6
  createApplication
7
7
  } from "./index-eabpd4tb.js";
@@ -308,11 +308,72 @@ async function runAgentStoreConformance(config) {
308
308
  createdAt: "2026-08-22T00:00:00.000Z",
309
309
  updatedAt: "2026-08-22T00:00:02.000Z"
310
310
  });
311
+ if (checkpointedRun.fencingToken === undefined) {
312
+ throw new Error("Acquisition must persist a fencing token the store can read back");
313
+ }
314
+ const staleFence = await store.checkpointRunAssistant({
315
+ conversationId,
316
+ runId: running.id,
317
+ expectedRevision: checkpointedRun.revision,
318
+ ownerId: "conformance-owner",
319
+ fencingToken: checkpointedRun.fencingToken + 1,
320
+ assistant: AgentMessageSchema.parse({
321
+ schemaVersion: 1,
322
+ id: running.assistantMessageId,
323
+ conversationId,
324
+ runId: running.id,
325
+ role: "assistant",
326
+ status: "streaming",
327
+ parts: [{ type: "text", text: "stale fence" }],
328
+ createdAt: "2026-08-22T00:00:00.000Z",
329
+ updatedAt: "2026-08-22T00:00:01.500Z"
330
+ })
331
+ });
332
+ if (staleFence.outcome !== "conflict") {
333
+ throw new Error("A checkpoint with a stale fencing token must conflict");
334
+ }
335
+ const foreignOwner = await store.checkpointRunAssistant({
336
+ conversationId,
337
+ runId: running.id,
338
+ expectedRevision: checkpointedRun.revision,
339
+ ownerId: "a-different-runtime",
340
+ assistant: AgentMessageSchema.parse({
341
+ schemaVersion: 1,
342
+ id: running.assistantMessageId,
343
+ conversationId,
344
+ runId: running.id,
345
+ role: "assistant",
346
+ status: "streaming",
347
+ parts: [{ type: "text", text: "foreign owner" }],
348
+ createdAt: "2026-08-22T00:00:00.000Z",
349
+ updatedAt: "2026-08-22T00:00:01.700Z"
350
+ })
351
+ });
352
+ if (foreignOwner.outcome !== "conflict") {
353
+ throw new Error("A checkpoint from another owner must conflict");
354
+ }
355
+ const recoverable = await store.scanRecoverable({ limit: 10 });
356
+ if (!recoverable.items.some((item) => item.run.id === running.id)) {
357
+ throw new Error("A running run must appear in a recoverable scan");
358
+ }
359
+ const interrupted = await store.requestRunInterrupt({
360
+ conversationId,
361
+ runId: running.id,
362
+ expectedRevision: checkpointedRun.revision
363
+ });
364
+ requireOutcome(interrupted, "applied");
365
+ const interruptedRun = interrupted.snapshot.runs.find((run) => run.id === running.id);
366
+ if (interruptedRun?.state !== "interrupt_requested") {
367
+ throw new Error("A durable interrupt must move the run to interrupt_requested");
368
+ }
369
+ if (interruptedRun.usage?.cost?.value !== 0.25) {
370
+ throw new Error("An interrupt must not discard the figure the run had already spent");
371
+ }
311
372
  const terminalResults = await Promise.all([
312
373
  store.commitRunTerminal({
313
374
  conversationId,
314
375
  runId: running.id,
315
- expectedRevision: checkpointedRun.revision,
376
+ expectedRevision: interruptedRun.revision,
316
377
  ownerId: "conformance-owner",
317
378
  assistant: terminalAssistant,
318
379
  reason: "success",
@@ -325,7 +386,7 @@ async function runAgentStoreConformance(config) {
325
386
  store.commitRunTerminal({
326
387
  conversationId,
327
388
  runId: running.id,
328
- expectedRevision: checkpointedRun.revision,
389
+ expectedRevision: interruptedRun.revision,
329
390
  ownerId: "conformance-owner",
330
391
  assistant: terminalAssistant,
331
392
  reason: "success",
package/llms-full.txt CHANGED
@@ -58,7 +58,7 @@ own, recorded as an ADR.
58
58
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
59
59
  | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
60
60
  | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
61
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 7 of the 9 minors since 0.56.2, most recently 0.64.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
61
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 8 of the 10 minors since 0.56.2, most recently 0.65.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
62
62
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
63
63
  | `stitchkit/application` | server | evolving | managed resource graph, readiness, admission, schedules and bounded shutdown |
64
64
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
@@ -3496,7 +3496,9 @@ mechanics: durable acceptance, history projection, the AI SDK stream loop,
3496
3496
  checkpoints, keyed interruption, terminal commit and stable application events.
3497
3497
 
3498
3498
  If the application already owns that loop, continue importing `mountAgent` from
3499
- `stitchkit/tools`. Neither path depends on the other at runtime.
3499
+ `stitchkit/tools`. `stitchkit/tools` does not depend on this entrypoint at all;
3500
+ this entrypoint uses the tool executor from it, but pulls no MCP peer, so
3501
+ choosing `mountAgent` alone costs you nothing from here.
3500
3502
 
3501
3503
  ## Install
3502
3504
 
@@ -3504,6 +3506,13 @@ If the application already owns that loop, continue importing `mountAgent` from
3504
3506
  bun add stitchkit ai zod
3505
3507
  ```
3506
3508
 
3509
+ `mountAgent` from `stitchkit/tools` — used in the composition below — additionally
3510
+ needs the MCP peer, which that entrypoint imports statically:
3511
+
3512
+ ```sh
3513
+ bun add @modelcontextprotocol/server
3514
+ ```
3515
+
3507
3516
  OpenRouter is isolated so other runtime users do not resolve its package:
3508
3517
 
3509
3518
  ```sh
@@ -3543,7 +3552,7 @@ const models = defineModelRegistry({
3543
3552
  },
3544
3553
  })
3545
3554
 
3546
- const prompt = composeAgentPrompt([
3555
+ const prompt = composeAgentPrompt<{ userId: string }>([
3547
3556
  {
3548
3557
  name: 'product',
3549
3558
  stability: 'stable',
@@ -3674,15 +3683,15 @@ input + queued run → running → execution settled → terminal CAS → succes
3674
3683
 
3675
3684
  ## What happens to a run when new input arrives
3676
3685
 
3677
- `runs.inputPolicy` decides. It takes three values (or a function returning one);
3678
- the fourth row below is a behaviour it deliberately does not offer yet:
3686
+ `runs.inputPolicy` decides. It takes three values, or a function returning one;
3687
+ the fourth row is a behaviour that shipped and was withdrawn:
3679
3688
 
3680
3689
  | policy | the run in flight | what it already produced |
3681
3690
  |--------|-------------------|--------------------------|
3682
3691
  | `queue` (default) | finishes first | kept |
3683
3692
  | `interrupt` | ends | kept, and marked as cut off |
3684
3693
  | `supersede` | ends | discarded from the prompt, kept in the record |
3685
- | `inject` | continues | kept it takes the new input at a step boundary |
3694
+ | _inject_ | continues | withdrawn in 0.65.0 see below |
3686
3695
 
3687
3696
  `interrupt` and `supersede` differ in exactly one thing, and the question that
3688
3697
  picks between them is **not** "was the run interrupted" but **"did anyone see
@@ -3766,37 +3775,19 @@ const { decisions } = await projectAgentHistoryDetailed(snapshot.messages)
3766
3775
  process-local escape hatch chooses the reason, so a caller that knows the answer
3767
3776
  was never delivered can discard it without a newer input arriving.
3768
3777
 
3769
- ### The one that ends nothing
3770
-
3771
- **`inject`** hands the input to the loop between tool calls and lets the run keep
3772
- going. It is right when the input *refines* rather than redirects a correction
3773
- arriving while a multi-step task is halfway through, where discarding the
3774
- finished steps would be pure loss.
3775
-
3776
- It queues like `queue`, and a run already in flight takes it on at its next step
3777
- boundary. That ordering is the whole design: the tempting shape, attaching a new
3778
- input straight to a running run, has a loss case with no honest answer, because
3779
- the run may terminate before the loop reaches a boundary and the input would then
3780
- be recorded as answered by a turn that never saw it. Queue first and absorb
3781
- opportunistically, and the fallback is simply that the successor runs — the
3782
- behaviour every other policy already has.
3783
-
3784
- When a run does absorb one:
3785
-
3786
- - both inputs land in the answering run's `inputMessageIds`, so the durable
3787
- record matches what the model was actually asked;
3788
- - both submissions' tickets resolve to the same terminal result;
3789
- - the absorbed run is marked `absorbed` with `absorbedIntoRunId` pointing at the
3790
- run that answered. It is kept, not deleted — its admission receipt still points
3791
- at it, so a duplicate submission has to resolve to something. It leaves the
3792
- conversation snapshot, because a snapshot carries active runs plus those a
3793
- message references and an absorbed run never wrote an assistant message;
3794
- - the next provider call carries the re-projected history. An application's own
3795
- `prepareStep` wins if it sets `messages` itself — it is the one that knows why.
3796
-
3797
- A step boundary is the only place this can happen: the provider is between calls,
3798
- so the next request can carry the new message. A single-step run never reaches
3799
- one, and its successor simply runs next.
3778
+ ### The one that is not offered
3779
+
3780
+ **inject** hand the input to the loop between tool calls and let the run
3781
+ continue shipped in 0.63.0 and was **withdrawn in 0.65.0**. It committed the
3782
+ absorption durably at a step boundary, before the answer existed, and everything
3783
+ downstream of that ordering was wrong: an accepted input could end up in a state
3784
+ that was neither active, recoverable nor terminal, so `close()` could report
3785
+ `settled: true` while leaving it permanently unanswerable, and a duplicate
3786
+ submission of the same idempotency key was refused forever.
3787
+
3788
+ The redesign is tracked in the backlog and is not a patch to what shipped: the
3789
+ absorption has to commit atomically **with** the terminal record, so that a run
3790
+ which ends first simply leaves an ordinary queued successor behind.
3800
3791
 
3801
3792
  With `runs.coalescePending: true`, an active lane has at most one queued
3802
3793
  successor. Every later accepted input is atomically appended to that successor;
@@ -3918,8 +3909,16 @@ crash between the database commit and `publish`. Reconnect should load canonical
3918
3909
  state. Exactly-once external delivery remains an application-owned outbox.
3919
3910
 
3920
3911
  Durable event IDs are derived from run, event type and snapshot version. Use
3921
- `advanceAgentRuntimeEventCursor` to classify accepted, duplicate and gap delivery; a gap triggers a
3922
- snapshot reload. `createAgentRuntimeEventSink` adds a bounded failure-isolated lifecycle and typed
3912
+ `advanceAgentRuntimeEventCursor` to classify delivery.
3913
+
3914
+ **`gap` is reported for transient events only**, where `sequence` is a per-run
3915
+ counter the runtime really does increment once per event. Durable events carry
3916
+ the *conversation's* version, which advances on every mutation including the many
3917
+ that publish nothing — checkpoints, compaction, an acceptance that has not
3918
+ started — so two consecutive durable events are routinely several versions apart
3919
+ and adjacency says nothing. A durable loss is not detectable from the cursor;
3920
+ reload on reconnect, which is what a bounded fire-and-forget sink asks of you
3921
+ anyway. `createAgentRuntimeEventSink` adds a bounded failure-isolated lifecycle and typed
3923
3922
  projection/redaction hook. `onPublishError` records direct publisher failures without changing the
3924
3923
  already committed run.
3925
3924
 
@@ -4046,9 +4045,33 @@ spent nothing, and an omitted object could not tell you which one you had.
4046
4045
  Two costs in different currencies do not add: the sum reports `unavailable`
4047
4046
  rather than picking a label. The core records a currency and never converts one.
4048
4047
 
4049
- Usage is **not durable**. It reaches you on the operator and delivery event
4050
- streams and in `AgentRuntimeResult.metrics`, and stitchkit writes no spend to the
4051
- store where a figure lives afterwards is the application's (→ ADR 0002).
4048
+ **A run's figure is durable, and that is where to read it when a channel loses
4049
+ it.** `AgentRun.usage` is written at every checkpoint and again with the terminal
4050
+ record, so a crashed process leaves behind what it had already spent and a
4051
+ dropped event is not a lost number:
4052
+
4053
+ ```ts
4054
+ const snapshot = await store.loadSnapshot(conversationId)
4055
+ const spent = snapshot.runs.find((run) => run.id === runId)?.usage
4056
+ ```
4057
+
4058
+ Two gaps are open and are stated rather than left to be discovered:
4059
+
4060
+ - **Both event sinks are bounded and drop under load**, by arrival order — the
4061
+ event carrying the money is exactly as droppable as the one carrying nothing.
4062
+ The run record is the recovery, and this paragraph is the only place that says
4063
+ so.
4064
+ - **`AgentRuntimeResult.metrics` is `undefined` when this executor did not win
4065
+ the terminal race.** Read `result.run.usage` — the durable figure — rather than
4066
+ `result.metrics`, which is the channel that can go missing.
4067
+ - **Compaction spend is invisible unless you report it.** `config.history.compact`
4068
+ calls a model inside the turn and produces no step and no event of its own.
4069
+ Return `usage` from `AgentCompactionResult` and it joins the run's figure;
4070
+ omit it and the run under-reports by whatever summarising cost.
4071
+
4072
+ What stitchkit does **not** keep is a ledger: one figure, on the run that
4073
+ produced it, never aggregated and never reconciled against a provider invoice
4074
+ (→ ADR 0110).
4052
4075
 
4053
4076
  ### Reconciling with the provider's own accounting
4054
4077
 
@@ -4066,8 +4089,10 @@ The join is yours, and `runId` is the key:
4066
4089
  await ledger.record({
4067
4090
  runId: terminal.run.id,
4068
4091
  conversationId: terminal.run.conversationId,
4069
- costUsd: terminal.metrics?.usage?.cost?.value ?? null, // null when `unavailable`
4070
- provenance: terminal.metrics?.usage?.cost?.provenance ?? 'unavailable',
4092
+ // The durable figure, not `terminal.metrics` that one is absent whenever
4093
+ // this executor did not win the terminal race.
4094
+ costUsd: terminal.run.usage?.cost?.value ?? null, // null when `unavailable`
4095
+ provenance: terminal.run.usage?.cost?.provenance ?? 'unavailable',
4071
4096
  })
4072
4097
 
4073
4098
  // later — the provider's accounting names the same generation
@@ -7819,6 +7844,81 @@ implement `AgentRuntimeStoreDriver` and compose the aggregate with
7819
7844
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
7820
7845
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
7821
7846
 
7847
+ ## Released migration: 0.65.0
7848
+
7849
+ The largest migration of the pre-1.0 line, and most of it is the compiler
7850
+ pointing at things. One item is a feature withdrawal and two change behaviour
7851
+ with no compile error at all.
7852
+
7853
+ ### A projection a provider accepts, and a policy withdrawn
7854
+
7855
+ ### If you compact, or use `system-note`, you were broken and now are not
7856
+
7857
+ No code change to adopt the fix — but **check your own code if you call
7858
+ `projectAgentHistory` yourself.** System and summary records no longer appear in
7859
+ `messages`; they come back in `system` and belong in the provider's instructions
7860
+ channel, which is the only place `ai` accepts them:
7861
+
7862
+ ```ts
7863
+ // before — the provider refuses this outright
7864
+ const messages = await projectAgentHistory(snapshot.messages)
7865
+ streamText({ model, messages })
7866
+
7867
+ // after
7868
+ const { messages, system } = await projectAgentHistoryDetailed(snapshot.messages)
7869
+ streamText({
7870
+ model,
7871
+ instructions: system.map((content) => ({ role: 'system', content })),
7872
+ messages,
7873
+ })
7874
+ ```
7875
+
7876
+ If you use `createAgentRuntime`, this is handled for you.
7877
+
7878
+ ### `inputPolicy: 'inject'` is withdrawn
7879
+
7880
+ ```ts
7881
+ // before
7882
+ runs: { inputPolicy: 'inject' }
7883
+ // after
7884
+ runs: { inputPolicy: 'queue' }
7885
+ ```
7886
+
7887
+ `'absorbed'`, `AgentRun.absorbedIntoRunId` and `AgentRuntimeStore.absorbQueuedRun`
7888
+ go with it. A driver built on `AgentRuntimeStoreDriver` needs no change.
7889
+
7890
+ ### Records must agree with themselves
7891
+
7892
+ If you construct `AgentRun` values — a store double, a fixture, a migration —
7893
+ derive the state instead of setting it:
7894
+
7895
+ ```ts
7896
+ // after
7897
+ import { runStateForTerminalReason } from 'stitchkit/agent-runtime'
7898
+ AgentRunSchema.parse({ …run, terminalReason: reason, state: runStateForTerminalReason(reason) })
7899
+ ```
7900
+
7901
+ A terminal state with no reason, a queued run carrying one, and `policy_stop`
7902
+ without a `terminalPolicyName` are now all refused at parse time.
7903
+
7904
+ ### Enum and field changes the compiler will point at
7905
+
7906
+ - `AgentTerminalReason`: `'tool_failure'` removed (never produced),
7907
+ `'context_overflow'` added — this runtime's own refusal when the prompt does
7908
+ not fit, which used to report `provider_failure`.
7909
+ - `AgentRunState`: `'absorbed'` removed.
7910
+ - `AgentRunMetrics.usage` is required.
7911
+ - `AgentHistoryBudgetDecision['reason']`: `'superseded'` → `'unspeakable'`.
7912
+
7913
+ ### Two behaviour changes with no compile error
7914
+
7915
+ - **`loop.idleTimeoutMs` now defaults to 60 000.** A run whose provider stream
7916
+ goes quiet for a minute ends as `timeout` instead of holding the lane forever.
7917
+ Pass `null` for the old behaviour, and think about why you want it.
7918
+ - **`advanceAgentRuntimeEventCursor` stops returning `gap` for durable events.**
7919
+ If you reload a conversation on `gap`, you were reloading after essentially
7920
+ every run. Transient events still report it, and there it is real.
7921
+
7822
7922
  ## Released migration: 0.64.0
7823
7923
 
7824
7924
  Two changes, and only one of them can break a build. Nothing was removed.
@@ -10379,7 +10479,7 @@ Server-only optional application runtime. See the
10379
10479
  | `AgentRuntimeRecordIds` | _type_ | optional caller-provided input, run and assistant IDs for stable application records |
10380
10480
  | `AgentRuntimeAdmission` | _type_ | canonical committed input, assigned run, pending assistant projection, compatibility IDs and snapshot version |
10381
10481
  | `AgentAdmissionEventSchema` | schema | post-commit admission projection; removes store rereads but does not imply exactly-once delivery |
10382
- | `AgentRunMetricsSchema` | schema | optional provenance-aware usage and timings; `partial` distinguishes checkpoint from terminal totals |
10482
+ | `AgentRunMetricsSchema` | schema | optional provenance-aware usage and timings; `partial` says the provider never reported the run finished, so the figure beside it is not a confirmed total |
10383
10483
  | `AgentRuntimeRecoverOptions` | _type_ | bounded paged startup recovery with context resolver and explicit evidence policy |
10384
10484
  | `AgentRuntimeConflictError` | class | thrown when a store mutation loses to a concurrent writer — catchable by type from `stitchkit/agent-runtime` |
10385
10485
  | `AgentSessionCloseOptions` | _type_ | `gracePeriodMs` for natural settlement, then abort, then `forceTimeoutMs` for bounded settlement after it |
@@ -10414,8 +10514,8 @@ Canonical protocol exports are `AgentProtocol`, `AgentProtocolConfig`, `AgentRec
10414
10514
  Store command/result exports are `AcceptInputAndAssignRun`, `AcceptInputAndAssignRunSchema`,
10415
10515
  `AcquireAgentRun`, `AcquireAgentRunSchema`, `CheckpointRunAssistant`,
10416
10516
  `CheckpointRunAssistantSchema`, `CommitRunTerminal`, `CommitRunTerminalSchema`,
10417
- `RequestRunInterrupt`, `RequestRunInterruptSchema`, `AbsorbQueuedRun`, `AbsorbQueuedRunSchema`,
10418
- `RecoverAgentRun`, `ReplaceCompactedRange`,
10517
+ `RequestRunInterrupt`, `RequestRunInterruptSchema`, `runStateForTerminalReason`,
10518
+ `ACTIVE_AGENT_RUN_STATES`, `RecoverAgentRun`, `ReplaceCompactedRange`,
10419
10519
  `ReplaceCompactedRangeSchema`, `AgentStoreMutationResult`, `AgentStoreMutationResultSchema`,
10420
10520
  `AgentStoreAppliedSchema`, `AgentStoreConflictSchema`, `AgentStoreDuplicateSchema`,
10421
10521
  `AgentStoreNotFoundSchema`, `AgentAdmissionReceipt`, `AgentAdmissionReceiptSchema`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.64.0",
3
+ "version": "0.65.1",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",