@hue-run/sdk 0.8.0 → 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/dist/client.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { createHash } from "node:crypto";
2
3
  import { context, isSpanContextValid, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
3
4
  import { defaultTextMapGetter, defaultTextMapSetter } from "@opentelemetry/api";
4
5
  import { SeverityNumber } from "@opentelemetry/api-logs";
@@ -7,10 +8,12 @@ import { LoggerProvider } from "@opentelemetry/sdk-logs";
7
8
  import { TracerProvider } from "@opentelemetry/sdk-trace";
8
9
  import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
9
10
  import { HUE_SCOPE } from "./config.js";
11
+ import { hostedServerAddresses, hostedToolActivity, hostedToolProvider } from "./provider-tools.js";
10
12
  import { encodeContent, noopSpan, safeSpan } from "./safety.js";
11
13
  import { createHueTransport, HueExportError } from "./transport.js";
12
14
  import { verifyTrace } from "./receipt.js";
13
15
  import { sdkVersion } from "./version.js";
16
+ import { MAX_FILE_DATA_BYTES } from "./config.js";
14
17
  /**
15
18
  * Thrown by {@link HueClient.checkConnection} and {@link HueClient.verifyTrace} when Hue cannot be
16
19
  * reached, rejects the project key or answers unexpectedly. The message is fixed and safe to log;
@@ -34,12 +37,16 @@ function identifier(value) {
34
37
  value.length > 4096 ||
35
38
  value.includes("\u0000") ||
36
39
  !value.isWellFormed())
37
- throw new TypeError("Session/user identifiers must contain 1–4096 valid characters");
40
+ throw new TypeError("Session/user/workspace identifiers must contain 1–4096 valid characters");
38
41
  return value;
39
42
  }
40
- /** 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. */
41
44
  function isLabel(value) {
42
- return typeof value === "string" && value.trim() !== "" && value.length <= 256;
45
+ return (typeof value === "string" &&
46
+ value.trim() !== "" &&
47
+ value.length <= 256 &&
48
+ !value.includes("\u0000") &&
49
+ value.isWellFormed());
43
50
  }
44
51
  /**
45
52
  * Runs `work` exactly once with `active` as OpenTelemetry's current context, so instrumentations
@@ -96,6 +103,7 @@ class ContextualTracer {
96
103
  ...options.attributes,
97
104
  ...(active?.sessionId ? { "gen_ai.conversation.id": active.sessionId } : {}),
98
105
  ...(active?.userId ? { "user.id": active.userId } : {}),
106
+ ...(active?.workspaceId ? { "hue.workspace.id": active.workspaceId } : {}),
99
107
  },
100
108
  }, parent ?? active?.context ?? context.active()), this.failed);
101
109
  }
@@ -238,6 +246,7 @@ export class HueClient {
238
246
  context: options.parentContext ?? inherited?.context ?? context.active(),
239
247
  sessionId: identifier(options.sessionId ?? inherited?.sessionId),
240
248
  userId: identifier(options.userId ?? inherited?.userId),
249
+ workspaceId: identifier(options.workspaceId ?? inherited?.workspaceId),
241
250
  model: inherited?.model,
242
251
  };
243
252
  span = this.storage.run(active, () => this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, active.context));
@@ -301,7 +310,8 @@ export class HueClient {
301
310
  * `options.callId` is recorded as `gen_ai.tool.call.id`, like the Python `call_id=` keyword.
302
311
  * `options.mcp` records the MCP `initialize` `serverInfo` as `mcp.server.name` /
303
312
  * `mcp.server.version` so a generic tool name can be attributed to the server that
304
- * handled it. Pass `client.getServerVersion()`.
313
+ * handled it. Pass `client.getServerVersion()`. `mcp.provider` / `mcp.surface` record the Hue
314
+ * provider and surface as `hue.mcp.provider` / `hue.mcp.surface`.
305
315
  */
306
316
  async tool(name, input, execute, options = {}) {
307
317
  const attributes = {
@@ -320,6 +330,8 @@ export class HueClient {
320
330
  stamp("gen_ai.tool.call.id", options.callId);
321
331
  stamp("mcp.server.name", options.mcp?.name);
322
332
  stamp("mcp.server.version", options.mcp?.version);
333
+ stamp("hue.mcp.provider", options.mcp?.provider);
334
+ stamp("hue.mcp.surface", options.mcp?.surface);
323
335
  return this.withSpan(`execute_tool ${name}`, async ({ span }) => {
324
336
  this.setContent(span, "gen_ai.tool.call.arguments", input);
325
337
  const result = await execute();
@@ -337,7 +349,8 @@ export class HueClient {
337
349
  * `gen_ai.request.model` and `gen_ai.provider.name`. The argument order matches `withSpan`. The
338
350
  * handle's `setInput`/`setOutput` record `gen_ai.input.messages` / `gen_ai.output.messages`,
339
351
  * which should use the GenAI semantic-convention message shape; `recordMessages` inside the
340
- * callback inherits the request metadata.
352
+ * callback inherits the request metadata. `options.systemInstructions` and `options.tools` are
353
+ * recorded as `gen_ai.system_instructions` and `gen_ai.tool.definitions`, content like `input`.
341
354
  */
342
355
  async model(model, callback, options) {
343
356
  // A disabled or closed client creates no span, so invalid metadata is not an instrumentation
@@ -357,7 +370,7 @@ export class HueClient {
357
370
  ? `${operation} ${requestModel}`
358
371
  : label(options.name, `${operation} ${requestModel}`);
359
372
  const metadata = { operation, provider, requestModel };
360
- const { sessionId, userId, parentContext, input } = options ?? {};
373
+ const { sessionId, userId, workspaceId, parentContext, input, systemInstructions, tools, } = options ?? {};
361
374
  return this.withSpan(name, (span) => {
362
375
  const handle = {
363
376
  ...span,
@@ -366,12 +379,17 @@ export class HueClient {
366
379
  };
367
380
  if (input !== undefined)
368
381
  handle.setInput(input);
382
+ if (systemInstructions !== undefined)
383
+ this.setContent(span.span, "gen_ai.system_instructions", systemInstructions);
384
+ if (tools !== undefined)
385
+ this.setContent(span.span, "gen_ai.tool.definitions", tools);
369
386
  // recordMessages inside the callback copies this request metadata onto its log record.
370
387
  const store = this.storage.getStore() ?? { context: span.context };
371
388
  return this.storage.run({ ...store, model: metadata }, () => callback(handle));
372
389
  }, {
373
390
  sessionId,
374
391
  userId,
392
+ workspaceId,
375
393
  parentContext,
376
394
  kind: SpanKind.CLIENT,
377
395
  attributes: {
@@ -462,6 +480,8 @@ export class HueClient {
462
480
  body["gen_ai.input.messages"] = messages.input;
463
481
  if (messages.output !== undefined)
464
482
  body["gen_ai.output.messages"] = messages.output;
483
+ if (messages.systemInstructions !== undefined)
484
+ body["gen_ai.system_instructions"] = messages.systemInstructions;
465
485
  // Request metadata is inherited only when the record correlates with the enclosing helper
466
486
  // scope; an unrelated explicit context carries caller-supplied values alone.
467
487
  const enclosing = explicitContext === undefined || explicitContext === store?.context ? store : undefined;
@@ -492,6 +512,168 @@ export class HueClient {
492
512
  this.transport.instrumentationFailure("logs");
493
513
  }
494
514
  }
515
+ /**
516
+ * Records the tools a model provider executed itself while producing `response`, which no
517
+ * `hue.tool()` call saw: OpenAI Responses `mcp_call`, `web_search_call`, `file_search_call` and
518
+ * `code_interpreter_call` items, and Anthropic Messages `mcp_tool_use` / `server_tool_use`
519
+ * blocks with their result blocks. Each becomes an `execute_tool {name}` child span of the active
520
+ * (or given) context with `gen_ai.tool.type` `extension` and `gen_ai.tool.call.id`; MCP calls add
521
+ * `mcp.server.name` (the provider's label, or the `servers` entry for it). Arguments and results
522
+ * follow `captureContent`; a failed call carries `error.type` and ERROR status. An OpenAI
523
+ * `mcp_list_tools` item becomes a `tools/list` child span carrying that server's tools as
524
+ * `gen_ai.tool.definitions`. Call it inside `hue.model()` so the spans nest under the model call
525
+ * and `provider` defaults to its provider; pass `request` to record each server's host as
526
+ * `server.address`. The spans have no duration of their own: the provider ran the tools inside
527
+ * the model request. Unreadable items are skipped and counted; nothing is thrown.
528
+ */
529
+ recordProviderToolCalls(response, options = {}) {
530
+ if (!this.enabled || this.closed)
531
+ return;
532
+ try {
533
+ const store = this.storage.getStore();
534
+ const provider = options.provider ?? hostedToolProvider(store?.model?.provider);
535
+ if (provider === undefined)
536
+ throw new TypeError("Unknown provider for hosted tool calls");
537
+ const parent = options.parentContext ?? store?.context ?? context.active();
538
+ const addresses = hostedServerAddresses(provider, options.request);
539
+ const activity = hostedToolActivity(provider, response);
540
+ if (activity.skipped > 0)
541
+ this.transport.instrumentationFailure("traces", undefined, activity.skipped);
542
+ const server = (label) => {
543
+ const attributes = {};
544
+ if (label === undefined)
545
+ return attributes;
546
+ const info = options.servers && Object.hasOwn(options.servers, label)
547
+ ? options.servers[label]
548
+ : undefined;
549
+ for (const [key, value] of [
550
+ ["mcp.server.name", info?.name ?? label],
551
+ ["mcp.server.version", info?.version],
552
+ ["hue.mcp.provider", info?.provider],
553
+ ["hue.mcp.surface", info?.surface],
554
+ ]) {
555
+ if (value === undefined)
556
+ continue;
557
+ if (isLabel(value))
558
+ attributes[key] = value;
559
+ else
560
+ this.transport.instrumentationFailure();
561
+ }
562
+ const address = addresses.get(label);
563
+ if (address !== undefined)
564
+ attributes["server.address"] = address;
565
+ return attributes;
566
+ };
567
+ const fail = (span, type) => {
568
+ span.setAttribute("error.type", type);
569
+ span.setStatus({ code: SpanStatusCode.ERROR });
570
+ };
571
+ for (const call of activity.calls) {
572
+ const span = this.tracer.startSpan(`execute_tool ${call.name}`, {
573
+ kind: SpanKind.INTERNAL,
574
+ attributes: {
575
+ "gen_ai.operation.name": "execute_tool",
576
+ "gen_ai.tool.name": call.name,
577
+ "gen_ai.tool.type": "extension",
578
+ ...(call.callId === undefined ? {} : { "gen_ai.tool.call.id": call.callId }),
579
+ ...server(call.server),
580
+ },
581
+ }, parent);
582
+ if (call.arguments !== undefined)
583
+ this.setContent(span, "gen_ai.tool.call.arguments", call.arguments);
584
+ if (call.result !== undefined)
585
+ this.setContent(span, "gen_ai.tool.call.result", call.result);
586
+ if (call.errorType !== undefined)
587
+ fail(span, call.errorType);
588
+ span.end();
589
+ }
590
+ for (const listing of activity.listings) {
591
+ const span = this.tracer.startSpan("tools/list", {
592
+ kind: SpanKind.INTERNAL,
593
+ attributes: { "mcp.method.name": "tools/list", ...server(listing.server) },
594
+ }, parent);
595
+ this.setContent(span, "gen_ai.tool.definitions", listing.definitions);
596
+ if (listing.errorType !== undefined)
597
+ fail(span, listing.errorType);
598
+ span.end();
599
+ }
600
+ }
601
+ catch {
602
+ this.transport.instrumentationFailure();
603
+ }
604
+ }
605
+ /**
606
+ * Adds a `hue.file` event to the active (or given) span for a file the work read, received or
607
+ * produced: `hue.file.sha256`, `hue.file.role`, `hue.file.media_type`, `hue.file.size` when known
608
+ * and, when `captureContent` is true, `hue.file.name`. `data` is hashed and measured locally and
609
+ * never exported. The event is metadata, so it is recorded in both capture modes. An invalid
610
+ * record, or one without an active span, is omitted and counted, never thrown; an invalid name
611
+ * alone is omitted and counted while the rest is recorded.
612
+ */
613
+ recordFile(file, explicitContext) {
614
+ if (!this.enabled || this.closed)
615
+ return;
616
+ try {
617
+ const span = trace.getSpan(explicitContext ?? this.storage.getStore()?.context ?? context.active());
618
+ if (!span?.isRecording())
619
+ throw new Error("File records require an active span");
620
+ const { role, mediaType, data, name } = file;
621
+ if (role !== "input" && role !== "attachment" && role !== "output")
622
+ throw new TypeError("Invalid file role");
623
+ if (!isLabel(mediaType))
624
+ throw new TypeError("Invalid media type");
625
+ let sha256 = typeof file.sha256 === "string" ? file.sha256.toLowerCase() : file.sha256;
626
+ let byteSize = file.byteSize;
627
+ if (data !== undefined) {
628
+ const bytes = typeof data === "string"
629
+ ? (() => {
630
+ // Buffer.byteLength measures UTF-8 without allocating the copy that hashing would
631
+ // otherwise require. Reject before Buffer.from/createHash can retain large input.
632
+ if (data.length > MAX_FILE_DATA_BYTES ||
633
+ Buffer.byteLength(data, "utf8") > MAX_FILE_DATA_BYTES)
634
+ throw new RangeError("File data exceeds Hue's 25 MiB limit");
635
+ return Buffer.from(data, "utf8");
636
+ })()
637
+ : data instanceof Uint8Array
638
+ ? data.byteLength <= MAX_FILE_DATA_BYTES
639
+ ? data
640
+ : (() => {
641
+ throw new RangeError("File data exceeds Hue's 25 MiB limit");
642
+ })()
643
+ : undefined;
644
+ if (!bytes)
645
+ throw new TypeError("File data must be bytes or a string");
646
+ const digest = createHash("sha256").update(bytes).digest("hex");
647
+ // A caller-supplied digest or size must describe the same bytes.
648
+ if ((sha256 !== undefined && sha256 !== digest) ||
649
+ (byteSize !== undefined && byteSize !== bytes.byteLength))
650
+ throw new TypeError("File digest or size does not match its data");
651
+ sha256 = digest;
652
+ byteSize = bytes.byteLength;
653
+ }
654
+ if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/.test(sha256))
655
+ throw new TypeError("A file needs a SHA-256 digest or its data");
656
+ if (byteSize !== undefined && (!Number.isSafeInteger(byteSize) || byteSize < 0))
657
+ throw new TypeError("Invalid file size");
658
+ const attributes = {
659
+ "hue.file.sha256": sha256,
660
+ "hue.file.role": role,
661
+ "hue.file.media_type": mediaType,
662
+ };
663
+ if (byteSize !== undefined)
664
+ attributes["hue.file.size"] = byteSize;
665
+ if (this.captureContent && name !== undefined) {
666
+ if (isLabel(name))
667
+ attributes["hue.file.name"] = name;
668
+ else
669
+ this.transport.instrumentationFailure();
670
+ }
671
+ span.addEvent("hue.file", attributes);
672
+ }
673
+ catch {
674
+ this.transport.instrumentationFailure();
675
+ }
676
+ }
495
677
  setContent(span, key, value) {
496
678
  if (!this.enabled || this.closed || !this.captureContent)
497
679
  return;
package/dist/config.d.ts CHANGED
@@ -3,6 +3,8 @@ export declare const MAX_BODY_BYTES: number;
3
3
  export declare const MAX_CONTENT_BYTES: number;
4
4
  /** Instrumentation scope of the client's own tracer and logger. */
5
5
  export declare const HUE_SCOPE = "@hue-run/sdk";
6
+ /** Maximum bytes accepted when recordFile hashes caller-provided data locally. */
7
+ export declare const MAX_FILE_DATA_BYTES: number;
6
8
  /** Loopback hostnames that may use plain HTTP without opting in. */
7
9
  export declare function isLoopbackHost(hostname: string): boolean;
8
10
  /** True when a validated origin exports over plain HTTP to a host other than loopback. */
package/dist/config.js CHANGED
@@ -2,6 +2,8 @@ export const MAX_BODY_BYTES = 1024 * 1024;
2
2
  export const MAX_CONTENT_BYTES = 256 * 1024;
3
3
  /** Instrumentation scope of the client's own tracer and logger. */
4
4
  export const HUE_SCOPE = "@hue-run/sdk";
5
+ /** Maximum bytes accepted when recordFile hashes caller-provided data locally. */
6
+ export const MAX_FILE_DATA_BYTES = 25 * 1024 * 1024;
5
7
  /** Loopback hostnames that may use plain HTTP without opting in. */
6
8
  export function isLoopbackHost(hostname) {
7
9
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
@@ -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. */
@@ -29,7 +29,7 @@ export interface BindEnvironmentToolsOptions {
29
29
  /**
30
30
  * Binds a run's generated catalog to plain local callables without changing the agent
31
31
  * framework. Each call is recorded through {@link HueClient.tool}. Catalog entries that
32
- * include `mcp` stamp `mcp.server.name` / `mcp.server.version` so a generic verb is
33
- * attributed to that MCP server.
32
+ * include `mcp` stamp `mcp.server.name` / `mcp.server.version` and `hue.mcp.provider` /
33
+ * `hue.mcp.surface` so a generic verb is attributed to that MCP server and Hue surface.
34
34
  */
35
35
  export declare function bindEnvironmentTools(options: BindEnvironmentToolsOptions): Record<string, EnvironmentTool>;
@@ -2,8 +2,8 @@ import { randomUUID } from "node:crypto";
2
2
  /**
3
3
  * Binds a run's generated catalog to plain local callables without changing the agent
4
4
  * framework. Each call is recorded through {@link HueClient.tool}. Catalog entries that
5
- * include `mcp` stamp `mcp.server.name` / `mcp.server.version` so a generic verb is
6
- * attributed to that MCP server.
5
+ * include `mcp` stamp `mcp.server.name` / `mcp.server.version` and `hue.mcp.provider` /
6
+ * `hue.mcp.surface` so a generic verb is attributed to that MCP server and Hue surface.
7
7
  */
8
8
  export function bindEnvironmentTools(options) {
9
9
  const tools = {};
@@ -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"> {
@@ -252,8 +272,9 @@ export interface ActionDefinition {
252
272
  /** Generated input contract. */
253
273
  inputSchema: ActionSchema;
254
274
  /**
255
- * MCP `initialize` identity when this action is served by one MCP surface.
256
- * Bound environment tools record it on the tool span as `mcp.server.name`.
275
+ * MCP `initialize` identity and Hue provider/surface when this action is served by one MCP
276
+ * surface. Bound environment tools record it on the tool span as `mcp.server.name`,
277
+ * `mcp.server.version`, `hue.mcp.provider` and `hue.mcp.surface`.
257
278
  */
258
279
  mcp?: McpServerInfo;
259
280
  }
@@ -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. */