@crvouga/mockingbird-service-hermes 0.1.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/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # @crvouga/mockingbird-service-hermes
2
+
3
+ > Familiar calls. Faithful echoes. Part of [Mockingbird](https://github.com/crvouga/mockingbird).
4
+
5
+ Work-in-progress mock for the Hermes Agent public peer-run API pinned to
6
+ `v2026.8.31`. Submission, polling and stop work with explicit synthetic lifecycle observations.
7
+ Events, approval and steer remain unsupported. No agent or inference runs.
8
+ [API_EVIDENCE.md](API_EVIDENCE.md) records source-backed semantics and gaps;
9
+ [SUPPORT.md](SUPPORT.md) records current operation support.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ bun add @crvouga/mockingbird-service-hermes
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { createRuntime } from "@crvouga/mockingbird-service-hermes"
21
+
22
+ const hermes = createRuntime({ seed: 42 })
23
+ const response = await hermes.fetch(new Request("http://hermes.mock/__admin/health"))
24
+ console.log(await response.json())
25
+ ```
26
+
27
+ Use the portable `HermesAPI` class for provider routes alone. `createRuntime`
28
+ adds health, namespaces, admin controls, faults, clock, metrics, metadata-only
29
+ request journal, and shared Timeline checkpoints. It needs no Hermes installation,
30
+ provider account, prompt data, API key, or local service.
31
+
32
+ For an HTTP endpoint:
33
+
34
+ ```ts
35
+ import { createServer } from "@crvouga/mockingbird-service-hermes/server"
36
+
37
+ const server = await createServer() // ephemeral loopback port
38
+ try {
39
+ console.log(await (await fetch(`${server.url}/__admin/health`)).json())
40
+ } finally {
41
+ await server.close()
42
+ }
43
+ ```
44
+
45
+ The CLI is `mockingbird-hermes serve --port 8827`. Point a peer HTTP client's base
46
+ URL at `http://127.0.0.1:8827`; submission returns immediately while execution remains queued until scripted.
47
+ The Node entry is separate from the portable Fetch entry.
48
+
49
+ ## Routes and controls
50
+
51
+ `POST /v1/runs` admits a run and `GET /v1/runs/{run_id}` polls it.
52
+ `POST /v1/runs/{run_id}/stop` requests interruption. Events, approval and steer
53
+ return a mock-only 501 envelope with `error.type` of
54
+ `mockingbird_unsupported` and `error.code` of `operation_not_implemented`.
55
+ Missing runs use the pinned `run_not_found` 404 envelope. Unknown paths return
56
+ 404. Mock-only errors do not claim real Hermes rejection behavior.
57
+
58
+ - `GET /__admin/health` identifies the `hermes` runtime.
59
+ - Select isolated namespaces with `x-mockingbird-namespace` or `/__admin/ns/<name>/…`.
60
+ - `POST /__admin/reset` clears selected provider state and Timeline. Shared clock,
61
+ faults and diagnostic journal retain their standard independent lifetimes.
62
+ - `POST /__admin/clock` controls the shared clock.
63
+ - `POST /__admin/faults` configures scoped operation/path/method fault rules;
64
+ `DELETE /__admin/faults` clears them. Named Hermes presets are documented below.
65
+ - `GET /__admin/requests` exposes request metadata without bodies, query values
66
+ or credentials. `DELETE /__admin/requests` clears selected diagnostic history.
67
+ - `POST /__admin/checkpoints` captures shared Timeline state. Checkout through
68
+ `POST /__admin/branches/<name>/checkout` with `{ "checkpoint": "cp_…" }`.
69
+
70
+ The existing shared `adminKey` option controls admin access. No credential-based
71
+ namespace mapping or Hermes provider authentication logic is introduced.
72
+
73
+ ## API
74
+
75
+ The portable entry exports:
76
+
77
+ - `HermesAPI`: provider `fetch` and `reset`, with namespace and shared SQLite storage.
78
+ - `HERMES_NAMESPACE`: the default storage namespace, `hermes`.
79
+ - `createRuntime`: the standard service runtime and shared controls.
80
+ - `document`: annotated OpenAPI contract.
81
+ - `operationIds`: all inventoried operations.
82
+ - `supportedOperationIds`: `RunCreate`, `RunGet`, and `RunStop`.
83
+
84
+ The Node-only `/server` entry exports:
85
+
86
+ - `createServer`: returns `runtime`, `server`, `url`, `host`, `port`, and `close`.
87
+ - `DEFAULT_PORT`: CLI default 8827; programmatic calls default to an ephemeral port.
88
+ - `serveTarget`: shared CLI server target.
89
+
90
+ Types include `HermesAPIOptions`, `HermesRuntime`, `HermesRuntimeOptions`,
91
+ `OperationId`, `SupportedOperationId`, `HermesServer`, and `HermesServerOptions`.
92
+ The executable `mockingbird-hermes` provides `serve`.
93
+
94
+ ## Script a run
95
+
96
+ ```ts
97
+ import { createRuntime } from "@crvouga/mockingbird-service-hermes"
98
+
99
+ const hermes = createRuntime({ seed: 42 })
100
+ const accepted = await hermes.fetch(new Request("http://hermes.mock/v1/runs", {
101
+ method: "POST",
102
+ headers: { "content-type": "application/json" },
103
+ body: JSON.stringify({ input: "synthetic prompt" }),
104
+ }))
105
+ const { run_id } = await accepted.json()
106
+ await hermes.fetch(new Request(`http://hermes.mock/__admin/hermes/runs/${run_id}/observe`, {
107
+ method: "POST",
108
+ headers: { "content-type": "application/json" },
109
+ body: JSON.stringify({ status: "completed", output: "synthetic result" }),
110
+ }))
111
+ const status = await hermes.fetch(new Request(`http://hermes.mock/v1/runs/${run_id}`))
112
+ console.log(await status.json())
113
+ ```
114
+
115
+ Admission returns 202 `{ run_id, status: "started", replayed: false }`. The stored
116
+ status starts as `queued`. Repeated polling does not advance execution or time.
117
+ Use the same namespace on submission, observations and polling. Optional
118
+ `session_id` defaults to the run ID when absent or falsy. `model` defaults to
119
+ `hermes-agent` only when absent; an explicit null or empty value is retained, as
120
+ in the pinned handler. A valid `X-Hermes-Session-Key` is echoed without deriving a
121
+ conversation ID or authenticating credentials. Model routing and transcript loading
122
+ are not simulated. IDs are deterministic opaque hex strings, separate from sessions.
123
+
124
+ The observation control accepts `status`: `queued`, `running`,
125
+ `waiting_for_approval`, `stopping`, `completed`, `failed`, `cancelled`, or
126
+ `interrupted`. It accepts optional `last_event`; `approval` only while waiting;
127
+ `error` only for failed/interrupted; and `output`, `usage`, `pending_steer` only
128
+ for completed. Usage has three nonnegative integer counts: `input_tokens`,
129
+ `output_tokens`, `total_tokens`. Completion defaults to empty output and zero counts.
130
+ Terminal observations cannot be changed (409); invalid control payloads return 400
131
+ without changing state. These are mock controls, not additional Hermes routes.
132
+
133
+ Timestamps are Unix seconds from the shared clock. Run records and their identity
134
+ sequence use shared storage and Timeline; reset clears them. No timers, agent
135
+ handles or prompts are stored. Polling result content stays out of journals.
136
+ Malformed roots/final input elements, hosted rooms and invalid memory-scope headers
137
+ currently return explicit mock-only 501 responses. See evidence for these limits.
138
+
139
+ ## Idempotent submission and synthetic scope
140
+
141
+ Send `Idempotency-Key` to reserve a delivery. Identical retries return 202 with
142
+ the original run ID, its current status, `replayed: true`, and
143
+ `Idempotency-Replayed: true`. A changed payload with the same scoped key returns
144
+ 409 `idempotency_key_conflict`. Concurrent identical requests reserve one run.
145
+ Empty/whitespace keys disable deduplication; other keys must contain 1–255 visible
146
+ ASCII characters after Python-style trimming. Validation errors reserve nothing.
147
+
148
+ The fingerprint covers the entire parsed JSON body, including unknown fields,
149
+ and the trimmed `X-Hermes-Session-Key`. Object key order does not matter; array
150
+ order and session/body changes do. Raw integer/float forms follow Python:
151
+ `1` and `1.0` conflict, `1.0` and `1e0` replay, and negative floating zero differs
152
+ from positive zero. Lone Unicode surrogates return an explicit mock-only 501;
153
+ Python cannot UTF-8 encode that fingerprint either.
154
+
155
+ `POST /__admin/hermes/scope` with `{ "profile": "synthetic-profile",
156
+ "identity": "synthetic-listener" }` selects an explicit synthetic scope within
157
+ the current Mockingbird namespace. Defaults are `default` and
158
+ `unauthenticated-test-listener`. Use only synthetic labels, never credentials.
159
+ Changing scope isolates reservations and public polling; returning to it restores
160
+ access to its runs. Session IDs and memory keys are not scope selectors, and
161
+ bearer text is ignored. This control does not implement authentication.
162
+
163
+ Reservations and owners survive reconstructing a `HermesAPI` with the same
164
+ SQLite client and namespace. Terminal replay is verified after that modeled
165
+ adapter reconstruction. Actual process/disk durability and unfinished-run restart
166
+ outcomes are modeled by the explicit restart control below. Shared
167
+ Timeline restores scope, reservations and run records together; reset clears them.
168
+ No public key-lookup endpoint is added. Request bodies are hashed in memory and
169
+ not persisted; stored reservations contain the key, hash, run identity and durable observation.
170
+
171
+ ## Stop and logical restart
172
+
173
+ `POST /v1/runs/{run_id}/stop` returns 200 `{ run_id, status: "stopping" }` for
174
+ locally active work. Repeated stop requests retain that intermediate state. Use
175
+ `observe` to script the eventual outcome: `cancelled` for acknowledged interruption,
176
+ `completed` when normal completion wins the race, or `failed` for execution failure.
177
+ Stopping a terminal run returns its full unchanged status, not a new cancellation.
178
+ Missing or inaccessible runs return 404 `run_not_found`.
179
+
180
+ `POST /__admin/hermes/restart` with `{ "owner": "stale" }` models loss of the
181
+ current namespace's gateway. It discards cached observations and retains surviving keyed durable
182
+ reservations across all synthetic profiles in that namespace. An unfinished keyed
183
+ run becomes `interrupted` when next polled, replayed or stopped, with
184
+ `The gateway restarted before this run settled.` and `run.interrupted`.
185
+ Its creation timestamp remains intact; its update timestamp reflects that first
186
+ observation. Terminal runs retain their status and result. Retrying an old delivery
187
+ key replays the interrupted run; a new key creates a different run.
188
+
189
+ Use `{ "owner": "alive" }` to model another live owner after gateway replacement.
190
+ Keyed observations stay unchanged, but stopping unfinished foreign work returns
191
+ 409 `run_not_active`. An explicit `observe` can represent that owner's eventual
192
+ completion. Neither control reads PIDs, interrupts a real process, or runs an agent.
193
+ It is a logical scenario, not a process/disk durability guarantee. Shared Timeline
194
+ restores the owner flags and run/reservation state together.
195
+
196
+ ## Retention and history
197
+
198
+ The controlled clock governs two independent retention layers. Cached `completed`,
199
+ `failed` and `cancelled` results expire when their update age is strictly greater
200
+ than one hour; equality survives and `interrupted` is excluded. The mock evaluates
201
+ elapsed 60-second sweep ticks on admission or observation, without a background
202
+ process. `POST /__admin/hermes/sweep` with `{}` explicitly executes one sweep at the
203
+ current clock, returning `cacheRemoved` and `simulated: true`.
204
+
205
+ Keyed terminal reservations expire only when their durable update age is strictly
206
+ greater than 24 hours. Valid keyed admission triggers that pruning, including a
207
+ request that subsequently conflicts. Polling and keyless admission do not prune
208
+ durable rows. Active reservations never expire by age. Polling can recover a
209
+ retained durable result after cache eviction, even beyond 24 hours before pruning;
210
+ a cached result can also temporarily outlive its pruned reservation. Missing results
211
+ return the ordinary 404 `run_not_found`, without an invented expired state.
212
+
213
+ Same-status progress observations update cached timestamps but do not persist the
214
+ new progress payload unless a selected durable field changes. Logical restart
215
+ clears cached observations and hydrates only surviving durable rows. Timeline
216
+ restores both layers, ownership, the sweep schedule and controlled clock together.
217
+ These are shared-storage and logical-restart guarantees, not process-crash or disk
218
+ durability evidence. SSE buffer and hosted-room retention remain outside the subset.
219
+
220
+ ## Fault and consumer scenarios
221
+
222
+ Use `POST /__admin/faults` with `{ "preset": "hermes_submit_accepted_drop" }` to
223
+ admit one new run and lose its response. The shared journal records acceptance and
224
+ its checkpoint even though delivery fails. Retry the same body and idempotency key
225
+ to recover the original run. Validation failures still return their normal errors.
226
+
227
+ Other one-shot presets are `hermes_poll_timeout` (delay one poll by 1000ms),
228
+ `hermes_throttled` (429 with the pinned default-capacity envelope), and
229
+ `hermes_draining` (503 with the pinned draining envelope). Both rejection presets
230
+ include `Retry-After: 1` and run before admission. These are explicit scenarios;
231
+ they do not automatically count capacity or simulate a real draining gateway.
232
+ To script an executor failure, use `observe` with `status: "failed"` and synthetic
233
+ `error` text, then poll or replay the same run.
234
+
235
+ The Node HTTP consumer fixture owns its disposable server and uses an independent
236
+ HTTP client to test lost responses, replay, polling, stopping, client deadlines and
237
+ expiry. It does not import Initiative or impose a consumer retry policy. Property
238
+ tests compare eligible peer operations and assert coverage; a seeded poll check
239
+ injects a schema-valid lifecycle divergence to test the comparator. Self-parity is
240
+ consistency evidence, not an independent Hermes runtime compatibility claim.
241
+
242
+ ## Deliberately not modelled
243
+
244
+ Prompts, instructions and history are validated then discarded.
245
+ Synthetic result text can be stored by observation controls and is cleared by reset.
246
+ Kanban attempts, client intake/recovery policy, inference, Python dispatchers,
247
+ host tool execution and credential enforcement are excluded. There are no outgoing
248
+ webhooks, provider calls or agent processes. Public peer-run IDs will remain
249
+ separate from consumer intake and Kanban identities.
250
+
251
+ Source evidence is not runtime parity. Package/shared-control tests do not establish
252
+ compatibility with a real Hermes process. The opt-in
253
+ [pinned oracle](oracle/README.md) separately passed 33 documented public HTTP
254
+ comparisons, including a fresh-process SQLite restart; its substitutions and
255
+ limitations remain explicit. Keep this package WIP until independent Ready requirements are met.
256
+
257
+
258
+ ## HTTP replay example
259
+
260
+ Start `mockingbird-hermes serve --port 8827`, then submit a synthetic delivery:
261
+
262
+ ```sh
263
+ curl -sS http://127.0.0.1:8827/v1/runs \
264
+ -H 'Content-Type: application/json' \
265
+ -H 'Idempotency-Key: example-delivery' \
266
+ -d '{"input":"synthetic prompt"}'
267
+ ```
268
+
269
+ Repeating the identical request returns the same `run_id` and a replay header.
270
+ Poll `GET /v1/runs/<run_id>`; request interruption with
271
+ `POST /v1/runs/<run_id>/stop`. A stop response is intermediate. Complete the
272
+ scenario with `POST /__admin/hermes/runs/<run_id>/observe`, using a JSON body
273
+ such as `{ "status": "cancelled" }`, then poll again. Use only synthetic input
274
+ and result data. No Python dispatcher or Kanban integration is required.
275
+
276
+ ## Verification and evidence
277
+
278
+ The package remains **WIP**. Three of six inventoried operations are supported;
279
+ SSE events, approval execution and steering remain excluded. The shipped
280
+ [oracle guide](oracle/README.md), [source manifest](oracle/source-lock.json), and
281
+ [recorded comparisons](evidence/pinned-oracle.json) make the exact upstream pin
282
+ and substitutions inspectable. Run the oracle only after separately authorizing
283
+ its dependency, disposable database and process operations. Normal package tests
284
+ need none of those operations or provider credentials.
285
+
286
+ From this repository, run `bun test packages/service/hermes` and the package's
287
+ `typecheck`, `lint`, `generate:check`, `openapi:check`, `build`, `portability` and
288
+ `pack:check` scripts. Root `bun run check` also verifies catalog and generated
289
+ documentation freshness. The docs catalog playground uses the same portable
290
+ runtime; its state is local to the browser.
package/SUPPORT.md ADDED
@@ -0,0 +1,16 @@
1
+ # Hermes Agent peer runs (Mockingbird) — operation support
2
+
3
+ Generated from `openapi.yaml`; do not edit by hand.
4
+
5
+ - operations in spec: **6**
6
+ - supported by the mock: **3**
7
+ - parity enabled: **3**
8
+
9
+ | operationId | route | mock | parity | notes |
10
+ | --- | --- | --- | --- | --- |
11
+ | `RunCreate` | `POST /v1/runs` | ✅ supported | ⚠️ unsafe (opt-in) | |
12
+ | `RunGet` | `GET /v1/runs/{run_id}` | ✅ supported | ✅ | |
13
+ | `RunStop` | `POST /v1/runs/{run_id}/stop` | ✅ supported | ⚠️ unsafe (opt-in) | |
14
+ | `RunEvents` | `GET /v1/runs/{run_id}/events` | ❌ unsupported | — | SSE is outside the public peer-run mock subset. |
15
+ | `RunApproval` | `POST /v1/runs/{run_id}/approval` | ❌ unsupported | — | Host tool approval execution is outside this mock. |
16
+ | `RunSteer` | `POST /v1/runs/{run_id}/steer` | ❌ unsupported | — | Agent execution and steering are outside this mock. |