@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/CLI.md +11 -6
- package/ENVIRONMENTS.md +36 -5
- package/README.md +105 -4
- package/dist/ai-sdk.d.ts +3 -3
- package/dist/ai-sdk.js +3 -3
- package/dist/cli/env-file.d.ts +22 -0
- package/dist/cli/env-file.js +21 -0
- package/dist/cli/eval.js +21 -6
- package/dist/cli/login.d.ts +1 -1
- package/dist/cli/login.js +9 -4
- package/dist/client.d.ts +31 -3
- package/dist/client.js +188 -6
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/environment/client.d.ts +10 -2
- package/dist/environment/client.js +43 -12
- package/dist/environment/tools.d.ts +2 -2
- package/dist/environment/tools.js +2 -2
- package/dist/environment/types.d.ts +33 -12
- package/dist/environment.d.ts +1 -1
- package/dist/environment.js +1 -1
- package/dist/evals/environment-target.d.ts +5 -0
- package/dist/evals/environment-target.js +56 -5
- package/dist/evals/scenarios.d.ts +1 -1
- package/dist/evals/scenarios.js +4 -1
- package/dist/evals/types.d.ts +6 -1
- package/dist/experimental-telemetry.d.ts +3 -2
- package/dist/experimental-telemetry.js +3 -2
- package/dist/inline-files.d.ts +10 -0
- package/dist/inline-files.js +86 -0
- package/dist/privacy.js +54 -5
- package/dist/provider-tools.d.ts +39 -0
- package/dist/provider-tools.js +249 -0
- package/dist/tool-definitions.d.ts +20 -0
- package/dist/tool-definitions.js +274 -0
- package/dist/transport.js +4 -2
- package/dist/types.d.ts +76 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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:
|
|
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" &&
|
|
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
|
|
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.
|
|
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
|
-
|
|
127
|
-
|
|
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) =>
|
|
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`
|
|
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`
|
|
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
|
|
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
|
}
|
package/dist/environment.d.ts
CHANGED
|
@@ -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";
|
package/dist/environment.js
CHANGED
|
@@ -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. */
|