@vllm-sr/dsh 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # @vllm-sr/dsh
2
2
 
3
- **System Duo** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): *Thinking, fast and slow. One
4
- key.*
3
+ **System Duo** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): *Thinking, fast and slow.*
5
4
 
6
5
  An agent's LLM thinks slowly, in tokens. System One, a decision model, answers the many small questions around it in
7
6
  about half a second each, with calibrated probabilities: is this command dangerous, does this file try to give the
@@ -12,22 +11,25 @@ agent orders, is the work really done, which team should get each of these 230 t
12
11
  - **System One**, the decision model, decides: `vllm-sr/d3-ultra`. It answers typed questions (choice, yes/no, score,
13
12
  box) with probabilities, from one forward pass per question instead of generated text.
14
13
 
15
- Both come from [System Duo](https://app.vllm-sr.ai) with one free key, and the plugin gets that key for you: System One
16
- answers at `https://app.vllm-sr.ai/v1/systemone`, and `vllm-sr/auto` at `https://app.vllm-sr.ai/v1/chat/completions`.
14
+ Both take a free [System Duo](https://app.vllm-sr.ai) key, which the plugin gets for you: System One answers at `https://app.vllm-sr.ai/v1/systemone`,
15
+ and `vllm-sr/auto` at `https://app.vllm-sr.ai/v1/chat/completions`.
17
16
 
18
17
  ## Quick start
19
18
 
20
19
  ```bash
21
- npm install -g @deepseek-ai/dsh # DeepSeek Harness 0.2.1-rc.5 or a later 0.2 release, Node 24.9 or later
20
+ npm install -g @deepseek-ai/dsh pnpm
22
21
  dsh plugin --profile web add @vllm-sr/dsh
23
22
  dsh web
24
23
  ```
25
24
 
26
- The package can also come straight from GitHub: `dsh plugin --profile web add github:Xunzhuo/dsh-system-one-plugin`.
25
+ The first line installs DeepSeek Harness (0.2.1-rc.5 or a later 0.2 release, on Node 24.9 or later) and pnpm, which
26
+ `dsh plugin` uses to install plugins; without pnpm, `dsh plugin add` stops with "pnpm was not found". On a machine
27
+ with git, the package can also come straight from GitHub: `dsh plugin --profile web add github:Xunzhuo/dsh-system-one-plugin`.
27
28
 
28
- On its first start the plugin gets one free System Duo key, with the same small proof of work the site's "Get your
29
- API key" button runs in the browser, and saves it in DeepSeek Harness's credential store and in `~/.dsh/.env` (readable
30
- by you only). A notice in the session says so. `vllm-sr/auto` becomes the default model; choose another model
29
+ On its first start the plugin gets a free System Duo key, with the same small proof of work the site's "Get your API
30
+ key" button runs in the browser, and saves it in DeepSeek Harness's credential store and in `~/.dsh/.env` (readable
31
+ by you only). A line under your first request says so: "Free API key created for app.vllm-sr.ai, saved in
32
+ ~/.dsh/.env", or why the site refused one. `vllm-sr/auto` becomes the default model; choose another model
31
33
  in the app and System One keeps working beside it.
32
34
 
33
35
  Headless:
@@ -69,15 +71,55 @@ badge that carries the decision's latency:
69
71
  - **Done-check**: a card after the answer: done, waiting on you, or sent back to finish.
70
72
  - **`system_one` tool**: its row shows the batch: items, questions, each question's answer distribution and the
71
73
  results file.
74
+ - **Free key**: a **System Duo** line under your first request when the plugin created a key, or could not.
72
75
 
73
76
  Routine checks (an allowed call, a clean result) are one line and fold with the turn's work. Cards stay visible when
74
77
  the work folds, so a finished turn still shows what System One decided. Parallel calls in one step share one line or
75
78
  card, and every decision pops in with a short animation the first time it appears (none with reduced motion). Each
76
- element shows the Decision API's own answer: the probabilities, the model and the measured latency.
79
+ element shows the Decision API's own answer: the probabilities, the model (in bold, as `vllm-sr/d3-ultra`) and the
80
+ measured latency.
77
81
 
78
- Every element carries `data-system-one` (`card`, `compact` or `tool`), `data-system-one-reflex` (`triage`, `guard`,
79
- `shield`, `done` or `tool`) and `data-system-one-verdict`, for tests and screen recordings. Set `systemOneUi` to
80
- `compact` for one line per decision, or to `off` to hide them.
82
+ Set `systemOneUi` to `compact` for one line per decision, or to `off` to hide them.
83
+
84
+ ### Selectors
85
+
86
+ For tests and screen recordings, each System One element in Chat is one root marked `data-system-one`, with stable
87
+ attributes that say what it shows:
88
+
89
+ | Attribute | Values |
90
+ | --- | --- |
91
+ | `data-system-one` | `card`, `compact` (one line), `tool` (the `system_one` row) or `notice` (the free-key line) |
92
+ | `data-system-one-reflex` | `triage`, `guard`, `shield`, `done`, `tool` or `key` |
93
+ | `data-system-one-verdict` | triage `brief`; guard `allow`, `ask` or `deny`; shield `clean`, `removed` or `flagged`; done-check `stop` or `continue`; tool `running` or `done`; key `created` or `failed`; `error` when System One did not answer |
94
+ | `data-system-one-outcome` | guard, once you answer its approval: `allowed`, `rejected`, `cancelled` or `failed` |
95
+ | `data-system-one-why` | done-check: `complete`, `waiting`, `unfinished` or `unchecked` |
96
+ | `data-system-one-tone` | its colour: `ok` (green), `info` (ember), `warn` (amber), `bad` (red) or `muted` |
97
+ | `data-system-one-count` | how many decisions it holds (a batch: how many items) |
98
+ | `data-system-one-turn`, `data-system-one-step` | where it acted (the step for guard and shield) |
99
+ | `data-system-one-model` | the model that answered, as `vllm-sr/d3-ultra` |
100
+
101
+ Inside a root, `data-system-one-part` marks `badge` (the ember **System One** pill), `latency`, `reflex`, `summary`,
102
+ `model`, `head`, `toggle` (the button of a one-line element), `rows`, `verdict`, `outcome`, `subject`, `facts`,
103
+ `note` and `more`, in the batch row also `stack`, `legend`, `failed`, `results` and `raw`, and in the free-key line
104
+ `badge` (the **System Duo** pill), `summary` and `links`. Each number is a
105
+ `data-system-one-fact`: `kind`, `effort`, `unclear` and `risky` (triage), `risk`, `leaks` and `on-task` (guard),
106
+ `passages` and `injection` (shield), `complete`, `checked` and `waiting` (done-check). Decision rows carry
107
+ `data-system-one-decision` and `data-system-one-call`, batch questions `data-system-one-question`. For example:
108
+
109
+ | Element | Selector |
110
+ | --- | --- |
111
+ | Triage card | `[data-system-one-reflex="triage"]` |
112
+ | Guard card that asked you (amber) | `[data-system-one-reflex="guard"][data-system-one-verdict="ask"]` |
113
+ | The same card once you rejected the call (red) | `[data-system-one-reflex="guard"][data-system-one-outcome="rejected"]` |
114
+ | Injection removed (red) | `[data-system-one-reflex="shield"][data-system-one-verdict="removed"]` |
115
+ | A routine check (one line) | `[data-system-one="compact"][data-system-one-reflex="guard"]`, and `"shield"` |
116
+ | Done-check | `[data-system-one-reflex="done"]` |
117
+ | Batch row, finished | `[data-system-one="tool"][data-system-one-verdict="done"]` |
118
+ | The ember badge, and its latency | `[data-system-one-part="badge"]`, `[data-system-one-part="latency"]` |
119
+ | "Dangerous", and "You rejected it" | `[data-system-one-fact="risk"]`, `[data-system-one-part="outcome"]` |
120
+ | The reason | `[data-system-one-part="note"]` |
121
+ | The model's name (bold) | `[data-system-one-part="model"]` |
122
+ | Free key created | `[data-system-one="notice"][data-system-one-verdict="created"]` |
81
123
 
82
124
  ## Your data
83
125
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vllm-sr/dsh",
3
- "version": "0.4.0",
4
- "description": "System Duo for DeepSeek Harness. Thinking, fast and slow. One key: System One decision models guard, shield, triage and check your agent around System Two (vllm-sr/auto), with one free app.vllm-sr.ai key.",
3
+ "version": "0.4.2",
4
+ "description": "System Duo for DeepSeek Harness. Thinking, fast and slow: System One decision models guard, shield, triage and check your agent around System Two (vllm-sr/auto), with a free app.vllm-sr.ai key.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
package/src/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * - done: agent/turn-stopping is the work really finished and checked?
10
10
  * - tool: system_one one rubric over many items, for the LLM to call
11
11
  *
12
- * On first use it gets one free System Duo key (app.vllm-sr.ai) for vllm-sr/auto and the decision models (keys.js).
12
+ * On first use it gets a free System Duo key (app.vllm-sr.ai) for vllm-sr/auto and the decision models (keys.js).
13
13
  * Each decision is also recorded in the session (records.js), and the browser half (web/client.js) draws it in Chat.
14
14
  */
15
15
  import { homedir } from 'node:os'
@@ -19,9 +19,9 @@ import z from '@deepseek-ai/schemastery'
19
19
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
20
20
  import * as dshSession from '@deepseek-ai/dsh-session'
21
21
  import { createDecisionClient, compactAnswer, yes } from './client.js'
22
- import { SERVICES, appendDotenv, ensureKeys, readDotenv } from './keys.js'
22
+ import { SERVICES, appendDotenv, describeKeys, ensureKeys, readDotenv } from './keys.js'
23
23
  import { DONE, EFFORT_LEVELS, GUARD, RISK_LEVELS, TRIAGE } from './questions.js'
24
- import { UI_MODES, approvalOutcome, createRecorder, subject } from './records.js'
24
+ import { UI_MODES, approvalOutcome, createRecorder, relativeTo, subject } from './records.js'
25
25
  import { screen } from './shield.js'
26
26
  import { systemOneTool } from './tool.js'
27
27
  import { clip, createTrace } from './trace.js'
@@ -125,7 +125,9 @@ function briefCall(exec) {
125
125
  export function apply(ctx, config) {
126
126
  let credentials
127
127
  let keysReady
128
+ let keysSettled = false
128
129
  let keysNotice
130
+ let keysRecord
129
131
  const keysAbort = new AbortController()
130
132
  // DeepSeek Harness's user dotenv: the credential store falls back to it, and a headless run reads it at launch
131
133
  const envFile = join(process.env.DSH_HOME || join(homedir(), '.dsh'), '.env')
@@ -161,7 +163,7 @@ export function apply(ctx, config) {
161
163
  if (!id) return undefined
162
164
  let state = sessions.get(id)
163
165
  if (!state) {
164
- state = { id, user: '', calls: [], assistant: '', steers: 0, triage: undefined, turn: 0, step: 0, asked: new Map(), pendingTriage: undefined }
166
+ state = { id, user: '', calls: [], assistant: '', steers: 0, triage: undefined, turn: 0, step: 0, asked: new Map(), pendingTriage: undefined, pendingKey: undefined }
165
167
  sessions.set(id, state)
166
168
  }
167
169
  return state
@@ -192,24 +194,15 @@ export function apply(ctx, config) {
192
194
  return ensureKeys({ resolve, store: keep, services, label: config.keyLabel, signal: keysAbort.signal })
193
195
  })().then(({ allocated, failed }) => {
194
196
  trace({ ev: 'keys', store: Boolean(store), allocated: allocated.map(s => s.site), failed: failed.map(f => ({ site: f.service.site, code: f.error.code, message: clip(String(f.error.message), 200) })) })
195
- const lines = []
196
- if (allocated.length) {
197
- const saved = store ? `DeepSeek Harness's credential store and ${where}` : `${where}`
198
- const later = !store ? ' A headless run reads keys when it starts, so vllm-sr/auto answers from the next run.' : ''
199
- const s = allocated[0]
200
- lines.push(`[System Duo] One free key for vllm-sr/auto and the decision models (${new URL(s.site).host}) is saved in ${saved}. Using it accepts the terms (${s.terms}); requests are logged to improve the services and to train models (${s.privacy}).${later}`)
201
- }
202
- for (const { service, error } of failed) {
203
- if (keysAbort.signal.aborted) continue
204
- // a DOMException's code is a legacy number (20 for an abort, 23 for a timeout); its name says what happened
205
- const why = typeof error.code === 'number' ? error.name : error.code ?? error.status ?? 'error'
206
- lines.push(`[System Duo] No free key from ${new URL(service.site).host} (${why}: ${clip(String(error.message), 160)}). Get one at ${service.site} and save it as ${service.ref}.`)
207
- }
208
- if (lines.length === 0) return
209
- if (!store) process.stderr.write(`${lines.join('\n')}\n`)
210
- keysNotice = notice(lines.join('\n'), allocated.length ? 'System Duo got a free key for vllm-sr/auto and the decision models' : 'System Duo could not get a free key')
197
+ const said = describeKeys({ allocated, failed: keysAbort.signal.aborted ? [] : failed }, { file: where, store: Boolean(store) })
198
+ if (!said) return
199
+ if (!store) process.stderr.write(`${said.text}\n`)
200
+ keysNotice = notice(said.text, said.summary)
201
+ keysRecord = said.record
211
202
  }, error => {
212
203
  trace({ ev: 'keys', error: clip(String(error?.message ?? error), 300) })
204
+ }).finally(() => {
205
+ keysSettled = true
213
206
  })
214
207
  }
215
208
 
@@ -222,10 +215,13 @@ export function apply(ctx, config) {
222
215
  startKeys(scoped.credentials)
223
216
  })
224
217
 
225
- function withNotices(decision, messages) {
218
+ // Chat hides notices, so the keys' one also goes to the session as a record that Chat draws under the request.
219
+ function withNotices(decision, messages, state, agent) {
226
220
  if (decision.kind !== 'enter') return decision
227
221
  const extra = [keysNotice, ...messages].filter(Boolean)
222
+ if (keysNotice && keysRecord && state && agent?.session) state.pendingKey = { session: agent.session, data: { ...keysRecord, turn: state.turn, step: state.step } }
228
223
  keysNotice = undefined
224
+ keysRecord = undefined
229
225
  return extra.length === 0 ? decision : { ...decision, messages: [...decision.messages, ...extra] }
230
226
  }
231
227
 
@@ -254,10 +250,11 @@ export function apply(ctx, config) {
254
250
  const AFTER_REQUEST = new Set(['request/header', 'request/context', 'assistant/message', 'step/end', 'turn/end'])
255
251
  ctx.on('session/event', (session, event) => {
256
252
  const state = sessions.get(session?.id)
257
- const pending = state?.pendingTriage
258
- if (pending && ((event?.type === 'user/message' && event.data?.source?.kind === 'user') || AFTER_REQUEST.has(event?.type))) {
253
+ const pending = [state?.pendingKey, state?.pendingTriage].filter(Boolean)
254
+ if (pending.length && ((event?.type === 'user/message' && event.data?.source?.kind === 'user') || AFTER_REQUEST.has(event?.type))) {
255
+ state.pendingKey = undefined
259
256
  state.pendingTriage = undefined
260
- queueMicrotask(() => record(pending.session, pending.data))
257
+ queueMicrotask(() => pending.forEach(entry => record(entry.session, entry.data)))
261
258
  }
262
259
  if (event?.type !== 'assistant/message') return
263
260
  const text = textOf(event.data?.message?.content)
@@ -289,7 +286,7 @@ export function apply(ctx, config) {
289
286
  // Triage: one look at each new user request before the model sees it.
290
287
  ctx.on('agent/pre-step', async (payload, next) => {
291
288
  if (!keysReady && !credentials) startKeys(undefined)
292
- if (keysReady) await Promise.race([keysReady, sleep(KEYS_WAIT_MS)])
289
+ if (keysReady && !keysSettled) await Promise.race([keysReady, sleep(KEYS_WAIT_MS, undefined, { ref: false })])
293
290
  const { agent, messages, signal, turn, step } = payload
294
291
  const userMessages = messages.filter(message => message.source?.kind === 'user')
295
292
  const state = sessionState(agent)
@@ -302,7 +299,7 @@ export function apply(ctx, config) {
302
299
  state.calls = []
303
300
  state.steers = 0
304
301
  trace({ ev: 'user', session: state.id, turn, text: clip(state.user, 4000) })
305
- if (!config.triage || !state.user) return withNotices(await next(), [])
302
+ if (!config.triage || !state.user) return withNotices(await next(), [], state, agent)
306
303
  let brief
307
304
  try {
308
305
  const response = await client.ask({ model: config.model, questions: TRIAGE, inputs: [{ state: clip(state.user, 12000) }], signal, timeoutMs: config.hookTimeoutMs, retries: config.hookRetries })
@@ -326,7 +323,7 @@ export function apply(ctx, config) {
326
323
  trace({ ev: 's1', reflex: 'triage', session: state.id, turn, error: String(error?.message ?? error) })
327
324
  if (!signal?.aborted) state.pendingTriage = { session: agent.session, data: { reflex: 'triage', turn, step, error: subject(error?.message ?? error) } }
328
325
  }
329
- return withNotices(await next(), [brief])
326
+ return withNotices(await next(), [brief], state, agent)
330
327
  })
331
328
 
332
329
  // Guard: every side-effecting call is reviewed before it runs.
@@ -335,12 +332,13 @@ export function apply(ctx, config) {
335
332
  ctx.on('tools/pre-execute', async (exec, next) => {
336
333
  if (!gated.has(exec.name) || !exec.agent) return next()
337
334
  const state = sessionState(exec.agent)
335
+ const workspace = await workspaceOf(exec)
338
336
  const situation = [
339
337
  `The user asked the agent:\n"""${clip(state?.user || '(unknown)', 1500)}"""`,
340
- `Workspace: ${await workspaceOf(exec)}`,
338
+ `Workspace: ${workspace}`,
341
339
  `The agent now wants to run:\n${describeCall(exec)}`,
342
340
  ].join('\n\n')
343
- const base = { reflex: 'guard', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(briefCall(exec)) }
341
+ const base = { reflex: 'guard', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(relativeTo(briefCall(exec), workspace, homedir())) }
344
342
  let response
345
343
  try {
346
344
  response = await client.ask({ model: config.model, questions: GUARD, inputs: [{ state: situation }], signal: exec.signal, timeoutMs: config.hookTimeoutMs, retries: config.hookRetries })
@@ -414,7 +412,7 @@ export function apply(ctx, config) {
414
412
  const tail = text.slice(screened.length)
415
413
  const where = exec.name === 'web_fetch' ? `a web page (${exec.arguments?.url})` : exec.name === 'read' ? `the file ${exec.arguments?.file_path}` : `the output of: ${clip(exec.arguments?.command, 200)}`
416
414
  const prefix = `The user asked the agent: "${clip(state?.user || '(unknown)', 300)}"\nThe agent just read this passage from ${where}:`
417
- const base = { reflex: 'shield', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(briefCall(exec)) }
415
+ const base = { reflex: 'shield', turn: state?.turn, step: state?.step, callId: exec.callId, tool: exec.name, call: subject(relativeTo(briefCall(exec), await workspaceOf(exec), homedir())) }
418
416
  const started = performance.now()
419
417
  let verdict
420
418
  try {
package/src/keys.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The free System Duo key (app.vllm-sr.ai): one key for both vllm-sr/auto, the LLM (System Two), and the
3
- * decision models (System One). The site hands it out for a small proof of work, the same one its "Get your API
4
- * key" button runs in the browser; the key is kept in DeepSeek Harness's credential store.
2
+ * The free System Duo key (app.vllm-sr.ai), which both vllm-sr/auto, the LLM (System Two), and the decision models
3
+ * (System One) take. The site hands it out for a small proof of work, the same one its "Get your API key" button runs
4
+ * in the browser; the key is kept in DeepSeek Harness's credential store.
5
5
  */
6
6
  import { createHash, hash as oneShot } from 'node:crypto'
7
7
  import { closeSync, fchmodSync, mkdirSync, openSync, readFileSync, writeSync } from 'node:fs'
@@ -172,3 +172,40 @@ export async function ensureKeys({ resolve, store, services = SERVICES, label, f
172
172
  }))
173
173
  return { allocated, failed }
174
174
  }
175
+
176
+ /**
177
+ * What the session says about the keys a start fetched: the notice the model reads (and Trajectory shows), and the
178
+ * record Chat draws as an inline line.
179
+ * @param {object} outcome - ensureKeys' result
180
+ * @param {object} where
181
+ * @param {string} where.file - the dotenv file the key went to, as the user writes it (~/.dsh/.env)
182
+ * @param {boolean} where.store - whether DeepSeek Harness's credential store holds it too
183
+ * @returns {{text: string, summary: string, record: object} | undefined} nothing when no key was fetched or refused
184
+ */
185
+ export function describeKeys({ allocated, failed }, { file, store }) {
186
+ const host = site => new URL(site).host
187
+ const lines = []
188
+ if (allocated.length) {
189
+ const s = allocated[0]
190
+ const saved = store ? `DeepSeek Harness's credential store and ${file}` : file
191
+ const later = store ? '' : ' A headless run reads keys when it starts, so vllm-sr/auto answers from the next run.'
192
+ lines.push(`[System Duo] Free API key created for ${host(s.site)}, saved in ${saved}. Using it accepts the terms (${s.terms}); requests are logged to improve the services and to train models (${s.privacy}).${later}`)
193
+ }
194
+ const refused = failed.map(({ service, error }) => {
195
+ // a DOMException's code is a legacy number (20 for an abort, 23 for a timeout); its name says what happened
196
+ const why = typeof error.code === 'number' ? error.name : error.code ?? error.status ?? 'error'
197
+ const message = String(error.message).slice(0, 160)
198
+ lines.push(`[System Duo] No free API key from ${host(service.site)} (${why}: ${message}). Get one at ${service.site} and save it as ${service.ref}.`)
199
+ return { service, why, message }
200
+ })
201
+ if (lines.length === 0) return undefined
202
+ const s = allocated[0]
203
+ const record = s
204
+ ? { reflex: 'key', verdict: 'created', site: host(s.site), saved: file, store, terms: s.terms, privacy: s.privacy }
205
+ : { reflex: 'key', verdict: 'failed', site: host(refused[0].service.site), url: refused[0].service.site, ref: refused[0].service.ref, error: { code: String(refused[0].why), message: refused[0].message } }
206
+ return {
207
+ text: lines.join('\n'),
208
+ summary: s ? `Free API key created for ${host(s.site)}` : `No free API key from ${host(refused[0].service.site)}`,
209
+ record,
210
+ }
211
+ }
package/src/records.js CHANGED
@@ -20,6 +20,28 @@ export function subject(text, max = 200) {
20
20
  return redact(clip(text, max))
21
21
  }
22
22
 
23
+ const escaped = text => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
24
+ // a path starts after a separator and ends at one, so /work does not match inside /work2 or /x/work
25
+ const START = String.raw`(?<=^|[\s'"=:(])`
26
+ const END = String.raw`(?=$|[\s'"/;&|)<>,])`
27
+
28
+ /**
29
+ * A call as the cards show it: paths in the session's working directory relative to it, the home directory as `~`,
30
+ * and no leading `cd` into the working directory itself.
31
+ */
32
+ export function relativeTo(text, workspace, home) {
33
+ let out = String(text ?? '')
34
+ const root = dir => (typeof dir === 'string' && dir.replace(/\/+$/, '').length > 1 ? escaped(dir.replace(/\/+$/, '')) : undefined)
35
+ const work = root(workspace)
36
+ if (work) {
37
+ out = out.replace(new RegExp(`${START}${work}/`, 'g'), '').replace(new RegExp(`${START}${work}${END}`, 'g'), '.')
38
+ out = out.replace(/^cd \.\s*&&\s*/, '')
39
+ }
40
+ const own = root(home)
41
+ if (own) out = out.replace(new RegExp(`${START}${own}${END}`, 'g'), '~')
42
+ return out
43
+ }
44
+
23
45
  /**
24
46
  * @param {object} options
25
47
  * @param {((session: object, type: string, data: object) => unknown) | undefined} options.append - DeepSeek
package/src/tool.js CHANGED
@@ -12,20 +12,38 @@ You write the rubric as questions; System One answers every question for every i
12
12
 
13
13
  Question types:
14
14
  - "choice": pick one option. criteria: an object of option name -> short description (2 or more options).
15
- - "noul": yes or no. The answer is the probability of yes.
15
+ - "noul": yes or no, with no criteria: say in the instructions what counts as yes. The answer is the probability of yes.
16
16
  - "score": an ordered scale. criteria: a list of level descriptions, lowest first (2 to 10). The answer is the expected level (0 = first).
17
17
  - "box": where something is in an image (images only); instructions name the target.
18
18
 
19
19
  Example questions: {"team": {"type": "choice", "instructions": "Which team should handle this ticket?", "criteria": {"billing": "Payments, invoices, refunds", "engineering": "Bugs, errors, outages", "sales": "Plans and pricing"}}, "urgency": {"type": "score", "instructions": "How urgent is this ticket?", "criteria": ["Low", "Normal", "Urgent"]}, "angry": {"type": "noul", "instructions": "Is the customer angry?"}}`
20
20
 
21
- function parseQuestions(value) {
21
+ /** The type a question's criteria imply: a list of levels is a score, named options a choice, anything else yes/no. */
22
+ function impliedType(criteria) {
23
+ if (Array.isArray(criteria)) return 'score'
24
+ if (!criteria || typeof criteria !== 'object') return 'noul'
25
+ return Object.keys(criteria).map(name => name.toLowerCase()).sort().join() === 'no,yes' ? 'noul' : 'choice'
26
+ }
27
+
28
+ export function parseQuestions(value) {
22
29
  const questions = typeof value === 'string' ? JSON.parse(value) : value
23
30
  if (!questions || typeof questions !== 'object' || Array.isArray(questions) || Object.keys(questions).length === 0) {
24
31
  throw new Error('questions must be an object of question id -> {type, instructions, criteria}')
25
32
  }
26
- for (const [id, question] of Object.entries(questions)) {
33
+ for (const [id, asked] of Object.entries(questions)) {
34
+ // System Two often leaves the type out of its first call
35
+ const question = asked && typeof asked === 'object' && asked.type === undefined ? { ...asked, type: impliedType(asked.criteria) } : asked
36
+ questions[id] = question
27
37
  if (!['choice', 'noul', 'score', 'box'].includes(question?.type)) throw new Error(`question "${id}" needs type choice, noul, score or box`)
28
38
  if (typeof question.instructions !== 'string' || !question.instructions.trim()) throw new Error(`question "${id}" needs instructions`)
39
+ // the Decision API does not answer a noul question that carries criteria
40
+ if (question.type === 'noul' && question.criteria !== undefined) {
41
+ const { criteria, ...rest } = question
42
+ const rules = typeof criteria === 'string' ? [criteria]
43
+ : Array.isArray(criteria) ? criteria.map(String)
44
+ : criteria && typeof criteria === 'object' ? Object.entries(criteria).map(([name, rule]) => `${name}: ${rule}`) : []
45
+ questions[id] = { ...rest, instructions: [rest.instructions.trim(), ...rules].join('\n') }
46
+ }
29
47
  }
30
48
  return questions
31
49
  }
package/web/client.js CHANGED
@@ -64,7 +64,8 @@ window.__ModuleLoader__.load({
64
64
  const num = value => (typeof value === 'number' && Number.isFinite(value) ? value : undefined)
65
65
  const p2 = value => (num(value) === undefined ? '?' : value.toFixed(2))
66
66
  const secs = ms => (num(ms) === undefined ? '' : `${(ms / 1000).toFixed(2)} s`)
67
- const shortModel = model => (typeof model === 'string' ? model.replace(/^vllm-sr\//, '') : '')
67
+ // System One's models are vllm-sr's; the name always shows its owner, whatever form the answer gave it in
68
+ const fullModel = model => (typeof model === 'string' && model ? (model.includes('/') ? model : `vllm-sr/${model}`) : '')
68
69
  const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
69
70
  const clamp01 = value => Math.min(1, Math.max(0, value))
70
71
  const level = (labels, score) => labels[Math.min(labels.length - 1, Math.max(0, Math.round(score)))]
@@ -171,10 +172,10 @@ window.__ModuleLoader__.load({
171
172
  verdict: 'brief', label: kindLabel, short: kindLabel.toLowerCase(), tone: 'info',
172
173
  headline: [`${kindLabel} work`, `${effortLabel.toLowerCase()} effort`, ...flags].join(' · '),
173
174
  facts: [
174
- { label: 'Kind', value: `${kindLabel} ${p2(confidence)}`, bar: confidence },
175
- { label: 'Effort', value: effort === undefined ? '?' : `${effortLabel} · ${effort.toFixed(1)} of 3`, bar: effort === undefined ? undefined : effort / 3 },
176
- { label: 'Unclear', value: p2(unclear), bar: unclear, tone: unclear >= 0.6 ? 'warn' : undefined },
177
- { label: 'Risky', value: p2(risky), bar: risky, tone: risky >= 0.5 ? 'bad' : undefined },
175
+ { id: 'kind', label: 'Kind', value: `${kindLabel} ${p2(confidence)}`, bar: confidence },
176
+ { id: 'effort', label: 'Effort', value: effort === undefined ? '?' : `${effortLabel} · ${effort.toFixed(1)} of 3`, bar: effort === undefined ? undefined : effort / 3 },
177
+ { id: 'unclear', label: 'Unclear', value: p2(unclear), bar: unclear, tone: unclear >= 0.6 ? 'warn' : undefined },
178
+ { id: 'risky', label: 'Risky', value: p2(risky), bar: risky, tone: risky >= 0.5 ? 'bad' : undefined },
178
179
  ],
179
180
  note: hints.length ? `Brief for System Two: ${hints.join(' ')}` : undefined,
180
181
  }
@@ -184,21 +185,21 @@ window.__ModuleLoader__.load({
184
185
  const outcome = OUTCOME[row.outcome]
185
186
  if (row.error) {
186
187
  const fallback = { allow: { text: 'so the call ran', tone: 'muted' }, ask: { text: 'so you were asked', tone: 'warn' }, deny: { text: 'so the call was blocked', tone: 'bad' } }[row.verdict]
187
- return { ...errorView(row, fallback), outcome }
188
+ return { ...errorView(row, fallback), outcome, outcomeKey: outcome ? row.outcome : undefined }
188
189
  }
189
190
  const subject = { tool: row.tool, text: row.call }
190
191
  const verdict = GUARD[row.verdict]
191
- if (!verdict) return { verdict: row.outcome ?? 'unknown', ...(outcome ?? { label: 'Reviewed', short: 'reviewed', tone: 'muted' }), subject, facts: [] }
192
+ if (!verdict) return { verdict: row.outcome ?? 'unknown', ...(outcome ?? { label: 'Reviewed', short: 'reviewed', tone: 'muted' }), outcomeKey: outcome ? row.outcome : undefined, subject, facts: [] }
192
193
  const a = row.answers ?? {}
193
194
  const risk = scoreOf(a.risk)
194
195
  const leaks = yesOf(a.exfiltration)
195
196
  const onTask = yesOf(a.on_task)
196
197
  const facts = []
197
- if (risk !== undefined) facts.push({ label: 'Risk', value: `${level(RISK, Math.min(3, risk))} · ${risk.toFixed(2)} of 3`, bar: risk / 3, tone: risk >= 2.5 ? 'bad' : risk >= 1.8 ? 'warn' : 'ok' })
198
- if (leaks !== undefined) facts.push({ label: 'Leaks secrets', value: p2(leaks), bar: leaks, tone: leaks >= 0.7 ? 'bad' : undefined })
199
- if (onTask !== undefined) facts.push({ label: 'On task', value: p2(onTask), bar: onTask, tone: onTask < 0.15 ? 'warn' : undefined })
198
+ if (risk !== undefined) facts.push({ id: 'risk', label: 'Risk', value: `${level(RISK, Math.min(3, risk))} · ${risk.toFixed(2)} of 3`, bar: risk / 3, tone: risk >= 2.5 ? 'bad' : risk >= 1.8 ? 'warn' : 'ok' })
199
+ if (leaks !== undefined) facts.push({ id: 'leaks', label: 'Leaks secrets', value: p2(leaks), bar: leaks, tone: leaks >= 0.7 ? 'bad' : undefined })
200
+ if (onTask !== undefined) facts.push({ id: 'on-task', label: 'On task', value: p2(onTask), bar: onTask, tone: onTask < 0.15 ? 'warn' : undefined })
200
201
  return {
201
- verdict: row.verdict, ...verdict, subject, facts, outcome, verdictTone: verdict.tone,
202
+ verdict: row.verdict, ...verdict, subject, facts, outcome, outcomeKey: outcome ? row.outcome : undefined, verdictTone: verdict.tone,
202
203
  tone: outcome && SEVERITY[outcome.tone] > SEVERITY[verdict.tone] ? outcome.tone : verdict.tone,
203
204
  note: row.verdict !== 'allow' && row.why ? `Because ${row.why}.` : undefined,
204
205
  }
@@ -211,8 +212,8 @@ window.__ModuleLoader__.load({
211
212
  return {
212
213
  verdict: row.verdict, ...verdict, subject: { tool: row.tool, text: row.call },
213
214
  facts: [
214
- { label: 'Passages', value: String(num(row.passages) ?? '?') },
215
- { label: 'Injection', value: p2(row.maxP), bar: num(row.maxP), tone: row.maxP >= 0.5 ? 'bad' : 'ok' },
215
+ { id: 'passages', label: 'Passages', value: String(num(row.passages) ?? '?') },
216
+ { id: 'injection', label: 'Injection', value: p2(row.maxP), bar: num(row.maxP), tone: row.maxP >= 0.5 ? 'bad' : 'ok' },
216
217
  ],
217
218
  note: row.verdict === 'removed' ? `Removed ${plural(lines, 'line')} that gave orders to AI agents before System Two read the result.`
218
219
  : row.verdict === 'flagged' ? `Found ${plural(lines, 'line')} that give orders to AI agents; the result was withheld.` : undefined,
@@ -226,12 +227,12 @@ window.__ModuleLoader__.load({
226
227
  const verdict = DONE[why]
227
228
  const round = `${num(row.steers) ?? '?'} of ${num(row.maxSteers) ?? '?'}`
228
229
  return {
229
- verdict: row.verdict === 'continue' ? 'continue' : 'stop', ...verdict, short: verdict.label.toLowerCase(),
230
+ verdict: row.verdict === 'continue' ? 'continue' : 'stop', ...verdict, why, short: verdict.label.toLowerCase(),
230
231
  headline: row.verdict === 'continue' ? `${verdict.label} to System Two (${round})` : why === 'waiting' ? 'Waiting on you' : 'The work is done',
231
232
  facts: [
232
- { label: 'Complete', value: p2(yesOf(a.complete)), bar: yesOf(a.complete), tone: yesOf(a.complete) < 0.5 ? 'warn' : 'ok' },
233
- { label: 'Checked', value: p2(yesOf(a.checked)), bar: yesOf(a.checked) },
234
- { label: 'Waiting on you', value: p2(yesOf(a.waiting)), bar: yesOf(a.waiting) },
233
+ { id: 'complete', label: 'Complete', value: p2(yesOf(a.complete)), bar: yesOf(a.complete), tone: yesOf(a.complete) < 0.5 ? 'warn' : 'ok' },
234
+ { id: 'checked', label: 'Checked', value: p2(yesOf(a.checked)), bar: yesOf(a.checked) },
235
+ { id: 'waiting', label: 'Waiting on you', value: p2(yesOf(a.waiting)), bar: yesOf(a.waiting) },
235
236
  ],
236
237
  note: why === 'unfinished' ? `Not finished yet, so System Two goes back to work (${round}).`
237
238
  : why === 'unchecked' ? `The changes were not checked, so System Two goes back to verify them (${round}).` : undefined,
@@ -273,7 +274,7 @@ window.__ModuleLoader__.load({
273
274
  const label = view.outcome ? `${view.label} → ${view.outcome.label}` : view.label
274
275
  const rest = []
275
276
  if (view.subject?.text) rest.push(`${view.subject.tool ?? ''} ${view.subject.text}`.trim())
276
- const lead = view.facts.find(fact => fact.label === 'Risk' || fact.label === 'Injection' || fact.label === 'Complete' || fact.label === 'Effort')
277
+ const lead = view.facts.find(fact => ['risk', 'injection', 'complete', 'effort'].includes(fact.id))
277
278
  if (lead) rest.push(`${lead.label.toLowerCase()} ${lead.value}`)
278
279
  if (view.error) rest.push(view.error)
279
280
  return { label, tone: view.tone, rest: rest.join(' · ') }
@@ -313,18 +314,23 @@ window.__ModuleLoader__.load({
313
314
 
314
315
  function Badge({ id, ms, time }) {
315
316
  const fresh = useFresh(`badge:${id}`, time)
316
- return h('span', { className: cx('vsr-s1__badge', fresh && 'vsr-s1__badge--fresh') },
317
+ return h('span', { className: cx('vsr-s1__badge', fresh && 'vsr-s1__badge--fresh'), 'data-system-one-part': 'badge' },
317
318
  h('i', { className: 'vsr-s1__dot', 'aria-hidden': true }),
318
319
  h('span', { className: 'vsr-s1__name' }, 'System One'),
319
- secs(ms) ? h('span', { className: 'vsr-s1__ms' }, secs(ms)) : null)
320
+ secs(ms) ? h('span', { className: 'vsr-s1__ms', 'data-system-one-part': 'latency' }, secs(ms)) : null)
320
321
  }
321
322
 
322
- function Pill({ tone, children, arrow }) {
323
- return h('span', { className: cx('vsr-s1__pill', `vsr-s1__pill--${tone}`, arrow && 'vsr-s1__pill--then') }, children)
323
+ function Model({ model }) {
324
+ const name = fullModel(model)
325
+ return name ? h('b', { className: 'vsr-s1__model', 'data-system-one-part': 'model', title: name }, name) : null
324
326
  }
325
327
 
326
- function Fact({ label, value, bar, tone }) {
327
- return h('span', { className: 'vsr-s1__fact' },
328
+ function Pill({ tone, children, arrow, part }) {
329
+ return h('span', { className: cx('vsr-s1__pill', `vsr-s1__pill--${tone}`, arrow && 'vsr-s1__pill--then'), 'data-system-one-part': part }, children)
330
+ }
331
+
332
+ function Fact({ id, label, value, bar, tone }) {
333
+ return h('span', { className: 'vsr-s1__fact', 'data-system-one-fact': id },
328
334
  h('span', { className: 'vsr-s1__fact-label' }, label),
329
335
  num(bar) === undefined ? null : h('span', { className: cx('vsr-s1__bar', tone && `vsr-s1__bar--${tone}`), style: { '--p': clamp01(bar) }, 'aria-hidden': true }, h('i')),
330
336
  h('b', null, value))
@@ -335,34 +341,44 @@ window.__ModuleLoader__.load({
335
341
  return h('div', {
336
342
  className: cx('vsr-s1__row', fresh && 'vsr-s1__row--fresh'),
337
343
  'data-system-one-decision': view.verdict,
344
+ 'data-system-one-outcome': view.outcomeKey,
338
345
  'data-system-one-call': row.callId,
339
346
  },
340
347
  h('span', { className: 'vsr-s1__verdicts' },
341
- h(Pill, { tone: view.verdictTone ?? view.tone }, view.label),
342
- view.outcome ? h(Pill, { tone: view.outcome.tone, arrow: true }, view.outcome.label) : null),
348
+ h(Pill, { tone: view.verdictTone ?? view.tone, part: 'verdict' }, view.label),
349
+ view.outcome ? h(Pill, { tone: view.outcome.tone, arrow: true, part: 'outcome' }, view.outcome.label) : null),
343
350
  h('div', { className: 'vsr-s1__main' },
344
- view.subject?.text ? h('div', { className: 'vsr-s1__subject' },
351
+ view.subject?.text ? h('div', { className: 'vsr-s1__subject', 'data-system-one-part': 'subject' },
345
352
  view.subject.tool ? h('span', { className: 'vsr-s1__tool' }, view.subject.tool) : null,
346
353
  h('code', { className: 'vsr-s1__code', title: view.subject.text }, view.subject.text)) : null,
347
- view.facts.length ? h('div', { className: 'vsr-s1__facts' }, view.facts.map(fact => h(Fact, { key: fact.label, ...fact }))) : null,
348
- view.note ? h('div', { className: 'vsr-s1__note' }, view.note) : null),
349
- !single && secs(row.ms) ? h('span', { className: 'vsr-s1__rowms' }, secs(row.ms)) : null)
354
+ view.facts.length ? h('div', { className: 'vsr-s1__facts', 'data-system-one-part': 'facts' }, view.facts.map(fact => h(Fact, { key: fact.id, ...fact }))) : null,
355
+ view.note ? h('div', { className: 'vsr-s1__note', 'data-system-one-part': 'note' }, view.note) : null),
356
+ !single && secs(row.ms) ? h('span', { className: 'vsr-s1__rowms', 'data-system-one-part': 'latency' }, secs(row.ms)) : null)
350
357
  }
351
358
 
352
359
  function Rows({ reflex, rows, views }) {
353
360
  const [all, setAll] = React.useState(false)
354
361
  const hidden = all ? 0 : Math.max(0, rows.length - ROWS_SHOWN)
355
- return h('div', { className: 'vsr-s1__rows' },
356
- hidden > 0 ? h('button', { type: 'button', className: 'vsr-s1__more', onClick: () => setAll(true) }, `Show ${plural(hidden, `earlier ${reflex === 'guard' ? 'call' : 'result'}`)}`) : null,
362
+ return h('div', { className: 'vsr-s1__rows', 'data-system-one-part': 'rows' },
363
+ hidden > 0 ? h('button', { type: 'button', className: 'vsr-s1__more', 'data-system-one-part': 'more', onClick: () => setAll(true) }, `Show ${plural(hidden, `earlier ${reflex === 'guard' ? 'call' : 'result'}`)}`) : null,
357
364
  rows.slice(hidden).map((row, index) => h(Row, { key: idOf(row), row, view: views[hidden + index], single: rows.length === 1 })))
358
365
  }
359
366
 
360
- function attributes(kind, state, views, worst) {
367
+ const whole = value => (Number.isSafeInteger(value) ? String(value) : undefined)
368
+
369
+ // a public contract (README.md, "Selectors"); React leaves out an attribute whose value is undefined
370
+ function attributes(kind, state, worst) {
361
371
  return {
362
372
  'data-system-one': kind,
363
373
  'data-system-one-reflex': state.reflex,
364
374
  'data-system-one-verdict': worst.verdict,
375
+ 'data-system-one-outcome': worst.outcomeKey,
376
+ 'data-system-one-why': worst.why,
377
+ 'data-system-one-tone': worst.tone,
365
378
  'data-system-one-count': String(state.rows.length),
379
+ 'data-system-one-turn': whole(state.rows[0].turn),
380
+ 'data-system-one-step': state.reflex === 'guard' || state.reflex === 'shield' ? whole(state.rows[0].step) : undefined,
381
+ 'data-system-one-model': fullModel(state.rows[state.rows.length - 1].model) || undefined,
366
382
  }
367
383
  }
368
384
 
@@ -377,14 +393,14 @@ window.__ModuleLoader__.load({
377
393
  className: cx('vsr-s1', 'vsr-s1--card', `vsr-s1--${worst.tone}`, fresh && 'vsr-s1--fresh'),
378
394
  role: 'group',
379
395
  'aria-label': `System One ${reflex}: ${headline}`,
380
- ...attributes('card', state, views, worst),
396
+ ...attributes('card', state, worst),
381
397
  },
382
398
  h('span', { className: 'vsr-s1__sheen', 'aria-hidden': true }),
383
- h('div', { className: 'vsr-s1__head' },
399
+ h('div', { className: 'vsr-s1__head', 'data-system-one-part': 'head' },
384
400
  h(Badge, { key: idOf(last), id: idOf(last), ms: last.ms, time: last.time }),
385
- h('span', { className: 'vsr-s1__reflex' }, reflex),
386
- h('span', { className: 'vsr-s1__summary', title: headline }, headline),
387
- last.model ? h('span', { className: 'vsr-s1__model', title: last.model }, shortModel(last.model)) : null),
401
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, reflex),
402
+ h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
403
+ h(Model, { model: last.model })),
388
404
  h(Rows, { reflex: state.reflex, rows: state.rows, views }))
389
405
  }
390
406
 
@@ -406,23 +422,56 @@ window.__ModuleLoader__.load({
406
422
  className: cx('vsr-s1', 'vsr-s1--compact', `vsr-s1--${worst.tone}`, open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'),
407
423
  role: 'group',
408
424
  'aria-label': `System One ${reflex}: ${said}`,
409
- ...attributes('compact', state, views, worst),
425
+ ...attributes('compact', state, worst),
410
426
  },
411
- h('button', { type: 'button', className: 'vsr-s1__chip', 'aria-expanded': open, onClick: () => setOpen(!open) },
427
+ h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
412
428
  h(Badge, { key: idOf(last), id: idOf(last), ms: last.ms, time: last.time }),
413
- h('span', { className: 'vsr-s1__reflex' }, reflex),
414
- h('span', { className: 'vsr-s1__summary', title: said },
415
- h('span', { className: cx('vsr-s1__verdict', line.tone && `vsr-s1__verdict--${line.tone}`) }, line.label),
429
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, reflex),
430
+ h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: said },
431
+ h('span', { className: cx('vsr-s1__verdict', line.tone && `vsr-s1__verdict--${line.tone}`), 'data-system-one-part': 'verdict' }, line.label),
416
432
  line.rest ? ` · ${line.rest}` : null),
433
+ open ? h(Model, { model: last.model }) : null,
417
434
  h(Chevron)),
418
435
  open ? h(Rows, { reflex: state.reflex, rows: state.rows, views }) : null)
419
436
  }
420
437
 
438
+ function KeyGlyph() {
439
+ return h('svg', { className: 'vsr-s1__glyph', viewBox: '0 0 16 16', width: 12, height: 12, 'aria-hidden': true },
440
+ h('circle', { cx: 5.5, cy: 8, r: 3, fill: 'none', stroke: 'currentColor', strokeWidth: 1.6 }),
441
+ h('path', { d: 'M8.5 8H14M12 8v2.5M14 8v2', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round' }))
442
+ }
443
+
444
+ // The free key a start fetched, or why it could not: one line under the request, in either mode.
445
+ function KeyNotice({ state }) {
446
+ const row = state.rows[state.rows.length - 1]
447
+ const created = row.verdict === 'created'
448
+ const fresh = useFresh(`key:${idOf(row)}`, row.time)
449
+ const site = text(row.site) || 'app.vllm-sr.ai'
450
+ const said = created
451
+ ? `Free API key created for ${site}, saved in ${text(row.saved) || '~/.dsh/.env'}`
452
+ : `No free API key from ${site}: ${text(row.error?.message) || 'the site refused'}. Get one at ${site} and save it as ${text(row.ref) || 'VLLM_SR_API_KEY'}.`
453
+ const link = (href, label) => (typeof href === 'string' && /^https:\/\//.test(href) ? h('a', { className: 'vsr-s1__link', href, target: '_blank', rel: 'noreferrer' }, label) : null)
454
+ return h('div', {
455
+ className: cx('vsr-s1', 'vsr-s1--notice', created ? 'vsr-s1--ok' : 'vsr-s1--bad', fresh && 'vsr-s1--fresh'),
456
+ role: 'note',
457
+ 'aria-label': `System Duo: ${said}`,
458
+ 'data-system-one': 'notice',
459
+ 'data-system-one-reflex': 'key',
460
+ 'data-system-one-verdict': created ? 'created' : 'failed',
461
+ 'data-system-one-tone': created ? 'ok' : 'bad',
462
+ 'data-system-one-turn': whole(row.turn),
463
+ },
464
+ h('span', { className: cx('vsr-s1__duo', fresh && 'vsr-s1__badge--fresh'), 'data-system-one-part': 'badge' }, h(KeyGlyph), 'System Duo'),
465
+ h('span', { className: cx('vsr-s1__summary', !created && 'vsr-s1__summary--bad'), 'data-system-one-part': 'summary', title: said }, said),
466
+ created && (row.terms || row.privacy) ? h('span', { className: 'vsr-s1__links', 'data-system-one-part': 'links' }, link(row.terms, 'Terms'), link(row.privacy, 'Privacy')) : null)
467
+ }
468
+
421
469
  // A routine check (an allowed call, a clean result) is one line in either mode; a decision that changed
422
470
  // something, and the triage and done-check that frame the turn, are cards unless the mode is compact.
423
471
  function SystemOneNode(props) {
424
472
  const state = props?.node?.data
425
473
  if (!state || !Array.isArray(state.rows) || state.rows.length === 0) return null
474
+ if (state.reflex === 'key') return h(Contained, null, h(KeyNotice, { state }))
426
475
  const slim = state.ui === 'compact' || !standsOut(state)
427
476
  return h(Contained, null, h(slim ? Compact : Card, { state }))
428
477
  }
@@ -455,7 +504,7 @@ window.__ModuleLoader__.load({
455
504
 
456
505
  function Stack({ parts, fresh }) {
457
506
  const total = parts.reduce((sum, part) => sum + part.n, 0)
458
- return h('span', { className: cx('vsr-s1__stack', fresh && 'vsr-s1__stack--fresh'), 'aria-hidden': true },
507
+ return h('span', { className: cx('vsr-s1__stack', fresh && 'vsr-s1__stack--fresh'), 'data-system-one-part': 'stack', 'aria-hidden': true },
459
508
  total > 0 ? parts.filter(part => part.n > 0).map((part, index) => h('i', { key: part.label, style: { flexGrow: part.n, background: part.color, animationDelay: `${0.12 + index * 0.06}s` } })) : null)
460
509
  }
461
510
 
@@ -463,12 +512,13 @@ window.__ModuleLoader__.load({
463
512
  const LABEL_CHARS = 28
464
513
  const shortLabel = label => (label.length > LABEL_CHARS ? `${label.slice(0, LABEL_CHARS - 1).trimEnd()}…` : label)
465
514
 
466
- function Legend({ parts, extra }) {
467
- return h('span', { className: 'vsr-s1__legend' },
515
+ function Legend({ parts, extra, failed }) {
516
+ return h('span', { className: 'vsr-s1__legend', 'data-system-one-part': 'legend' },
468
517
  parts.map(part => h('span', { key: part.label, className: 'vsr-s1__key', title: `${part.label}: ${part.n}` },
469
518
  h('i', { className: 'vsr-s1__swatch', style: { background: part.color } }),
470
519
  `${shortLabel(part.label)} `, h('b', null, String(part.n)))),
471
- extra ? h('span', { className: 'vsr-s1__key vsr-s1__key--note' }, extra) : null)
520
+ extra ? h('span', { className: 'vsr-s1__key vsr-s1__key--note' }, extra) : null,
521
+ failed ? h('span', { className: 'vsr-s1__key vsr-s1__key--bad', 'data-system-one-part': 'failed' }, `${failed} failed`) : null)
472
522
  }
473
523
 
474
524
  function questionParts(summary) {
@@ -501,14 +551,14 @@ window.__ModuleLoader__.load({
501
551
  h('code', { className: 'vsr-s1__qname', title: id }, id),
502
552
  h('div', { className: 'vsr-s1__qbody' },
503
553
  parts.length ? h(Stack, { parts, fresh }) : null,
504
- h(Legend, { parts, extra: [extra, summary?.failed ? `${summary.failed} failed` : undefined].filter(Boolean).join(' · ') || undefined })))
554
+ h(Legend, { parts, extra, failed: num(summary?.failed) || undefined })))
505
555
  }
506
556
 
507
557
  function ToolBody({ meta, fresh, cwd, openFile }) {
508
558
  const entries = Object.entries(meta.summary ?? {})
509
- return h('div', { className: 'vsr-s1__rows' },
559
+ return h('div', { className: 'vsr-s1__rows', 'data-system-one-part': 'rows' },
510
560
  entries.map(([id, summary]) => h(Question, { key: id, id, summary, fresh })),
511
- meta.output ? h('div', { className: 'vsr-s1__foot' },
561
+ meta.output ? h('div', { className: 'vsr-s1__foot', 'data-system-one-part': 'results' },
512
562
  'Results ',
513
563
  typeof openFile === 'function'
514
564
  ? h('button', { type: 'button', className: 'vsr-s1__link', onClick: () => openFile(meta.output) }, relative(meta.output, cwd))
@@ -524,36 +574,44 @@ window.__ModuleLoader__.load({
524
574
  const [open, setOpen] = React.useState(false)
525
575
  const source = args.file ? basename(args.file) : Array.isArray(args.images) ? plural(args.images.length, 'image') : Array.isArray(args.items) ? plural(args.items.length, 'item') : ''
526
576
  const asked = questionCount(args.questions)
527
- const common = { 'data-system-one': 'tool', 'data-system-one-reflex': 'tool', 'data-system-one-call': callId }
577
+ const common = { 'data-system-one-reflex': 'tool', 'data-system-one-call': callId }
528
578
 
529
579
  if (phase !== 'result') {
530
580
  const line = phase === 'preparing' ? 'Preparing a batch' : `Answering ${plural(asked, 'question')}${source ? ` over ${source}` : ''}`
531
- return h('div', { className: 'vsr-s1 vsr-s1--compact vsr-s1--running', ...common, 'data-system-one-verdict': 'running' },
581
+ return h('div', { className: 'vsr-s1 vsr-s1--compact vsr-s1--running', 'data-system-one': 'tool', ...common, 'data-system-one-verdict': 'running', 'data-system-one-tone': 'info' },
532
582
  h('div', { className: 'vsr-s1__chip vsr-s1__chip--static' },
533
583
  h(Badge, { id: `${callId}:running`, time: Date.now() }),
534
- h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
535
- h('span', { className: 'vsr-s1__summary vsr-s1__shimmer' }, line)))
584
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
585
+ h('span', { className: 'vsr-s1__summary vsr-s1__shimmer', 'data-system-one-part': 'summary' }, line)))
536
586
  }
537
587
 
538
588
  if (block?.isError || !meta || ui === 'off') {
539
589
  const body = resultText(block)
540
590
  const first = body.split('\n')[0] || (block?.isError ? 'The batch failed' : 'Done')
541
- return h('div', { className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--plain', block?.isError && 'vsr-s1--bad', open && 'vsr-s1--open'), ...common, 'data-system-one-verdict': block?.isError ? 'error' : 'done' },
542
- h('button', { type: 'button', className: 'vsr-s1__chip', 'aria-expanded': open, onClick: () => setOpen(!open) },
543
- h('span', { className: 'vsr-s1__reflex' }, 'System One'),
544
- h('span', { className: cx('vsr-s1__summary', block?.isError && 'vsr-s1__summary--bad'), title: first }, first),
545
- h(Chevron)),
546
- open ? h('pre', { className: 'vsr-s1__raw' }, body) : null)
591
+ return h('div', {
592
+ className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--plain', block?.isError && 'vsr-s1--bad', open && 'vsr-s1--open'),
593
+ 'data-system-one': 'tool', ...common, 'data-system-one-verdict': block?.isError ? 'error' : 'done', 'data-system-one-tone': block?.isError ? 'bad' : 'muted',
594
+ },
595
+ h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
596
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, 'System One'),
597
+ h('span', { className: cx('vsr-s1__summary', block?.isError && 'vsr-s1__summary--bad'), 'data-system-one-part': 'summary', title: first }, first),
598
+ h(Chevron)),
599
+ open ? h('pre', { className: 'vsr-s1__raw', 'data-system-one-part': 'raw' }, body) : null)
547
600
  }
548
601
 
549
602
  const ms = num(meta.seconds) === undefined ? undefined : meta.seconds * 1000
550
603
  const headline = [plural(num(meta.items) ?? 0, 'item'), plural(num(meta.questions) ?? 0, 'question'), plural(num(meta.requests) ?? 0, 'request')].join(' · ')
604
+ const done = {
605
+ ...common, 'data-system-one-verdict': 'done', 'data-system-one-tone': 'ok', 'data-system-one-count': String(meta.items ?? ''),
606
+ 'data-system-one-model': fullModel(meta.model) || undefined,
607
+ }
551
608
  if (ui === 'compact') {
552
- return h('div', { className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--ok', open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'), ...common, 'data-system-one-verdict': 'done', 'data-system-one-count': String(meta.items ?? '') },
553
- h('button', { type: 'button', className: 'vsr-s1__chip', 'aria-expanded': open, onClick: () => setOpen(!open) },
609
+ return h('div', { className: cx('vsr-s1', 'vsr-s1--compact', 'vsr-s1--ok', open && 'vsr-s1--open', fresh && 'vsr-s1--fresh'), 'data-system-one': 'tool', ...done },
610
+ h('button', { type: 'button', className: 'vsr-s1__chip', 'data-system-one-part': 'toggle', 'aria-expanded': open, onClick: () => setOpen(!open) },
554
611
  h(Badge, { id: `${callId}:done`, ms, time: block?.time }),
555
- h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
556
- h('span', { className: 'vsr-s1__summary', title: headline }, headline),
612
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
613
+ h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
614
+ open ? h(Model, { model: meta.model }) : null,
557
615
  h(Chevron)),
558
616
  open ? h(ToolBody, { meta, fresh, cwd, openFile }) : null)
559
617
  }
@@ -561,16 +619,15 @@ window.__ModuleLoader__.load({
561
619
  className: cx('vsr-s1', 'vsr-s1--card', 'vsr-s1--ok', 'vsr-s1--tool', fresh && 'vsr-s1--fresh'),
562
620
  role: 'group',
563
621
  'aria-label': `System One batch: ${headline}`,
564
- ...common,
565
- 'data-system-one-verdict': 'done',
566
- 'data-system-one-count': String(meta.items ?? ''),
622
+ 'data-system-one': 'tool',
623
+ ...done,
567
624
  },
568
625
  h('span', { className: 'vsr-s1__sheen', 'aria-hidden': true }),
569
- h('div', { className: 'vsr-s1__head' },
626
+ h('div', { className: 'vsr-s1__head', 'data-system-one-part': 'head' },
570
627
  h(Badge, { id: `${callId}:done`, ms, time: block?.time }),
571
- h('span', { className: 'vsr-s1__reflex' }, REFLEX.tool),
572
- h('span', { className: 'vsr-s1__summary', title: headline }, headline),
573
- meta.model ? h('span', { className: 'vsr-s1__model', title: meta.model }, shortModel(meta.model)) : null),
628
+ h('span', { className: 'vsr-s1__reflex', 'data-system-one-part': 'reflex' }, REFLEX.tool),
629
+ h('span', { className: 'vsr-s1__summary', 'data-system-one-part': 'summary', title: headline }, headline),
630
+ h(Model, { model: meta.model })),
574
631
  h(ToolBody, { meta, fresh, cwd, openFile }))
575
632
  }
576
633
 
@@ -599,7 +656,13 @@ window.__ModuleLoader__.load({
599
656
  .vsr-s1__summary--bad{color:var(--vsr-s1-bad)}
600
657
  .vsr-s1__verdict{font-weight:600;color:var(--vsr-s1-text)}
601
658
  .vsr-s1__verdict--ok{color:var(--vsr-s1-ok)}.vsr-s1__verdict--warn{color:color-mix(in srgb,var(--vsr-s1-warn) 82%,var(--vsr-s1-text))}.vsr-s1__verdict--bad{color:var(--vsr-s1-bad)}.vsr-s1__verdict--info{color:var(--vsr-s1-ember-deep)}.vsr-s1__verdict--muted{color:var(--vsr-s1-text-3)}
602
- .vsr-s1__model{flex:none;color:var(--vsr-s1-text-3);font-size:12px;white-space:nowrap}
659
+ .vsr-s1__model{flex:none;color:var(--vsr-s1-text-2);font-size:12px;font-weight:700;letter-spacing:-.005em;white-space:nowrap}
660
+ .vsr-s1--notice{display:flex;align-items:center;gap:9px;min-width:0;margin:4px 0;padding:3px 0}
661
+ .vsr-s1--notice .vsr-s1__summary{white-space:normal}
662
+ .vsr-s1__duo{position:relative;display:inline-flex;flex:none;align-items:center;gap:5px;height:22px;padding:0 9px 0 8px;border-radius:999px;color:#fff;font-size:12px;font-weight:650;white-space:nowrap;background:linear-gradient(110deg,var(--vsr-s1-ember) 0%,#B04FD0 58%,#2F7BF6 100%);box-shadow:0 1px 2px rgba(90,60,170,.25),inset 0 1px 0 rgba(255,255,255,.32)}
663
+ .vsr-s1__glyph{flex:none}
664
+ .vsr-s1__links{display:inline-flex;flex:none;gap:10px}
665
+ a.vsr-s1__link{text-decoration:none}
603
666
  .vsr-s1__rows{display:grid;grid-template-columns:auto minmax(0,1fr) auto;column-gap:12px;row-gap:8px;margin-top:9px}
604
667
  .vsr-s1__rows>*{grid-column:1/-1}
605
668
  .vsr-s1__row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-columns:subgrid;column-gap:12px;align-items:start}
@@ -631,14 +694,15 @@ window.__ModuleLoader__.load({
631
694
  .vsr-s1--plain .vsr-s1__reflex{color:var(--vsr-s1-text-2)}
632
695
  .vsr-s1__raw{margin:4px 0 8px;padding:8px 10px;max-height:320px;overflow:auto;border-radius:8px;background:color-mix(in srgb,var(--vsr-s1-text-3) 10%,transparent);font-family:var(--vsr-s1-mono);font-size:12px;white-space:pre-wrap}
633
696
  .vsr-s1__q{display:grid;grid-template-columns:minmax(80px,140px) minmax(0,1fr);column-gap:14px;align-items:start}
634
- .vsr-s1__qname{overflow:hidden;color:var(--vsr-s1-text);font-family:var(--vsr-s1-mono);font-size:12.5px;line-height:10px;white-space:nowrap;text-overflow:ellipsis;background:none;padding:0}
697
+ .vsr-s1__qname{overflow:hidden;color:var(--vsr-s1-text);font-family:var(--vsr-s1-mono);font-size:12.5px;line-height:16px;white-space:nowrap;text-overflow:ellipsis;background:none;padding:0}
635
698
  .vsr-s1__qbody{display:grid;gap:4px;min-width:0}
636
- .vsr-s1__stack{display:flex;gap:2px;height:8px;border-radius:4px;overflow:hidden;background:var(--vsr-s1-track)}
699
+ .vsr-s1__stack{display:flex;gap:2px;height:8px;margin-top:4px;border-radius:4px;overflow:hidden;background:var(--vsr-s1-track)}
637
700
  .vsr-s1__stack>i{display:block;min-width:3px;height:100%;transform-origin:left center}
638
701
  .vsr-s1__legend{display:flex;flex-wrap:wrap;gap:1px 14px;color:var(--vsr-s1-text-2);font-size:12.5px}
639
702
  .vsr-s1__key{display:inline-flex;align-items:center;white-space:nowrap}
640
703
  .vsr-s1__key b{margin-left:4px;color:var(--vsr-s1-text);font-weight:600;font-variant-numeric:tabular-nums}
641
704
  .vsr-s1__key--note{color:var(--vsr-s1-text-3)}
705
+ .vsr-s1__key--bad{color:var(--vsr-s1-bad);font-weight:600}
642
706
  .vsr-s1__swatch{flex:none;width:8px;height:8px;margin-right:6px;border-radius:2px}
643
707
  .vsr-s1__foot{color:var(--vsr-s1-text-3);font-size:12.5px}
644
708
  .vsr-s1__foot code{font-family:var(--vsr-s1-mono)}