stitchkit 0.77.0 → 0.79.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 (99) hide show
  1. package/dist/agent-runtime-browser.js +53 -581
  2. package/dist/agent-runtime-coding-tools.js +6 -5
  3. package/dist/agent-runtime-harness.js +20 -93
  4. package/dist/agent-runtime-openrouter.js +1 -1
  5. package/dist/agent-runtime.js +65 -61
  6. package/dist/application/decisions.d.ts +28 -25
  7. package/dist/application/decisions.d.ts.map +1 -1
  8. package/dist/application/kernel.d.ts +2 -0
  9. package/dist/application/kernel.d.ts.map +1 -1
  10. package/dist/application/keyspace.d.ts +11 -1
  11. package/dist/application/keyspace.d.ts.map +1 -1
  12. package/dist/application/schedule.d.ts.map +1 -1
  13. package/dist/application/schemas.d.ts +2 -0
  14. package/dist/application/schemas.d.ts.map +1 -1
  15. package/dist/application/server-resource.d.ts.map +1 -1
  16. package/dist/application/watch-hub.d.ts.map +1 -1
  17. package/dist/application-diagnostic-journal.d.ts +21 -0
  18. package/dist/application-diagnostic-journal.d.ts.map +1 -0
  19. package/dist/application-diagnostic-journal.js +632 -0
  20. package/dist/application-opentelemetry.js +3 -2
  21. package/dist/application-schemas.js +14 -98
  22. package/dist/application.d.ts +1 -2
  23. package/dist/application.d.ts.map +1 -1
  24. package/dist/application.js +155 -882
  25. package/dist/cli.js +13 -11
  26. package/dist/contract/index.js +15 -10
  27. package/dist/files/boundary.d.ts.map +1 -1
  28. package/dist/files.js +1 -1
  29. package/dist/{index-7b188kmz.js → index-2qzskm8h.js} +15 -1
  30. package/dist/{index-k2zczx1g.js → index-2rwwmr7v.js} +3 -3
  31. package/dist/{index-8eywc9zv.js → index-3gye9wzb.js} +6 -3
  32. package/dist/index-4fpa9dy2.js +51 -0
  33. package/dist/{index-7qy2ex0m.js → index-5mtc969c.js} +2 -2
  34. package/dist/index-5s3zajp8.js +192 -0
  35. package/dist/{index-3cwck0rm.js → index-6s7n2v50.js} +60 -374
  36. package/dist/{index-s4c8wy8m.js → index-758zx2ha.js} +11 -90
  37. package/dist/{index-qyrqwr4c.js → index-77fekveh.js} +4 -1
  38. package/dist/{index-58jzmnn4.js → index-7ag81v2f.js} +355 -34
  39. package/dist/index-7gnkny4z.js +326 -0
  40. package/dist/index-7zbps32p.js +89 -0
  41. package/dist/index-85vfqd7m.js +135 -0
  42. package/dist/{index-m668wzyc.js → index-9s6z8vgf.js} +17 -141
  43. package/dist/{index-bfcpjw20.js → index-actkayzn.js} +7 -3
  44. package/dist/index-b9aefrqd.js +20 -0
  45. package/dist/index-dre2ywck.js +48 -0
  46. package/dist/{index-nemjkxjp.js → index-dwf1f2ap.js} +16 -12
  47. package/dist/{index-wwst0td5.js → index-dx9xehwt.js} +1 -1
  48. package/dist/{index-ezmn6ac6.js → index-dxesg10d.js} +4 -4
  49. package/dist/{index-aczggrty.js → index-dxjjv399.js} +2 -2
  50. package/dist/{index-jqtsc9mj.js → index-egaabxkz.js} +3 -3
  51. package/dist/{index-vc1b0b1b.js → index-evcnmsc3.js} +49 -8
  52. package/dist/{index-nt1mp8km.js → index-fxz0v7vg.js} +84 -84
  53. package/dist/index-h4pj6fta.js +87 -0
  54. package/dist/{index-1rxswfbv.js → index-hb5pf3s4.js} +1 -1
  55. package/dist/{index-t8qyvrvg.js → index-k9ay6v6d.js} +21 -272
  56. package/dist/index-kzfs85xp.js +9 -0
  57. package/dist/{index-vbf2p6me.js → index-m9kzax52.js} +91 -89
  58. package/dist/{index-22by16v6.js → index-mgtxfe6b.js} +1 -1
  59. package/dist/index-p7hgdyhb.js +22 -0
  60. package/dist/index-pq95qkbf.js +0 -0
  61. package/dist/index-t0z5p5w6.js +318 -0
  62. package/dist/{index-s2rchahr.js → index-vwp69871.js} +13 -11
  63. package/dist/index-wc4x00bn.js +103 -0
  64. package/dist/{index-da1aqnhb.js → index-xcetq6j3.js} +8 -6
  65. package/dist/{index-f6n5n7nz.js → index-y01f4223.js} +33 -126
  66. package/dist/{index-35z5h2ty.js → index-ywd0y6m1.js} +18 -218
  67. package/dist/index.js +65 -1588
  68. package/dist/internal/deadline.d.ts +17 -0
  69. package/dist/internal/deadline.d.ts.map +1 -0
  70. package/dist/live.js +15 -89
  71. package/dist/node.js +17 -15
  72. package/dist/observability/context.d.ts.map +1 -1
  73. package/dist/observability/index.js +7 -6
  74. package/dist/primitives.js +5 -2
  75. package/dist/react.js +1 -1
  76. package/dist/remote.js +7 -6
  77. package/dist/server/event-bus.d.ts.map +1 -1
  78. package/dist/server/index.js +26 -38
  79. package/dist/telegram.js +1 -1
  80. package/dist/testing.js +86 -97
  81. package/dist/tool-invoker.js +11 -9
  82. package/dist/tools/async-operation-contract.d.ts +61 -0
  83. package/dist/tools/async-operation-contract.d.ts.map +1 -0
  84. package/dist/tools/async-operation.d.ts +2 -47
  85. package/dist/tools/async-operation.d.ts.map +1 -1
  86. package/dist/tools/view-file-contract.d.ts +98 -0
  87. package/dist/tools/view-file-contract.d.ts.map +1 -0
  88. package/dist/tools/view-file.d.ts +4 -83
  89. package/dist/tools/view-file.d.ts.map +1 -1
  90. package/dist/tools-contract.d.ts +29 -0
  91. package/dist/tools-contract.d.ts.map +1 -0
  92. package/dist/tools-contract.js +24 -0
  93. package/dist/tools.d.ts +3 -3
  94. package/dist/tools.d.ts.map +1 -1
  95. package/dist/tools.js +59 -95
  96. package/llms-full.txt +146 -14
  97. package/package.json +14 -7
  98. package/dist/index-ksp6e2ye.js +0 -24
  99. package/dist/index-vj3vvpaa.js +0 -307
package/llms-full.txt CHANGED
@@ -53,6 +53,7 @@ own, recorded as an ADR.
53
53
  | `stitchkit/server` | server (Bun) | stable | `createServer`, `implement`, hooks, auth, Socket.IO server, server primitives |
54
54
  | `stitchkit/node` | server (Node ≥ 22) | stable | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
55
55
  | `stitchkit/tools` | server | stable | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
56
+ | `stitchkit/tools/contract` | browser + server | evolving | the shapes a tool surface speaks — async-operation contract, snapshot and cancel schemas, view-file input/output — without the runtime that serves them |
56
57
  | `stitchkit/tools/invoker` | server | stable | peer-free `createToolInvoker` over the canonical contract tool runner |
57
58
  | `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
58
59
  | `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
@@ -60,9 +61,9 @@ own, recorded as an ADR.
60
61
  | `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification and Bot API send-failure classification |
61
62
  | `stitchkit/observability` | server | stable | request/tool event projections — `createObservability`, trace context, sanitisation |
62
63
  | `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
- | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
64
+ | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
64
65
  | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
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
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 24 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
66
67
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
67
68
  | `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
69
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
@@ -70,10 +71,11 @@ own, recorded as an ADR.
70
71
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
71
72
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
72
73
  | `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 22 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
74
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 6 of the 24 minors since 0.56.2, most recently 0.79.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
74
75
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
75
76
  | `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 |
77
+ | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
78
+ | `stitchkit/application/diagnostic-journal` | server | evolving | the local diagnostic journal — the one part of the kernel that spawns, locks and writes files |
77
79
 
78
80
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
79
81
  code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
@@ -5509,9 +5511,26 @@ await app.restart({ resourceId: 'cache' })
5509
5511
  **Refused is not failed.** An unknown id, a restart during shutdown, and a
5510
5512
  restart before the application is ready all return `refused` with a reason and
5511
5513
  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.
5514
+ not come up — and the result agrees with the snapshot: if any affected resource
5515
+ ends the restart in `failed`, so does the restart, including an optional one
5516
+ whose failure the start loop does not re-throw.
5517
+
5518
+ **The close phase is bounded**, by the application's shutdown budget or by one
5519
+ this call names:
5520
+
5521
+ ```ts
5522
+ await app.restart({ resourceId: 'database', gracePeriodMs: 5_000, forceTimeoutMs: 2_000 })
5523
+ ```
5524
+
5525
+ **A resource has to be able to start twice.** The kernel calls `start` again, so
5526
+ a resource holding state across its own lifetime rebuilds it there. The ones this
5527
+ framework ships do — a schedule re-arms, a keyspace opens a new generation and
5528
+ re-loads, a managed server built from a factory gets a fresh server. A managed
5529
+ server given a server *instance* cannot, and says so by name rather than
5530
+ republishing something it has shut down. Give a keyspace a
5531
+ `backend: () => …` factory when its backend cannot be re-opened after `close()`.
5532
+
5533
+ → [ADR 0157](../decisions/0157-a-restartable-resource-begins-a-generation.md).
5515
5534
 
5516
5535
  Restarts of overlapping subtrees queue behind each other rather than being
5517
5536
  refused. Two callers asking at once is ordinary; two generations of one resource
@@ -5547,7 +5566,7 @@ const pipeline = createDecisionPipeline<{ userId: string; scope: string }>([
5547
5566
  ? { outcome: 'allow' }
5548
5567
  : { outcome: 'defer' }) },
5549
5568
  { id: 'default', decide: () => ({ outcome: 'deny', reason: 'no policy allowed this' }) },
5550
- ])
5569
+ ], { policyTimeoutMs: 2_000 })
5551
5570
 
5552
5571
  const result = await pipeline.decide(request)
5553
5572
  result.outcome // 'allow' | 'deny' — never 'defer'
@@ -5568,8 +5587,19 @@ answer that wrongly while looking complete.
5568
5587
  **Every policy deferring raises.** `DecisionUndecidedError`, not a default —
5569
5588
  defaulting to `allow` turns an incomplete policy set into an open door, and
5570
5589
  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.
5590
+ refusal.
5591
+
5592
+ **A policy that does not answer raises too.** A non-decision, a throw, and running
5593
+ past `policyTimeoutMs` are one error — `DecisionPolicyError`, naming the policy
5594
+ and carrying the trace so far — because they are indistinguishable downstream and
5595
+ handling one but not the others is how a broken policy becomes a skipped one. The
5596
+ chain stops there rather than falling through to whatever the next policy would
5597
+ have said.
5598
+
5599
+ `policyTimeoutMs` is required and has no default. Pick a number your slowest
5600
+ policy comfortably beats. The framework will not choose it for you: a policy that
5601
+ never settles hangs every caller of the operation it guards, and a default here
5602
+ is a number nobody chose applied to code the framework has never seen.
5573
5603
 
5574
5604
  The same `allow`/`deny`/`defer` type is what an event topic declared
5575
5605
  `mode: 'decision'` uses in `stitchkit/live` — one vocabulary, because a listener
@@ -5671,8 +5701,14 @@ changing its revision or status contract. → ADR 0119.
5671
5701
  Use `createDiagnosticJournal` when a process needs finite, ordered local metadata evidence and the
5672
5702
  deployment log pipeline is not the right boundary:
5673
5703
 
5704
+ It lives at `stitchkit/application/diagnostic-journal`, not in the main barrel:
5705
+ it is the one part of the kernel that spawns, locks and writes files, and while it
5706
+ was exported from `stitchkit/application` that single line made the whole
5707
+ entrypoint unusable in a browser bundle. Its schemas stay in
5708
+ `stitchkit/application`.
5709
+
5674
5710
  ```ts
5675
- import { createDiagnosticJournal } from 'stitchkit/application'
5711
+ import { createDiagnosticJournal } from 'stitchkit/application/diagnostic-journal'
5676
5712
  import { z } from 'zod'
5677
5713
 
5678
5714
  const journal = await createDiagnosticJournal({
@@ -10101,6 +10137,84 @@ makes one thing your job rather than the resolver's:
10101
10137
  The mechanical part is identical either way. Only the *noticing* differs, and an
10102
10138
  exact pin moves it onto you.
10103
10139
 
10140
+ ## Released migration: 0.79.0
10141
+
10142
+ One thing, and only if you assert on the whole snapshot.
10143
+
10144
+ ```bash
10145
+ rg -n "getSnapshot\(\)|projectApplicationStatus|status\(\)" --glob '*.test.ts' --glob '*.spec.ts'
10146
+ ```
10147
+
10148
+ `ApplicationSnapshot` and `ApplicationStatusProjection` each gained a
10149
+ `restarting` field — ids in the snapshot, a count in the projection. A reader
10150
+ that names the fields it wants is unaffected. A test that compares the whole
10151
+ object with `toEqual` fails until it adds the field:
10152
+
10153
+ ```ts
10154
+ // before
10155
+ expect(body).toEqual({ id, lifecycle: 'ready', /* … */ resources: { total: 1, ready: 1, degraded: 0, failed: 0 } })
10156
+ // after
10157
+ expect(body).toEqual({ id, lifecycle: 'ready', /* … */ resources: { total: 1, ready: 1, degraded: 0, failed: 0 }, restarting: 0 })
10158
+ ```
10159
+
10160
+ It is zero except while a restart is running, and then it names the subtree being
10161
+ replaced. That is the point: a snapshot taken mid-restart used to be
10162
+ indistinguishable from a resource that had failed on its own, so a dashboard read
10163
+ a planned replacement as an outage.
10164
+
10165
+ **If you watch a hub across a restart**, nothing to change, but the behaviour is
10166
+ different and better: closing a hub now tells its subscribers `unavailable` /
10167
+ `source-unavailable` instead of dropping them, so a page stops showing a value
10168
+ that will never update again.
10169
+
10170
+ **If you need a tool contract in a browser**, `stitchkit/tools/contract` now
10171
+ carries the async-operation helpers, its schemas and the view-file input/output
10172
+ without the runtime. `stitchkit/tools` still exports all of them.
10173
+
10174
+ ## Released migration: 0.78.0
10175
+
10176
+ Two mechanical edits. Find them both:
10177
+
10178
+ ```bash
10179
+ rg -n "createDecisionPipeline|createDiagnosticJournal" --glob '*.ts' --glob '*.tsx'
10180
+ ```
10181
+
10182
+ **1. `createDecisionPipeline` takes a deadline.**
10183
+
10184
+ ```ts
10185
+ // before
10186
+ createDecisionPipeline([policyA, policyB])
10187
+ // after
10188
+ createDecisionPipeline([policyA, policyB], { policyTimeoutMs: 2_000 })
10189
+ ```
10190
+
10191
+ Pick a number your slowest policy comfortably beats. There is no default on
10192
+ purpose: a policy that never settles hangs every caller of the operation it
10193
+ guards, and the framework has no basis for choosing that number for you.
10194
+
10195
+ Past the deadline — and on a throw, and on a non-decision — the pipeline raises
10196
+ `DecisionPolicyError` naming the policy, with the trace so far. If you already
10197
+ catch that for a bad return value, you now catch two more cases with it.
10198
+
10199
+ **2. `createDiagnosticJournal` moved.**
10200
+
10201
+ ```ts
10202
+ // before
10203
+ import { createDiagnosticJournal } from 'stitchkit/application'
10204
+ // after
10205
+ import { createDiagnosticJournal } from 'stitchkit/application/diagnostic-journal'
10206
+ ```
10207
+
10208
+ Only the factory moved. `DiagnosticJournalConfig`, every
10209
+ `DiagnosticJournal*Schema` and `readDiagnosticJournalLockDiagnosis` stay where
10210
+ they were.
10211
+
10212
+ **If you restart resources**, nothing to change — but three things that did not
10213
+ work now do: a managed schedule can be restarted, a restarted keyspace accepts
10214
+ writes again, and a restarted managed server is actually shut down at exit. If
10215
+ you pass `managedServerResource` a server *instance*, a restart is now refused
10216
+ by name; pass a factory (`server: (context) => …`) to restart it.
10217
+
10104
10218
  ## Released migration: 0.77.0
10105
10219
 
10106
10220
  Two type renames, and only if you named them. Nothing runtime moved.
@@ -13570,7 +13684,7 @@ cutovers are covered by the executable
13570
13684
  |--------|------|---------|
13571
13685
  | `createApplication` | function | compose a validated resource DAG into one startup, readiness, admission and shutdown state machine |
13572
13686
  | `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 |
13687
+ | `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id, and optionally `gracePeriodMs` / `forceTimeoutMs` for this restart — the application's own shutdown budget otherwise |
13574
13688
  | `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
13689
  | `ApplicationRestartOutcomeSchema` / `ApplicationRestartOutcome` | schema / _type_ | `restarted`, `failed` or `refused` — a refusal (unknown id, shutting down, not yet ready) is not a failure and touches nothing |
13576
13690
  | `ApplicationResourceFailure` | _type_ | one resource failure with the cause its phase label cannot carry — delivered to `onResourceFailure` |
@@ -13721,11 +13835,12 @@ vocabulary with an event topic declared `mode: 'decision'`.
13721
13835
  | Export | Kind | Summary |
13722
13836
  |--------|------|---------|
13723
13837
  | `createDecisionPipeline` | function | run policies in declaration order; the first terminal verdict wins and the rest do not run |
13838
+ | `DecisionPipelineConfig` | _type_ | `policyTimeoutMs` — how long one policy has to answer. Required, no default: a policy that never settles hangs every caller of the operation it guards |
13724
13839
  | `PolicyDecisionSchema` / `PolicyDecision` | schema / _type_ | `allow`, `deny` with a reason, or `defer` — a deny cannot be silent |
13725
13840
  | `DecisionPolicy` / `DecisionPipeline` | _types_ | one named voter over the caller's subject, and the pipeline it composes into |
13726
13841
  | `DecisionResult` / `DecisionTraceEntry` | _types_ | the verdict, and the trace of what actually ran — not what was declared |
13727
13842
  | `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 |
13843
+ | `DecisionPolicyError` | class | a policy returned a non-decision, threw, or ran past `policyTimeoutMs` one error for all three, carrying the policy id and the trace so far |
13729
13844
 
13730
13845
  ### Keyspace and watched reads
13731
13846
 
@@ -13733,7 +13848,7 @@ vocabulary with an event topic declared `mode: 'decision'`.
13733
13848
  |--------|------|---------|
13734
13849
  | `defineKeyspace` / `keyspaceResource` | function | a named record set read synchronously from memory and written through one serialised chain; backend first, then memory, then the change event |
13735
13850
  | `openKeyspace` / `OpenedKeyspace` | function / type | the same keyspace opened directly, for an application that owns its own lifecycle rather than declaring resources to a kernel |
13736
- | `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle |
13851
+ | `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle. `backend` takes a value or a **factory** — a factory is called once per generation, so a restarted keyspace gets a backend that was never closed |
13737
13852
  | `memoryKeyspaceBackend` / `sqliteKeyspaceBackend` / `SqliteKeyspaceBackendConfig` | function / type | a disposable in-process backend, and one table with a key and a JSON payload over a caller-owned database |
13738
13853
  | `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _types_ | the minimal synchronous SQLite boundary the framework types against |
13739
13854
  | `createWatchHub` / `watchKey` | function | one read per question however many are asking: single-flight per key, re-read on a declared topic, publish only what changed |
@@ -13770,6 +13885,23 @@ snapshots; the adapter owns no SDK lifecycle, polling or delta state.
13770
13885
  | `ApplicationTelemetryMeter` | _type_ | minimal structural `Meter.createObservableGauge` boundary compatible with `@opentelemetry/api` |
13771
13886
  | `ApplicationOpenTelemetryCollectionError` | _type_ | isolated instrument-name/error diagnostic without product/provider attributes |
13772
13887
 
13888
+ ## `stitchkit/application/diagnostic-journal`
13889
+
13890
+ | Export | Kind | Summary |
13891
+ |--------|------|---------|
13892
+ | `createDiagnosticJournal` | function | the bounded local journal: a file lock, a spawn to diagnose a stale one, and framed writes |
13893
+
13894
+ Its own entrypoint because of what it reaches — `node:child_process`,
13895
+ `node:fs`, `node:os`, `node:util` — with `promisify(execFile)` evaluated while
13896
+ the module initialises. Exported from `stitchkit/application`, that one line made
13897
+ the entire barrel unusable in a browser bundle: not by failing at the call, but
13898
+ by throwing during module initialisation, on every route.
13899
+
13900
+ Its **contract** stays in `stitchkit/application` — every
13901
+ `DiagnosticJournal*Schema`, the states and the refusal reasons touch nothing but
13902
+ Zod, and a client reading a journal's status has as much right to them as the
13903
+ server writing it.
13904
+
13773
13905
  ## `stitchkit/application/schemas`
13774
13906
 
13775
13907
  The canonical application records — and nothing else. Every export here is also
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.77.0",
3
+ "version": "0.79.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",
@@ -61,6 +61,10 @@
61
61
  "types": "./dist/tools.d.ts",
62
62
  "import": "./dist/tools.js"
63
63
  },
64
+ "./tools/contract": {
65
+ "types": "./dist/tools-contract.d.ts",
66
+ "import": "./dist/tools-contract.js"
67
+ },
64
68
  "./tools/invoker": {
65
69
  "types": "./dist/tool-invoker.d.ts",
66
70
  "import": "./dist/tool-invoker.js"
@@ -129,6 +133,10 @@
129
133
  "types": "./dist/application-opentelemetry.d.ts",
130
134
  "import": "./dist/application-opentelemetry.js"
131
135
  },
136
+ "./application/diagnostic-journal": {
137
+ "types": "./dist/application-diagnostic-journal.d.ts",
138
+ "import": "./dist/application-diagnostic-journal.js"
139
+ },
132
140
  "./application/schemas": {
133
141
  "types": "./dist/application-schemas.d.ts",
134
142
  "import": "./dist/application-schemas.js"
@@ -160,18 +168,17 @@
160
168
  "scripts": {
161
169
  "build:native-contained-files": "node scripts/build-contained-files-native.mjs",
162
170
  "check": "bun x tsc --noEmit",
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",
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",
165
- "build:js": "bun run build:browser && bun run build:server && bun scripts/preserve-webpack-ignore.mjs",
171
+ "build:js": "bun run build:entrypoints && bun scripts/preserve-webpack-ignore.mjs",
166
172
  "build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly && bun scripts/rewrite-declaration-specifiers.mjs",
167
- "build": "rm -rf dist && bun run build:js && bun run build:types && bun scripts/check-browser-clean.mjs && bun scripts/check-env-live.mjs && bun scripts/check-public-types.mjs",
168
- "dev": "bun run build:js -- --watch",
173
+ "build": "rm -rf dist && bun run build:js && bun run build:types && bun scripts/check-browser-clean.mjs && bun scripts/check-browser-executes.mjs && bun scripts/check-env-live.mjs && bun scripts/check-public-types.mjs",
174
+ "dev": "bun run build:entrypoints -- --watch",
169
175
  "prepack": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
170
176
  "test": "bun test",
171
177
  "smoke:node": "node scripts/node-smoke.mjs",
172
178
  "smoke:next-ssr": "node scripts/next-ssr-retry-smoke.mjs",
173
179
  "consumer-lane": "bun scripts/consumer-lane/run.mjs",
174
- "bench:mcp-preparation": "bun scripts/benchmark-mcp-preparation.ts"
180
+ "bench:mcp-preparation": "bun scripts/benchmark-mcp-preparation.ts",
181
+ "build:entrypoints": "bun scripts/build-entrypoints.mjs"
175
182
  },
176
183
  "peerDependencies": {
177
184
  "@modelcontextprotocol/ext-apps": "^1.7.2",
@@ -1,24 +0,0 @@
1
- // src/internal/typed.ts
2
- function typedEntries(value) {
3
- return Object.entries(value);
4
- }
5
- function isRecord(value) {
6
- return typeof value === "object" && value !== null && !Array.isArray(value);
7
- }
8
- function transportResult(value) {
9
- return value;
10
- }
11
- function mapObject(source, mapper) {
12
- const result = {};
13
- for (const [key, value] of typedEntries(source)) {
14
- const mapped = mapper(key, value);
15
- if (mapped !== undefined)
16
- result[key] = mapped;
17
- }
18
- return result;
19
- }
20
- function mapObjectTypeBoundary(source, mapper) {
21
- return mapObject(source, (key, value) => mapper(key, value));
22
- }
23
-
24
- export { typedEntries, isRecord, transportResult, mapObject, mapObjectTypeBoundary };
@@ -1,307 +0,0 @@
1
- // src/contract/define.ts
2
- import { z } from "zod";
3
-
4
- // src/internal/route-pattern.ts
5
- var PARAM_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
- function joinRoutePath(...parts) {
7
- const joined = parts.filter((part) => Boolean(part)).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
8
- return `/${joined}`;
9
- }
10
- function parseTrailingWildcard(path) {
11
- const segments = path.split("/").filter(Boolean);
12
- const paramNames = new Set;
13
- let wildcard = null;
14
- for (const [segmentIndex, segment] of segments.entries()) {
15
- if (segment.startsWith(":")) {
16
- const name2 = segment.slice(1);
17
- if (!PARAM_IDENTIFIER.test(name2)) {
18
- throw new Error(`Invalid route parameter name "${name2}" in path "${path}"`);
19
- }
20
- if (paramNames.has(name2)) {
21
- throw new Error(`Duplicate route parameter name "${name2}" in path "${path}"`);
22
- }
23
- paramNames.add(name2);
24
- continue;
25
- }
26
- if (!segment.startsWith("*")) {
27
- if (segment.includes("*")) {
28
- throw new Error(`Wildcard must occupy its own segment in path "${path}"`);
29
- }
30
- continue;
31
- }
32
- const name = segment.slice(1);
33
- if (!PARAM_IDENTIFIER.test(name)) {
34
- throw new Error(`Trailing wildcard in path "${path}" must be named, for example "/*filePath"`);
35
- }
36
- if (segmentIndex !== segments.length - 1) {
37
- throw new Error(`Wildcard "*${name}" must be the final segment in path "${path}"`);
38
- }
39
- if (wildcard) {
40
- throw new Error(`Path "${path}" contains more than one wildcard`);
41
- }
42
- if (paramNames.has(name)) {
43
- throw new Error(`Duplicate route parameter name "${name}" in path "${path}"`);
44
- }
45
- wildcard = { name, segmentIndex };
46
- }
47
- return wildcard;
48
- }
49
-
50
- // src/internal/safe-json.ts
51
- function isUnsafeKey(key) {
52
- return key === "__proto__";
53
- }
54
- function safeJsonParse(text) {
55
- return JSON.parse(text, (key, value) => isUnsafeKey(key) ? undefined : value);
56
- }
57
-
58
- // src/contract/define.ts
59
- function defineContract(meta, endpoints) {
60
- const toolTransports = new Map;
61
- for (const [key, ep] of Object.entries(endpoints)) {
62
- const wildcard = parseTrailingWildcard(ep.path);
63
- if (wildcard) {
64
- if (!ep.params) {
65
- throw new Error(`Contract "${meta.prefix}": endpoint "${key}" wildcard "${wildcard.name}" requires a params schema field`);
66
- }
67
- const paramsJson = z.toJSONSchema(ep.params, { io: "input" });
68
- if (!paramsJson.properties || !(wildcard.name in paramsJson.properties)) {
69
- throw new Error(`Contract "${meta.prefix}": endpoint "${key}" params schema is missing wildcard field "${wildcard.name}"`);
70
- }
71
- }
72
- if (ep.desc.trim() === "") {
73
- throw new Error(`Contract "${meta.prefix}": endpoint "${key}" has an empty desc`);
74
- }
75
- if (ep.maxJsonBodyBytes !== undefined && (!Number.isSafeInteger(ep.maxJsonBodyBytes) || ep.maxJsonBodyBytes <= 0)) {
76
- throw new Error(`Contract "${meta.prefix}": endpoint "${key}" maxJsonBodyBytes must be a positive safe integer, received ${ep.maxJsonBodyBytes}`);
77
- }
78
- if (ep.multipart)
79
- assertMultipartEndpoint(meta.prefix, key, ep);
80
- if (ep.rawResponse)
81
- assertRawEndpoint(meta.prefix, key, ep);
82
- if ("stream" in ep)
83
- assertStreamingResponseEndpoint(meta.prefix, key, ep);
84
- if (ep.method === "HEAD")
85
- assertHeadEndpoint(meta.prefix, key, ep);
86
- if (ep.rawBody)
87
- assertRawBodyEndpoint(meta.prefix, key, ep);
88
- if ("responseMeta" in ep)
89
- assertResponseMetaEndpoint(meta.prefix, key, ep);
90
- if (!("toolName" in ep) || !ep.toolName)
91
- continue;
92
- const transports = new Set(ep.expose ? ep.expose.filter((t) => t !== "HTTP") : ["MCP", "AGENT"]);
93
- if (transports.size === 0) {
94
- throw new Error(`Contract "${meta.prefix}": endpoint "${key}" sets toolName "${ep.toolName}" but is not exposed on any tool transport (MCP / AGENT)`);
95
- }
96
- const existing = toolTransports.get(ep.toolName);
97
- if (existing) {
98
- for (const t of transports) {
99
- if (existing.transports.has(t)) {
100
- throw new Error(`Contract "${meta.prefix}": duplicate toolName "${ep.toolName}" on transport "${t}" (endpoints: "${existing.key}" and "${key}")`);
101
- }
102
- existing.transports.add(t);
103
- }
104
- } else {
105
- toolTransports.set(ep.toolName, { key, transports });
106
- }
107
- }
108
- return { meta, endpoints };
109
- }
110
- function assertPositiveLimit(where, name, value) {
111
- if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
112
- throw new Error(`${where} ${name} must be a positive safe integer, received ${value}`);
113
- }
114
- }
115
- function assertMultipartEndpoint(prefix, key, ep) {
116
- const where = `Contract "${prefix}": multipart endpoint "${key}"`;
117
- if (ep.method !== "POST" && ep.method !== "PUT" && ep.method !== "PATCH") {
118
- throw new Error(`${where} must use POST, PUT or PATCH`);
119
- }
120
- const multipart = ep.multipart;
121
- if (!multipart || typeof multipart !== "object") {
122
- throw new Error(`${where} must declare a multipart descriptor`);
123
- }
124
- assertPositiveLimit(where, "maxRequestBytes", multipart.maxRequestBytes);
125
- assertPositiveLimit(where, "maxFieldBytes", multipart.maxFieldBytes);
126
- const entries = Object.entries(multipart.files);
127
- if (entries.length === 0)
128
- throw new Error(`${where} must declare at least one file field`);
129
- for (const [field, policy] of entries) {
130
- if (!field || isUnsafeKey(field))
131
- throw new Error(`${where} has an invalid file field name`);
132
- assertPositiveLimit(`${where} field "${field}"`, "maxBytes", policy.maxBytes);
133
- assertPositiveLimit(`${where} field "${field}"`, "maxFiles", policy.maxFiles);
134
- if (policy.multiple !== true && policy.maxFiles !== undefined) {
135
- throw new Error(`${where} field "${field}" may set maxFiles only with multiple: true`);
136
- }
137
- if (policy.contentTypes) {
138
- if (policy.contentTypes.length === 0) {
139
- throw new Error(`${where} field "${field}" contentTypes cannot be empty`);
140
- }
141
- for (const contentType of policy.contentTypes) {
142
- if (!/^[a-z0-9!#$&^_.+-]+\/(?:[a-z0-9!#$&^_.+-]+|\*)$/i.test(contentType)) {
143
- throw new Error(`${where} field "${field}" has invalid content type policy "${contentType}"`);
144
- }
145
- }
146
- }
147
- }
148
- }
149
- function assertRawEndpoint(prefix, key, ep) {
150
- const where = `Contract "${prefix}": raw endpoint "${key}"`;
151
- if (ep.output)
152
- throw new Error(`${where} cannot declare an output schema`);
153
- if ("toolName" in ep && ep.toolName)
154
- throw new Error(`${where} cannot set a toolName`);
155
- if ("ui" in ep && ep.ui)
156
- throw new Error(`${where} cannot set MCP ui metadata`);
157
- if ("annotations" in ep && ep.annotations) {
158
- throw new Error(`${where} cannot set MCP annotations`);
159
- }
160
- const nonHttp = (ep.expose ?? []).filter((t) => t !== "HTTP");
161
- if (nonHttp.length > 0) {
162
- throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
163
- }
164
- }
165
- function assertStreamingResponseEndpoint(prefix, key, ep) {
166
- const where = `Contract "${prefix}": streaming endpoint "${key}"`;
167
- if (!("stream" in ep) || !ep.stream || typeof ep.stream !== "object") {
168
- throw new Error(`${where} must declare a stream descriptor`);
169
- }
170
- if (!ep.stream.item || typeof ep.stream.item.parse !== "function") {
171
- throw new Error(`${where} must declare an item schema`);
172
- }
173
- const runtimeFormat = Reflect.get(ep.stream, "format");
174
- const runtimeFinalLine = Reflect.get(ep.stream, "finalLine");
175
- if (ep.stream.framing === "item" && runtimeFormat === "sse") {
176
- throw new Error(`${where} item framing is supported only for ndjson`);
177
- }
178
- if (ep.stream.framing === "item" && ep.stream.completion !== "terminal") {
179
- throw new Error(`${where} item framing requires terminal completion`);
180
- }
181
- if (ep.stream.completion === "terminal" && !ep.stream.terminal) {
182
- throw new Error(`${where} terminal completion requires a terminal schema`);
183
- }
184
- if (runtimeFinalLine === "require-newline" && runtimeFormat === "sse") {
185
- throw new Error(`${where} finalLine applies only to ndjson`);
186
- }
187
- assertPositiveLimit(where, "maxFrameBytes", ep.stream.maxFrameBytes);
188
- assertPositiveLimit(where, "lifetimeMs", ep.stream.lifetimeMs);
189
- assertPositiveLimit(where, "heartbeatMs", ep.stream.heartbeatMs);
190
- if (ep.stream.idleTimeoutSeconds !== undefined && (!Number.isSafeInteger(ep.stream.idleTimeoutSeconds) || ep.stream.idleTimeoutSeconds < 0)) {
191
- throw new Error(`${where} idleTimeoutSeconds must be a non-negative safe integer`);
192
- }
193
- if (ep.output)
194
- throw new Error(`${where} cannot declare an output schema`);
195
- if (ep.rawResponse)
196
- throw new Error(`${where} cannot also be rawResponse`);
197
- if (ep.multipart)
198
- throw new Error(`${where} cannot be multipart`);
199
- if ("toolName" in ep && ep.toolName)
200
- throw new Error(`${where} cannot set a toolName`);
201
- const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
202
- if (nonHttp.length > 0) {
203
- throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
204
- }
205
- }
206
- function assertHeadEndpoint(prefix, key, ep) {
207
- const where = `Contract "${prefix}": HEAD endpoint "${key}"`;
208
- if (!ep.rawResponse)
209
- throw new Error(`${where} must declare rawResponse: true`);
210
- if (ep.input)
211
- throw new Error(`${where} cannot declare an input schema`);
212
- if (ep.multipart)
213
- throw new Error(`${where} cannot be multipart`);
214
- if (ep.rawBody)
215
- throw new Error(`${where} cannot retain a raw body`);
216
- }
217
- function assertRawBodyEndpoint(prefix, key, ep) {
218
- const where = `Contract "${prefix}": rawBody endpoint "${key}"`;
219
- if (!ep.input)
220
- throw new Error(`${where} must declare an input schema`);
221
- if (ep.multipart)
222
- throw new Error(`${where} cannot be multipart`);
223
- if (ep.method !== "POST" && ep.method !== "PUT" && ep.method !== "PATCH") {
224
- throw new Error(`${where} must use POST, PUT or PATCH`);
225
- }
226
- if ("toolName" in ep && ep.toolName)
227
- throw new Error(`${where} cannot set a toolName`);
228
- if ("ui" in ep && ep.ui)
229
- throw new Error(`${where} cannot set MCP ui metadata`);
230
- if ("annotations" in ep && ep.annotations) {
231
- throw new Error(`${where} cannot set MCP annotations`);
232
- }
233
- const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
234
- if (nonHttp.length > 0) {
235
- throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
236
- }
237
- }
238
- function assertResponseMetaEndpoint(prefix, key, ep) {
239
- const where = `Contract "${prefix}": responseMeta endpoint "${key}"`;
240
- if (!ep.responseMeta || typeof ep.responseMeta !== "object") {
241
- throw new Error(`${where} must declare responseMeta as an object`);
242
- }
243
- const status = ep.responseMeta.status;
244
- if (status !== undefined && (!Number.isSafeInteger(status) || status < 200 || status > 299)) {
245
- throw new Error(`${where} status must be a successful 2xx integer, received ${status}`);
246
- }
247
- if (ep.output && (status === 204 || status === 205)) {
248
- throw new Error(`${where} cannot combine output with bodyless status ${status}`);
249
- }
250
- if (ep.rawResponse)
251
- throw new Error(`${where} cannot also be a rawResponse endpoint`);
252
- if ("toolName" in ep && ep.toolName)
253
- throw new Error(`${where} cannot set a toolName`);
254
- if ("ui" in ep && ep.ui)
255
- throw new Error(`${where} cannot set MCP ui metadata`);
256
- if ("annotations" in ep && ep.annotations) {
257
- throw new Error(`${where} cannot set MCP annotations`);
258
- }
259
- const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
260
- if (nonHttp.length > 0) {
261
- throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
262
- }
263
- }
264
- function mergeMeta(contractMeta, endpointMeta) {
265
- if (!contractMeta)
266
- return endpointMeta;
267
- if (!endpointMeta)
268
- return { ...contractMeta };
269
- return { ...contractMeta, ...endpointMeta };
270
- }
271
- // src/contract/errors-factory.ts
272
- import { z as z2 } from "zod";
273
- // src/contract/pagination.ts
274
- import { z as z3 } from "zod";
275
-
276
- // src/internal/base64url.ts
277
- function bytesToBase64Url(bytes) {
278
- let binary = "";
279
- for (const byte of bytes)
280
- binary += String.fromCharCode(byte);
281
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
282
- }
283
- function base64UrlToBytes(segment) {
284
- if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) {
285
- throw new Error("invalid base64url segment");
286
- }
287
- const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
288
- const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
289
- return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
290
- }
291
- // src/contract/stream.ts
292
- import { z as z4 } from "zod";
293
- var DEFAULT_CONTRACT_STREAM_FRAME_BYTES = 256 * 1024;
294
- var ContractStreamFrameSchema = z4.discriminatedUnion("type", [
295
- z4.object({ type: z4.literal("data"), data: z4.unknown() }).strict(),
296
- z4.object({
297
- type: z4.literal("error"),
298
- error: z4.object({
299
- code: z4.string(),
300
- message: z4.string().optional(),
301
- details: z4.unknown().optional(),
302
- hint: z4.string().optional()
303
- }).strict()
304
- }).strict(),
305
- z4.object({ type: z4.literal("end") }).strict()
306
- ]);
307
- export { joinRoutePath, parseTrailingWildcard, isUnsafeKey, safeJsonParse, defineContract, mergeMeta, bytesToBase64Url, base64UrlToBytes, DEFAULT_CONTRACT_STREAM_FRAME_BYTES, ContractStreamFrameSchema };