stitchkit 0.56.1 → 0.56.3

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 (67) 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 +132 -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 +1955 -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-75njxz7p.js → index-9zn9fb4e.js} +8 -6
  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.map +1 -1
  53. package/dist/observability/index.js +15 -178
  54. package/dist/remote.js +4 -3
  55. package/dist/server/error-hook.d.ts +9 -7
  56. package/dist/server/error-hook.d.ts.map +1 -1
  57. package/dist/server/index.js +9 -7
  58. package/dist/testing.js +5 -3
  59. package/dist/tools/agent.d.ts.map +1 -1
  60. package/dist/tools/execute.d.ts +7 -0
  61. package/dist/tools/execute.d.ts.map +1 -1
  62. package/dist/tools.d.ts +2 -1
  63. package/dist/tools.d.ts.map +1 -1
  64. package/dist/tools.js +23 -12
  65. package/llms-full.txt +382 -10
  66. package/llms.txt +1 -0
  67. 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` |
@@ -2836,6 +2840,10 @@ list.
2836
2840
 
2837
2841
  ## AI agents — `mountAgent`
2838
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
+
2839
2847
  `mountAgent` turns a service into a Vercel AI SDK `ToolSet`, ready for
2840
2848
  `generateText` / `streamText`:
2841
2849
 
@@ -3340,6 +3348,303 @@ the rest of the context (`params`, `input`, anything `context` injects) is
3340
3348
  identical. Write the handler once; it serves every surface.
3341
3349
 
3342
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
+ recordIds: {
3472
+ inputMessageId: 'user-message-id',
3473
+ runId: 'run-id',
3474
+ assistantMessageId: 'assistant-message-id',
3475
+ },
3476
+ })
3477
+
3478
+ const admission = await ticket.admission
3479
+ await ticket.accepted
3480
+ const terminal = await ticket.result
3481
+ ```
3482
+
3483
+ `recordIds` is optional. Supply stable application record IDs when an accepted-response transport must
3484
+ return durable placeholders before the run finishes. `ticket.admission` resolves after the store
3485
+ acceptance CAS and reports the actually assigned `runId`, `assistantMessageId` and snapshot version.
3486
+ Those assigned IDs can differ from the proposal when an input coalesces into an existing queued
3487
+ successor. Reuse the same `inputMessageId` for retries carrying the same idempotency key; input
3488
+ identity is caller-stable, while the receipt reports the run/assistant identities that assignment
3489
+ may change. Await `admission` first on the immediate accepted-response path. `ticket.accepted`
3490
+ remains the signal-only compatibility surface and additionally waits for admission publication.
3491
+
3492
+ The in-memory store is a reference adapter and has process-local durability
3493
+ only. Production applications implement `AgentRuntimeStore` with their own
3494
+ database transaction and, when needed, distributed lease/fencing token.
3495
+
3496
+ ## Durable order
3497
+
3498
+ `acceptInputAndAssignRun` is one atomic operation. It is followed by ownership
3499
+ acquisition, revision-checked assistant checkpoints and one terminal CAS. The
3500
+ process-local coordinator releases its lane only after terminal commit.
3501
+
3502
+ ```text
3503
+ input + queued run → running → execution settled → terminal CAS → successor
3504
+ ```
3505
+
3506
+ With `runs.coalescePending: true`, an active lane has at most one queued
3507
+ successor. Every later accepted input is atomically appended to that successor;
3508
+ its `AgentRun.inputMessageIds` records the whole batch and every input ticket
3509
+ resolves to the same terminal run. Coalescing never mutates the active run.
3510
+
3511
+ `AbortSignal.aborted` is only a cooperation request. A successor does not begin
3512
+ while the predecessor still owns managed callbacks. A hung predecessor blocks
3513
+ the lane in the first version.
3514
+
3515
+ Use `await runtime.interrupt({ conversationId, runId })` when the interruption
3516
+ must be durable: it first commits `interrupt_requested`, then aborts the local
3517
+ coordinator signal. `runtime.stop(key)` is the process-local signal-only escape
3518
+ hatch.
3519
+
3520
+ ## Store operations
3521
+
3522
+ An adapter implements the aggregate `AgentRuntimeStore`, not separate message
3523
+ and run CRUD stores:
3524
+
3525
+ - `acceptInputAndAssignRun`
3526
+ - `acquireRun`
3527
+ - `checkpointRunAssistant`
3528
+ - `requestRunInterrupt`
3529
+ - `recoverRun`
3530
+ - `commitRunTerminal`
3531
+ - `replaceCompactedRange`
3532
+ - `loadSnapshot` and `scanRecoverable`
3533
+
3534
+ Every mutation carries an expected run revision or snapshot version. Input
3535
+ assignment additionally carries an idempotency identity. A conflict is a
3536
+ control outcome; stale data is never silently overwritten.
3537
+
3538
+ On startup, `scanRecoverable` returns queued/acquired records. `recoverRun`
3539
+ may abandon them, or requeue an already acquired run only with explicit
3540
+ `replaySafe: true` evidence. The framework never guesses that an external
3541
+ side effect is replayable. After an application reconstructs its typed context,
3542
+ `runtime.resume({ conversationId, runId, context })` admits that queued record
3543
+ through the same acquisition CAS and coordinator lane; it never creates a
3544
+ second input message.
3545
+
3546
+ Canonical records currently write `schemaVersion: 1`. A durable adapter owns
3547
+ read-time migration of older rows: migrate to the current shape at its storage
3548
+ boundary, validate with the exported schema, and write only the current
3549
+ version. Core deliberately does not guess an application's database migration
3550
+ or silently accept an unknown future version.
3551
+
3552
+ ## Events and reconnect
3553
+
3554
+ `publish` receives event classes with different guarantees:
3555
+
3556
+ - `assistant-delta` is transient and ordered by
3557
+ `(runId, runtimeEpoch, sequence)`;
3558
+ - `assistant-checkpoint` follows a successful checkpoint CAS;
3559
+ - `run-state` follows durable queue/acquire/interrupt transitions;
3560
+ - `tool-status` is transient lifecycle presentation with JSON-safe input on
3561
+ start and output on completion; internal tool failures remain generic;
3562
+ - `terminal` follows the winning terminal CAS.
3563
+
3564
+ A named custom stop condition terminalizes with `policy_stop`; its `policyName`
3565
+ is persisted on the run and included in the terminal event/result. `max-steps`
3566
+ is the reserved built-in policy name. `loop.prepareStep` is the controlled AI
3567
+ SDK step boundary for changing active tools, model, instructions or messages.
3568
+ It cannot replace the managed tool set or bypass its lifecycle fence.
3569
+
3570
+ `loop.idleTimeoutMs` is inactivity, not total duration: the deadline resets on
3571
+ every model stream event. A stalled call aborts with durable reason `timeout`;
3572
+ user interruption and shutdown remain distinct.
3573
+
3574
+ Reconnect loads the durable snapshot. Missing transient deltas do not mean the
3575
+ canonical result was lost. Exactly-once external delivery requires the
3576
+ application's transactional outbox or stable-ID deduplication.
3577
+
3578
+ ## Managed tools
3579
+
3580
+ Always compose `toolFenceLifecycle` into `mountAgent`. It checks ownership
3581
+ before a managed side effect and again before accepting its result. Fence loss
3582
+ uses an internal control signal: it stops the old loop and is not sent to the
3583
+ model as a tool error.
3584
+
3585
+ The framework cannot undo an already-started non-cooperative external effect.
3586
+ Pass the stable call/run idempotency identity into business mutations when the
3587
+ effect must be replay-safe.
3588
+
3589
+ ## History, provider metadata and files
3590
+
3591
+ `projectAgentHistory` converts canonical engine records into provider-valid AI
3592
+ SDK messages and pairs tool calls/results. Provider-required metadata is kept
3593
+ in a versioned opaque envelope and omitted from product delivery by default.
3594
+
3595
+ `ComposedAgentPrompt.instructions` accepts the AI SDK `Instructions` contract.
3596
+ Use `adaptInstructions` when a provider needs metadata on the system message:
3597
+
3598
+ ```ts
3599
+ prompt({
3600
+ context,
3601
+ signal,
3602
+ adaptInstructions: (content) => ({
3603
+ role: 'system',
3604
+ content,
3605
+ providerOptions: { openrouter: { cacheControl: { type: 'ephemeral' } } },
3606
+ }),
3607
+ })
3608
+ ```
3609
+
3610
+ Attachments and generated files remain application-owned. The runtime only
3611
+ stores a reference returned by `persistGeneratedFile`; it never silently puts
3612
+ base64 blobs into the neutral history store.
3613
+
3614
+ To send a stored attachment back to a multimodal model, configure
3615
+ `history.resolveFile`. It maps the neutral file reference to AI SDK file data
3616
+ (URL, bytes, provider reference or text). Without a resolver the explicit
3617
+ `unresolvedFile` policy is `text` by default; choose `omit` or `error` when a
3618
+ placeholder would be incorrect.
3619
+
3620
+ ## Compaction
3621
+
3622
+ `structuredCompaction` summarizes complete provider-valid turn groups outside
3623
+ the store lock, then calls `replaceCompactedRange` against the exact snapshot
3624
+ version. A concurrent input produces `conflict`; the stale summary is not
3625
+ applied. Summary records use the dedicated `summary` role and project to a
3626
+ provider system message. The consumer supplies the structured summary schema
3627
+ and prompt. Pass `previousSummary` for a direct call, or configure
3628
+ `readPreviousSummary` for runtime-managed compaction, to merge and atomically
3629
+ replace a leading summary on the next compaction.
3630
+
3631
+ ## Observability
3632
+
3633
+ `createAgentObservability` emits a separate operator-only `AgentRunEvent`. It
3634
+ reuses the same bounded sink lifecycle as request/tool observability without
3635
+ sending new event kinds to existing request sinks. Product events omit provider
3636
+ causes; the operator terminal event may include `internalCause`, so its sink
3637
+ must use internal retention and redaction policy.
3638
+
3639
+ Usage values carry `provider-reported`, `computed`, `estimated` or
3640
+ `unavailable` provenance. Cost additionally carries an ISO currency code;
3641
+ OpenRouter-reported cost is normalized as USD. Missing values remain absent,
3642
+ never zero-filled.
3643
+
3644
+ In-memory sink delivery is at-most-once per execution. Stable event IDs allow
3645
+ consumer dedupe; cross-crash exactly-once requires a durable outbox.
3646
+
3647
+
3343
3648
  ==============================================================================
3344
3649
  # Guide: CLI (docs/guide/cli.md)
3345
3650
  ==============================================================================
@@ -4699,22 +5004,36 @@ error round-trips without adding correlation data to the response body.
4699
5004
  ### Stitch codes vs your codes
4700
5005
 
4701
5006
  A `code` is a free string — your app codes (`BOT_NOT_FOUND`, …) are yours and the
4702
- core never models them (ADR 0002). But stitchkit itself emits a fixed set:
5007
+ core never models them (ADR 0002). But stitchkit itself emits a set of its own:
4703
5008
  `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `METHOD_NOT_ALLOWED`,
4704
- `CONFLICT`, `RATE_LIMITED`, `VALIDATION_ERROR`, `INTERNAL_SERVER_ERROR`. They are
5009
+ `CONFLICT`, `RATE_LIMITED`, `VALIDATION_ERROR`, `FILE_INVALID_PATH`,
5010
+ `FILE_OUTSIDE_ROOT`, `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`,
5011
+ `FILE_INSPECTION_REJECTED`, `FILE_TOO_LARGE`, `FILE_EXISTS`,
5012
+ `REALTIME_CONTRACT_VIOLATION`, `INTERNAL_SERVER_ERROR` — a set that grows in
5013
+ ordinary releases, each addition named in the changelog. They are
4705
5014
  published as **`STITCH_ERROR_STATUS`** (the `code → status` map) and
4706
5015
  **`StitchErrorCode`** (its `keyof`), with **`isStitchErrorCode()`** (→ ADR 0026).
4707
5016
 
4708
5017
  If you translate stitch's framework errors into your own wire codes in an
4709
- `onError` hook, key the map by `StitchErrorCode` so it stays exhaustive a code
4710
- stitch adds or renames becomes a compile error, not a silent `500`:
5018
+ `onError` hook, you choose how the set's growth reaches you. Keying the map by
5019
+ `StitchErrorCode` makes it exhaustive, so a code stitch adds or renames becomes
5020
+ a compile error rather than an unfamiliar code on your wire — worth it when the
5021
+ envelope is a published contract, at the cost of an edit on those releases:
4711
5022
 
4712
5023
  ```ts
5024
+ // Exhaustive on purpose: the annotation is what turns a new stitch code into a
5025
+ // compile error here. Drop it (or use `satisfies` on a partial map) to let an
5026
+ // unmapped code travel as itself instead.
4713
5027
  const STITCH_TO_APP: Record<StitchErrorCode, AppCode> = {
4714
5028
  NOT_FOUND: 'NOT_FOUND', METHOD_NOT_ALLOWED: 'METHOD_NOT_ALLOWED',
4715
5029
  BAD_REQUEST: 'VALIDATION_ERROR', VALIDATION_ERROR: 'VALIDATION_ERROR',
4716
5030
  UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', CONFLICT: 'CONFLICT',
4717
5031
  RATE_LIMITED: 'RATE_LIMITED', INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
5032
+ REALTIME_CONTRACT_VIOLATION: 'INTERNAL_SERVER_ERROR',
5033
+ FILE_INVALID_PATH: 'VALIDATION_ERROR', FILE_OUTSIDE_ROOT: 'VALIDATION_ERROR',
5034
+ FILE_NOT_FOUND: 'NOT_FOUND', FILE_NOT_REGULAR: 'VALIDATION_ERROR',
5035
+ FILE_INSPECTION_REJECTED: 'VALIDATION_ERROR', FILE_TOO_LARGE: 'VALIDATION_ERROR',
5036
+ FILE_EXISTS: 'CONFLICT',
4718
5037
  }
4719
5038
  onError: (ctx, err) => {
4720
5039
  if (AppError.is(err) && isStitchErrorCode(err.code)) {
@@ -4790,20 +5109,20 @@ codes and schemas remain application-owned; Stitchkit stays domain-free.
4790
5109
 
4791
5110
  ## `createErrorHook`
4792
5111
 
4793
- `createErrorHook` is the code-map above, packaged — you supply the exhaustive
4794
- `codeMap` and the envelope shape, it does the normalisation (including the
5112
+ `createErrorHook` is the code-map above, packaged — you supply a `codeMap` and
5113
+ the envelope shape, it does the normalisation (including the
4795
5114
  never-leak-an-internal-message rule for a raw throw):
4796
5115
 
4797
5116
  ```ts
4798
5117
  const onError = createErrorHook({
5118
+ // Map the codes you have an opinion about; the rest travel as themselves.
4799
5119
  codeMap: {
4800
5120
  BAD_REQUEST: 'bad_request', VALIDATION_ERROR: 'bad_request',
4801
5121
  UNAUTHORIZED: 'unauthenticated', FORBIDDEN: 'forbidden',
4802
5122
  NOT_FOUND: 'not_found', METHOD_NOT_ALLOWED: 'not_found',
4803
5123
  CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
4804
5124
  INTERNAL_SERVER_ERROR: 'internal',
4805
- REALTIME_CONTRACT_VIOLATION: 'internal',
4806
- } satisfies Record<StitchErrorCode, string>,
5125
+ },
4807
5126
  // `ctx` is the request's RuntimeContext — read `ctx.traceId` for a
4808
5127
  // correlation id in the envelope. Declaring it is optional.
4809
5128
  render: (info, ctx) => ({
@@ -4816,8 +5135,24 @@ const onError = createErrorHook({
4816
5135
  createServer({ services, hooks: { onError } })
4817
5136
  ```
4818
5137
 
4819
- Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
4820
- `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
5138
+ `codeMap` is partial: map the codes you have an opinion about. A stitchkit code
5139
+ you did not list travels as itself — the same thing a code you threw yourself
5140
+ always did. That is what keeps a code added by a future release from being a
5141
+ compile break for every project that translates codes at all.
5142
+
5143
+ One `satisfies` is the **opt-in** to the stricter deal:
5144
+
5145
+ ```ts
5146
+ codeMap: { /* … every code … */ } satisfies Record<StitchErrorCode, string>,
5147
+ ```
5148
+
5149
+ That makes the map exhaustive on your side, so a release that adds a code stops
5150
+ your build until you decide what the new code is called on your wire. Take it
5151
+ when your envelope is a published contract and a code surfacing in stitchkit's
5152
+ spelling would violate it; leave it off when passing one through is fine.
5153
+ Neither choice is silent — the changelog names every added code. For a single
5154
+ catch-all instead of either, decide it in `render`, where `info.code` is the
5155
+ resolved value.
4821
5156
 
4822
5157
  Both `onError` and `render` may be asynchronous and receive the matched endpoint
4823
5158
  as their final argument. The observer is awaited before rendering, so it can
@@ -7582,6 +7917,40 @@ Also re-exports the error helpers from `stitchkit/contract`.
7582
7917
 
7583
7918
  ---
7584
7919
 
7920
+ ## `stitchkit/agent-runtime`
7921
+
7922
+ Server-only optional application runtime. See the
7923
+ [agent runtime guide](../guide/agent-runtime.md).
7924
+
7925
+ | Export | Kind | Summary |
7926
+ |--------|------|---------|
7927
+ | `createAgentRuntime` | function | compose durable acceptance, stream loop, checkpoints, coordination, managed tools and terminal publication |
7928
+ | `defineAgentProtocol` | function | declare and validate context, input metadata and canonical message parts |
7929
+ | `AgentMessageSchema` / `AgentRunSchema` / `AgentSnapshotSchema` | schema | versioned canonical engine records |
7930
+ | `AgentRuntimeStore` | _type_ | aggregate CAS transaction boundary for message, run and compaction mutations |
7931
+ | `RecoverAgentRunSchema` | schema | explicit abandon/requeue recovery decision; acquired runs require replay-safe evidence |
7932
+ | `createMemoryAgentRuntimeStore` | function | process-local reference adapter, not production durability |
7933
+ | `projectAgentHistory` | function | asynchronously project canonical records and resolved multimodal files into provider-valid AI SDK messages |
7934
+ | `defineModelRegistry` | function | typed language-model descriptors, capabilities and provider construction |
7935
+ | `composeAgentPrompt` | function | ordered prompt contributions and provenance-aware context budget |
7936
+ | `structuredCompaction` | function | summarize a provider-valid snapshot range and replace it through CAS |
7937
+ | `createAgentSessionCoordinator` | function | strict process-local queue/interrupt lifecycle |
7938
+ | `AgentRuntimeStopPolicy` | _type_ | named custom AI SDK stop condition persisted and published on policy stop |
7939
+ | `AgentRuntimePrepareStep` | _type_ | per-run controlled step callback with typed domain context and managed run signal/fence |
7940
+ | `AgentRuntimeRecordIds` | _type_ | optional caller-provided input, run and assistant IDs for stable application records |
7941
+ | `AgentRuntimeAdmission` | _type_ | actual assigned run/assistant identity and snapshot version after durable admission |
7942
+ | `AgentHistoryProjectionOptions` | _type_ | storage-neutral file resolver and explicit unresolved-file behavior |
7943
+ | `createAgentToolFenceLifecycle` | function | pre-effect and post-effect run ownership fence for `mountAgent` |
7944
+ | `AgentRuntimeEventSchema` | schema | transient delta, durable checkpoint/run-state/tool and terminal event union |
7945
+ | `createAgentObservability` | function | separate agent-run sink over the shared bounded observability lifecycle |
7946
+
7947
+ ## `stitchkit/agent-runtime/openrouter`
7948
+
7949
+ | Export | Kind | Summary |
7950
+ |--------|------|---------|
7951
+ | `openRouterProvider` | function | isolated `@openrouter/ai-sdk-provider` language-model factory |
7952
+ | `OpenRouterProviderSettings` | _type_ | official provider settings accepted by the factory |
7953
+
7585
7954
  ## `stitchkit/observability`
7586
7955
 
7587
7956
  Server-only. The audit layer one level above the raw hooks — W3C trace context,
@@ -7755,6 +8124,9 @@ payload.
7755
8124
  | `Toolkit` | _type_ | the context-pinned tool surface from `createToolkit` |
7756
8125
  | `ToolExtend` | _type_ | extra-args extension for `mountMcp` / `mountAgent` |
7757
8126
  | `ToolLifecycle` | _type_ | `beforeHandle` / `afterHandle` gate for tool calls — [guide](../guide/mcp-and-agents.md#guarding-tools--lifecycle) |
8127
+ | `ToolExecutionControlReason` | _type_ | internal managed-execution stop reason: stale run or requested interruption |
8128
+ | `ToolExecutionControlError` | class | control-flow error used to unwind a superseded managed tool call without presenting it as a model-facing tool failure |
8129
+ | `isToolExecutionControlError` | function | narrow an unknown thrown value to the managed execution control error |
7758
8130
  | `ToolOperation` | _type_ | executable path-free operation shape shared by contract and framework-native runners |
7759
8131
  | `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)) |
7760
8132
  | `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.1",
3
+ "version": "0.56.3",
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",