@hue-run/sdk 0.8.1 → 0.9.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.8.1`, `@opentelemetry/api@1.9.1` and
45
+ not a completed integration. TypeScript uses `@hue-run/sdk@0.9.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.8.1.tgz \
308
+ --archive .artifacts/typescript/hue-run-sdk-0.9.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.8.1.tgz \
319
+ --archive .artifacts/typescript/hue-run-sdk-0.9.0.tgz \
320
320
  --origin https://STAGING_ORIGIN \
321
321
  --project "$project" --command claim \
322
322
  --evidence .context/setup-staging-after-claim.json
package/ENVIRONMENTS.md CHANGED
@@ -126,6 +126,18 @@ Hue canonicalizes valid synthetic-principal UUIDs to lowercase, and repository r
126
126
  the same before comparing immutable digests, so casing-only UUID changes reuse the stored
127
127
  version without dropping provider bindings.
128
128
 
129
+ Each Gmail provider instance names its mailbox carrier in `configuration`
130
+ (`GmailMailboxConfiguration`, narrowed by `kind`). `gmail_mailbox/v1` binds `messagesCollection`
131
+ and `draftsCollection` in the simplified shape Hue's `hue.gmail.*` actions read.
132
+ `gmail_mailbox/v2` adds `labelsCollection` and binds all three in the entity shapes the Gmail
133
+ mirrors serve: messages may also carry `cc`, `bcc`, `htmlBody`, `snippet`, attachment metadata,
134
+ `sizeEstimate`, `historyId` and `raw`, each draft names a `DRAFT`-labelled message, and threads
135
+ are derived from each message's `threadId`. Only the simulation gateway serves it: the pinned
136
+ provider-profile preflight above reports `profile_unavailable` for a `gmail_mailbox/v2`
137
+ instance. Its `mailboxAddress` must be the synthetic owner `owner@example.test` (compared
138
+ case-insensitively) or publication is refused, and a definition whose provider instances all use
139
+ it may publish with `actions: []`.
140
+
129
141
  ```ts
130
142
  const definition = {
131
143
  kind: "repository" as const,
@@ -230,22 +242,38 @@ try {
230
242
  await spawnAgent({ env: child, mcpConfigPath: config.path });
231
243
  } finally {
232
244
  await config.dispose();
233
- await environmentClient.finishRun(run.id, {
245
+ const finished = await environmentClient.finishRun(run.id, {
234
246
  idempotencyKey: `execution:${executionId}:completed`,
235
247
  status: "completed",
236
248
  });
249
+ const graceEnd = Date.parse(finished.completingUntil ?? "") || Date.now();
250
+ await new Promise((resolve) =>
251
+ setTimeout(resolve, Math.min(10_000, Math.max(0, graceEnd - Date.now()))),
252
+ );
253
+ const sealDeadline = Date.now() + 30_000;
254
+ let sealed = false;
255
+ while (Date.now() < sealDeadline) {
256
+ if ((await environmentClient.getRun(run.id)).status !== "open") {
257
+ sealed = true;
258
+ break;
259
+ }
260
+ await new Promise((resolve) => setTimeout(resolve, 250));
261
+ }
262
+ if (!sealed) throw new Error("World was not sealed after its completion grace");
237
263
  }
238
264
  ```
239
265
 
240
266
  `runSimulation`, `runLocalAgent` and `hue eval` do this for you: they create the world with the
241
267
  execution, the stable key, the case span's context and the agent revision, pass the handoff as
242
- `context.world`, and finish before returning so telemetry is flushed and the execution completed
243
- afterwards. For one compatibility release `context.mcp` is the world's first MCP mirror with the
268
+ `context.world`, and finish and wait for the seal before returning so telemetry is flushed and the
269
+ execution completed afterwards. For one compatibility release `context.mcp` is the world's first MCP mirror with the
244
270
  world token, so an adapter that read `HUE_MCP_URL` and `HUE_MCP_TOKEN` keeps working;
245
271
  `agentEnvironment` sets those names too unless `legacyMcpVariables: false`. A gateway world binds
246
272
  no Hue-native `tools` (Hue refuses them); a world created while the gateway is off keeps its tools
247
273
  and the `hue_sim_` capability and emits a one-time `DeprecationWarning`.
248
274
 
275
+ A refusal such as 409 `simulation_gateway_required` exposes its validated server code as `HueEnvironmentError.diagnostic`.
276
+
249
277
  `agentEnvironment` removes Hue control-plane credentials from the child by default: `HUE_API_KEY`,
250
278
  `HUE_MCP_KEY` and any variable whose value is a `hue_sk_`, `hue_mcp_` or `hue_attempt_`
251
279
  credential. Pass `includeHueCredentials: true` only for an agent that must call Hue's own API.
@@ -253,7 +281,9 @@ Nothing in these helpers logs the token; keep it out of your own logs and checkp
253
281
 
254
282
  Finish answers `lifecycle: "completing"` with `sealedAt: null` for a gateway world: the seal
255
283
  follows a 5 s grace so in-flight writes land, and a late finish answers 409, which the helpers
256
- treat as the seal they can no longer change. `getEvidence(runId, { section, bodies })` reads the
284
+ treat as the seal they can no longer change. Completing an execution while the world is open
285
+ returns 409, so direct clients read the run after the finish response's `completingUntil`
286
+ until it is no longer open; the helper waits through transient failures. `getEvidence(runId, { section, bodies })` reads the
257
287
  sealed world's evaluator-only evidence (start and end state, the diff, the call ledger, coverage,
258
288
  fingerprint) with the project key; a world token can never read it. The client honors Hue's
259
289
  `Retry-After` on 429 and 503.
package/dist/client.js CHANGED
@@ -40,7 +40,7 @@ function identifier(value) {
40
40
  throw new TypeError("Session/user/workspace identifiers must contain 1–4096 valid characters");
41
41
  return value;
42
42
  }
43
- /** A usable metadata label: a non-blank string of at most 256 characters. */
43
+ /** A usable metadata label: non-blank, at most 256 UTF-16 code units, with no NUL or unpaired surrogate. */
44
44
  function isLabel(value) {
45
45
  return (typeof value === "string" &&
46
46
  value.trim() !== "" &&
@@ -48,18 +48,6 @@ function isLabel(value) {
48
48
  !value.includes("\u0000") &&
49
49
  value.isWellFormed());
50
50
  }
51
- /** A source label uses the stricter wire-safe validation without changing existing labels. */
52
- function isSourceLabel(value) {
53
- return (typeof value === "string" &&
54
- value.trim() !== "" &&
55
- value.length <= 256 &&
56
- !value.includes("\u0000") &&
57
- value.isWellFormed());
58
- }
59
- /** A label that is also free of NUL and unpaired surrogates, which export would reject. */
60
- function isTextLabel(value) {
61
- return isLabel(value) && !value.includes("\u0000") && value.isWellFormed();
62
- }
63
51
  /**
64
52
  * Runs `work` exactly once with `active` as OpenTelemetry's current context, so instrumentations
65
53
  * that use the global API parent under the Hue span. Without a registered context manager
@@ -330,11 +318,11 @@ export class HueClient {
330
318
  "gen_ai.operation.name": "execute_tool",
331
319
  "gen_ai.tool.name": name,
332
320
  };
333
- const stamp = (key, value, valid = isLabel) => {
321
+ const stamp = (key, value) => {
334
322
  if (value === undefined)
335
323
  return;
336
324
  // A blank or non-string label is omitted and counted; the tool call itself still runs.
337
- if (valid(value))
325
+ if (isLabel(value))
338
326
  attributes[key] = value;
339
327
  else if (this.enabled && !this.closed)
340
328
  this.transport.instrumentationFailure();
@@ -342,8 +330,8 @@ export class HueClient {
342
330
  stamp("gen_ai.tool.call.id", options.callId);
343
331
  stamp("mcp.server.name", options.mcp?.name);
344
332
  stamp("mcp.server.version", options.mcp?.version);
345
- stamp("hue.mcp.provider", options.mcp?.provider, isSourceLabel);
346
- stamp("hue.mcp.surface", options.mcp?.surface, isSourceLabel);
333
+ stamp("hue.mcp.provider", options.mcp?.provider);
334
+ stamp("hue.mcp.surface", options.mcp?.surface);
347
335
  return this.withSpan(`execute_tool ${name}`, async ({ span }) => {
348
336
  this.setContent(span, "gen_ai.tool.call.arguments", input);
349
337
  const result = await execute();
@@ -555,7 +543,9 @@ export class HueClient {
555
543
  const attributes = {};
556
544
  if (label === undefined)
557
545
  return attributes;
558
- const info = options.servers?.[label];
546
+ const info = options.servers && Object.hasOwn(options.servers, label)
547
+ ? options.servers[label]
548
+ : undefined;
559
549
  for (const [key, value] of [
560
550
  ["mcp.server.name", info?.name ?? label],
561
551
  ["mcp.server.version", info?.version],
@@ -630,7 +620,7 @@ export class HueClient {
630
620
  const { role, mediaType, data, name } = file;
631
621
  if (role !== "input" && role !== "attachment" && role !== "output")
632
622
  throw new TypeError("Invalid file role");
633
- if (!isTextLabel(mediaType))
623
+ if (!isLabel(mediaType))
634
624
  throw new TypeError("Invalid media type");
635
625
  let sha256 = typeof file.sha256 === "string" ? file.sha256.toLowerCase() : file.sha256;
636
626
  let byteSize = file.byteSize;
@@ -673,7 +663,7 @@ export class HueClient {
673
663
  if (byteSize !== undefined)
674
664
  attributes["hue.file.size"] = byteSize;
675
665
  if (this.captureContent && name !== undefined) {
676
- if (isTextLabel(name))
666
+ if (isLabel(name))
677
667
  attributes["hue.file.name"] = name;
678
668
  else
679
669
  this.transport.instrumentationFailure();
@@ -16,12 +16,18 @@ export declare class HueEnvironmentError extends Error {
16
16
  readonly status?: number | undefined;
17
17
  /** Hue's `Retry-After` in milliseconds, bounded, when a 429 or 503 carried one. */
18
18
  readonly retryAfterMs?: number | undefined;
19
+ /** The server's `X-Hue-Diagnostic` code, when it is a short snake-case value. */
20
+ readonly diagnostic?: string | undefined;
19
21
  constructor(
20
22
  /** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
21
23
  status?: number | undefined,
22
24
  /** Hue's `Retry-After` in milliseconds, bounded, when a 429 or 503 carried one. */
23
- retryAfterMs?: number | undefined);
25
+ retryAfterMs?: number | undefined,
26
+ /** The server's `X-Hue-Diagnostic` code, when it is a short snake-case value. */
27
+ diagnostic?: string | undefined);
24
28
  }
29
+ /** A connection failure or a status the client retries; seal polling continues through these. */
30
+ export declare function isTransientEnvironmentError(error: unknown): boolean;
25
31
  /** Typed client for authored environments, isolated runs and immutable journals. */
26
32
  export declare class EnvironmentClient {
27
33
  /** Validated Hue origin. */
@@ -47,7 +53,9 @@ export declare class EnvironmentClient {
47
53
  /** Creates or recovers one fresh isolated world using a stable idempotency key. */
48
54
  createRun(input: CreateRunInput): Promise<EnvironmentRun>;
49
55
  /** Reads authoritative current or sealed world state. */
50
- getRun(runId: string): Promise<{
56
+ getRun(runId: string, options?: {
57
+ signal?: AbortSignal;
58
+ }): Promise<{
51
59
  id: string;
52
60
  environmentVersionId: string;
53
61
  executionId: string | null;
@@ -4,20 +4,35 @@ import { aggregateBounds, json, uuid, valueBounds } from "../evals/json.js";
4
4
  export class HueEnvironmentError extends Error {
5
5
  status;
6
6
  retryAfterMs;
7
+ diagnostic;
7
8
  constructor(
8
9
  /** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
9
10
  status,
10
11
  /** Hue's `Retry-After` in milliseconds, bounded, when a 429 or 503 carried one. */
11
- retryAfterMs) {
12
+ retryAfterMs,
13
+ /** The server's `X-Hue-Diagnostic` code, when it is a short snake-case value. */
14
+ diagnostic) {
12
15
  super(status
13
- ? `Hue environment request failed (HTTP ${status})`
16
+ ? `Hue environment request failed (HTTP ${status}${diagnostic ? `, ${diagnostic}` : ""})`
14
17
  : "Hue environment connection or response failed");
15
18
  this.status = status;
16
19
  this.retryAfterMs = retryAfterMs;
20
+ this.diagnostic = diagnostic;
17
21
  this.name = "HueEnvironmentError";
18
22
  }
19
23
  }
24
+ const DIAGNOSTIC = /^[a-z_]{1,64}$/;
25
+ /** The response's diagnostic code, or undefined when absent or invalid. */
26
+ function diagnosticOf(response) {
27
+ const value = response.headers.get("x-hue-diagnostic");
28
+ return value !== null && DIAGNOSTIC.test(value) ? value : undefined;
29
+ }
20
30
  const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
31
+ /** A connection failure or a status the client retries; seal polling continues through these. */
32
+ export function isTransientEnvironmentError(error) {
33
+ return (error instanceof HueEnvironmentError &&
34
+ (error.status === undefined || RETRYABLE.has(error.status)));
35
+ }
21
36
  /** A `Retry-After` longer than this waits this long: Hue asks for a second, never minutes. */
22
37
  const MAX_RETRY_AFTER_MS = 10_000;
23
38
  const TRACEPARENT = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/;
@@ -64,9 +79,10 @@ export class EnvironmentClient {
64
79
  throw new RangeError("maxAttempts must be 1–10");
65
80
  this.maxAttempts = attempts;
66
81
  }
67
- async send(method, path, payload) {
82
+ async send(method, path, payload, options = {}) {
68
83
  let response;
69
84
  try {
85
+ const timeout = AbortSignal.timeout(this.timeoutMillis);
70
86
  response = await fetch(`${this.baseUrl}/api/v1${path}`, {
71
87
  method,
72
88
  headers: {
@@ -75,7 +91,7 @@ export class EnvironmentClient {
75
91
  },
76
92
  body: payload,
77
93
  redirect: "error",
78
- signal: AbortSignal.timeout(this.timeoutMillis),
94
+ signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,
79
95
  });
80
96
  }
81
97
  catch {
@@ -83,7 +99,7 @@ export class EnvironmentClient {
83
99
  }
84
100
  if (!response.ok) {
85
101
  await response.body?.cancel();
86
- throw new HueEnvironmentError(response.status, response.status === 429 || response.status === 503 ? retryAfterMillis(response) : undefined);
102
+ throw new HueEnvironmentError(response.status, response.status === 429 || response.status === 503 ? retryAfterMillis(response) : undefined, diagnosticOf(response));
87
103
  }
88
104
  try {
89
105
  const reader = response.body?.getReader();
@@ -111,25 +127,40 @@ export class EnvironmentClient {
111
127
  throw new HueEnvironmentError();
112
128
  }
113
129
  }
114
- async request(method, path, body) {
130
+ async request(method, path, body, options = {}) {
115
131
  // Serialize once: a body this client cannot encode is a caller error that no retry fixes.
116
132
  const payload = body === undefined
117
133
  ? undefined
118
134
  : JSON.stringify(json(Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined)), REQUEST_BOUNDS));
119
135
  for (let attempt = 1;; attempt++) {
120
136
  try {
121
- return await this.send(method, path, payload);
137
+ return await this.send(method, path, payload, options);
122
138
  }
123
139
  catch (error) {
124
140
  if (!(error instanceof HueEnvironmentError))
125
141
  throw error;
126
- const recoverable = error.status === undefined || RETRYABLE.has(error.status);
127
- if (!recoverable || attempt >= this.maxAttempts)
142
+ if (options.signal?.aborted)
143
+ throw error;
144
+ if (!isTransientEnvironmentError(error) || attempt >= this.maxAttempts)
128
145
  throw error;
129
146
  // Hue's admission refusals say how long to wait; anything else backs off.
130
147
  const backoff = Math.min(100 * 2 ** (attempt - 1), 2000);
131
148
  const wait = error.retryAfterMs ?? backoff + Math.random() * backoff;
132
- await new Promise((resolve) => setTimeout(resolve, wait));
149
+ await new Promise((resolve, reject) => {
150
+ const timer = setTimeout(() => {
151
+ options.signal?.removeEventListener("abort", abort);
152
+ resolve();
153
+ }, wait);
154
+ const abort = () => {
155
+ clearTimeout(timer);
156
+ options.signal?.removeEventListener("abort", abort);
157
+ reject(new HueEnvironmentError());
158
+ };
159
+ if (options.signal?.aborted)
160
+ abort();
161
+ else
162
+ options.signal?.addEventListener("abort", abort, { once: true });
163
+ });
133
164
  }
134
165
  }
135
166
  }
@@ -194,8 +225,8 @@ export class EnvironmentClient {
194
225
  });
195
226
  }
196
227
  /** Reads authoritative current or sealed world state. */
197
- async getRun(runId) {
198
- const run = await this.request("GET", `/environment-runs/${uuid(runId)}`);
228
+ async getRun(runId, options = {}) {
229
+ const run = await this.request("GET", `/environment-runs/${uuid(runId)}`, undefined, options);
199
230
  return { validity: "not_assessed", coverageGap: null, ...run };
200
231
  }
201
232
  /** Record a known coverage gap with durable identity; retries reuse the exact request. */
@@ -173,6 +173,35 @@ export interface EnvironmentDefinition {
173
173
  /** The extendable legacy name remains V1. Publication and runs select their
174
174
  * explicit version; provider context is validated by the authoritative server. */
175
175
  export type EnvironmentDefinitionV1 = EnvironmentDefinition;
176
+ /** Gmail mailbox carrier holding messages and drafts in the simplified shape Hue's
177
+ * `hue.gmail.*` actions read. */
178
+ export interface GmailMailboxConfigurationV1 {
179
+ /** Gmail mailbox configuration discriminator. */
180
+ kind: "gmail_mailbox/v1";
181
+ /** Collection containing synthetic messages. */
182
+ messagesCollection: string;
183
+ /** Collection containing synthetic drafts. */
184
+ draftsCollection: string;
185
+ /** Synthetic mailbox address. */
186
+ mailboxAddress: string;
187
+ }
188
+ /** Gmail mailbox carrier served by Hue's simulation gateway: messages, drafts and labels in the
189
+ * entity shapes the Gmail mirrors serve, with threads derived from each message's `threadId`. A
190
+ * definition whose provider instances all use it may publish with no actions. */
191
+ export interface GmailMailboxConfigurationV2 {
192
+ /** Gmail mailbox configuration discriminator. */
193
+ kind: "gmail_mailbox/v2";
194
+ /** Collection containing synthetic messages. */
195
+ messagesCollection: string;
196
+ /** Collection containing synthetic drafts, each naming a `DRAFT`-labelled message. */
197
+ draftsCollection: string;
198
+ /** Collection containing synthetic labels. */
199
+ labelsCollection: string;
200
+ /** Synthetic mailbox address; Hue accepts only `owner@example.test`, in any letter case. */
201
+ mailboxAddress: string;
202
+ }
203
+ /** Either Gmail mailbox carrier, discriminated by `kind`; Hue validates both at publication. */
204
+ export type GmailMailboxConfiguration = GmailMailboxConfigurationV1 | GmailMailboxConfigurationV2;
176
205
  /** One synthetic Gmail principal and its world-state collection bindings. */
177
206
  export interface GmailProviderInstance {
178
207
  /** Stable instance key referenced by attempt provider selection. */
@@ -182,16 +211,7 @@ export interface GmailProviderInstance {
182
211
  /** Synthetic principal UUID, canonicalized to lowercase by Hue. */
183
212
  syntheticPrincipalId: string;
184
213
  /** Versioned mapping from Gmail concepts to authored-world collections. */
185
- configuration: {
186
- /** Gmail mailbox configuration discriminator. */
187
- kind: "gmail_mailbox/v1";
188
- /** Collection containing synthetic messages. */
189
- messagesCollection: string;
190
- /** Collection containing synthetic drafts. */
191
- draftsCollection: string;
192
- /** Synthetic mailbox address. */
193
- mailboxAddress: string;
194
- };
214
+ configuration: GmailMailboxConfiguration;
195
215
  }
196
216
  /** V2 authored world with immutable provider-instance bindings. */
197
217
  export interface EnvironmentDefinitionV2 extends Omit<EnvironmentDefinition, "schemaVersion"> {
@@ -1,4 +1,4 @@
1
- export { createEnvironmentClient, EnvironmentClient, HueEnvironmentError, } from "./environment/client.js";
1
+ export { createEnvironmentClient, EnvironmentClient, HueEnvironmentError, isTransientEnvironmentError, } from "./environment/client.js";
2
2
  export type { EnvironmentClientOptions } from "./environment/client.js";
3
3
  export { bindEnvironmentTools } from "./environment/tools.js";
4
4
  export type { BindEnvironmentToolsOptions, EnvironmentTool } from "./environment/tools.js";
@@ -1,3 +1,3 @@
1
- export { createEnvironmentClient, EnvironmentClient, HueEnvironmentError, } from "./environment/client.js";
1
+ export { createEnvironmentClient, EnvironmentClient, HueEnvironmentError, isTransientEnvironmentError, } from "./environment/client.js";
2
2
  export { bindEnvironmentTools } from "./environment/tools.js";
3
3
  export { agentEnvironment, HUE_CONTROL_PLANE_VARIABLES, isHueControlPlaneCredential, legacyMcpCapability, stripHueControlPlaneCredentials, worldHandoff, writeMcpConfig, } from "./environment/world.js";
@@ -91,6 +91,11 @@ export interface RunEnvironmentTargetOptions {
91
91
  onProgress?(event: EnvironmentTargetProgress): void | Promise<void>;
92
92
  target(inputs: JsonValue, context: EnvironmentTargetContext): JsonValue | undefined | Promise<JsonValue | undefined>;
93
93
  }
94
+ /** The completion grace is five seconds; cap an unexpectedly distant timestamp and let reads
95
+ * force the seal after the grace. */
96
+ export declare const MAX_GRACE_WAIT_MS = 10000;
97
+ export declare const SEAL_POLL_MS = 250;
98
+ export declare const SEAL_WAIT_MS = 30000;
94
99
  /** The W3C context of the case span, sent on create so the world span parents on it. The
95
100
  * flags are the span's own: an unsampled case span is not exported, and the World API must not
96
101
  * be told otherwise. */
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { HueEnvironmentError } from "../environment/client.js";
2
+ import { HueEnvironmentError, isTransientEnvironmentError, } from "../environment/client.js";
3
3
  import { bindEnvironmentTools } from "../environment/tools.js";
4
4
  import { legacyMcpCapability, worldHandoff } from "../environment/world.js";
5
5
  import { actualAgentManifestV2, attemptBaselineV2, projectMcpConnectionV2, requestedAttemptProvidersV2, validateAttemptConnectionBundleV2, } from "./attempt.js";
@@ -42,22 +42,73 @@ export function pinRequestedAttemptV2(requested, config) {
42
42
  }
43
43
  return { ...requested, expectedAgentManifestDigest: baseline.data.expectedAgentManifestDigest };
44
44
  }
45
- /** Confirm a seal from the authoritative run after a lost acknowledgement. */
45
+ /** The completion grace is five seconds; cap an unexpectedly distant timestamp and let reads
46
+ * force the seal after the grace. */
47
+ export const MAX_GRACE_WAIT_MS = 10_000;
48
+ export const SEAL_POLL_MS = 250;
49
+ export const SEAL_WAIT_MS = 30_000;
50
+ /** Finish, then wait for the authoritative run to leave open. */
46
51
  async function seal(client, runId, executionId, status) {
52
+ let completingUntil;
47
53
  try {
48
- await client.finishRun(runId, {
54
+ const finished = await client.finishRun(runId, {
49
55
  idempotencyKey: `execution:${executionId}:${status}`,
50
56
  status,
51
57
  });
58
+ if (finished.lifecycle === "completing")
59
+ completingUntil = finished.completingUntil ?? null;
52
60
  }
53
61
  catch (error) {
54
- // A gateway world answers 409 once it is completing, sealed or expired: each is the
55
- // outcome the caller wanted or the one it can no longer change.
62
+ // A gateway world answers 409 once it is completing, sealed or expired.
56
63
  const recovered = await client.getRun(runId).catch(() => undefined);
57
64
  if (recovered?.status !== status &&
58
65
  recovered?.status !== "expired" &&
59
66
  !(recovered?.status === "open" && recovered.lifecycle === "completing"))
60
67
  throw new TargetOutcomeUncertainError(executionId, { cause: error });
68
+ if (recovered?.status === "open")
69
+ completingUntil = recovered.completingUntil ?? null;
70
+ }
71
+ if (completingUntil !== undefined) {
72
+ try {
73
+ await awaitSeal(client, runId, completingUntil);
74
+ }
75
+ catch (error) {
76
+ throw new TargetOutcomeUncertainError(executionId, { cause: error });
77
+ }
78
+ }
79
+ }
80
+ /** Wait out a completing world's grace, then read until the server seals it. */
81
+ async function awaitSeal(client, runId, completingUntil) {
82
+ const graceEnd = Date.parse(completingUntil ?? "");
83
+ let wait = Number.isFinite(graceEnd)
84
+ ? Math.min(Math.max(0, graceEnd - Date.now()), MAX_GRACE_WAIT_MS)
85
+ : 0;
86
+ const deadline = performance.now() + wait + SEAL_WAIT_MS;
87
+ for (;;) {
88
+ await new Promise((resolve) => setTimeout(resolve, wait));
89
+ const remaining = deadline - performance.now();
90
+ if (remaining <= 0)
91
+ throw new Error(`World ${runId} was not sealed after its completion grace`);
92
+ const controller = new AbortController();
93
+ const timer = setTimeout(() => controller.abort(), remaining);
94
+ try {
95
+ if ((await client.getRun(runId, { signal: controller.signal })).status !== "open")
96
+ return;
97
+ }
98
+ catch (error) {
99
+ if (!isTransientEnvironmentError(error))
100
+ throw error;
101
+ if (performance.now() >= deadline)
102
+ throw new Error(`World ${runId} was not sealed after its completion grace`, {
103
+ cause: error,
104
+ });
105
+ }
106
+ finally {
107
+ clearTimeout(timer);
108
+ }
109
+ if (performance.now() >= deadline)
110
+ throw new Error(`World ${runId} was not sealed after its completion grace`);
111
+ wait = SEAL_POLL_MS;
61
112
  }
62
113
  }
63
114
  const deprecations = new Set();
@@ -10,7 +10,7 @@ export interface ScenarioPins {
10
10
  datasetId: string;
11
11
  /** Pinned dataset version; frozen only when `saved` is true. */
12
12
  datasetVersionId: string;
13
- /** Pinned scorer versions; a Scenario pins exactly one. */
13
+ /** Pinned scorer versions; a Scenario's publication lists them, its outcome scorer first. */
14
14
  scorerVersionIds: string[];
15
15
  /** Pinned simulated-world version, or `null` when the selection does not pin one. */
16
16
  environmentVersionId: string | null;
@@ -74,7 +74,10 @@ async function pinsFromScenario(client, scenario, dataset) {
74
74
  name: dataset.name,
75
75
  datasetId: scenario.publication.datasetId,
76
76
  datasetVersionId: version.id,
77
- scorerVersionIds: [scenario.publication.scorerVersionId],
77
+ // Publications list every pin, the outcome scorer first; older ones carry the single field.
78
+ scorerVersionIds: scenario.publication.scorerVersionIds?.length
79
+ ? [...new Set(scenario.publication.scorerVersionIds)]
80
+ : [scenario.publication.scorerVersionId],
78
81
  environmentVersionId: scenario.publication.environmentVersionId,
79
82
  saved: version.frozenAt !== null,
80
83
  revision: version.revision,
@@ -879,8 +879,13 @@ export interface CaseConversionPublication {
879
879
  environmentVersionId: string;
880
880
  /** Scorer identity of the published outcome checks. */
881
881
  scorerId: string;
882
- /** Immutable scorer version pinned by the Scenario. */
882
+ /** Immutable scorer version pinned by the Scenario for its outcome checks. */
883
883
  scorerVersionId: string;
884
+ /**
885
+ * Every immutable scorer version the Scenario pins, `scorerVersionId` first. Absent on
886
+ * Scenarios published before publications listed their pins.
887
+ */
888
+ scorerVersionIds?: string[];
884
889
  }
885
890
  /** A Scenario read by {@link EvaluationClient.getCaseConversion}. Extra server fields are ignored. */
886
891
  export interface CaseConversion extends Partial<Omit<CaseConversionSummary, "id" | "status">> {
@@ -5,6 +5,30 @@ const MAX_PROVIDER_SERVERS = 512;
5
5
  function isItem(value) {
6
6
  return value !== null && typeof value === "object" && !Array.isArray(value);
7
7
  }
8
+ /** Only provider-executed tool items contribute to invalid-item diagnostics when a response is
9
+ * truncated; messages and reasoning are harmless response content. */
10
+ function isProviderToolItem(provider, value) {
11
+ if (!isItem(value) || typeof value.type !== "string")
12
+ return false;
13
+ if (provider === "openai")
14
+ return (value.type === "mcp_call" ||
15
+ value.type === "mcp_list_tools" ||
16
+ value.type === "web_search_call" ||
17
+ value.type === "file_search_call" ||
18
+ value.type === "code_interpreter_call");
19
+ return value.type === "mcp_tool_use" || value.type === "server_tool_use";
20
+ }
21
+ function countProviderToolItems(provider, items, start = 0) {
22
+ let count = 0;
23
+ for (const key of Object.keys(items)) {
24
+ const index = Number(key);
25
+ if (!Number.isInteger(index) || index < start || index >= items.length || String(index) !== key)
26
+ continue;
27
+ if (isProviderToolItem(provider, items[index]))
28
+ count++;
29
+ }
30
+ return count;
31
+ }
8
32
  function text(value) {
9
33
  return typeof value === "string" &&
10
34
  value.trim() !== "" &&
@@ -14,6 +38,9 @@ function text(value) {
14
38
  ? value
15
39
  : undefined;
16
40
  }
41
+ function errorCode(value) {
42
+ return typeof value === "string" && /^[a-z0-9_]{1,64}$/.test(value) ? value : "error";
43
+ }
17
44
  /** MCP arguments arrive as a JSON string; record the structure when it parses, else the text. */
18
45
  function jsonArguments(value) {
19
46
  if (typeof value !== "string")
@@ -30,7 +57,7 @@ function jsonArguments(value) {
30
57
  /** OpenAI Responses `output` items. Built-in tools are named by their kind; MCP calls by tool. */
31
58
  function openaiCalls(items, activity) {
32
59
  const count = Math.min(items.length, MAX_PROVIDER_ITEMS);
33
- activity.skipped += items.length - count;
60
+ activity.skipped += countProviderToolItems("openai", items, count);
34
61
  for (let index = 0; index < count; index++) {
35
62
  const item = items[index];
36
63
  if (!isItem(item))
@@ -121,7 +148,7 @@ function anthropicCalls(blocks, activity) {
121
148
  const results = new Map();
122
149
  const count = Math.min(blocks.length, MAX_PROVIDER_ITEMS);
123
150
  const truncated = blocks.length > MAX_PROVIDER_ITEMS;
124
- activity.skipped += blocks.length - count;
151
+ activity.skipped += countProviderToolItems("anthropic", blocks, count);
125
152
  for (let index = 0; index < count; index++) {
126
153
  const block = blocks[index];
127
154
  if (isItem(block) &&
@@ -152,7 +179,7 @@ function anthropicCalls(blocks, activity) {
152
179
  if (result?.is_error === true)
153
180
  errorType = "mcp_error";
154
181
  else if (isItem(content) && typeof content.type === "string" && content.type.endsWith("_error"))
155
- errorType = text(content.error_code) ?? "error";
182
+ errorType = errorCode(content.error_code);
156
183
  activity.calls.push({
157
184
  name,
158
185
  callId,
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Package version shared by the instrumentation scope and the export User-Agent. */
2
- export declare const sdkVersion = "0.8.1";
2
+ export declare const sdkVersion = "0.9.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/write-version.mjs from package.json; do not edit by hand.
2
2
  /** Package version shared by the instrumentation scope and the export User-Agent. */
3
- export const sdkVersion = "0.8.1";
3
+ export const sdkVersion = "0.9.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hue-run/sdk",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {