stitchkit 0.122.1 → 0.122.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.
@@ -1,4 +1,4 @@
1
- # stitchkit/agent-runtime — stitchkit docs slice, part 1 of 4
1
+ # stitchkit/agent-runtime — stitchkit docs slice, part 1 of 5
2
2
 
3
3
  > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
4
  > `llms.txt` in this package.
@@ -7,8 +7,9 @@
7
7
  Parts of this slice:
8
8
  - `llms/agent-runtime.txt`: Guide: Agent application runtime (docs/guide/agent-runtime.md)
9
9
  - `llms/agent-runtime.2.txt`: Guide: Agent application runtime (docs/guide/agent-runtime.md)
10
- - `llms/agent-runtime.3.txt`: Guide: Agent application runtime (docs/guide/agent-runtime.md) · API reference: `stitchkit/agent-runtime` (docs/api/reference.md)
11
- - `llms/agent-runtime.4.txt`: API reference: `stitchkit-tui` (docs/api/reference.md)
10
+ - `llms/agent-runtime.3.txt`: Guide: Agent application runtime (docs/guide/agent-runtime.md)
11
+ - `llms/agent-runtime.4.txt`: Guide: Agent application runtime (docs/guide/agent-runtime.md) · API reference: `stitchkit/agent-runtime` (docs/api/reference.md)
12
+ - `llms/agent-runtime.5.txt`: API reference: `stitchkit-tui` (docs/api/reference.md)
12
13
 
13
14
  ==============================================================================
14
15
  # Guide: Agent application runtime (docs/guide/agent-runtime.md)
@@ -168,655 +169,3 @@ A stop policy's `when` receives the steps so far together with the same run cont
168
169
  typed by the runtime's `CONTEXT`. A rule that depends on its own run (a receipt bound to the
169
170
  run's request, a per-tenant limit) reads it there; it needs no map of its own keyed by `runId`.
170
171
  The first policy to return `true` ends the run as `policy_stop` with its `name` as `policyName`.
171
-
172
- ## Bounded deferred tool catalogs
173
-
174
- Use `createDeferredAgentToolSurface` when a measured large catalog makes
175
- sending every schema on every provider step expensive. The controller mounts
176
- the complete executable surface in process, but initially advertises only its
177
- search tool, explicit `alwaysOn` tools and bounded policy pins:
178
-
179
- ```ts
180
- import { createDeferredAgentToolSurface } from 'stitchkit/agent-runtime'
181
- import { composeToolLifecycle } from 'stitchkit/tools'
182
-
183
- const deferred = createDeferredAgentToolSurface({
184
- surfaces: {
185
- member: { services: memberServices, runtimeTools, alwaysOn: ['ask_user'] },
186
- broadcast: { services: broadcastServices, runtimeTools },
187
- },
188
- selectSurface: ({ context }) => context.mode,
189
- pins: ({ context }) => context.skillTools,
190
- search: {
191
- name: 'tool_search',
192
- maxQueryBytes: 1_024,
193
- maxResults: 8,
194
- maxResultBytes: 8_192,
195
- select: ({ query, manifest }) => rankCatalog(query, manifest),
196
- },
197
- activation: {
198
- maxSelectedTools: 8,
199
- maxActiveTools: 16,
200
- maxSchemaBytes: 32_768,
201
- },
202
- observe: event => recordDeferredToolEvidence(event),
203
- })
204
-
205
- const runtime = createAgentRuntime({
206
- // protocol, store, models and prompt...
207
- tools: runContext => deferred.mount(runContext, {
208
- context: runContext.context,
209
- lifecycle: composeToolLifecycle(authLifecycle, runContext.toolFenceLifecycle),
210
- }),
211
- loop: { prepareStep: deferred.prepareStep(applicationPrepareStep) },
212
- })
213
- ```
214
-
215
- The built-in selector deterministically ranks exact name, prefix/token and
216
- description matches. A custom async selector returns names only; unknown and
217
- duplicate output is removed against the local canonical manifest. Search never
218
- executes the selected operation and never returns its schema. A later provider
219
- step receives the real selected tools by name, so their direct lifecycle,
220
- fencing, hooks, errors and `present.agent` output remain unchanged.
221
-
222
- Selection is a versioned receipt in the ordinary durable tool-result history.
223
- The latest valid same-run search replaces the earlier selection; parallel
224
- searches in one step merge in call order. Recovery reconstructs it, while a
225
- different run or finite surface cannot inherit it. Known inactive calls take a
226
- recoverable `SEARCH_REQUIRED` search round; unknown names still fail.
227
-
228
- Every ceiling is required. `maxSchemaBytes` measures UTF-8 bytes over canonical
229
- name, description and presentation schema. It is useful for before/after
230
- comparison, but provider serialization and tokenization differ: use existing
231
- provider-reported input usage and cost to decide whether the extra search round
232
- is beneficial. Controller evidence intentionally omits query text, prompts,
233
- arguments and application context. → ADR 0129.
234
-
235
- ### Published headless harness and structured runner
236
-
237
- `stitchkit/agent-runtime/harness` publishes the resource-aware composition over
238
- `createAgentRuntime`. `createHeadlessAgentHarness` accepts the existing protocol and store,
239
- caller-provided model resolution, bounded instruction/skill/resource loading, direct tools and a
240
- prompt-budget callback. Resource names are unique; count, UTF-8 bytes and diagnostics are bounded.
241
- Invalid resources fail before a provider request. Diagnostics are evidence for the caller: their
242
- observer is isolated, and the loader decides whether a reported condition is fatal.
243
-
244
- `createAgentHarnessFileResources` is the optional filesystem implementation. It reads only
245
- caller-declared absolute roots, refuses symlinks and reports provenance as
246
- `rootId:relative/path`. Instructions enter the prompt immediately. Skills and ordinary resources
247
- enter as bounded name/description summaries; `read_resource` loads one exact body later as
248
- a normal direct typed tool. Custom loaders remain supported unchanged.
249
-
250
- Each run may resolve a different provider adapter without changing the harness. The
251
- `profile-applied` observation records the actual `AgentModelDescriptor`, resource kind/name/
252
- provenance and sorted direct tool names; it intentionally contains no resource text, prompts,
253
- arguments or credentials. Observer failure never changes the canonical run outcome.
254
-
255
- [`packages/core/examples/agent-runtime/headless-agent-runner.ts`](../../packages/core/examples/agent-runtime/headless-agent-runner.ts)
256
- is a reference structured control loop for `submit`, `interrupt`, `snapshot` and bounded `close`.
257
- It starts no process and chooses no framing, authentication or transport. Importing either example
258
- has no side effect; an external supervisor owns process placement and restart.
259
-
260
- | Concern | Public Stitchkit composition | Boundary |
261
- | --- | --- | --- |
262
- | execution loop | `createAgentRuntime` | already available; do not copy a second loop |
263
- | model choice | `defineModelRegistry` and `models.resolve` | provider credentials/discovery stay application-owned |
264
- | resources | `createHeadlessAgentHarness` → `composeAgentPrompt` | paths, trust, precedence and watching stay application-owned |
265
- | tools | `mountAgent` + `createAgentToolFenceLifecycle` | auth and domain effects stay application-owned |
266
- | follow-up | `runs.inputPolicy: 'queue'` | durable FIFO after the current run |
267
- | interrupt | `interrupt` or `interrupt-next` | `interrupt-next` terminates the active run and gives the new durable admission next priority |
268
- | recovery | `runtime.recover` + `scanRecoverable` | replay safety and context reconstruction are explicit callbacks |
269
- | reconnect | canonical snapshot + `advanceAgentMultiSessionCursor` | conversation/run-scoped gaps require resync |
270
- | persistence | memory reference, public driver, or SQLite leaf | product rows/outbox remain outside the runtime store |
271
- | executable lifecycle | structured runner example | framing, auth, process placement, restart and OS isolation stay host-owned |
272
-
273
- `createAgentHarnessControlServer` adds transport-neutral correlated requests, shared observers and
274
- one exclusive controller lease per conversation. `close()` on a connection detaches only that
275
- client; it never closes the harness. Normal `event` delivery stays serialized. A bounded slow
276
- consumer is detached and receives `resync-required` through the required synchronous
277
- `onOverflow` callback; that callback closes/aborts the underlying transport, which then reconnects
278
- and requests a fresh snapshot. It never writes a second frame concurrently with a blocked
279
- `deliver`. Browser code uses `createAgentControlView`,
280
- `reduceAgentControlSnapshot` and `reduceAgentControlEvent`; the reducer owns no renderer and marks a
281
- conversation `resyncRequired` after a transient sequence gap.
282
-
283
- Configure `loop.toolApproval` to use the installed AI SDK approval policy and
284
- `loop.toolApprovalSecret` to sign exact requests. A request is stored on the terminal assistant
285
- before any effect. `pendingApprovals()` derives unresolved requests from canonical messages;
286
- `respondToApproval()` writes one tool-role approval response and queues a successor. The SDK
287
- revalidates the signature/tool call/input before the original direct tool reaches its existing
288
- fence. Reconnect and SQLite reopen require no promise registry. Remembered policy and a stronger
289
- cross-crash exactly-once guarantee remain application concerns.
290
-
291
- Approval requests suspend a call; they do not settle its result. History carries exact call and
292
- approval identity across assistant/tool records, so an approved result can precede the next signed
293
- request in a successor run. Automatic decisions follow the same chronology. Unknown/duplicate
294
- responses and results with a different call or tool name are invalid; dropping an invalid active
295
- approval input fails the run with a private diagnostic rather than starting a fresh model turn.
296
-
297
- `stitchkit/agent-runtime/coding-tools` returns ordinary direct runtime tools named `read_file`,
298
- `write_file`, `edit_file`, `list_directory`, `glob`, `search_files`, `run_command` and optional
299
- `read_output`. Every call passes a required host authorization callback. An optional async/sync
300
- `authorizePath({ path })` is the one operation-independent policy for direct file access and
301
- discovery: direct read/write/edit refuses a denied path, while listing, glob and search omit it.
302
- Denial is recursive on every surface — direct access asks about each named ancestor and the file,
303
- outermost first, stopping at the first refusal; discovery rooted at `.` also asks about `.`.
304
- Thus denying `credentials` denies everything
305
- under it. The comparison is segment-wise, so it does not deny `credentials-backup`, and a path of
306
- depth N costs N questions for direct access. Directory decisions happen before descent; file decisions and
307
- `search_files.include` happen before opening content. Incompatible glob directory prefixes are
308
- pruned before policy or directory opens. Requested paths use `/`: a backslash is
309
- refused rather than normalised, because the workspace walk reads it as a separator and every
310
- authorization callback would otherwise be asked about a different decomposition than the one opened. File paths are relative, bounded and contained after
311
- descriptor-relative resolution: each ancestor is opened without following symlinks. Reads and edits
312
- stay pinned through authorization; `write_file` plans before it and opens the parent after, because
313
- the parent it creates does not exist yet. Reads revalidate the pinned file identity;
314
- writes and patches revalidate the pinned parent identity; search and resource discovery descend
315
- only through opened directories. Every filesystem tool selects its root capability before operation or path authorization and retains
316
- that identity if a callback replaces the root path or configured alias. On Linux the ordinary
317
- command launcher spawns the command with the pinned directory's `/proc` descriptor link as its cwd;
318
- on macOS it inherits the pinned cwd through an isolated guard — one short-lived guard process per
319
- command, tens of milliseconds, outside `shellTimeoutMs`, which counts from the command's start. The
320
- application process never changes cwd. Where
321
- contained descriptors are unavailable (Windows), `run_command` accepts a cwd whose realpath is
322
- that same name under the root — a symlink segment refuses there too — and checks the directory's
323
- identity again immediately before launch, after every host decision.
324
- A custom sandbox launcher owns atomic cwd capture at its pathname handoff; the SDK refuses a
325
- changed identity after policy/prepare and immediately before that handoff. A path that changed
326
- during direct-access authorization refuses with `CONFLICT`.
327
- A path spelled differently from the name stored on disk refuses before any policy reads it, so on a
328
- case-insensitive volume a rule on `credentials` cannot be passed by asking for `CREDENTIALS`.
329
- NUL bytes are refused before either host policy sees a path.
330
- Reads and edits open file candidates without blocking on special files, then require a regular
331
- file. A writerless FIFO therefore refuses instead of occupying a filesystem worker. `write_file`
332
- supplies the exact missing-directory plan as `createsDirectories` to host authorization; the
333
- descriptor walk may create only those entries. An ancestor that disappears after approval refuses
334
- with `CONFLICT`, without expanding that plan. Linux traverses `/proc/self/fd`; macOS uses the package's
335
- architecture-specific Node-API `openat` backend. Unsupported platforms, including FreeBSD until a
336
- real backend is qualified, fail closed. The package needs no install-time compiler or script.
337
- Other platforms fail these filesystem operations closed because Node exposes no equivalent
338
- portable directory-handle-relative API. Writes/edits reject symlink targets, content is strict
339
- UTF-8 and retained bytes are finite. Shell accepts a finite alias mapped by the host to an absolute executable plus an
340
- argument array — never a shell command string — and uses only the explicitly supplied environment.
341
- Arguments, output and time are bounded, while cancellation terminates the child. The configured
342
- root and cwd are path boundaries, not a security sandbox: isolate the process when an executable
343
- must not access the rest of the machine. Path admission does not constrain `run_command`; an
344
- executable requires process isolation for that guarantee. → ADR 0172
345
-
346
- An absolute path is still refused. When it is inside the canonical workspace root, the refusal
347
- includes the exact workspace-relative `recoveryPath`; an absolute path outside the root has no
348
- suggested replacement. Applying the suggestion still passes the same descriptor-relative
349
- containment and symlink checks.
350
-
351
- `edit_file` replaces one exact snippet. It inserts `newText` literally in both single and
352
- `replaceAll` modes, including JavaScript replacement tokens such as `$$`, `$&`, `` $` `` and `$'`.
353
- `oldText` is itself the freshness guard for the region it changes, so the digest is the optional `expectedSha256` and an edit is one call; pass the digest
354
- `read_file` returned when you want the whole-file guarantee, and read the `sha256` it returns to
355
- chain the next edit without re-reading. The read, the occurrence count and the construction of the
356
- new content all happen inside one per-target lock, so two concurrent edits of different snippets in
357
- one file cannot each build a file from the same base and have the second erase the first — the lock
358
- is process-local, which covers many agents in one process and not two processes over one workspace.
359
- It authorizes the exact replacement count, result digest and byte size, and deliberately does not
360
- claim multi-file atomicity.
361
-
362
- `write_file` creates missing parent directories inside the root and names them in
363
- `createdDirectories`; the walk that finds them runs before authorization and reports them in the
364
- authorization payload, so no directory is created before a host approves it.
365
-
366
- **Every ordinary outcome is a refusal a model can act on** — a missing file is `NOT_FOUND`, an
367
- existing file without `overwrite` is `CONFLICT`, an ambiguous snippet is `CONFLICT` carrying its
368
- occurrence count, a path outside the root is `FORBIDDEN`, an unknown spilled-output reference is
369
- `SPILL_REFERENCE_UNKNOWN` — each with a `hint` naming the next move.
370
- Host-level causes stay scrubbed to `INTERNAL_SERVER_ERROR` and name nothing outside the workspace.
371
- Every refusal code is a member of `STITCH_ERROR_STATUS`, so its status survives an envelope that
372
- crosses a process boundary and is rebuilt from the wire.
373
- → ADR 0139
374
-
375
- `list_directory` marks excluded directories rather than hiding them, and `glob` reports
376
- `skippedDirectories` beside its matches: an empty result from a tree whose files all live under an
377
- excluded directory is not "no files", and a model told only "nothing found" concludes the wrong
378
- thing. `search_files` takes `regex`, `context` lines and an anchored `include` pattern matched
379
- against the whole workspace-relative path. `*` does not cross `/`, so files in subdirectories need
380
- `**/`; `scannedFiles` counts files admitted by both that filter and host authorization that reached
381
- comparison. One finite glob automaton matches files and prunes incompatible directory prefixes;
382
- `glob` also confines its walk to the requested path. A `hint` can explain include filtering or
383
- pruning, without counting files below unopened directories.
384
-
385
- Search processes one file at a time. List, glob and search share directory, entry, serialized-output,
386
- timeout and cancellation budgets, including canonical spelling checks before path authorization.
387
- `list_directory` caps raw enumeration at the smaller of `maxListEntries` and `maxSearchEntries`;
388
- its coverage scope lists symlinks while glob/search skip them. Glob patterns are limited to 4,096
389
- UTF-16 units. A scoped glob measures depth from its requested base directory. In addition to `maxSearchFiles` (10,000), `maxSearchDepth`
390
- (32), `maxSearchResults` (100) and `maxReadBytes` (262,144), hosts can configure
391
- `maxSearchDirectories` (1,000 directory-open attempts, root included), `maxSearchEntries`
392
- (20,000 entries observed by bounded readers), `maxSearchBytes` (16,777,216 aggregate content
393
- bytes, including rejected UTF-8), `maxSearchOutputBytes` (65,536 bytes for the entire serialized
394
- UTF-8 JSON, minimum 1,024) and `searchTimeoutMs` (5,000). An input whose minimum coverage
395
- envelope cannot fit the output ceiling refuses before filesystem access. Directory readers cap
396
- enumeration before materialization and sorting; reaching an entry or result ceiling is
397
- conservatively partial, without reading a lookahead entry or match.
398
-
399
- `list_directory`, `glob` and `search_files` return `coverage.complete` and `coverage.reasons`
400
- to distinguish full empty results from partial work. Glob `resultTruncated` conservatively marks
401
- the result ceiling even when no extra match was read.
402
- Coverage names the include/exclusion/symlink scope, directory and entry counters, actual content
403
- bytes and elapsed milliseconds. Limits, cancellation, denial, unreadable files and oversized
404
- lines are explicit reasons. `errors` names only a workspace-relative path, operation and safe code;
405
- `errorsTruncated` and `deniedTruncated` report shortened diagnostics. Excluded directories and
406
- symlinks are outside the declared search scope. The caller's signal and deadline reach host
407
- authorization, file reads and traversal; CPU checkpoints and asynchronous races stop further work.
408
- Late opened handles are closed, while already-running kernel calls may finish later. Directory
409
- enumeration and glob matching have internal checkpoints; cancellation stops their producer loops.
410
- Host policy and visitor exceptions propagate to error hooks with their original cause, rather
411
- than becoming filesystem diagnostics. Diagnostic byte admission is linear and checks the deadline
412
- even for an empty result.
413
-
414
- Regex searches use JavaScript `RegExp` syntax without flags — groups, alternation, classes,
415
- anchors, `\b`, quantifiers and `(?ims-ims:…)` modifiers — with bounded cost per line. A line
416
- whose backtracking work (paths × longest path × line length × (number of groups + 1), every
417
- repetition capped at the line length beyond the iterations it requires) stays within 2^24 steps runs on the native engine, so ordinary `.*` searches over code
418
- lines keep native speed and the deadline is observed between lines; any other line runs on a
419
- cached linear automaton, so `(a+)+$` on a long line costs the same as `a+` and the deadline is
420
- observed inside the line. Backreferences and
421
- lookaround refuse before I/O; lines over 8,192 UTF-8 bytes are skipped with incomplete coverage.
422
- Literal queries allow up to 8,192 UTF-16 units and include patterns up to 4,096.
423
-
424
- With an optional `AgentCodingArtifactStore`, shell output beyond the inline
425
- preview continues into an opaque bounded artifact and `read_output` reads slices without
426
- exposing a host path. Without a store, the previous finite output-limit behavior is unchanged.
427
-
428
- `run_command` treats every invocation as finite. `shellTimeoutMs` bounds normal execution and
429
- `shellTerminationGraceMs` bounds settlement after timeout, caller cancellation, output overflow or
430
- parent exit. POSIX hosts launch a dedicated process group and kill every descendant remaining in
431
- that group; a process that deliberately escapes the group is outside the guarantee. Node's
432
- portable Windows child-process API cannot kill a complete descendant tree without invoking a
433
- second host executable, so Windows kills the direct child, destroys retained pipes at the grace
434
- deadline and makes no descendant-cleanup claim. A signal already aborted before execution returns
435
- `cancelled` without spawning.
436
-
437
- Coding commands compose the shared native execution owner. Artifact retention and UTF-8
438
- previews remain coding-tool policy; normal leader exit still forcibly stops remaining POSIX
439
- group members before draining. A structural sandbox launcher uses the same settlement owner
440
- with a finite output queue. Sandbox shutdown waits for actual owned completion and surfaces
441
- cleanup refusal rather than treating it as successful teardown.
442
-
443
- One `createAgentHarnessFileResources` instance represents one immutable discovery generation:
444
- concurrent and repeated `load()` calls share it, so a direct resource read cannot cross into a
445
- new catalog unexpectedly. Discovery uses the same descriptor-anchored traversal as coding search;
446
- construct a new loader to refresh after filesystem changes.
447
-
448
- Runtime events preserve reasoning/text/tool lifecycle order. Durable admission,
449
- checkpoint, run-state and terminal events carry stable identities; transient
450
- deltas carry one runtime epoch and monotonic sequence. On a transient gap,
451
- reload the canonical snapshot and resume from the durable record instead of
452
- persisting deltas as a second history.
453
-
454
- Prompt `contextWindow` budgeting is a pre-request decision over the assembled
455
- context. `AgentRun.usage` is cumulative measured/provider-reported execution
456
- evidence across attempts and compaction. They answer different questions and
457
- must not be substituted for each other. TTFT and throughput require timestamps
458
- from the actual provider stream plus a provider-reported or tokenizer-measured
459
- token count; character counts cannot produce exact token or throughput figures.
460
-
461
- `recordIds` is optional. Supply stable application record IDs when an accepted-response transport must
462
- return durable placeholders before the run finishes. `ticket.admission` resolves after the store
463
- acceptance CAS and reports the canonical committed `input`, assigned `run`, a typed `pending`
464
- assistant projection, compatibility IDs and snapshot version.
465
- Those assigned IDs can differ from the proposal when an input coalesces into an existing queued
466
- successor. Reuse the same `inputMessageId` for retries carrying the same idempotency key; input
467
- identity is caller-stable, while the receipt reports the run/assistant identities that assignment
468
- may change. Await `admission` first on the immediate accepted-response path. `ticket.accepted`
469
- remains the signal-only compatibility surface and additionally waits for admission publication.
470
-
471
- The in-memory store is a reference adapter and has process-local durability
472
- only. Production applications normally call `createAgentRuntimeStore()` and
473
- provide one database transaction driver. Its `transaction` owns the retry of a
474
- serialization conflict, wherever the database reports it: on a statement or on
475
- `COMMIT`, which some clients surface in a different shape. Pass
476
- `armCommitConflict` to `runAgentStoreConformance` to prove it:
477
-
478
- ```ts
479
- const store = createAgentRuntimeStore({
480
- transaction: (work, options) =>
481
- options?.access === 'read'
482
- ? db.readTransaction(tx => work(tx))
483
- : db.writeTransaction(tx => work(tx)),
484
- head: {
485
- load: (tx, conversationId) => loadRuntimeHead(tx, conversationId),
486
- compareAndSwap: (tx, operation) => casRuntimeHead(tx, operation),
487
- },
488
- runs: {
489
- load: (tx, identity) => loadRun(tx, identity),
490
- loadByAssistantMessageId: (tx, identity) => loadRunByAssistant(tx, identity),
491
- loadMany: (tx, identities) => loadRuns(tx, identities),
492
- listActive: (tx, conversationId) => listActiveRuns(tx, conversationId),
493
- save: (tx, record) => saveRun(tx, record),
494
- },
495
- admissions: {
496
- load: (tx, identity) => loadAdmission(tx, identity),
497
- loadByInputMessageId: (tx, identity) => loadAdmissionByInput(tx, identity),
498
- create: (tx, receipt) => createAdmission(tx, receipt),
499
- },
500
- history: {
501
- load: (tx, conversationId) => loadCanonicalMessages(tx, conversationId),
502
- apply: (tx, mutation) => applyCanonicalHistoryMutation(tx, mutation),
503
- },
504
- scanRecoverable: page => scanRecoverableRuns(page),
505
- })
506
- ```
507
-
508
- Prompt budgeting keeps reservation deficits signed. If instructions,
509
- `reservedOutput`, tool schemas, attachments and provider overhead already exceed
510
- `contextWindow`, `composeAgentPrompt` returns `contextDecision: 'oversized'`
511
- and a negative `availableHistoryTokens` even when history is empty. A `compact`
512
- policy is returned only when removing history could actually make the prompt
513
- fit. Exact equality (including a zero window with zero reservations/history)
514
- fits; any unavailable component keeps the decision `unavailable`.
515
-
516
- The same opaque `tx` reaches head, run, admission and history callbacks. The adapter maps rows
517
- and supplies atomicity; Stitchkit owns transition validation and revision
518
- arithmetic. The executable reference is
519
- [`examples/agent-store-prisma/adapter.ts`](../../examples/agent-store-prisma/adapter.ts).
520
- `compareAndSwap` returns either `{ outcome: 'applied' }` or
521
- `{ outcome: 'conflict', actualVersion }`. Decide that outcome by reading the
522
- version, comparing it and then writing unconditionally — inside the transaction
523
- the adapter supplies, that is atomic. A conditional upsert is the shorter thing
524
- to write and it is wrong twice: `ON CONFLICT ... WHERE` guards the update branch
525
- only, so the first write to a conversation applies whatever `expectedVersion` it
526
- was given, and the affected-row count that would report the outcome is the
527
- driver's, which need not be the rows the statement moved. The head contains only
528
- schema version, conversation identity and monotonic version. Runs and admission receipts are normalized records;
529
- recovery queries active run states directly instead of maintaining a second projection.
530
- An admission receipt retains its canonical input, and a terminal run retains its canonical
531
- assistant, so physical product-history compaction cannot break idempotent retries.
532
-
533
- ### Built-in SQLite persistence
534
-
535
- Use the runtime-specific leaf; the neutral agent runtime never imports either
536
- SQLite built-in:
537
-
538
- ```ts
539
- // Bun
540
- import { createBunSqliteAgentRuntimeStore } from 'stitchkit/agent-runtime/sqlite/bun'
541
-
542
- const sqlite = createBunSqliteAgentRuntimeStore({ filename: './agent-runtime.sqlite' })
543
- const runtime = createAgentRuntime({ ...config, store: sqlite.store })
544
-
545
- await runtime.close()
546
- await sqlite.close()
547
- ```
548
-
549
- ```ts
550
- // Node 22.5+
551
- import { createNodeSqliteAgentRuntimeStore } from 'stitchkit/agent-runtime/sqlite/node'
552
-
553
- const sqlite = createNodeSqliteAgentRuntimeStore({ filename: './agent-runtime.sqlite' })
554
- ```
555
-
556
- Initialization creates only `stitchkit_agent_runtime_*` tables and records
557
- schema version 2 in `stitchkit_agent_runtime_meta`; it does not use
558
- `PRAGMA user_version` or mutate application tables. An unknown schema version or
559
- unversioned partial Stitchkit schema is refused. The connection is owned by the
560
- returned handle and closes only after accepted operations drain.
561
-
562
- Each handle serializes its transactions. Separate connections use
563
- `busy_timeout = 0`: a competing synchronous writer fails promptly with
564
- SQLite's lock error, so an awaited transaction can resume and commit or roll
565
- back. Retry that explicit error with an application-owned bounded policy; do
566
- not configure a blocking busy timeout on the same JavaScript thread.
567
-
568
- Aggregate reads and catalog pages use `BEGIN`, so a WAL writer may remain active
569
- while the reader sees one committed snapshot. Mutations and purge use
570
- `BEGIN IMMEDIATE`: they reserve the writer slot before reading state for CAS and
571
- never attempt a deferred read-to-write upgrade. A custom driver may inspect the
572
- additive `transaction` option `{ access: 'read' }`; ignoring it preserves the
573
- previous write-safe behavior.
574
-
575
- The adapter proves canonical durability across reopen and process restart. It
576
- does not make external tool effects exactly once, join application projections
577
- atomically or replace an application outbox. If a product row must commit with
578
- an agent transition, implement `AgentRuntimeStoreDriver` over the application's
579
- own transaction boundary instead.
580
-
581
- Opening a version 1 store migrates it in one transaction, forward only — back
582
- the file up first, a 0.85.x package refuses a migrated file. Existing
583
- normalized state becomes one `runtime/baseline` event per conversation, dated
584
- when the migration ran and carrying `asOf` for the last message it describes;
585
- search and projections address that pre-migration history as one snapshot at
586
- `seq 1`. New transitions continue the monotonic conversation sequence. Version
587
- 2 requires SQLite FTS5 because event search is an advertised capability, so a
588
- build without FTS5 is a startup error (`requires FTS5 support`) rather than an
589
- empty search result; check with
590
- `SELECT sqlite_compileoption_used('ENABLE_FTS5')`.
591
-
592
- The handle returned by `createBunSqliteAgentRuntimeStore` /
593
- `createNodeSqliteAgentRuntimeStore` is `{ store, conversations, database,
594
- transaction, close }`. `database` is the shared connection and `transaction`
595
- is one write transaction in the store's own serialization: the SQLite
596
- companions — `createSqliteAgentProjectionStore`, `createSqliteAgentSpillStore`,
597
- `createSqliteAgentChildManager`, `createAgentScheduleService` — take `{ sqlite }`
598
- and write their rows and events through it, so a row and its event land
599
- together or not at all.
600
-
601
- `conversations.messages` pages the model's history: what compaction removed is
602
- not in it, which is the point of compaction. A person reading back their own
603
- conversation needs the other view, and asks for it explicitly:
604
-
605
- ```ts
606
- const page = await conversations.messages({
607
- conversationId,
608
- limit: 50,
609
- direction: 'after',
610
- includeCompacted: true,
611
- })
612
- const removed = new Set(page.compacted)
613
- ```
614
-
615
- `items` stays one sequence in the order it happened, and `compacted` names the
616
- ids inside it that the model no longer sees — the boundary is a mark on the
617
- conversation, not a second list. A compaction summary sits at the position of
618
- the last message it replaced, so the block reads through and the summary
619
- arrives after it, where it was written; in the model's history, where every
620
- replaced message is gone, it occupies the block's place either way. Without
621
- the flag the page is exactly what it was, `compacted`
622
- empty. The v1 → v2 migration baseline records the same thing: the whole
623
- sequence, with `compacted` naming what had been folded away. A file migrated
624
- by 0.86.0 has a baseline built from the active history only — the messages
625
- themselves are still in the store and this read reaches them, but that one
626
- event cannot be rewritten after the fact.
627
-
628
- ### Retried provider streams
629
-
630
- With `loop.retry` set, a provider stream that fails before any tool call in the
631
- attempt is retried at the step boundary. Subscribers see `attempt-reset` for the
632
- run before the next attempt's first delta and drop what the failed attempt
633
- streamed; the durable draft is checkpointed after that output is discarded; an
634
- input injected into the failed attempt is taken again by the retry; and the
635
- failed attempt's reported usage stays in the run's spend. Transient events —
636
- deltas, reasoning, `attempt-reset` — are numbered from 1 per run, counting only
637
- what was published, so a cursor sees no gap on an ordinary stream.
638
-
639
- ### Branch or restore with a new identity
640
-
641
- Use `store.branchConversation()` to create an independent conversation. `importConversation()`
642
- restores the original identity and is not a fork. The built-in memory and SQLite stores and
643
- `createAgentRuntimeStore()` expose branching; a hand-written `AgentRuntimeStore` can advertise this
644
- optional capability. The host authorizes the caller's source/archive access and destination
645
- creation before calling this trusted store boundary.
646
-
647
- ```ts
648
- const copy = await sqlite.store.branchConversation({
649
- requestKey: 'branch-request-1',
650
- destinationConversationId: 'new-conversation',
651
- source: { kind: 'conversation', conversationId: 'original', throughRunId: 'finished-run' },
652
- // Required when selected history has spills: an explicit policy for this destination.
653
- artifactAuthorization: { operation: 'read', path: 'approved-destination-workspace' },
654
- })
655
-
656
- const restored = await sqlite.store.branchConversation({
657
- requestKey: 'restore-request-1',
658
- destinationConversationId: 'restored-as-new',
659
- source: { kind: 'archive', bytes: archiveBytes },
660
- })
661
- ```
662
-
663
- `throughRunId` selects its exact terminal/abandon event. Omit it to include the settled tip and
664
- compaction recorded after the last run. The selected prefix must contain no queued or acquired
665
- runs. A later active source run does not prevent branching an earlier settled prefix. A migrated
666
- baseline has no earlier per-run event boundary; it can be copied at its settled tip.
667
-
668
- The result is `{ outcome: 'created' | 'duplicate', receipt }`. Keep the receipt: it binds source
669
- and destination identity, source kind, exact `sourceSeq`/`sourceVersion`, `sourceDigest`, request
670
- fingerprint and creation time. A retry with the same destination/key and selector returns the
671
- original receipt even after the source advances or the destination continues. Changing the
672
- request conflicts; no existing destination is overwritten. Archive fingerprints bind the supplied
673
- bytes. The source digest binds the selected canonical events, artifacts and reconstructed snapshot.
674
- Run update timestamps reconstructed from events use their recorded `occurredAt`. Abandon transitions
675
- retain the full failed assistant in the same event; a legacy archive must retain that exact assistant
676
- in its runtime snapshot, otherwise copying refuses instead of inventing historical content or time.
677
-
678
- No provider or tool runs during copying. The next ordinary runtime `submit` continues the copied
679
- history. All engine identities and typed references are new, including repeated references to one
680
- spill. Compacted content stays in the event history and SQLite inactive rows; the model reads the
681
- same active summaries. A hash-only checkpoint is historical digest evidence, marked
682
- `sourceSha256`; a later full terminal record must settle its content.
683
-
684
- Recorded provider options and canonical reasoning/tool-call provider metadata are retained.
685
- Provider message hashes and request references remain consistent after rekey. Metadata is
686
- data, subject to the opaque JSON reference guard: an undocumented runtime reference key
687
- refuses instead of carrying a source identity into the destination. Unknown message fields
688
- and malformed request/message digests also refuse before any destination writes.
689
-
690
- A branch grants no source authority. Terminal run owners/fences and approval signatures are
691
- removed. State, schedules, children, retries, sandbox, approval rejection and durability facts are
692
- retained as inert `runtime/branch-observation` records; they cannot repopulate operational state
693
- or pending effects. Spills discard source authorization and require the explicit fresh
694
- `artifactAuthorization`; normal destination read authorization still applies. No cached projection,
695
- schedule row or child relation is cloned. Ordinary companion projections in the export have
696
- `{ name, version, uptoSeq, value, updatedAt }` envelopes. Branch validates their envelopes,
697
- unique names and checkpoints within the supplied journal, then discards their values. They are
698
- derived application caches, including ones containing source references; their values are never
699
- replayed as state or copied as authority. Rebuild destination projections with
700
- `createSqliteAgentProjectionStore().advance(...)` over the destination ledger. An exact earlier
701
- boundary never inherits a later cache value.
702
-
703
- The store validates the complete archive before writing: contiguous events, a matching runtime
704
- snapshot, known typed forms, reference closure, and spill bytes/digests. Unsupported provider
705
- envelopes, encrypted invocation payloads, unknown event versions, malformed projection envelopes,
706
- missing artifacts and undocumented reference fields in opaque JSON refuse atomically. They need
707
- an owner-defined transfer codec; never recursively rewrite internal JSON in the application.
708
- Third-party drivers must provide atomic write transactions and archive support for spills;
709
- `history.restore` is required for migrated baselines with inactive rows.
710
-
711
- ### Event ledger, projections and durable capabilities
712
-
713
- `AgentRuntimeStore` exposes bounded `readEvents`, declared `appendEvent`, canonical
714
- `exportConversation` and empty-target `importConversation`. Runtime transitions, exact provider
715
- requests and completed provider response identities enter the same append-only ledger. A
716
- `provider/response` payload binds `response: { id, provider? }` to its `runId`, retry `attempt` and
717
- `stepNumber`; `step-finished.response` projects the same object to the operator sink. The optional
718
- upstream name comes from the model provider's own `resolveResponseProvider` — `openRouterProvider`
719
- supplies it — so the runtime carries the fact without learning any gateway's metadata key. The normalized
720
- head/run/message tables remain the fast operational projection.
721
-
722
- For `runtime/transition` payload inspection, use `AgentStoreTransitionRecordSchema` and its
723
- inferred `AgentStoreTransitionRecord` type. A checkpoint record may hold either the full assistant
724
- message or `{ id, status: 'streaming', sha256?, sourceSha256?, parts: number, bytes: number }`;
725
- exactly one digest is present. Numeric `parts` counts parts; it is not an array of message parts.
726
- `sourceSha256` preserves a branch's historical source digest. These hash-only records are
727
- observations, not executable checkpoint inputs. `AgentStoreTransitionSchema` describes full
728
- store mutations. Shape inspection does not replace branch's complete replay/reference validation.
729
- Schema v4 archives written by 0.109.4 use the same journal representation.
730
-
731
- Use `defineAgentProjection` with `createSqliteAgentProjectionStore` for a
732
- deterministic versioned fold. Every value reports `uptoSeq`, so a caller can
733
- distinguish current data from a projection that still has events to consume.
734
- Changing the projection version folds from sequence one.
735
-
736
- The built-ins are `agentSummaryProjection`, `agentUsageProjection` and
737
- `agentOutlineProjection`; focused card, state-slot and schedule projections are
738
- also available. `list(projection)` reads all materialized conversation rows in
739
- one SQLite query instead of reopening their event logs.
740
-
741
- Durable state is declared once and injected on every provider request:
742
-
743
- ```ts
744
- const goal = defineStateSlot({
745
- name: 'goal',
746
- schema: z.object({ objective: z.string(), status: z.string() }),
747
- })
748
-
749
- const slots = createAgentStateSlotStore({ store: sqlite.store, definitions: [goal] })
750
- await slots.set({
751
- conversationId,
752
- name: 'goal',
753
- value: { objective: 'finish migration', status: 'active' },
754
- actor: 'human',
755
- })
756
-
757
- const runtime = createAgentRuntime({ ...config, store: sqlite.store, stateSlots: [goal] })
758
- ```
759
-
760
- `createAgentStateTools` binds `create_goal`, `update_goal`, `get_goal` and
761
- `todo_write` to one conversation. Human changes call the same slot store with
762
- `actor: 'human'`; they remain structured `state/set` facts rather than forged
763
- chat messages.
764
-
765
- `createSqliteAgentSpillStore` persists oversized output separately and records
766
- its locator, hash and lifecycle in the ledger. `read_output` and
767
- `search_output` re-run both locator authorization and the originating tool
768
- authorization before reading bytes. A reference the store does not hold is a
769
- refusal like any other — `SPILL_REFERENCE_UNKNOWN` / 404, naming the reference
770
- and pointing at the one the spilling command returned. The lookup is scoped by
771
- conversation, so a reference belonging to another conversation is absent rather
772
- than forbidden, and the refusal cannot tell a model that it exists elsewhere. `createSqliteAgentEventSearch` returns the
773
- exact conversation and event sequence; cross-conversation results are denied
774
- unless `authorizeConversation` approves each target.
775
- `createAgentEventSearchTools` supplies `session_search`, `session_trace` and
776
- `session_event` over the same bounded APIs.
777
-
778
- For credential-free failure tests, import `createFaultProviderServer` and
779
- `createReplayAgentProvider` from `stitchkit/agent-runtime/testing`. Runtime
780
- `loop.retry` applies an explicit `AgentRetryPolicy`: each decision is recorded
781
- as `retry/scheduled`, each new attempt as `retry/started`, and an attempt that
782
- has invoked a tool is never automatically replayed.
783
-
784
- `createAgentScheduleService` persists `at`, `after` and `every` input. Repeating
785
- schedules require an explicit IANA time zone. Dispatch uses a stable
786
- `schedule:<id>:<occurrence>` idempotency key, and restart lateness is recorded.
787
- Resolve after durable admission; return `{ status: 'retry', reason }` for a
788
- transient refusal or `{ status: 'terminal', reason }` to stop the entire schedule in
789
- `failed` state. Only those two shapes are failures: any other returned value — a
790
- queue length, a receipt — means the callback returned, so it delivered. Throws retry
791
- too. `maxAttempts` (default 1000, about sixteen hours at the retry ceiling) bounds
792
- one occurrence: a one-off schedule then becomes `failed`, a recurring one gives up
793
- that occurrence (`schedule/failed` with `skipped: true` and the `nextAt` it moves
794
- to) and keeps its cadence. A failure reason is kept to 1000 characters. Backoff is 1, 2, 4, 8, 16, 32, then 60 seconds,
795
- capped at 60 seconds and persisted across restart. `retryAt`, `attempts` and `lastError`
796
- show retry diagnostics; success resets them. `nextAt` and `lateByMs` retain the original
797
- occurrence time. Recurring schedules fire once after an idle stretch and advance to the
798
- next future slot on their original cadence.
799
-
800
- Dispatch receives an `AbortSignal`, has a 30-second deadline and holds a unique
801
- 60-second lease. A late or cancelled attempt cannot settle or emit firing events.
802
- Honor the signal and durably deduplicate the key: a crash after admission but before
803
- settlement can replay delivery. This is at-least-once, not exactly-once.
804
- Due selection is indexed, limited to 32 parallel deliveries per tick; one hung batch
805
- can delay newly due work by at most its deadline. The service keeps one nearest-due
806
- timer plus per-attempt deadline timers. Storage failures use a bounded recovery timer;
807
- idle ticks do not write. SQLite v4 migration preserves due times and claims; stop older
808
- scheduler processes before migration. See [ADR 0202](../decisions/0202-schedule-retries-preserve-occurrence-identity.md).
809
- `createAgentScheduleTools` exposes the exact `schedule_after`, `schedule_at`,
810
- `schedule_every`, `schedule_list` and `schedule_cancel` surface.
811
-
812
- `createSqliteAgentChildManager` seeds a child from the parent's ledger through
813
- an exact sequence, stores the graph, clamps requested limits to a durable parent
814
- remainder and stops budget overruns at the next step boundary as
815
- `policy_stop`. That enforcement runs inside the child's own runtime: the host that spawns it gives the child
816
- `loop.stopPolicies: [agentChildBudgetStopPolicy({ manager, childConversationId })]`, which
817
- calls `recordStepUsage` at every step boundary; `recordStepUsage` alone measures and decides,
818
- and reports `enforced: false` when the deciding process holds no handle to stop. Given to `createAgentRuntime` as `children`, the manager stops the parent's children after the parent's terminal is durable — interrupted, cancelled, timed out or shut down; a successful, policy-stopped, superseded or failed parent leaves them to the conversation (ADR 0175 carries the table); without `children` nothing cascades; an unreachable child is recorded
819
- as `lost`, not misreported as a provider failure.
820
- `createAgentChildTools` adds `subagent`, `subagent_fork`, `list_agents`,
821
- `send_message` and `interrupt_agent`; final output stays behind its bounded
822
- result locator.