@hue-run/sdk 0.9.0 → 0.10.0

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.9.0`, `@opentelemetry/api@1.9.1` and
45
+ not a completed integration. TypeScript uses `@hue-run/sdk@0.10.0`, `@opentelemetry/api@1.9.1` and
46
46
  `@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
47
47
  Content capture requires an ordinary account-managed key and a later explicit application decision.
48
48
 
@@ -305,7 +305,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
305
305
  # Set project to an existing supported fixture; use the same directory on resume.
306
306
  project=/absolute/path/to/supported-fixture
307
307
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
308
- --archive .artifacts/typescript/hue-run-sdk-0.9.0.tgz \
308
+ --archive .artifacts/typescript/hue-run-sdk-0.10.0.tgz \
309
309
  --origin https://STAGING_ORIGIN \
310
310
  --project "$project" --command setup \
311
311
  --evidence .context/setup-staging-before-claim.json
@@ -316,7 +316,7 @@ the private local handoff and finish the real browser claim, then reconcile the
316
316
 
317
317
  ```sh
318
318
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
319
- --archive .artifacts/typescript/hue-run-sdk-0.9.0.tgz \
319
+ --archive .artifacts/typescript/hue-run-sdk-0.10.0.tgz \
320
320
  --origin https://STAGING_ORIGIN \
321
321
  --project "$project" --command claim \
322
322
  --evidence .context/setup-staging-after-claim.json
@@ -351,8 +351,11 @@ the CLI never prints it. The optional `zod` peer of `@hue-run/sdk/evals` must be
351
351
  Write an adapter module that hands the case inputs and the world's tools or MCP connection to
352
352
  the real agent. The module exports `default` or `runMyAgent`; `context` is the SDK's
353
353
  `SimulationTargetContext` (`world`, `mcp`, `tools`, `config`, `item`, `executionId`,
354
- `environmentRunId`, `signal`; `world` carries the provider mirror URLs, the world token, `env` and
355
- `mcpConfig` where Hue's simulation gateway serves the world):
354
+ `environmentRunId`, `files`, `outputDirectory`, `signal`; `world` carries the provider mirror URLs,
355
+ the world token, `env` and `mcpConfig` where Hue's simulation gateway serves the world). When the
356
+ case also pins input files, `files` holds verified copies of its agent-visible ones (see
357
+ [Evaluate a document eval set](#evaluate-a-document-eval-set)); return
358
+ `withFiles(output, files)` from `@hue-run/sdk/evals` to upload documents the agent produced:
356
359
 
357
360
  ```ts
358
361
  // hue-agent.ts: erasable TypeScript only; Node.js 24 and Bun strip the types natively.
@@ -368,27 +371,35 @@ export default function runMyAgent(inputs: JsonValue, context: SimulationTargetC
368
371
  ```
369
372
 
370
373
  ```sh
371
- hue eval --case "Refund an eligible charge" ./hue-agent.ts --env-file .env.hue
372
- hue eval --case https://app.hue.run/projects/demo/scenarios/<id> ./hue-agent.ts --baseline <run id>
373
- hue eval --set "Billing regressions" --scorer-version <id> ./hue-agent.ts --save-version
374
+ hue eval --case "Refund an eligible charge" ./hue-agent.ts --content --env-file .env.hue
375
+ hue eval --case https://app.hue.run/projects/demo/scenarios/<id> ./hue-agent.ts --content --baseline <run id>
376
+ hue eval --set "Billing regressions" --scorer-version <id> ./hue-agent.ts --content --save-version
374
377
  hue eval --case "Refund an eligible charge" --command "python agent.py" --timeout 120 --content
375
- hue eval --worker ./hue-agent.ts --agent-key support-agent --env-file .env.hue
378
+ hue eval --worker ./hue-agent.ts --agent-key support-agent --content --env-file .env.hue
376
379
  ```
377
380
 
378
381
  `--scenario` remains an alias for `--case` for existing scripts. Pass one selection flag.
379
382
 
380
- The one-shot mode resolves the selection (`--case` by name, ID or URL; `--set` by name, ID or
381
- URL with explicit `--scorer-version` pins; or `--dataset-version` with `--scorer-version`),
383
+ The one-shot mode resolves the selection (`--case` by name, or by the ID or URL of the published
384
+ case or of the case conversion that published it, among the project's first 1,000 Scenarios;
385
+ `--set` by name, ID or URL with explicit `--scorer-version` pins; or `--dataset-version` with `--scorer-version`),
382
386
  creates a fresh run from those immutable pins named `<agent key> @ <revision>` (a commit hash is
383
387
  shortened to 7 characters)
384
388
  (`--name` overrides), prints `Run: <url>` and `Experiment: <id>` as soon as the experiment exists,
385
389
  one line per case event (world created, agent started, world sealed), then
386
390
  `Waiting for Hue checks...` and a table with one row per case: boolean metrics as `PASS`/`FAIL`,
387
391
  numbers as values, text and category metrics as-is, an overall result per case and a pass count.
388
- Failing cases print the scorer explanation. `--baseline <experiment id|url>` adds improvement,
392
+ Failing cases print the scorer explanation. An evaluator Hue records as not applicable to a case
393
+ (its outcome scoring has no outcome criteria or conversion rubric to grade there) shows `n/a` in
394
+ that case's columns and neither passes nor fails it, so in a mixed eval set a case is decided by
395
+ the evaluators that apply to it; the pass line counts the not-applicable results. A case that no
396
+ pinned evaluator applies to is an error that names what they need. `--baseline <experiment id|url>` adds improvement,
389
397
  regression and unchanged counts with per-case deltas. `--json` prints one JSON document
390
398
  (`experimentId`, `runId`, `runUrl`, `complete`, `cases`, `totals`, optional `baseline`) on stdout
391
- and sends progress to stderr. `--wait <seconds>` (default 300) bounds the verdict wait because
399
+ and sends progress to stderr; each case lists its not-applicable evaluator versions in
400
+ `notApplicable`, and `totals.notApplicable` counts them; a case failed for its telemetry has `state: "error"`,
401
+ `passed: false` and
402
+ `telemetry: { code: "telemetry_not_accepted", issues: [{ signal, kind, status?, count }] }`. `--wait <seconds>` (default 300) bounds the verdict wait because
392
403
  Hue-owned `world_outcome` checks are graded after the world seals. An experiment always covers
393
404
  every case of the saved version; there is no case subset.
394
405
 
@@ -399,35 +410,83 @@ every case of the saved version; there is no case subset.
399
410
  `HUE_EXECUTION_ID`, `HUE_ENVIRONMENT_RUN_ID`, `HUE_CASE_ID` and `HUE_CASE_KEY`, and
400
411
  `{"inputs": ..., "config": ...}` on stdin. The child does not receive `HUE_API_KEY`, `HUE_MCP_KEY`
401
412
  or any other Hue control-plane credential unless `--allow-hue-credentials` is passed; the rest of
402
- the parent environment (model keys, application settings) is inherited. Its stdout is the answer
413
+ the parent environment (model keys, application settings) is inherited. `HUE_CASE_DIR`,
414
+ `HUE_CASE_INPUTS` and `HUE_CASE_OUTPUT_DIR` name a private case directory with the layout direct
415
+ cases use: the case inputs, verified copies of the case's agent-visible pinned files under
416
+ `files/<role>/`, and `output/`, whose files are uploaded after the world seals (the command still
417
+ starts in the current directory). The case directory never holds the world token and is removed
418
+ after the case. Its stdout is the answer
403
419
  (JSON when it parses, otherwise trimmed text; empty means no output); a non-zero exit or the
404
- per-case `--timeout` (default 600 seconds) is a target failure. The world token is never logged.
420
+ per-case `--timeout` (default 600 seconds) is a target failure. A timed-out or interrupted command
421
+ is stopped as a whole process group on macOS and Linux: SIGTERM, then SIGKILL for anything still
422
+ running 5 seconds later, including an agent a compound command started after its shell exited;
423
+ the case settles only once nothing in the group is left. A command that exits normally but leaves
424
+ processes in its group has them stopped the same way before its answer and files are read. A
425
+ second Ctrl+C during that grace kills the group at once, removes the world case's files, the MCP
426
+ configuration file and the checkpoint locks, and exits with 130. The world token is never logged.
405
427
  A world created while the deployment's simulation gateway is off gets the legacy `hue_sim_`
406
428
  capability under the same `HUE_MCP_*` names. The agent key defaults to the slug of the command's
407
429
  script name; `--revision` is sent to Hue as the agent revision of every world.
408
430
 
409
431
  `--worker` registers the adapter through `runLocalAgent()` with key `--agent-key` (default: the
410
432
  adapter filename slug), name `--agent-name` (default: the key), revision `--revision` (default:
411
- `AGENT_REVISION`, then the Git `HEAD` short hash, then `dev`) and capability `environment:v1`,
412
- prints the registration and each claimed run with its URL, executes runs launched from Hue until
413
- Ctrl+C or `--max-runs <n>`, and prints the verdict table after each run. Selection flags do not
414
- apply; Hue chooses the pinned experiment. The worker exits 0 when it stops normally.
433
+ `AGENT_REVISION`, then the Git `HEAD` short hash, then `dev`) and capability `environment:v1`
434
+ plus any repeated `--capability <value>`, prints the registration and each claimed run with its
435
+ URL, executes runs launched from Hue until Ctrl+C or `--max-runs <n>`, and prints the verdict table
436
+ after each run. Selection flags do not apply; Hue chooses the pinned experiment. The worker exits 0
437
+ when it stops normally. Hue offers a case whose world comes with agent-visible files only to a
438
+ registration that declares `environment-files:v1` and `input:<extension>` for each file type, for
439
+ example `--capability environment-files:v1 --capability input:pdf`; the adapter or command then
440
+ receives the files as described above. Hue keeps each registered revision's capabilities fixed,
441
+ so declare new ones under a new `--revision`. A world pinned to provider profiles, a Gmail world
442
+ for example, needs a reviewed `attemptBaselineV2` (see
443
+ [Pinned provider-profile preflight](ENVIRONMENTS.md#pinned-provider-profile-preflight)); `--worker`
444
+ registers none, so put `"attemptBaselineV2"` in the run's configuration JSON when you launch the
445
+ run from Hue.
415
446
 
416
447
  A Scenario or eval set whose dataset version is not saved cannot back an experiment: the command
417
448
  exits 1 and asks for **Save eval-set version** in Hue or `--save-version`, which freezes that
418
449
  version at its current revision. Connection settings are `HUE_API_KEY` and `HUE_BASE_URL`
419
450
  (default `https://app.hue.run`), loaded from `--env-file <path>` (or its alias `--env-path`) first when given; `--origin`
420
- overrides the origin. Telemetry content capture stays off unless `--content` is passed; in
421
- one-shot mode it also decides whether case outputs, error messages and explanations are persisted
422
- to Hue. `--worker` always persists them, because a run launched from Hue is read on its run page:
423
- that is `runLocalAgent()`'s contract and `--content` does not change it. Trace evidence is required
424
- for every case.
451
+ overrides the origin. Telemetry content capture stays off unless `--content` is passed; the
452
+ examples pass it so the run's case spans carry content. Model and tool spans inside the agent come
453
+ only from the agent's own instrumentation. Case outputs, error messages and explanations are
454
+ stored in Hue whether or not `--content` is passed, so a case's answer can be graded and read on
455
+ its run page. A `--command`'s stdout is its answer unless it writes a result file, and is stored,
456
+ so it must not print credentials or debug logs there; before storing, `hue eval` replaces the
457
+ credentials it handed the case (the world token and MCP headers, a legacy MCP token, attempt
458
+ bearers) and every Hue control-plane credential in its environment with `[redacted]`, in the
459
+ answer, an adapter's thrown message and name, generated file names and the local checkpoint.
460
+ `--no-output` keeps outputs, error messages and explanations out of a one-shot run's stored
461
+ results; with `--content` the case span still carries the output. `--worker` always stores them, because a run launched from Hue is read
462
+ on its run page (that is `runLocalAgent()`'s contract), and refuses `--no-output`. An interrupted
463
+ one-shot run keeps the choice it started with, so one run never mixes stored and unstored outputs:
464
+ rerunning it with other `--no-output` or `--content` flags is refused with the flags it started
465
+ with. Resume a run that an earlier SDK started without `--content` by passing `--no-output`.
466
+
467
+ Files the agent writes to `output/` are uploaded as generated documents with or without
468
+ `--no-output`, and they are not redacted the way the answer is: `hue eval` replaces the same
469
+ credentials only in the UTF-8 `.txt`, `.csv` and `.json` documents it collects, while PDF, Office
470
+ and image documents, and files an adapter returns by `path`, are uploaded as written. A direct
471
+ case also keeps the agent's raw `output/` files on disk under its checkpoint directory
472
+ (`.hue/eval/<agent-key>/<project>/direct/<experiment>/files/`). Never write credentials to
473
+ `output/`.
474
+
475
+ Trace evidence is required for every case: when Hue does not accept a case's traces or logs, the
476
+ case is completed as failed (error `TelemetryNotAccepted`, evidence omitted as
477
+ `telemetry_not_accepted`, no output or generated files attached) instead of being left started,
478
+ and the run goes on. Stderr names the case as it completes, with the export
479
+ issue counts, for example
480
+ `[refund] telemetry not accepted, case failed: telemetry_not_accepted: traces failed 1 (HTTP 400)`;
481
+ the case counts as an error in the table and JSON whatever its scores, and the command exits 1.
482
+ Counts carry signals, kinds, HTTP statuses and record numbers only, never content or credentials.
425
483
  Resumable checkpoints live in `.hue/eval/<agent-key>/<project id>/` (a `.gitignore` is written
426
484
  inside `.hue/eval/`); `--checkpoint-dir` overrides the root. Rerunning the same selection resumes
427
485
  an interrupted run without invoking the agent again; a different selection is refused until the
428
486
  unfinished one is resumed or its directory is removed.
429
487
 
430
- Exit codes: `0` every case passed, `1` a case failed, errored, was skipped or Hue's checks were
488
+ Exit codes: `0` every case passed the evaluators that apply to it, `1` a case failed, errored
489
+ (including one no pinned evaluator applies to), was skipped or Hue's checks were
431
490
  still pending at `--wait`, `2` usage or configuration error (including a missing key), `130`
432
491
  interrupted. On Node.js 22, load TypeScript adapters with `NODE_OPTIONS=--experimental-strip-types`;
433
492
  non-erasable syntax (enums, parameter properties, namespaces) needs a loader such as `--import tsx`
@@ -445,8 +504,8 @@ published version, beside or instead of explicit `--scorer-version` IDs.
445
504
  ```sh
446
505
  hue eval --set gia-d1-citation --scorer gia-d1-citation \
447
506
  --command "pnpm --filter @august/frontend run hue:gia-agent" \
448
- --revision prompt-v10 --wait 1800 --json --env-file .env.hue
449
- hue eval --set gia-d1-citation --set-version 1 --scorer gia-d1-citation ./hue-agent.ts --baseline <experiment id>
507
+ --revision prompt-v10 --wait 1800 --content --json --env-file .env.hue
508
+ hue eval --set gia-d1-citation --set-version 1 --scorer gia-d1-citation ./hue-agent.ts --content --baseline <experiment id>
450
509
  ```
451
510
 
452
511
  For each case the command is spawned once **inside a private case directory** with
@@ -469,6 +528,17 @@ the command's stdout is the output (JSON when it parses). A single generated fil
469
528
  document by default. Model and provider credentials stay in the command's own environment; the
470
529
  scoped case files are copies under `--checkpoint-dir` (default `.hue/eval/<agent-key>/<project>/direct/<experiment>`).
471
530
 
531
+ The same collection serves world cases, and it does not trust paths the agent controls. `output/`
532
+ must be a real directory, and a helper name, when present, a regular file; a symlink, FIFO or
533
+ directory in either place is the case's error. Symlinks and other special files among the
534
+ documents are skipped, as are hidden and lock entries. Every file is opened without following a
535
+ final symlink or blocking, must be the regular file the listing saw and within its limit (4 MiB
536
+ for a helper, 25 MiB for a document, checked before reading), and its upload is staged from the
537
+ bytes read. A file or directory that vanished or changed while it was collected, more than 32
538
+ documents or more than 1024 entries is the case's error too. A hard link is an ordinary file and
539
+ is collected like one: an agent running as your user could copy the file just as well, so run an
540
+ agent that must not reach your files under another account or in a sandbox.
541
+
472
542
  An adapter file works too: it is called with `(inputs, context)` where `context.mode` is
473
543
  `"direct"` and `context` carries `config`, `item`, `executionId`, `files` (agent-visible pinned
474
544
  files on disk), `outputDirectory` and `signal`. Return a JSON value, or `withFiles(output, files)`
@@ -478,5 +548,6 @@ Code evaluators pinned to the run are **not** executed on your machine: the CLI
478
548
  deferred (`deferUnboundLocalScorers`) and Hue's grading executor scores the uploaded documents;
479
549
  evaluator-only pinned files such as a legal corpus are never downloaded. `Waiting for Hue
480
550
  checks...` then covers that grading, so size `--wait` to the evaluator's runtime. The
481
- `--json` document gains `"mode": "direct"` and `"deferredScorerVersionIds"`. Exit codes, `--baseline`
482
- and checkpoints behave as for Scenarios.
551
+ `--json` document gains `"mode": "direct"` and `"deferredScorerVersionIds"`. Exit codes, `--baseline`,
552
+ `--content`, `--no-output` and checkpoints behave as for Scenarios; generated documents are
553
+ uploaded with or without them.
package/ENVIRONMENTS.md CHANGED
@@ -62,7 +62,7 @@ to the callback and is never written to checkpoints.
62
62
 
63
63
  ### Published pins and verdicts
64
64
 
65
- `scenario: { kind: "pins", datasetVersionId, scorerVersionIds, config?, name? }` runs already
65
+ `definition: { kind: "pins", datasetVersionId, scorerVersionIds, config?, name? }` runs already
66
66
  published immutable pins, such as a Scenario's frozen case and Hue-owned outcome checks or a saved
67
67
  eval set with explicitly chosen scorer versions. `resolveScenarioPins(client, selector)` reads
68
68
  those pins from a Scenario ID, its Hue URL or its name (`listScenarios` and `getScenario` expose
@@ -76,9 +76,11 @@ bound `local_code` pins.
76
76
  Hue grades `world_outcome` pins after the world seals, so the runner's report precedes the
77
77
  verdicts. `waitForResults(client, { runId, scorerVersionIds, subjectIds, timeoutMillis })` polls
78
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
79
+ `summarizeVerdicts` turns results into per-case rows with `passed` and totals; a result Hue marks
80
+ `notApplicable` (the evaluator has no outcome criteria or conversion rubric to grade in that case)
81
+ neither passes nor fails its case and is listed in the row's `notApplicable`, and a case no pinned
82
+ evaluator applies to is an error. `compareVerdicts` diffs two summaries by case key, and
83
+ `collectExperimentVerdicts` combines those reads for one experiment. The `hue eval` command uses the same path; see
82
84
  [Evaluate an agent against a case](CLI.md#evaluate-an-agent-against-a-case).
83
85
 
84
86
  ### Pinned provider-profile preflight
@@ -223,6 +225,8 @@ its own Gmail MCP or REST client, the mirror URL, the world token where the Goog
223
225
  import {
224
226
  agentEnvironment,
225
227
  createEnvironmentClient,
228
+ HueEnvironmentError,
229
+ isTransientEnvironmentError,
226
230
  worldHandoff,
227
231
  writeMcpConfig,
228
232
  } from "@hue-run/sdk/environment";
@@ -252,12 +256,22 @@ try {
252
256
  );
253
257
  const sealDeadline = Date.now() + 30_000;
254
258
  let sealed = false;
255
- while (Date.now() < sealDeadline) {
256
- if ((await environmentClient.getRun(run.id)).status !== "open") {
257
- sealed = true;
258
- break;
259
+ while (!sealed && Date.now() < sealDeadline) {
260
+ let wait = 250;
261
+ try {
262
+ // The deadline also ends a read that hangs, with the client's retries inside it.
263
+ const state = await environmentClient.getRun(run.id, {
264
+ signal: AbortSignal.timeout(Math.max(0, sealDeadline - Date.now())),
265
+ });
266
+ sealed = state.status !== "open";
267
+ } catch (error) {
268
+ if (!isTransientEnvironmentError(error)) throw error; // a refusal such as 404 is final
269
+ wait = Math.max(wait, (error as HueEnvironmentError).retryAfterMs ?? 0);
259
270
  }
260
- await new Promise((resolve) => setTimeout(resolve, 250));
271
+ if (!sealed)
272
+ await new Promise((resolve) =>
273
+ setTimeout(resolve, Math.min(wait, Math.max(0, sealDeadline - Date.now()))),
274
+ );
261
275
  }
262
276
  if (!sealed) throw new Error("World was not sealed after its completion grace");
263
277
  }
@@ -272,6 +286,27 @@ world token, so an adapter that read `HUE_MCP_URL` and `HUE_MCP_TOKEN` keeps wor
272
286
  no Hue-native `tools` (Hue refuses them); a world created while the gateway is off keeps its tools
273
287
  and the `hue_sim_` capability and emits a one-time `DeprecationWarning`.
274
288
 
289
+ A case pinned to a world may also pin input files. Before the case's execution starts, the
290
+ helpers download each agent-visible file (`source`, `attached_template`, `attached_reference`,
291
+ `original`), verify its size and SHA-256 against the case manifest and hand the verified copies to
292
+ the callback as `context.files` beside `context.world`, in a private directory (mode 0700, files
293
+ 0600), with a private `context.outputDirectory`. Evaluator-only files (`org_template`,
294
+ `evaluator_reference`) never reach the agent; a local code evaluator's copies are checked before
295
+ the execution starts but saved only after the agent finished. Bytes that differ from the manifest raise
296
+ `CaseFileError` with code `case_file_mismatch`; a name that is not one safe file name (a path
297
+ separator, `.` or `..`, a C0 or C1 control character, a character Windows reserves such as `:` or
298
+ `?`, a Windows device name such as `CON`, a trailing dot or space, more than 200 bytes) raises
299
+ `case_file_name_refused`. Either stops the run before an
300
+ execution or world exists for the case. Return `withFiles(output, files)` to upload what the agent
301
+ produced: the files are published and linked to the execution as `artifactIds` and
302
+ `primaryArtifactId`, within the limits of direct cases (at most 32 files, 25 MiB each, the
303
+ accepted document types). The world token is never written into the case directory, which is
304
+ removed when the case ends; only staged outputs an interrupted upload resumes from are kept until
305
+ it does. `hue eval --command` receives the same files through
306
+ `HUE_CASE_DIR`, `HUE_CASE_INPUTS` and `HUE_CASE_OUTPUT_DIR`. A `runLocalAgent` worker is offered
307
+ such cases only when it declares `environment-files:v1` and `input:<extension>` for each file
308
+ type; see [EVALUATIONS.md](EVALUATIONS.md#direct-cases-and-files).
309
+
275
310
  A refusal such as 409 `simulation_gateway_required` exposes its validated server code as `HueEnvironmentError.diagnostic`.
276
311
 
277
312
  `agentEnvironment` removes Hue control-plane credentials from the child by default: `HUE_API_KEY`,
package/EVALUATIONS.md CHANGED
@@ -18,7 +18,8 @@ import { builtins, createEvaluationClient, runExperiment, rescore } from "@hue-r
18
18
 
19
19
  const connection = { apiKey: process.env.HUE_API_KEY! };
20
20
  const client = createEvaluationClient(connection);
21
- const hue = createHue({ ...connection, serviceName: "evaluation-demo", captureContent: false });
21
+ // captureContent: false sends metadata only.
22
+ const hue = createHue({ ...connection, serviceName: "evaluation-demo", captureContent: true });
22
23
 
23
24
  // Registry writes use optimistic revisions; they do not have automatic retries.
24
25
  const dataset = await client.createDataset({ name: "Greetings", slug: "greetings" });
@@ -115,7 +116,7 @@ Both choices are required and independent:
115
116
 
116
117
  Frozen dataset inputs/references already exist on Hue. Their presence is independent of these switches. No output is inferred from telemetry. JavaScript `undefined` means unavailable output; JSON `null`, false, zero and empty string remain present values. Historical subjects with unavailable output produce skipped results without executing a scorer callback. A failed quality metric remains `state:"scored"` with `passed:false`. Target errors and scorer errors remain separate. Target error types are generalized to `TargetError`; when content is enabled, bounded messages may be stored. Never put credentials in error messages, output, custom metric values, names or metadata.
117
118
 
118
- `traceEvidence:{mode:"required"}` waits for trace and log export acknowledgement after the root span ends. The explicit alternative `{mode:"omit",reason:"..."}` stores the omission reason and declared trace ID without a fake snapshot. It can complete despite an export failure; export diagnostics remain available on the Hue client. There is no automatic fallback to omission.
119
+ `traceEvidence:{mode:"required"}` waits for trace and log export acknowledgement after the root span ends. The explicit alternative `{mode:"omit",reason:"..."}` stores the omission reason and declared trace ID without a fake snapshot. It can complete despite an export failure; export diagnostics remain available on the Hue client. There is no automatic fallback to omission. When required telemetry is not accepted, the runner by default keeps the saved outcome, leaves the execution started and rejects (`traceNotAccepted: "stop"`). With `traceNotAccepted: "fail_case"` it instead completes that case as failed (a `TelemetryNotAccepted` error, the evidence omitted with a reason starting `telemetry_not_accepted`, and no output or generated files attached, so no scorer can pass it), calls `onTelemetryNotAccepted(entry)` once, as soon as it completes the case (a resumed call only lists it), lists it in `report.telemetryNotAccepted` with sanitized issue counts, and goes on; `hue eval` uses this. A scorer that grades only the world can still score such a case, so treat a listed case as failed whatever its results say, as `hue eval` does.
119
120
 
120
121
  ## Local scorers
121
122
 
@@ -192,7 +193,7 @@ The runner stops scheduling more cases after an operational failure and waits fo
192
193
 
193
194
  ## Verification boundaries
194
195
 
195
- `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.
196
+ `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, a declared file the target could not deliver recorded as the target's own error, and a worker world case that receives its agent-visible files with the world, uploads the returned document and refuses altered bytes as `case_file_mismatch`. `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.
196
197
 
197
198
  ## Outbound local agent worker
198
199
 
@@ -215,7 +216,8 @@ import { createEvaluationClient, runLocalAgent } from "@hue-run/sdk/evals";
215
216
  import { runMyAgent } from "./agent.js"; // Your existing application entry point.
216
217
 
217
218
  const connection = { apiKey: process.env.HUE_API_KEY! };
218
- const hue = createHue({ ...connection, serviceName: "local-worker", captureContent: false });
219
+ // captureContent: false sends metadata only.
220
+ const hue = createHue({ ...connection, serviceName: "local-worker", captureContent: true });
219
221
  try {
220
222
  await runLocalAgent({
221
223
  client: createEvaluationClient(connection),
@@ -233,7 +235,9 @@ try {
233
235
 
234
236
  The callback receives cloned inputs, local tools, and an allowlisted context containing
235
237
  `config`, `item: {id, externalKey}`, `executionId`, `environmentRunId`,
236
- `trace: {traceId,spanId}` and a short-lived `mcp` capability. Expected outcomes, case metadata
238
+ `trace: {traceId,spanId}`, a short-lived `mcp` capability, `files` (verified copies of the case's
239
+ agent-visible input files, empty when it has none) and a private `outputDirectory`; return
240
+ `withFiles(output, files)` to upload generated files, as for direct cases. Expected outcomes, case metadata
237
241
  and original source pins remain private to grading. Pass the tools or scoped MCP capability into
238
242
  the agent's actual tool boundary; their presence does not redirect provider calls. Capabilities
239
243
  are not written to checkpoints. `maxRuns` limits completed runs for one-shot workers, while
@@ -260,8 +264,8 @@ installed-registry-package to hosted-facade acceptance remains a post-publicatio
260
264
  ## Direct cases and files
261
265
 
262
266
  Cases without a simulated world — document workflows whose inputs are a task plus pinned files —
263
- run on the same runner under the same checkpoint rules. This is unreleased and ships in the next
264
- `@hue-run/sdk` release. It requires a Hue deployment that serves case `inputFiles` on experiment
267
+ run on the same runner under the same checkpoint rules. They shipped in `@hue-run/sdk` `0.5.0`
268
+ and require a Hue deployment that serves case `inputFiles` on experiment
265
269
  items, subject `files`, and the artifact reservation, upload, completion and download APIs. The
266
270
  Python SDK has no equivalent.
267
271
 
@@ -270,7 +274,14 @@ On the outbound worker, supply `directTarget` beside or instead of `target`. Sup
270
274
  `environment:v1` (both strings are exported as `localAgentCapabilities`). Declare the file
271
275
  capabilities the agent accepts and returns — `input:docx`, `input:pdf`, `output:docx` and so on — so
272
276
  Hue matches them against each case's `hue.requiredCapabilities` metadata and offers only matching
273
- cases. `registeredCapabilities` refuses a registration that names a capability without its callback.
277
+ cases. A case pinned to a world whose manifest also holds agent-visible files is offered only to a
278
+ registration that declares `environment-files:v1` (`localAgentCapabilities.environmentFiles`)
279
+ and `input:<extension>` for each file; `target` then receives them as `context.files` with the
280
+ world, as described in
281
+ [ENVIRONMENTS.md](ENVIRONMENTS.md#worlds-served-by-the-simulation-gateway). The worker never adds
282
+ `environment-files:v1` itself: Hue keeps each registered revision's capabilities fixed, so declare
283
+ it under a new revision. `registeredCapabilities` refuses a registration that names a capability
284
+ without its callback.
274
285
 
275
286
  ```ts
276
287
  import { basename } from "node:path";
@@ -312,16 +323,23 @@ The direct callback receives cloned inputs and an allowlisted context of `config
312
323
  plus the worker's `signal` when one was supplied. Expected outcomes and case metadata stay with
313
324
  grading, as they do for world cases.
314
325
 
315
- Before an execution exists, `runExperiment` downloads every pinned input file named by the frozen
326
+ Before an execution exists, `runExperiment` downloads the pinned input files named by the frozen
316
327
  case's `inputFiles` and verifies byte count and SHA-256. A download failure is an SDK failure and
317
- consumes no execution slot; a file already saved with the pinned identity is reused instead of
318
- downloaded again. The target sees only the agent-visible roles — `source`, `attached_template`,
319
- `attached_reference` and `original` — as `context.files`; evaluator-only
328
+ consumes no execution slot; bytes that differ from the manifest raise `CaseFileError` with the
329
+ stable code `case_file_mismatch`, and a download stops one byte past the pinned size. A file
330
+ already saved with the pinned identity is reused instead of downloaded again. The target sees only the agent-visible roles — `source`,
331
+ `attached_template`, `attached_reference` and `original` — as `context.files`; evaluator-only
320
332
  `org_template` and `evaluator_reference` files (an organization's template, a legal corpus, an
321
- answer key) reach scorers but not the agent, and are not even downloaded when no bound code
322
- evaluator runs in this process. Verified copies live under `filesDirectory` (default
323
- `<checkpointDirectory>/files`, created mode 0700), and each case gets its own private
324
- `context.outputDirectory` to write into.
333
+ answer key) reach scorers but not the agent. They are fetched only when a bound code evaluator
334
+ runs in this process: checked with the others before the execution starts but not kept, then
335
+ downloaded again and saved after the target finished, so they are not on disk while the agent
336
+ runs. An agent running as the same operating-system user
337
+ as the runner can still read whatever the runner can; run it under another account when local
338
+ evaluator material must stay out of its reach. Verified copies live under `filesDirectory` (default
339
+ `<checkpointDirectory>/files`, created mode 0700), evaluator-only files apart from the agent's, and
340
+ each case gets its own private `context.outputDirectory` to write into. The files of a case pinned
341
+ to a world are removed when it ends, except staged outputs an interrupted upload resumes from;
342
+ those of a direct case stay for inspection.
325
343
 
326
344
  Return generated documents by wrapping the output in `withFiles(output, files)`. Each entry names a
327
345
  `path` or in-memory `bytes`, a `filename`, a Hue-accepted `contentType` and at most one `primary`.
@@ -329,9 +347,11 @@ The runner copies them next to its checkpoint, publishes them through the artifa
329
347
  upload and verified completion APIs with stable per-execution keys, and completes the execution with
330
348
  `artifactIds` and `primaryArtifactId`, so the subject's frozen manifest holds inputs and outputs
331
349
  together. Generated files are always uploaded regardless of `persistResultContent`: they are the
332
- execution's evidence. A declared file that cannot be read, exceeds 25 MiB, repeats a filename or has
333
- an unsupported content type is saved as the target's error (`TargetError`), not as an uncertain
334
- execution. A crash after the target finished resumes from the staged files without invoking the
350
+ execution's evidence. A declared `path` is read once, as a regular file (a symlink, FIFO or device
351
+ is refused) whose size is checked before reading, and staged owner-only from those bytes. A
352
+ declared file that cannot be read, exceeds 25 MiB, repeats a filename or has an unsupported
353
+ content type is saved as the target's error (`TargetError`), not as an uncertain execution. A
354
+ files directory must be owned by the current user and closed to everyone else (mode 0700). A crash after the target finished resumes from the staged files without invoking the
335
355
  target again; if result content is not persisted, the JSON output cannot be reconstructed and the
336
356
  case is reported as uncertain.
337
357
 
package/README.md CHANGED
@@ -35,7 +35,7 @@ import { createHue, HueExportError } from "@hue-run/sdk";
35
35
  const hue = createHue({
36
36
  apiKey: process.env.HUE_API_KEY!, // a project service key, on the server only
37
37
  serviceName: "my-agent",
38
- captureContent: false, // required: explicitly choose true or false
38
+ captureContent: true, // required: true records content; false sends metadata only
39
39
  onExportIssue: (issue) => console.error(issue), // sanitized counts, never server bodies
40
40
  });
41
41
 
@@ -58,6 +58,11 @@ try {
58
58
  await hue.shutdown(); // flushes and releases providers owned by this client
59
59
  ```
60
60
 
61
+ `captureContent` is required. The example sets `true`, the recommended choice: trace inspection,
62
+ evaluations and judges read the prompts, responses and tool inputs/outputs it records. Choose
63
+ `false` for metadata only when a policy forbids sending that content; see
64
+ [Privacy and content](#privacy-and-content).
65
+
61
66
  The default destination is `https://app.hue.run`. Data goes to
62
67
  `/api/v1/otlp/v1/traces` and `/api/v1/otlp/v1/logs` with a Bearer project key.
63
68
  Set `baseUrl` only for another Hue deployment. It must be an origin without an API path; a trailing slash is accepted.
@@ -287,6 +292,12 @@ outputs and exports through Hue's attached processors keeps the label.
287
292
 
288
293
  ## Privacy and content
289
294
 
295
+ Set `captureContent: true` for full traces: prompts and messages, responses and tool inputs/outputs,
296
+ alongside model, usage, timing and errors. Trace inspection, evaluations and judges read that
297
+ content. Choose `false`, metadata-only mode, when your users decline or an application or data
298
+ policy forbids sending that content to another service. `redact` and the credential filtering
299
+ described below apply in both modes.
300
+
290
301
  `captureContent: false` disables manual input/output/messages/tool content and
291
302
  removes recognized GenAI, Vercel, OpenInference and OpenLLMetry content attributes,
292
303
  legacy GenAI content events, log bodies, status messages and exception text before
@@ -306,12 +317,18 @@ an OpenAI hosted MCP tool. Before export, and before `redact`, Hue replaces the
306
317
  credential-like fields including `authorization`, `authorization_token`, `headers`, `api_key`,
307
318
  `access_token`, `x-api-key`, and keys ending in `token`, `secret`, `password`, `apikey` or
308
319
  `credential` (case-insensitively, ignoring `-` and `_`) with `"[redacted]"` in recorded tool definitions
309
- (`gen_ai.tool.definitions`, `ai.prompt.tools`, `llm.tools.*.tool.json_schema`) and in the `tools`
310
- and `mcp_servers` entries of a raw provider request or response recorded as `input.value`,
311
- `output.value` or `llm.invocation_parameters`. Parameters named in a JSON Schema `properties`
320
+ (`gen_ai.tool.definitions`, `ai.prompt.tools`, `llm.tools.*.tool.json_schema`) and in the
321
+ top-level `tools` and `mcp_servers` entries of any JSON object recorded as `input.value`,
322
+ `output.value` or `llm.invocation_parameters`, whether or not it is a provider request; nothing
323
+ else in that object changes. Parameters named in a JSON Schema `properties`
312
324
  object keep their schemas, so a tool that takes a `headers` argument is still described. A
313
325
  definition nested more than 256 levels deep rejects its record. Sensitive `default`, `const`,
314
- `examples` and `enum` values under credential-named schema parameters are redacted too.
326
+ `examples` and `enum` values under credential-named schema parameters are redacted too. A `url`
327
+ or `server_url` field (named like the credential fields, so `serverUrl` too) that has userinfo, a
328
+ query or a fragment loses its userinfo and fragment, and each query value becomes `[redacted]`
329
+ under its original name; the result is serialized as WHATWG `URL` does (lowercase scheme and
330
+ host, IDN hosts in Punycode, default ports dropped, dot segments resolved). A URL with none of
331
+ them is exported as recorded, and a value `URL` cannot parse becomes `"[redacted]"`.
315
332
 
316
333
  With `captureContent: false`, export removes tool definitions but keeps a summary of them on the
317
334
  same record: `hue.tool.names` lists each definition's `name` (Chat Completions `function.name`,
@@ -324,12 +341,18 @@ rotates. Only definitions another integration recorded can be summarized: `hueTe
324
341
  inputs and exports through Hue's attached processors gets the summary.
325
342
  Recorded messages can inline files: GenAI `blob` parts in `gen_ai.input.messages` /
326
343
  `gen_ai.output.messages` (what the AI SDK 7 adapter records for a file part) and AI SDK 6 `file`
327
- parts in `ai.prompt.messages`. A span whose messages exceed 256 KiB would be rejected, so before
328
- export Hue replaces the `content`/`data` of any such part longer than 64 KiB with the file's
329
- `sha256` (of the decoded bytes for base64 and `data:` URLs, of the UTF-8 text otherwise) and
330
- `size`, keeping the part's other fields such as `type`, `mime_type` and `mediaType`. Smaller inline
344
+ parts in `ai.prompt.messages`. A span whose messages exceed 256 KiB would be rejected, so when a
345
+ record is queued Hue replaces the `content`/`data` of any such part longer than 64 KiB with the
346
+ file's `sha256` and `size`, both of the file's own bytes whatever its media type (base64 content
347
+ and `;base64` `data:` URLs decoded, other `data:` URLs percent-decoded, anything else as UTF-8 text), keeping the part's other fields such as `type`, `mime_type` and `mediaType`. Smaller inline
331
348
  files are exported as recorded. The digest matches `hue.recordFile`'s `hue.file.sha256` for the same
332
- bytes, so a file can be recognized wherever it appears.
349
+ bytes, so a file can be recognized wherever it appears. The replacement happens before the record is
350
+ charged to the queue budget, so a large file does not drop its span. It is bounded: a message
351
+ attribute longer than 8 MiB of text is left unchanged, and the default 8 MiB `maxQueueBytes` then
352
+ drops the span. Base64 grows a file by a third, so that ceiling is an inline file of about 6 MiB.
353
+ The same message text is hashed once per record, and a record inspects at most 16 MiB of message
354
+ text in all; messages past that are charged as recorded. With `captureContent: false` the messages,
355
+ which export removes, are neither hashed nor charged.
333
356
 
334
357
  Manual helpers encode JSON values without converting null into absence. Unknown
335
358
  outputs and usage remain absent. This SDK does not estimate tokens or cost. A thrown
@@ -361,7 +384,7 @@ import { createHue, createHueTransport } from "@hue-run/sdk";
361
384
  const transport = createHueTransport({
362
385
  apiKey: process.env.HUE_API_KEY!,
363
386
  serviceName: "existing-app",
364
- captureContent: false,
387
+ captureContent: true, // false sends metadata only
365
388
  });
366
389
  const tracerProvider = new TracerProvider({ spanProcessors: [transport.spanProcessor] });
367
390
  const loggerProvider = new LoggerProvider({ processors: [transport.logRecordProcessor] });
@@ -576,9 +599,9 @@ an uncertain preparation. See the
576
599
  an issued facade endpoint or the official Gmail service, and do not claim universal provider
577
600
  parity.
578
601
 
579
- An unreleased `directTarget` callback extends the same worker to cases without a world: the runner
580
- verifies the case's pinned input files, hands them to the agent, and uploads the documents it
581
- returns as verified Hue artifacts for scoring. See
602
+ The `directTarget` callback, shipped in TypeScript `0.5.0`, extends the same worker to cases
603
+ without a world: the runner verifies the case's pinned input files, hands them to the agent, and
604
+ uploads the documents it returns as verified Hue artifacts for scoring. See
582
605
  [direct cases and files](EVALUATIONS.md#direct-cases-and-files).
583
606
 
584
607
  Scorer deferral shipped in TypeScript `0.3.1`. Only built-ins
@@ -591,17 +614,23 @@ their agents with `runLocalAgent()`; setup does not register workers or launch s
591
614
 
592
615
  ### Command-line evaluation
593
616
 
594
- The unreleased `hue eval` command wraps `runSimulation()` and `runLocalAgent()` for an adapter
595
- file or a shell command: `hue eval --case "<name>" ./hue-agent.ts` creates a fresh run
596
- from a published case's immutable pins, runs the agent in one isolated world per case, waits
617
+ The `hue eval` command, shipped in TypeScript `0.5.0` (`--case` since `0.6.0`, `--scenario`
618
+ before), wraps `runSimulation()` and `runLocalAgent()` for an adapter file or a shell command:
619
+ `hue eval --case "<name>" ./hue-agent.ts --content` creates a fresh run from a published case's
620
+ immutable pins, runs the agent in one isolated world per case, waits
597
621
  for Hue's outcome checks and prints the run URL and per-case PASS/FAIL verdicts with an exit code;
598
622
  `--worker` registers the same adapter for runs launched from Hue. It needs a Read and
599
- write key in `HUE_API_KEY` (never printed) and keeps content capture off unless `--content`
600
- is passed. See [Evaluate an agent against a case](CLI.md#evaluate-an-agent-against-a-case).
623
+ write key in `HUE_API_KEY` (never printed). Content capture stays off unless `--content` is
624
+ passed; the example passes it so case spans carry content. Case outputs, error messages and
625
+ explanations are stored in Hue either way (`--no-output` keeps them out of a one-shot run's
626
+ results); a command's stdout is its stored answer, with the credentials `hue eval` handed the case
627
+ replaced by `[redacted]`.
628
+ Instrument the agent itself for its model and tool spans. See
629
+ [Evaluate an agent against a case](CLI.md#evaluate-an-agent-against-a-case).
601
630
  Eval sets whose cases pin files instead of a world run as direct cases through `runExperiment()`:
602
- `hue eval --set <slug> --scorer <slug> --command "…"` hands the agent each case's pinned files in a
603
- private directory, uploads the documents it writes and waits for Hue's grading executor to score
604
- them. See [Evaluate a document eval set](CLI.md#evaluate-a-document-eval-set).
631
+ `hue eval --set <slug> --scorer <slug> --command "…" --content` hands the agent each case's
632
+ pinned files in a private directory, uploads the documents it writes and waits for Hue's grading
633
+ executor to score them. See [Evaluate a document eval set](CLI.md#evaluate-a-document-eval-set).
605
634
 
606
635
  ## Managed targets
607
636
 
@@ -38,10 +38,21 @@ export declare function stageDirectCase(root: string, input: {
38
38
  executionId: string;
39
39
  files: LocalFile[];
40
40
  }): Promise<DirectCaseLayout>;
41
+ /** Test seams: `beforeEntry` runs before each listed entry is examined, `afterListing` between
42
+ * the listing and the reads. */
43
+ export interface CollectionSeams {
44
+ beforeEntry?: (name: string) => Promise<void>;
45
+ afterListing?: () => Promise<void>;
46
+ }
41
47
  /**
42
48
  * Turns the output folder into the target's result. Every regular file, in subdirectories too,
43
49
  * becomes a generated file; an unsupported extension is the agent's error rather than a silently
44
50
  * dropped document. `fallbackOutput` (for example the command's stdout) is used when no JSON
45
51
  * output or summary file was written.
52
+ *
53
+ * Nothing here trusts a path the agent could have changed: the output directory must be a real
54
+ * directory, every file is opened without following a final symlink or blocking on a FIFO, must
55
+ * be the regular file the listing saw and within its size limit, and the result carries the bytes
56
+ * read from that open file. `seams` are for tests only.
46
57
  */
47
- export declare function collectDirectOutputs(outputDirectory: string, fallbackOutput?: JsonValue): Promise<TargetResult>;
58
+ export declare function collectDirectOutputs(outputDirectory: string, fallbackOutput?: JsonValue, seams?: CollectionSeams): Promise<TargetResult>;