@hue-run/sdk 0.4.2 → 0.5.1

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/CLI.md CHANGED
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
42
42
  The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
43
43
  `capture_content=False`. For a supported application, setup installs the dependency and adds the
44
44
  managed import and middleware registration to the existing entrypoint; an unreferenced helper is
45
- not a completed integration. TypeScript uses `@hue-run/sdk@0.4.2`, `@opentelemetry/api@1.9.1` and
45
+ not a completed integration. TypeScript uses `@hue-run/sdk@0.5.1`, `@opentelemetry/api@1.9.1` and
46
46
  `@opentelemetry/context-async-hooks@2.11.0`; Python uses published `hue-run==0.2.2`.
47
47
  Content capture requires an ordinary account-managed key and a later explicit application decision.
48
48
 
@@ -79,7 +79,7 @@ events, checkpoints or receipts. Ordinary app starts have no handshake and retai
79
79
  listener behavior. This prevents accidental requests to an unrelated listener, not access by
80
80
  malicious code running as the same local user.
81
81
 
82
- Setup creates no Scenario, Hue Run, evaluation, source capture, worker or remote execution. Package
82
+ Setup creates no simulation, Hue Run, evaluation, source capture, worker or remote execution. Package
83
83
  manager lifecycle scripts are disabled. The supported existing application entrypoint is executed
84
84
  directly with fixed argv solely for its bounded local HTTP verification; no shell command is accepted.
85
85
 
@@ -111,6 +111,8 @@ hue claim --restart # explicit owner recovery; requires an interactive loc
111
111
  hue setup --agent # noninteractive version-2 JSONL events
112
112
  hue setup --format human # explicit append-only terminal rendering
113
113
  hue setup --origin http://127.0.0.1:PORT # isolated loopback tests only
114
+ hue login # validate keys created in Hue and store them in ./.env.hue
115
+ hue mcp install --client claude-code # write the Hue MCP configuration for a coding agent
114
116
  ```
115
117
 
116
118
  Hosted setup accepts HTTPS origins only. HTTP is accepted solely for `localhost`, `127.0.0.1` and
@@ -138,6 +140,63 @@ uses the observed predecessor for compare-and-swap. A conflict refreshes status
138
140
  At most 32 distinct handoff IDs exist per installation; `SETUP_HANDOFF_LIMIT` is terminal. Replacing a
139
141
  handoff does not create a trial, reset quota or rerun business work.
140
142
 
143
+ ## Sign in and store keys
144
+
145
+ `hue login` stores keys that a person creates in Hue; it never mints one, because setup
146
+ credentials are deliberately isolated from ordinary project keys. It prints the key settings page
147
+ (`<origin>/settings/integrations`, opened in a browser only when a terminal is attached and
148
+ `--no-browser` is absent), then reads one **Read and write** key from stdin without echo. By
149
+ default that single key serves both uses: it is checked with `GET /api/v1/projects/current` and
150
+ `GET /api/v1/datasets` (a **Read** or **Tracing only** key is refused because it cannot use
151
+ evaluations) and with an MCP `tools/list` request, then stored as `HUE_API_KEY` with
152
+ `HUE_BASE_URL` and `HUE_MCP_KEY` with `HUE_MCP_URL` in one write. A rejected key (`401` or `403`)
153
+ exits `1` and stores nothing. `--keys evaluations` stores only `HUE_API_KEY`; `--keys coding-agent`
154
+ stores only `HUE_MCP_KEY` and also accepts a **Read** key, for an agent that should only inspect the
155
+ project. Run them separately with different keys if you want to revoke either use alone.
156
+
157
+ ```sh
158
+ hue login # one key for both uses into ./.env.hue
159
+ hue login --keys coding-agent --gitignore # only HUE_MCP_KEY; add .env.hue to .gitignore
160
+ hue login --origin https://staging.hue.run --env-file .env.staging
161
+ ```
162
+
163
+ The env file is written with mode `0600` through a temporary file and an atomic rename. Other
164
+ lines are preserved; a symlink or a non-regular file is refused; an existing different value is
165
+ replaced only with `--force`. Empty values, whitespace and URLs are refused before any request.
166
+ The MCP endpoint is `https://mcp.hue.run/mcp` for `https://app.hue.run`,
167
+ `https://mcp.staging.hue.run/mcp` for `https://staging.hue.run` and `<origin>/api/mcp` otherwise;
168
+ plain HTTP origins are accepted for loopback test servers only. Output names variables and lengths
169
+ (`Stored the key (NN chars) as HUE_API_KEY and HUE_MCP_KEY`), never values. When git does not ignore the env file, the command
170
+ warns; `--gitignore` appends the file name to the `.gitignore` next to it. Exit codes: `0` stored,
171
+ `1` failed, `2` usage error, `130` interrupted.
172
+
173
+ ## Install the MCP for your coding agent
174
+
175
+ `hue mcp install --client <name>` writes, runs or prints the configuration for Hue's MCP server.
176
+ Every shape matches the snippet Hue shows in Settings: server name `hue`, the endpoint (default
177
+ `https://mcp.hue.run/mcp`; `--url https://mcp.staging.hue.run/mcp` for staging) and a reference to
178
+ the `HUE_MCP_KEY` environment variable. A key value is never written.
179
+
180
+ | Client | Result |
181
+ | --- | --- |
182
+ | `claude-code` | Merges `mcpServers.hue` into `./.mcp.json`. `--scope user` runs `claude mcp add --transport http --scope user hue URL --header 'Authorization: Bearer ${HUE_MCP_KEY}'` when `claude` is on `PATH`, otherwise prints it. |
183
+ | `cursor` | Merges `mcpServers.hue` into `./.cursor/mcp.json` with `${env:HUE_MCP_KEY}`. |
184
+ | `codex` | Runs `codex mcp add hue --url URL --bearer-token-env-var HUE_MCP_KEY`, or prints the `[mcp_servers.hue]` TOML block for `~/.codex/config.toml`. |
185
+ | `vscode` | Merges `servers.hue` and the `hue-mcp-key` password input into `./.vscode/mcp.json`. |
186
+ | `windsurf` | Prints the `serverUrl` snippet for `~/.codeium/windsurf/mcp_config.json`; nothing is written to the home directory. |
187
+ | `gemini` | Runs `gemini mcp add --transport http hue URL -H 'Authorization: Bearer $HUE_MCP_KEY'`, or prints it. |
188
+
189
+ JSON files are parsed and merged: other servers, inputs and top-level fields are kept, only the
190
+ `hue` entry is replaced, and invalid JSON (including comments) is refused together with the snippet
191
+ to add by hand. Files are written with mode `0644` through a temporary file and an atomic rename;
192
+ symlinks are refused. `--dry-run` prints the resulting file content or command without writing or
193
+ running; `--print` prints only the snippet. Client CLIs run without a shell, so the
194
+ `${HUE_MCP_KEY}` and `$HUE_MCP_KEY` references reach them literally; the printed commands use
195
+ single quotes for the same reason. After installation the command reminds you to export
196
+ `HUE_MCP_KEY` in the shell that starts the client (VS Code prompts for the key instead) and prints
197
+ the verification prompt: `Use the Hue MCP: call get_project_context, then show my 5 most recent
198
+ error traces with links.` Exit codes: `0` done or printed, `1` failed, `2` usage error.
199
+
141
200
  ## Local state and conflicts
142
201
 
143
202
  Before its first network write, setup creates a lowercase UUIDv4 and a 32-byte random installation
@@ -241,7 +300,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
241
300
  # Set project to an existing supported fixture; use the same directory on resume.
242
301
  project=/absolute/path/to/supported-fixture
243
302
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
244
- --archive .artifacts/typescript/hue-run-sdk-0.4.2.tgz \
303
+ --archive .artifacts/typescript/hue-run-sdk-0.5.1.tgz \
245
304
  --origin https://STAGING_ORIGIN \
246
305
  --project "$project" --command setup \
247
306
  --evidence .context/setup-staging-before-claim.json
@@ -252,7 +311,7 @@ the private local handoff and finish the real browser claim, then reconcile the
252
311
 
253
312
  ```sh
254
313
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
255
- --archive .artifacts/typescript/hue-run-sdk-0.4.2.tgz \
314
+ --archive .artifacts/typescript/hue-run-sdk-0.5.1.tgz \
256
315
  --origin https://STAGING_ORIGIN \
257
316
  --project "$project" --command claim \
258
317
  --evidence .context/setup-staging-after-claim.json
@@ -273,3 +332,135 @@ only the private local file and produces no secret-bearing screenshots, traces,
273
332
  Publication under a candidate dist-tag, registry acceptance, production activation, promotion of
274
333
  the same version to `latest`, and clean-project literal `@latest` smoke remain separate gates; see
275
334
  [RELEASING.md](../../RELEASING.md).
335
+
336
+ ## Evaluate an agent against a Scenario
337
+
338
+ `hue eval` runs a developer's local agent against a published Hue Scenario or a saved eval set
339
+ and prints Hue's verdicts. It is the command-line form of `runSimulation()` (one-shot) and
340
+ `runLocalAgent()` (worker): the agent, its prompts and its provider credentials stay in the local
341
+ process, Hue creates one isolated simulated world per case, and Hue-owned outcome checks grade
342
+ the sealed world. Hue never executes the agent. `HUE_API_KEY` must be a **Read and
343
+ write** project key (a **Read** or **Tracing only** key cannot read Scenarios or create experiments);
344
+ the CLI never prints it. The optional `zod` peer of `@hue-run/sdk/evals` must be installed.
345
+
346
+ Write an adapter module that hands the case inputs and the world's tools or MCP connection to
347
+ the real agent. The module exports `default` or `runMyAgent`; `context` is the SDK's
348
+ `SimulationTargetContext` (`tools`, `mcp`, `config`, `item`, `executionId`, `environmentRunId`,
349
+ `signal`):
350
+
351
+ ```ts
352
+ // hue-agent.ts: erasable TypeScript only; Node.js 24 and Bun strip the types natively.
353
+ import type { JsonValue, SimulationTargetContext } from "@hue-run/sdk/evals";
354
+ import { runAgent } from "./src/agent.js"; // The application's existing entry point.
355
+
356
+ export default function runMyAgent(inputs: JsonValue, context: SimulationTargetContext) {
357
+ // Hand context.tools or context.mcp to the agent's real tool boundary; configuration alone
358
+ // does not redirect provider calls. Pass context.signal for cooperative cancellation.
359
+ const { config, tools, mcp, signal } = context;
360
+ return runAgent({ inputs, config, tools, mcp, signal });
361
+ }
362
+ ```
363
+
364
+ ```sh
365
+ hue eval --scenario "Refund an eligible charge" ./hue-agent.ts --env-file .env.hue
366
+ hue eval --scenario https://app.hue.run/projects/demo/scenarios/<id> ./hue-agent.ts --baseline <experiment id>
367
+ hue eval --set "Billing regressions" --scorer-version <id> ./hue-agent.ts --save-version
368
+ hue eval --scenario "Refund an eligible charge" --command "python agent.py" --timeout 120 --content
369
+ hue eval --worker ./hue-agent.ts --agent-key support-agent --env-file .env.hue
370
+ ```
371
+
372
+ The one-shot mode resolves the selection (`--scenario` by name, ID or URL; `--set` by name, ID or
373
+ URL with explicit `--scorer-version` pins; or `--dataset-version` with `--scorer-version`),
374
+ creates a fresh experiment from those immutable pins named `<scenario> · <agent key> · <revision>`
375
+ (`--name` overrides), prints `Run: <url>` and `Experiment: <id>` as soon as the experiment exists,
376
+ one line per case event (world created, agent started, world sealed), then
377
+ `Waiting for Hue checks...` and a table with one row per case: boolean metrics as `PASS`/`FAIL`,
378
+ numbers as values, text and category metrics as-is, an overall result per case and a pass count.
379
+ Failing cases print the scorer explanation. `--baseline <experiment id|url>` adds improvement,
380
+ regression and unchanged counts with per-case deltas. `--json` prints one JSON document
381
+ (`experimentId`, `runId`, `runUrl`, `complete`, `cases`, `totals`, optional `baseline`) on stdout
382
+ and sends progress to stderr. `--wait <seconds>` (default 300) bounds the verdict wait because
383
+ Hue-owned `world_outcome` checks are graded after the world seals. An experiment always covers
384
+ every case of the saved version; there is no case subset.
385
+
386
+ `--command "<shell command>"` spawns the command once per case with `HUE_MCP_URL`,
387
+ `HUE_MCP_TOKEN`, `HUE_MCP_EXPIRES_AT`, `HUE_EXECUTION_ID`, `HUE_ENVIRONMENT_RUN_ID`, `HUE_CASE_ID`
388
+ and `HUE_CASE_KEY` in its environment and `{"inputs": ..., "config": ...}` on stdin. Its stdout is
389
+ the answer (JSON when it parses, otherwise trimmed text; empty means no output); a non-zero exit
390
+ or the per-case `--timeout` (default 600 seconds) is a target failure. The scoped MCP token is
391
+ never logged. The agent key defaults to the slug of the command's script name.
392
+
393
+ `--worker` registers the adapter through `runLocalAgent()` with key `--agent-key` (default: the
394
+ adapter filename slug), name `--agent-name` (default: the key), revision `--revision` (default:
395
+ `AGENT_REVISION`, then the Git `HEAD` short hash, then `dev`) and capability `environment:v1`,
396
+ prints the registration and each claimed run with its URL, executes runs launched from Hue until
397
+ Ctrl+C or `--max-runs <n>`, and prints the verdict table after each run. Selection flags do not
398
+ apply; Hue chooses the pinned experiment. The worker exits 0 when it stops normally.
399
+
400
+ A Scenario or eval set whose dataset version is not saved cannot back an experiment: the command
401
+ exits 1 and asks for **Save eval-set version** in Hue or `--save-version`, which freezes that
402
+ version at its current revision. Connection settings are `HUE_API_KEY` and `HUE_BASE_URL`
403
+ (default `https://app.hue.run`), loaded from `--env-file <path>` first when given; `--origin`
404
+ overrides the origin. Telemetry content capture stays off unless `--content` is passed; in
405
+ one-shot mode it also decides whether case outputs, error messages and explanations are persisted
406
+ to Hue. `--worker` always persists them, because a run launched from Hue is read on its run page:
407
+ that is `runLocalAgent()`'s contract and `--content` does not change it. Trace evidence is required
408
+ for every case.
409
+ Resumable checkpoints live in `.hue/eval/<agent-key>/<project id>/` (a `.gitignore` is written
410
+ inside `.hue/eval/`); `--checkpoint-dir` overrides the root. Rerunning the same selection resumes
411
+ an interrupted run without invoking the agent again; a different selection is refused until the
412
+ unfinished one is resumed or its directory is removed.
413
+
414
+ Exit codes: `0` every case passed, `1` a case failed, errored, was skipped or Hue's checks were
415
+ still pending at `--wait`, `2` usage or configuration error (including a missing key), `130`
416
+ interrupted. On Node.js 22, load TypeScript adapters with `NODE_OPTIONS=--experimental-strip-types`;
417
+ non-erasable syntax (enums, parameter properties, namespaces) needs a loader such as `--import tsx`
418
+ on any Node.js version.
419
+
420
+ ## Evaluate a document eval set
421
+
422
+ Eval sets whose cases pin no simulated world — a task plus pinned input files, answered with
423
+ generated documents — run as **direct** cases through `runExperiment()`. `hue eval` detects this
424
+ from the saved version (`--mode direct|simulation` overrides the detection; `--scenario` is always
425
+ a simulation). `--set` accepts the eval set's slug, name, ID or URL; `--set-version <n>` pins a
426
+ saved version other than the latest; `--scorer <slug|name|id>` pins an evaluator at its newest
427
+ published version, beside or instead of explicit `--scorer-version` IDs.
428
+
429
+ ```sh
430
+ hue eval --set gia-d1-citation --scorer gia-d1-citation \
431
+ --command "pnpm --filter @august/frontend run hue:gia-agent" \
432
+ --revision prompt-v10 --wait 1800 --json --env-file .env.hue
433
+ hue eval --set gia-d1-citation --set-version 1 --scorer gia-d1-citation ./hue-agent.ts --baseline <experiment id>
434
+ ```
435
+
436
+ For each case the command is spawned once **inside a private case directory** with
437
+ `HUE_CASE_DIR`, `HUE_CASE_INPUTS`, `HUE_CASE_OUTPUT_DIR`, `HUE_CASE_ID`, `HUE_CASE_KEY` and
438
+ `HUE_EXECUTION_ID` in its environment (`{"inputs","config"}` is also written to stdin):
439
+
440
+ ```text
441
+ <case dir>/inputs.json the case inputs (for example {"query": "...", "tipo_diligencia": "Virtual"})
442
+ <case dir>/case.json id, external key, execution id, run config, staged file list
443
+ <case dir>/files/<role>/<name> verified copies of the agent-visible pinned files
444
+ <case dir>/output/ write the generated documents here
445
+ ```
446
+
447
+ Every regular file the command leaves under `output/` is uploaded as a generated document
448
+ (accepted: `.pdf .docx .pptx .xlsx .json .txt .csv .png .jpg .jpeg .webp`; another extension or an
449
+ empty file is the case's error). Optional helpers: `manifest.json` (`{"primary": "<filename>",
450
+ "output": <json>}`) names the primary document and the JSON output; `result.json` is the JSON
451
+ output; `summary.txt` or `summary.md` is recorded as `{"summary": "..."}`. When none is written,
452
+ the command's stdout is the output (JSON when it parses). A single generated file is the primary
453
+ document by default. Model and provider credentials stay in the command's own environment; the
454
+ scoped case files are copies under `--checkpoint-dir` (default `.hue/eval/<agent-key>/<project>/direct/<experiment>`).
455
+
456
+ An adapter file works too: it is called with `(inputs, context)` where `context.mode` is
457
+ `"direct"` and `context` carries `config`, `item`, `executionId`, `files` (agent-visible pinned
458
+ files on disk), `outputDirectory` and `signal`. Return a JSON value, or `withFiles(output, files)`
459
+ from `@hue-run/sdk/evals` to attach generated documents.
460
+
461
+ Code evaluators pinned to the run are **not** executed on your machine: the CLI leaves them
462
+ deferred (`deferUnboundLocalScorers`) and Hue's grading executor scores the uploaded documents;
463
+ evaluator-only pinned files such as a legal corpus are never downloaded. `Waiting for Hue
464
+ checks...` then covers that grading, so size `--wait` to the evaluator's runtime. The
465
+ `--json` document gains `"mode": "direct"` and `"deferredScorerVersionIds"`. Exit codes, `--baseline`
466
+ and checkpoints behave as for Scenarios.
package/ENVIRONMENTS.md CHANGED
@@ -10,7 +10,7 @@ Your agent runs in your process while a disposable simulated world runs in Hue.
10
10
  authoritative and records an ordered journal; Hue does not execute your agent code or provider
11
11
  credentials.
12
12
 
13
- ## Run a scenario like a test
13
+ ## Run a definition like a test
14
14
 
15
15
  `runSimulation` is the one-shot developer path. It calls your existing callback directly, so
16
16
  IDE breakpoints and cooperative cancellation work. It does not register a worker, poll for jobs,
@@ -29,7 +29,7 @@ try {
29
29
  client: createEvaluationClient(connection),
30
30
  environmentClient: createEnvironmentClient(connection),
31
31
  hue,
32
- checkpointDirectory: ".hue-checkpoints/refund-scenario",
32
+ checkpointDirectory: ".hue-checkpoints/refund-definition",
33
33
  definition: { kind: "experiment", experimentId: process.env.HUE_EXPERIMENT_ID! },
34
34
  persistResultContent: true,
35
35
  traceEvidence: { mode: "required" },
@@ -60,6 +60,27 @@ identifies the same world for adapter control operations such as
60
60
  `environmentClient.recordCoverageGap`; it is not a credential. The MCP token is delivered only
61
61
  to the callback and is never written to checkpoints.
62
62
 
63
+ ### Published pins and verdicts
64
+
65
+ `scenario: { kind: "pins", datasetVersionId, scorerVersionIds, config?, name? }` runs already
66
+ published immutable pins, such as a Scenario's frozen case and Hue-owned outcome checks or a saved
67
+ eval set with explicitly chosen scorer versions. `resolveScenarioPins(client, selector)` reads
68
+ those pins from a Scenario ID, its Hue URL or its name (`listScenarios` and `getScenario` expose
69
+ the underlying reads; a Read and write key is required), and `resolveEvalSetPins`
70
+ resolves an eval set to its latest saved version. `runSimulation` creates the experiment directly
71
+ (`name` defaults to the dataset name, `config` to `{}`) and binds the pins and configuration into
72
+ the checkpoint identity, so resuming with different pins is refused like the other kinds.
73
+ Hue-executed pins such as `world_outcome` need no local callback; pass `localScorers` only for
74
+ bound `local_code` pins.
75
+
76
+ Hue grades `world_outcome` pins after the world seals, so the runner's report precedes the
77
+ verdicts. `waitForResults(client, { runId, scorerVersionIds, subjectIds, timeoutMillis })` polls
78
+ until every item has a terminal result for every pin or the budget elapses (`complete: false`);
79
+ `summarizeVerdicts` turns results into per-case rows with `passed` and totals; `compareVerdicts`
80
+ diffs two summaries by case key; and `collectExperimentVerdicts` combines those reads for one
81
+ experiment. The `hue eval` command uses the same path; see
82
+ [Evaluate an agent against a Scenario](CLI.md#evaluate-an-agent-against-a-scenario).
83
+
63
84
  ### Pinned provider-profile preflight
64
85
 
65
86
  An experiment with an immutable `attemptBaselineV2` can require the local process to describe
@@ -84,10 +105,10 @@ facade acceptance remains a post-publication Fern integration gate. No public SD
84
105
  official Gmail service, and passing these tests is not evidence of universal Gmail or Slack
85
106
  parity. A matching Hue deployment and verified provider profile remain required.
86
107
 
87
- ## Repository-authored scenarios
108
+ ## Repository-authored definitions
88
109
 
89
- Repository scenarios publish through the same validated environment, dataset, scorer and
90
- experiment APIs as Hue-authored scenarios. Stable slugs reuse matching immutable content
110
+ Repository definitions publish through the same validated environment, dataset, scorer and
111
+ experiment APIs as definitions authored in Hue. Stable slugs reuse matching immutable content
91
112
  digests; changed definitions, tasks or scorers publish new versions. Hue does not synchronize
92
113
  files back from its UI, and the helper refuses an unrelated mutable dataset draft instead of
93
114
  overwriting it.
@@ -106,7 +127,7 @@ the same before comparing immutable digests, so casing-only UUID changes reuse t
106
127
  version without dropping provider bindings.
107
128
 
108
129
  ```ts
109
- const scenario = {
130
+ const definition = {
110
131
  kind: "repository" as const,
111
132
  name: "Refund an eligible charge",
112
133
  slug: "refund-eligible-charge",
@@ -130,8 +151,8 @@ await runSimulation({
130
151
  client,
131
152
  environmentClient,
132
153
  hue,
133
- checkpointDirectory: ".hue-checkpoints/refund-scenario",
134
- scenario,
154
+ checkpointDirectory: ".hue-checkpoints/refund-definition",
155
+ definition,
135
156
  persistResultContent: false,
136
157
  traceEvidence: { mode: "required" },
137
158
  target: (inputs, context) => runMyExistingAgent({ inputs, ...context }),
package/EVALUATIONS.md CHANGED
@@ -9,7 +9,7 @@ npm install @hue-run/sdk zod
9
9
 
10
10
  The SDK executes targets and scorers on your machine. Hue stores pinned definitions, experiment progress and results. It does not execute uploaded source code. Follow the [installation guide](https://docs.hue.run/installation) to add `@hue-run/sdk` to your application.
11
11
 
12
- Create a **Tracing and evaluations** project service key under **Settings → Integrations & API keys** and expose it to this server-side process as `HUE_API_KEY`. A **Tracing only** key cannot author datasets or evaluation runs.
12
+ Create a **Read and write** project service key under **Settings → Integrations & API keys** and expose it to this server-side process as `HUE_API_KEY`. A **Tracing only** key cannot author datasets or evaluation runs.
13
13
 
14
14
  ```ts
15
15
  import { randomUUID } from "node:crypto";
@@ -72,10 +72,20 @@ try {
72
72
 
73
73
  Create another experiment with the same frozen version and different `config` to compare configurations. The runner reads the exact experiment case/version and scorer definitions; it never resolves a mutable latest version. `rescore` accepts an existing evaluation-run ID and has no target callback. Subject IDs refer to immutable saved outputs and trace evidence.
74
74
 
75
+ `rescore` preserves terminal scores already recorded for each item and evaluator version,
76
+ including built-in checks scheduled by Hue's **Grade again** flow. It computes only the missing
77
+ local scores and reports the existing receipts alongside the new ones. If another executor
78
+ finishes the same score while this one uploads, the runner accepts that exact item/version
79
+ receipt; an unrelated conflict still fails. To evaluate the same saved output again, create a new
80
+ scoring run.
81
+
75
82
  For the shorter agent-against-a-hosted-world workflow, use `runSimulation`. It owns immutable
76
83
  resolution, a fresh linked world per case, local and hosted MCP tools, finalization, sealed
77
84
  evidence and scoring while retaining this runner's checkpoint guarantees. See
78
- [Simulated environments](ENVIRONMENTS.md#run-a-scenario-like-a-test).
85
+ [Simulated environments](ENVIRONMENTS.md#run-a-definition-like-a-test).
86
+
87
+ Cases whose inputs are a task plus pinned documents, and rescoring over documents a run already
88
+ saved, use the same runner without a world. See [Direct cases and files](#direct-cases-and-files).
79
89
 
80
90
  ## Content and result states
81
91
 
@@ -110,7 +120,7 @@ const published = await client.publishScorerVersion(identity.id, local.definitio
110
120
  // Pin published.id in the experiment and pass scorers:[local] to the runner.
111
121
  ```
112
122
 
113
- A callback receives `{inputs,hasOutput,output?,hasExpected,expected?,metadata,executionState}` and returns one of:
123
+ A callback receives `{inputs,hasOutput,output?,hasExpected,expected?,metadata,executionState,files?}` and returns one of:
114
124
 
115
125
  - `{state:"scored",metrics:[{name,value,passed?}],explanation?,evidence?}` (explanation or evidence required).
116
126
  - `{state:"error",error:{type,message?}}`.
@@ -118,6 +128,13 @@ A callback receives `{inputs,hasOutput,output?,hasExpected,expected?,metadata,ex
118
128
 
119
129
  All declared metrics must appear exactly once and satisfy pinned types, bounds and categories. The binding must match language, entrypoint, SHA-256 source digest and metric definitions. The digest is an authenticated caller declaration; it does not attest closures, dependency versions or actual execution. Callback source is never downloaded or evaluated. Callbacks are trusted local code; they have **no execution timeout or side-effect cancellation**. Concurrency limits active cases to 1–16 (default 1), with scorers evaluated sequentially within each case.
120
130
 
131
+ `files` is present only when the runner handled files for that execution: every pinned input file
132
+ (all roles, including the evaluator-only `org_template`) and every generated output, each with its
133
+ verified local `path`, `role`, `filename`, `contentType`, `byteSize`, `sha256`, `artifactId` and
134
+ `primary` flag. A `local_code` callback runs when generated files exist even without a JSON output;
135
+ built-ins keep their usual missing-output behavior. See
136
+ [Direct cases and files](#direct-cases-and-files).
137
+
121
138
  ### Hosted and manual scorer pins
122
139
 
123
140
  Scorer deferral shipped in TypeScript `0.3.1`: the local runner executes only the three known built-in entries and bound `local_code` scorers. It leaves every other pin pending and reports its ID in `deferredScorerVersionIds`, including kinds and built-in entries introduced by a newer server. It never uploads a placeholder result that would occupy the immutable result slot, including placeholders already saved in an older SDK's checkpoint. Direct `scoreLocally()` calls reject pins that require another executor.
@@ -156,7 +173,7 @@ The runner stops scheduling more cases after an operational failure and waits fo
156
173
 
157
174
  ## Verification boundaries
158
175
 
159
- `scripts/verify-package.mjs` installs a real packed tarball outside the monorepo and runs HTTP contract tests against a synthetic service plus actual OpenTelemetry exporters. It checks two configurations, rescoring without target invocation, absent/null output, upload resume, uncertain execution, exclusive checkpoints, source/metric contracts, content policy and terminating schema workers. It also exercises the local worker's ready, incomplete and uncertain provider-attempt control-plane paths, but does not call an issued provider facade. `scripts/verify-evaluation-api.mjs` is a separate opt-in acceptance against a real Hue receiver/API; it creates synthetic datasets/scorers/experiments in the project associated with the supplied development key.
176
+ `scripts/verify-package.mjs` installs a real packed tarball outside the monorepo and runs HTTP contract tests against a synthetic service plus actual OpenTelemetry exporters. It checks two configurations, rescoring without target invocation, absent/null output, upload resume, uncertain execution, exclusive checkpoints, source/metric contracts, content policy and terminating schema workers. It also exercises the local worker's ready, incomplete and uncertain provider-attempt control-plane paths, but does not call an issued provider facade. `scripts/verify-file-cases.mjs` runs against the same installed consumer: pinned input download and verification, artifact publication of a generated document, file-aware local scoring during the run and again on a later rescore, and a declared file the target could not deliver recorded as the target's own error. `scripts/verify-evaluation-api.mjs` is a separate opt-in acceptance against a real Hue receiver/API; it creates synthetic datasets/scorers/experiments in the project associated with the supplied development key.
160
177
 
161
178
  ## Outbound local agent worker
162
179
 
@@ -168,7 +185,9 @@ establish hosted provider availability.
168
185
  `runLocalAgent` registers one fixed application callback and polls for queued runs. Hue selects
169
186
  the registered key/revision; it does not send executable code or shell commands. Keep the
170
187
  checkpoint directory private and durable. The worker persists result content and requires
171
- acknowledged trace and sealed environment evidence.
188
+ acknowledged trace evidence; a case pinned to a world also requires sealed environment evidence.
189
+ A case without a world runs through `directTarget` instead, described in
190
+ [Direct cases and files](#direct-cases-and-files).
172
191
 
173
192
  ```ts
174
193
  import { createHue } from "@hue-run/sdk";
@@ -218,3 +237,105 @@ the worker reports `attention` and stops; operator investigation is required. Su
218
237
  automatically reclaimed, and presenting the same uncertain checkpoint again cannot replay the
219
238
  candidate. Public package acceptance proves this lifecycle against local fixtures; exact
220
239
  installed-registry-package to hosted-facade acceptance remains a post-publication Fern gate.
240
+
241
+ ## Direct cases and files
242
+
243
+ Cases without a simulated world — document workflows whose inputs are a task plus pinned files —
244
+ run on the same runner under the same checkpoint rules. This is unreleased and ships in the next
245
+ `@hue-run/sdk` release. It requires a Hue deployment that serves case `inputFiles` on experiment
246
+ items, subject `files`, and the artifact reservation, upload, completion and download APIs. The
247
+ Python SDK has no equivalent.
248
+
249
+ On the outbound worker, supply `directTarget` beside or instead of `target`. Supplying
250
+ `directTarget` registers the `direct:v1` capability and supplying `target` registers
251
+ `environment:v1` (both strings are exported as `localAgentCapabilities`). Declare the file
252
+ capabilities the agent accepts and returns — `input:docx`, `input:pdf`, `output:docx` and so on — so
253
+ Hue matches them against each case's `hue.requiredCapabilities` metadata and offers only matching
254
+ cases. `registeredCapabilities` refuses a registration that names a capability without its callback.
255
+
256
+ ```ts
257
+ import { basename } from "node:path";
258
+ import { createEnvironmentClient } from "@hue-run/sdk/environment";
259
+ import { createEvaluationClient, runLocalAgent, withFiles } from "@hue-run/sdk/evals";
260
+ import { runMyAgent } from "./document-agent.js"; // Your existing application entry point.
261
+ import { letterGrader } from "./letter-grader.js"; // A defineLocalScorer binding.
262
+
263
+ await runLocalAgent({
264
+ client: createEvaluationClient(connection),
265
+ environmentClient: createEnvironmentClient(connection),
266
+ hue,
267
+ checkpointDirectory: ".hue-checkpoints/document-worker",
268
+ agent: {
269
+ key: "letter-agent",
270
+ name: "Letter agent",
271
+ revision: process.env.GIT_COMMIT!,
272
+ capabilities: ["input:docx", "input:pdf", "output:docx"], // direct:v1 is added for directTarget
273
+ },
274
+ scorers: [letterGrader], // Its callback reads context.files.
275
+ directTarget: async (inputs, { config, files, outputDirectory }) => {
276
+ // `files` are verified copies of the case's agent-visible input files on disk.
277
+ const { summary, documents } = await runMyAgent({ inputs, config, files, outputDirectory });
278
+ return withFiles(
279
+ { summary },
280
+ documents.map((path, index) => ({
281
+ path,
282
+ filename: basename(path),
283
+ contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
284
+ primary: index === 0,
285
+ })),
286
+ );
287
+ },
288
+ });
289
+ ```
290
+
291
+ The direct callback receives cloned inputs and an allowlisted context of `config`,
292
+ `item: {id, externalKey}`, `executionId`, `trace: {traceId,spanId}`, `files` and `outputDirectory`,
293
+ plus the worker's `signal` when one was supplied. Expected outcomes and case metadata stay with
294
+ grading, as they do for world cases.
295
+
296
+ Before an execution exists, `runExperiment` downloads every pinned input file named by the frozen
297
+ case's `inputFiles` and verifies byte count and SHA-256. A download failure is an SDK failure and
298
+ consumes no execution slot; a file already saved with the pinned identity is reused instead of
299
+ downloaded again. The target sees only the agent-visible roles — `source`, `attached_template`,
300
+ `attached_reference` and `original` — as `context.files`; evaluator-only
301
+ `org_template` and `evaluator_reference` files (an organization's template, a legal corpus, an
302
+ answer key) reach scorers but not the agent, and are not even downloaded when no bound code
303
+ evaluator runs in this process. Verified copies live under `filesDirectory` (default
304
+ `<checkpointDirectory>/files`, created mode 0700), and each case gets its own private
305
+ `context.outputDirectory` to write into.
306
+
307
+ Return generated documents by wrapping the output in `withFiles(output, files)`. Each entry names a
308
+ `path` or in-memory `bytes`, a `filename`, a Hue-accepted `contentType` and at most one `primary`.
309
+ The runner copies them next to its checkpoint, publishes them through the artifact reservation,
310
+ upload and verified completion APIs with stable per-execution keys, and completes the execution with
311
+ `artifactIds` and `primaryArtifactId`, so the subject's frozen manifest holds inputs and outputs
312
+ together. Generated files are always uploaded regardless of `persistResultContent`: they are the
313
+ execution's evidence. A declared file that cannot be read, exceeds 25 MiB, repeats a filename or has
314
+ an unsupported content type is saved as the target's error (`TargetError`), not as an uncertain
315
+ execution. A crash after the target finished resumes from the staged files without invoking the
316
+ target again; if result content is not persisted, the JSON output cannot be reconstructed and the
317
+ case is reported as uncertain.
318
+
319
+ `rescore` downloads a subject's frozen `files` — the pinned inputs and the generated outputs, with
320
+ `role: "output"` for the documents a run produced — so a code evaluator can grade saved documents
321
+ without invoking an agent. Local scorers receive them as `context.files` exactly as they do during
322
+ an experiment.
323
+
324
+ `runExperiment` and `rescore` accept `environmentEvidence: "required" | "when_pinned"`. A connected
325
+ worker that supplies `directTarget` uses `when_pinned`: direct cases never contact the environment
326
+ evidence endpoint, while cases pinned to a world still require sealed evidence. Use the same policy
327
+ for file-only regrading, and keep `required` for a generic target that attaches a world
328
+ independently of the case pin.
329
+
330
+ ### Grading on Hue's side
331
+
332
+ By default a pinned `local_code` version without a matching local scorer refuses the run: nobody
333
+ would produce its result. `deferUnboundLocalScorers: true` changes that for both `runExperiment`
334
+ and `rescore`: unbound code-evaluator pins are reported in `deferredScorerVersionIds` and left to
335
+ the executor that owns their source, typically a grading worker Hue operates with its own provider
336
+ credentials. The customer's process then runs only the agent and uploads its documents; nothing
337
+ grader-related is installed there and evaluator-only files never reach it. The grading worker
338
+ calls `rescore` on the same run with the evaluator bound and the same option, so pins belonging to
339
+ other evaluators are left alone rather than refused. `EvaluationClient.listEvaluationRuns` pages
340
+ the project's runs so such a worker can find the ones that still owe it results. `hue eval` uses
341
+ this mode for eval sets whose cases pin no simulated world.
package/README.md CHANGED
@@ -74,9 +74,15 @@ preflight checks availability and presents the published [privacy notice](https:
74
74
  and [security information](https://trust.hue.run/) before telemetry. Anonymous trials last 24 hours
75
75
  and are limited to 100 traces, 1,000 spans and 2 MiB. A private owner-only browser handoff supports
76
76
  account linkage; the original request evidence is retained and business work is never replayed on
77
- claim. Setup never enables content capture or creates a Scenario, Hue Run, evaluation, source capture
77
+ claim. Setup never enables content capture or creates a simulation, Hue Run, evaluation, source capture
78
78
  or remote execution. See the [setup CLI contract](./CLI.md) for the supported shapes and release gates.
79
79
 
80
+ For an existing account, `hue login` validates keys created in Hue Settings and stores them in
81
+ `.env.hue` without printing them, and `hue mcp install --client claude-code` (or `cursor`, `codex`,
82
+ `vscode`, `windsurf`, `gemini`) writes the Hue MCP configuration that references `HUE_MCP_KEY`. See
83
+ [Sign in and store keys](./CLI.md#sign-in-and-store-keys) and
84
+ [Install the MCP for your coding agent](./CLI.md#install-the-mcp-for-your-coding-agent).
85
+
80
86
  `checkConnection()` rejects with `HueConnectionError`: its fixed message is safe to log, `status`
81
87
  carries the HTTP status when Hue answered, and `cause` carries the underlying network, timeout or
82
88
  parsing error. `serviceVersion` and `resourceAttributes` (for example
@@ -423,13 +429,32 @@ an uncertain preparation. See the
423
429
  an issued facade endpoint or the official Gmail service, and do not claim universal provider
424
430
  parity.
425
431
 
432
+ An unreleased `directTarget` callback extends the same worker to cases without a world: the runner
433
+ verifies the case's pinned input files, hands them to the agent, and uploads the documents it
434
+ returns as verified Hue artifacts for scoring. See
435
+ [direct cases and files](EVALUATIONS.md#direct-cases-and-files).
436
+
426
437
  Scorer deferral shipped in TypeScript `0.3.1`. Only built-ins
427
438
  and bound `local_code` callbacks run here; other pins remain pending for their authorized executor.
428
439
  See [scorer execution](EVALUATIONS.md#hosted-and-manual-scorer-pins).
429
440
 
430
441
  The published [setup CLI](CLI.md) is a resumable local inspection core; the unreleased `0.4.0`
431
442
  candidate adds the bounded application onboarding flow described above. Existing customers connect
432
- their agents with `runLocalAgent()`; setup does not register workers or launch Scenarios.
443
+ their agents with `runLocalAgent()`; setup does not register workers or launch simulations.
444
+
445
+ ### Command-line evaluation
446
+
447
+ The unreleased `hue eval` command wraps `runSimulation()` and `runLocalAgent()` for an adapter
448
+ file or a shell command: `hue eval --scenario "<name>" ./hue-agent.ts` creates a fresh experiment
449
+ from a published Scenario's immutable pins, runs the agent in one isolated world per case, waits
450
+ for Hue's outcome checks and prints the run URL and per-case PASS/FAIL verdicts with an exit code;
451
+ `--worker` registers the same adapter for runs launched from Hue. It needs a Read and
452
+ write key in `HUE_API_KEY` (never printed) and keeps content capture off unless `--content`
453
+ is passed. See [Evaluate an agent against a Scenario](CLI.md#evaluate-an-agent-against-a-scenario).
454
+ Eval sets whose cases pin files instead of a world run as direct cases through `runExperiment()`:
455
+ `hue eval --set <slug> --scorer <slug> --command "…"` hands the agent each case's pinned files in a
456
+ private directory, uploads the documents it writes and waits for Hue's grading executor to score
457
+ them. See [Evaluate a document eval set](CLI.md#evaluate-a-document-eval-set).
433
458
 
434
459
  ## Managed targets
435
460
 
@@ -0,0 +1,47 @@
1
+ import type { LocalFile, TargetResult } from "../evals/types.js";
2
+ import type { JsonValue } from "../types.js";
3
+ /**
4
+ * Direct (file) cases for `hue eval`: a case is a task plus pinned input files, and the agent's
5
+ * answer is one or more generated documents. The agent command receives a private case directory
6
+ * instead of stdin/stdout JSON, the same layout Hue's document workers use:
7
+ *
8
+ * <case dir>/inputs.json the case inputs
9
+ * <case dir>/case.json id, external key, run config and the staged file list
10
+ * <case dir>/files/<role>/<name> verified copies of the agent-visible pinned files
11
+ * <case dir>/output/ where the command writes what it produced
12
+ *
13
+ * Everything under `output/` becomes the execution's generated files. Optional helpers there:
14
+ * `manifest.json` (`{ "primary": "<filename>", "output": <json> }`), `result.json` (the JSON
15
+ * output) and `summary.txt` / `summary.md` (recorded as `{ "summary": "…" }`).
16
+ */
17
+ export declare const directCaseEnvironment: readonly ["HUE_CASE_DIR", "HUE_CASE_INPUTS", "HUE_CASE_OUTPUT_DIR", "HUE_CASE_ID", "HUE_CASE_KEY", "HUE_EXECUTION_ID"];
18
+ /** Generated-file extensions Hue accepts and the content type recorded for each. */
19
+ export declare const outputExtensions: Readonly<Record<string, string>>;
20
+ export interface DirectCaseLayout {
21
+ caseDirectory: string;
22
+ inputsPath: string;
23
+ outputDirectory: string;
24
+ files: {
25
+ role: string;
26
+ filename: string;
27
+ path: string;
28
+ }[];
29
+ }
30
+ /** Writes the case directory: inputs, descriptor, verified file copies and an empty output folder. */
31
+ export declare function stageDirectCase(root: string, input: {
32
+ inputs: JsonValue;
33
+ config: JsonValue;
34
+ item: {
35
+ id: string;
36
+ externalKey: string;
37
+ };
38
+ executionId: string;
39
+ files: LocalFile[];
40
+ }): Promise<DirectCaseLayout>;
41
+ /**
42
+ * Turns the output folder into the target's result. Every regular file, in subdirectories too,
43
+ * becomes a generated file; an unsupported extension is the agent's error rather than a silently
44
+ * dropped document. `fallbackOutput` (for example the command's stdout) is used when no JSON
45
+ * output or summary file was written.
46
+ */
47
+ export declare function collectDirectOutputs(outputDirectory: string, fallbackOutput?: JsonValue): Promise<TargetResult>;