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