stitchkit 0.91.1 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/dist/agent-runtime-coding-tools.js +3 -3
- package/dist/agent-runtime-harness.js +6 -6
- package/dist/agent-runtime-sandbox.js +3 -3
- package/dist/agent-runtime.js +9 -9
- package/dist/cli.d.ts +6 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +371 -50
- package/dist/contract/define.d.ts +84 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/{index-qrbdq8jk.js → index-2c2bjw3s.js} +3 -3
- package/dist/{index-ya611vx3.js → index-7n145nbe.js} +183 -109
- package/dist/{index-bh70rk41.js → index-bx75zwt7.js} +3 -3
- package/dist/{index-709nh7z7.js → index-cxqhvq21.js} +2 -1
- package/dist/{index-13cjtmrw.js → index-e0znv5gn.js} +31 -8
- package/dist/{index-f4p4b8f6.js → index-e42wzzz5.js} +3 -3
- package/dist/{index-13e1k7v9.js → index-f477k430.js} +1 -1
- package/dist/{index-qjeyx5t2.js → index-fq77yfzc.js} +1 -1
- package/dist/{index-fenaekmk.js → index-h9etkkyk.js} +37 -27
- package/dist/{index-1qvdwnp2.js → index-mzvgxp0n.js} +2 -2
- package/dist/{index-zsqy3xfn.js → index-n0f51dfn.js} +4 -4
- package/dist/{index-bv7mytab.js → index-sr93cwf9.js} +1 -1
- package/dist/{index-vsbzgd7b.js → index-tmwcrptx.js} +1 -0
- package/dist/{index-b971qjes.js → index-v5573g1k.js} +1 -1
- package/dist/{index-8z9we758.js → index-vjpvjqpn.js} +1 -1
- package/dist/{index-bx6yq2m4.js → index-vk3kq9sb.js} +1 -1
- package/dist/{index-137jjs4v.js → index-yewwh1w4.js} +1 -1
- package/dist/{index-mqhjmadk.js → index-yn8remb1.js} +61 -57
- package/dist/index.js +4 -4
- package/dist/internal/atomic-file.d.ts +21 -0
- package/dist/internal/atomic-file.d.ts.map +1 -0
- package/dist/internal/errors.d.ts +10 -0
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/node.js +5 -5
- package/dist/observability/audit.d.ts +5 -1
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/changes.d.ts.map +1 -1
- package/dist/observability/context.d.ts +27 -6
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/event.d.ts +34 -6
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.d.ts +2 -1
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +51 -7
- package/dist/observability/work.d.ts +55 -0
- package/dist/observability/work.d.ts.map +1 -0
- package/dist/remote.js +3 -3
- package/dist/server/context-contribution.d.ts.map +1 -1
- package/dist/server/index.js +7 -7
- package/dist/server/types.d.ts +20 -6
- package/dist/server/types.d.ts.map +1 -1
- package/dist/testing/surface-manifest.d.ts.map +1 -1
- package/dist/testing.js +9 -7
- package/dist/tool-invoker.js +6 -6
- package/dist/tools/cli-checkpoint.d.ts +24 -0
- package/dist/tools/cli-checkpoint.d.ts.map +1 -0
- package/dist/tools/cli-format.d.ts +10 -0
- package/dist/tools/cli-format.d.ts.map +1 -1
- package/dist/tools/cli-invoke.d.ts +118 -0
- package/dist/tools/cli-invoke.d.ts.map +1 -0
- package/dist/tools/cli-manifest.d.ts +6 -9
- package/dist/tools/cli-manifest.d.ts.map +1 -1
- package/dist/tools/cli-signature.d.ts +81 -0
- package/dist/tools/cli-signature.d.ts.map +1 -0
- package/dist/tools/cli-stream.d.ts +93 -0
- package/dist/tools/cli-stream.d.ts.map +1 -0
- package/dist/tools/cli-update.d.ts +62 -0
- package/dist/tools/cli-update.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +4 -13
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/connections/index.js +2 -2
- package/dist/tools/execute.d.ts +21 -0
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/mcp-progress.d.ts +46 -0
- package/dist/tools/mcp-progress.d.ts.map +1 -0
- package/dist/tools/mcp-round-policy.d.ts +25 -1
- package/dist/tools/mcp-round-policy.d.ts.map +1 -1
- package/dist/tools/mcp-round.d.ts +20 -1
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mcp-trace.d.ts +9 -1
- package/dist/tools/mcp-trace.d.ts.map +1 -1
- package/dist/tools/mount-wait.d.ts.map +1 -1
- package/dist/tools.js +160 -51
- package/llms-full.txt +358 -8
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -66,11 +66,11 @@ own, recorded as an ADR.
|
|
|
66
66
|
| `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
|
|
67
67
|
| `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
|
|
68
68
|
| `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
|
|
69
|
-
| `stitchkit/observability` | server | stable<br>_redefined in
|
|
69
|
+
| `stitchkit/observability` | server | stable<br>_redefined in 2 of the 37 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
|
|
70
70
|
| `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
|
|
71
71
|
| `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
|
|
72
72
|
| `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
|
|
73
|
-
| `stitchkit/agent-runtime` | server | evolving<br>_redefined in 20 of the
|
|
73
|
+
| `stitchkit/agent-runtime` | server | evolving<br>_redefined in 20 of the 37 minors since 0.56.2, most recently 0.89.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
|
|
74
74
|
| `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
|
|
75
75
|
| `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults and deterministic race controls |
|
|
76
76
|
| `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
|
|
@@ -80,7 +80,7 @@ own, recorded as an ADR.
|
|
|
80
80
|
| `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
|
|
81
81
|
| `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
|
|
82
82
|
| `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
|
|
83
|
-
| `stitchkit/application` | browser + server | evolving<br>_redefined in 7 of the
|
|
83
|
+
| `stitchkit/application` | browser + server | evolving<br>_redefined in 7 of the 37 minors since 0.56.2, most recently 0.83.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
|
|
84
84
|
| `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
|
|
85
85
|
| `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
|
|
86
86
|
| `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
|
|
@@ -3639,6 +3639,48 @@ never for authentication, RBAC, tenant selection or rate limiting. Verified
|
|
|
3639
3639
|
identity still comes from `auth`; the `context(auth)` callback remains the
|
|
3640
3640
|
application-context factory and receives no protocol metadata.
|
|
3641
3641
|
|
|
3642
|
+
### Telling the host what a long call is doing — `ctx.reportProgress`
|
|
3643
|
+
|
|
3644
|
+
A tool that takes ten minutes tells a text host nothing: the call is made, and
|
|
3645
|
+
the next thing the host shows is the answer. The protocol has a channel for
|
|
3646
|
+
this — the host sends a `progressToken` with the call, the server relates
|
|
3647
|
+
progress notifications to it — and a handler reaches it through
|
|
3648
|
+
`ctx.reportProgress`:
|
|
3649
|
+
|
|
3650
|
+
```ts
|
|
3651
|
+
render: async (ctx) => {
|
|
3652
|
+
await ctx.reportProgress?.({ message: 'queued' })
|
|
3653
|
+
const job = await jobs.start(ctx.input)
|
|
3654
|
+
await ctx.reportProgress?.({ message: 'rendering', progress: 1, total: 3 })
|
|
3655
|
+
return await jobs.finish(job)
|
|
3656
|
+
}
|
|
3657
|
+
```
|
|
3658
|
+
|
|
3659
|
+
`reportProgress` is present on every **tool** call — MCP, agent and CLI — so a
|
|
3660
|
+
handler never branches on transport, and it does nothing wherever nobody is
|
|
3661
|
+
listening. It is absent on the HTTP transport, which has no channel for it, so a
|
|
3662
|
+
handler that also serves HTTP writes `ctx.reportProgress?.(…)`. It never throws and never
|
|
3663
|
+
rejects: a message about work must not be able to kill the work it describes, so
|
|
3664
|
+
a refused notification leaves the host exactly where a host that never asked for
|
|
3665
|
+
progress already is.
|
|
3666
|
+
|
|
3667
|
+
`progress` is a number the protocol requires and a handler often does not have —
|
|
3668
|
+
there is a stage to name and no scale to name it on. Omit it and the ordinal of
|
|
3669
|
+
the update is sent, with no `total`, so the host shows a counter rather than a
|
|
3670
|
+
bar stuck at 3%. That is a fact about what happened; a synthesised percentage
|
|
3671
|
+
would be a claim nobody measured.
|
|
3672
|
+
|
|
3673
|
+
The framework sends nothing on its own. Progress is meaningful only where an
|
|
3674
|
+
operation has observable stages, and manufacturing one per call would be
|
|
3675
|
+
inventing a second transport beside the one the protocol already provides. The
|
|
3676
|
+
one exception is the managed wait tool (`mountWait`), which already polls and
|
|
3677
|
+
therefore already knows: when the host asked for progress it reports each poll
|
|
3678
|
+
and the phase it last saw, which is the tool whose silence raised the question
|
|
3679
|
+
in the first place.
|
|
3680
|
+
|
|
3681
|
+
The token itself is on `ctx.mcp.progressToken` when a caller wants to know
|
|
3682
|
+
whether anyone is listening.
|
|
3683
|
+
|
|
3642
3684
|
### Multi-round tool input (`input_required`)
|
|
3643
3685
|
|
|
3644
3686
|
An MCP-only operation can require typed user input before its final side effect:
|
|
@@ -3690,6 +3732,58 @@ ordinary unsupported result rather than a falsely advertised round. Signed
|
|
|
3690
3732
|
state is tamper-resistant, not an exactly-once replay store; destructive
|
|
3691
3733
|
handlers must still be idempotent where retries matter.
|
|
3692
3734
|
|
|
3735
|
+
#### Questions chosen from the arguments
|
|
3736
|
+
|
|
3737
|
+
The list above is fixed when the contract is written, which is right when the
|
|
3738
|
+
questions belong to the operation. It is not enough when they belong to the
|
|
3739
|
+
**arguments** — one model takes `aspect_ratio`, another `duration`, a third an
|
|
3740
|
+
input image — and there a declared list can only be empty. `inputRequired` also
|
|
3741
|
+
accepts a function of the parsed call:
|
|
3742
|
+
|
|
3743
|
+
```ts
|
|
3744
|
+
mcp: {
|
|
3745
|
+
inputRequired: async ({ input }) => {
|
|
3746
|
+
const model = await catalog.get(input.model)
|
|
3747
|
+
return model.needsRatio
|
|
3748
|
+
? [{ key: 'ratio', schema: RatioSchema, message: 'Which aspect ratio?' }]
|
|
3749
|
+
: []
|
|
3750
|
+
},
|
|
3751
|
+
}
|
|
3752
|
+
```
|
|
3753
|
+
|
|
3754
|
+
It receives the same `params` / `input` the handler receives — parsed, so
|
|
3755
|
+
defaults and coercions are already applied. Returning an empty array means this
|
|
3756
|
+
call needs nothing and runs straight through. Everything else is unchanged: the
|
|
3757
|
+
state is still signed and bound to principal, operation and argument digest, the
|
|
3758
|
+
rounds are still counted against `maxRounds`, and a resolved list with duplicate
|
|
3759
|
+
keys or more rounds than allowed is refused.
|
|
3760
|
+
|
|
3761
|
+
**The plan is fingerprinted, and that matters more than it looks.** Between two
|
|
3762
|
+
rounds there is a full round trip to the host, and a resolver that reads
|
|
3763
|
+
anything outside its arguments — a model catalog, a feature flag — can answer
|
|
3764
|
+
differently the second time. Every other check would still pass: same principal,
|
|
3765
|
+
same operation, same arguments, round still in range. Round 2's question would
|
|
3766
|
+
be asked under round 1's key, and if the schemas happened to be compatible the
|
|
3767
|
+
user's answer would be accepted for a question nobody asked. So the resolved
|
|
3768
|
+
list is fingerprinted into the signed state and re-checked every round; a plan
|
|
3769
|
+
that moved is refused, not asked. A resolver that reads only its arguments never
|
|
3770
|
+
sees this.
|
|
3771
|
+
|
|
3772
|
+
**What a dynamic declaration costs.** One extra pass through the contract
|
|
3773
|
+
pipeline per round — `lifecycle.beforeHandle` runs once more and one more
|
|
3774
|
+
`toolPhase: 'input-round'` audit row is written. That is the price of ordering:
|
|
3775
|
+
the resolver is your code and may reach a network, so it runs **after**
|
|
3776
|
+
authorisation, never before, and the only thing that can authorise is a pipeline
|
|
3777
|
+
pass. Resolving itself is free — the arguments are parsed by the same pure
|
|
3778
|
+
function the call parses with, not by a second run of anything. A fixed list
|
|
3779
|
+
pays none of this; its call sequence is unchanged.
|
|
3780
|
+
|
|
3781
|
+
One thing does change for a fixed list: the signed round state now carries a
|
|
3782
|
+
fingerprint of the question plan, and it is checked on every round. A
|
|
3783
|
+
conversation already in flight when you deploy this has no fingerprint in its
|
|
3784
|
+
state and is refused once with `INVALID_REQUEST_STATE`; the host re-asks and the
|
|
3785
|
+
retry succeeds.
|
|
3786
|
+
|
|
3693
3787
|
### Guarding tools — `lifecycle`
|
|
3694
3788
|
|
|
3695
3789
|
A tool call runs the same handler an HTTP request would. `lifecycle` makes it
|
|
@@ -7976,6 +8070,166 @@ Carry the build stamp inside the binary (`CliBuildStampSchema`,
|
|
|
7976
8070
|
`formatCliBuildStamp`) so the tool can say what it is rather than leaving the
|
|
7977
8071
|
reader to infer it from behaviour.
|
|
7978
8072
|
|
|
8073
|
+
### Proving who built it — signing the manifest
|
|
8074
|
+
|
|
8075
|
+
The asset digest proves the bytes that arrived are the bytes the manifest named.
|
|
8076
|
+
It proves nothing about who named them: the manifest and the assets come from
|
|
8077
|
+
one origin, so whoever can replace one can replace the other. Authorship needs a
|
|
8078
|
+
key the build carries and the server never holds.
|
|
8079
|
+
|
|
8080
|
+
```ts
|
|
8081
|
+
// Publishing — the only side that holds a private key.
|
|
8082
|
+
const signature = signCliManifest(manifest, { keyId: 'release-2026', privateKey })
|
|
8083
|
+
publish({ ...manifest, signature })
|
|
8084
|
+
|
|
8085
|
+
// Installing — the trust root is compiled into the binary. One fetched at run
|
|
8086
|
+
// time from the same origin as the manifest would prove nothing.
|
|
8087
|
+
const trust = { keys: { 'release-2026': PUBLISHED_KEY } }
|
|
8088
|
+
const check = await checkCliUpdate({ manifestUrl, currentVersion, trust })
|
|
8089
|
+
if (check.status === 'outdated' && check.asset) {
|
|
8090
|
+
await applyCliUpdate({ asset: check.asset, manifest: check.manifest, trust, backupPath })
|
|
8091
|
+
}
|
|
8092
|
+
```
|
|
8093
|
+
|
|
8094
|
+
The signature covers `{name, version, commit, builtAt, assets[]}` with **every
|
|
8095
|
+
asset's digest**, so the chain closes on the file that will execute rather than
|
|
8096
|
+
on the document describing it; moving an asset to a new URL does not invalidate
|
|
8097
|
+
it, changing what an asset contains does.
|
|
8098
|
+
|
|
8099
|
+
Five verdicts, and `unenforced` is the one that earns its keep: a build with no
|
|
8100
|
+
pinned key keeps updating, and the fact that nothing was checked is *visible*
|
|
8101
|
+
rather than assumed. `missing`, `unknown-key` and `invalid` are distinguished
|
|
8102
|
+
because they need different answers — a key you forgot to pin is not a forged
|
|
8103
|
+
signature.
|
|
8104
|
+
|
|
8105
|
+
A bad verdict does not produce a fifth status. `outdated` is an instruction to
|
|
8106
|
+
install, and a manifest that failed its signature has not established that there
|
|
8107
|
+
is a newer build worth installing — only that a document claims one. The check
|
|
8108
|
+
answers `unknown` with the verdict in its reason, so it is never mistaken for a
|
|
8109
|
+
network failure. `applyCliUpdate` checks again and refuses **before** the
|
|
8110
|
+
download: a signature verified after the bytes are on the machine produces a
|
|
8111
|
+
cleanup problem rather than a refusal.
|
|
8112
|
+
|
|
8113
|
+
### A way back — `backupPath` and `rollbackCliUpdate`
|
|
8114
|
+
|
|
8115
|
+
Rolling back is a property of updating, not of the application:
|
|
8116
|
+
|
|
8117
|
+
```ts
|
|
8118
|
+
const applied = await applyCliUpdate({ asset, targetPath, backupPath })
|
|
8119
|
+
// …the new build turns out to be wrong. `backupSha256` is absent when there was
|
|
8120
|
+
// nothing to keep — a first install — and that is the case with nothing to roll
|
|
8121
|
+
// back to:
|
|
8122
|
+
if (applied.backupSha256) {
|
|
8123
|
+
rollbackCliUpdate({ targetPath, backupPath, expectedSha256: applied.backupSha256 })
|
|
8124
|
+
}
|
|
8125
|
+
```
|
|
8126
|
+
|
|
8127
|
+
The copy is taken between "the new bytes verified" and the replacement — earlier
|
|
8128
|
+
would preserve a binary about to be replaced by a download that then failed its
|
|
8129
|
+
digest, later has nothing left to copy — and it carries the target's file mode,
|
|
8130
|
+
because a backup that cannot be executed is not a way back. A first install has
|
|
8131
|
+
nothing to keep, which is not a failure.
|
|
8132
|
+
|
|
8133
|
+
`expectedSha256` is required, not optional. A rollback that installs whatever
|
|
8134
|
+
happens to be at the backup path is a second install of an unverified binary,
|
|
8135
|
+
and the day it is used is the day nobody is in a position to check.
|
|
8136
|
+
|
|
8137
|
+
### Two release tracks
|
|
8138
|
+
|
|
8139
|
+
`assertCliPublishable` takes one manifest **or every manifest already
|
|
8140
|
+
published**:
|
|
8141
|
+
|
|
8142
|
+
```ts
|
|
8143
|
+
assertCliPublishable([stableManifest, betaManifest], next)
|
|
8144
|
+
```
|
|
8145
|
+
|
|
8146
|
+
With a single argument and two tracks the check is silently useless: `1.2.3`
|
|
8147
|
+
goes to beta from one commit and to stable from another, the function is called
|
|
8148
|
+
with the track being published and sees no conflict, and the person who
|
|
8149
|
+
installed the beta is told they are current forever. Nothing on their machine
|
|
8150
|
+
looks wrong.
|
|
8151
|
+
|
|
8152
|
+
**The channel itself is not an argument of the framework, and will not become
|
|
8153
|
+
one.** The framework has no model of how your URLs are built and should not: an
|
|
8154
|
+
asset's address comes from the manifest, and the manifest's address belongs to
|
|
8155
|
+
your server. A channel is two documents at two addresses — which of them a build
|
|
8156
|
+
consults is your decision, and `assertCliPublishable` is told about all of them
|
|
8157
|
+
rather than taught the shape of any.
|
|
8158
|
+
|
|
8159
|
+
## One operation per line — `createCliInvoker`
|
|
8160
|
+
|
|
8161
|
+
The most common way an agent drives a CLI is not one command: it is a stream —
|
|
8162
|
+
a JSON line per operation, an answer per line — and a resumable batch of the
|
|
8163
|
+
same. The mechanics contain nothing product-specific, and until now the
|
|
8164
|
+
framework left the one piece that cannot be written outside it: running an
|
|
8165
|
+
already-parsed call.
|
|
8166
|
+
|
|
8167
|
+
```ts
|
|
8168
|
+
import { createCliInvoker, defineCliStreamCommand, defineCliBatchCommand } from 'stitchkit/cli'
|
|
8169
|
+
|
|
8170
|
+
const invoker = await createCliInvoker({ name: 'myapp', services })
|
|
8171
|
+
|
|
8172
|
+
const outcome = await invoker.invoke('create_item', { title: 'hello', tags: ['a'] })
|
|
8173
|
+
// { ok: true, exitCode: 0, data: { … } } — nothing written, nothing exited
|
|
8174
|
+
```
|
|
8175
|
+
|
|
8176
|
+
`invoke` runs the same pipeline a typed command runs — the same validation, the
|
|
8177
|
+
same `lifecycle` gate, the same hooks — and returns the handler's validated
|
|
8178
|
+
output with **the exit code the printed path would give**, from the same table.
|
|
8179
|
+
`data` is typed `unknown`: the surface is resolved at run time from services and
|
|
8180
|
+
runtime tools, so there is no compile-time name to key a result type on. Narrow
|
|
8181
|
+
it with the contract's own output schema. That table used to
|
|
8182
|
+
live inside the function that prints, so the only way to learn a code was to
|
|
8183
|
+
print it.
|
|
8184
|
+
|
|
8185
|
+
Without this, a stream loop had to re-spawn the binary per line: arguments
|
|
8186
|
+
serialised back into `--flag value` strings, nested objects pushed through
|
|
8187
|
+
`JSON.stringify` into one argv slot and parsed again on the other side, the
|
|
8188
|
+
result read back out of stdout text. Three conversions of data the framework was
|
|
8189
|
+
already holding, plus a process start measured at 0.15 s — thirty seconds for a
|
|
8190
|
+
two-hundred-line manifest before any work begins. The ban on nesting one stream
|
|
8191
|
+
inside another was a consequence of that child process, not a rule anyone wanted.
|
|
8192
|
+
|
|
8193
|
+
### Mounting the loop
|
|
8194
|
+
|
|
8195
|
+
```ts
|
|
8196
|
+
await createCli({
|
|
8197
|
+
name: 'myapp', version, services,
|
|
8198
|
+
commands: [
|
|
8199
|
+
defineCliStreamCommand({ name: 'jsonl', invoker }),
|
|
8200
|
+
defineCliBatchCommand({ name: 'batch', invoker, checkpointPath: '.myapp-batch.json' }),
|
|
8201
|
+
],
|
|
8202
|
+
})
|
|
8203
|
+
```
|
|
8204
|
+
|
|
8205
|
+
They are factories, not framework-owned names. Reserving `jsonl` and `batch`
|
|
8206
|
+
would take two names out of a namespace that belongs to the application — one
|
|
8207
|
+
that already has a `batch` command would either fail at startup or find its own
|
|
8208
|
+
command silently shadowed — so you mount them under whatever you call them, like
|
|
8209
|
+
any other command of yours.
|
|
8210
|
+
|
|
8211
|
+
Each line is `{ id, command, args }`; each answer is the invocation result plus
|
|
8212
|
+
that `id`. A line that is not JSON, or has no `id`, is **answered** and the
|
|
8213
|
+
stream continues: a stream that goes quiet on one line leaves its consumer
|
|
8214
|
+
unable to tell which request it lost. Answers go to stdout, one object per line,
|
|
8215
|
+
and nothing else does.
|
|
8216
|
+
|
|
8217
|
+
### Resuming a batch
|
|
8218
|
+
|
|
8219
|
+
The checkpoint records, per id, a digest of the line that produced the recorded
|
|
8220
|
+
answer, and is written after **every** line by atomic rename — a checkpoint that
|
|
8221
|
+
only survives a clean finish protects against exactly the case that does not
|
|
8222
|
+
happen.
|
|
8223
|
+
|
|
8224
|
+
A re-run replays what already ran. A line whose content changed under the same
|
|
8225
|
+
id is **refused**: replaying it would report success for an operation nobody
|
|
8226
|
+
ran, and re-running it would repeat a paid call. Both answer a question nobody
|
|
8227
|
+
asked.
|
|
8228
|
+
|
|
8229
|
+
What stays yours: what makes a repeat safe (an idempotency key derived from the
|
|
8230
|
+
line id), which operations a batch may contain, and how much of it runs at once.
|
|
8231
|
+
That is product knowledge, and the framework has no business guessing it.
|
|
8232
|
+
|
|
7979
8233
|
## Commands discovered from a running server
|
|
7980
8234
|
|
|
7981
8235
|
A CLI compiled from contracts carries the surface of the build it was compiled
|
|
@@ -10453,7 +10707,7 @@ export const observability = createObservability({
|
|
|
10453
10707
|
write,
|
|
10454
10708
|
includePayload: false, // default: no Request.clone(), payload is null
|
|
10455
10709
|
includeCancelled: false, // default: keep client closes in access logs only
|
|
10456
|
-
filter: (event) => event.method !== 'GET',
|
|
10710
|
+
filter: (event) => event.method !== 'GET', // see the note on jobs below
|
|
10457
10711
|
},
|
|
10458
10712
|
tools: {
|
|
10459
10713
|
write,
|
|
@@ -10639,21 +10893,56 @@ queryable across all three:
|
|
|
10639
10893
|
|
|
10640
10894
|
| Field | Notes |
|
|
10641
10895
|
|-------|-------|
|
|
10642
|
-
| `source` | `http` \| `mcp` \| `agent` |
|
|
10643
|
-
| `
|
|
10896
|
+
| `source` | `http` \| `mcp` \| `agent` \| `job` |
|
|
10897
|
+
| `kind` | `request` \| `job` — written on **every** row, so a filter never has to read it off an absent `method` |
|
|
10898
|
+
| `name` | what the work is, on a job (`agent-loop`, `broadcast-send`); absent on a request, which its method and path name |
|
|
10899
|
+
| `method` / `path` | the verb + path, or `TOOL` + `/{source}/{tool}` — **absent** on a job, which arrived over no transport |
|
|
10644
10900
|
| `serviceName` / `action` | stable contract identity of the operation (→ ADR 0022) — from the contract, not parsed from `path`; set on every surface, present even on a pre-handler 400 |
|
|
10645
10901
|
| `toolName` | tool calls only |
|
|
10646
10902
|
| `httpMethod` | the contract verb on **tool** events (their `method` is `TOOL`) — filter reads vs writes across both surfaces with `(event.httpMethod ?? event.method) !== 'GET'` |
|
|
10647
10903
|
| `dimensions` | app-defined domain dimensions (tenant / project / entity id) — see [request context](#request-context) |
|
|
10648
10904
|
| `traceId` / `spanId` / `parentSpanId` | [W3C trace context](#trace-context) |
|
|
10649
10905
|
| `outcome` | optional `'cancelled'` on explicitly enabled HTTP client-close rows; ordinary rows omit it |
|
|
10650
|
-
| `ok` / `statusCode` | legacy success bit plus real HTTP status, or `200`/`400` for a tool; an opted-in cancellation is `false` / `499` |
|
|
10906
|
+
| `ok` / `statusCode` | legacy success bit plus real HTTP status, or `200`/`400` for a tool; an opted-in cancellation is `false` / `499`. `statusCode` is **absent** on a job — it had no transport to have a status on, and inventing one would be the same defect as the `method: 'AGENT'` this replaced. A job's outcome is `ok`, plus `errorCode` when it failed |
|
|
10651
10907
|
| `durationMs` / `startedAt` | timing |
|
|
10652
10908
|
| `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
|
|
10653
10909
|
| `payload` | sanitised tool arguments; HTTP is `null` unless request `includePayload` is enabled |
|
|
10654
10910
|
| `resultSize` / `responseBytes` | result item count + serialised size |
|
|
10655
10911
|
| `userId` / `ipAddress` / `userAgent` | identity |
|
|
10656
10912
|
|
|
10913
|
+
### Work that is not a request — `runUnitOfWork`
|
|
10914
|
+
|
|
10915
|
+
An agent loop launched fire-and-forget, a scheduled broadcast, a scenario engine
|
|
10916
|
+
tick: each has a start, a duration and an outcome, and none of them arrived over
|
|
10917
|
+
a transport. Run one inside a context and it is audited like everything else:
|
|
10918
|
+
|
|
10919
|
+
```ts
|
|
10920
|
+
await runUnitOfWork({ name: 'broadcast-send', observability }, async () => {
|
|
10921
|
+
setRequestDimensions({ projectId })
|
|
10922
|
+
await send()
|
|
10923
|
+
})
|
|
10924
|
+
```
|
|
10925
|
+
|
|
10926
|
+
The row carries `kind: 'job'`, the `name`, the duration and the outcome, and
|
|
10927
|
+
carries **no** `method`, `path` or `statusCode`, because the work had none. That
|
|
10928
|
+
absence is the point. The alternative is what consuming code had to do before —
|
|
10929
|
+
write `method: 'AGENT'` into the field for verbs — and a row that names a verb
|
|
10930
|
+
nobody used is worse than a row that names none, because a filter reading
|
|
10931
|
+
`method !== 'GET'` counts it as a write and nothing looks wrong.
|
|
10932
|
+
|
|
10933
|
+
Which is why **a filter on `method` needs a decision about jobs.** `auditChanges`
|
|
10934
|
+
makes it explicitly: a row with no verb is kept, because whether it changed
|
|
10935
|
+
anything cannot be answered from the row, and an audit that drops what it did not
|
|
10936
|
+
examine reports zero and looks exactly like a system in which nothing happened.
|
|
10937
|
+
A hand-written `event.method !== 'GET'` keeps jobs too (`undefined !== 'GET'`);
|
|
10938
|
+
`event.kind === 'request' && event.method !== 'GET'` is how you exclude them on
|
|
10939
|
+
purpose.
|
|
10940
|
+
|
|
10941
|
+
Without an `observability` the work still runs inside a context, so trace ids,
|
|
10942
|
+
`setRequestDimensions` and the bounded logger behave — recording is a separate
|
|
10943
|
+
decision from having a context. `traceparent` continues the trace of whatever
|
|
10944
|
+
scheduled the work, instead of starting an unrelated one.
|
|
10945
|
+
|
|
10657
10946
|
### Write the calls that changed something
|
|
10658
10947
|
|
|
10659
10948
|
`auditChanges` is the filter most projects end up writing, shipped so they do not
|
|
@@ -12132,6 +12421,52 @@ analytics policy belong to the application.
|
|
|
12132
12421
|
|
|
12133
12422
|
# Upgrading stitchkit
|
|
12134
12423
|
|
|
12424
|
+
## Released migration: 0.92.0
|
|
12425
|
+
|
|
12426
|
+
1. **`RequestEvent.method`, `path` and `statusCode` are optional.** Work that did
|
|
12427
|
+
not arrive over a transport now records their **absence** instead of inventing
|
|
12428
|
+
values — a background loop used to write `method: 'AGENT'`, which is not a
|
|
12429
|
+
verb. **Who must act:** anyone whose `RequestEventSinkConfig.write` puts these
|
|
12430
|
+
into NOT NULL columns, or whose filter reads `event.method`.
|
|
12431
|
+
|
|
12432
|
+
```ts
|
|
12433
|
+
// before
|
|
12434
|
+
write: (event) => db.audit.create({ data: { method: event.method, path: event.path, status: event.statusCode } })
|
|
12435
|
+
// after — a job has none of the three; `kind` says which kind of row it is
|
|
12436
|
+
write: (event) => db.audit.create({ data: {
|
|
12437
|
+
kind: event.kind,
|
|
12438
|
+
name: event.name ?? null,
|
|
12439
|
+
method: event.method ?? null,
|
|
12440
|
+
path: event.path ?? null,
|
|
12441
|
+
status: event.statusCode ?? null,
|
|
12442
|
+
} })
|
|
12443
|
+
```
|
|
12444
|
+
|
|
12445
|
+
A filter written as `event.method !== 'GET'` keeps jobs (`undefined !== 'GET'`),
|
|
12446
|
+
which is usually what you want. To exclude them deliberately:
|
|
12447
|
+
`event.kind === 'request' && event.method !== 'GET'`.
|
|
12448
|
+
|
|
12449
|
+
2. **`applyCliUpdate` needs the manifest when given a trust root.** The signature
|
|
12450
|
+
covers the manifest and every asset digest together, so an asset alone cannot
|
|
12451
|
+
be verified. **Who must act:** only callers adopting signature verification;
|
|
12452
|
+
the unsigned call is unchanged.
|
|
12453
|
+
|
|
12454
|
+
```ts
|
|
12455
|
+
// before
|
|
12456
|
+
await applyCliUpdate({ asset })
|
|
12457
|
+
// after
|
|
12458
|
+
await applyCliUpdate({ asset, manifest: check.manifest, trust })
|
|
12459
|
+
```
|
|
12460
|
+
|
|
12461
|
+
3. **An MCP continuation minted before this release is refused once.** The signed
|
|
12462
|
+
round state now carries a fingerprint of the question plan, checked on every
|
|
12463
|
+
round, so a plan that moved between rounds is a refusal rather than a
|
|
12464
|
+
different question asked under the previous one's key. A conversation already
|
|
12465
|
+
in flight at deploy time has no fingerprint and gets
|
|
12466
|
+
`INVALID_REQUEST_STATE`; the host re-asks and the second attempt succeeds.
|
|
12467
|
+
**Who must act:** nobody — but expect a small number of these in the minutes
|
|
12468
|
+
after a deploy, and do not read them as tampering.
|
|
12469
|
+
|
|
12135
12470
|
## Released migration: 0.91.0
|
|
12136
12471
|
|
|
12137
12472
|
1. `STITCH_ERROR_STATUS` gained `NOT_IMPLEMENTED` (501). An **exhaustive** error
|
|
@@ -15832,6 +16167,10 @@ from the root `stitchkit`.
|
|
|
15832
16167
|
| `HandlerContext` | _type_ | the typed context seen by a handler, including optional typed `mcp` metadata |
|
|
15833
16168
|
| `McpCallContext` | _type_ | validated metadata for the active managed MCP call (`era`, method, tool, client and multi-round fields) |
|
|
15834
16169
|
| `McpClientInfo` | _type_ | self-reported MCP host name/version; attribution only, never application identity |
|
|
16170
|
+
| `McpInputRequiredCall` | _type_ | the parsed `params` / `input` an elicitation resolver chooses its questions from |
|
|
16171
|
+
| `McpInputRequiredResolver` | _type_ | `mcp.inputRequired` as a function of the parsed arguments — returns the rounds for this call, or none |
|
|
16172
|
+
| `McpProgressUpdate` | _type_ | one mid-call progress update — `message`, and `progress`/`total` when the operation knows a scale |
|
|
16173
|
+
| `McpReportProgress` | _type_ | `ctx.reportProgress` — sends one update to the host that asked for progress; a no-op when none did, and never throws |
|
|
15835
16174
|
| `McpRoundOutcome` | _type_ | managed multi-round attempt outcome |
|
|
15836
16175
|
| `EndpointHandlerContext` | _type_ | one endpoint handler's fully inferred params, input, files and runtime context |
|
|
15837
16176
|
| `EndpointFn` | _type_ | the call signature of one client method |
|
|
@@ -16949,6 +17288,8 @@ audit event. See the [Observability guide](../guide/observability.md).
|
|
|
16949
17288
|
| `DEFAULT_REDACT_PATHS` | constant | baseline credential/token paths added to the sanitizer's sensitive-key policy |
|
|
16950
17289
|
| `BoundedLoggerBounds` / `BoundedLoggerOptions` | _type_ | per-value and total record ceilings (`stringLengthByKey` raises the string bound for named keys such as `stack`) plus sink/redaction configuration |
|
|
16951
17290
|
| `RequestEvent` | _type_ | the normalised audit event handed to the sink; opt-in HTTP cancellation rows carry `outcome: 'cancelled'` |
|
|
17291
|
+
| `runUnitOfWork` / `RunUnitOfWorkOptions` | function / _type_ | run work that did not arrive over a transport — an agent loop, a scheduled broadcast — inside one context, with the same completion record a request gets |
|
|
17292
|
+
| `RequestContextKind` | _type_ | `'request'` \| `'job'` — what the row describes, written on every event so a filter never has to read it off an absent `method` |
|
|
16952
17293
|
| `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
|
|
16953
17294
|
| `Observability` | _type_ | `{ request?, toolCall, getStatus(), flush(bound?): Promise<boolean>, close(bound?) }` with bounded sink lifecycle |
|
|
16954
17295
|
| `ObservabilityDrainBound` | _type_ | `timeoutMs` and/or `signal` limiting how long `flush`/`close` wait; the wait ends, outstanding writes do not |
|
|
@@ -17690,7 +18031,11 @@ SDK nor the `ai` peer.
|
|
|
17690
18031
|
| `renderCliInstaller` | function | generate the one-line installer from a build manifest, with the URL and digest substituted — [guide](../guide/cli.md#distribution-and-self-update) |
|
|
17691
18032
|
| `checkCliUpdate` | function | bounded, interval-limited, never-throwing check for a newer published build |
|
|
17692
18033
|
| `applyCliUpdate` | function | download, verify the decompressed digest and replace the binary by rename |
|
|
17693
|
-
| `assertCliPublishable` | function | refuse republishing one version from a different commit |
|
|
18034
|
+
| `assertCliPublishable` | function | refuse republishing one version from a different commit — takes one manifest or every manifest already published, because the promise a version makes does not stop at a channel boundary |
|
|
18035
|
+
| `rollbackCliUpdate` / `CliRollbackConfig` / `RolledBackCliUpdate` | function / _types_ | put the kept previous build back, digest-checked first and written by the same atomic rename the update uses |
|
|
18036
|
+
| `signCliManifest` / `verifyCliManifest` / `cliManifestSigningPayload` | function | Ed25519 over `{name, version, commit, builtAt, assets[]}` — every asset's digest included, so the chain closes on the file that executes |
|
|
18037
|
+
| `cliSignatureAccepted` / `CliSignatureVerdict` | function / _type_ | `valid` \| `unenforced` \| `missing` \| `unknown-key` \| `invalid`; `unenforced` keeps an unpinned build updating while making the absence of a check visible |
|
|
18038
|
+
| `CliTrustRoot` / `CliSigningKey` / `CliBuildSignature` / `CliBuildSignatureSchema` | _types_ / schema | the keys a build trusts, by id, and the detached signature a manifest carries |
|
|
17694
18039
|
| `selectCliBuildAsset` | function | the asset for one target, or `undefined` |
|
|
17695
18040
|
| `currentCliBuildTarget` | function | `{ platform, arch }` of the running process |
|
|
17696
18041
|
| `formatCliBuildStamp` | function | one line saying what the running build is |
|
|
@@ -17709,6 +18054,11 @@ SDK nor the `ai` peer.
|
|
|
17709
18054
|
| `CliArgvRoute` | _type_ | `{ command, commandArgv, topLevelHelp, version, error? }` returned by `routeCliArgv` |
|
|
17710
18055
|
| `pollUntilDone` | function | the generic `--wait` poller (advanced) |
|
|
17711
18056
|
| `emitResult` | function | write a pretty or compact `ToolResult` record to stdout/stderr + exit code (advanced) |
|
|
18057
|
+
| `cliExitCode` | function | the exit code a result earns, from the same table, without writing anything — so an in-process run and the printed path cannot disagree |
|
|
18058
|
+
| `createCliInvoker` / `CliInvoker` / `CliInvokerConfig` / `CliInvocationResult` / `CliInvokerCommand` / `cliInvocationResult` | function / _types_ | compile the managed surface once and run parsed calls against it in process — no argv round-trip, no child process, no writing |
|
|
18059
|
+
| `defineCliStreamCommand` / `CliStreamCommandConfig` / `CliStreamAnswer` / `CliStreamHooks` / `runCliStream` | function / _types_ | one operation per JSON line of stdin, answered with the same `id`; a bad line answers and the stream continues |
|
|
18060
|
+
| `defineCliBatchCommand` / `CliBatchCommandConfig` | function / _type_ | the same loop, resumable: a recorded line replays, a line whose content changed under the same id is refused |
|
|
18061
|
+
| `readCliCheckpoint` / `writeCliCheckpoint` / `CliCheckpoint` / `CliCheckpointEntry` | function / _types_ | the batch's record — id plus a digest of the line, written by atomic rename after every line |
|
|
17712
18062
|
| `DEFAULT_EXIT_CODES` | const | the default `ToolResult.code` → exit-code map |
|
|
17713
18063
|
| `CliConfig` | _type_ | config for `createCli`; `defaultCommand`, `globalOptions`, `optionAliases` and `positionals` define the shared command presentation policy |
|
|
17714
18064
|
| `CliPresentationPolicyConfig` | _type_ | shared command presentation-policy subset of `CliConfig` |
|
package/package.json
CHANGED