stitchkit 0.76.2 → 0.77.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.
package/llms-full.txt CHANGED
@@ -62,7 +62,7 @@ own, recorded as an ADR.
62
62
  | `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 |
63
63
  | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
64
64
  | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
65
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 21 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
65
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 22 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
66
66
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
67
67
  | `stitchkit/agent-runtime/coding-tools` | server (Bun or Node) | evolving | bounded host-authorized direct file and shell tools; a root boundary, not an OS sandbox |
68
68
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
@@ -70,9 +70,10 @@ own, recorded as an ADR.
70
70
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
71
71
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
72
72
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
73
- | `stitchkit/application` | server | evolving<br>_redefined in 4 of the 21 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
73
+ | `stitchkit/application` | server | evolving<br>_redefined in 4 of the 22 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
74
74
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
75
75
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
76
+ | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas with no server runtime behind them |
76
77
 
77
78
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
78
79
  code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
@@ -5478,6 +5479,105 @@ Every instrument uses unit `1` and reports an absolute current/lifetime value:
5478
5479
  | `stitchkit.application.schedule.accepting`, `.active`, `.queued`, `.runs_started`, `.runs_completed`, `.runs_failed`, `.ticks_skipped` | current schedule state and absolute run/tick counts |
5479
5480
  | `stitchkit.application.activity.active`, `.queued`, `.completed`, `.failed` | absolute stage projections for declared activity sources |
5480
5481
 
5482
+ ## Replacing part of the graph without stopping the process
5483
+
5484
+ `restart` takes down one resource **and everything that depends on it**, then
5485
+ brings that subtree back. Everything else keeps running, and the process epoch
5486
+ does not move.
5487
+
5488
+ ```ts
5489
+ const result = await app.restart({ resourceId: 'database' })
5490
+
5491
+ result.outcome // 'restarted' | 'failed' | 'refused'
5492
+ result.affected // ['database', 'repository', 'api'] — in start order
5493
+ ```
5494
+
5495
+ The dependants come down with it because they are holding what the resource
5496
+ published. A repository that kept running across a database replacement holds a
5497
+ pool that has been closed: it still typechecks, still has methods, and fails at
5498
+ whatever moment the first call happens. There is no version of this where a
5499
+ dependant keeps a live handle, so the subtree — not the resource — is the unit.
5500
+
5501
+ A leaf restart affects one resource, and an independent neighbour is never
5502
+ touched:
5503
+
5504
+ ```ts
5505
+ await app.restart({ resourceId: 'cache' })
5506
+ // mailer and database were not stopped, not started, not activated
5507
+ ```
5508
+
5509
+ **Refused is not failed.** An unknown id, a restart during shutdown, and a
5510
+ restart before the application is ready all return `refused` with a reason and
5511
+ touch nothing. `failed` means the subtree came down and the new generation did
5512
+ not come up — the snapshot says so too, with the failing resource marked
5513
+ `failed` / `unhealthy`, because the restart records a failure the same way a
5514
+ startup does.
5515
+
5516
+ Restarts of overlapping subtrees queue behind each other rather than being
5517
+ refused. Two callers asking at once is ordinary; two generations of one resource
5518
+ alive at once is the thing this must make impossible.
5519
+
5520
+ ```ts
5521
+ // Both succeed, one complete pass after the other.
5522
+ await Promise.all([
5523
+ app.restart({ resourceId: 'database' }),
5524
+ app.restart({ resourceId: 'database' }),
5525
+ ])
5526
+ ```
5527
+
5528
+ What a restart is **not** is a process restart: it replaces resources, and does
5529
+ not re-read configuration the kernel captured when it was constructed.
5530
+
5531
+ → [ADR 0154](../decisions/0154-the-unit-of-a-restart-is-the-subtree.md).
5532
+
5533
+ ## Decisions a policy set makes together
5534
+
5535
+ `createDecisionPipeline` runs an ordered list of policies that each vote
5536
+ `allow`, `deny` (with a reason) or `defer`. The first terminal verdict wins and
5537
+ the rest do not run.
5538
+
5539
+ ```ts
5540
+ import { createDecisionPipeline } from 'stitchkit/application'
5541
+
5542
+ const pipeline = createDecisionPipeline<{ userId: string; scope: string }>([
5543
+ { id: 'banned', decide: (r) => (isBanned(r.userId)
5544
+ ? { outcome: 'deny', reason: 'account suspended' }
5545
+ : { outcome: 'defer' }) },
5546
+ { id: 'scope', decide: (r) => (r.scope === 'admin'
5547
+ ? { outcome: 'allow' }
5548
+ : { outcome: 'defer' }) },
5549
+ { id: 'default', decide: () => ({ outcome: 'deny', reason: 'no policy allowed this' }) },
5550
+ ])
5551
+
5552
+ const result = await pipeline.decide(request)
5553
+ result.outcome // 'allow' | 'deny' — never 'defer'
5554
+ result.trace // the policies that actually ran, in order
5555
+ ```
5556
+
5557
+ Three things are deliberate.
5558
+
5559
+ **A deny carries a reason, by schema.** `{ outcome: 'deny' }` alone does not
5560
+ typecheck. A refusal whose cause exists only in the log of whoever refused is a
5561
+ support ticket.
5562
+
5563
+ **The trace is what ran, not what was declared.** It stops at the terminal
5564
+ verdict. When something was denied the question is which policy denied it and
5565
+ what the ones before it said, and a trace listing policies that never ran would
5566
+ answer that wrongly while looking complete.
5567
+
5568
+ **Every policy deferring raises.** `DecisionUndecidedError`, not a default —
5569
+ defaulting to `allow` turns an incomplete policy set into an open door, and
5570
+ defaulting to `deny` turns it into an outage whose cause reads as a legitimate
5571
+ refusal. A policy that throws or times out denies with the policy named
5572
+ (`DecisionPolicyError`): broken must not mean skipped.
5573
+
5574
+ The same `allow`/`deny`/`defer` type is what an event topic declared
5575
+ `mode: 'decision'` uses in `stitchkit/live` — one vocabulary, because a listener
5576
+ voting on an event and a policy voting on a request are answering the same
5577
+ question.
5578
+
5579
+ → [ADR 0155](../decisions/0155-one-decision-vocabulary-and-an-unanswered-question-is-an-error.md).
5580
+
5481
5581
  ## Admission and graceful shutdown
5482
5582
 
5483
5583
  Use the application operation lease for work that is not already counted by a
@@ -10001,6 +10101,29 @@ makes one thing your job rather than the resolver's:
10001
10101
  The mechanical part is identical either way. Only the *noticing* differs, and an
10002
10102
  exact pin moves it onto you.
10003
10103
 
10104
+ ## Released migration: 0.77.0
10105
+
10106
+ Two type renames, and only if you named them. Nothing runtime moved.
10107
+
10108
+ ```bash
10109
+ rg -n "EventDecision|EventUndecided" --glob '*.ts' --glob '*.tsx'
10110
+ ```
10111
+
10112
+ ```ts
10113
+ // before
10114
+ import type { EventDecision, EventUndecided } from 'stitchkit/live'
10115
+ // after
10116
+ import type { PolicyDecision, UndecidedOutcome } from 'stitchkit/live'
10117
+ ```
10118
+
10119
+ The shapes are identical — `{ outcome: 'allow' } | { outcome: 'deny', reason } | { outcome: 'defer' }`
10120
+ and `'allow' | 'deny'`. If you only ever *return* decisions from listeners and never named the
10121
+ type, the search above finds nothing and there is nothing to do.
10122
+
10123
+ They were renamed because `createDecisionPipeline` (new in this release) votes with exactly the
10124
+ same three outcomes, and two identical types under two names is the thing that makes a search for
10125
+ either one return half the truth.
10126
+
10004
10127
  ## Released migration: 0.76.0
10005
10128
 
10006
10129
  One change, and only if you hand `createWatchClient` a transport you wrote yourself.
@@ -13150,7 +13273,8 @@ realtime contract from `stitchkit`, and the server halves live in `stitchkit/app
13150
13273
  |--------|------|---------|
13151
13274
  | `defineEvents` | function | declare topics: a wire name, one payload schema and how the topic is delivered to in-process listeners |
13152
13275
  | `toRealtimeContract` | function | project a declaration onto `RealtimeContract`, so the existing validated socket carries it |
13153
- | `EventDeliveryMode` / `EventDecision` / `EventUndecided` | _types_ | `emit` / `serial` / `decision`, one listener's vote, and the outcome when every listener defers |
13276
+ | `EventDeliveryMode` | _type_ | `emit` / `serial` / `decision` how a topic reaches its in-process listeners |
13277
+ | `PolicyDecision` / `UndecidedOutcome` | _types_ | one voter's `allow` / `deny` / `defer`, and what a run settles on when every voter defers — the same two types the decision pipeline in `stitchkit/application` uses, because a listener voting on an event and a policy voting on a request are answering the same question |
13154
13278
  | `EventTopicDeclaration` / `EventTopicRegistry` / `EventsConfig` / `EventsDeclaration` | _types_ | one topic's schema, mode, `whenAllDefer` and `listenerTimeoutMs`, and the declaration they compose into |
13155
13279
  | `EventPayloads` / `EventTopicsOfMode` / `WireTopic` | _types_ | payload map keyed by wire topic, the topics of one mode, and the `prefix.name` a topic is addressed by |
13156
13280
  | `createWatchClient` | function | contract-shaped watch client: `watch.action(args)` returns a ref-counted handle sharing one subscription |
@@ -13444,7 +13568,11 @@ cutovers are covered by the executable
13444
13568
 
13445
13569
  | Export | Kind | Summary |
13446
13570
  |--------|------|---------|
13447
- | `createApplication` | function | compose a validated resource DAG into one non-restartable startup, readiness, admission and shutdown state machine |
13571
+ | `createApplication` | function | compose a validated resource DAG into one startup, readiness, admission and shutdown state machine |
13572
+ | `ApplicationHandle.restart` | method | replace one resource and everything that depends on it, leaving the rest of the graph running and the process epoch unchanged |
13573
+ | `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id |
13574
+ | `ApplicationRestartResultSchema` / `ApplicationRestartResult` | schema / _type_ | the subtree that was actually taken down and brought back, in start order, with the outcome, the reason on anything but success, and how long it took |
13575
+ | `ApplicationRestartOutcomeSchema` / `ApplicationRestartOutcome` | schema / _type_ | `restarted`, `failed` or `refused` — a refusal (unknown id, shutting down, not yet ready) is not a failure and touches nothing |
13448
13576
  | `ApplicationResourceFailure` | _type_ | one resource failure with the cause its phase label cannot carry — delivered to `onResourceFailure` |
13449
13577
  | `ApplicationResourcePhase` | _type_ | the phase a managed resource failed in — the vocabulary of `ApplicationResourceShutdown.failures` |
13450
13578
  | `ApplicationShutdownOptionsSchema` / `ApplicationShutdownOptions` | schema / _type_ | the two shutdown budgets and an abort signal — without the HTTP-only `retryAfterSeconds` |
@@ -13568,6 +13696,10 @@ and `ApplicationEventSinkConfig`.
13568
13696
 
13569
13697
  ### Canonical application records
13570
13698
 
13699
+ These are also published on their own as
13700
+ [`stitchkit/application/schemas`](#stitchkitapplicationschemas), which carries
13701
+ no server runtime and can therefore be imported from a browser bundle.
13702
+
13571
13703
  The entrypoint exports each Zod schema beside its inferred type:
13572
13704
  `ApplicationIdSchema` / `ApplicationId`, `ApplicationLifecycleSchema` /
13573
13705
  `ApplicationLifecycle`, `ApplicationHealthSchema` / `ApplicationHealth`,
@@ -13581,6 +13713,20 @@ the function that derives it from a snapshot,
13581
13713
  `ApplicationResourceShutdownSchema` / `ApplicationResourceShutdown`, and
13582
13714
  `ApplicationShutdownResultSchema` / `ApplicationShutdownResult`.
13583
13715
 
13716
+ ### Decisions
13717
+
13718
+ A pipeline of policies that each vote `allow`, `deny` or `defer`, sharing its
13719
+ vocabulary with an event topic declared `mode: 'decision'`.
13720
+
13721
+ | Export | Kind | Summary |
13722
+ |--------|------|---------|
13723
+ | `createDecisionPipeline` | function | run policies in declaration order; the first terminal verdict wins and the rest do not run |
13724
+ | `PolicyDecisionSchema` / `PolicyDecision` | schema / _type_ | `allow`, `deny` with a reason, or `defer` — a deny cannot be silent |
13725
+ | `DecisionPolicy` / `DecisionPipeline` | _types_ | one named voter over the caller's subject, and the pipeline it composes into |
13726
+ | `DecisionResult` / `DecisionTraceEntry` | _types_ | the verdict, and the trace of what actually ran — not what was declared |
13727
+ | `DecisionUndecidedError` | class | every policy deferred and no undecided outcome was declared: an unanswered question, raised rather than guessed |
13728
+ | `DecisionPolicyError` | class | a policy threw or timed out; the pipeline denies with the policy named |
13729
+
13584
13730
  ### Keyspace and watched reads
13585
13731
 
13586
13732
  | Export | Kind | Summary |
@@ -13624,6 +13770,33 @@ snapshots; the adapter owns no SDK lifecycle, polling or delta state.
13624
13770
  | `ApplicationTelemetryMeter` | _type_ | minimal structural `Meter.createObservableGauge` boundary compatible with `@opentelemetry/api` |
13625
13771
  | `ApplicationOpenTelemetryCollectionError` | _type_ | isolated instrument-name/error diagnostic without product/provider attributes |
13626
13772
 
13773
+ ## `stitchkit/application/schemas`
13774
+
13775
+ The canonical application records — and nothing else. Every export here is also
13776
+ reachable from `stitchkit/application`; the difference is what comes with it.
13777
+
13778
+ `stitchkit/application` is the server runtime: it reaches `node:child_process`,
13779
+ `node:fs` and `node:crypto`. A browser bundler does not omit those, it
13780
+ substitutes stubs — so a module that merely *names* an application schema fails
13781
+ while it is initialising, and the page never mounts, on every route rather than
13782
+ the one that wanted the schema. A contract whose `output` is an application
13783
+ snapshot could be declared and never consumed, which is the one thing a contract
13784
+ is for.
13785
+
13786
+ So the same schemas ship a second way, from a module with nothing behind them:
13787
+
13788
+ ```ts
13789
+ // browser and server alike
13790
+ import { ApplicationSnapshotSchema } from 'stitchkit/application/schemas';
13791
+ ```
13792
+
13793
+ The exports are exactly those listed under
13794
+ [canonical application records](#canonical-application-records) above.
13795
+ `packages/core/scripts/check-browser-clean.mjs` holds the promise against the
13796
+ built artifact: it walks the browser lane's real `dist` output and refuses a
13797
+ `node:` import reachable from any of it, and refuses an entry built for the
13798
+ browser that no `exports` path leads to — which is how this one was missing.
13799
+
13627
13800
  ---
13628
13801
 
13629
13802
  ## `stitchkit/agent-runtime`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.76.2",
3
+ "version": "0.77.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",
@@ -129,6 +129,10 @@
129
129
  "types": "./dist/application-opentelemetry.d.ts",
130
130
  "import": "./dist/application-opentelemetry.js"
131
131
  },
132
+ "./application/schemas": {
133
+ "types": "./dist/application-schemas.d.ts",
134
+ "import": "./dist/application-schemas.js"
135
+ },
132
136
  "./testing": {
133
137
  "types": "./dist/testing.d.ts",
134
138
  "import": "./dist/testing.js"
@@ -156,7 +160,7 @@
156
160
  "scripts": {
157
161
  "build:native-contained-files": "node scripts/build-contained-files-native.mjs",
158
162
  "check": "bun x tsc --noEmit",
159
- "build:browser": "bun build src/index.ts src/live.ts src/react.ts src/contract/index.ts src/primitives.ts src/declaration.ts src/agent-runtime-browser.ts --outdir dist --target node --packages external --splitting --root src",
163
+ "build:browser": "bun build src/index.ts src/live.ts src/react.ts src/contract/index.ts src/primitives.ts src/declaration.ts src/application-schemas.ts src/agent-runtime-browser.ts --outdir dist --target node --packages external --splitting --root src",
160
164
  "build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/tool-invoker.ts src/cli.ts src/remote.ts src/files.ts src/testing.ts src/observability/index.ts src/agent-runtime.ts src/agent-runtime-harness.ts src/agent-runtime-coding-tools.ts src/agent-runtime-openrouter.ts src/agent-runtime-sqlite-bun.ts src/agent-runtime-sqlite-node.ts src/application.ts src/application-grammy.ts src/application-opentelemetry.ts src/telegram.ts --outdir dist --target node --packages external --splitting --root src",
161
165
  "build:js": "bun run build:browser && bun run build:server && bun scripts/preserve-webpack-ignore.mjs",
162
166
  "build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly && bun scripts/rewrite-declaration-specifiers.mjs",