stitchkit 0.61.0 → 0.62.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 (31) hide show
  1. package/dist/agent-runtime/compaction.d.ts.map +1 -1
  2. package/dist/agent-runtime/coordinator.d.ts +17 -2
  3. package/dist/agent-runtime/coordinator.d.ts.map +1 -1
  4. package/dist/agent-runtime/events.d.ts +16 -0
  5. package/dist/agent-runtime/events.d.ts.map +1 -1
  6. package/dist/agent-runtime/history.d.ts +39 -1
  7. package/dist/agent-runtime/history.d.ts.map +1 -1
  8. package/dist/agent-runtime/observability.d.ts +2 -0
  9. package/dist/agent-runtime/observability.d.ts.map +1 -1
  10. package/dist/agent-runtime/prompt.d.ts +1 -1
  11. package/dist/agent-runtime/prompt.d.ts.map +1 -1
  12. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  13. package/dist/agent-runtime/runtime-internals.d.ts +47 -0
  14. package/dist/agent-runtime/runtime-internals.d.ts.map +1 -1
  15. package/dist/agent-runtime/runtime.d.ts +1 -0
  16. package/dist/agent-runtime/runtime.d.ts.map +1 -1
  17. package/dist/agent-runtime/schemas.d.ts +9 -0
  18. package/dist/agent-runtime/schemas.d.ts.map +1 -1
  19. package/dist/agent-runtime/store-driver.d.ts +11 -0
  20. package/dist/agent-runtime/store-driver.d.ts.map +1 -1
  21. package/dist/agent-runtime/store.d.ts +27 -0
  22. package/dist/agent-runtime/store.d.ts.map +1 -1
  23. package/dist/agent-runtime/terminal-commit.d.ts +3 -3
  24. package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
  25. package/dist/agent-runtime/terminal-status.d.ts +13 -0
  26. package/dist/agent-runtime/terminal-status.d.ts.map +1 -1
  27. package/dist/agent-runtime.js +251 -82
  28. package/dist/{index-vtjgx3vv.js → index-58vkx74h.js} +3 -0
  29. package/dist/testing.js +1 -1
  30. package/llms-full.txt +266 -8
  31. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -3672,6 +3672,110 @@ process-local coordinator releases its lane only after terminal commit.
3672
3672
  input + queued run → running → execution settled → terminal CAS → successor
3673
3673
  ```
3674
3674
 
3675
+ ## What happens to a run when new input arrives
3676
+
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:
3679
+
3680
+ | policy | the run in flight | what it already produced |
3681
+ |--------|-------------------|--------------------------|
3682
+ | `queue` (default) | finishes first | kept |
3683
+ | `interrupt` | ends | kept, and marked as cut off |
3684
+ | `supersede` | ends | discarded from the prompt, kept in the record |
3685
+ | _inject_ | continues | — not supported; see below |
3686
+
3687
+ `interrupt` and `supersede` differ in exactly one thing, and the question that
3688
+ picks between them is **not** "was the run interrupted" but **"did anyone see
3689
+ what it produced"**:
3690
+
3691
+ - The user pressed **stop**. The partial answer was streamed to their screen and
3692
+ they read it. It belongs in the conversation — dropping it makes the history
3693
+ lie to the model about what the human has seen. That is `interrupt`.
3694
+ - A newer message **superseded** the run. Whether the partial reached anyone
3695
+ depends on the delivery surface: a token stream shows it as it is produced, a
3696
+ surface that sends nothing until the run is done never showed it at all. When
3697
+ it reached nobody, it is not part of the conversation. That is `supersede`.
3698
+
3699
+ **Stitchkit cannot answer that question for you** — delivery belongs to the
3700
+ transport, and the runtime sees an abort, not a screen. Hence a declared policy
3701
+ (→ ADR 0108), and hence `inputPolicy` accepting a function, so one application
3702
+ can hold two surfaces with different rules without the core learning which is
3703
+ which:
3704
+
3705
+ ```ts
3706
+ runs: {
3707
+ inputPolicy: (input) =>
3708
+ protocol.parseContext(input.context).surface === 'operator' ? 'queue' : 'supersede',
3709
+ }
3710
+ ```
3711
+
3712
+ `input.context` is the **raw** context here — admission runs before the runtime
3713
+ parses it, so the callback narrows it itself with the protocol it already has.
3714
+
3715
+ A superseded run ends with `terminalReason: 'superseded'`, run state
3716
+ `'superseded'` and an assistant message of status `'superseded'`. **The record
3717
+ is kept** — excluded from the projection, not deleted — so an operator can see
3718
+ what was thrown away, and run identity, admission receipts and the terminal CAS
3719
+ keep the row they depend on. Compaction leaves it alone for the same reason: a
3720
+ turn whose answer is never spoken is not a turn that may be summarised into one,
3721
+ because that would both feed the discarded text to the summariser and drop the
3722
+ record in `replacedMessageIds`.
3723
+
3724
+ It is also outside the token budget. `selectAgentHistory` removes it with reason
3725
+ `'superseded'` and does not count it, so an abandoned fragment cannot push a
3726
+ real turn out of a context it never occupies.
3727
+
3728
+ ### How an interrupted turn reaches the model
3729
+
3730
+ An interrupted turn is projected, and says so:
3731
+
3732
+ ```text
3733
+ { role: 'assistant', content: [
3734
+ { type: 'text', text: 'We are the team, where would you like' },
3735
+ { type: 'text', text: '[interrupted: this turn was cut off before it finished]' },
3736
+ ]}
3737
+ ```
3738
+
3739
+ `history.interruptedAssistant` chooses the form, and the difference between the
3740
+ first two is structural rather than cosmetic. **An assistant turn in provider
3741
+ history is a commitment**: the model reads its own previous turn as something it
3742
+ said and stays consistent with it. A system line is context.
3743
+
3744
+ | value | form | right when |
3745
+ |-------|------|-----------|
3746
+ | `assistant-marked` (default) | assistant turn plus a marker | the human read the text |
3747
+ | `system-note` | `[interrupted] partial response: …` as a system line | the fragment reached nobody |
3748
+ | `omit` | not projected at all | you want it gone from the request |
3749
+
3750
+ There is deliberately no value that reproduces what the projection used to do,
3751
+ which was to send the partial as an ordinary assistant turn and drop its
3752
+ `control` marker on the way. That was the defect, not a behaviour to stay
3753
+ compatible with.
3754
+
3755
+ `projectAgentHistoryDetailed` reports what reached the provider, including part
3756
+ types that no projected content stands for:
3757
+
3758
+ ```ts
3759
+ const { decisions } = await projectAgentHistoryDetailed(snapshot.messages)
3760
+ // → { messageId: 'assistant-2', action: 'projected', reason: 'projected',
3761
+ // omittedParts: ['source', 'provider'] }
3762
+ // → { messageId: 'assistant-1', action: 'omitted', reason: 'superseded' }
3763
+ ```
3764
+
3765
+ `runtime.stop(key, 'supersede')` is the same decision taken by hand: the
3766
+ process-local escape hatch chooses the reason, so a caller that knows the answer
3767
+ was never delivered can discard it without a newer input arriving.
3768
+
3769
+ ### The one that is not supported
3770
+
3771
+ **inject** — hand the input to the loop between tool calls and let the run
3772
+ continue — has no primitive. `loop.prepareStep` passes the AI SDK return type
3773
+ through, so an application *can* append messages between steps, but nothing
3774
+ hands `prepareStep` the pending inputs and nothing attaches an absorbed input to
3775
+ the running run's `inputMessageIds`. A run that answered two messages would
3776
+ carry a durable record claiming it answered one. It is reachable by hand, not
3777
+ supported.
3778
+
3675
3779
  With `runs.coalescePending: true`, an active lane has at most one queued
3676
3780
  successor. Every later accepted input is atomically appended to that successor;
3677
3781
  its `AgentRun.inputMessageIds` records the whole batch and every input ticket
@@ -3722,8 +3826,12 @@ coordinator signal. If provider completion races that revision change, the termi
3722
3826
  the canonical snapshot. An already-terminal winner settles the ticket directly; a still-owned
3723
3827
  `interrupt_requested` run is committed as `interrupted`, and unrelated aggregate-head conflicts
3724
3828
  remain retriable while the run is active with the same owner and fencing token. A stale owner or
3725
- fencing token remains a conflict. Only the execution that applies the terminal mutation emits the
3726
- terminal event and operator metrics; a loser settles from canonical state without republishing it.
3829
+ fencing token remains a conflict. Only the execution that applies the terminal mutation publishes the
3830
+ **delivery** `terminal` event a loser settles from canonical state without republishing the turn,
3831
+ and its `AgentRuntimeResult.metrics` is `undefined`. The **operator** `run-terminal` event is not
3832
+ gated that way: a losing execution still ran, and still spent whatever it spent, so it reports its
3833
+ own usage. The two channels answer to different readers — delivering a turn twice is a user's
3834
+ problem, and omitting a run's cost is an operator's.
3727
3835
  `runtime.stop(key)` is the process-local signal-only escape hatch.
3728
3836
 
3729
3837
  ## Store operations
@@ -3882,10 +3990,43 @@ sending new event kinds to existing request sinks. Product events omit provider
3882
3990
  causes. Operator `internalCause` is also redacted by default; an operator-only sink must explicitly
3883
3991
  set `includeInternalCause` and own its retention policy.
3884
3992
 
3885
- Usage values carry `provider-reported`, `computed`, `estimated` or
3886
- `unavailable` provenance. Cost additionally carries an ISO currency code;
3887
- OpenRouter-reported cost is normalized as USD. Missing values remain absent,
3888
- never zero-filled.
3993
+ ### What a run says it spent
3994
+
3995
+ Usage values carry `provider-reported`, `computed`, `estimated` or `unavailable`
3996
+ provenance, **per field**. Cost additionally carries an ISO currency code;
3997
+ OpenRouter-reported cost is normalized as USD.
3998
+
3999
+ Read the provenance before the number (→ ADR 0109):
4000
+
4001
+ - **`provider-reported`** — the provider handed us exactly this. On
4002
+ `step-finished`, that is what a step's figures are.
4003
+ - **`computed`** — a total, added up over steps. **Every figure on a terminal
4004
+ event is this**, tokens included: the AI SDK's `totalUsage` is a sum it
4005
+ performed, not a number a provider reported for the run. It is not a figure to
4006
+ bill against unchanged.
4007
+ - **`unavailable`** — nobody reported it. Not zero.
4008
+
4009
+ Two rules follow from that last one, and they differ by field on purpose:
4010
+
4011
+ - **A token total with an unreported step is a floor**, labelled `computed`. A
4012
+ token count is a diagnostic, and a floor is a useful one.
4013
+ - **A cost with an unreported step is `unavailable`, not a floor.** Money is what
4014
+ people bill against, and "at least $1.00" reported as `$1.00` is the same class
4015
+ of lie this whole section exists to remove. One step that did not report its
4016
+ cost makes the run's cost unknown — not smaller. It also stays unknown: later
4017
+ steps reporting normally cannot revive it.
4018
+
4019
+ **A terminal event always carries `usage`.** A run that ended before the provider
4020
+ reported anything — superseded, interrupted, timed out, shut down, failed —
4021
+ carries every field `unavailable`. That is deliberately different from a run that
4022
+ spent nothing, and an omitted object could not tell you which one you had.
4023
+
4024
+ Two costs in different currencies do not add: the sum reports `unavailable`
4025
+ rather than picking a label. The core records a currency and never converts one.
4026
+
4027
+ Usage is **not durable**. It reaches you on the operator and delivery event
4028
+ streams and in `AgentRuntimeResult.metrics`, and stitchkit writes no spend to the
4029
+ store — where a figure lives afterwards is the application's (→ ADR 0002).
3889
4030
 
3890
4031
  The sink deduplicates stable event IDs by default. Cross-crash exactly-once still requires a durable
3891
4032
  outbox.
@@ -7609,6 +7750,123 @@ current one *up to* your target, and apply each snippet.
7609
7750
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
7610
7751
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
7611
7752
 
7753
+ ## Released migration: 0.62.0
7754
+
7755
+ Two groups of behaviour changes. Nothing moves an export — the surface is
7756
+ strictly additive — and every item changes what a running system reports or
7757
+ sends, which is what this heading is for. One of them changes a number you may
7758
+ already be billing against.
7759
+
7760
+ ### A run reports what it spent
7761
+
7762
+ Three changes to what the runtime says about cost and tokens. Nothing moves an
7763
+ export; all three change numbers a running system reports, and one of them
7764
+ changes a number you may already be billing against.
7765
+
7766
+ #### Multi-step cost was under-reported and is now summed
7767
+
7768
+ No code change is needed to get the fix — but check any predicate that reads
7769
+ `provenance`:
7770
+
7771
+ ```ts
7772
+ // before — accepted a number that was one step's cost, not the run's
7773
+ if (usage.cost?.provenance === 'provider-reported') bill(usage.cost.value)
7774
+
7775
+ // after — a sum stitchkit performed says so
7776
+ if (usage.cost && usage.cost.provenance !== 'unavailable') bill(usage.cost.value)
7777
+ ```
7778
+
7779
+ `'computed'` means stitchkit added up provider-reported parts. It is not a guess
7780
+ — `'estimated'` is the word for that — but it is deliberately not
7781
+ `'provider-reported'`, because that label is what a caller filters on when it
7782
+ wants a figure it can bill against unchanged, and a sum is not one.
7783
+
7784
+ Token totals moved with it. The AI SDK's `totalUsage` is a sum *it* performed
7785
+ over per-step provider figures — not a run total any provider handed over — so
7786
+ labelling it `provider-reported` was the same overstatement. **A run total on a
7787
+ terminal event is always `computed`.** If you want a figure with the provider's
7788
+ own word on it, read `step-finished`: each step carries what that call reported.
7789
+
7790
+ #### `usage` is always present on a terminal event
7791
+
7792
+ ```ts
7793
+ // before — absent when the run ended before the provider's `finish`
7794
+ const spent = event.usage?.cost?.value ?? 0 // silently 0 for a real spend
7795
+
7796
+ // after — present, and it says what it does not know
7797
+ event.usage?.cost?.provenance === 'unavailable' // we spent, and cannot say how much
7798
+ ```
7799
+
7800
+ Keep the optional chaining: one `AgentRunEvent` shape covers `run-started`,
7801
+ `step-finished` and `run-terminal`, so `usage` stays optional on the type. The
7802
+ guarantee is about terminal events, and a schema shared with `run-started`
7803
+ cannot express it.
7804
+
7805
+ **Do not read `unavailable` as zero.** A run aborted mid-stream has spent real
7806
+ money that nobody has counted; a run that never reached the provider has not.
7807
+ Both used to look the same and now do not.
7808
+
7809
+ #### A losing executor reports its own spend
7810
+
7811
+ An execution that loses the terminal compare-and-swap now emits an operator
7812
+ `run-terminal` event, because it ran and it spent. If a sink treated those events
7813
+ as "runs this process committed", that is no longer true — `AgentRuntimeResult.metrics`
7814
+ is still `undefined` for a losing executor and remains the way to tell.
7815
+
7816
+ The *delivery* `terminal` event is unchanged and still fires only for the winner,
7817
+ so nothing delivers a turn twice.
7818
+
7819
+ ### An interrupted answer stops passing as a finished one
7820
+
7821
+ Two behaviour changes, one shared cause: a run ended by a newer input used to
7822
+ leave its half-written answer in the conversation with no sign that it was cut
7823
+ off, and the next request to the provider carried it as an ordinary assistant
7824
+ turn.
7825
+
7826
+ #### The projection marks an interrupted turn
7827
+
7828
+ Nothing to change to adopt the fix — the default is the fixed behaviour. What to
7829
+ check is whether the marker is the *right* form for your surface, and the
7830
+ question that decides it is not "was the run interrupted" but **"did anyone see
7831
+ what it produced"**.
7832
+
7833
+ ```ts
7834
+ // after — pick the form; the default is 'assistant-marked'
7835
+ createAgentRuntime({ history: { interruptedAssistant: 'system-note' } })
7836
+ ```
7837
+
7838
+ - **The user pressed stop and the text was on their screen** — keep
7839
+ `'assistant-marked'`. The assistant turn is the truthful record of what the
7840
+ human read, and the model should stay consistent with it.
7841
+ - **The partial never reached anyone** — a surface that sends nothing until the
7842
+ run is done — prefer `'system-note'`. An assistant turn in provider history is
7843
+ a commitment the model stays consistent with; a system line is context.
7844
+
7845
+ If you pass `history.project` you own the projection outright and none of this
7846
+ applies — but the same question does.
7847
+
7848
+ #### A run ended by a newer input can now say so
7849
+
7850
+ ```ts
7851
+ // before
7852
+ runs: { inputPolicy: 'interrupt' } // ends the run, keeps its partial answer
7853
+
7854
+ // after — for a surface where a follow-up message invalidates the answer
7855
+ // in flight rather than merely stopping it
7856
+ runs: { inputPolicy: 'supersede' } // ends the run, discards its partial answer
7857
+ ```
7858
+
7859
+ `inputPolicy` also accepts `(input) => policy`, which is how one application
7860
+ gives two conversation surfaces different rules without the runtime learning
7861
+ which is which.
7862
+
7863
+ A superseded run terminates with `terminalReason: 'superseded'`, state
7864
+ `'superseded'` and an assistant message of status `'superseded'`. **If you switch
7865
+ exhaustively on any of those enums, add the arm** — that is the part of this
7866
+ release that can break a build rather than a behaviour. The record itself is
7867
+ kept: it is excluded from the projection, not deleted, so an operator can still
7868
+ see what was thrown away.
7869
+
7612
7870
  ## Released migration: 0.61.0
7613
7871
 
7614
7872
  Three behaviour changes between versions. None moves an export — the surface is
@@ -9938,7 +10196,7 @@ Server-only optional application runtime. See the
9938
10196
  | `defineModelRegistry` | function | typed language-model descriptors, capabilities and provider construction |
9939
10197
  | `composeAgentPrompt` | function | ordered prompt contributions and provenance-aware context budget |
9940
10198
  | `structuredCompaction` | function | summarize a provider-valid snapshot range and replace it through CAS |
9941
- | `createAgentSessionCoordinator` | function | strict process-local queue/interrupt lifecycle |
10199
+ | `createAgentSessionCoordinator` | function | strict process-local queue/interrupt/supersede lifecycle |
9942
10200
  | `AgentRuntimeStopPolicy` | _type_ | named custom AI SDK stop condition persisted and published on policy stop |
9943
10201
  | `AgentRuntimePrepareStep` | _type_ | per-run controlled step callback with typed domain context and managed run signal/fence |
9944
10202
  | `AgentRuntimeRecordIds` | _type_ | optional caller-provided input, run and assistant IDs for stable application records |
@@ -9949,7 +10207,7 @@ Server-only optional application runtime. See the
9949
10207
  | `AgentRuntimeConflictError` | class | thrown when a store mutation loses to a concurrent writer — catchable by type from `stitchkit/agent-runtime` |
9950
10208
  | `AgentSessionCloseOptions` | _type_ | `gracePeriodMs` for natural settlement, then abort, then `forceTimeoutMs` for bounded settlement after it |
9951
10209
  | `AgentSessionCloseResult` | _type_ | what `close()` achieved: `settled`, or `timedOut` with `remaining` runs still in flight. Only omitting `forceTimeoutMs` guarantees nothing is in flight on return |
9952
- | `AgentHistoryProjectionOptions` | _type_ | storage-neutral file resolver and explicit unresolved-file behavior |
10210
+ | `AgentHistoryProjectionOptions` | _type_ | storage-neutral file resolver, explicit unresolved-file behavior, and how an interrupted turn reaches the model (`interruptedAssistant`) |
9953
10211
  | `createAgentToolFenceLifecycle` | function | pre-effect and post-effect run ownership fence for `mountAgent` |
9954
10212
  | `AgentRuntimeEventSchema` | schema | transient stream lifecycle plus post-commit admission/checkpoint/run-state/terminal projections |
9955
10213
  | `createAgentObservability` | function | separate agent-run sink over the shared bounded observability lifecycle |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.61.0",
3
+ "version": "0.62.0",
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",