stitchkit 0.56.0 → 0.56.2

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 (73) hide show
  1. package/README.md +12 -3
  2. package/dist/agent-runtime/compaction.d.ts +33 -0
  3. package/dist/agent-runtime/compaction.d.ts.map +1 -0
  4. package/dist/agent-runtime/coordinator.d.ts +24 -0
  5. package/dist/agent-runtime/coordinator.d.ts.map +1 -0
  6. package/dist/agent-runtime/events.d.ts +454 -0
  7. package/dist/agent-runtime/events.d.ts.map +1 -0
  8. package/dist/agent-runtime/history.d.ts +11 -0
  9. package/dist/agent-runtime/history.d.ts.map +1 -0
  10. package/dist/agent-runtime/managed-tools.d.ts +18 -0
  11. package/dist/agent-runtime/managed-tools.d.ts.map +1 -0
  12. package/dist/agent-runtime/models.d.ts +51 -0
  13. package/dist/agent-runtime/models.d.ts.map +1 -0
  14. package/dist/agent-runtime/observability.d.ts +134 -0
  15. package/dist/agent-runtime/observability.d.ts.map +1 -0
  16. package/dist/agent-runtime/prompt.d.ts +50 -0
  17. package/dist/agent-runtime/prompt.d.ts.map +1 -0
  18. package/dist/agent-runtime/protocol.d.ts +16 -0
  19. package/dist/agent-runtime/protocol.d.ts.map +1 -0
  20. package/dist/agent-runtime/runtime.d.ts +120 -0
  21. package/dist/agent-runtime/runtime.d.ts.map +1 -0
  22. package/dist/agent-runtime/schemas.d.ts +470 -0
  23. package/dist/agent-runtime/schemas.d.ts.map +1 -0
  24. package/dist/agent-runtime/store.d.ts +905 -0
  25. package/dist/agent-runtime/store.d.ts.map +1 -0
  26. package/dist/agent-runtime/testing.d.ts +17 -0
  27. package/dist/agent-runtime/testing.d.ts.map +1 -0
  28. package/dist/agent-runtime-openrouter.d.ts +6 -0
  29. package/dist/agent-runtime-openrouter.d.ts.map +1 -0
  30. package/dist/agent-runtime-openrouter.js +46 -0
  31. package/dist/agent-runtime.d.ts +13 -0
  32. package/dist/agent-runtime.d.ts.map +1 -0
  33. package/dist/agent-runtime.js +1931 -0
  34. package/dist/cli.js +8 -5
  35. package/dist/index-0nc0cddp.js +178 -0
  36. package/dist/{index-6y759j86.js → index-1tkyng3g.js} +9 -57
  37. package/dist/{index-sm2tjx06.js → index-41wm56v0.js} +2 -2
  38. package/dist/index-6djpbnda.js +56 -0
  39. package/dist/{index-j3dem06f.js → index-7c0gkyvj.js} +5 -3
  40. package/dist/{index-n4nfa7gh.js → index-9zn9fb4e.js} +74 -10
  41. package/dist/{index-xy8fmh6w.js → index-f6pymtqe.js} +3 -3
  42. package/dist/{index-jcc611vh.js → index-gnvzyw0k.js} +2 -2
  43. package/dist/{index-qh4xevf5.js → index-mr617n62.js} +4 -2
  44. package/dist/{index-5r13htq1.js → index-ps5pxp3h.js} +1 -1
  45. package/dist/index-sa2mbwa7.js +336 -0
  46. package/dist/index-smpbdg6k.js +27 -0
  47. package/dist/{index-7eq9va6e.js → index-y2rb7dwx.js} +1 -27
  48. package/dist/{index-escqg10p.js → index-zk5zn2nh.js} +32 -334
  49. package/dist/internal/observability-sink.d.ts +26 -0
  50. package/dist/internal/observability-sink.d.ts.map +1 -0
  51. package/dist/node.js +6 -5
  52. package/dist/observability/audit.d.ts +4 -0
  53. package/dist/observability/audit.d.ts.map +1 -1
  54. package/dist/observability/event.d.ts +5 -0
  55. package/dist/observability/event.d.ts.map +1 -1
  56. package/dist/observability/index.js +29 -186
  57. package/dist/remote.js +4 -3
  58. package/dist/server/create.d.ts.map +1 -1
  59. package/dist/server/error-hook.d.ts +10 -8
  60. package/dist/server/error-hook.d.ts.map +1 -1
  61. package/dist/server/index.js +11 -8
  62. package/dist/server/logger.d.ts.map +1 -1
  63. package/dist/server/request-body.d.ts.map +1 -1
  64. package/dist/testing.js +5 -3
  65. package/dist/tools/agent.d.ts.map +1 -1
  66. package/dist/tools/execute.d.ts +7 -0
  67. package/dist/tools/execute.d.ts.map +1 -1
  68. package/dist/tools.d.ts +2 -1
  69. package/dist/tools.d.ts.map +1 -1
  70. package/dist/tools.js +23 -12
  71. package/llms-full.txt +429 -18
  72. package/llms.txt +1 -0
  73. package/package.json +16 -3
package/llms-full.txt CHANGED
@@ -47,6 +47,8 @@ keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
47
47
  | `stitchkit/remote` | browser **and** server | peer-free `implementRemote` for thin HTTP proxy processes |
48
48
  | `stitchkit/files` | server (Bun or Node) | peer-free managed local-file boundary |
49
49
  | `stitchkit/observability` | server | request/tool event projections — `createObservability`, trace context, sanitisation |
50
+ | `stitchkit/agent-runtime` | server | optional durable conversation/run loop, history, models, prompts, fencing and events |
51
+ | `stitchkit/agent-runtime/openrouter` | server | isolated OpenRouter language-model adapter |
50
52
  | `stitchkit/testing` | tests on Bun or Node | in-process generated clients over a real Fetch handler, without a TCP port |
51
53
  | `stitchkit/react` | browser | `createCursorQuery`, `createCacheBridge` |
52
54
 
@@ -146,6 +148,8 @@ map — feature → packages:
146
148
  | `createServer` (Bun) | — (uses `Bun.serve`) |
147
149
  | `serveNode` (Node ≥ 22) | `srvx` (+ `@types/bun` dev) |
148
150
  | MCP / agent tools (`stitchkit/tools`) | `@modelcontextprotocol/server` `ai` |
151
+ | Agent application runtime (`stitchkit/agent-runtime`) | `ai` |
152
+ | OpenRouter runtime adapter (`stitchkit/agent-runtime/openrouter`) | `ai` `@openrouter/ai-sdk-provider` |
149
153
  | MCP host/client tests | `@modelcontextprotocol/client` |
150
154
  | MCP Apps UI widgets | `@modelcontextprotocol/ext-apps` |
151
155
  | React data layer (`stitchkit/react`) | `@tanstack/react-query` `react-query-kit` |
@@ -989,6 +993,18 @@ createServer({
989
993
  | `'pretty'` | two coloured lines per request — `→` on arrival, `←` on completion | no — a line sized for a terminal is not a record |
990
994
  | `'json'` | one structured line per completed request | yes |
991
995
 
996
+ Status `499` has one framework-wide meaning: the client closed the request. It
997
+ is logged at `info`, not under the ordinary `4xx → warn` rule. A confirmed
998
+ disconnect is not sent through project `onError`, `normalizeError` or the
999
+ request-error recorder; an `AbortError` while the request signal is still active
1000
+ remains an internal failure. A runtime abort reason may be preserved by identity
1001
+ through at most eight cycle-safe standard `cause` links; error messages and codes
1002
+ are never classifiers. The same rule applies when the disconnect happens while
1003
+ Stitchkit is reading a JSON upload body: bounded reads race every pending stream
1004
+ read against the request signal and never parse a cancelled partial body. `499`
1005
+ is transport telemetry, not a response declared in the contract or generated
1006
+ OpenAPI document.
1007
+
992
1008
  Unset, `format` follows `NODE_ENV`: `json` under `production`, `pretty`
993
1009
  otherwise. That default is read **per request** — not at import, not when this
994
1010
  package was built — so it reflects the environment your app actually runs in.
@@ -1159,7 +1175,9 @@ createServer({
1159
1175
  - **`afterHandle`** — receives the handler result; return a replacement to
1160
1176
  transform it.
1161
1177
  - **`onError`** — receives any thrown error; return a `Response` to customise
1162
- the error body. Without it, errors render through the standard envelope.
1178
+ the error body. Without it, errors render through the standard envelope. A
1179
+ confirmed client disconnect is a transport cancellation rather than an
1180
+ application error and deliberately bypasses this hook.
1163
1181
 
1164
1182
  Hooks see `RuntimeContext` (loose types); handlers see `HandlerContext` (typed).
1165
1183
  That split is deliberate — see [ADR 0003](../decisions/0003-two-context-types.md).
@@ -2822,6 +2840,10 @@ list.
2822
2840
 
2823
2841
  ## AI agents — `mountAgent`
2824
2842
 
2843
+ `mountAgent` remains the low-level path when the application owns its own model
2844
+ loop and conversation state. For the optional Stitchkit-owned run/history loop,
2845
+ see [Agent application runtime](./agent-runtime.md).
2846
+
2825
2847
  `mountAgent` turns a service into a Vercel AI SDK `ToolSet`, ready for
2826
2848
  `generateText` / `streamText`:
2827
2849
 
@@ -3326,6 +3348,288 @@ the rest of the context (`params`, `input`, anything `context` injects) is
3326
3348
  identical. Write the handler once; it serves every surface.
3327
3349
 
3328
3350
 
3351
+ ==============================================================================
3352
+ # Guide: Agent application runtime (docs/guide/agent-runtime.md)
3353
+ ==============================================================================
3354
+
3355
+ ---
3356
+ title: "Agent application runtime"
3357
+ description: Configure Stitchkit's optional durable history, stream loop, run coordination and managed-tool fencing.
3358
+ type: architecture
3359
+ status: active
3360
+ created: 2026-08-22
3361
+ updated: 2026-08-22
3362
+ ---
3363
+
3364
+ # Agent application runtime
3365
+
3366
+ `stitchkit/agent-runtime` is the server-only, opinionated layer above
3367
+ `mountAgent`. Use it when the application wants Stitchkit to own conversation
3368
+ mechanics: durable acceptance, history projection, the AI SDK stream loop,
3369
+ checkpoints, keyed interruption, terminal commit and stable application events.
3370
+
3371
+ If the application already owns that loop, continue importing `mountAgent` from
3372
+ `stitchkit/tools`. Neither path depends on the other at runtime.
3373
+
3374
+ ## Install
3375
+
3376
+ ```sh
3377
+ bun add stitchkit ai zod
3378
+ ```
3379
+
3380
+ OpenRouter is isolated so other runtime users do not resolve its package:
3381
+
3382
+ ```sh
3383
+ bun add @openrouter/ai-sdk-provider
3384
+ ```
3385
+
3386
+ ## Minimal composition
3387
+
3388
+ ```ts
3389
+ import { z } from 'zod'
3390
+ import {
3391
+ composeAgentPrompt,
3392
+ createAgentRuntime,
3393
+ createMemoryAgentRuntimeStore,
3394
+ defineAgentProtocol,
3395
+ defineModelRegistry,
3396
+ } from 'stitchkit/agent-runtime'
3397
+ import { openRouterProvider } from 'stitchkit/agent-runtime/openrouter'
3398
+ import { composeToolLifecycle, mountAgent } from 'stitchkit/tools'
3399
+
3400
+ const protocol = defineAgentProtocol({
3401
+ context: z.object({ userId: z.string() }),
3402
+ inputMetadata: z.object({}),
3403
+ })
3404
+
3405
+ const models = defineModelRegistry({
3406
+ providers: {
3407
+ openrouter: openRouterProvider({ apiKey: env.OPENROUTER_API_KEY }),
3408
+ },
3409
+ models: {
3410
+ fast: {
3411
+ provider: 'openrouter',
3412
+ modelId: 'provider/model',
3413
+ contextWindow: 128_000,
3414
+ capabilities: ['tools'],
3415
+ },
3416
+ },
3417
+ })
3418
+
3419
+ const prompt = composeAgentPrompt([
3420
+ {
3421
+ name: 'product',
3422
+ stability: 'stable',
3423
+ render: ({ context }) => `Help user ${context.userId}.`,
3424
+ },
3425
+ ])
3426
+
3427
+ const runtime = createAgentRuntime({
3428
+ protocol,
3429
+ store: createMemoryAgentRuntimeStore(),
3430
+ models: { resolve: () => models.resolve('fast', ['tools']) },
3431
+ prompt: ({ context, signal, model }) =>
3432
+ prompt({
3433
+ context,
3434
+ signal,
3435
+ budget: {
3436
+ contextWindow: model.descriptor.contextWindow,
3437
+ reservedOutput: 8_000,
3438
+ toolSchemas: { provenance: 'unavailable' },
3439
+ attachments: { value: 0, provenance: 'measured' },
3440
+ providerOverhead: { provenance: 'unavailable' },
3441
+ },
3442
+ }),
3443
+ tools: ({ context, toolFenceLifecycle }) =>
3444
+ mountAgent(service, {
3445
+ context,
3446
+ lifecycle: composeToolLifecycle(authLifecycle, toolFenceLifecycle),
3447
+ }),
3448
+ runs: { inputPolicy: 'interrupt', coalescePending: true },
3449
+ loop: {
3450
+ idleTimeoutMs: 60_000,
3451
+ prepareStep: ({ context, steps }) => ({
3452
+ activeTools: steps.some((step) => step.toolCalls.length > 0)
3453
+ ? ['lookup']
3454
+ : ['lookup', 'discover'],
3455
+ instructions: `Current account: ${context.userId}`,
3456
+ }),
3457
+ stopPolicies: [
3458
+ {
3459
+ name: 'repeated-tool-error',
3460
+ when: ({ steps }) => hasRepeatedToolError(steps),
3461
+ },
3462
+ ],
3463
+ },
3464
+ })
3465
+
3466
+ const ticket = runtime.submit({
3467
+ conversationId: 'conversation-id',
3468
+ idempotencyKey: 'request-id',
3469
+ context: { userId: 'user-id' },
3470
+ parts: [{ type: 'text', text: 'Hello' }],
3471
+ })
3472
+
3473
+ await ticket.accepted
3474
+ const terminal = await ticket.result
3475
+ ```
3476
+
3477
+ The in-memory store is a reference adapter and has process-local durability
3478
+ only. Production applications implement `AgentRuntimeStore` with their own
3479
+ database transaction and, when needed, distributed lease/fencing token.
3480
+
3481
+ ## Durable order
3482
+
3483
+ `acceptInputAndAssignRun` is one atomic operation. It is followed by ownership
3484
+ acquisition, revision-checked assistant checkpoints and one terminal CAS. The
3485
+ process-local coordinator releases its lane only after terminal commit.
3486
+
3487
+ ```text
3488
+ input + queued run → running → execution settled → terminal CAS → successor
3489
+ ```
3490
+
3491
+ With `runs.coalescePending: true`, an active lane has at most one queued
3492
+ successor. Every later accepted input is atomically appended to that successor;
3493
+ its `AgentRun.inputMessageIds` records the whole batch and every input ticket
3494
+ resolves to the same terminal run. Coalescing never mutates the active run.
3495
+
3496
+ `AbortSignal.aborted` is only a cooperation request. A successor does not begin
3497
+ while the predecessor still owns managed callbacks. A hung predecessor blocks
3498
+ the lane in the first version.
3499
+
3500
+ Use `await runtime.interrupt({ conversationId, runId })` when the interruption
3501
+ must be durable: it first commits `interrupt_requested`, then aborts the local
3502
+ coordinator signal. `runtime.stop(key)` is the process-local signal-only escape
3503
+ hatch.
3504
+
3505
+ ## Store operations
3506
+
3507
+ An adapter implements the aggregate `AgentRuntimeStore`, not separate message
3508
+ and run CRUD stores:
3509
+
3510
+ - `acceptInputAndAssignRun`
3511
+ - `acquireRun`
3512
+ - `checkpointRunAssistant`
3513
+ - `requestRunInterrupt`
3514
+ - `recoverRun`
3515
+ - `commitRunTerminal`
3516
+ - `replaceCompactedRange`
3517
+ - `loadSnapshot` and `scanRecoverable`
3518
+
3519
+ Every mutation carries an expected run revision or snapshot version. Input
3520
+ assignment additionally carries an idempotency identity. A conflict is a
3521
+ control outcome; stale data is never silently overwritten.
3522
+
3523
+ On startup, `scanRecoverable` returns queued/acquired records. `recoverRun`
3524
+ may abandon them, or requeue an already acquired run only with explicit
3525
+ `replaySafe: true` evidence. The framework never guesses that an external
3526
+ side effect is replayable. After an application reconstructs its typed context,
3527
+ `runtime.resume({ conversationId, runId, context })` admits that queued record
3528
+ through the same acquisition CAS and coordinator lane; it never creates a
3529
+ second input message.
3530
+
3531
+ Canonical records currently write `schemaVersion: 1`. A durable adapter owns
3532
+ read-time migration of older rows: migrate to the current shape at its storage
3533
+ boundary, validate with the exported schema, and write only the current
3534
+ version. Core deliberately does not guess an application's database migration
3535
+ or silently accept an unknown future version.
3536
+
3537
+ ## Events and reconnect
3538
+
3539
+ `publish` receives event classes with different guarantees:
3540
+
3541
+ - `assistant-delta` is transient and ordered by
3542
+ `(runId, runtimeEpoch, sequence)`;
3543
+ - `assistant-checkpoint` follows a successful checkpoint CAS;
3544
+ - `run-state` follows durable queue/acquire/interrupt transitions;
3545
+ - `tool-status` is transient lifecycle presentation with JSON-safe input on
3546
+ start and output on completion; internal tool failures remain generic;
3547
+ - `terminal` follows the winning terminal CAS.
3548
+
3549
+ A named custom stop condition terminalizes with `policy_stop`; its `policyName`
3550
+ is persisted on the run and included in the terminal event/result. `max-steps`
3551
+ is the reserved built-in policy name. `loop.prepareStep` is the controlled AI
3552
+ SDK step boundary for changing active tools, model, instructions or messages.
3553
+ It cannot replace the managed tool set or bypass its lifecycle fence.
3554
+
3555
+ `loop.idleTimeoutMs` is inactivity, not total duration: the deadline resets on
3556
+ every model stream event. A stalled call aborts with durable reason `timeout`;
3557
+ user interruption and shutdown remain distinct.
3558
+
3559
+ Reconnect loads the durable snapshot. Missing transient deltas do not mean the
3560
+ canonical result was lost. Exactly-once external delivery requires the
3561
+ application's transactional outbox or stable-ID deduplication.
3562
+
3563
+ ## Managed tools
3564
+
3565
+ Always compose `toolFenceLifecycle` into `mountAgent`. It checks ownership
3566
+ before a managed side effect and again before accepting its result. Fence loss
3567
+ uses an internal control signal: it stops the old loop and is not sent to the
3568
+ model as a tool error.
3569
+
3570
+ The framework cannot undo an already-started non-cooperative external effect.
3571
+ Pass the stable call/run idempotency identity into business mutations when the
3572
+ effect must be replay-safe.
3573
+
3574
+ ## History, provider metadata and files
3575
+
3576
+ `projectAgentHistory` converts canonical engine records into provider-valid AI
3577
+ SDK messages and pairs tool calls/results. Provider-required metadata is kept
3578
+ in a versioned opaque envelope and omitted from product delivery by default.
3579
+
3580
+ `ComposedAgentPrompt.instructions` accepts the AI SDK `Instructions` contract.
3581
+ Use `adaptInstructions` when a provider needs metadata on the system message:
3582
+
3583
+ ```ts
3584
+ prompt({
3585
+ context,
3586
+ signal,
3587
+ adaptInstructions: (content) => ({
3588
+ role: 'system',
3589
+ content,
3590
+ providerOptions: { openrouter: { cacheControl: { type: 'ephemeral' } } },
3591
+ }),
3592
+ })
3593
+ ```
3594
+
3595
+ Attachments and generated files remain application-owned. The runtime only
3596
+ stores a reference returned by `persistGeneratedFile`; it never silently puts
3597
+ base64 blobs into the neutral history store.
3598
+
3599
+ To send a stored attachment back to a multimodal model, configure
3600
+ `history.resolveFile`. It maps the neutral file reference to AI SDK file data
3601
+ (URL, bytes, provider reference or text). Without a resolver the explicit
3602
+ `unresolvedFile` policy is `text` by default; choose `omit` or `error` when a
3603
+ placeholder would be incorrect.
3604
+
3605
+ ## Compaction
3606
+
3607
+ `structuredCompaction` summarizes complete provider-valid turn groups outside
3608
+ the store lock, then calls `replaceCompactedRange` against the exact snapshot
3609
+ version. A concurrent input produces `conflict`; the stale summary is not
3610
+ applied. Summary records use the dedicated `summary` role and project to a
3611
+ provider system message. The consumer supplies the structured summary schema
3612
+ and prompt. Pass `previousSummary` for a direct call, or configure
3613
+ `readPreviousSummary` for runtime-managed compaction, to merge and atomically
3614
+ replace a leading summary on the next compaction.
3615
+
3616
+ ## Observability
3617
+
3618
+ `createAgentObservability` emits a separate operator-only `AgentRunEvent`. It
3619
+ reuses the same bounded sink lifecycle as request/tool observability without
3620
+ sending new event kinds to existing request sinks. Product events omit provider
3621
+ causes; the operator terminal event may include `internalCause`, so its sink
3622
+ must use internal retention and redaction policy.
3623
+
3624
+ Usage values carry `provider-reported`, `computed`, `estimated` or
3625
+ `unavailable` provenance. Cost additionally carries an ISO currency code;
3626
+ OpenRouter-reported cost is normalized as USD. Missing values remain absent,
3627
+ never zero-filled.
3628
+
3629
+ In-memory sink delivery is at-most-once per execution. Stable event IDs allow
3630
+ consumer dedupe; cross-crash exactly-once requires a durable outbox.
3631
+
3632
+
3329
3633
  ==============================================================================
3330
3634
  # Guide: CLI (docs/guide/cli.md)
3331
3635
  ==============================================================================
@@ -4685,22 +4989,36 @@ error round-trips without adding correlation data to the response body.
4685
4989
  ### Stitch codes vs your codes
4686
4990
 
4687
4991
  A `code` is a free string — your app codes (`BOT_NOT_FOUND`, …) are yours and the
4688
- core never models them (ADR 0002). But stitchkit itself emits a fixed set:
4992
+ core never models them (ADR 0002). But stitchkit itself emits a set of its own:
4689
4993
  `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `METHOD_NOT_ALLOWED`,
4690
- `CONFLICT`, `RATE_LIMITED`, `VALIDATION_ERROR`, `INTERNAL_SERVER_ERROR`. They are
4994
+ `CONFLICT`, `RATE_LIMITED`, `VALIDATION_ERROR`, `FILE_INVALID_PATH`,
4995
+ `FILE_OUTSIDE_ROOT`, `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`,
4996
+ `FILE_INSPECTION_REJECTED`, `FILE_TOO_LARGE`, `FILE_EXISTS`,
4997
+ `REALTIME_CONTRACT_VIOLATION`, `INTERNAL_SERVER_ERROR` — a set that grows in
4998
+ ordinary releases, each addition named in the changelog. They are
4691
4999
  published as **`STITCH_ERROR_STATUS`** (the `code → status` map) and
4692
5000
  **`StitchErrorCode`** (its `keyof`), with **`isStitchErrorCode()`** (→ ADR 0026).
4693
5001
 
4694
5002
  If you translate stitch's framework errors into your own wire codes in an
4695
- `onError` hook, key the map by `StitchErrorCode` so it stays exhaustive a code
4696
- stitch adds or renames becomes a compile error, not a silent `500`:
5003
+ `onError` hook, you choose how the set's growth reaches you. Keying the map by
5004
+ `StitchErrorCode` makes it exhaustive, so a code stitch adds or renames becomes
5005
+ a compile error rather than an unfamiliar code on your wire — worth it when the
5006
+ envelope is a published contract, at the cost of an edit on those releases:
4697
5007
 
4698
5008
  ```ts
5009
+ // Exhaustive on purpose: the annotation is what turns a new stitch code into a
5010
+ // compile error here. Drop it (or use `satisfies` on a partial map) to let an
5011
+ // unmapped code travel as itself instead.
4699
5012
  const STITCH_TO_APP: Record<StitchErrorCode, AppCode> = {
4700
5013
  NOT_FOUND: 'NOT_FOUND', METHOD_NOT_ALLOWED: 'METHOD_NOT_ALLOWED',
4701
5014
  BAD_REQUEST: 'VALIDATION_ERROR', VALIDATION_ERROR: 'VALIDATION_ERROR',
4702
5015
  UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', CONFLICT: 'CONFLICT',
4703
5016
  RATE_LIMITED: 'RATE_LIMITED', INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
5017
+ REALTIME_CONTRACT_VIOLATION: 'INTERNAL_SERVER_ERROR',
5018
+ FILE_INVALID_PATH: 'VALIDATION_ERROR', FILE_OUTSIDE_ROOT: 'VALIDATION_ERROR',
5019
+ FILE_NOT_FOUND: 'NOT_FOUND', FILE_NOT_REGULAR: 'VALIDATION_ERROR',
5020
+ FILE_INSPECTION_REJECTED: 'VALIDATION_ERROR', FILE_TOO_LARGE: 'VALIDATION_ERROR',
5021
+ FILE_EXISTS: 'CONFLICT',
4704
5022
  }
4705
5023
  onError: (ctx, err) => {
4706
5024
  if (AppError.is(err) && isStitchErrorCode(err.code)) {
@@ -4776,20 +5094,20 @@ codes and schemas remain application-owned; Stitchkit stays domain-free.
4776
5094
 
4777
5095
  ## `createErrorHook`
4778
5096
 
4779
- `createErrorHook` is the code-map above, packaged — you supply the exhaustive
4780
- `codeMap` and the envelope shape, it does the normalisation (including the
5097
+ `createErrorHook` is the code-map above, packaged — you supply a `codeMap` and
5098
+ the envelope shape, it does the normalisation (including the
4781
5099
  never-leak-an-internal-message rule for a raw throw):
4782
5100
 
4783
5101
  ```ts
4784
5102
  const onError = createErrorHook({
5103
+ // Map the codes you have an opinion about; the rest travel as themselves.
4785
5104
  codeMap: {
4786
5105
  BAD_REQUEST: 'bad_request', VALIDATION_ERROR: 'bad_request',
4787
5106
  UNAUTHORIZED: 'unauthenticated', FORBIDDEN: 'forbidden',
4788
5107
  NOT_FOUND: 'not_found', METHOD_NOT_ALLOWED: 'not_found',
4789
5108
  CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
4790
5109
  INTERNAL_SERVER_ERROR: 'internal',
4791
- REALTIME_CONTRACT_VIOLATION: 'internal',
4792
- } satisfies Record<StitchErrorCode, string>,
5110
+ },
4793
5111
  // `ctx` is the request's RuntimeContext — read `ctx.traceId` for a
4794
5112
  // correlation id in the envelope. Declaring it is optional.
4795
5113
  render: (info, ctx) => ({
@@ -4802,8 +5120,24 @@ const onError = createErrorHook({
4802
5120
  createServer({ services, hooks: { onError } })
4803
5121
  ```
4804
5122
 
4805
- Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
4806
- `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
5123
+ `codeMap` is partial: map the codes you have an opinion about. A stitchkit code
5124
+ you did not list travels as itself — the same thing a code you threw yourself
5125
+ always did. That is what keeps a code added by a future release from being a
5126
+ compile break for every project that translates codes at all.
5127
+
5128
+ One `satisfies` is the **opt-in** to the stricter deal:
5129
+
5130
+ ```ts
5131
+ codeMap: { /* … every code … */ } satisfies Record<StitchErrorCode, string>,
5132
+ ```
5133
+
5134
+ That makes the map exhaustive on your side, so a release that adds a code stops
5135
+ your build until you decide what the new code is called on your wire. Take it
5136
+ when your envelope is a published contract and a code surfacing in stitchkit's
5137
+ spelling would violate it; leave it off when passing one through is fine.
5138
+ Neither choice is silent — the changelog names every added code. For a single
5139
+ catch-all instead of either, decide it in `render`, where `info.code` is the
5140
+ resolved value.
4807
5141
 
4808
5142
  Both `onError` and `render` may be asynchronous and receive the matched endpoint
4809
5143
  as their final argument. The observer is awaited before rendering, so it can
@@ -4878,9 +5212,11 @@ a request context, and event projections — and you usually touch only the last
4878
5212
  ### createObservability
4879
5213
 
4880
5214
  `createObservability` configures request and tool projections independently.
4881
- Every completed call is normalised into one `RequestEvent`; HTTP completion is
4882
- owned directly by `createHandler`, while MCP/Agent completion uses the canonical
4883
- `ToolCallHooks` runner. There is no nested HTTP audit wrapper.
5215
+ Every completed application call is normalised into one `RequestEvent`; HTTP
5216
+ completion is owned directly by `createHandler`, while MCP/Agent completion uses
5217
+ the canonical `ToolCallHooks` runner. Confirmed HTTP client cancellation stays
5218
+ in the access log by default and becomes a structured event only when the
5219
+ request sink opts in. There is no nested HTTP audit wrapper.
4884
5220
 
4885
5221
  ```ts
4886
5222
  import { createObservability } from 'stitchkit/observability'
@@ -4901,6 +5237,7 @@ export const observability = createObservability({
4901
5237
  request: {
4902
5238
  write,
4903
5239
  includePayload: false, // default: no Request.clone(), payload is null
5240
+ includeCancelled: false, // default: keep client closes in access logs only
4904
5241
  filter: (event) => event.method !== 'GET',
4905
5242
  },
4906
5243
  tools: {
@@ -4923,6 +5260,40 @@ Each sink runs fire-and-forget and fails independently: a slow or broken request
4923
5260
  sink cannot block the response, suppress operational logging or break the tool
4924
5261
  sink.
4925
5262
 
5263
+ #### Client cancellation
5264
+
5265
+ A physical client close is classified only when the request's own signal is
5266
+ aborted and the thrown value is either an `AbortError` or contains the exact
5267
+ `request.signal.reason` by identity at the top level or within at most eight
5268
+ standard `cause` links. Cause traversal is cycle-safe. No message or error-code
5269
+ matching is used; active requests and unrelated/deeper causes remain application
5270
+ failures. The access completion is always `499/info`, without application error
5271
+ fields or project `onError`.
5272
+
5273
+ Structured request sinks are default-preserving: existing sinks receive no row
5274
+ for this outcome. Opt in when cancellation frequency belongs in the durable
5275
+ stream:
5276
+
5277
+ ```ts
5278
+ const observability = createObservability({
5279
+ request: {
5280
+ includeCancelled: true,
5281
+ write: (event) => {
5282
+ if (event.outcome === 'cancelled') return recordClientClose(event)
5283
+ return recordApplicationRequest(event)
5284
+ },
5285
+ },
5286
+ })
5287
+ ```
5288
+
5289
+ An opted-in row has `outcome: 'cancelled'`, `ok: false`, `statusCode: 499`, the
5290
+ ordinary identity/trace/timing fields and no `errorCode`, `errorMessage` or
5291
+ `errorDetail`. `ok` remains the legacy success bit: branch on `outcome` first.
5292
+ Cancellation rows use the same filter, capacity, diagnostics, ordering,
5293
+ `flush()` and `close()` machinery as every request event. MCP protocol
5294
+ cancellation remains represented by `event.mcp.outcome`; Agent and CLI have no
5295
+ generic client-disconnect signal and are not inferred from error text.
5296
+
4926
5297
  The fire-and-forget work has an explicit bounded lifecycle:
4927
5298
 
4928
5299
  ```ts
@@ -5003,7 +5374,8 @@ queryable across all three:
5003
5374
  | `httpMethod` | the contract verb on **tool** events (their `method` is `TOOL`) — filter reads vs writes across both surfaces with `(event.httpMethod ?? event.method) !== 'GET'` |
5004
5375
  | `dimensions` | app-defined domain dimensions (tenant / project / entity id) — see [request context](#request-context) |
5005
5376
  | `traceId` / `spanId` / `parentSpanId` | [W3C trace context](#trace-context) |
5006
- | `ok` / `statusCode` | outcome real HTTP status, or `200`/`400` for a tool |
5377
+ | `outcome` | optional `'cancelled'` on explicitly enabled HTTP client-close rows; ordinary rows omit it |
5378
+ | `ok` / `statusCode` | legacy success bit plus real HTTP status, or `200`/`400` for a tool; an opted-in cancellation is `false` / `499` |
5007
5379
  | `durationMs` / `startedAt` | timing |
5008
5380
  | `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
5009
5381
  | `payload` | sanitised tool arguments; HTTP is `null` unless request `includePayload` is enabled |
@@ -5253,6 +5625,10 @@ success and error alike — carrying the tool name, the arguments, the result, t
5253
5625
  duration, the call context, the endpoint identity, and (only when the call failed
5254
5626
  by throwing) the raw thrown value.
5255
5627
 
5628
+ A confirmed HTTP client disconnect is the exception to the raw-hook table: it
5629
+ is transport cancellation, so neither `afterHandle` nor project `onError` runs.
5630
+ Use the `499/info` access completion or opt-in request cancellation event above.
5631
+
5256
5632
  ```ts
5257
5633
  createMcpHandler({
5258
5634
  serverInfo, auth, services,
@@ -7526,6 +7902,38 @@ Also re-exports the error helpers from `stitchkit/contract`.
7526
7902
 
7527
7903
  ---
7528
7904
 
7905
+ ## `stitchkit/agent-runtime`
7906
+
7907
+ Server-only optional application runtime. See the
7908
+ [agent runtime guide](../guide/agent-runtime.md).
7909
+
7910
+ | Export | Kind | Summary |
7911
+ |--------|------|---------|
7912
+ | `createAgentRuntime` | function | compose durable acceptance, stream loop, checkpoints, coordination, managed tools and terminal publication |
7913
+ | `defineAgentProtocol` | function | declare and validate context, input metadata and canonical message parts |
7914
+ | `AgentMessageSchema` / `AgentRunSchema` / `AgentSnapshotSchema` | schema | versioned canonical engine records |
7915
+ | `AgentRuntimeStore` | _type_ | aggregate CAS transaction boundary for message, run and compaction mutations |
7916
+ | `RecoverAgentRunSchema` | schema | explicit abandon/requeue recovery decision; acquired runs require replay-safe evidence |
7917
+ | `createMemoryAgentRuntimeStore` | function | process-local reference adapter, not production durability |
7918
+ | `projectAgentHistory` | function | asynchronously project canonical records and resolved multimodal files into provider-valid AI SDK messages |
7919
+ | `defineModelRegistry` | function | typed language-model descriptors, capabilities and provider construction |
7920
+ | `composeAgentPrompt` | function | ordered prompt contributions and provenance-aware context budget |
7921
+ | `structuredCompaction` | function | summarize a provider-valid snapshot range and replace it through CAS |
7922
+ | `createAgentSessionCoordinator` | function | strict process-local queue/interrupt lifecycle |
7923
+ | `AgentRuntimeStopPolicy` | _type_ | named custom AI SDK stop condition persisted and published on policy stop |
7924
+ | `AgentRuntimePrepareStep` | _type_ | per-run controlled step callback with typed domain context and managed run signal/fence |
7925
+ | `AgentHistoryProjectionOptions` | _type_ | storage-neutral file resolver and explicit unresolved-file behavior |
7926
+ | `createAgentToolFenceLifecycle` | function | pre-effect and post-effect run ownership fence for `mountAgent` |
7927
+ | `AgentRuntimeEventSchema` | schema | transient delta, durable checkpoint/run-state/tool and terminal event union |
7928
+ | `createAgentObservability` | function | separate agent-run sink over the shared bounded observability lifecycle |
7929
+
7930
+ ## `stitchkit/agent-runtime/openrouter`
7931
+
7932
+ | Export | Kind | Summary |
7933
+ |--------|------|---------|
7934
+ | `openRouterProvider` | function | isolated `@openrouter/ai-sdk-provider` language-model factory |
7935
+ | `OpenRouterProviderSettings` | _type_ | official provider settings accepted by the factory |
7936
+
7529
7937
  ## `stitchkit/observability`
7530
7938
 
7531
7939
  Server-only. The audit layer one level above the raw hooks — W3C trace context,
@@ -7537,7 +7945,7 @@ audit event. See the [Observability guide](../guide/observability.md).
7537
7945
  | Export | Kind | Summary |
7538
7946
  |--------|------|---------|
7539
7947
  | `createObservability` | function | configure framework-owned request completion and canonical tool event sinks — [guide](../guide/observability.md#createobservability) |
7540
- | `RequestEvent` | _type_ | the normalised audit event handed to the sink |
7948
+ | `RequestEvent` | _type_ | the normalised audit event handed to the sink; opt-in HTTP cancellation rows carry `outcome: 'cancelled'` |
7541
7949
  | `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
7542
7950
  | `Observability` | _type_ | `{ request?, toolCall, getStatus(), flush(), close() }` with bounded sink lifecycle |
7543
7951
  | `ObservabilitySinkStatus` | _type_ | immutable counters for one bounded request/tool sink |
@@ -7545,11 +7953,11 @@ audit event. See the [Observability guide](../guide/observability.md).
7545
7953
  | `ObservabilityDrainReport` | _type_ | final closed/drained snapshot plus duration |
7546
7954
  | `ObservabilitySinkStatusSchema` / `ObservabilityStatusSchema` / `ObservabilityDrainReportSchema` | schema | runtime schemas for status/report integration boundaries |
7547
7955
  | `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
7548
- | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture |
7956
+ | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture and default-off `includeCancelled` rows |
7549
7957
  | `SinkDropReason` | _type_ | `'capacity' \| 'closed'` |
7550
7958
  | `SinkError` | _type_ | isolated sink/projection failure and optional event |
7551
7959
  | `SinkDrop` | _type_ | rejected event, reason and current pending count |
7552
- | `HttpRequestCompletion` | _type_ | the single framework-owned HTTP outcome projected to logging and request events |
7960
+ | `HttpRequestCompletion` | _type_ | the single framework-owned HTTP outcome, including optional neutral cancellation, projected to logging and request events |
7553
7961
  | `HttpRequestObserver` | _type_ | server-facing projection consumed by `HandlerConfig.observability` |
7554
7962
 
7555
7963
  ### Request context
@@ -7699,6 +8107,9 @@ payload.
7699
8107
  | `Toolkit` | _type_ | the context-pinned tool surface from `createToolkit` |
7700
8108
  | `ToolExtend` | _type_ | extra-args extension for `mountMcp` / `mountAgent` |
7701
8109
  | `ToolLifecycle` | _type_ | `beforeHandle` / `afterHandle` gate for tool calls — [guide](../guide/mcp-and-agents.md#guarding-tools--lifecycle) |
8110
+ | `ToolExecutionControlReason` | _type_ | internal managed-execution stop reason: stale run or requested interruption |
8111
+ | `ToolExecutionControlError` | class | control-flow error used to unwind a superseded managed tool call without presenting it as a model-facing tool failure |
8112
+ | `isToolExecutionControlError` | function | narrow an unknown thrown value to the managed execution control error |
7702
8113
  | `ToolOperation` | _type_ | executable path-free operation shape shared by contract and framework-native runners |
7703
8114
  | `ToolCallHooks` | _type_ | object-shaped `beforeToolCall` / `afterToolCall` / `onToolError` observability hooks; the raw thrown value reaches the last two as `error` ([guide](../guide/observability.md#the-cause-behind-a-failed-tool-call)) |
7704
8115
  | `BeforeToolCallOptions` | _type_ | `{ toolName, args, context, endpoint }` passed before execution |
package/llms.txt CHANGED
@@ -10,6 +10,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
10
10
  - [HTTP server](https://github.com/max-listov/stitchkit/blob/master/docs/guide/server.md): createServer/createHandler, implement, lifecycle hooks, raw routes + raw-response endpoints + helpers, scopePrefixes, serveFile, primitives
11
11
  - [Typed client](https://github.com/max-listov/stitchkit/blob/master/docs/guide/client.md): createClient/createHttpClient, the typed call surface, scoped clients, SSE
12
12
  - [MCP & agents](https://github.com/max-listov/stitchkit/blob/master/docs/guide/mcp-and-agents.md): contracts as MCP tools (createMcpHandler) and AI-agent tools (mountAgent); tool lifecycle, extend, identity
13
+ - [Agent application runtime](https://github.com/max-listov/stitchkit/blob/master/docs/guide/agent-runtime.md): optional durable history, prompt/model composition, stream loop, coordination, fencing and events
13
14
  - [CLI](https://github.com/max-listov/stitchkit/blob/master/docs/guide/cli.md): contracts as a command-line program
14
15
  - [Realtime](https://github.com/max-listov/stitchkit/blob/master/docs/guide/realtime.md): Socket.IO server/client wrappers, handshake auth, the cache bridge, a raw WebSocket lane
15
16
  - [Auth & errors](https://github.com/max-listov/stitchkit/blob/master/docs/guide/auth-and-errors.md): scopes, createAuthHook, JWT/cookies, the AppError model, the stitch error-code registry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.56.0",
3
+ "version": "0.56.2",
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",
@@ -76,6 +76,14 @@
76
76
  "types": "./dist/observability/index.d.ts",
77
77
  "import": "./dist/observability/index.js"
78
78
  },
79
+ "./agent-runtime": {
80
+ "types": "./dist/agent-runtime.d.ts",
81
+ "import": "./dist/agent-runtime.js"
82
+ },
83
+ "./agent-runtime/openrouter": {
84
+ "types": "./dist/agent-runtime-openrouter.d.ts",
85
+ "import": "./dist/agent-runtime-openrouter.js"
86
+ },
79
87
  "./testing": {
80
88
  "types": "./dist/testing.d.ts",
81
89
  "import": "./dist/testing.js"
@@ -95,7 +103,7 @@
95
103
  "scripts": {
96
104
  "check": "bun x tsc --noEmit",
97
105
  "build:browser": "bun build src/index.ts src/react.ts src/contract/index.ts --outdir dist --target node --packages external --splitting --root src",
98
- "build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/cli.ts src/remote.ts src/files.ts src/testing.ts src/observability/index.ts --outdir dist --target node --packages external --splitting --root src",
106
+ "build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/cli.ts src/remote.ts src/files.ts src/testing.ts src/observability/index.ts src/agent-runtime.ts src/agent-runtime-openrouter.ts --outdir dist --target node --packages external --splitting --root src",
99
107
  "build:js": "bun run build:browser && bun run build:server",
100
108
  "build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly",
101
109
  "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",
@@ -110,6 +118,7 @@
110
118
  "peerDependencies": {
111
119
  "@modelcontextprotocol/ext-apps": "^1.7.2",
112
120
  "@modelcontextprotocol/server": "^2.0.0",
121
+ "@openrouter/ai-sdk-provider": "^3.0.0",
113
122
  "@types/bun": "^1.3.14",
114
123
  "@socket.io/bun-engine": "^0.1.1",
115
124
  "@socket.io/component-emitter": "^3.1.2",
@@ -132,6 +141,9 @@
132
141
  "@modelcontextprotocol/server": {
133
142
  "optional": true
134
143
  },
144
+ "@openrouter/ai-sdk-provider": {
145
+ "optional": true
146
+ },
135
147
  "@socket.io/bun-engine": {
136
148
  "optional": true
137
149
  },
@@ -164,9 +176,10 @@
164
176
  "ky": "^2.0.2"
165
177
  },
166
178
  "devDependencies": {
167
- "@modelcontextprotocol/ext-apps": "^1.7.5",
168
179
  "@modelcontextprotocol/client": "^2.0.0",
180
+ "@modelcontextprotocol/ext-apps": "^1.7.5",
169
181
  "@modelcontextprotocol/server": "^2.0.0",
182
+ "@openrouter/ai-sdk-provider": "^3.0.0",
170
183
  "@socket.io/bun-engine": "^0.1.1",
171
184
  "@socket.io/component-emitter": "^3.1.2",
172
185
  "@tanstack/react-query": "5.101.4",