@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 +55 -13
- package/package.json +2 -2
- package/src/index.js +28 -30
- package/src/keys.js +40 -3
- package/src/records.js +22 -0
- package/src/tool.js +21 -3
- package/web/client.js +138 -74
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
|
|
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
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
|
29
|
-
|
|
30
|
-
by you only). A
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
4
|
-
"description": "System Duo for DeepSeek Harness. Thinking, fast and slow
|
|
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
|
|
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
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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(
|
|
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: ${
|
|
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)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =>
|
|
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
|
|
323
|
-
|
|
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
|
|
327
|
-
return h('span', { className: 'vsr-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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', {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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'),
|
|
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
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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-
|
|
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:
|
|
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)}
|