@qvac/harness 0.8.6 → 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 +1259 -507
- package/dist-lib/index.d.ts +11 -0
- package/dist-lib/index.js +102 -25
- package/dist-lib/lib/capabilities/completion.d.ts +4 -4
- package/dist-lib/lib/capabilities/completion.js +86 -57
- 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/connect.js +2 -0
- package/dist-lib/lib/contract-types.d.ts +2 -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/model-pool.d.ts +3 -1
- package/dist-lib/lib/models/model-pool.js +8 -11
- 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 +5 -1
- 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/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/schema/harness.js +2 -1
- package/dist-lib/spec/hrpc/messages.js +10 -5
- package/dist-lib/spec/hyperschema/index.js +10 -5
- package/dist-lib/spec/hyperschema/schema.json +6 -1
- package/dist-lib/spec/hyperschema/types.d.ts +2 -1
- package/package.json +5 -7
- package/worker/harness-sidecar.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,96 +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
|
+
|
|
11
117
|
## [0.8.6] - 2026-10-08
|
|
12
118
|
|
|
13
119
|
### Added
|
|
14
120
|
|
|
15
|
-
- `setModelMemoryCap(percent)`
|
|
16
|
-
device's memory budget, so chat models a
|
|
17
|
-
|
|
18
|
-
again.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
first
|
|
22
|
-
models
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
|
|
26
150
|
- `setDownloadsAllowed({ allowed })`, in process and over hrpc, lets a host hold
|
|
27
|
-
model downloads
|
|
28
|
-
|
|
29
|
-
download
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
33
162
|
runtimes and every pooled load (chat, embedding, speech, transcription).
|
|
34
|
-
|
|
35
|
-
every model event frame now carries an `addon` field, which is
|
|
36
|
-
the event is `held` or the `error` that ends a held wait.
|
|
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
|
+
|
|
37
183
|
- Ternary Bonsai 2 27B joins the curated catalog in two builds,
|
|
38
184
|
`Ternary-Bonsai-2-27B-PQ2_0` (7.2 GB) and `Ternary-Bonsai-2-27B-PTQ1_0`
|
|
39
185
|
(5.9 GB), each with its vision projection. Both sit in the `high` tier, by
|
|
40
|
-
their memory footprint rather than their parameter count.
|
|
41
|
-
|
|
42
|
-
|
|
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)
|
|
43
199
|
|
|
44
200
|
### Changed
|
|
45
201
|
|
|
46
202
|
- `@qvac/sdk` and `@qvac/inference` 0.21.0, with `@qvac/llm-llamacpp` 0.55.0.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
51
207
|
`assessModelFit` input. (#302)
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
GB
|
|
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)
|
|
56
212
|
|
|
57
213
|
### Fixed
|
|
58
214
|
|
|
59
|
-
- A run that preloads a skill (`@skill`, or a skill named in prose)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
deleted when the run ends. (#305)
|
|
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)
|
|
63
219
|
|
|
64
220
|
## [0.8.5] - 2026-10-06
|
|
65
221
|
|
|
66
222
|
### Added
|
|
67
223
|
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
77
251
|
|
|
78
252
|
### Changed
|
|
79
253
|
|
|
80
254
|
- **Breaking:** `@qvac/skills` is now 0.1.12, whose office and pdf skills run on
|
|
81
255
|
the `pptx`, `docx`, `xlsx` and `pdf` tools instead of Python. A host that
|
|
82
256
|
passes its own `skillsDir` needs 0.1.12 as well: older office skills import
|
|
83
|
-
Python libraries this release no longer ships.
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
86
272
|
across a workbook. Extracted workbook booleans read `true`/`false` instead of
|
|
87
273
|
`True`/`False`. `closeOfficeWorker` and `closeDocxWorker` remain as no-ops.
|
|
88
274
|
(#286)
|
|
89
275
|
- `exec` refuses Python that imports `pptx`, `docx` or `openpyxl`, or declares
|
|
90
|
-
their packages, before it runs
|
|
91
|
-
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)
|
|
92
279
|
- **Breaking:** Python run through `exec` asks for approval like any other
|
|
93
|
-
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)
|
|
94
296
|
|
|
95
297
|
### Removed
|
|
96
298
|
|
|
97
299
|
- **Breaking:** the vendored Python wheels for python-pptx, python-docx,
|
|
98
300
|
openpyxl, fpdf2 and pypdf, and their dependencies. Only micropip is still
|
|
99
301
|
seeded; any other package installs from PyPI, which sandbox mode refuses.
|
|
100
|
-
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
|
+
|
|
101
313
|
- The `exec` guidance written for python-pptx, python-docx and fpdf2 code:
|
|
102
314
|
their error hints and source checks, and the shrink-to-fit pass on saved
|
|
103
315
|
decks. (#288, #289)
|
|
@@ -106,42 +318,64 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
106
318
|
|
|
107
319
|
### Added
|
|
108
320
|
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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)
|
|
124
355
|
|
|
125
356
|
### Fixed
|
|
126
357
|
|
|
127
|
-
- Qwen3.5-family models
|
|
128
|
-
in the Qwen3.5 template's own
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
145
379
|
shell expansion is still refused, now with a hint to single-quote the text.
|
|
146
380
|
(#208)
|
|
147
381
|
|
|
@@ -149,223 +383,419 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
149
383
|
|
|
150
384
|
### Added
|
|
151
385
|
|
|
152
|
-
- A tool
|
|
153
|
-
|
|
154
|
-
The `tool-result` event still
|
|
155
|
-
|
|
156
|
-
|
|
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)
|
|
157
410
|
|
|
158
411
|
### Fixed
|
|
159
412
|
|
|
160
|
-
- `weather_lookup`
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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)
|
|
171
429
|
|
|
172
430
|
## [0.7.2] - 2026-10-01
|
|
173
431
|
|
|
174
432
|
### Added
|
|
175
433
|
|
|
176
|
-
-
|
|
177
|
-
besides
|
|
178
|
-
stage it to read past the returned text. The result
|
|
179
|
-
`attachmentId`, `fileName` and `byteLength`.
|
|
180
|
-
|
|
181
|
-
|
|
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)
|
|
182
452
|
|
|
183
453
|
### Changed
|
|
184
454
|
|
|
185
455
|
- Speech sounds cleaner on desktops. Devices on the `mid` profile and up now
|
|
186
456
|
speak with Supertonic 3 FP16 (`Supertonic3-FP16`, 207 MB) instead of Q8_0
|
|
187
457
|
(127 MB), at 16 inference steps instead of 5, which removes the artifacts
|
|
188
|
-
the 5-step output had.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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)
|
|
193
474
|
|
|
194
475
|
## [0.7.1] - 2026-09-30
|
|
195
476
|
|
|
196
477
|
### Added
|
|
197
478
|
|
|
198
|
-
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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).
|
|
207
491
|
|
|
208
492
|
```js
|
|
209
493
|
const loading = (await harness.list({ cachedOnly: true })).filter((row) => row.isLoading)
|
|
210
494
|
```
|
|
211
495
|
|
|
496
|
+
(#143)
|
|
497
|
+
|
|
212
498
|
### Changed
|
|
213
499
|
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
|
|
220
517
|
- `ModelInfo` requires `isLoading`, and `listModels`, `customModelRows` and
|
|
221
518
|
`mergeCustomRows` return `CatalogRow`, the row before the pools are asked. A
|
|
222
|
-
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
|
+
|
|
223
531
|
- A custom model's row reads `isLoaded` only once its load has landed; it used
|
|
224
532
|
to read `true` from the first attempt. (#143)
|
|
225
533
|
|
|
226
534
|
### Fixed
|
|
227
535
|
|
|
228
|
-
- A
|
|
229
|
-
the
|
|
230
|
-
|
|
231
|
-
`error` also refreshes the cached `isCached`, since a load may
|
|
232
|
-
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)
|
|
233
541
|
|
|
234
542
|
## [0.7.0] - 2026-09-29
|
|
235
543
|
|
|
236
544
|
### Added
|
|
237
545
|
|
|
238
|
-
-
|
|
239
|
-
`
|
|
240
|
-
`
|
|
241
|
-
|
|
242
|
-
sent over it. `ModelTaskType` is exported, and grows the four new values a
|
|
243
|
-
preview never returns. (#140)
|
|
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`.
|
|
244
550
|
|
|
245
|
-
|
|
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)
|
|
561
|
+
|
|
562
|
+
- `modelsForAgent(agent, host)` answers which models an agent runs on a device,
|
|
246
563
|
each with its `use`: the chat model, clamped by `runnableModelName`, then the
|
|
247
564
|
image and music models its skills need. A host deciding what to download, or
|
|
248
565
|
which agents a model delete affects, asks this instead of working it out.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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.
|
|
255
574
|
|
|
256
575
|
```js
|
|
257
|
-
|
|
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' }]
|
|
258
585
|
```
|
|
259
586
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
a response from an older harness reads as `false`. (#141)
|
|
587
|
+
```js
|
|
588
|
+
import { runnableModelName, QWEN_35_4B, QWEN_35_9B } from '@qvac/harness/model-catalog'
|
|
263
589
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
590
|
+
const host = { capabilityProfile: 'mid', recommendedModelName: QWEN_35_4B, mobile: true }
|
|
591
|
+
runnableModelName(QWEN_35_9B, host) // QWEN_35_4B
|
|
592
|
+
```
|
|
593
|
+
|
|
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
|
+
```
|
|
616
|
+
|
|
617
|
+
(#141)
|
|
267
618
|
|
|
268
619
|
### Changed
|
|
269
620
|
|
|
270
|
-
- **Breaking:** the package no longer publishes a prebuilt sidecar
|
|
621
|
+
- **Breaking:** the package no longer publishes a prebuilt sidecar.
|
|
271
622
|
`dist/harness.bundle`, `dist/harness.cjs`, `dist/prebuilds-manifest.json` and
|
|
272
623
|
the `./dist/harness.cjs` export are gone. A host stows
|
|
273
624
|
`worker/harness-sidecar.mjs` itself, as desktop and mobile already do, or
|
|
274
|
-
spawns `worker/stow-entry.mjs` straight from its install.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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)
|
|
285
662
|
- `SandboxHook` no longer checks `packages`. The exec tool refuses an install
|
|
286
663
|
the vendored wheels do not hold in full in sandbox mode, whether declared in
|
|
287
664
|
`packages` or on a pip line, with the same message as before. (#127)
|
|
288
665
|
- The vendored wheels are the `#pyodide-wheels` directory asset on Bare;
|
|
289
666
|
`QVAC_PYTHON_WHEELS_DIR` still overrides it. (#127)
|
|
290
667
|
- The python worker and the in-process deadline are spawned through
|
|
291
|
-
`worker_threads`, which the package maps to `bare-worker
|
|
668
|
+
`worker_threads`, which the package maps to `bare-worker`.
|
|
292
669
|
`#pyodide-worker-spawn` is gone. (#127)
|
|
293
|
-
- **Breaking:**
|
|
294
|
-
|
|
295
|
-
`bare-sidecar` to 0.5.7 (Bare 1.34.0) across the tree, `bare-supervisor`'s
|
|
296
|
-
included, with `bare-thread` at least 1.2.7 and `bare-module` 7.1.0 for
|
|
297
|
-
`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)
|
|
298
675
|
- `bare-module` is no longer a dependency or an override, and
|
|
299
|
-
`bare-module-traverse` is overridden to 2.5.8
|
|
676
|
+
`bare-module-traverse` is overridden to `~2.5.8`: earlier versions drop the
|
|
300
677
|
`asset` condition on `bare-sidecar`'s binary, and a stowed harness then fails
|
|
301
678
|
to boot on Bare 1.33 and later. (#127)
|
|
302
679
|
- `skillModelsForAgent` rows carry `use`, `image` or `music`, so a caller can
|
|
303
|
-
tell which skill a model is for without looking the name up in the catalogs.
|
|
304
|
-
|
|
305
|
-
|
|
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)
|
|
306
698
|
|
|
307
699
|
### Removed
|
|
308
700
|
|
|
309
|
-
- **Breaking:** `bareRuntime` is no longer exported from `./lib/tools/exec.ts
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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)
|
|
314
726
|
|
|
315
727
|
### Fixed
|
|
316
728
|
|
|
317
|
-
- Python runs start on Bare 1.34,
|
|
318
|
-
|
|
319
|
-
which the package maps to `bare-worker` on Bare
|
|
320
|
-
module as `createPyodideModule`, and the files pyodide reads
|
|
321
|
-
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)
|
|
322
734
|
|
|
323
735
|
## [0.6.0] - 2026-09-28
|
|
324
736
|
|
|
325
737
|
### Added
|
|
326
738
|
|
|
327
|
-
-
|
|
328
|
-
|
|
329
|
-
under
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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).
|
|
335
753
|
|
|
336
754
|
```js
|
|
755
|
+
import { QWEN_35_4B } from '@qvac/harness'
|
|
756
|
+
|
|
337
757
|
const { loaded } = await harness.loadModel({ model: QWEN_35_4B, agent: { ctxSize: 8192 } })
|
|
338
758
|
const { unloaded } = await harness.unloadModel({ model: QWEN_35_4B })
|
|
339
759
|
```
|
|
340
760
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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).
|
|
349
775
|
|
|
350
776
|
```js
|
|
777
|
+
import { QWEN_35_9B } from '@qvac/harness'
|
|
778
|
+
|
|
351
779
|
const { loaded, reason } = await harness.loadModel({ model: QWEN_35_9B })
|
|
352
780
|
if (reason === 'memory') showNotice('Unload a model first')
|
|
353
781
|
```
|
|
354
782
|
|
|
783
|
+
(#134)
|
|
784
|
+
|
|
355
785
|
### Changed
|
|
356
786
|
|
|
357
|
-
-
|
|
358
|
-
not only
|
|
359
|
-
|
|
360
|
-
-
|
|
361
|
-
|
|
362
|
-
|
|
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)
|
|
363
793
|
|
|
364
794
|
### Fixed
|
|
365
795
|
|
|
366
796
|
- A chat model loads when the host passes no `llmDevice`. 0.5.0 sent `device`,
|
|
367
797
|
`gpu_layers`, `main-gpu` and `openclCacheDir` to the SDK as `undefined`,
|
|
368
|
-
which the SDK spreads over its defaults
|
|
798
|
+
which the SDK spreads over its defaults. With no device the load failed with
|
|
369
799
|
"must specify a device: 'gpu' or 'cpu'", and with a device but no
|
|
370
800
|
`llmGpuLayers` the default of 99 GPU layers was lost. Unset fields are left
|
|
371
801
|
out again. (#137)
|
|
@@ -374,25 +804,39 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
374
804
|
|
|
375
805
|
### Added
|
|
376
806
|
|
|
377
|
-
- `setModelIdleTimeout(ms)` frees memory
|
|
378
|
-
long
|
|
807
|
+
- `setModelIdleTimeout(ms)` frees memory on a device that has sat idle that
|
|
808
|
+
long. With no `completion`, `invoke`, `embed`, `transcribe`, `speak`,
|
|
379
809
|
`speakStream` or `warmModel` call in flight, every idle resident model is
|
|
380
|
-
unloaded, including the image, music and translation runtimes
|
|
381
|
-
loads what it needs again.
|
|
382
|
-
|
|
383
|
-
|
|
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).
|
|
384
821
|
|
|
385
822
|
```js
|
|
386
|
-
await harness.setModelIdleTimeout(30 * 60_000)
|
|
823
|
+
await harness.setModelIdleTimeout(30 * 60_000) // 0 turns it off
|
|
387
824
|
```
|
|
388
825
|
|
|
826
|
+
(#131)
|
|
827
|
+
|
|
389
828
|
- `setCustomModels` entries take an optional `label`, the name the user gave
|
|
390
|
-
the model, and `list()` rows carry it as `label`.
|
|
391
|
-
|
|
392
|
-
|
|
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)
|
|
393
835
|
|
|
394
836
|
- `setCustomModels` entries take an optional `contextLength`, the model's
|
|
395
|
-
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).
|
|
396
840
|
|
|
397
841
|
```js
|
|
398
842
|
await harness.setCustomModels([
|
|
@@ -403,23 +847,39 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
403
847
|
contextLength: preview.contextLength
|
|
404
848
|
}
|
|
405
849
|
])
|
|
850
|
+
|
|
851
|
+
const [row] = (await harness.list({ type: 'llm' })).filter(
|
|
852
|
+
(model) => model.name === 'custom-7f3a'
|
|
853
|
+
)
|
|
854
|
+
row.label // 'Gemma 4 E4B'
|
|
406
855
|
```
|
|
407
856
|
|
|
857
|
+
(#124)
|
|
858
|
+
|
|
408
859
|
### Changed
|
|
409
860
|
|
|
410
861
|
- A custom model loads with a context of the smaller of 51,200 tokens and its
|
|
411
|
-
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
|
|
412
864
|
still wins. (#124)
|
|
413
865
|
|
|
414
866
|
### Fixed
|
|
415
867
|
|
|
416
868
|
- `previewModelSource` takes a direct link to one shard of a split `.gguf` as
|
|
417
|
-
the whole set
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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)
|
|
423
883
|
|
|
424
884
|
## [0.4.0] - 2026-09-24
|
|
425
885
|
|
|
@@ -428,27 +888,48 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
428
888
|
- `previewModelSource({ src, mmprojSrc })` checks a Hugging Face link, a direct
|
|
429
889
|
`.gguf` link or a local path before a host adds it as a custom model. It
|
|
430
890
|
reads the Hugging Face metadata and the file's GGUF header, never the whole
|
|
431
|
-
model.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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).
|
|
437
909
|
|
|
438
910
|
```js
|
|
439
911
|
const preview = await harness.previewModelSource({ src: link })
|
|
440
912
|
|
|
441
913
|
if (!preview.ok) return showError(preview.reason)
|
|
442
|
-
if (!preview.src) return pickOneOf(preview.files) //
|
|
443
|
-
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
|
+
}
|
|
444
918
|
```
|
|
445
919
|
|
|
446
|
-
- `setCustomModels` entries take `taskType` and `expectedSize`, and `list()`
|
|
447
|
-
returns the registered custom models after the catalog rows, with
|
|
448
|
-
`registrySource: 'custom'`, `isLoaded`, and an `isCached` that is false
|
|
449
|
-
until every file is on disk and, when `expectedSize` is set, adds up to it.
|
|
450
920
|
(#121)
|
|
451
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
|
+
|
|
452
933
|
```js
|
|
453
934
|
await harness.setCustomModels([
|
|
454
935
|
{
|
|
@@ -464,56 +945,90 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
464
945
|
const custom = models.filter((model) => model.registrySource === 'custom')
|
|
465
946
|
```
|
|
466
947
|
|
|
948
|
+
(#121)
|
|
949
|
+
|
|
467
950
|
- A `modelsDir` Harness option says where downloads live, so `list()` and
|
|
468
951
|
`deleteModel` can find a custom model's files. The spawned sidecar sets it
|
|
469
|
-
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).
|
|
470
954
|
|
|
471
955
|
```js
|
|
472
956
|
const harness = new Harness({ modelsDir })
|
|
473
957
|
```
|
|
474
958
|
|
|
959
|
+
(#121)
|
|
960
|
+
|
|
475
961
|
### Changed
|
|
476
962
|
|
|
477
|
-
- `deleteModel(name)`
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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).
|
|
481
971
|
|
|
482
972
|
```js
|
|
483
973
|
const { removed, freedBytes } = await harness.deleteModel('nanbeige-3b')
|
|
484
974
|
```
|
|
485
975
|
|
|
976
|
+
(#121)
|
|
977
|
+
|
|
486
978
|
- A custom model name resolves only where an `llm` is loaded, never for
|
|
487
|
-
another task
|
|
488
|
-
|
|
489
|
-
|
|
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)
|
|
490
982
|
|
|
491
983
|
### Fixed
|
|
492
984
|
|
|
493
|
-
- A
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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)
|
|
497
990
|
|
|
498
991
|
## [0.3.3] - 2026-09-24
|
|
499
992
|
|
|
500
993
|
### Changed
|
|
501
994
|
|
|
502
|
-
- `@qvac/sdk` and `@qvac/inference` 0.20.0.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
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
|
+
|
|
510
1023
|
- A user message with several images reaches the SDK as one turn again. The
|
|
511
1024
|
per-image split added for QVAC-24156 is gone, since SDK 0.20 carries the
|
|
512
1025
|
upstream fix (llama.cpp #27348) and equal-size still images no longer merge
|
|
513
|
-
as video frames and abort the encode.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
|
|
517
1032
|
- `@qvac/skills` 0.1.9. The diagrams skill carries every recipe inline, so a 2B
|
|
518
1033
|
model no longer loops on loading a reference file it cannot name. The excel
|
|
519
1034
|
skill routes a chat image to create rather than edit, the Google OAuth routes
|
|
@@ -531,16 +1046,21 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
531
1046
|
|
|
532
1047
|
### Changed
|
|
533
1048
|
|
|
534
|
-
- A networked Python run
|
|
535
|
-
|
|
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
|
|
536
1054
|
`network: no host named in the code`. On an approved run those hosts, and
|
|
537
|
-
their `www.` form, are reached without asking; a subdomain is not.
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
540
1059
|
`askUser` before its name is looked up. A host allowed once stays allowed
|
|
541
1060
|
for that run, and silence past a minute counts as a no. A host that does not
|
|
542
1061
|
wire `askUser` refuses every unnamed host, and a skip-all run asks nothing.
|
|
543
|
-
|
|
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).
|
|
544
1064
|
|
|
545
1065
|
```js
|
|
546
1066
|
for await (const event of harness.completion({
|
|
@@ -555,52 +1075,85 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
555
1075
|
}
|
|
556
1076
|
```
|
|
557
1077
|
|
|
1078
|
+
(#109)
|
|
1079
|
+
|
|
558
1080
|
### Fixed
|
|
559
1081
|
|
|
560
|
-
- A timed-out Python run
|
|
561
|
-
Pyodide
|
|
562
|
-
`QVAC_PYODIDE_WORKER_ENTRY`.
|
|
563
|
-
|
|
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
|
|
564
1088
|
blocking call such as a long `time.sleep` still sees the interrupt only once
|
|
565
|
-
it returns.
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
- A Python run
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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)
|
|
573
1103
|
|
|
574
1104
|
### Security
|
|
575
1105
|
|
|
576
1106
|
- Python in `exec` reached the network whenever the runtime allowed it,
|
|
577
1107
|
including a call that auto-ran as a read with no approval prompt, so a
|
|
578
1108
|
document build that loads an image from a URL could reach the web without
|
|
579
|
-
anyone seeing it.
|
|
580
|
-
|
|
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).
|
|
581
1115
|
(#108)
|
|
582
1116
|
|
|
583
1117
|
## [0.3.1] - 2026-09-23
|
|
584
1118
|
|
|
585
1119
|
### Added
|
|
586
1120
|
|
|
587
|
-
- Python in `exec` reaches the network on Bare
|
|
588
|
-
what is built on them, yt-dlp included, work unchanged, and `requests`
|
|
589
|
-
through urllib3's emscripten backend.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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)
|
|
604
1157
|
|
|
605
1158
|
```js
|
|
606
1159
|
const skills = await harness.skills()
|
|
@@ -615,63 +1168,95 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
615
1168
|
```
|
|
616
1169
|
|
|
617
1170
|
- A skill operation may name `response.decode` to turn a provider's JSON body
|
|
618
|
-
into a compact shape before the model sees it
|
|
619
|
-
`gmail-message`: the ids and labels, the From, To, Subject, Date,
|
|
620
|
-
and References headers, the snippet, and the body as text,
|
|
621
|
-
characters. A `gmail-get` request builder
|
|
622
|
-
|
|
623
|
-
|
|
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
|
+
```
|
|
624
1194
|
|
|
625
1195
|
### Changed
|
|
626
1196
|
|
|
627
|
-
- A compiled skill operation whose HTTP method is `GET`, `HEAD` or `OPTIONS`
|
|
628
|
-
|
|
629
|
-
`http_request`, and takes no `intent` argument. MCP operations and
|
|
630
|
-
method are still writes that ask. (#69)
|
|
631
|
-
- `@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
|
|
632
1202
|
`gmail_get` instead of telling the model to decode base64, which needs this
|
|
633
1203
|
release's decoder, and Asana rejects a non-MCP app at connect time through
|
|
634
1204
|
a `verify` probe. (#105)
|
|
635
1205
|
|
|
636
1206
|
### Fixed
|
|
637
1207
|
|
|
638
|
-
- A malformed `<tool_call>` left its JSON arguments in the reply. The whole
|
|
639
|
-
is now stripped, and an unclosed one is stripped to the end of the
|
|
640
|
-
(#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)
|
|
641
1211
|
- A misspelt, placeholder or repeated `skill` load returned an error the model
|
|
642
1212
|
retried until the turn ran out. A placeholder or the skill's own name now
|
|
643
|
-
reads the body, a near miss of one of its files loads that file, and a
|
|
644
|
-
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)
|
|
645
1215
|
- When the model reached for `pip` or a video tool on the shell path, it was
|
|
646
1216
|
only told what the path is not, and retried the same call. Each refusal now
|
|
647
1217
|
names the call that works, such as `packages: ["requests"]` with the pin a
|
|
648
|
-
vendored package installs offline under.
|
|
649
|
-
|
|
650
|
-
|
|
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)
|
|
651
1225
|
|
|
652
1226
|
## [0.3.0] - 2026-09-23
|
|
653
1227
|
|
|
654
1228
|
### Added
|
|
655
1229
|
|
|
656
|
-
- A `translate` capability for `invoke`:
|
|
657
|
-
LLM. The language pair picks the route before anything loads
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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)
|
|
665
1247
|
|
|
666
1248
|
```js
|
|
667
1249
|
for await (const event of harness.invoke({
|
|
668
1250
|
capability: 'translate',
|
|
669
1251
|
args: { text: 'The weather is nice today.', to: 'de' }
|
|
670
1252
|
})) {
|
|
671
|
-
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
|
+
}
|
|
672
1257
|
if (event.type === 'error') showError(event.message)
|
|
673
1258
|
}
|
|
674
|
-
// result: { text, from, to: 'de', engine: 'nmt', pivoted: false }
|
|
1259
|
+
// result: { text, from: 'en', to: 'de', engine: 'nmt', pivoted: false }
|
|
675
1260
|
```
|
|
676
1261
|
|
|
677
1262
|
- `translationPlanFor`, `NMT_TO_ENGLISH`, `NMT_FROM_ENGLISH`,
|
|
@@ -688,18 +1273,23 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
688
1273
|
if ('error' in plan) showError(plan.error)
|
|
689
1274
|
```
|
|
690
1275
|
|
|
691
|
-
- `enhance: 'never' | 'auto'` on `invoke
|
|
692
|
-
rewritten by a model already resident on the
|
|
693
|
-
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
|
+
|
|
694
1282
|
`metrics` reports the `route` taken (`direct` or `enhanced`), the
|
|
695
|
-
`routeModel` used and the tokens the rewrite spent.
|
|
696
|
-
|
|
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)
|
|
697
1286
|
|
|
698
1287
|
```js
|
|
699
1288
|
for await (const event of harness.invoke({
|
|
700
1289
|
capability: 'image',
|
|
701
1290
|
args: { prompt: 'funny meme' },
|
|
702
|
-
enhance: 'auto'
|
|
1291
|
+
enhance: 'auto',
|
|
1292
|
+
attach
|
|
703
1293
|
})) {
|
|
704
1294
|
if (event.type === 'metrics') log(event.route, event.routeModel)
|
|
705
1295
|
}
|
|
@@ -707,33 +1297,56 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
707
1297
|
|
|
708
1298
|
### Changed
|
|
709
1299
|
|
|
710
|
-
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
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)
|
|
714
1307
|
|
|
715
1308
|
## [0.2.12] - 2026-09-22
|
|
716
1309
|
|
|
1310
|
+
The first published release with the 0.2.11 changes (`invoke`,
|
|
1311
|
+
`detectLanguage`, `SkillOAuthConfig`), plus the fix below.
|
|
1312
|
+
|
|
717
1313
|
### Fixed
|
|
718
1314
|
|
|
719
|
-
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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)
|
|
723
1325
|
|
|
724
1326
|
## [0.2.11] - 2026-09-22
|
|
725
1327
|
|
|
726
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.
|
|
727
1330
|
|
|
728
1331
|
### Added
|
|
729
1332
|
|
|
730
1333
|
- `invoke({ capability, args })` runs one capability on its own, with no
|
|
731
1334
|
model, no dispatch and no approval callback: the caller already decided.
|
|
732
|
-
`image` is the first capability
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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)
|
|
737
1350
|
|
|
738
1351
|
```js
|
|
739
1352
|
for await (const event of harness.invoke({
|
|
@@ -743,14 +1356,20 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
743
1356
|
progress,
|
|
744
1357
|
signal
|
|
745
1358
|
})) {
|
|
746
|
-
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
|
+
}
|
|
747
1364
|
if (event.type === 'error') showError(event.message)
|
|
748
1365
|
}
|
|
749
1366
|
```
|
|
750
1367
|
|
|
751
|
-
- `detectLanguage({ text, topK })
|
|
752
|
-
on `
|
|
753
|
-
`
|
|
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
|
|
754
1373
|
probability. Above that it returns ranked candidates with their
|
|
755
1374
|
probabilities, best first, and may return fewer than asked. Text it cannot
|
|
756
1375
|
place answers `und`. (#75)
|
|
@@ -763,15 +1382,31 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
763
1382
|
// [{ code, language, probability }, ...]
|
|
764
1383
|
```
|
|
765
1384
|
|
|
766
|
-
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
+
```
|
|
771
1406
|
|
|
772
1407
|
### Changed
|
|
773
1408
|
|
|
774
|
-
- `@qvac/skills` 0.1.6
|
|
1409
|
+
- `@qvac/skills` `^0.1.6`: the eight OAuth skills declare their setup routes'
|
|
775
1410
|
OAuth config, the Spotify skill always makes the play call after a search,
|
|
776
1411
|
and the weather skill looks the place up with `weather_lookup`. (#88)
|
|
777
1412
|
|
|
@@ -788,11 +1423,17 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
788
1423
|
|
|
789
1424
|
### Added
|
|
790
1425
|
|
|
791
|
-
- `sandboxMode` agent option: an agent
|
|
792
|
-
|
|
793
|
-
|
|
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
|
+
|
|
794
1431
|
`exec` stays, but refuses package installs and well-known network commands
|
|
795
|
-
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).
|
|
796
1437
|
(#73)
|
|
797
1438
|
|
|
798
1439
|
```js
|
|
@@ -805,12 +1446,17 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
805
1446
|
}
|
|
806
1447
|
```
|
|
807
1448
|
|
|
808
|
-
- `@qvac/harness/office`: XLSX and PPTX text extraction for Knowledge beside
|
|
809
|
-
DOCX, through the same bundled Python worker.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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)
|
|
814
1460
|
|
|
815
1461
|
```js
|
|
816
1462
|
import { extractOfficeText, closeOfficeWorker } from '@qvac/harness/office'
|
|
@@ -822,43 +1468,57 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
822
1468
|
|
|
823
1469
|
### Fixed
|
|
824
1470
|
|
|
825
|
-
- A small model
|
|
826
|
-
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
|
|
827
1473
|
answered from its first result without running again, until a later round
|
|
828
|
-
succeeds. Three such answers end the turn's tool rounds
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
the
|
|
835
|
-
|
|
836
|
-
|
|
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)
|
|
837
1488
|
|
|
838
1489
|
## [0.2.9] - 2026-09-22
|
|
839
1490
|
|
|
840
1491
|
### Added
|
|
841
1492
|
|
|
842
|
-
-
|
|
843
|
-
`
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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)
|
|
851
1508
|
|
|
852
1509
|
```js
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
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)
|
|
856
1514
|
}
|
|
857
1515
|
```
|
|
858
1516
|
|
|
859
|
-
-
|
|
860
|
-
|
|
861
|
-
|
|
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).
|
|
862
1522
|
(#62)
|
|
863
1523
|
|
|
864
1524
|
```js
|
|
@@ -875,15 +1535,21 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
875
1535
|
### Added
|
|
876
1536
|
|
|
877
1537
|
- `weather_lookup`, a tool that looks weather up on wttr.in by `location` and
|
|
878
|
-
leads its report with the place the service actually resolved.
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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)
|
|
885
1550
|
|
|
886
1551
|
```js
|
|
1552
|
+
import { Harness } from '@qvac/harness'
|
|
887
1553
|
import { createWeatherLookupTool } from '@qvac/harness/lib/tools/weather-lookup.ts'
|
|
888
1554
|
|
|
889
1555
|
const harness = new Harness({ tools: [...tools, createWeatherLookupTool()] })
|
|
@@ -898,20 +1564,25 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
898
1564
|
the version. (#55)
|
|
899
1565
|
|
|
900
1566
|
```js
|
|
901
|
-
//
|
|
1567
|
+
// before
|
|
1568
|
+
import { currentReleaseLine } from '@qvac/harness/schema/release-tags'
|
|
902
1569
|
const line = currentReleaseLine(root) // '0.2'
|
|
903
1570
|
|
|
904
|
-
//
|
|
1571
|
+
// after
|
|
1572
|
+
import { currentReleaseVersion } from '@qvac/harness/schema/release-tags'
|
|
905
1573
|
const version = currentReleaseVersion(root) // Version 0.2.8
|
|
906
1574
|
const line = `${version.major}.${version.minor}`
|
|
907
1575
|
```
|
|
908
1576
|
|
|
909
|
-
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
the package version are left out.
|
|
913
|
-
|
|
914
|
-
|
|
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
|
+
|
|
915
1586
|
- `@qvac/skills` 0.1.4: the Apple Notes create-note operation no longer
|
|
916
1587
|
renames a note after creating it, which failed on iCloud accounts and left
|
|
917
1588
|
duplicate notes behind. (#48)
|
|
@@ -920,14 +1591,17 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
920
1591
|
|
|
921
1592
|
### Added
|
|
922
1593
|
|
|
923
|
-
- `@qvac/harness/docx`: `extractDocxText`
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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)
|
|
931
1605
|
|
|
932
1606
|
```js
|
|
933
1607
|
import { extractDocxText, closeDocxWorker } from '@qvac/harness/docx'
|
|
@@ -959,36 +1633,45 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
959
1633
|
})
|
|
960
1634
|
```
|
|
961
1635
|
|
|
962
|
-
- `writeClient` and
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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)
|
|
969
1645
|
|
|
970
1646
|
```js
|
|
971
1647
|
import { writeClient, writeCapabilities } from '@qvac/harness/schema/generate'
|
|
972
1648
|
|
|
973
1649
|
const optionalRequests = new Set(['@assistant/empty', '@assistant/mesh-scope'])
|
|
974
|
-
writeClient(builder, 'spec/calls.js', {
|
|
1650
|
+
writeClient(builder, 'spec/calls.js', {
|
|
1651
|
+
exportName: 'engineClient',
|
|
1652
|
+
optionalRequests
|
|
1653
|
+
})
|
|
975
1654
|
writeCapabilities(builder, 'spec/capabilities.d.ts', { optionalRequests })
|
|
976
1655
|
```
|
|
977
1656
|
|
|
978
1657
|
### Changed
|
|
979
1658
|
|
|
980
|
-
-
|
|
981
|
-
|
|
982
|
-
five minutes, and then loads anyway rather than failing.
|
|
983
|
-
|
|
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)
|
|
984
1665
|
|
|
985
1666
|
## [0.2.5] - 2026-09-14
|
|
986
1667
|
|
|
987
1668
|
### Added
|
|
988
1669
|
|
|
989
|
-
- A
|
|
990
|
-
|
|
991
|
-
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)
|
|
992
1675
|
|
|
993
1676
|
```js
|
|
994
1677
|
import { spawnHarness } from '@qvac/harness/lib/spawn.ts'
|
|
@@ -998,54 +1681,72 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
998
1681
|
sdkStorage,
|
|
999
1682
|
gpuStats: {
|
|
1000
1683
|
memory: { totalBytes: os.totalmem() },
|
|
1001
|
-
gpu: {
|
|
1684
|
+
gpu: {
|
|
1685
|
+
devices: [{ name: 'NVIDIA RTX 4070', videoMemoryBytes: 12884901888 }]
|
|
1686
|
+
}
|
|
1002
1687
|
}
|
|
1003
1688
|
})
|
|
1004
1689
|
```
|
|
1005
1690
|
|
|
1006
1691
|
### Fixed
|
|
1007
1692
|
|
|
1008
|
-
-
|
|
1009
|
-
supervisor respawned it in a loop: the boot-time capability probe
|
|
1010
|
-
second GPU-info instance beside the SDK's own.
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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)
|
|
1017
1705
|
|
|
1018
1706
|
## [0.2.4] - 2026-09-12
|
|
1019
1707
|
|
|
1020
1708
|
### Added
|
|
1021
1709
|
|
|
1022
|
-
- `@qvac/harness/worker/stow-entry.mjs`:
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
throwing: `@qvac/core` 0.1.2
|
|
1028
|
-
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)
|
|
1029
1718
|
|
|
1030
1719
|
```js
|
|
1031
1720
|
import { harnessChild } from '@qvac/harness/lib/spawn.ts'
|
|
1032
1721
|
|
|
1033
1722
|
const entry = new URL(import.meta.resolve('@qvac/harness/worker/stow-entry.mjs')).pathname
|
|
1034
|
-
const spec = harnessChild(entry, {
|
|
1723
|
+
const spec = harnessChild(entry, {
|
|
1724
|
+
runner,
|
|
1725
|
+
skillsDir,
|
|
1726
|
+
wheelsDir,
|
|
1727
|
+
sdkStorage: storage.sdk
|
|
1728
|
+
})
|
|
1035
1729
|
```
|
|
1036
1730
|
|
|
1037
1731
|
## [0.2.3] - 2026-09-12
|
|
1038
1732
|
|
|
1039
1733
|
### Added
|
|
1040
1734
|
|
|
1041
|
-
-
|
|
1042
|
-
|
|
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
|
+
```
|
|
1043
1743
|
|
|
1044
1744
|
### Changed
|
|
1045
1745
|
|
|
1046
|
-
-
|
|
1047
|
-
persistent root instead of the device HOME, which an app
|
|
1048
|
-
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)
|
|
1049
1750
|
|
|
1050
1751
|
```js
|
|
1051
1752
|
import { resolveStorage } from '@qvac/harness/storage'
|
|
@@ -1057,37 +1758,41 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1057
1758
|
|
|
1058
1759
|
### Fixed
|
|
1059
1760
|
|
|
1060
|
-
- Every `./lib/tools/*` subpath is
|
|
1061
|
-
|
|
1062
|
-
`read-files`, `create-documents`, `edit-documents
|
|
1063
|
-
`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)
|
|
1064
1766
|
|
|
1065
1767
|
## [0.2.2] - 2026-09-11
|
|
1066
1768
|
|
|
1067
1769
|
### Fixed
|
|
1068
1770
|
|
|
1069
|
-
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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)
|
|
1072
1775
|
|
|
1073
1776
|
## [0.2.1] - 2026-09-11
|
|
1074
1777
|
|
|
1075
1778
|
### Fixed
|
|
1076
1779
|
|
|
1077
|
-
- The generated wire spec
|
|
1078
|
-
on a download event and as a model
|
|
1079
|
-
version 7, the version 0.1.2 already
|
|
1080
|
-
|
|
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)
|
|
1081
1785
|
|
|
1082
1786
|
## [0.2.0] - 2026-09-11
|
|
1083
1787
|
|
|
1084
1788
|
### Added
|
|
1085
1789
|
|
|
1086
|
-
- `deleteKvCache(kvCacheKey)` deletes one chat's KV cache
|
|
1087
|
-
under that key
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
`
|
|
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)
|
|
1091
1796
|
|
|
1092
1797
|
```js
|
|
1093
1798
|
// the cacheKey the chat's completions ran under
|
|
@@ -1095,11 +1800,13 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1095
1800
|
if (result.status !== 'success') log(result.error)
|
|
1096
1801
|
```
|
|
1097
1802
|
|
|
1098
|
-
- A download
|
|
1099
|
-
|
|
1100
|
-
`
|
|
1101
|
-
|
|
1102
|
-
|
|
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)
|
|
1103
1810
|
|
|
1104
1811
|
```js
|
|
1105
1812
|
for await (const event of harness.download({ model })) {
|
|
@@ -1111,28 +1818,63 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1111
1818
|
})
|
|
1112
1819
|
```
|
|
1113
1820
|
|
|
1114
|
-
- `ToolDefinition.prompt
|
|
1115
|
-
|
|
1116
|
-
`exec` and `skill` ship one, so every
|
|
1117
|
-
their schemas. (#22)
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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)
|
|
1121
1853
|
|
|
1122
1854
|
```js
|
|
1123
1855
|
import { CONTEXT_OVERFLOW_MESSAGE } from '@qvac/harness'
|
|
1856
|
+
|
|
1857
|
+
if (event.type === 'error' && event.message === CONTEXT_OVERFLOW_MESSAGE) {
|
|
1858
|
+
compact(chat)
|
|
1859
|
+
}
|
|
1124
1860
|
```
|
|
1125
1861
|
|
|
1126
1862
|
- With `QVAC_HARNESS_LOG=1`, each turn logs `sdk tool definitions`: the tool
|
|
1127
1863
|
schemas it sent, largest first, with each one's size and the skill that
|
|
1128
1864
|
granted it. (#22)
|
|
1129
1865
|
|
|
1866
|
+
```sh
|
|
1867
|
+
QVAC_HARNESS_LOG=1 bare host.js
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1130
1870
|
### Changed
|
|
1131
1871
|
|
|
1132
1872
|
- `@qvac/sdk` 0.19.0, pinned exactly. Under Bare the SDK is now served by
|
|
1133
1873
|
`@qvac/inference` 0.19.0, through a new `#sdk` import alias.
|
|
1134
|
-
|
|
1135
|
-
|
|
1874
|
+
|
|
1875
|
+
`@qvac/audiogen-ggml` joins the deferred addons, so
|
|
1876
|
+
`prebuilds-manifest.json` names it and the host provides it too. (#22)
|
|
1877
|
+
|
|
1136
1878
|
- Image and music generation schemas reach the model once their skill's
|
|
1137
1879
|
instructions are loaded, not as soon as the skill is enabled. Loading the
|
|
1138
1880
|
skill unlocks them within the same turn, at the cost of one cache re-prime
|
|
@@ -1147,10 +1889,11 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1147
1889
|
|
|
1148
1890
|
### Fixed
|
|
1149
1891
|
|
|
1150
|
-
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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)
|
|
1154
1897
|
|
|
1155
1898
|
```js
|
|
1156
1899
|
// Bare: loads again, nothing else changes
|
|
@@ -1161,13 +1904,15 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1161
1904
|
|
|
1162
1905
|
### Added
|
|
1163
1906
|
|
|
1164
|
-
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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)
|
|
1171
1916
|
|
|
1172
1917
|
```js
|
|
1173
1918
|
import { harnessChild, spawnHarness } from '@qvac/harness/lib/spawn.ts'
|
|
@@ -1178,13 +1923,18 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1178
1923
|
|
|
1179
1924
|
### Added
|
|
1180
1925
|
|
|
1181
|
-
- First release of the QVAC engine as `@qvac/harness`.
|
|
1182
|
-
prebuilt `dist/harness.bundle
|
|
1183
|
-
generated `dist/harness.cjs`
|
|
1184
|
-
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
|
|
1185
1932
|
source the tarball leaves out, so a host takes `spawnHarness`,
|
|
1186
1933
|
`harnessChild` and the transport from the repository until a compiled
|
|
1187
|
-
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)
|
|
1188
1938
|
|
|
1189
1939
|
```js
|
|
1190
1940
|
const harness = require('@qvac/harness/dist/harness.cjs')
|
|
@@ -1192,17 +1942,19 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1192
1942
|
const { ipc, sidecar, exit } = await harness.start(args)
|
|
1193
1943
|
```
|
|
1194
1944
|
|
|
1195
|
-
- `dist/prebuilds-manifest.json`
|
|
1196
|
-
bundle defers, keyed by its path
|
|
1197
|
-
|
|
1198
|
-
`@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)
|
|
1199
1950
|
- Skills come from `@qvac/skills` 0.1.2, a content-addressed bundle the
|
|
1200
1951
|
harness materializes at runtime. The Python wheels the tool runtime installs
|
|
1201
1952
|
ship in `wheels/`, each pinned by hash in `wheels/manifest.json`. (#13, #15)
|
|
1202
|
-
-
|
|
1203
|
-
|
|
1953
|
+
- Every dependency installs from the public npm registry: GPU stats are read
|
|
1954
|
+
through `bare-gpu-info`. (#12)
|
|
1204
1955
|
|
|
1205
|
-
[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
|
|
1206
1958
|
[0.8.6]: https://github.com/tetherto/qvac-harness/compare/v0.8.5...v0.8.6
|
|
1207
1959
|
[0.8.5]: https://github.com/tetherto/qvac-harness/compare/v0.8.0...v0.8.5
|
|
1208
1960
|
[0.8.0]: https://github.com/tetherto/qvac-harness/compare/v0.7.3...v0.8.0
|