@qvac/harness 0.8.5 → 0.8.7
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 +1278 -472
- package/README.md +49 -0
- package/dist-lib/index.d.ts +42 -2
- package/dist-lib/index.js +447 -58
- package/dist-lib/lib/audio-transcode-bare.d.ts +1 -1
- package/dist-lib/lib/audio-transcode-bare.js +22 -25
- package/dist-lib/lib/audio-transcode-node.d.ts +1 -1
- package/dist-lib/lib/audio-transcode-node.js +1 -3
- package/dist-lib/lib/capabilities/audio-resample.d.ts +1 -1
- package/dist-lib/lib/capabilities/audio-resample.js +7 -8
- package/dist-lib/lib/capabilities/completion.d.ts +6 -3
- package/dist-lib/lib/capabilities/completion.js +96 -49
- package/dist-lib/lib/capabilities/download.d.ts +3 -1
- package/dist-lib/lib/capabilities/download.js +1 -1
- package/dist-lib/lib/capabilities/preview-model-source.d.ts +1 -0
- package/dist-lib/lib/capabilities/preview-model-source.js +1 -0
- package/dist-lib/lib/capabilities/tool-selection.d.ts +1 -3
- package/dist-lib/lib/capabilities/tool-selection.js +2 -4
- package/dist-lib/lib/capabilities/tool-turns.d.ts +0 -1
- package/dist-lib/lib/capabilities/tool-turns.js +0 -6
- package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
- package/dist-lib/lib/capabilities/voice.d.ts +5 -1
- package/dist-lib/lib/capabilities/voice.js +94 -26
- package/dist-lib/lib/connect.d.ts +4 -1
- package/dist-lib/lib/connect.js +16 -1
- package/dist-lib/lib/contract-types.d.ts +12 -0
- package/dist-lib/lib/helpers/tool-call-markup.d.ts +1 -0
- package/dist-lib/lib/helpers/tool-call-markup.js +3 -0
- package/dist-lib/lib/models/context-choice.d.ts +15 -0
- package/dist-lib/lib/models/context-choice.js +55 -0
- package/dist-lib/lib/models/download-gate.d.ts +20 -0
- package/dist-lib/lib/models/download-gate.js +73 -0
- package/dist-lib/lib/models/memory-cap.d.ts +13 -0
- package/dist-lib/lib/models/memory-cap.js +21 -0
- package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
- package/dist-lib/lib/models/model-catalog-constants.js +9 -1
- package/dist-lib/lib/models/model-catalog.d.ts +6 -2
- package/dist-lib/lib/models/model-catalog.js +35 -3
- package/dist-lib/lib/models/model-handle.d.ts +8 -1
- package/dist-lib/lib/models/model-handle.js +20 -2
- package/dist-lib/lib/models/model-list.d.ts +8 -1
- package/dist-lib/lib/models/model-list.js +23 -0
- package/dist-lib/lib/models/model-pool.d.ts +35 -5
- package/dist-lib/lib/models/model-pool.js +97 -32
- package/dist-lib/lib/models/sdk-faults.d.ts +1 -0
- package/dist-lib/lib/models/sdk-faults.js +6 -0
- package/dist-lib/lib/office/engine/pptx/picture-area.d.ts +23 -0
- package/dist-lib/lib/office/engine/pptx/picture-area.js +84 -0
- package/dist-lib/lib/office/engine/pptx/presentation.d.ts +2 -0
- package/dist-lib/lib/office/engine/pptx/presentation.js +55 -17
- package/dist-lib/lib/office/engine/pptx/types.d.ts +1 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +1 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.js +34 -3
- package/dist-lib/lib/serve.js +23 -3
- package/dist-lib/lib/spawn.js +4 -10
- package/dist-lib/lib/tools/exec.d.ts +4 -1
- package/dist-lib/lib/tools/exec.js +55 -11
- package/dist-lib/lib/tools/generate-image.d.ts +4 -2
- package/dist-lib/lib/tools/generate-image.js +5 -2
- package/dist-lib/lib/tools/generate-music.d.ts +4 -2
- package/dist-lib/lib/tools/generate-music.js +5 -2
- package/dist-lib/lib/tools/office/ops.js +1 -1
- package/dist-lib/lib/tools/office.js +29 -6
- package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
- package/dist-lib/lib/tools/skills/catalog.js +2 -0
- package/dist-lib/lib/tools/skills/cli-schema-check.d.ts +1 -0
- package/dist-lib/lib/tools/skills/cli-schema-check.js +3 -0
- package/dist-lib/lib/tools/skills/cli-schema-load.js +10 -0
- package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +2 -0
- package/dist-lib/lib/tools/skills/cli-schema.js +6 -3
- package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +1 -0
- package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
- package/dist-lib/lib/tools/skills/manifest-parse.js +12 -0
- package/dist-lib/lib/tools/skills/mention.d.ts +0 -1
- package/dist-lib/lib/tools/skills/mention.js +0 -3
- package/dist-lib/lib/tools/translate.d.ts +3 -1
- package/dist-lib/lib/tools/translate.js +6 -1
- package/dist-lib/schema/harness.js +43 -4
- package/dist-lib/spec/bind.js +9 -0
- package/dist-lib/spec/calls.js +3 -0
- package/dist-lib/spec/hrpc/hrpc.json +39 -0
- package/dist-lib/spec/hrpc/index.js +39 -3
- package/dist-lib/spec/hrpc/messages.js +101 -13
- package/dist-lib/spec/hyperschema/index.js +101 -13
- package/dist-lib/spec/hyperschema/schema.json +62 -1
- package/dist-lib/spec/hyperschema/types.d.ts +20 -2
- package/package.json +8 -10
- package/worker/harness-sidecar.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,43 +8,308 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.8.7] - 2026-10-12
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A skill's manifest may declare `env`, the variables its `exec` commands run
|
|
16
|
+
with. The child process env is an allowlist, so this is the only way a
|
|
17
|
+
skill's binary sees a setting of its own. The value `${session}` expands to
|
|
18
|
+
the chat the run belongs to, which lets a long-lived tool such as a browser
|
|
19
|
+
daemon keep one instance per chat; a run with no session drops that variable.
|
|
20
|
+
An overlay skill keeps the base skill's `env`, so an edit never widens what
|
|
21
|
+
a command can reach. `completion()` takes an optional `session`, in process
|
|
22
|
+
and over hrpc, and falls back to the first segment of `cacheKey`. (#341)
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
metadata:
|
|
26
|
+
env:
|
|
27
|
+
AGENT_BROWSER_SESSION: '${session}'
|
|
28
|
+
AGENT_BROWSER_HEADED: '1'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- A verb in a skill's `cli.schema.json` may carry `x-max-output-chars`, the
|
|
32
|
+
most output `exec` returns for that command. The ceiling wins over the
|
|
33
|
+
model's own `maxOutputChars`, the truncation note says which limit applied,
|
|
34
|
+
and a schema whose cap is not a positive integer fails to load. Page
|
|
35
|
+
snapshots and other verbose read verbs stay inside the context this way.
|
|
36
|
+
(#342)
|
|
37
|
+
|
|
38
|
+
- The `browser` skill ships with `@qvac/skills` 0.1.14: an agent drives a
|
|
39
|
+
Chromium through the `agent-browser` CLI, scoped to `exec(agent-browser)`,
|
|
40
|
+
with one browser session per chat. (#405)
|
|
41
|
+
|
|
42
|
+
- A chat model's context climbs as far as the device and the SDK allow when
|
|
43
|
+
neither the agent nor the host sets one: 262,144, then 131,072, then 65,536,
|
|
44
|
+
before the default. The device class caps the climb, the trained window from
|
|
45
|
+
the weight's GGUF header bounds it, and each rung must pass `assessModelFit`
|
|
46
|
+
with its own config. The choice is kept per pool key for the life of the
|
|
47
|
+
process. See the
|
|
48
|
+
[decision record](https://github.com/tetherto/qvac-harness/blob/v0.8.7/docs/decisions/2026-10-08-the-device-class-sets-how-far-the-context-climbs.md).
|
|
49
|
+
(#382)
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **Breaking for hosts:** a skill named with `@` or in prose now reaches the
|
|
54
|
+
model as a `skill` call, its result and the continuation nudge right after the
|
|
55
|
+
request, exactly the round a load the model makes itself leaves, instead of
|
|
56
|
+
being written into the user's message. The user's turn is sent as typed and
|
|
57
|
+
the run stays on the chat's KV cache key, so its rounds and the turn after it
|
|
58
|
+
read the cached prefix; the child key 0.8.6 ran these turns under (#305) and
|
|
59
|
+
its delete are gone. The host must replay each run's turns as it sent them
|
|
60
|
+
(`@qvac/core` does from 0.7.0, tetherto/qvac-core#338): a host that collapses
|
|
61
|
+
a run to its request and answer leaves the cache's message count stale, and
|
|
62
|
+
the next turn's whole history is appended to the cache again. Because the body
|
|
63
|
+
is replayed as sent, an edit to the skill reaches such a chat on its next
|
|
64
|
+
mention of the skill, not on the next turn.
|
|
65
|
+
|
|
66
|
+
- `bare-stow` 0.1.7, with the patches it made stale dropped. (#396)
|
|
67
|
+
- `lunte` 1.8.6 and `lunte-plugin-qvac` 0.2.0. (#397)
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Breaking:** a skill switched off on the agent can no longer be used. Naming
|
|
72
|
+
it with `@` or in prose loads nothing and unlocks nothing, and a chat that
|
|
73
|
+
loaded it earlier loses it as soon as it is switched off (QVAC-25357).
|
|
74
|
+
`skillIsActive` reads the agent's `enabledSkills` alone, and
|
|
75
|
+
`SkillActivation` drops its `mention` and `loadedSkills` fields. A host that
|
|
76
|
+
relied on a mention to grant a skill must enable it on the agent first. (#338)
|
|
77
|
+
- A round that says nothing and calls nothing is re-asked on a history that
|
|
78
|
+
keeps that round's turn. The re-ask used to resend the history without it,
|
|
79
|
+
which the SDK, having cached the turn, read as stale: it sent the whole
|
|
80
|
+
history again and the addon appended it to the KV cache. (#403)
|
|
81
|
+
- A model loaded by hand when nothing else is in memory is no longer refused,
|
|
82
|
+
even when its estimate is above the memory cap. Beside another model a
|
|
83
|
+
manual load is still refused, so it never piles a second model into memory.
|
|
84
|
+
(#394)
|
|
85
|
+
|
|
86
|
+
- When llama.cpp overruns its grammar mid-round and a complete
|
|
87
|
+
`<function=…></function>` call has already streamed, the round ends on that
|
|
88
|
+
text and markup recovery runs the call, instead of the turn failing with
|
|
89
|
+
"Unexpected empty grammar stack". The held-back prose is flushed before any
|
|
90
|
+
rethrow, and the inference inspector reports the recovered round as
|
|
91
|
+
`completed` with the grammar error as its `reason`. (#370)
|
|
92
|
+
|
|
93
|
+
- `@mention`s, the implicit skill preload and the explicit web-search check
|
|
94
|
+
read the turn as the user typed it (`CompletionContext.said`, carried in the
|
|
95
|
+
start frame's existing payload), so a host that rewrites the stored turn no
|
|
96
|
+
longer hides a mention. Hosts that do not send `said` behave as before.
|
|
97
|
+
(#372)
|
|
98
|
+
|
|
99
|
+
- Office files keep their pictures off the text: in a deck a picture fills an
|
|
100
|
+
empty placeholder, sits below the text when there is room, and otherwise
|
|
101
|
+
takes the right half; an op aimed at a slide a later `addSlide` creates is
|
|
102
|
+
refused rather than moved. In a workbook `addImage` without a `cell` lands
|
|
103
|
+
beside the data, as `addChart` does, and a second picture or chart stacks
|
|
104
|
+
below the first. A nested op is named as its own entry in a tool error, and
|
|
105
|
+
the tool result says when an attached image went unused. (#406)
|
|
106
|
+
|
|
107
|
+
### Removed
|
|
108
|
+
|
|
109
|
+
- **Breaking:** `skillPreloadMessage` (`lib/tools/skills/mention.ts`) and
|
|
110
|
+
`withoutSkillLoadTurns` (`lib/capabilities/tool-turns.ts`). Nothing writes a
|
|
111
|
+
skill body into the user's message any more, so no earlier load turn has to
|
|
112
|
+
be dropped to keep the two from contradicting each other.
|
|
113
|
+
- `CompletionSdk.deleteCache`. Nothing calls it now that the child key (#305)
|
|
114
|
+
and the chat-cache drop after a model-loaded skill (#373, reverted in #400)
|
|
115
|
+
are gone.
|
|
116
|
+
|
|
117
|
+
## [0.8.6] - 2026-10-08
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
121
|
+
- `setModelMemoryCap(percent)` keeps several models loaded side by side. Loaded
|
|
122
|
+
models may fill that share of the device's memory budget, so chat models a
|
|
123
|
+
request loaded stay loaded while they fit, and a chat switching between them
|
|
124
|
+
loads neither again.
|
|
125
|
+
|
|
126
|
+
A load that would not fit unloads idle models one at a time, least recently
|
|
127
|
+
used first within each group: image and music models first, then embedding,
|
|
128
|
+
speech and transcription models, then chat models a request loaded, then
|
|
129
|
+
those loaded by hand. A manual `loadModel` of any kind never unloads another
|
|
130
|
+
model.
|
|
131
|
+
|
|
132
|
+
Each load is sized by `assessModelFit`. `list()` rows of loaded models carry
|
|
133
|
+
the estimate as `memoryBytes`, and `memoryUsage()` answers their total. `0`,
|
|
134
|
+
a mobile device, and a host that never sets a cap keep one chat model a
|
|
135
|
+
request loaded, as before (QVAC-25735). See
|
|
136
|
+
[Keeping several models in memory](https://github.com/tetherto/qvac-harness/blob/v0.8.6/README.md#keeping-several-models-in-memory)
|
|
137
|
+
and the
|
|
138
|
+
[decision record](https://github.com/tetherto/qvac-harness/blob/v0.8.6/docs/decisions/2026-10-07-loaded-models-share-a-memory-budget.md).
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
await harness.setModelMemoryCap(85)
|
|
142
|
+
|
|
143
|
+
const { usedBytes } = await harness.memoryUsage()
|
|
144
|
+
const loaded = (await harness.list({ cachedOnly: true })).filter((m) => m.isLoaded)
|
|
145
|
+
// each loaded row carries memoryBytes, the estimate its load was sized by
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
(#358)
|
|
149
|
+
|
|
150
|
+
- `setDownloadsAllowed({ allowed })`, in process and over hrpc, lets a host hold
|
|
151
|
+
model downloads, for example while a phone is on cellular, so a request
|
|
152
|
+
cannot pull several GB without asking. Downloads are allowed by default, and
|
|
153
|
+
`download()` itself is never held: it is how the host fetches a held model
|
|
154
|
+
once the user approves.
|
|
155
|
+
|
|
156
|
+
While downloads are not allowed, a load whose model is not on the device
|
|
157
|
+
emits a `model` event with `status: 'held'`, the model's download name and
|
|
158
|
+
its `addon`, then waits. It goes ahead once a `download()` puts the model on
|
|
159
|
+
the device or downloads are allowed again. Aborting the caller's signal ends
|
|
160
|
+
it with `AbortedError`, and a wait that ends without loading reports `error`
|
|
161
|
+
with `message: 'aborted'`. This covers the image, music and translation
|
|
162
|
+
runtimes and every pooled load (chat, embedding, speech, transcription).
|
|
163
|
+
|
|
164
|
+
Over hrpc, every model event frame now carries an `addon` field, which is
|
|
165
|
+
`null` unless the event is `held` or the `error` that ends a held wait. See
|
|
166
|
+
[Holding model downloads](https://github.com/tetherto/qvac-harness/blob/v0.8.6/README.md#holding-model-downloads)
|
|
167
|
+
and the
|
|
168
|
+
[decision record](https://github.com/tetherto/qvac-harness/blob/v0.8.6/docs/decisions/2026-10-07-a-load-waits-for-a-download-the-host-holds.md).
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
await harness.setDownloadsAllowed({ allowed: false })
|
|
172
|
+
|
|
173
|
+
harness.on('model', (state) => {
|
|
174
|
+
if (state.status === 'held') askToDownload(state.model, state.addon)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// later, once the user agrees
|
|
178
|
+
await harness.setDownloadsAllowed({ allowed: true })
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
(#340)
|
|
182
|
+
|
|
183
|
+
- Ternary Bonsai 2 27B joins the curated catalog in two builds,
|
|
184
|
+
`Ternary-Bonsai-2-27B-PQ2_0` (7.2 GB) and `Ternary-Bonsai-2-27B-PTQ1_0`
|
|
185
|
+
(5.9 GB), each with its vision projection. Both sit in the `high` tier, by
|
|
186
|
+
their memory footprint rather than their parameter count.
|
|
187
|
+
|
|
188
|
+
It is a Qwen3.8-27B fine-tune whose name the SDK's dialect router does not
|
|
189
|
+
recognise, so it parses tool calls as `qwen35`; a catalog entry can now name
|
|
190
|
+
its tool dialect.
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
import { BONSAI_2_27B_PTQ1 } from '@qvac/harness/model-catalog'
|
|
194
|
+
|
|
195
|
+
await harness.loadModel({ model: BONSAI_2_27B_PTQ1, agent: { ctxSize: 8192 } })
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
(#303)
|
|
199
|
+
|
|
200
|
+
### Changed
|
|
201
|
+
|
|
202
|
+
- `@qvac/sdk` and `@qvac/inference` 0.21.0, with `@qvac/llm-llamacpp` 0.55.0.
|
|
203
|
+
A host that packages the addons exempts `@qvac/fabric-<os>-<cpu>` from any
|
|
204
|
+
install age gate: `@qvac/fabric` now ships its prebuilds as those
|
|
205
|
+
per-platform packages. `@qvac/model-fit` leaves the native graph. The fit
|
|
206
|
+
check passes the model's source to the SDK's new `loadModel`-shaped
|
|
207
|
+
`assessModelFit` input. (#302)
|
|
208
|
+
- Transcribing long audio no longer needs memory in proportion to its length.
|
|
209
|
+
It streams the audio to the model in 300 s segments instead of decoding the
|
|
210
|
+
whole clip first: a 3.8 h clip peaked at about 0.9 GB above baseline, down
|
|
211
|
+
from 1.8 GB. Clips under 60 s are transcribed as before. (#291)
|
|
212
|
+
|
|
213
|
+
### Fixed
|
|
214
|
+
|
|
215
|
+
- A run that preloads a skill (`@skill`, or a skill named in prose) no longer
|
|
216
|
+
prefills the whole prompt on every round of a tool loop. It caches its prompt
|
|
217
|
+
under a child of the chat's key for the length of the run, and the child key
|
|
218
|
+
is deleted when the run ends. (#305)
|
|
219
|
+
|
|
11
220
|
## [0.8.5] - 2026-10-06
|
|
12
221
|
|
|
13
222
|
### Added
|
|
14
223
|
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
224
|
+
- The model can now read, edit and create PowerPoint, Word and Excel files
|
|
225
|
+
without Python, on Bare and Node: skill-gated `pptx`, `docx` and `xlsx`
|
|
226
|
+
tools on the office engine. These are the tools the presentations, word and
|
|
227
|
+
excel skills in `@qvac/skills` 0.1.12 call.
|
|
228
|
+
|
|
229
|
+
A call takes an `attachmentId` (or `create` options), `ops` that name engine
|
|
230
|
+
methods with named arguments, and an `output` file name; with an
|
|
231
|
+
`attachmentId` alone it returns the file's contents. The first failing op
|
|
232
|
+
saves nothing and returns the engine's error.
|
|
233
|
+
|
|
234
|
+
An edit is never saved under the attached file's own name. Saving the same
|
|
235
|
+
file again in one turn answers with the one already attached, and a changed
|
|
236
|
+
file under that name is refused. The one-line answer comes back as `reply`.
|
|
237
|
+
See the
|
|
238
|
+
[office engine](https://github.com/tetherto/qvac-harness/blob/v0.8.5/lib/office/engine/README.md)
|
|
239
|
+
for the methods an op can name.
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
// the arguments a model passes to the pptx tool
|
|
243
|
+
{
|
|
244
|
+
create: { title: 'Q3 review' },
|
|
245
|
+
ops: [{ op: 'addSlide', title: 'Revenue', body: 'Up 12% on Q2' }],
|
|
246
|
+
output: 'q3-review.pptx'
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
(#286)
|
|
24
251
|
|
|
25
252
|
### Changed
|
|
26
253
|
|
|
27
254
|
- **Breaking:** `@qvac/skills` is now 0.1.12, whose office and pdf skills run on
|
|
28
255
|
the `pptx`, `docx`, `xlsx` and `pdf` tools instead of Python. A host that
|
|
29
256
|
passes its own `skillsDir` needs 0.1.12 as well: older office skills import
|
|
30
|
-
Python libraries this release no longer ships.
|
|
31
|
-
|
|
32
|
-
|
|
257
|
+
Python libraries this release no longer ships. A host on the bundled skills
|
|
258
|
+
has nothing to do.
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
# a host whose own skillsDir comes from @qvac/skills
|
|
262
|
+
# before
|
|
263
|
+
npm install @qvac/skills@0.1.11
|
|
264
|
+
# after
|
|
265
|
+
npm install @qvac/skills@0.1.12
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
(#286)
|
|
269
|
+
|
|
270
|
+
- `extractOfficeText` and `extractDocxText` no longer start Pyodide: they read
|
|
271
|
+
.docx, .xlsx and .pptx in process on the engine, and read at most 1M cells
|
|
33
272
|
across a workbook. Extracted workbook booleans read `true`/`false` instead of
|
|
34
273
|
`True`/`False`. `closeOfficeWorker` and `closeDocxWorker` remain as no-ops.
|
|
35
274
|
(#286)
|
|
36
275
|
- `exec` refuses Python that imports `pptx`, `docx` or `openpyxl`, or declares
|
|
37
|
-
their packages, before it runs
|
|
38
|
-
it) to use instead
|
|
276
|
+
their packages, before it runs. The refusal names the tool (or the skill
|
|
277
|
+
that grants it) to use instead, so the model moves to the office tools
|
|
278
|
+
rather than trying to install the package. (#287)
|
|
39
279
|
- **Breaking:** Python run through `exec` asks for approval like any other
|
|
40
|
-
write, including a run that only builds a new PDF.
|
|
280
|
+
write, including a run that only builds a new PDF. Those runs used to
|
|
281
|
+
skip `approve`; now the user is asked. The `pdf` tool, which builds PDFs now,
|
|
282
|
+
counts as a read and does not ask.
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
// before: exec running Python that only built a new PDF skipped approve
|
|
286
|
+
// after: it reaches approve as a write, like any other Python run
|
|
287
|
+
const context = {
|
|
288
|
+
model,
|
|
289
|
+
agent,
|
|
290
|
+
messages,
|
|
291
|
+
approve: async ({ name, effect }) => (effect === 'write' ? askUser(name) : true)
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
(#289)
|
|
41
296
|
|
|
42
297
|
### Removed
|
|
43
298
|
|
|
44
299
|
- **Breaking:** the vendored Python wheels for python-pptx, python-docx,
|
|
45
300
|
openpyxl, fpdf2 and pypdf, and their dependencies. Only micropip is still
|
|
46
301
|
seeded; any other package installs from PyPI, which sandbox mode refuses.
|
|
47
|
-
Use the office and `pdf` tools.
|
|
302
|
+
Use the office and `pdf` tools.
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
// before: exec built a PDF offline with the vendored fpdf2
|
|
306
|
+
{ language: 'python', command: 'from fpdf import FPDF\n...', outputs: ['report.pdf'] }
|
|
307
|
+
// after: the pdf tool builds it
|
|
308
|
+
{ action: 'create', markdown: '# Report\n...', output: 'report.pdf' }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
(#286, #289)
|
|
312
|
+
|
|
48
313
|
- The `exec` guidance written for python-pptx, python-docx and fpdf2 code:
|
|
49
314
|
their error hints and source checks, and the shrink-to-fit pass on saved
|
|
50
315
|
decks. (#288, #289)
|
|
@@ -53,42 +318,64 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
53
318
|
|
|
54
319
|
### Added
|
|
55
320
|
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
321
|
+
- The model can read, create and change PDFs without Python: a skill-gated
|
|
322
|
+
`pdf` tool on `bare-pdfium` 1.6.0. The pdf skill in the next `@qvac/skills`
|
|
323
|
+
release uses it instead of Python. On Node it reports that the device cannot
|
|
324
|
+
open PDFs.
|
|
325
|
+
|
|
326
|
+
With an `attachmentId` and no `action` it reads a PDF: the page count, each
|
|
327
|
+
page's text and the form fields. With an `action` it saves a new file named
|
|
328
|
+
`output` and attaches it: `create` from Markdown (chat images placed with
|
|
329
|
+
``), `merge`, `split`, `rotate`, `watermark`, `fill`,
|
|
330
|
+
`encrypt`, `decrypt` and `text`.
|
|
331
|
+
|
|
332
|
+
Errors name what exists, a locked PDF takes its password on any action, and
|
|
333
|
+
the one-line answer comes back as `reply`.
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
// the arguments a model passes to the pdf tool
|
|
337
|
+
{ attachmentId, action: 'rotate', degrees: 90, pages: [2], output: 'scan-rotated.pdf' }
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
(#256)
|
|
341
|
+
|
|
342
|
+
- An office engine in `lib/office/engine/` reads, edits and creates `.pptx`,
|
|
343
|
+
`.docx` and `.xlsx` files in TypeScript on Bare and Node, so office editing
|
|
344
|
+
no longer needs Python and can run on mobile. It is internal in this release:
|
|
345
|
+
the tools that drive it come in 0.8.5.
|
|
346
|
+
|
|
347
|
+
It keeps untouched parts byte for byte, fails with a `DocsError` whose code
|
|
348
|
+
names the problem, and moves Excel references when rows and columns change.
|
|
349
|
+
It adds `@xmldom/xmldom`, makes `bare-zlib` a direct dependency and maps
|
|
350
|
+
`#zlib` to `bare-zlib` or `zlib`. See the
|
|
351
|
+
[engine's README](https://github.com/tetherto/qvac-harness/blob/v0.8.0/lib/office/engine/README.md)
|
|
352
|
+
and the
|
|
353
|
+
[decision record](https://github.com/tetherto/qvac-harness/blob/v0.8.0/docs/decisions/2026-09-28-office-files-are-edited-by-the-harness-engine.md).
|
|
354
|
+
(#150)
|
|
71
355
|
|
|
72
356
|
### Fixed
|
|
73
357
|
|
|
74
|
-
- Qwen3.5-family models
|
|
75
|
-
in the Qwen3.5 template's own
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
358
|
+
- Qwen3.5-family models no longer stall several tool calls into a turn. A tool
|
|
359
|
+
call that ran goes back into the chat history in the Qwen3.5 template's own
|
|
360
|
+
format (`<tool_call>` wrapper, one line per tag, string values as written)
|
|
361
|
+
instead of a compact `<function=…>` line.
|
|
362
|
+
|
|
363
|
+
A model several calls into a turn copied the compact form, the tool grammar
|
|
364
|
+
rejected it, and the run ended in "This didn't finish". Models with another
|
|
365
|
+
tool format (e.g. MedPsy) still get a format that is not their own, until
|
|
366
|
+
the SDK accepts structured tool calls in history. (QVAC-23092, #146)
|
|
367
|
+
|
|
368
|
+
- A skill's gated tools now appear when the model first asks the skill for a
|
|
369
|
+
file it does not have. Loaded with such a `file` before its instructions were
|
|
370
|
+
loaded, the skill returns those instructions with a line naming the files it
|
|
371
|
+
has, so it counts as loaded. Before, a one-file skill answered that the model
|
|
372
|
+
already had its instructions, and its tools stayed hidden. (#257)
|
|
373
|
+
- Small models no longer keep calling tools on their own output until the round
|
|
374
|
+
cap. After a tool attaches a file, the loop tells the model the file is shown
|
|
375
|
+
and to answer, allows one more tool round, then writes the answer in a
|
|
376
|
+
tool-less round. (#258)
|
|
377
|
+
- `exec`: text like `$1,299` in a shell command runs on the first try. A `$`
|
|
378
|
+
before a digit, a space or punctuation is escaped instead of rejected. Real
|
|
92
379
|
shell expansion is still refused, now with a hint to single-quote the text.
|
|
93
380
|
(#208)
|
|
94
381
|
|
|
@@ -96,223 +383,419 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
96
383
|
|
|
97
384
|
### Added
|
|
98
385
|
|
|
99
|
-
- A tool
|
|
100
|
-
|
|
101
|
-
The `tool-result` event still
|
|
102
|
-
|
|
103
|
-
|
|
386
|
+
- A tool can now end the turn with its own answer. A tool result may carry
|
|
387
|
+
`answer: true` beside a non-empty `content`, and that content becomes the
|
|
388
|
+
reply without another round through the model. The `tool-result` event still
|
|
389
|
+
fires.
|
|
390
|
+
|
|
391
|
+
`weather_lookup` uses it for an ambiguous or unknown place: the question that
|
|
392
|
+
lists the namesakes, or the not-found line, is the reply.
|
|
393
|
+
|
|
394
|
+
```js
|
|
395
|
+
const harness = new Harness({
|
|
396
|
+
tools: [
|
|
397
|
+
{
|
|
398
|
+
name: 'store_hours',
|
|
399
|
+
description: 'Opening hours of the store',
|
|
400
|
+
parameters: { type: 'object', properties: {} },
|
|
401
|
+
toolset: null,
|
|
402
|
+
effect: () => 'read',
|
|
403
|
+
execute: async () => ({ content: 'Open 9 to 5, Monday to Saturday.', answer: true })
|
|
404
|
+
}
|
|
405
|
+
]
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
(#153)
|
|
104
410
|
|
|
105
411
|
### Fixed
|
|
106
412
|
|
|
107
|
-
- `weather_lookup`
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
413
|
+
- `weather_lookup` reports the place that was asked about: a bare "Nassau" now
|
|
414
|
+
reports Nassau, Bahamas. It used to ask wttr.in by name, whose own geocoder
|
|
415
|
+
put it in Westbury, New York.
|
|
416
|
+
|
|
417
|
+
It now finds the place before it asks for the weather. It looks the city up
|
|
418
|
+
on Open-Meteo's geocoder, `geocoding-api.open-meteo.com`, a new outbound
|
|
419
|
+
host, and asks wttr.in for the forecast at the coordinates of the place it
|
|
420
|
+
picked, which leads the report. The resource label names both hosts.
|
|
421
|
+
|
|
422
|
+
A name that places of a similar size share, such as Springfield, gets a
|
|
423
|
+
question listing up to four of them instead of a forecast: any namesake with
|
|
424
|
+
a tenth of the largest one's population counts. A country or state in the
|
|
425
|
+
request picks among them. When the geocoder is blocked, slow or failing, or
|
|
426
|
+
knows no such place, the lookup asks wttr.in by name, as before. See the
|
|
427
|
+
[decision record](https://github.com/tetherto/qvac-harness/blob/v0.7.3/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md).
|
|
428
|
+
(#153)
|
|
118
429
|
|
|
119
430
|
## [0.7.2] - 2026-10-01
|
|
120
431
|
|
|
121
432
|
### Added
|
|
122
433
|
|
|
123
|
-
-
|
|
124
|
-
besides
|
|
125
|
-
stage it to read past the returned text. The result
|
|
126
|
-
`attachmentId`, `fileName` and `byteLength`.
|
|
127
|
-
|
|
128
|
-
|
|
434
|
+
- A plain-text file `web_fetch` fetched (.txt, .csv, .json…) is now attached
|
|
435
|
+
to the chat besides its text being returned, so the user can open or save
|
|
436
|
+
it and `exec` can stage it to read past the returned text. The result
|
|
437
|
+
carries its `attachmentId`, `fileName` and `byteLength`.
|
|
438
|
+
|
|
439
|
+
A body cut at the fetch cap is not attached, and a host that cannot store
|
|
440
|
+
files gets the text alone, as with page images.
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
for await (const event of harness.completion(context)) {
|
|
444
|
+
if (event.type === 'tool-result' && event.name === 'web_fetch') {
|
|
445
|
+
const { attachmentId, fileName, byteLength } = event.result
|
|
446
|
+
// attachmentId is undefined when nothing was attached
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
(#147, #148)
|
|
129
452
|
|
|
130
453
|
### Changed
|
|
131
454
|
|
|
132
455
|
- Speech sounds cleaner on desktops. Devices on the `mid` profile and up now
|
|
133
456
|
speak with Supertonic 3 FP16 (`Supertonic3-FP16`, 207 MB) instead of Q8_0
|
|
134
457
|
(127 MB), at 16 inference steps instead of 5, which removes the artifacts
|
|
135
|
-
the 5-step output had.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
458
|
+
the 5-step output had.
|
|
459
|
+
|
|
460
|
+
Synthesis takes about 2.7× as long, still around 7× faster than real time on
|
|
461
|
+
an M-series Mac. Phones (`low`) and `tiny` devices keep Q4_0 at 5 steps.
|
|
462
|
+
|
|
463
|
+
A host that prefetches `infraModelsFor` downloads the FP16 model once;
|
|
464
|
+
`Supertonic3-Q8_0` stays in the catalog, and now also runs at 16 steps when a
|
|
465
|
+
host asks for it by name.
|
|
466
|
+
|
|
467
|
+
```js
|
|
468
|
+
import { defaultTtsModelName } from '@qvac/harness/model-defaults'
|
|
469
|
+
|
|
470
|
+
defaultTtsModelName('mid') // 'Supertonic3-FP16', was 'Supertonic3-Q8_0'
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
(#149)
|
|
140
474
|
|
|
141
475
|
## [0.7.1] - 2026-09-30
|
|
142
476
|
|
|
143
477
|
### Added
|
|
144
478
|
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
479
|
+
- A host can show a model as loading without counting loads itself. `list()`
|
|
480
|
+
rows carry `isLoading`: true from the moment a load of that model starts,
|
|
481
|
+
whoever asked for it (a request, `warmModel` or `loadModel`), until it is
|
|
482
|
+
ready or the load fails. It is read off the pool.
|
|
483
|
+
|
|
484
|
+
A model loading again under other settings while a copy of it is resident
|
|
485
|
+
reads `isLoaded` and `isLoading` both true. Chat models read it today. Image,
|
|
486
|
+
music and translation models load for each generation, and speech,
|
|
487
|
+
transcription and embedding pools key by source, so those rows read `false`
|
|
488
|
+
for now. It is an optional field on the wire; a row from an older harness
|
|
489
|
+
reads `false`. See
|
|
490
|
+
[Load and unload a model](https://github.com/tetherto/qvac-harness/blob/v0.7.1/README.md#load-and-unload-a-model).
|
|
154
491
|
|
|
155
492
|
```js
|
|
156
493
|
const loading = (await harness.list({ cachedOnly: true })).filter((row) => row.isLoading)
|
|
157
494
|
```
|
|
158
495
|
|
|
496
|
+
(#143)
|
|
497
|
+
|
|
159
498
|
### Changed
|
|
160
499
|
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
500
|
+
- A host that re-reads `list()` on `catalogChanged` sees a model turn loading,
|
|
501
|
+
then loaded, with no bookkeeping of its own. The `model` event carries
|
|
502
|
+
`catalogChanged` at every edge a re-read would show: the `loading` that
|
|
503
|
+
starts a load and the `ready` or `error` that ends it, beside `unloaded`.
|
|
504
|
+
Progress ticks still do not.
|
|
505
|
+
|
|
506
|
+
The served event stream never drops a frame that carries it, so two `ready`
|
|
507
|
+
frames of one model under two keys both cross.
|
|
508
|
+
|
|
509
|
+
```js
|
|
510
|
+
harness.on('model', async (state) => {
|
|
511
|
+
if (state.catalogChanged) render(await harness.list({ cachedOnly: true }))
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
(#143)
|
|
516
|
+
|
|
167
517
|
- `ModelInfo` requires `isLoading`, and `listModels`, `customModelRows` and
|
|
168
518
|
`mergeCustomRows` return `CatalogRow`, the row before the pools are asked. A
|
|
169
|
-
typed fake `list()` adds the field.
|
|
519
|
+
typed fake `list()` adds the field.
|
|
520
|
+
|
|
521
|
+
```js
|
|
522
|
+
// before
|
|
523
|
+
list: async () => [{ ...row, isLoaded: false }]
|
|
524
|
+
|
|
525
|
+
// after
|
|
526
|
+
list: async () => [{ ...row, isLoaded: false, isLoading: false }]
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
(#143)
|
|
530
|
+
|
|
170
531
|
- A custom model's row reads `isLoaded` only once its load has landed; it used
|
|
171
532
|
to read `true` from the first attempt. (#143)
|
|
172
533
|
|
|
173
534
|
### Fixed
|
|
174
535
|
|
|
175
|
-
- A
|
|
176
|
-
the
|
|
177
|
-
|
|
178
|
-
`error` also refreshes the cached `isCached`, since a load may
|
|
179
|
-
the weights before it failed. (#143)
|
|
536
|
+
- A host can tell a failed load from one still running. A load that ends
|
|
537
|
+
without the model in memory emits `model` with `error` and the reason in
|
|
538
|
+
`message`: it failed, or a suspend or close cancelled it. It used to end in
|
|
539
|
+
silence. The `error` also refreshes the cached `isCached`, since a load may
|
|
540
|
+
have downloaded the weights before it failed. (#143)
|
|
180
541
|
|
|
181
542
|
## [0.7.0] - 2026-09-29
|
|
182
543
|
|
|
183
544
|
### Added
|
|
184
545
|
|
|
185
|
-
-
|
|
186
|
-
`
|
|
187
|
-
`
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
546
|
+
- A host can tell what each model is for without keeping its own table.
|
|
547
|
+
`list()` rows carry `taskType`: `llm`, `embeddings`, `speech-to-text`,
|
|
548
|
+
`text-to-speech`, `image-generation`, `audio-generation`, `translation`,
|
|
549
|
+
`text-recognition` or `other`.
|
|
550
|
+
|
|
551
|
+
It is decided from the addon in one place, `lib/models/model-task.ts`, on
|
|
552
|
+
each side of the wire rather than sent over it. `ModelTaskType` is exported,
|
|
553
|
+
and grows the four new values a preview never returns.
|
|
554
|
+
|
|
555
|
+
```js
|
|
556
|
+
const rows = await harness.list({ cachedOnly: true })
|
|
557
|
+
const transcribers = rows.filter((row) => row.taskType === 'speech-to-text')
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
(#140)
|
|
191
561
|
|
|
192
|
-
- `modelsForAgent(agent, host)`
|
|
562
|
+
- `modelsForAgent(agent, host)` answers which models an agent runs on a device,
|
|
193
563
|
each with its `use`: the chat model, clamped by `runnableModelName`, then the
|
|
194
564
|
image and music models its skills need. A host deciding what to download, or
|
|
195
565
|
which agents a model delete affects, asks this instead of working it out.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
566
|
+
|
|
567
|
+
`runnableModelName(requested, host)` and `isMobilePlatform(platform)` move
|
|
568
|
+
here from `@qvac/core`: on a phone, a pick above the device's profile runs as
|
|
569
|
+
its recommended model. Whether the host is a phone is the required `mobile`
|
|
570
|
+
on `HostCapability`, which `capability()` now reports, so its answer passes
|
|
571
|
+
straight in; a host built by hand has to say. `runnableModelName` and
|
|
572
|
+
`isMobilePlatform` are on the browser-safe `@qvac/harness/model-catalog`
|
|
573
|
+
entry too.
|
|
202
574
|
|
|
203
575
|
```js
|
|
204
|
-
|
|
576
|
+
import { modelsForAgent, QWEN_35_9B } from '@qvac/harness'
|
|
577
|
+
|
|
578
|
+
const agent = {
|
|
579
|
+
modelName: QWEN_35_9B,
|
|
580
|
+
installedSkills: ['image-generation'],
|
|
581
|
+
skillsScope: 'all'
|
|
582
|
+
}
|
|
583
|
+
const models = modelsForAgent(agent, await harness.capability())
|
|
584
|
+
// [{ name, addon: 'llm', use: 'chat' }, { name, addon, use: 'image' }]
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
```js
|
|
588
|
+
import { runnableModelName, QWEN_35_4B, QWEN_35_9B } from '@qvac/harness/model-catalog'
|
|
589
|
+
|
|
590
|
+
const host = { capabilityProfile: 'mid', recommendedModelName: QWEN_35_4B, mobile: true }
|
|
591
|
+
runnableModelName(QWEN_35_9B, host) // QWEN_35_4B
|
|
205
592
|
```
|
|
206
593
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
594
|
+
(#141)
|
|
595
|
+
|
|
596
|
+
- `capability()` answers `mobile`, whether the harness runs on a phone, read
|
|
597
|
+
off the platform it probes. It rides `capability-response` as an optional
|
|
598
|
+
field; a response from an older harness reads as `false`.
|
|
599
|
+
|
|
600
|
+
```js
|
|
601
|
+
const { mobile } = await harness.capability()
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
(#141)
|
|
605
|
+
|
|
606
|
+
- A host no longer needs to know what `modelChoice` means in an agent's
|
|
607
|
+
`generate_music` tool settings. `musicModelChoiceFrom(settings)` reads the
|
|
608
|
+
music model pick out of them, and `isMusicModelChoice` is exported beside it.
|
|
609
|
+
|
|
610
|
+
```js
|
|
611
|
+
import { musicModelChoiceFrom } from '@qvac/harness/music-catalog'
|
|
612
|
+
|
|
613
|
+
musicModelChoiceFrom({ modelChoice: 'turbo-hq' }) // 'turbo-hq'
|
|
614
|
+
musicModelChoiceFrom({}) // undefined
|
|
615
|
+
```
|
|
210
616
|
|
|
211
|
-
|
|
212
|
-
`generate_music` tool settings, so a host no longer knows what `modelChoice`
|
|
213
|
-
means there. `isMusicModelChoice` is exported beside it. (#141)
|
|
617
|
+
(#141)
|
|
214
618
|
|
|
215
619
|
### Changed
|
|
216
620
|
|
|
217
|
-
- **Breaking:** the package no longer publishes a prebuilt sidecar
|
|
621
|
+
- **Breaking:** the package no longer publishes a prebuilt sidecar.
|
|
218
622
|
`dist/harness.bundle`, `dist/harness.cjs`, `dist/prebuilds-manifest.json` and
|
|
219
623
|
the `./dist/harness.cjs` export are gone. A host stows
|
|
220
624
|
`worker/harness-sidecar.mjs` itself, as desktop and mobile already do, or
|
|
221
|
-
spawns `worker/stow-entry.mjs` straight from its install.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
625
|
+
spawns `worker/stow-entry.mjs` straight from its install. See
|
|
626
|
+
[The published package](https://github.com/tetherto/qvac-harness/blob/v0.7.0/README.md#the-published-package)
|
|
627
|
+
and [Spawning](https://github.com/tetherto/qvac-harness/blob/v0.7.0/README.md#spawning).
|
|
628
|
+
|
|
629
|
+
```js
|
|
630
|
+
// before
|
|
631
|
+
const harness = require('@qvac/harness/dist/harness.cjs')
|
|
632
|
+
const { ipc, sidecar, exit } = await harness.start(args)
|
|
633
|
+
|
|
634
|
+
// after
|
|
635
|
+
import { spawnHarness } from '@qvac/harness'
|
|
636
|
+
|
|
637
|
+
const harness = spawnHarness({
|
|
638
|
+
entry: require.resolve('@qvac/harness/worker/stow-entry.mjs'),
|
|
639
|
+
sdkStorage
|
|
640
|
+
})
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
(#127)
|
|
644
|
+
|
|
645
|
+
- **Breaking:** a stowed harness runs python in a worker rather than
|
|
646
|
+
in-process. A host that stows the harness:
|
|
647
|
+
|
|
648
|
+
- stops deferring `pyodide`
|
|
649
|
+
- keeps `QVAC_PYODIDE_CACHE_DIR` writable
|
|
650
|
+
- no longer stages a separate worker bundle
|
|
651
|
+
- ships what stow offloads beside the bundle, which now includes pyodide's
|
|
652
|
+
wasm, stdlib and lock file and the vendored wheels
|
|
653
|
+
|
|
654
|
+
A target that embeds assets instead of offloading them cannot run python,
|
|
655
|
+
since `fs` cannot read inside a bundle. See
|
|
656
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.7.0/docs/decisions/2026-09-25-the-python-worker-runs-on-bare-worker.md).
|
|
657
|
+
(#127)
|
|
658
|
+
|
|
659
|
+
- A bundle of the package root carries neither pyodide nor the vendored
|
|
660
|
+
wheels. Only an entry that runs python pulls them in: the exec tool, or
|
|
661
|
+
`@qvac/harness/docx` and `/office`. (#127)
|
|
232
662
|
- `SandboxHook` no longer checks `packages`. The exec tool refuses an install
|
|
233
663
|
the vendored wheels do not hold in full in sandbox mode, whether declared in
|
|
234
664
|
`packages` or on a pip line, with the same message as before. (#127)
|
|
235
665
|
- The vendored wheels are the `#pyodide-wheels` directory asset on Bare;
|
|
236
666
|
`QVAC_PYTHON_WHEELS_DIR` still overrides it. (#127)
|
|
237
667
|
- The python worker and the in-process deadline are spawned through
|
|
238
|
-
`worker_threads`, which the package maps to `bare-worker
|
|
668
|
+
`worker_threads`, which the package maps to `bare-worker`.
|
|
239
669
|
`#pyodide-worker-spawn` is gone. (#127)
|
|
240
|
-
- **Breaking:**
|
|
241
|
-
|
|
242
|
-
`bare-sidecar` to 0.5.7 (Bare 1.34.0) across the tree, `bare-supervisor`'s
|
|
243
|
-
included, with `bare-thread` at least 1.2.7 and `bare-module` 7.1.0 for
|
|
244
|
-
`bare-worker`'s copy. (#127)
|
|
670
|
+
- **Breaking:** Bare refuses to load the package on runtimes older than 1.34.0:
|
|
671
|
+
`engines.bare` is `>=1.34.0`. `bare-runtime` is pinned to 1.34.0 and
|
|
672
|
+
`bare-sidecar` to 0.5.7 (Bare 1.34.0) across the tree, `bare-supervisor`'s
|
|
673
|
+
copy included, with `bare-thread` at least 1.2.7 and `bare-module` 7.1.0 for
|
|
674
|
+
`bare-worker`'s copy. A host upgrades its Bare to 1.34.0 or later. (#127)
|
|
245
675
|
- `bare-module` is no longer a dependency or an override, and
|
|
246
|
-
`bare-module-traverse` is overridden to 2.5.8
|
|
676
|
+
`bare-module-traverse` is overridden to `~2.5.8`: earlier versions drop the
|
|
247
677
|
`asset` condition on `bare-sidecar`'s binary, and a stowed harness then fails
|
|
248
678
|
to boot on Bare 1.33 and later. (#127)
|
|
249
679
|
- `skillModelsForAgent` rows carry `use`, `image` or `music`, so a caller can
|
|
250
|
-
tell which skill a model is for without looking the name up in the catalogs.
|
|
251
|
-
|
|
252
|
-
|
|
680
|
+
tell which skill a model is for without looking the name up in the catalogs.
|
|
681
|
+
(#141)
|
|
682
|
+
- `CapabilityInfo` has a required `mobile`. A `HarnessContract` whose
|
|
683
|
+
`capability()` builds its own answer, a test stub included, returns it too.
|
|
684
|
+
|
|
685
|
+
```js
|
|
686
|
+
// before
|
|
687
|
+
capability: async () => ({ capabilityProfile: 'mid', recommendedModelName: QWEN_35_4B })
|
|
688
|
+
|
|
689
|
+
// after
|
|
690
|
+
capability: async () => ({
|
|
691
|
+
capabilityProfile: 'mid',
|
|
692
|
+
recommendedModelName: QWEN_35_4B,
|
|
693
|
+
mobile: false
|
|
694
|
+
})
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
(#141)
|
|
253
698
|
|
|
254
699
|
### Removed
|
|
255
700
|
|
|
256
|
-
- **Breaking:** `bareRuntime` is no longer exported from `./lib/tools/exec.ts
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
701
|
+
- **Breaking:** `bareRuntime` is no longer exported from `./lib/tools/exec.ts`,
|
|
702
|
+
so the package root no longer loads the exec tool to reach it. It is at
|
|
703
|
+
`./lib/tools/exec/bare-runtime.ts`.
|
|
704
|
+
|
|
705
|
+
```js
|
|
706
|
+
// before
|
|
707
|
+
import { bareRuntime } from '@qvac/harness/lib/tools/exec.ts'
|
|
708
|
+
|
|
709
|
+
// after
|
|
710
|
+
import { bareRuntime } from '@qvac/harness/lib/tools/exec/bare-runtime.ts'
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
(#127)
|
|
714
|
+
|
|
715
|
+
- **Breaking:** `QVAC_PYODIDE_TEST_LOADER` is gone. A test points
|
|
716
|
+
`QVAC_PYODIDE_WORKER_ENTRY` at an entry that serves its fake interpreter.
|
|
717
|
+
|
|
718
|
+
```sh
|
|
719
|
+
# before
|
|
720
|
+
QVAC_PYODIDE_TEST_LOADER=/path/to/fake-loader.js
|
|
721
|
+
# after
|
|
722
|
+
QVAC_PYODIDE_WORKER_ENTRY=/path/to/fake-worker-entry.js
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
(#127)
|
|
261
726
|
|
|
262
727
|
### Fixed
|
|
263
728
|
|
|
264
|
-
- Python runs start on Bare 1.34,
|
|
265
|
-
|
|
266
|
-
which the package maps to `bare-worker` on Bare
|
|
267
|
-
module as `createPyodideModule`, and the files pyodide reads
|
|
268
|
-
assets, so a bundler carries them beside the bundle. (#127)
|
|
729
|
+
- Python runs start on Bare 1.34, including in a stowed harness. Threads there
|
|
730
|
+
cannot read a module off disk, so the worker is spawned through
|
|
731
|
+
`worker_threads`, which the package maps to `bare-worker` on Bare. Pyodide is
|
|
732
|
+
handed its asm module as `createPyodideModule`, and the files pyodide reads
|
|
733
|
+
are named as assets, so a bundler carries them beside the bundle. (#127)
|
|
269
734
|
|
|
270
735
|
## [0.6.0] - 2026-09-28
|
|
271
736
|
|
|
272
737
|
### Added
|
|
273
738
|
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
under
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
739
|
+
- A host can load a model before anyone asks for it, and unload it on demand.
|
|
740
|
+
`loadModel({ model, agent })` loads a downloaded chat, embedding, speech or
|
|
741
|
+
transcription model under the key a request for it would load under, so the
|
|
742
|
+
first request finds it loaded. Pass the agent a chat model will run for: its
|
|
743
|
+
context size and KV cache types are part of that key.
|
|
744
|
+
|
|
745
|
+
`unloadModel({ model })` unloads every copy of a model once the requests
|
|
746
|
+
holding it finish, and says whether it could. Image and music models load for
|
|
747
|
+
each generation and are refused.
|
|
748
|
+
|
|
749
|
+
A load or an unload counts as a call in flight for `setModelIdleTimeout`, and
|
|
750
|
+
one that lands during an idle unload waits for it. Both are on the wire as
|
|
751
|
+
`load-model` and `unload-model`. See
|
|
752
|
+
[Load and unload a model](https://github.com/tetherto/qvac-harness/blob/v0.6.0/README.md#load-and-unload-a-model).
|
|
282
753
|
|
|
283
754
|
```js
|
|
755
|
+
import { QWEN_35_4B } from '@qvac/harness'
|
|
756
|
+
|
|
284
757
|
const { loaded } = await harness.loadModel({ model: QWEN_35_4B, agent: { ctxSize: 8192 } })
|
|
285
758
|
const { unloaded } = await harness.unloadModel({ model: QWEN_35_4B })
|
|
286
759
|
```
|
|
287
760
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
761
|
+
(#133)
|
|
762
|
+
|
|
763
|
+
- Several chat models can stay loaded at once. A chat model loaded with
|
|
764
|
+
`loadModel` is pinned: it does not count against `completionModelCap`, and a
|
|
765
|
+
request for another model leaves it in place.
|
|
766
|
+
|
|
767
|
+
A manual load never unloads another model to fit. It returns
|
|
768
|
+
`{ loaded: false, reason: 'memory' }` when the SDK's `assessModelFit` says it
|
|
769
|
+
would not fit beside what is loaded, so the host unloads one first. A request
|
|
770
|
+
always gets its model: when it would not fit, pinned models no reply is using
|
|
771
|
+
give way oldest first. A request or a manual load for a model under other
|
|
772
|
+
settings replaces its idle copy. Image and music generation and an idle
|
|
773
|
+
unload still clear every idle chat model, pinned or not. See
|
|
774
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.6.0/docs/decisions/2026-09-25-a-model-loaded-ahead-of-use-lands-where-its-request-looks.md).
|
|
296
775
|
|
|
297
776
|
```js
|
|
777
|
+
import { QWEN_35_9B } from '@qvac/harness'
|
|
778
|
+
|
|
298
779
|
const { loaded, reason } = await harness.loadModel({ model: QWEN_35_9B })
|
|
299
780
|
if (reason === 'memory') showNotice('Unload a model first')
|
|
300
781
|
```
|
|
301
782
|
|
|
783
|
+
(#134)
|
|
784
|
+
|
|
302
785
|
### Changed
|
|
303
786
|
|
|
304
|
-
-
|
|
305
|
-
not only
|
|
306
|
-
|
|
307
|
-
-
|
|
308
|
-
|
|
309
|
-
|
|
787
|
+
- `list()` reports a fresh `isLoaded` for embedding, speech and transcription
|
|
788
|
+
models, not only chat models. A model loading or unloading in any pool now
|
|
789
|
+
emits `model` with `catalogChanged`. (#133)
|
|
790
|
+
- A speech, transcription or embedding model named once by its catalog name and
|
|
791
|
+
once by its src is one copy in memory. Those requests key their pool entry by
|
|
792
|
+
the model's registry src however the caller names it. (#133)
|
|
310
793
|
|
|
311
794
|
### Fixed
|
|
312
795
|
|
|
313
796
|
- A chat model loads when the host passes no `llmDevice`. 0.5.0 sent `device`,
|
|
314
797
|
`gpu_layers`, `main-gpu` and `openclCacheDir` to the SDK as `undefined`,
|
|
315
|
-
which the SDK spreads over its defaults
|
|
798
|
+
which the SDK spreads over its defaults. With no device the load failed with
|
|
316
799
|
"must specify a device: 'gpu' or 'cpu'", and with a device but no
|
|
317
800
|
`llmGpuLayers` the default of 99 GPU layers was lost. Unset fields are left
|
|
318
801
|
out again. (#137)
|
|
@@ -321,25 +804,39 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
321
804
|
|
|
322
805
|
### Added
|
|
323
806
|
|
|
324
|
-
- `setModelIdleTimeout(ms)` frees memory
|
|
325
|
-
long
|
|
807
|
+
- `setModelIdleTimeout(ms)` frees memory on a device that has sat idle that
|
|
808
|
+
long. With no `completion`, `invoke`, `embed`, `transcribe`, `speak`,
|
|
326
809
|
`speakStream` or `warmModel` call in flight, every idle resident model is
|
|
327
|
-
unloaded, including the image, music and translation runtimes
|
|
328
|
-
loads what it needs again.
|
|
329
|
-
|
|
330
|
-
|
|
810
|
+
unloaded, including the image, music and translation runtimes, and the next
|
|
811
|
+
call loads what it needs again.
|
|
812
|
+
|
|
813
|
+
The clock is device-wide: any call resets it, whichever model it uses. `0`,
|
|
814
|
+
the default, is Never and arms no timer. A turn waiting on a tool or an
|
|
815
|
+
approval between rounds counts as busy, so it keeps its model. The call is on
|
|
816
|
+
an in-process `Harness`, on the `RemoteHarness` client, and on the wire as
|
|
817
|
+
`set-model-idle-timeout`. See
|
|
818
|
+
[Freeing memory when idle](https://github.com/tetherto/qvac-harness/blob/v0.5.0/README.md#freeing-memory-when-idle)
|
|
819
|
+
and
|
|
820
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.5.0/docs/decisions/2026-09-25-idle-unload-is-opt-in-and-device-wide.md).
|
|
331
821
|
|
|
332
822
|
```js
|
|
333
|
-
await harness.setModelIdleTimeout(30 * 60_000)
|
|
823
|
+
await harness.setModelIdleTimeout(30 * 60_000) // 0 turns it off
|
|
334
824
|
```
|
|
335
825
|
|
|
826
|
+
(#131)
|
|
827
|
+
|
|
336
828
|
- `setCustomModels` entries take an optional `label`, the name the user gave
|
|
337
|
-
the model, and `list()` rows carry it as `label`.
|
|
338
|
-
|
|
339
|
-
|
|
829
|
+
the model, and `list()` rows carry it back as `label`.
|
|
830
|
+
|
|
831
|
+
The label also picks the tool-call format: the model family (Qwen, Gemma,
|
|
832
|
+
LFM and so on) is matched against the label as well as the file name, with
|
|
833
|
+
spaces read as hyphens, so a custom model registered under an opaque id
|
|
834
|
+
still gets its family's format. (#124)
|
|
340
835
|
|
|
341
836
|
- `setCustomModels` entries take an optional `contextLength`, the model's
|
|
342
|
-
trained window
|
|
837
|
+
trained window, which caps the context it loads with by default (see
|
|
838
|
+
Changed). Pass the preview's `contextLength` straight through. See
|
|
839
|
+
[Register the set](https://github.com/tetherto/qvac-harness/blob/v0.5.0/README.md#register-the-set).
|
|
343
840
|
|
|
344
841
|
```js
|
|
345
842
|
await harness.setCustomModels([
|
|
@@ -350,23 +847,39 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
350
847
|
contextLength: preview.contextLength
|
|
351
848
|
}
|
|
352
849
|
])
|
|
850
|
+
|
|
851
|
+
const [row] = (await harness.list({ type: 'llm' })).filter(
|
|
852
|
+
(model) => model.name === 'custom-7f3a'
|
|
853
|
+
)
|
|
854
|
+
row.label // 'Gemma 4 E4B'
|
|
353
855
|
```
|
|
354
856
|
|
|
857
|
+
(#124)
|
|
858
|
+
|
|
355
859
|
### Changed
|
|
356
860
|
|
|
357
861
|
- A custom model loads with a context of the smaller of 51,200 tokens and its
|
|
358
|
-
trained `contextLength`,
|
|
862
|
+
trained `contextLength`, where it always took 51,200 before. A model trained
|
|
863
|
+
on 40,960 tokens now loads with 40,960. A context size the caller asks for
|
|
359
864
|
still wins. (#124)
|
|
360
865
|
|
|
361
866
|
### Fixed
|
|
362
867
|
|
|
363
868
|
- `previewModelSource` takes a direct link to one shard of a split `.gguf` as
|
|
364
|
-
the whole set
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
869
|
+
the whole set. `src` is the first part, which holds the metadata, and
|
|
870
|
+
`sizeBytes` adds up every part, so `isCached` can match the finished
|
|
871
|
+
download. Every part is checked with a `HEAD` request, and a missing part
|
|
872
|
+
refuses with `not-found` or `unreachable`, naming the part. See
|
|
873
|
+
[Preview a source before adding it](https://github.com/tetherto/qvac-harness/blob/v0.5.0/README.md#preview-a-source-before-adding-it).
|
|
874
|
+
(#124)
|
|
875
|
+
|
|
876
|
+
- `isCached` reports a sharded custom download once every shard is on disk in
|
|
877
|
+
the folder the SDK keeps them in; it always read false before. `deleteModel`
|
|
878
|
+
removes all of its shards, where it left them behind. See
|
|
879
|
+
[List them](https://github.com/tetherto/qvac-harness/blob/v0.5.0/README.md#list-them)
|
|
880
|
+
and
|
|
881
|
+
[Delete a download](https://github.com/tetherto/qvac-harness/blob/v0.5.0/README.md#delete-a-download).
|
|
882
|
+
(#124)
|
|
370
883
|
|
|
371
884
|
## [0.4.0] - 2026-09-24
|
|
372
885
|
|
|
@@ -375,27 +888,48 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
375
888
|
- `previewModelSource({ src, mmprojSrc })` checks a Hugging Face link, a direct
|
|
376
889
|
`.gguf` link or a local path before a host adds it as a custom model. It
|
|
377
890
|
reads the Hugging Face metadata and the file's GGUF header, never the whole
|
|
378
|
-
model.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
891
|
+
model.
|
|
892
|
+
|
|
893
|
+
The answer is one of three:
|
|
894
|
+
|
|
895
|
+
- a file to register: `ok: true` with `src`, `sizeBytes`, `contextLength`,
|
|
896
|
+
`architecture`, `sizeLabel`, `taskType`, `license`, `fit` and
|
|
897
|
+
`fitReasons`;
|
|
898
|
+
- a choice: `ok: true` with a list of `files` and no `src`, for a repo that
|
|
899
|
+
holds several weights;
|
|
900
|
+
- a refusal: `ok: false` with a `refusal` code and a `reason` to show.
|
|
901
|
+
|
|
902
|
+
It is served on the `RemoteHarness` client and on an in-process `Harness`,
|
|
903
|
+
and its types (`ModelSourcePreview`, `ModelSourceFile`,
|
|
904
|
+
`ModelSourceRefusal`, `ModelFitVerdict`, `PreviewModelSourceContext`) are
|
|
905
|
+
exported from the package root. See
|
|
906
|
+
[Preview a source before adding it](https://github.com/tetherto/qvac-harness/blob/v0.4.0/README.md#preview-a-source-before-adding-it)
|
|
907
|
+
and
|
|
908
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.4.0/docs/decisions/2026-09-22-a-model-source-is-previewed-before-it-is-added.md).
|
|
384
909
|
|
|
385
910
|
```js
|
|
386
911
|
const preview = await harness.previewModelSource({ src: link })
|
|
387
912
|
|
|
388
913
|
if (!preview.ok) return showError(preview.reason)
|
|
389
|
-
if (!preview.src) return pickOneOf(preview.files) //
|
|
390
|
-
if (preview.taskType !== 'llm')
|
|
914
|
+
if (!preview.src) return pickOneOf(preview.files) // preview the pick next
|
|
915
|
+
if (preview.taskType !== 'llm') {
|
|
916
|
+
return showError('Only an llm runs as a custom model')
|
|
917
|
+
}
|
|
391
918
|
```
|
|
392
919
|
|
|
393
|
-
- `setCustomModels` entries take `taskType` and `expectedSize`, and `list()`
|
|
394
|
-
returns the registered custom models after the catalog rows, with
|
|
395
|
-
`registrySource: 'custom'`, `isLoaded`, and an `isCached` that is false
|
|
396
|
-
until every file is on disk and, when `expectedSize` is set, adds up to it.
|
|
397
920
|
(#121)
|
|
398
921
|
|
|
922
|
+
- `setCustomModels` entries take `taskType` and `expectedSize`, and `list()`
|
|
923
|
+
returns the registered custom models after the catalog rows, under the same
|
|
924
|
+
`type` and `cachedOnly` filters.
|
|
925
|
+
|
|
926
|
+
A custom row has `registrySource: 'custom'` and `isLoaded`. Its `isCached`
|
|
927
|
+
is false until every file is on disk and, when `expectedSize` is set, their
|
|
928
|
+
sizes add up to it, so a partial download is not cached. See
|
|
929
|
+
[Register the set](https://github.com/tetherto/qvac-harness/blob/v0.4.0/README.md#register-the-set)
|
|
930
|
+
and
|
|
931
|
+
[List them](https://github.com/tetherto/qvac-harness/blob/v0.4.0/README.md#list-them).
|
|
932
|
+
|
|
399
933
|
```js
|
|
400
934
|
await harness.setCustomModels([
|
|
401
935
|
{
|
|
@@ -411,56 +945,90 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
411
945
|
const custom = models.filter((model) => model.registrySource === 'custom')
|
|
412
946
|
```
|
|
413
947
|
|
|
948
|
+
(#121)
|
|
949
|
+
|
|
414
950
|
- A `modelsDir` Harness option says where downloads live, so `list()` and
|
|
415
951
|
`deleteModel` can find a custom model's files. The spawned sidecar sets it
|
|
416
|
-
from its SDK storage; an in-process `Harness` has to pass it.
|
|
952
|
+
from its SDK storage; an in-process `Harness` has to pass it. See
|
|
953
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.4.0/docs/decisions/2026-09-24-a-custom-download-is-found-at-the-sdk-cache-path.md).
|
|
417
954
|
|
|
418
955
|
```js
|
|
419
956
|
const harness = new Harness({ modelsDir })
|
|
420
957
|
```
|
|
421
958
|
|
|
959
|
+
(#121)
|
|
960
|
+
|
|
422
961
|
### Changed
|
|
423
962
|
|
|
424
|
-
- `deleteModel(name)`
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
963
|
+
- `deleteModel(name)` frees the disk a custom model's download took: it
|
|
964
|
+
removes the weight and projection the model downloaded, where it used to
|
|
965
|
+
remove nothing.
|
|
966
|
+
|
|
967
|
+
It never touches a local path the user pointed at, since those are the
|
|
968
|
+
user's files. The model stays registered and lists as not cached, ready to
|
|
969
|
+
download again. See
|
|
970
|
+
[Delete a download](https://github.com/tetherto/qvac-harness/blob/v0.4.0/README.md#delete-a-download).
|
|
428
971
|
|
|
429
972
|
```js
|
|
430
973
|
const { removed, freedBytes } = await harness.deleteModel('nanbeige-3b')
|
|
431
974
|
```
|
|
432
975
|
|
|
976
|
+
(#121)
|
|
977
|
+
|
|
433
978
|
- A custom model name resolves only where an `llm` is loaded, never for
|
|
434
|
-
another task
|
|
435
|
-
|
|
436
|
-
|
|
979
|
+
another task. A `setCustomModels` entry whose `taskType` is anything other
|
|
980
|
+
than `llm` is not registered. An entry without `taskType` is an `llm`, as
|
|
981
|
+
before, so a host that registers only text models changes nothing. (#121)
|
|
437
982
|
|
|
438
983
|
### Fixed
|
|
439
984
|
|
|
440
|
-
- A
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
985
|
+
- A chat no longer fails with a context overflow after turns that load a
|
|
986
|
+
skill up front. A turn that preloads a skill with `@skill`, or names an
|
|
987
|
+
active skill in its prompt, used to cache the inlined SKILL.md body under
|
|
988
|
+
the chat's KV key. Each such turn left about 7k tokens stranded in the
|
|
989
|
+
cache, until a later turn overflowed the context window and failed. (#120)
|
|
444
990
|
|
|
445
991
|
## [0.3.3] - 2026-09-24
|
|
446
992
|
|
|
447
993
|
### Changed
|
|
448
994
|
|
|
449
|
-
- `@qvac/sdk` and `@qvac/inference` 0.20.0.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
and the
|
|
995
|
+
- `@qvac/sdk` and `@qvac/inference` 0.20.0. A host that packages the addons
|
|
996
|
+
has packaging work to do.
|
|
997
|
+
|
|
998
|
+
Some addons now ship their prebuilds as per-platform packages
|
|
999
|
+
(`@qvac/<addon>-<os>-<cpu>`, for `asr-ggml` 0.5, `audiogen-ggml` 0.4 and
|
|
1000
|
+
`tts-ggml` 0.9), which the addon names in its `#host-addon` import map, and
|
|
1001
|
+
`@qvac/model-fit` and `@qvac/fabric` join the native graph as deferred
|
|
1002
|
+
addons. The harness no longer pins `@qvac/asr-ggml` and lets the SDK resolve
|
|
1003
|
+
it.
|
|
1004
|
+
|
|
1005
|
+
A host that packages the addons resolves prebuilds through that map, and
|
|
1006
|
+
exempts the new package names from any install age gate: bun skips a blocked
|
|
1007
|
+
optional dependency without a word, and the prebuild goes missing. See
|
|
1008
|
+
[The published package](https://github.com/tetherto/qvac-harness/blob/v0.3.3/README.md#the-published-package).
|
|
1009
|
+
|
|
1010
|
+
```toml
|
|
1011
|
+
# bunfig.toml, for each host the app ships to
|
|
1012
|
+
[install]
|
|
1013
|
+
minimumReleaseAgeExcludes = [
|
|
1014
|
+
"@qvac/model-fit",
|
|
1015
|
+
"@qvac/asr-ggml-darwin-arm64",
|
|
1016
|
+
"@qvac/audiogen-ggml-darwin-arm64",
|
|
1017
|
+
"@qvac/tts-ggml-darwin-arm64"
|
|
1018
|
+
]
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
(#85)
|
|
1022
|
+
|
|
457
1023
|
- A user message with several images reaches the SDK as one turn again. The
|
|
458
1024
|
per-image split added for QVAC-24156 is gone, since SDK 0.20 carries the
|
|
459
1025
|
upstream fix (llama.cpp #27348) and equal-size still images no longer merge
|
|
460
|
-
as video frames and abort the encode.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1026
|
+
as video frames and abort the encode.
|
|
1027
|
+
|
|
1028
|
+
This release must not be paired with SDK 0.19, where that crash comes back.
|
|
1029
|
+
`<key>-vision-history-v1` cache directories left by the split are no longer
|
|
1030
|
+
read, and clearing all caches removes them. (#116)
|
|
1031
|
+
|
|
464
1032
|
- `@qvac/skills` 0.1.9. The diagrams skill carries every recipe inline, so a 2B
|
|
465
1033
|
model no longer loops on loading a reference file it cannot name. The excel
|
|
466
1034
|
skill routes a chat image to create rather than edit, the Google OAuth routes
|
|
@@ -478,16 +1046,21 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
478
1046
|
|
|
479
1047
|
### Changed
|
|
480
1048
|
|
|
481
|
-
- A networked Python run
|
|
482
|
-
|
|
1049
|
+
- A person approving a networked Python run now sees where it will connect,
|
|
1050
|
+
and any other host is asked about as the run reaches it.
|
|
1051
|
+
|
|
1052
|
+
The approval label leads with the hosts the source names in a URL, as
|
|
1053
|
+
`python · network: www.youtube.com …`, or says
|
|
483
1054
|
`network: no host named in the code`. On an approved run those hosts, and
|
|
484
|
-
their `www.` form, are reached without asking; a subdomain is not.
|
|
485
|
-
|
|
486
|
-
|
|
1055
|
+
their `www.` form, are reached without asking; a subdomain is not.
|
|
1056
|
+
|
|
1057
|
+
Any other host, such as a CDN a library fans out to, a redirect or a URL
|
|
1058
|
+
built at runtime, pauses the run and is asked about through the completion's
|
|
487
1059
|
`askUser` before its name is looked up. A host allowed once stays allowed
|
|
488
1060
|
for that run, and silence past a minute counts as a no. A host that does not
|
|
489
1061
|
wire `askUser` refuses every unnamed host, and a skip-all run asks nothing.
|
|
490
|
-
|
|
1062
|
+
See
|
|
1063
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.3.2/docs/decisions/2026-09-22-python-reaches-the-network-through-the-guarded-fetch.md).
|
|
491
1064
|
|
|
492
1065
|
```js
|
|
493
1066
|
for await (const event of harness.completion({
|
|
@@ -502,52 +1075,85 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
502
1075
|
}
|
|
503
1076
|
```
|
|
504
1077
|
|
|
1078
|
+
(#109)
|
|
1079
|
+
|
|
505
1080
|
### Fixed
|
|
506
1081
|
|
|
507
|
-
- A timed-out Python run
|
|
508
|
-
Pyodide
|
|
509
|
-
`QVAC_PYODIDE_WORKER_ENTRY`.
|
|
510
|
-
|
|
1082
|
+
- A timed-out Python run no longer spins at full CPU until the process is
|
|
1083
|
+
killed when Pyodide has no worker thread, as for a stowed bundle whose host
|
|
1084
|
+
does not set `QVAC_PYODIDE_WORKER_ENTRY`.
|
|
1085
|
+
|
|
1086
|
+
It is now interrupted at the deadline, rejects with
|
|
1087
|
+
`Command timed out after <ms>ms`, and the interpreter keeps its state. A
|
|
511
1088
|
blocking call such as a long `time.sleep` still sees the interrupt only once
|
|
512
|
-
it returns.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
- A Python run
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1089
|
+
it returns. See
|
|
1090
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.3.2/docs/decisions/2026-08-11-pyodide-worker-thread.md).
|
|
1091
|
+
(#104)
|
|
1092
|
+
|
|
1093
|
+
- A timed-out Python run on the worker thread keeps its interpreter state. It
|
|
1094
|
+
could miss its SIGINT, so the worker was killed after the grace window along
|
|
1095
|
+
with the interpreter's state. The interrupt is now resent every 100 ms until
|
|
1096
|
+
the run settles. (#112)
|
|
1097
|
+
|
|
1098
|
+
- A Python run that fails no longer attaches a broken file. A run that failed
|
|
1099
|
+
partway through a save attached the half-written file, and each failing
|
|
1100
|
+
retry attached another copy under the same name. A non-zero exit now
|
|
1101
|
+
attaches nothing and reports every declared output in `missingOutputs`, so
|
|
1102
|
+
the retry writes it again. (#110)
|
|
520
1103
|
|
|
521
1104
|
### Security
|
|
522
1105
|
|
|
523
1106
|
- Python in `exec` reached the network whenever the runtime allowed it,
|
|
524
1107
|
including a call that auto-ran as a read with no approval prompt, so a
|
|
525
1108
|
document build that loads an image from a URL could reach the web without
|
|
526
|
-
anyone seeing it.
|
|
527
|
-
|
|
1109
|
+
anyone seeing it.
|
|
1110
|
+
|
|
1111
|
+
The network now opens only to a call a person approved or that skip-all let
|
|
1112
|
+
through, and sandbox mode stays offline. 0.3.1 is affected; upgrade from it.
|
|
1113
|
+
See
|
|
1114
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.3.2/docs/decisions/2026-09-22-python-reaches-the-network-through-the-guarded-fetch.md).
|
|
528
1115
|
(#108)
|
|
529
1116
|
|
|
530
1117
|
## [0.3.1] - 2026-09-23
|
|
531
1118
|
|
|
532
1119
|
### Added
|
|
533
1120
|
|
|
534
|
-
- Python in `exec` reaches the network on Bare
|
|
535
|
-
what is built on them, yt-dlp included, work unchanged, and `requests`
|
|
536
|
-
through urllib3's emscripten backend.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1121
|
+
- Python in `exec` reaches the network on Bare. `urllib`, `http.client` and
|
|
1122
|
+
what is built on them, yt-dlp included, work unchanged, and `requests`
|
|
1123
|
+
works through urllib3's emscripten backend.
|
|
1124
|
+
|
|
1125
|
+
Every request goes through the guarded fetch the web tools use: each hop is
|
|
1126
|
+
checked before it is sent, credentials do not follow a redirect to another
|
|
1127
|
+
origin, and a response is capped at 100 MB. Where the global `fetch` is not
|
|
1128
|
+
the guarded one, as on Node, Python stays offline, and `sandboxMode` keeps
|
|
1129
|
+
it offline everywhere. A Python run may now declare `.mp4`, `.webm`, `.m4a`
|
|
1130
|
+
and `.mp3` outputs, each checked by its file signature. See
|
|
1131
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.3.1/docs/decisions/2026-09-22-python-reaches-the-network-through-the-guarded-fetch.md).
|
|
1132
|
+
(#94)
|
|
1133
|
+
|
|
1134
|
+
```js
|
|
1135
|
+
// exec arguments a model can now send on Bare
|
|
1136
|
+
{
|
|
1137
|
+
language: 'python',
|
|
1138
|
+
command:
|
|
1139
|
+
"import urllib.request\n" +
|
|
1140
|
+
"urllib.request.urlretrieve('https://example.com/talk.mp3', 'talk.mp3')",
|
|
1141
|
+
outputs: ['talk.mp3']
|
|
1142
|
+
}
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
- A host can probe a fresh OAuth token before saving it.
|
|
1146
|
+
`SkillOAuthConfig.verify` is parsed from a skill manifest's
|
|
1147
|
+
`setup.routes[].oauth.verify` and carried over the wire;
|
|
1148
|
+
`SkillOAuthVerify` and `SkillOAuthVerifyRule` are exported. The harness
|
|
1149
|
+
only parses the probe; the host runs it.
|
|
1150
|
+
|
|
1151
|
+
A probe is an https `url` on the same site as the route's `tokenUrl`, an
|
|
1152
|
+
optional `GET` or `POST` `method` and JSON `body`, and `reject` rules. A
|
|
1153
|
+
rule matches a listed `status`, or a 2xx JSON body whose `field` is not
|
|
1154
|
+
`notEqual` (a rule with both needs both), and its `error` names the
|
|
1155
|
+
rejection. A probe that breaks these rules is dropped and the route kept.
|
|
1156
|
+
(#105)
|
|
551
1157
|
|
|
552
1158
|
```js
|
|
553
1159
|
const skills = await harness.skills()
|
|
@@ -562,63 +1168,95 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
562
1168
|
```
|
|
563
1169
|
|
|
564
1170
|
- A skill operation may name `response.decode` to turn a provider's JSON body
|
|
565
|
-
into a compact shape before the model sees it
|
|
566
|
-
`gmail-message`: the ids and labels, the From, To, Subject, Date,
|
|
567
|
-
and References headers, the snippet, and the body as text,
|
|
568
|
-
characters. A `gmail-get` request builder
|
|
569
|
-
|
|
570
|
-
|
|
1171
|
+
into a compact shape before the model sees it. The first decoder is
|
|
1172
|
+
`gmail-message`: the ids and labels, the From, To, Subject, Date,
|
|
1173
|
+
Message-ID and References headers, the snippet, and the body as text,
|
|
1174
|
+
capped at 4000 characters. A `gmail-get` request builder goes with it.
|
|
1175
|
+
|
|
1176
|
+
A manifest naming both `pick` and `decode`, or a builder or decoder this
|
|
1177
|
+
harness does not have, is rejected. See
|
|
1178
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.3.1/docs/decisions/2026-09-01-skill-operation-manifests.md).
|
|
1179
|
+
(#69)
|
|
1180
|
+
|
|
1181
|
+
```js
|
|
1182
|
+
// an operation in a skill's operations.json (gmail, @qvac/skills 0.1.7)
|
|
1183
|
+
{
|
|
1184
|
+
tool: 'gmail_get',
|
|
1185
|
+
request: {
|
|
1186
|
+
method: 'GET',
|
|
1187
|
+
url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}',
|
|
1188
|
+
query: { metadataHeaders: 'Subject,From,To,Date,Message-ID,References' },
|
|
1189
|
+
builder: 'gmail-get'
|
|
1190
|
+
},
|
|
1191
|
+
response: { decode: 'gmail-message' }
|
|
1192
|
+
}
|
|
1193
|
+
```
|
|
571
1194
|
|
|
572
1195
|
### Changed
|
|
573
1196
|
|
|
574
|
-
- A compiled skill operation whose HTTP method is `GET`, `HEAD` or `OPTIONS`
|
|
575
|
-
|
|
576
|
-
`http_request`, and takes no `intent` argument. MCP operations and
|
|
577
|
-
method are still writes that ask. (#69)
|
|
578
|
-
- `@qvac/skills` 0.1.7, now pinned exactly
|
|
1197
|
+
- A compiled skill operation whose HTTP method is `GET`, `HEAD` or `OPTIONS`
|
|
1198
|
+
now runs without an approval prompt. It is a read, like the same request
|
|
1199
|
+
through `http_request`, and takes no `intent` argument. MCP operations and
|
|
1200
|
+
every other method are still writes that ask. (#69)
|
|
1201
|
+
- `@qvac/skills` 0.1.7, now pinned exactly. Gmail reads a message with
|
|
579
1202
|
`gmail_get` instead of telling the model to decode base64, which needs this
|
|
580
1203
|
release's decoder, and Asana rejects a non-MCP app at connect time through
|
|
581
1204
|
a `verify` probe. (#105)
|
|
582
1205
|
|
|
583
1206
|
### Fixed
|
|
584
1207
|
|
|
585
|
-
- A malformed `<tool_call>` left its JSON arguments in the reply. The whole
|
|
586
|
-
is now stripped, and an unclosed one is stripped to the end of the
|
|
587
|
-
(#100)
|
|
1208
|
+
- A malformed `<tool_call>` left its JSON arguments in the reply. The whole
|
|
1209
|
+
span is now stripped, and an unclosed one is stripped to the end of the
|
|
1210
|
+
text. (#100)
|
|
588
1211
|
- A misspelt, placeholder or repeated `skill` load returned an error the model
|
|
589
1212
|
retried until the turn ran out. A placeholder or the skill's own name now
|
|
590
|
-
reads the body, a near miss of one of its files loads that file, and a
|
|
591
|
-
is acknowledged without re-reading it. (#101)
|
|
1213
|
+
reads the body, a near miss of one of its files loads that file, and a
|
|
1214
|
+
repeat is acknowledged without re-reading it. (#101)
|
|
592
1215
|
- When the model reached for `pip` or a video tool on the shell path, it was
|
|
593
1216
|
only told what the path is not, and retried the same call. Each refusal now
|
|
594
1217
|
names the call that works, such as `packages: ["requests"]` with the pin a
|
|
595
|
-
vendored package installs offline under.
|
|
596
|
-
|
|
597
|
-
|
|
1218
|
+
vendored package installs offline under.
|
|
1219
|
+
|
|
1220
|
+
A missing import names the package that provides it, and `python -c`, a
|
|
1221
|
+
heredoc or bare Python source runs as Python instead of being sent back for
|
|
1222
|
+
a `language` argument. See
|
|
1223
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.3.1/docs/decisions/2026-09-21-exec-names-the-call-that-works.md).
|
|
1224
|
+
(#71)
|
|
598
1225
|
|
|
599
1226
|
## [0.3.0] - 2026-09-23
|
|
600
1227
|
|
|
601
1228
|
### Added
|
|
602
1229
|
|
|
603
|
-
- A `translate` capability for `invoke`:
|
|
604
|
-
LLM. The language pair picks the route before anything loads
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1230
|
+
- A `translate` capability for `invoke`: text translation without loading an
|
|
1231
|
+
LLM. The language pair picks the route before anything loads:
|
|
1232
|
+
|
|
1233
|
+
- English and one of nine European languages go through the
|
|
1234
|
+
TranslatePsy-EuroNano packs.
|
|
1235
|
+
- The other 43 languages Bergamot serves go through their per-pair models,
|
|
1236
|
+
pivoting through English when neither side is English.
|
|
1237
|
+
- 16 languages Bergamot has no model for go through TranslatePsy-AfriSLM,
|
|
1238
|
+
named by ISO 639-1 or FLORES-200 code.
|
|
1239
|
+
|
|
1240
|
+
An omitted `from` is detected from the text, except for the AfriSLM
|
|
1241
|
+
languages, which need it named. The `tool-result` carries the text, the pair
|
|
1242
|
+
and the `engine` that answered (`nmt`, `slm`, or `none` when the text is
|
|
1243
|
+
already in the target language). A pair no route serves, or a source that
|
|
1244
|
+
cannot be detected, comes back as `result.error`. See
|
|
1245
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.3.0/docs/decisions/2026-09-21-translation-pivots-through-english.md).
|
|
1246
|
+
(#76)
|
|
612
1247
|
|
|
613
1248
|
```js
|
|
614
1249
|
for await (const event of harness.invoke({
|
|
615
1250
|
capability: 'translate',
|
|
616
1251
|
args: { text: 'The weather is nice today.', to: 'de' }
|
|
617
1252
|
})) {
|
|
618
|
-
if (event.type === 'tool-result')
|
|
1253
|
+
if (event.type === 'tool-result') {
|
|
1254
|
+
if (event.result.error) showError(event.result.error)
|
|
1255
|
+
else show(event.result.text)
|
|
1256
|
+
}
|
|
619
1257
|
if (event.type === 'error') showError(event.message)
|
|
620
1258
|
}
|
|
621
|
-
// result: { text, from, to: 'de', engine: 'nmt', pivoted: false }
|
|
1259
|
+
// result: { text, from: 'en', to: 'de', engine: 'nmt', pivoted: false }
|
|
622
1260
|
```
|
|
623
1261
|
|
|
624
1262
|
- `translationPlanFor`, `NMT_TO_ENGLISH`, `NMT_FROM_ENGLISH`,
|
|
@@ -635,18 +1273,23 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
635
1273
|
if ('error' in plan) showError(plan.error)
|
|
636
1274
|
```
|
|
637
1275
|
|
|
638
|
-
- `enhance: 'never' | 'auto'` on `invoke
|
|
639
|
-
rewritten by a model already resident on the
|
|
640
|
-
load: with nothing resident, or a rewrite that
|
|
1276
|
+
- `enhance: 'never' | 'auto'` on `invoke`, for the `image` capability. With
|
|
1277
|
+
`'auto'`, the prompt is rewritten by a model already resident on the
|
|
1278
|
+
device, and never triggers a load: with nothing resident, or a rewrite that
|
|
1279
|
+
fails, the run goes direct. Omitting `enhance` behaves as `'never'`, which
|
|
1280
|
+
passes the arguments through untouched.
|
|
1281
|
+
|
|
641
1282
|
`metrics` reports the `route` taken (`direct` or `enhanced`), the
|
|
642
|
-
`routeModel` used and the tokens the rewrite spent.
|
|
643
|
-
|
|
1283
|
+
`routeModel` used and the tokens the rewrite spent. See
|
|
1284
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.3.0/docs/decisions/2026-09-21-a-caller-states-constraints-not-mechanisms.md).
|
|
1285
|
+
(#77)
|
|
644
1286
|
|
|
645
1287
|
```js
|
|
646
1288
|
for await (const event of harness.invoke({
|
|
647
1289
|
capability: 'image',
|
|
648
1290
|
args: { prompt: 'funny meme' },
|
|
649
|
-
enhance: 'auto'
|
|
1291
|
+
enhance: 'auto',
|
|
1292
|
+
attach
|
|
650
1293
|
})) {
|
|
651
1294
|
if (event.type === 'metrics') log(event.route, event.routeModel)
|
|
652
1295
|
}
|
|
@@ -654,33 +1297,56 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
654
1297
|
|
|
655
1298
|
### Changed
|
|
656
1299
|
|
|
657
|
-
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
dependency of `@qvac/sdk
|
|
1300
|
+
- A host that assembles the native prebuilds beside the bundle now has to
|
|
1301
|
+
include `@qvac/translation-nmtcpp`. The bundle defers it like the other
|
|
1302
|
+
inference addons, so `prebuilds-manifest.json` names its prebuilds. It is
|
|
1303
|
+
already a dependency of `@qvac/sdk`, so it is in the host's
|
|
1304
|
+
`node_modules`. See
|
|
1305
|
+
[The published package](https://github.com/tetherto/qvac-harness/blob/v0.3.0/README.md#the-published-package).
|
|
1306
|
+
(#76)
|
|
661
1307
|
|
|
662
1308
|
## [0.2.12] - 2026-09-22
|
|
663
1309
|
|
|
1310
|
+
The first published release with the 0.2.11 changes (`invoke`,
|
|
1311
|
+
`detectLanguage`, `SkillOAuthConfig`), plus the fix below.
|
|
1312
|
+
|
|
664
1313
|
### Fixed
|
|
665
1314
|
|
|
666
|
-
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1315
|
+
- The pdf, presentations, word and excel skills work again under
|
|
1316
|
+
`sandboxMode`. Sandbox mode refused every `exec` call that installs
|
|
1317
|
+
packages, including installs that never leave the device.
|
|
1318
|
+
|
|
1319
|
+
A request whose packages are all vendored, each pinned to the version the
|
|
1320
|
+
shipped wheels carry, now installs from those wheels. One unvendored
|
|
1321
|
+
package, an unpinned version, or a host that ships no wheels would fall
|
|
1322
|
+
back to PyPI, and is still refused. See
|
|
1323
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.2.12/docs/decisions/2026-09-21-sandbox-mode-no-network.md#consequences).
|
|
1324
|
+
(#91)
|
|
670
1325
|
|
|
671
1326
|
## [0.2.11] - 2026-09-22
|
|
672
1327
|
|
|
673
1328
|
Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
1329
|
+
Install 0.2.12 or later to get them.
|
|
674
1330
|
|
|
675
1331
|
### Added
|
|
676
1332
|
|
|
677
1333
|
- `invoke({ capability, args })` runs one capability on its own, with no
|
|
678
1334
|
model, no dispatch and no approval callback: the caller already decided.
|
|
679
|
-
`image` is the first capability
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1335
|
+
`image` is the first capability, and `CAPABILITIES` and `isCapability` list
|
|
1336
|
+
and check the names.
|
|
1337
|
+
|
|
1338
|
+
It is a frame on the existing `completion` stream, so it takes the same
|
|
1339
|
+
`attach`, `progress` and abort signal, and emits the same `tool-call`,
|
|
1340
|
+
`tool-result`, `metrics`, `error` and `aborted` events. `metrics` reports
|
|
1341
|
+
`totalTokens: 0`. An optional `agent` supplies the image defaults (model,
|
|
1342
|
+
steps, size). A failure inside the tool, such as a missing `attach`, comes
|
|
1343
|
+
back as `result.error` on the `tool-result`; the `error` event is for a
|
|
1344
|
+
capability the device lacks or a tool that threw.
|
|
1345
|
+
|
|
1346
|
+
It is on `Harness` and `RemoteHarness` alike, and optional on
|
|
1347
|
+
`HarnessContract`. See
|
|
1348
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.2.11/docs/decisions/2026-09-21-a-run-that-loads-no-model.md).
|
|
1349
|
+
(#74)
|
|
684
1350
|
|
|
685
1351
|
```js
|
|
686
1352
|
for await (const event of harness.invoke({
|
|
@@ -690,14 +1356,20 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
690
1356
|
progress,
|
|
691
1357
|
signal
|
|
692
1358
|
})) {
|
|
693
|
-
if (event.type === 'tool-result')
|
|
1359
|
+
if (event.type === 'tool-result') {
|
|
1360
|
+
// { attachmentId, fileName, byteLength, width, height } or { error }
|
|
1361
|
+
if (event.result.error) showError(event.result.error)
|
|
1362
|
+
else showImage(event.result.attachmentId)
|
|
1363
|
+
}
|
|
694
1364
|
if (event.type === 'error') showError(event.message)
|
|
695
1365
|
}
|
|
696
1366
|
```
|
|
697
1367
|
|
|
698
|
-
- `detectLanguage({ text, topK })
|
|
699
|
-
on `
|
|
700
|
-
`
|
|
1368
|
+
- `detectLanguage({ text, topK })`: language detection with no model to load.
|
|
1369
|
+
It is on `Harness`, served over hrpc as `detect-language`, and on
|
|
1370
|
+
`RemoteHarness`.
|
|
1371
|
+
|
|
1372
|
+
Without `topK`, or with 1, it answers with the single best match and no
|
|
701
1373
|
probability. Above that it returns ranked candidates with their
|
|
702
1374
|
probabilities, best first, and may return fewer than asked. Text it cannot
|
|
703
1375
|
place answers `und`. (#75)
|
|
@@ -710,15 +1382,31 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
710
1382
|
// [{ code, language, probability }, ...]
|
|
711
1383
|
```
|
|
712
1384
|
|
|
713
|
-
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1385
|
+
- A host can run a skill's OAuth setup from its manifest instead of its own
|
|
1386
|
+
provider table. `SkillSetupRoute.oauth` carries a `SkillOAuthConfig` parsed
|
|
1387
|
+
from `setup.routes[].oauth` and sent over the wire, and `SkillOAuthConfig`
|
|
1388
|
+
is exported.
|
|
1389
|
+
|
|
1390
|
+
`stateKey`, an https `authUrl` and `tokenUrl`, and `tokenAuth`
|
|
1391
|
+
(`pkce-only`, `secret-in-body` or `secret-in-basic`) are required; an
|
|
1392
|
+
`oauth` route without a valid config is dropped. `extraAuthParams` may not
|
|
1393
|
+
override the parameters the flow owns, such as `redirect_uri` or `state`.
|
|
1394
|
+
(#88)
|
|
1395
|
+
|
|
1396
|
+
```js
|
|
1397
|
+
const skills = await harness.skills()
|
|
1398
|
+
const route = skills
|
|
1399
|
+
.find((skill) => skill.name === 'spotify')
|
|
1400
|
+
?.setup?.routes?.find((r) => r.kind === 'oauth')
|
|
1401
|
+
|
|
1402
|
+
// { stateKey, authUrl, tokenUrl, scopes, tokenAuth,
|
|
1403
|
+
// extraAuthParams?, registrationEndpoint?, port?, sharedIosSession? }
|
|
1404
|
+
const oauth = route?.oauth
|
|
1405
|
+
```
|
|
718
1406
|
|
|
719
1407
|
### Changed
|
|
720
1408
|
|
|
721
|
-
- `@qvac/skills` 0.1.6
|
|
1409
|
+
- `@qvac/skills` `^0.1.6`: the eight OAuth skills declare their setup routes'
|
|
722
1410
|
OAuth config, the Spotify skill always makes the play call after a search,
|
|
723
1411
|
and the weather skill looks the place up with `weather_lookup`. (#88)
|
|
724
1412
|
|
|
@@ -735,11 +1423,17 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
735
1423
|
|
|
736
1424
|
### Added
|
|
737
1425
|
|
|
738
|
-
- `sandboxMode` agent option: an agent
|
|
739
|
-
|
|
740
|
-
|
|
1426
|
+
- `sandboxMode` agent option: an agent that cannot reach the network. Set it
|
|
1427
|
+
on the agent and every network tool is withheld before the model sees the
|
|
1428
|
+
catalog: the web search, HTTP request and MCP toolsets, including skill
|
|
1429
|
+
operations compiled over HTTP or MCP, not just web search.
|
|
1430
|
+
|
|
741
1431
|
`exec` stays, but refuses package installs and well-known network commands
|
|
742
|
-
such as `curl`, `git` and `npm`. That is a guard, not an OS-level
|
|
1432
|
+
such as `curl`, `git` and `npm`. That is a name-list guard, not an OS-level
|
|
1433
|
+
sandbox: another binary, or Python calling `pyfetch`, can still open a
|
|
1434
|
+
socket. The six toolset switches stay independent; this is the one knob
|
|
1435
|
+
that means "no network". See
|
|
1436
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.2.10/docs/decisions/2026-09-21-sandbox-mode-no-network.md).
|
|
743
1437
|
(#73)
|
|
744
1438
|
|
|
745
1439
|
```js
|
|
@@ -752,12 +1446,17 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
752
1446
|
}
|
|
753
1447
|
```
|
|
754
1448
|
|
|
755
|
-
- `@qvac/harness/office`: XLSX and PPTX text extraction for Knowledge beside
|
|
756
|
-
DOCX, through the same bundled Python worker.
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1449
|
+
- `@qvac/harness/office`: XLSX and PPTX text extraction for Knowledge, beside
|
|
1450
|
+
DOCX, through the same bundled Python worker. `extractOfficeText` picks the
|
|
1451
|
+
format from the file extension, with `mimeType` as the fallback.
|
|
1452
|
+
|
|
1453
|
+
XLSX keeps sheet names, column positions, cached formula values (formula
|
|
1454
|
+
text where there is no cached value). PPTX keeps slide text, grouped shapes
|
|
1455
|
+
and tables in slide order; speaker notes, images and charts are left out.
|
|
1456
|
+
Legacy XLS and PPT are not supported. `@qvac/harness/docx` is unchanged and
|
|
1457
|
+
shares the worker. See
|
|
1458
|
+
[Office extraction](https://github.com/tetherto/qvac-harness/blob/v0.2.10/README.md#office-extraction)
|
|
1459
|
+
for the cache and wheel paths a host sets. (#83)
|
|
761
1460
|
|
|
762
1461
|
```js
|
|
763
1462
|
import { extractOfficeText, closeOfficeWorker } from '@qvac/harness/office'
|
|
@@ -769,43 +1468,57 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
769
1468
|
|
|
770
1469
|
### Fixed
|
|
771
1470
|
|
|
772
|
-
- A small model
|
|
773
|
-
cap. A call that already failed this turn with the same arguments is
|
|
1471
|
+
- A small model no longer loops on the same failing tool call up to the round
|
|
1472
|
+
cap. A call that already failed this turn with the same arguments is
|
|
774
1473
|
answered from its first result without running again, until a later round
|
|
775
|
-
succeeds. Three such answers end the turn's tool rounds
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
the
|
|
782
|
-
|
|
783
|
-
|
|
1474
|
+
succeeds. Three such answers end the turn's tool rounds; the tool set is
|
|
1475
|
+
never changed, so the prompt cache is kept.
|
|
1476
|
+
|
|
1477
|
+
A failure that may pass on retry, such as a timeout, a 5xx, 408 or 429,
|
|
1478
|
+
still runs again, and the tools that can fail that way now say so in a
|
|
1479
|
+
`code` or `status` field on their result. See
|
|
1480
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.2.10/docs/decisions/2026-09-21-a-failed-call-is-answered-from-its-own-result.md).
|
|
1481
|
+
(#78)
|
|
1482
|
+
|
|
1483
|
+
- A guessed `auth.tokenCredentialKey` on `http_request` or `mcp_call` now
|
|
1484
|
+
works instead of failing with an error claiming the skill was not loaded.
|
|
1485
|
+
It resolves to the one connected credential allowed for that URL. When
|
|
1486
|
+
none is connected, the error says so and tells the user to connect it in
|
|
1487
|
+
Skills. (#72)
|
|
784
1488
|
|
|
785
1489
|
## [0.2.9] - 2026-09-22
|
|
786
1490
|
|
|
787
1491
|
### Added
|
|
788
1492
|
|
|
789
|
-
-
|
|
790
|
-
`
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1493
|
+
- A host can record exactly what each inference round sent. Set
|
|
1494
|
+
`captureInferenceCalls` on a completion's context, and each round emits an
|
|
1495
|
+
`inference-call` event with the payload it is about to send: model,
|
|
1496
|
+
messages, tools, generation params, cache key and prompt prefix.
|
|
1497
|
+
|
|
1498
|
+
An `inference-sent` event follows once the round ends, with the tokens it
|
|
1499
|
+
spent, its `outcome` (`completed`, `overflowed` or `failed`) and, on the
|
|
1500
|
+
bundled patched SDK, the slice of `messages` the KV cache let through. An
|
|
1501
|
+
attempt the overflow ladder rejected is still recorded, with its
|
|
1502
|
+
`droppedTurns`. See
|
|
1503
|
+
[What actually gets sent](https://github.com/tetherto/qvac-harness/blob/v0.2.9/docs/kvcache-reference.md#what-actually-gets-sent).
|
|
1504
|
+
|
|
1505
|
+
Off unless asked for, since every record carries a whole prompt. The flag
|
|
1506
|
+
and both events are new on the hrpc wire, so the host and the spawned
|
|
1507
|
+
harness both need 0.2.9. (#51)
|
|
798
1508
|
|
|
799
1509
|
```js
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (event.type === 'inference-
|
|
1510
|
+
const context = { model, agent, messages, captureInferenceCalls: true }
|
|
1511
|
+
for await (const event of harness.completion(context)) {
|
|
1512
|
+
if (event.type === 'inference-call') record(event.call)
|
|
1513
|
+
if (event.type === 'inference-sent') settle(event.round, event.outcome)
|
|
803
1514
|
}
|
|
804
1515
|
```
|
|
805
1516
|
|
|
806
|
-
-
|
|
807
|
-
|
|
808
|
-
|
|
1517
|
+
- A host can tell a cut-off answer from a complete one. The `metrics` event
|
|
1518
|
+
carries the last round's `stopReason`, and leaves it out when the SDK
|
|
1519
|
+
reports none. A turn cut off by the context window ends cleanly with
|
|
1520
|
+
`stopReason: 'length'`. See
|
|
1521
|
+
[Compaction](https://github.com/tetherto/qvac-harness/blob/v0.2.9/docs/kvcache-reference.md#compaction).
|
|
809
1522
|
(#62)
|
|
810
1523
|
|
|
811
1524
|
```js
|
|
@@ -822,15 +1535,21 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
822
1535
|
### Added
|
|
823
1536
|
|
|
824
1537
|
- `weather_lookup`, a tool that looks weather up on wttr.in by `location` and
|
|
825
|
-
leads its report with the place the service actually resolved.
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1538
|
+
leads its report with the place the service actually resolved.
|
|
1539
|
+
|
|
1540
|
+
When that place disagrees with the country or state the user named, it
|
|
1541
|
+
refuses instead of reporting the weather of wherever the geocoder landed: a
|
|
1542
|
+
nonexistent town no longer gets a forecast, and a bare "Rome" the service
|
|
1543
|
+
places in Togo says so.
|
|
1544
|
+
|
|
1545
|
+
It is offered only once the weather skill is loaded, needs no approval, and
|
|
1546
|
+
the `http-request` toolset switch turns it off. The spawned sidecar registers
|
|
1547
|
+
it; an in-process host adds it to its tools. See
|
|
1548
|
+
[the decision record](https://github.com/tetherto/qvac-harness/blob/v0.2.8/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md).
|
|
1549
|
+
(#52)
|
|
832
1550
|
|
|
833
1551
|
```js
|
|
1552
|
+
import { Harness } from '@qvac/harness'
|
|
834
1553
|
import { createWeatherLookupTool } from '@qvac/harness/lib/tools/weather-lookup.ts'
|
|
835
1554
|
|
|
836
1555
|
const harness = new Harness({ tools: [...tools, createWeatherLookupTool()] })
|
|
@@ -845,20 +1564,25 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
845
1564
|
the version. (#55)
|
|
846
1565
|
|
|
847
1566
|
```js
|
|
848
|
-
//
|
|
1567
|
+
// before
|
|
1568
|
+
import { currentReleaseLine } from '@qvac/harness/schema/release-tags'
|
|
849
1569
|
const line = currentReleaseLine(root) // '0.2'
|
|
850
1570
|
|
|
851
|
-
//
|
|
1571
|
+
// after
|
|
1572
|
+
import { currentReleaseVersion } from '@qvac/harness/schema/release-tags'
|
|
852
1573
|
const version = currentReleaseVersion(root) // Version 0.2.8
|
|
853
1574
|
const line = `${version.major}.${version.minor}`
|
|
854
1575
|
```
|
|
855
1576
|
|
|
856
|
-
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
the package version are left out.
|
|
860
|
-
|
|
861
|
-
|
|
1577
|
+
- A schema change is now checked against the build users are running.
|
|
1578
|
+
`previousReleaseTags` from the same module counts the newest shipped tag on
|
|
1579
|
+
the line being built as a baseline, not only older lines; only tags newer
|
|
1580
|
+
than the package version are left out.
|
|
1581
|
+
|
|
1582
|
+
A host that regenerates its `spec/` against it re-derives hyperschema's
|
|
1583
|
+
version counters once, so expect one regeneration commit after upgrading.
|
|
1584
|
+
(#55, #58)
|
|
1585
|
+
|
|
862
1586
|
- `@qvac/skills` 0.1.4: the Apple Notes create-note operation no longer
|
|
863
1587
|
renames a note after creating it, which failed on iCloud accounts and left
|
|
864
1588
|
duplicate notes behind. (#48)
|
|
@@ -867,14 +1591,17 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
867
1591
|
|
|
868
1592
|
### Added
|
|
869
1593
|
|
|
870
|
-
- `@qvac/harness/docx`: `extractDocxText`
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1594
|
+
- `@qvac/harness/docx`: `extractDocxText` gives Knowledge a DOCX file's text,
|
|
1595
|
+
pulled with the bundled Python runtime. Paragraphs and tables keep document
|
|
1596
|
+
order, one table row per line with cells joined by `' | '`.
|
|
1597
|
+
|
|
1598
|
+
It takes up to 20 MiB and only the shipped wheels, never a network install.
|
|
1599
|
+
Set `QVAC_PYODIDE_CACHE_DIR` to a writable directory first, so the worker
|
|
1600
|
+
seeds its cache from those wheels and works offline. A bundled host also
|
|
1601
|
+
sets `QVAC_PYODIDE_WORKER_ENTRY` and `QVAC_PYTHON_WHEELS_DIR` to its staged
|
|
1602
|
+
worker and wheels. See
|
|
1603
|
+
[DOCX extraction](https://github.com/tetherto/qvac-harness/blob/v0.2.7/README.md#docx-extraction).
|
|
1604
|
+
(#46)
|
|
878
1605
|
|
|
879
1606
|
```js
|
|
880
1607
|
import { extractDocxText, closeDocxWorker } from '@qvac/harness/docx'
|
|
@@ -906,36 +1633,45 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
906
1633
|
})
|
|
907
1634
|
```
|
|
908
1635
|
|
|
909
|
-
- `writeClient` and
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1636
|
+
- A caller can declare which hrpc requests may be omitted. `writeClient` and
|
|
1637
|
+
`writeCapabilities` from `@qvac/harness/schema/generate` take an options
|
|
1638
|
+
argument whose `optionalRequests` names those request types: the generated
|
|
1639
|
+
client sends `{}` for them, and the generated capability types mark the input
|
|
1640
|
+
optional.
|
|
1641
|
+
|
|
1642
|
+
Left out, only `@assistant/empty` is optional, as before. A set you pass
|
|
1643
|
+
replaces that default, so include it if you still want it. `writeClient`
|
|
1644
|
+
still takes an export name as a string. (#44)
|
|
916
1645
|
|
|
917
1646
|
```js
|
|
918
1647
|
import { writeClient, writeCapabilities } from '@qvac/harness/schema/generate'
|
|
919
1648
|
|
|
920
1649
|
const optionalRequests = new Set(['@assistant/empty', '@assistant/mesh-scope'])
|
|
921
|
-
writeClient(builder, 'spec/calls.js', {
|
|
1650
|
+
writeClient(builder, 'spec/calls.js', {
|
|
1651
|
+
exportName: 'engineClient',
|
|
1652
|
+
optionalRequests
|
|
1653
|
+
})
|
|
922
1654
|
writeCapabilities(builder, 'spec/capabilities.d.ts', { optionalRequests })
|
|
923
1655
|
```
|
|
924
1656
|
|
|
925
1657
|
### Changed
|
|
926
1658
|
|
|
927
|
-
-
|
|
928
|
-
|
|
929
|
-
five minutes, and then loads anyway rather than failing.
|
|
930
|
-
|
|
1659
|
+
- Switching models no longer stacks a second one in memory beside a live run.
|
|
1660
|
+
A completion that needs a different model waits for that run to release its
|
|
1661
|
+
model, for up to five minutes, and then loads anyway rather than failing.
|
|
1662
|
+
|
|
1663
|
+
A `warmModel` call waits two seconds at most, and skips the warm if no room
|
|
1664
|
+
was made. (#41)
|
|
931
1665
|
|
|
932
1666
|
## [0.2.5] - 2026-09-14
|
|
933
1667
|
|
|
934
1668
|
### Added
|
|
935
1669
|
|
|
936
|
-
- A
|
|
937
|
-
|
|
938
|
-
own probe. Left out, the harness probes for itself.
|
|
1670
|
+
- A host that already has the GPU reading can hand it over: a `gpuStats`
|
|
1671
|
+
option on `spawnHarness`, `harnessChild` and `HarnessOptions`, and the
|
|
1672
|
+
harness skips its own probe. Left out, the harness probes for itself. It
|
|
1673
|
+
takes the `bare-gpu-info` shape: `memory.totalBytes` and `gpu.devices`, each
|
|
1674
|
+
with a `name` and `videoMemoryBytes`. (#36)
|
|
939
1675
|
|
|
940
1676
|
```js
|
|
941
1677
|
import { spawnHarness } from '@qvac/harness/lib/spawn.ts'
|
|
@@ -945,54 +1681,72 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
945
1681
|
sdkStorage,
|
|
946
1682
|
gpuStats: {
|
|
947
1683
|
memory: { totalBytes: os.totalmem() },
|
|
948
|
-
gpu: {
|
|
1684
|
+
gpu: {
|
|
1685
|
+
devices: [{ name: 'NVIDIA RTX 4070', videoMemoryBytes: 12884901888 }]
|
|
1686
|
+
}
|
|
949
1687
|
}
|
|
950
1688
|
})
|
|
951
1689
|
```
|
|
952
1690
|
|
|
953
1691
|
### Fixed
|
|
954
1692
|
|
|
955
|
-
-
|
|
956
|
-
supervisor respawned it in a loop: the boot-time capability probe
|
|
957
|
-
second GPU-info instance beside the SDK's own.
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1693
|
+
- The harness stays up on a Linux snap. It exited about a second after `open`,
|
|
1694
|
+
and the supervisor respawned it in a loop: the boot-time capability probe
|
|
1695
|
+
created a second GPU-info instance beside the SDK's own.
|
|
1696
|
+
|
|
1697
|
+
On Linux and Windows under Bare, GPUs are now enumerated in a short-lived
|
|
1698
|
+
child process, which gives up after 5 seconds and reports no GPU. (#36)
|
|
1699
|
+
|
|
1700
|
+
- On Windows, asking for an image after a presentation no longer leaves the
|
|
1701
|
+
chat stuck on "Loading … into memory". Pyodide's boot opened the process
|
|
1702
|
+
streams from the Python worker thread, which blocked the image-model loader's
|
|
1703
|
+
progress. Boot now never touches them; a run's own output capture is
|
|
1704
|
+
unchanged. (#34)
|
|
964
1705
|
|
|
965
1706
|
## [0.2.4] - 2026-09-12
|
|
966
1707
|
|
|
967
1708
|
### Added
|
|
968
1709
|
|
|
969
|
-
- `@qvac/harness/worker/stow-entry.mjs`:
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
throwing: `@qvac/core` 0.1.2
|
|
975
|
-
for a host that relocates the
|
|
1710
|
+
- `@qvac/harness/worker/stow-entry.mjs`: a sidecar entry a host running from
|
|
1711
|
+
inside `node_modules` spawns directly, without the prebuilt bundle. Every
|
|
1712
|
+
native addon then resolves beside the JS that calls it, from one install.
|
|
1713
|
+
|
|
1714
|
+
With the bundle, the addons come from the host's own install, and a version
|
|
1715
|
+
skew there hangs the child on boot rather than throwing: `@qvac/core` 0.1.2
|
|
1716
|
+
shipped that way. The bundle stays published for a host that relocates the
|
|
1717
|
+
code out of `node_modules`. (#31)
|
|
976
1718
|
|
|
977
1719
|
```js
|
|
978
1720
|
import { harnessChild } from '@qvac/harness/lib/spawn.ts'
|
|
979
1721
|
|
|
980
1722
|
const entry = new URL(import.meta.resolve('@qvac/harness/worker/stow-entry.mjs')).pathname
|
|
981
|
-
const spec = harnessChild(entry, {
|
|
1723
|
+
const spec = harnessChild(entry, {
|
|
1724
|
+
runner,
|
|
1725
|
+
skillsDir,
|
|
1726
|
+
wheelsDir,
|
|
1727
|
+
sdkStorage: storage.sdk
|
|
1728
|
+
})
|
|
982
1729
|
```
|
|
983
1730
|
|
|
984
1731
|
## [0.2.3] - 2026-09-12
|
|
985
1732
|
|
|
986
1733
|
### Added
|
|
987
1734
|
|
|
988
|
-
-
|
|
989
|
-
|
|
1735
|
+
- A duplex handler's frames now type without a local widening:
|
|
1736
|
+
`schema/generate` emits `writeStream` on the generated `HandlerFrames`.
|
|
1737
|
+
(#29)
|
|
1738
|
+
|
|
1739
|
+
```js
|
|
1740
|
+
// in a generated spec, a duplex handler can watch its write side close
|
|
1741
|
+
frames.writeStream?.on('close', onClose)
|
|
1742
|
+
```
|
|
990
1743
|
|
|
991
1744
|
### Changed
|
|
992
1745
|
|
|
993
|
-
-
|
|
994
|
-
persistent root instead of the device HOME, which an app
|
|
995
|
-
Desktop and Snap are unchanged, and `overrides.models` still
|
|
1746
|
+
- Models download on Android and iOS. `resolveStorage` keeps them under the
|
|
1747
|
+
app's own persistent root there, instead of the device HOME, which an app
|
|
1748
|
+
cannot write. Desktop and Snap are unchanged, and `overrides.models` still
|
|
1749
|
+
wins. (#29)
|
|
996
1750
|
|
|
997
1751
|
```js
|
|
998
1752
|
import { resolveStorage } from '@qvac/harness/storage'
|
|
@@ -1004,37 +1758,41 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1004
1758
|
|
|
1005
1759
|
### Fixed
|
|
1006
1760
|
|
|
1007
|
-
- Every `./lib/tools/*` subpath is
|
|
1008
|
-
|
|
1009
|
-
`read-files`, `create-documents`, `edit-documents
|
|
1010
|
-
`PACKAGE_PATH_NOT_EXPORTED`. 0.1.1 to 0.2.2 are
|
|
1761
|
+
- The mobile worklet's file tools load again. Every `./lib/tools/*` subpath is
|
|
1762
|
+
published now; the tarball listed only the ones `qvac-core` imports, so
|
|
1763
|
+
`file-glob`, `grep`, `read-files`, `create-documents`, `edit-documents` and
|
|
1764
|
+
`fs/scope` failed with `PACKAGE_PATH_NOT_EXPORTED`. 0.1.1 to 0.2.2 are
|
|
1765
|
+
affected. (#29)
|
|
1011
1766
|
|
|
1012
1767
|
## [0.2.2] - 2026-09-11
|
|
1013
1768
|
|
|
1014
1769
|
### Fixed
|
|
1015
1770
|
|
|
1016
|
-
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1771
|
+
- The engine bundle runs on the patched `@qvac/inference` 0.19.0 the harness
|
|
1772
|
+
pins, and nothing else. A stale lockfile entry had given `@qvac/sdk` its own
|
|
1773
|
+
nested `@qvac/inference` 0.19.1 beside it. A packaging fix with no API
|
|
1774
|
+
change. (#27)
|
|
1019
1775
|
|
|
1020
1776
|
## [0.2.1] - 2026-09-11
|
|
1021
1777
|
|
|
1022
1778
|
### Fixed
|
|
1023
1779
|
|
|
1024
|
-
- The generated wire spec
|
|
1025
|
-
on a download event and as a model
|
|
1026
|
-
version 7, the version 0.1.2 already
|
|
1027
|
-
|
|
1780
|
+
- The generated wire spec matches the schema it is built from again. 0.2.0
|
|
1781
|
+
stamped the fields it added (`downloaded` on a download event and as a model
|
|
1782
|
+
status, and `kvCacheKey`) with schema version 7, the version 0.1.2 already
|
|
1783
|
+
shipped. They are version 8 now. Only the generated spec changed; no API
|
|
1784
|
+
change. (#25)
|
|
1028
1785
|
|
|
1029
1786
|
## [0.2.0] - 2026-09-11
|
|
1030
1787
|
|
|
1031
1788
|
### Added
|
|
1032
1789
|
|
|
1033
|
-
- `deleteKvCache(kvCacheKey)` deletes one chat's KV cache
|
|
1034
|
-
under that key
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
`
|
|
1790
|
+
- `deleteKvCache(kvCacheKey)` deletes one chat's KV cache: every file kept
|
|
1791
|
+
under that key. `clearKvCache()` still deletes them all.
|
|
1792
|
+
|
|
1793
|
+
It is safe on a key still in use: the next turn under it primes from scratch.
|
|
1794
|
+
It rides a new `delete-kv-cache` hrpc route, and resolves with `{ status }`
|
|
1795
|
+
like `clearKvCache`. (#22)
|
|
1038
1796
|
|
|
1039
1797
|
```js
|
|
1040
1798
|
// the cacheKey the chat's completions ran under
|
|
@@ -1042,11 +1800,13 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1042
1800
|
if (result.status !== 'success') log(result.error)
|
|
1043
1801
|
```
|
|
1044
1802
|
|
|
1045
|
-
- A download
|
|
1046
|
-
|
|
1047
|
-
`
|
|
1048
|
-
|
|
1049
|
-
|
|
1803
|
+
- A host can tell a fresh download from a model already on disk. A download's
|
|
1804
|
+
`complete` event carries `downloaded`: `true` when bytes were fetched,
|
|
1805
|
+
`false` when the model was already there.
|
|
1806
|
+
|
|
1807
|
+
An image model's `model` events report `loading` rather than `downloading`
|
|
1808
|
+
when it is cached, and a new `downloaded` status once a fetched one has
|
|
1809
|
+
loaded, so a host that switches over `status` has one more case. (#22)
|
|
1050
1810
|
|
|
1051
1811
|
```js
|
|
1052
1812
|
for await (const event of harness.download({ model })) {
|
|
@@ -1058,28 +1818,63 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1058
1818
|
})
|
|
1059
1819
|
```
|
|
1060
1820
|
|
|
1061
|
-
- `ToolDefinition.prompt
|
|
1062
|
-
|
|
1063
|
-
`exec` and `skill` ship one, so every
|
|
1064
|
-
their schemas. (#22)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1821
|
+
- `ToolDefinition.prompt` lets a tool show the model a shorter schema: an
|
|
1822
|
+
optional compact `description` and `parameters` sent in place of the full
|
|
1823
|
+
ones, which still validate the call. `exec` and `skill` ship one, so every
|
|
1824
|
+
turn spends less of the prompt on their schemas. (#22)
|
|
1825
|
+
|
|
1826
|
+
```js
|
|
1827
|
+
const lookup = {
|
|
1828
|
+
name: 'lookup',
|
|
1829
|
+
description: 'Look a record up by its id, and return ...',
|
|
1830
|
+
parameters: {
|
|
1831
|
+
type: 'object',
|
|
1832
|
+
properties: { id: { type: 'string', description: 'The record id ...' } },
|
|
1833
|
+
required: ['id']
|
|
1834
|
+
},
|
|
1835
|
+
// what the model sees; the full schema above still validates the call
|
|
1836
|
+
prompt: {
|
|
1837
|
+
description: 'Look a record up by id.',
|
|
1838
|
+
parameters: {
|
|
1839
|
+
type: 'object',
|
|
1840
|
+
properties: { id: { type: 'string' } },
|
|
1841
|
+
required: ['id']
|
|
1842
|
+
}
|
|
1843
|
+
},
|
|
1844
|
+
toolset: null,
|
|
1845
|
+
effect: () => 'read',
|
|
1846
|
+
execute: async ({ id }) => find(id)
|
|
1847
|
+
}
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
- `CONTEXT_OVERFLOW_MESSAGE` is exported: the message a turn fails with when it
|
|
1851
|
+
does not fit the model's context window. A host can tell that failure apart
|
|
1852
|
+
and compact the chat. (#22)
|
|
1068
1853
|
|
|
1069
1854
|
```js
|
|
1070
1855
|
import { CONTEXT_OVERFLOW_MESSAGE } from '@qvac/harness'
|
|
1856
|
+
|
|
1857
|
+
if (event.type === 'error' && event.message === CONTEXT_OVERFLOW_MESSAGE) {
|
|
1858
|
+
compact(chat)
|
|
1859
|
+
}
|
|
1071
1860
|
```
|
|
1072
1861
|
|
|
1073
1862
|
- With `QVAC_HARNESS_LOG=1`, each turn logs `sdk tool definitions`: the tool
|
|
1074
1863
|
schemas it sent, largest first, with each one's size and the skill that
|
|
1075
1864
|
granted it. (#22)
|
|
1076
1865
|
|
|
1866
|
+
```sh
|
|
1867
|
+
QVAC_HARNESS_LOG=1 bare host.js
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1077
1870
|
### Changed
|
|
1078
1871
|
|
|
1079
1872
|
- `@qvac/sdk` 0.19.0, pinned exactly. Under Bare the SDK is now served by
|
|
1080
1873
|
`@qvac/inference` 0.19.0, through a new `#sdk` import alias.
|
|
1081
|
-
|
|
1082
|
-
|
|
1874
|
+
|
|
1875
|
+
`@qvac/audiogen-ggml` joins the deferred addons, so
|
|
1876
|
+
`prebuilds-manifest.json` names it and the host provides it too. (#22)
|
|
1877
|
+
|
|
1083
1878
|
- Image and music generation schemas reach the model once their skill's
|
|
1084
1879
|
instructions are loaded, not as soon as the skill is enabled. Loading the
|
|
1085
1880
|
skill unlocks them within the same turn, at the cost of one cache re-prime
|
|
@@ -1094,10 +1889,11 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1094
1889
|
|
|
1095
1890
|
### Fixed
|
|
1096
1891
|
|
|
1097
|
-
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1892
|
+
- `configureSdkStorage()` runs under Bare again.
|
|
1893
|
+
`@qvac/harness/sdk-storage` and `@qvac/harness/sdk-config-loader` failed to
|
|
1894
|
+
resolve there with `MODULE_NOT_FOUND`, because the tarball carried only the
|
|
1895
|
+
Node loader behind `#sdk-config-loader`. Both loaders ship now. 0.1.1 is
|
|
1896
|
+
affected. (#20)
|
|
1101
1897
|
|
|
1102
1898
|
```js
|
|
1103
1899
|
// Bare: loads again, nothing else changes
|
|
@@ -1108,13 +1904,15 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1108
1904
|
|
|
1109
1905
|
### Added
|
|
1110
1906
|
|
|
1111
|
-
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1907
|
+
- `@qvac/harness` and its subpaths now import from npm under plain Node or
|
|
1908
|
+
Bare, with no TypeScript source or type-stripping loader. The library surface
|
|
1909
|
+
ships compiled: `dist-lib/` carries JavaScript and a `.d.ts` for every
|
|
1910
|
+
export.
|
|
1911
|
+
|
|
1912
|
+
Export keys are unchanged, `.ts`-suffixed deep paths included, so a host that
|
|
1913
|
+
took the library from the repository switches to the package without editing
|
|
1914
|
+
a specifier. Of `./lib/tools/*`, only the paths `qvac-core` imports are
|
|
1915
|
+
published. (#18)
|
|
1118
1916
|
|
|
1119
1917
|
```js
|
|
1120
1918
|
import { harnessChild, spawnHarness } from '@qvac/harness/lib/spawn.ts'
|
|
@@ -1125,13 +1923,18 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1125
1923
|
|
|
1126
1924
|
### Added
|
|
1127
1925
|
|
|
1128
|
-
- First release of the QVAC engine as `@qvac/harness`.
|
|
1129
|
-
prebuilt `dist/harness.bundle
|
|
1130
|
-
generated `dist/harness.cjs`
|
|
1131
|
-
process.
|
|
1926
|
+
- First release of the QVAC engine as `@qvac/harness`. A host gets the
|
|
1927
|
+
prebuilt engine, `dist/harness.bundle` (one bundle for every platform), and
|
|
1928
|
+
the generated `dist/harness.cjs` it requires to spawn that bundle as a
|
|
1929
|
+
bare-sidecar process.
|
|
1930
|
+
|
|
1931
|
+
The package spawns; it does not import. Its export map points at library
|
|
1132
1932
|
source the tarball leaves out, so a host takes `spawnHarness`,
|
|
1133
1933
|
`harnessChild` and the transport from the repository until a compiled
|
|
1134
|
-
surface ships.
|
|
1934
|
+
surface ships. See
|
|
1935
|
+
[The npm artifact spawns; it does not import](https://github.com/tetherto/qvac-harness/blob/v0.1.0/README.md#the-npm-artifact-spawns-it-does-not-import)
|
|
1936
|
+
and [Spawning](https://github.com/tetherto/qvac-harness/blob/v0.1.0/README.md#spawning).
|
|
1937
|
+
(#13)
|
|
1135
1938
|
|
|
1136
1939
|
```js
|
|
1137
1940
|
const harness = require('@qvac/harness/dist/harness.cjs')
|
|
@@ -1139,17 +1942,20 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1139
1942
|
const { ipc, sidecar, exit } = await harness.start(args)
|
|
1140
1943
|
```
|
|
1141
1944
|
|
|
1142
|
-
- `dist/prebuilds-manifest.json`
|
|
1143
|
-
bundle defers, keyed by its path
|
|
1144
|
-
|
|
1145
|
-
`@qvac` inference addons and `pyodide`
|
|
1945
|
+
- `dist/prebuilds-manifest.json` tells a host which native prebuilds to supply.
|
|
1946
|
+
It names every prebuild directory the bundle defers, keyed by its path
|
|
1947
|
+
relative to the bundle. The host assembles those beside the bundle from its
|
|
1948
|
+
own `node_modules`, and installs the `@qvac` inference addons and `pyodide`
|
|
1949
|
+
alongside. (#13)
|
|
1146
1950
|
- Skills come from `@qvac/skills` 0.1.2, a content-addressed bundle the
|
|
1147
1951
|
harness materializes at runtime. The Python wheels the tool runtime installs
|
|
1148
1952
|
ship in `wheels/`, each pinned by hash in `wheels/manifest.json`. (#13, #15)
|
|
1149
|
-
-
|
|
1150
|
-
|
|
1953
|
+
- Every dependency installs from the public npm registry: GPU stats are read
|
|
1954
|
+
through `bare-gpu-info`. (#12)
|
|
1151
1955
|
|
|
1152
|
-
[Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.
|
|
1956
|
+
[Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.7...HEAD
|
|
1957
|
+
[0.8.7]: https://github.com/tetherto/qvac-harness/compare/v0.8.6...v0.8.7
|
|
1958
|
+
[0.8.6]: https://github.com/tetherto/qvac-harness/compare/v0.8.5...v0.8.6
|
|
1153
1959
|
[0.8.5]: https://github.com/tetherto/qvac-harness/compare/v0.8.0...v0.8.5
|
|
1154
1960
|
[0.8.0]: https://github.com/tetherto/qvac-harness/compare/v0.7.3...v0.8.0
|
|
1155
1961
|
[0.7.3]: https://github.com/tetherto/qvac-harness/compare/v0.7.2...v0.7.3
|