@qvac/harness 0.7.0 → 1.0.0-rc.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.
Files changed (204) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +239 -2
  3. package/dist-lib/index.d.ts +58 -3
  4. package/dist-lib/index.js +736 -239
  5. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.d.ts +1 -0
  6. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.js +10 -0
  7. package/dist-lib/lib/capabilities/completion-engine/actors/answering.d.ts +3 -0
  8. package/dist-lib/lib/capabilities/completion-engine/actors/answering.js +17 -0
  9. package/dist-lib/lib/capabilities/completion-engine/actors/approval.d.ts +4 -0
  10. package/dist-lib/lib/capabilities/completion-engine/actors/approval.js +29 -0
  11. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.d.ts +4 -0
  12. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.js +73 -0
  13. package/dist-lib/lib/capabilities/completion-engine/actors/execute.d.ts +41 -0
  14. package/dist-lib/lib/capabilities/completion-engine/actors/execute.js +49 -0
  15. package/dist-lib/lib/capabilities/completion-engine/actors/executing.d.ts +3 -0
  16. package/dist-lib/lib/capabilities/completion-engine/actors/executing.js +102 -0
  17. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.d.ts +47 -0
  18. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.js +87 -0
  19. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.d.ts +2 -0
  20. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.js +32 -0
  21. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.d.ts +3 -0
  22. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.js +75 -0
  23. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.d.ts +3 -0
  24. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.js +152 -0
  25. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.d.ts +3 -0
  26. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.js +94 -0
  27. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.d.ts +9 -0
  28. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.js +11 -0
  29. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.d.ts +3 -0
  30. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.js +114 -0
  31. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.d.ts +3 -0
  32. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.js +198 -0
  33. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.d.ts +3 -0
  34. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.js +71 -0
  35. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.d.ts +18 -0
  36. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.js +70 -0
  37. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.d.ts +3 -0
  38. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.js +60 -0
  39. package/dist-lib/lib/capabilities/completion-engine/actors/stream.d.ts +21 -0
  40. package/dist-lib/lib/capabilities/completion-engine/actors/stream.js +48 -0
  41. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.d.ts +3 -0
  42. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.js +156 -0
  43. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.d.ts +7 -0
  44. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.js +61 -0
  45. package/dist-lib/lib/capabilities/completion-engine/driver.d.ts +25 -0
  46. package/dist-lib/lib/capabilities/completion-engine/driver.js +553 -0
  47. package/dist-lib/lib/capabilities/completion-engine/durable.d.ts +61 -0
  48. package/dist-lib/lib/capabilities/completion-engine/durable.js +150 -0
  49. package/dist-lib/lib/capabilities/completion-engine/operations.d.ts +6 -0
  50. package/dist-lib/lib/capabilities/completion-engine/operations.js +239 -0
  51. package/dist-lib/lib/capabilities/completion-engine/ports.d.ts +3 -0
  52. package/dist-lib/lib/capabilities/completion-engine/ports.js +267 -0
  53. package/dist-lib/lib/capabilities/completion-engine/session.d.ts +76 -0
  54. package/dist-lib/lib/capabilities/completion-engine/session.js +70 -0
  55. package/dist-lib/lib/capabilities/completion-loop/index.d.ts +80 -0
  56. package/dist-lib/lib/capabilities/completion-loop/index.js +158 -0
  57. package/dist-lib/lib/capabilities/completion-loop/metrics.d.ts +5 -0
  58. package/dist-lib/lib/capabilities/completion-loop/metrics.js +29 -0
  59. package/dist-lib/lib/capabilities/completion-loop/round.d.ts +33 -0
  60. package/dist-lib/lib/capabilities/completion-loop/round.js +353 -0
  61. package/dist-lib/lib/capabilities/completion-loop/tool-execution.d.ts +60 -0
  62. package/dist-lib/lib/capabilities/completion-loop/tool-execution.js +270 -0
  63. package/dist-lib/lib/capabilities/completion-loop/turns.d.ts +23 -0
  64. package/dist-lib/lib/capabilities/completion-loop/turns.js +112 -0
  65. package/dist-lib/lib/capabilities/completion-runtime.d.ts +46 -0
  66. package/dist-lib/lib/capabilities/completion-runtime.js +74 -0
  67. package/dist-lib/lib/capabilities/completion.d.ts +18 -335
  68. package/dist-lib/lib/capabilities/completion.js +20 -1301
  69. package/dist-lib/lib/capabilities/download.d.ts +1 -1
  70. package/dist-lib/lib/capabilities/download.js +7 -2
  71. package/dist-lib/lib/capabilities/invoke.js +5 -3
  72. package/dist-lib/lib/capabilities/tool-plan.d.ts +35 -0
  73. package/dist-lib/lib/capabilities/tool-plan.js +66 -0
  74. package/dist-lib/lib/capabilities/tool-turns.d.ts +11 -2
  75. package/dist-lib/lib/capabilities/tool-turns.js +30 -2
  76. package/dist-lib/lib/capabilities/types.d.ts +287 -0
  77. package/dist-lib/lib/capabilities/types.js +1 -0
  78. package/dist-lib/lib/completion-lifecycle.d.ts +14 -0
  79. package/dist-lib/lib/completion-lifecycle.js +126 -0
  80. package/dist-lib/lib/connect.d.ts +10 -3
  81. package/dist-lib/lib/connect.js +329 -23
  82. package/dist-lib/lib/contract-types.d.ts +19 -2
  83. package/dist-lib/lib/harness-lifecycle.d.ts +66 -0
  84. package/dist-lib/lib/harness-lifecycle.js +129 -0
  85. package/dist-lib/lib/helpers/detach-json.d.ts +1 -0
  86. package/dist-lib/lib/helpers/detach-json.js +146 -0
  87. package/dist-lib/lib/helpers/markup-scanner.d.ts +26 -0
  88. package/dist-lib/lib/helpers/markup-scanner.js +156 -0
  89. package/dist-lib/lib/helpers/markup-stream-filter.js +138 -35
  90. package/dist-lib/lib/helpers/race-abort.d.ts +9 -4
  91. package/dist-lib/lib/helpers/race-abort.js +2 -7
  92. package/dist-lib/lib/helpers/recovery-required.d.ts +1 -0
  93. package/dist-lib/lib/helpers/recovery-required.js +1 -0
  94. package/dist-lib/lib/helpers/run-outcome.d.ts +10 -2
  95. package/dist-lib/lib/helpers/run-outcome.js +8 -5
  96. package/dist-lib/lib/helpers/tool-call-markup.d.ts +2 -0
  97. package/dist-lib/lib/helpers/tool-call-markup.js +197 -82
  98. package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -0
  99. package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -0
  100. package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -0
  101. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  102. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +1 -0
  103. package/dist-lib/lib/hooks/built-in/sandbox.js +6 -1
  104. package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -0
  105. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  106. package/dist-lib/lib/hooks/pipeline.d.ts +4 -1
  107. package/dist-lib/lib/hooks/pipeline.js +24 -2
  108. package/dist-lib/lib/hooks/run.d.ts +2 -1
  109. package/dist-lib/lib/hooks/run.js +19 -7
  110. package/dist-lib/lib/hooks/types.d.ts +17 -6
  111. package/dist-lib/lib/machine/completion/actions/approval.d.ts +3 -0
  112. package/dist-lib/lib/machine/completion/actions/approval.js +16 -0
  113. package/dist-lib/lib/machine/completion/actions/completion.d.ts +23 -0
  114. package/dist-lib/lib/machine/completion/actions/completion.js +142 -0
  115. package/dist-lib/lib/machine/completion/actions/skills.d.ts +9 -0
  116. package/dist-lib/lib/machine/completion/actions/skills.js +83 -0
  117. package/dist-lib/lib/machine/completion/actions/tools.d.ts +16 -0
  118. package/dist-lib/lib/machine/completion/actions/tools.js +173 -0
  119. package/dist-lib/lib/machine/completion/context.d.ts +9 -0
  120. package/dist-lib/lib/machine/completion/context.js +44 -0
  121. package/dist-lib/lib/machine/completion/contract.d.ts +6 -0
  122. package/dist-lib/lib/machine/completion/contract.js +1 -0
  123. package/dist-lib/lib/machine/completion/guards/retry.d.ts +9 -0
  124. package/dist-lib/lib/machine/completion/guards/retry.js +25 -0
  125. package/dist-lib/lib/machine/completion/implementations.d.ts +81 -0
  126. package/dist-lib/lib/machine/completion/implementations.js +152 -0
  127. package/dist-lib/lib/machine/completion/machine.d.ts +375 -0
  128. package/dist-lib/lib/machine/completion/machine.js +511 -0
  129. package/dist-lib/lib/machine/completion/ports.d.ts +65 -0
  130. package/dist-lib/lib/machine/completion/ports.js +224 -0
  131. package/dist-lib/lib/machine/completion/rules.d.ts +39 -0
  132. package/dist-lib/lib/machine/completion/rules.js +115 -0
  133. package/dist-lib/lib/machine/completion/types.d.ts +493 -0
  134. package/dist-lib/lib/machine/completion/types.js +1 -0
  135. package/dist-lib/lib/machine/engine.d.ts +93 -0
  136. package/dist-lib/lib/machine/engine.js +483 -0
  137. package/dist-lib/lib/machine/errors.d.ts +44 -0
  138. package/dist-lib/lib/machine/errors.js +373 -0
  139. package/dist-lib/lib/machine/guards.d.ts +2 -0
  140. package/dist-lib/lib/machine/guards.js +6 -0
  141. package/dist-lib/lib/machine/index.d.ts +20 -0
  142. package/dist-lib/lib/machine/index.js +11 -0
  143. package/dist-lib/lib/machine/replay-output.d.ts +24 -0
  144. package/dist-lib/lib/machine/replay-output.js +50 -0
  145. package/dist-lib/lib/machine/stable-stringify.d.ts +1 -0
  146. package/dist-lib/lib/machine/stable-stringify.js +11 -0
  147. package/dist-lib/lib/models/model-handle.d.ts +5 -1
  148. package/dist-lib/lib/models/model-handle.js +8 -3
  149. package/dist-lib/lib/models/model-list.d.ts +5 -3
  150. package/dist-lib/lib/models/model-pool.d.ts +18 -7
  151. package/dist-lib/lib/models/model-pool.js +126 -38
  152. package/dist-lib/lib/run-log/history-codec.d.ts +23 -0
  153. package/dist-lib/lib/run-log/history-codec.js +973 -0
  154. package/dist-lib/lib/run-log/patch.d.ts +6 -0
  155. package/dist-lib/lib/run-log/patch.js +93 -0
  156. package/dist-lib/lib/run-log/persisted-actor.d.ts +18 -0
  157. package/dist-lib/lib/run-log/persisted-actor.js +218 -0
  158. package/dist-lib/lib/run-log/protocol-types.d.ts +153 -0
  159. package/dist-lib/lib/run-log/protocol-types.js +1 -0
  160. package/dist-lib/lib/run-log/protocol.d.ts +27 -0
  161. package/dist-lib/lib/run-log/protocol.js +1056 -0
  162. package/dist-lib/lib/run-log/replay.d.ts +25 -0
  163. package/dist-lib/lib/run-log/replay.js +129 -0
  164. package/dist-lib/lib/run-log/run-journal.d.ts +125 -0
  165. package/dist-lib/lib/run-log/run-journal.js +783 -0
  166. package/dist-lib/lib/run-log-inspect.d.ts +25 -0
  167. package/dist-lib/lib/run-log-inspect.js +627 -0
  168. package/dist-lib/lib/run-log-protocol.d.ts +2 -0
  169. package/dist-lib/lib/run-log-protocol.js +1 -0
  170. package/dist-lib/lib/run-log-redact.d.ts +27 -0
  171. package/dist-lib/lib/run-log-redact.js +1107 -0
  172. package/dist-lib/lib/serve.js +308 -12
  173. package/dist-lib/lib/spawn.js +3 -2
  174. package/dist-lib/lib/tool-call-display.d.ts +1 -21
  175. package/dist-lib/lib/tools/exec.d.ts +2 -1
  176. package/dist-lib/lib/tools/exec.js +57 -2
  177. package/dist-lib/lib/tools/generate-image.js +1 -1
  178. package/dist-lib/lib/tools/generate-music.js +1 -1
  179. package/dist-lib/lib/tools/http-request.d.ts +2 -1
  180. package/dist-lib/lib/tools/http-request.js +2 -1
  181. package/dist-lib/lib/tools/mcp-call.js +8 -1
  182. package/dist-lib/lib/tools/operations/builders.d.ts +1 -0
  183. package/dist-lib/lib/tools/operations/builders.js +1 -0
  184. package/dist-lib/lib/tools/operations/compile.js +11 -1
  185. package/dist-lib/lib/tools/operations/manifest.js +1 -0
  186. package/dist-lib/lib/tools/outbound-guard.d.ts +8 -6
  187. package/dist-lib/lib/tools/outbound-guard.js +25 -8
  188. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  189. package/dist-lib/lib/tools/skills/catalog.js +35 -0
  190. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +1 -0
  191. package/dist-lib/lib/tools/skills/cli-schema.js +25 -0
  192. package/dist-lib/lib/wire.d.ts +5 -0
  193. package/dist-lib/lib/wire.js +128 -5
  194. package/dist-lib/schema/harness.js +22 -6
  195. package/dist-lib/spec/hrpc/messages.js +100 -18
  196. package/dist-lib/spec/hyperschema/index.js +100 -18
  197. package/dist-lib/spec/hyperschema/schema.json +74 -1
  198. package/dist-lib/spec/hyperschema/types.d.ts +11 -3
  199. package/package.json +19 -1
  200. package/patches/@qvac%2Finference@0.20.0.patch +67 -0
  201. package/patches/bare-stow@0.1.5.patch +68 -0
  202. package/patches/hypercore-blob-server@1.15.0.patch +15 -0
  203. package/patches/localwatch@1.4.3.patch +13 -0
  204. package/patches/pyodide@314.0.2.patch +144 -0
package/CHANGELOG.md CHANGED
@@ -8,6 +8,46 @@ While the major version is `0`, a breaking change bumps the minor version.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.7.1] - 2026-09-30
12
+
13
+ ### Added
14
+
15
+ - `list()` rows carry `isLoading`: true from the moment a load of that model
16
+ starts, whoever asked for it, until it is ready or the load fails. It is read
17
+ off the pool, so a host shows a model as loading without counting loads
18
+ itself. A model loading again under other settings while a copy of it is
19
+ resident reads `isLoaded` and `isLoading` both true. Chat models read it
20
+ today: image, music and translation models load for each generation, and
21
+ speech, transcription and embedding pools key by source, so those rows read
22
+ `false` for now. An optional field on the wire; a row from an older harness
23
+ reads `false`. (#143)
24
+
25
+ ```js
26
+ const loading = (await harness.list({ cachedOnly: true })).filter((row) => row.isLoading)
27
+ ```
28
+
29
+ ### Changed
30
+
31
+ - The `model` event carries `catalogChanged` at every edge a `list()` re-read
32
+ would show: the `loading` that starts a load and the `ready` or `error` that
33
+ ends it, beside `unloaded`. Progress ticks still do not. A host that re-reads
34
+ the list on `catalogChanged` sees a model turn loading, then loaded, with no
35
+ bookkeeping of its own. The served event stream never drops a frame that
36
+ carries it, so two `ready` frames of one model under two keys both cross. (#143)
37
+ - `ModelInfo` requires `isLoading`, and `listModels`, `customModelRows` and
38
+ `mergeCustomRows` return `CatalogRow`, the row before the pools are asked. A
39
+ typed fake `list()` adds the field. (#143)
40
+ - A custom model's row reads `isLoaded` only once its load has landed; it used
41
+ to read `true` from the first attempt. (#143)
42
+
43
+ ### Fixed
44
+
45
+ - A load that ends without the model in memory emits `model` with `error` and
46
+ the reason: it failed, or a suspend or close cancelled it. It used to end in
47
+ silence, so a host could not tell a failed load from one still running. The
48
+ `error` also refreshes the cached `isCached`, since a load may have downloaded
49
+ the weights before it failed. (#143)
50
+
11
51
  ## [0.7.0] - 2026-09-29
12
52
 
13
53
  ### Added
@@ -979,7 +1019,8 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
979
1019
  - GPU stats are read through `bare-gpu-info`, so every dependency installs from
980
1020
  the public npm registry. (#12)
981
1021
 
982
- [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.7.0...HEAD
1022
+ [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.7.1...HEAD
1023
+ [0.7.1]: https://github.com/tetherto/qvac-harness/compare/v0.7.0...v0.7.1
983
1024
  [0.7.0]: https://github.com/tetherto/qvac-harness/compare/v0.6.0...v0.7.0
984
1025
  [0.6.0]: https://github.com/tetherto/qvac-harness/compare/v0.5.0...v0.6.0
985
1026
  [0.5.0]: https://github.com/tetherto/qvac-harness/compare/v0.4.0...v0.5.0
package/README.md CHANGED
@@ -165,6 +165,7 @@ const custom = models.filter((model) => model.registrySource === 'custom')
165
165
  // registrySource: 'custom',
166
166
  // isCached: false,
167
167
  // isLoaded: false,
168
+ // isLoading: false,
168
169
  // supportsImages: false,
169
170
  // expectedSize: 1868506272,
170
171
  // label: 'Nanbeige 4.2 3B',
@@ -234,8 +235,20 @@ if (big.reason === 'memory') showNotice('Unload a model first')
234
235
  `unloadModel` waits for any request holding the model to finish, then unloads
235
236
  every copy of it. `unloaded` is false when a request still held it after the
236
237
  wait, so a caller that wants the model gone at once stops those requests first.
237
- Both change `isLoaded` in the next `list()`, and emit a `model` event with
238
- `catalogChanged` set. A load or an unload counts as a call in flight for
238
+ Both change `isLoaded` in the next `list()`. `isLoading` is true on a row from
239
+ the moment a load of its model starts, whoever asked for it (a request,
240
+ `warmModel` or `loadModel`), until the model is ready or the load fails; it is
241
+ read off the pool, so a host keeps no count of its own. A model loading again
242
+ under other settings while a copy of it is resident reads `isLoaded: true` and
243
+ `isLoading: true` at once. The `model` event carries `catalogChanged` at each
244
+ edge a `list()` re-read would show: the `loading` that starts a load, the
245
+ `ready` or `error` that ends it, and `unloaded`. Progress ticks between them do
246
+ not. A load that ends without the model in memory reports
247
+ `error` with the reason in `message`: it failed, or a suspend or close cancelled it.
248
+ An image, music or translation model loads for each generation and never reads
249
+ `isLoading: true`; a speech, transcription or embedding model loads under its
250
+ source rather than its row name, so its row reads `false` for now. A load or an
251
+ unload counts as a call in flight for
239
252
  [`setModelIdleTimeout`](#freeing-memory-when-idle), and one that lands during an
240
253
  idle unload waits for it, so a load never loads a second copy and an unload
241
254
  answers once the memory is free.
@@ -295,6 +308,16 @@ bun run build:schema # regenerates spec/ (wire-compat rewind once v* tags exist
295
308
  bun run build:bundle # stows the sidecar entry into dist/, to prove it still stows
296
309
  ```
297
310
 
311
+ ## Engine layout
312
+
313
+ `lib/machine/` contains `completion/**`, `engine.ts`, `errors.ts`, `index.ts`, `replay-output.ts`,
314
+ `stable-stringify.ts`, and `guards.ts`. The run-log layer is in `lib/run-log/`:
315
+ `protocol.ts`, `protocol-types.ts`, `history-codec.ts`, `run-journal.ts`,
316
+ `persisted-actor.ts`, `replay.ts`, and `patch.ts`.
317
+
318
+ The completion graph has 24 states and 61 transitions. Reproduce these
319
+ counts with `examples/bench/graph-metrics.ts`.
320
+
298
321
  The schema baseline rewinds `spec/` against this repo's `v*` release tags and
299
322
  skips loudly while none exist. Dependency `overrides` pin the resolutions the
300
323
  engine is validated against — keep them aligned with direct specs, npm refuses
@@ -303,3 +326,217 @@ a mismatch on publish.
303
326
  ## License
304
327
 
305
328
  See [LICENSE](./LICENSE.md).
329
+
330
+ ## Resuming a completion
331
+
332
+ The harness emits resumable steps to a host-owned log. Direct `Harness.completion()` runs without
333
+ that host log work normally, but cannot recover after process death. For remote runs, pass a
334
+ `HostRunLog` to `connectHarness(...).completion()` and reuse the same `runId` when reconnecting.
335
+ The peer must advertise `hostRunLogVersion: 1`.
336
+
337
+ ```ts
338
+ const request = { runId: 'draft-42', model, messages, agent, runLog }
339
+ for await (const event of remote.completion(request)) {
340
+ // Save the run ID and render completion events.
341
+ }
342
+
343
+ // begin() returns the saved boot for this run. connectHarness resumes from it and sends the
344
+ // same request identity; supply current callbacks, tool configs, and filesystem grants again.
345
+ for await (const event of remote.completion({ runId: 'draft-42', resume: true, runLog, approve })) {
346
+ // Handle run-resumed, round-reset, content, tool activity, and paused.
347
+ }
348
+ ```
349
+
350
+ `HostRunLog.begin(runId)` acquires one exclusive generation and returns its compact boot, if the
351
+ run already exists, plus a 32-byte `digestKey`. Each `append(runId, generation, step)` must durably
352
+ commit the whole `EngineStep`—its `seq`, expected logical record length, and records—in one
353
+ transaction. The client waits for that commit before continuing. `release(runId, generation)` fences
354
+ late appends from the ended connection. A host may export its authenticated records for inspection;
355
+ the harness does not fetch storage or decrypt a host's log itself.
356
+
357
+ `hostAppendTimeoutMs` (default 5000) bounds that wait; it applies in full even to a resume issued
358
+ after the run's own deadline has passed. A spawned host raises it via the `runLog` start payload
359
+ when its appends (e.g. a large first append on resume) can take longer than the default.
360
+
361
+ Cancellation, iterator disposal, suspension, shutdown, and remote disconnect pause a run. A
362
+ `paused` event ends that stream without making the run terminal. Resume retains completed tool
363
+ results; time spent paused does not count against the run's deadline. A `round-reset` event identifies
364
+ the unfinished round. Ordinary resets carry no `replacedTypes`; only the terminal replay reset
365
+ (when resuming) lists the output kinds to replace. Both replace content and thinking. Text and
366
+ tool events carry run/round identities; tool events also carry their call identity. Completed runs
367
+ replay their final answer and metrics without loading a model.
368
+
369
+ An unfinished write produces `recovery-required` with an `operation` containing its ID, name,
370
+ resolved arguments, and effect. A write that the pause stopped before it started runs again. Reconcile the external result, then supply exactly that operation's
371
+ resolution on resume:
372
+
373
+ ```ts
374
+ remote.completion({
375
+ runId: 'draft-42',
376
+ resume: true,
377
+ model,
378
+ messages,
379
+ agent,
380
+ recovery: [{ operationId: operation.id, decision: 'result', result: confirmedResult }],
381
+ runLog,
382
+ approve
383
+ })
384
+
385
+ // If re-execution is appropriate, explicitly authorize it instead:
386
+ // recovery: [{ operationId: operation.id, decision: 'retry' }]
387
+ ```
388
+
389
+ A retry can repeat an external effect. Receipts prevent replay of completed operations; they do
390
+ not provide transactions or exactly-once effects in external systems. Missing runtime callbacks or
391
+ a model that cannot load leave the run paused with a recovery reason. For these runtime/read
392
+ reasons, restore the dependencies and resume without a `recovery` resolution. A tool that is gone
393
+ or whose schema changed since the pause ends the run with an error, because no resolution can
394
+ bring the old tool back. Stale resolutions and attempts to change the original conversation or
395
+ model are rejected.
396
+
397
+ A round can pause with one call's arguments redacted, whether or not a sibling call in the same
398
+ round already landed or was already dispatched. Resuming refuses with `PENDING_ARGS_REDACTED`
399
+ instead of replaying the redacted call. Its `recoveryOperationIds` lists every call the round still
400
+ needs settled before it can proceed: a dispatched write not yet resolved (redacted or not), or a
401
+ call whose real arguments exist only in the redacted snapshot, whether dispatched as a read or
402
+ never dispatched at all. A plain call that was never redacted and never dispatched is not listed;
403
+ it simply runs when the round resumes. Settle any listed operation with `decision: 'result'` and
404
+ the known outcome. `decision: 'skip'` is also available when the host knows a redacted operation
405
+ did not land, or for a read; it records a readable placeholder body,
406
+ `{ skipped: true, reason: 'not executed; arguments unavailable after resume' }`, never the literal
407
+ `undefined`. For a dispatched redacted write, `skip` asserts the call never landed; if the host
408
+ skips a write that did land, the model sees "not executed" and may issue it again. It remains
409
+ refused for a dispatched write whose arguments are available. The `retry` decision re-dispatches a
410
+ call still pending; it is refused once that call already has a settled result, for a call never
411
+ dispatched at all, and for a dispatched call whose arguments were redacted, which re-raises
412
+ `PENDING_ARGS_REDACTED` instead of retrying blind. Settling every listed operation finishes the
413
+ round; the landed write is never repeated, though a redacted read may run again. When no operation
414
+ can be reconciled this way, the error's `forkAt` gives the last log length that never held redacted
415
+ arguments as real ones, and `forkSafe` says whether forking there is actually safe: `false` means a
416
+ write whose intent starts at or after `forkAt` has an unknown outcome — its result already landed, or it was dispatched
417
+ and never resolved — so a fork could repeat it and the host should resolve `recoveryOperationIds`
418
+ instead. `forkRun(sourceCore, targetCore, { at: forkAt, runId })` copies the log up to that point
419
+ under a new `runId`, and the round reruns from there.
420
+
421
+ Tool definitions are checked again on resume, including their function source. A tool whose
422
+ behavior depends on values captured in a closure must supply and update `policyVersion` when
423
+ those values change; function source cannot reveal them. The `exec` and `http_request` factories
424
+ accept this value as an option; `exec` also fingerprints its static path and Python settings.
425
+
426
+ Tool configuration values are not stored in the log; a digest verifies that the caller supplies
427
+ the same configuration on resume. Credentials embedded in those values must currently remain
428
+ unchanged. Credentials managed through `setCredentials()` or runtime callbacks can be refreshed
429
+ without changing the request.
430
+
431
+ For host-owned logs, resume uses the boot returned by `begin()` and does not require the peer's
432
+ local `completionRecovery` capability. A transport `done` frame only means the stream ended; it
433
+ does not mean the run succeeded. Run-log and machine versions must match. XState restore accepts
434
+ older stable patches in the installed minor; future patches, other minors, and prereleases are
435
+ rejected. The first checkpoint and child actor snapshots are trusted inputs; diagnostic replay does
436
+ not verify actor effects or the initial context.
437
+
438
+ `connectHarness(transport, { runLogReleaseTimeoutMs })` bounds host lease cleanup to 5 seconds
439
+ by default. A rejected, throwing, or timed-out release is logged without replacing the completion
440
+ outcome. The host must keep generation fencing active until release actually settles; a cleanup
441
+ timeout does not authorize another writer.
442
+
443
+ Each run's metadata contains a JSON-only definition of the machine, including state hierarchy,
444
+ initial states, tags, invoked actors, and named transition guards/actions. Checkpoints and deltas
445
+ also carry the exact event, source/target state IDs, fired guard names, and action names observed in
446
+ XState's transition trace. The inspector displays these recorded traces and can replay state/context
447
+ at the same machine and XState versions; event-triggered guard/action traces are re-evaluated and
448
+ must match the recorded trace exactly, or replay reports a divergence. Eventless traces (no
449
+ triggering event, e.g. `always` transitions) are not replay-verified; the inspector marks each such
450
+ line `[eventless; not replay-verified]`. A version mismatch, legacy log, or prefix selection skips
451
+ replay and leaves the stored snapshot and trace authoritative. The checked-in [completion
452
+ graph](./docs/statecharts/completion-machine.mmd) is generated from the same machine definition and
453
+ CI rejects a stale artifact.
454
+
455
+ Export records from the host, then inspect a selected logical record or physical core length:
456
+
457
+ ```sh
458
+ bare examples/run-log/inspect.ts <records.json> --context <recordIndex>
459
+ bare examples/run-log/view.ts <records.json> report.html --limit <recordIndex>
460
+ ```
461
+
462
+ Logical record index, engine step sequence, and physical storage length are different positions;
463
+ host exports can include the mapping. The CLI reads exported records only. The host remains
464
+ responsible for remote lookup, authentication, decryption, and sharing access.
465
+
466
+ `@qvac/harness/run-log-inspect` is the library behind that CLI: `inspectRun(core, opts)` yields the
467
+ same lines the `inspect.ts` example prints, and `renderRunLogHtml(core, opts)` renders the HTML
468
+ report `view.ts` writes. A host imports either directly instead of shelling out to the examples,
469
+ passing a `MachineLog` adapter or an envelope whose `records()` method returns an async iterable
470
+ of `{ recordIndex, stepSeq, physicalLength, record }` entries. A raw records array or parsed JSON
471
+ envelope must first be adapted; the CLI's `recordsCore` helper shows this conversion. Options are
472
+ `recordLimit` (an exclusive logical index), `contextRecord`, `physicalLength`, and
473
+ `contextPhysicalLength`.
474
+
475
+ The completion machine is internal: its state names are part of the run-log format, not the API.
476
+ Hook consumers observe `runPause` and `runResume` for a run that can resume. A run with no run-log
477
+ store cannot resume, so a pause ends it and `runEnd` reports `aborted` (the caller aborted) or
478
+ `abandoned` (the consumer stopped iterating).
479
+
480
+ ## Tool-call markup grammar
481
+
482
+ A tool-call block opens with `<function`, up to 64 ASCII whitespace characters, `=`, up to 64
483
+ more ASCII whitespace characters, a name, up to 64 ASCII whitespace characters, and `>`. Tags are
484
+ case-insensitive, except in the legacy nested reading below. An executable name is 1–64
485
+ non-whitespace characters other than `<` or `>`; persistence also recognizes longer names so an
486
+ oversized call cannot bypass redaction. A parameter uses the same form with `parameter` in place
487
+ of `function`, and ends at the first case-insensitive
488
+ `</parameter>`. A function block ends at the first case-insensitive `</function>`. Anything after
489
+ that closer is prose and cannot extend the call, including more parameters or closers.
490
+
491
+ Text that does not form a complete opener and closed block is prose. In particular, empty names,
492
+ overlong executable names, bare parameter tags, and function-tag examples without a matching
493
+ closer are neither executed nor hidden from the displayed answer. Stray function closers, including
494
+ `</FUNCTION>`, also stay visible. A function opener inside a function block or a parameter opener
495
+ inside a parameter value makes that block ambiguous, and persistence redacts it conservatively.
496
+ A block with a nested function opener keeps the legacy recovery reading: the first lowercase
497
+ `<function=` with no whitespace before `=` names the call. The text between `=` and `>` is limited
498
+ to 64 characters before the name is trimmed. It runs with complete lowercase
499
+ parameters up to the first lowercase `</function>` within that block. That reading ignores other
500
+ function openers, so `<function=a><function=login>…` runs `a`,
501
+ `<FUNCTION=a><function=login>…` and `<function =a><function=login>…` run `login`, and
502
+ `<FUNCTION=a><FUNCTION=login>…` runs nothing. Likewise,
503
+ `<function=x><function= login ><parameter=path>p</parameter></function>` runs `x` with `path: "p"`
504
+ when `x` is a known tool. An unclosed parameter, such as `<parameter=key>S</function>`, contributes
505
+ no argument and does not itself make the function block ambiguous. Persistence still redacts its
506
+ value when the name is a credential field or a registered sensitive parameter, including in prose.
507
+
508
+ ## Run-log data and privacy
509
+
510
+ The harness redacts structured values before it sends a durable step to the host. String or object
511
+ values under exact credential field names are redacted. Numeric values under known credential
512
+ names are also redacted; generic `token` counts and `secret` flags are preserved. Registered tool
513
+ parameters marked `sensitive: true` are redacted regardless of name or value type. Known names include `db_password`, `GITHUB_TOKEN`,
514
+ `openai_api_key`, `client_password`, and `client_passphrase` (case, underscores, and hyphens
515
+ are normalized).
516
+ Name-based redaction is best effort: provider key names such as `ANTHROPIC_API_KEY`, `GH_TOKEN`,
517
+ and `PGPASSWORD` are not covered, and a bare `token` argument (e.g. a pagination cursor) is
518
+ redacted and lost on resume just like a real secret. The durable way to mark a value as a secret,
519
+ regardless of its name, is the schema's `sensitive: true` annotation.
520
+ Encoded assistant tool calls are redacted when history crosses persistence. Within one live process,
521
+ a model's next request still sees its own prior call's real arguments, because that history lives
522
+ only in the actor's memory. A **resumed** run has no such memory: its context is rebuilt from the
523
+ persisted (redacted) log, so the model's first request after resume sees `[redacted]` for any tool
524
+ call it made before the run was interrupted, not the original argument; that argument cannot be
525
+ recovered. The model sees that prior call as well-formed markup with the literal `"[redacted]"`
526
+ value, not an error; a model that re-issues such a call runs it with that literal placeholder.
527
+ A persisted `raw` or `rawCall` of 4,096 characters or more is rebuilt from its (redacted) arguments
528
+ even when nothing is sensitive, so it may differ byte-for-byte from the live call's `rawCall`.
529
+ Shorter unchanged text stays identical to the original.
530
+ Other arbitrary strings, tool results, and prose are preserved. Persisted machine
531
+ history can reference matching original request messages by index and materializes to the same
532
+ messages on resume and inspection — this applies to the caller-supplied conversation the run
533
+ started from, which is never redacted, not to arguments the model generated mid-run. Hosts must
534
+ treat free text and results as confidential and
535
+ protect their log with encryption and access control. The host's `digestKey` verifies request
536
+ identity and never enters a run record.
537
+
538
+ A pending tool call whose arguments were redacted cannot be executed on resume and is rejected.
539
+ A literal `[redacted]` supplied by the user is not enough to trigger that check. Tool configuration
540
+ values are not stored in the run log; a keyed digest verifies that the caller supplies the same
541
+ configuration on resume. Credentials managed through `setCredentials()` or runtime callbacks can
542
+ be refreshed without changing the request identity.
@@ -1,5 +1,6 @@
1
1
  import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
2
2
  import ReadyResource from 'ready-resource';
3
+ import type { EngineBoot, EngineStep, RecoveryResolution } from './lib/machine/index.ts';
3
4
  import { type Agent, type ApproveFn, type AskUserFn, type AttachFn, type CompletionEvent, type FetchAttachmentFn, type Message, type ProgressFn, type RagSearchFn, type ResponseFormat, type ToolDefinition, type ToolExecuteContext } from './lib/capabilities/completion.ts';
4
5
  import { type DetectLanguageContext, type DetectedLanguage } from './lib/capabilities/detect-language.ts';
5
6
  import { type Capability } from './lib/capabilities/invoke.ts';
@@ -40,6 +41,7 @@ export * from './lib/models/model-catalog.ts';
40
41
  export * from './lib/models/model-list.ts';
41
42
  export type { ModelTaskType } from './lib/models/model-task.ts';
42
43
  export type { DeleteModelResult, DeleteModelSummary } from './lib/models/model-delete.ts';
44
+ export type { RecoveryResolution, RecoveryOperation } from './lib/machine/index.ts';
43
45
  export { harnessChild, spawnHarness, type HarnessChildOptions, type SpawnHarnessOptions } from './lib/spawn.ts';
44
46
  export { settingsDimension, settingsSteps } from './lib/tools/generate-image.ts';
45
47
  export * from './lib/tools/skills/catalog.ts';
@@ -52,6 +54,7 @@ export { readSetup as normalizeSkillSetup } from './lib/tools/skills/manifest-pa
52
54
  export type { CredentialRecord, CredentialStoreValue } from './lib/tools/skills/auth-resolver.ts';
53
55
  export { isAvailableState, SKILL_STATE, type SkillState } from './lib/tools/skills/skill-state.ts';
54
56
  export { connectHarness, HARNESS_DIED, RemoteHarness } from './lib/connect.ts';
57
+ export type { ConnectOptions, HostRunLog, HostRunLogLease } from './lib/connect.ts';
55
58
  export type { KvCacheDeletionResult } from './lib/capabilities/kv-cache.ts';
56
59
  export type { AgentKnobs, ApprovalCall, ChatMessage, CredentialEntry, DownloadContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, HarnessStatus, InfraModel, ModelState, SkillsOptions, SkillWriteResult } from './lib/contract-types.ts';
57
60
  import type { HarnessInfo } from './lib/contract-types.ts';
@@ -96,6 +99,9 @@ export interface HarnessOptions {
96
99
  completionModelCap?: number;
97
100
  }
98
101
  export interface CompletionContext {
102
+ runId?: string;
103
+ resume?: boolean;
104
+ recovery?: RecoveryResolution[];
99
105
  model: string;
100
106
  agent?: Agent;
101
107
  messages: Message[];
@@ -115,8 +121,16 @@ export interface CompletionContext {
115
121
  ragSearch?: RagSearchFn;
116
122
  progress?: ProgressFn;
117
123
  askUser?: AskUserFn;
124
+ boot?: EngineBoot;
125
+ onStep?: (step: EngineStep) => Promise<void>;
126
+ hostAppendTimeoutMs?: number;
127
+ requestDigestKey?: Uint8Array;
118
128
  signal?: BareAbortSignal;
119
129
  }
130
+ export type ResumeCompletionContext = Omit<Partial<CompletionContext>, 'runId' | 'resume'> & {
131
+ runId: string;
132
+ resume: true;
133
+ };
120
134
  export interface InvokeContext {
121
135
  capability: Capability;
122
136
  args: Record<string, unknown>;
@@ -145,6 +159,7 @@ export declare class Harness extends ReadyResource {
145
159
  private _models;
146
160
  private _embedModels;
147
161
  private readonly _retiredEmbedModels;
162
+ private readonly _faultedEmbedKeys;
148
163
  private _embedLane;
149
164
  private _embedFaults;
150
165
  private readonly _embedGpuLayers?;
@@ -153,7 +168,20 @@ export declare class Harness extends ReadyResource {
153
168
  private _ttsModels;
154
169
  private _transcriptionModels;
155
170
  private _voiceLane;
156
- private _state;
171
+ private readonly _lifecycle;
172
+ private readonly _detachedRuns;
173
+ private readonly _effectDrainWaiters;
174
+ private _activeWork;
175
+ private readonly _workDrainWaiters;
176
+ private _runtimeDraining;
177
+ private _runtimeUnloadFailed;
178
+ private _poolUnloadFailed;
179
+ private _poolCleanupFailed;
180
+ private readonly _failedModelUnloads;
181
+ private _sdkClosed;
182
+ private _runtimesReleased;
183
+ private _closeRetryNeeded;
184
+ private _closeRequested;
157
185
  private _skillsDir?;
158
186
  private readonly _modelsDir?;
159
187
  private _overlayWatcher;
@@ -178,6 +206,7 @@ export declare class Harness extends ReadyResource {
178
206
  private _musicSdk;
179
207
  private _translationSdk;
180
208
  private _musicRuntime;
209
+ private readonly _completionRuns;
181
210
  private _translationRuntime;
182
211
  private _capabilityTools;
183
212
  private _capability;
@@ -218,6 +247,9 @@ export declare class Harness extends ReadyResource {
218
247
  describe(): Promise<HarnessInfo>;
219
248
  diagnostics(): HarnessDiagnostics;
220
249
  private _loadedModels;
250
+ private _loadingModels;
251
+ private _settledModels;
252
+ private _pools;
221
253
  private _loadPluginSdk;
222
254
  private _loadDiffusionSdk;
223
255
  private _slmTranslate;
@@ -225,26 +257,46 @@ export declare class Harness extends ReadyResource {
225
257
  private _loadMusicSdk;
226
258
  private _deviceConfig;
227
259
  private _createPool;
260
+ private _loadModelLifecycleSdk;
228
261
  private _createEmbedPool;
229
262
  private _createTtsPool;
230
263
  private _createTranscriptionPool;
264
+ private _recordModelUnloadFailure;
231
265
  private _notifyModel;
232
266
  private _collectRetiredEmbedPool;
233
267
  private _releaseEmbedResource;
268
+ private _releaseEmbedResourceBeforeWork;
269
+ private _markEmbedFault;
234
270
  private _releaseEmbedResourceAfter;
235
271
  private _destroyEmbedPools;
236
272
  private _enqueueEmbedControl;
273
+ private _runLifecycle;
274
+ private _abortCompletions;
275
+ private _assertNotClosing;
276
+ private _enterWork;
277
+ private _waitForWorkDrain;
278
+ private _effectsSettled;
279
+ private _notifyEffectDrain;
280
+ private _waitForEffectDrain;
281
+ private _trackDetachedRun;
282
+ private _closeCompletions;
237
283
  getEmbeddingDevice(): EmbedDevice;
238
284
  setEmbeddingDevice(device: EmbedDevice): Promise<void>;
239
285
  private _setEmbeddingDevice;
240
286
  presuspend(): Promise<void>;
241
287
  suspend(): Promise<void>;
288
+ private _suspendRuntimes;
242
289
  private _sdkLifecycle;
243
290
  private _releaseRuntimes;
244
291
  private _unloadIdleModels;
245
292
  resume(): Promise<void>;
293
+ private _resumeRuntimes;
246
294
  wakeup(): Promise<void>;
295
+ ready(): Promise<void>;
247
296
  _close(): Promise<void>;
297
+ close(): Promise<void>;
298
+ private _closeWhileOpening;
299
+ private _finishClose;
248
300
  download(args: DownloadArgs): DownloadEventStream;
249
301
  embed(context: EmbedContext): Promise<number[][]>;
250
302
  private _embed;
@@ -275,10 +327,13 @@ export declare class Harness extends ReadyResource {
275
327
  private _yieldPins;
276
328
  private _residency;
277
329
  private _completionLoadOptions;
330
+ private _acquireModelLease;
278
331
  invoke(context: InvokeContext): AsyncGenerator<CompletionEvent>;
279
332
  private _invoke;
280
- completion(context: CompletionContext): AsyncGenerator<CompletionEvent>;
281
- private _completion;
333
+ completion(context: CompletionContext | ResumeCompletionContext): AsyncGenerator<CompletionEvent>;
334
+ private _runCompletion;
335
+ private _completionFailed;
336
+ private _completionRuntime;
282
337
  private _residentEnhancer;
283
338
  private _makeRoomForCompletion;
284
339
  private _evictWedgedModel;