@hue-run/sdk 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLI.md +27 -14
- package/ENVIRONMENTS.md +63 -1
- package/README.md +121 -5
- 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 +100 -30
- 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 +199 -7
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/environment/client.d.ts +16 -2
- package/dist/environment/client.js +46 -3
- package/dist/environment/tools.d.ts +2 -2
- package/dist/environment/tools.js +2 -2
- package/dist/environment/types.d.ts +134 -4
- package/dist/environment/world.d.ts +50 -0
- package/dist/environment/world.js +105 -0
- package/dist/environment.d.ts +2 -0
- package/dist/environment.js +1 -0
- package/dist/evals/environment-target.d.ts +53 -2
- package/dist/evals/environment-target.js +114 -10
- package/dist/evals/local-worker.d.ts +12 -5
- package/dist/evals/local-worker.js +13 -5
- package/dist/evals/runner.d.ts +1 -1
- package/dist/evals/runner.js +2 -2
- package/dist/evals/simulation.d.ts +14 -5
- package/dist/evals/simulation.js +26 -8
- 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 +222 -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,28 @@ 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
43
|
/** A usable metadata label: a non-blank string of at most 256 characters. */
|
|
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());
|
|
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();
|
|
43
62
|
}
|
|
44
63
|
/**
|
|
45
64
|
* Runs `work` exactly once with `active` as OpenTelemetry's current context, so instrumentations
|
|
@@ -96,6 +115,7 @@ class ContextualTracer {
|
|
|
96
115
|
...options.attributes,
|
|
97
116
|
...(active?.sessionId ? { "gen_ai.conversation.id": active.sessionId } : {}),
|
|
98
117
|
...(active?.userId ? { "user.id": active.userId } : {}),
|
|
118
|
+
...(active?.workspaceId ? { "hue.workspace.id": active.workspaceId } : {}),
|
|
99
119
|
},
|
|
100
120
|
}, parent ?? active?.context ?? context.active()), this.failed);
|
|
101
121
|
}
|
|
@@ -238,6 +258,7 @@ export class HueClient {
|
|
|
238
258
|
context: options.parentContext ?? inherited?.context ?? context.active(),
|
|
239
259
|
sessionId: identifier(options.sessionId ?? inherited?.sessionId),
|
|
240
260
|
userId: identifier(options.userId ?? inherited?.userId),
|
|
261
|
+
workspaceId: identifier(options.workspaceId ?? inherited?.workspaceId),
|
|
241
262
|
model: inherited?.model,
|
|
242
263
|
};
|
|
243
264
|
span = this.storage.run(active, () => this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, active.context));
|
|
@@ -301,18 +322,19 @@ export class HueClient {
|
|
|
301
322
|
* `options.callId` is recorded as `gen_ai.tool.call.id`, like the Python `call_id=` keyword.
|
|
302
323
|
* `options.mcp` records the MCP `initialize` `serverInfo` as `mcp.server.name` /
|
|
303
324
|
* `mcp.server.version` so a generic tool name can be attributed to the server that
|
|
304
|
-
* handled it. Pass `client.getServerVersion()`.
|
|
325
|
+
* handled it. Pass `client.getServerVersion()`. `mcp.provider` / `mcp.surface` record the Hue
|
|
326
|
+
* provider and surface as `hue.mcp.provider` / `hue.mcp.surface`.
|
|
305
327
|
*/
|
|
306
328
|
async tool(name, input, execute, options = {}) {
|
|
307
329
|
const attributes = {
|
|
308
330
|
"gen_ai.operation.name": "execute_tool",
|
|
309
331
|
"gen_ai.tool.name": name,
|
|
310
332
|
};
|
|
311
|
-
const stamp = (key, value) => {
|
|
333
|
+
const stamp = (key, value, valid = isLabel) => {
|
|
312
334
|
if (value === undefined)
|
|
313
335
|
return;
|
|
314
336
|
// A blank or non-string label is omitted and counted; the tool call itself still runs.
|
|
315
|
-
if (
|
|
337
|
+
if (valid(value))
|
|
316
338
|
attributes[key] = value;
|
|
317
339
|
else if (this.enabled && !this.closed)
|
|
318
340
|
this.transport.instrumentationFailure();
|
|
@@ -320,6 +342,8 @@ export class HueClient {
|
|
|
320
342
|
stamp("gen_ai.tool.call.id", options.callId);
|
|
321
343
|
stamp("mcp.server.name", options.mcp?.name);
|
|
322
344
|
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);
|
|
323
347
|
return this.withSpan(`execute_tool ${name}`, async ({ span }) => {
|
|
324
348
|
this.setContent(span, "gen_ai.tool.call.arguments", input);
|
|
325
349
|
const result = await execute();
|
|
@@ -337,7 +361,8 @@ export class HueClient {
|
|
|
337
361
|
* `gen_ai.request.model` and `gen_ai.provider.name`. The argument order matches `withSpan`. The
|
|
338
362
|
* handle's `setInput`/`setOutput` record `gen_ai.input.messages` / `gen_ai.output.messages`,
|
|
339
363
|
* which should use the GenAI semantic-convention message shape; `recordMessages` inside the
|
|
340
|
-
* callback inherits the request metadata.
|
|
364
|
+
* callback inherits the request metadata. `options.systemInstructions` and `options.tools` are
|
|
365
|
+
* recorded as `gen_ai.system_instructions` and `gen_ai.tool.definitions`, content like `input`.
|
|
341
366
|
*/
|
|
342
367
|
async model(model, callback, options) {
|
|
343
368
|
// A disabled or closed client creates no span, so invalid metadata is not an instrumentation
|
|
@@ -357,7 +382,7 @@ export class HueClient {
|
|
|
357
382
|
? `${operation} ${requestModel}`
|
|
358
383
|
: label(options.name, `${operation} ${requestModel}`);
|
|
359
384
|
const metadata = { operation, provider, requestModel };
|
|
360
|
-
const { sessionId, userId, parentContext, input } = options ?? {};
|
|
385
|
+
const { sessionId, userId, workspaceId, parentContext, input, systemInstructions, tools, } = options ?? {};
|
|
361
386
|
return this.withSpan(name, (span) => {
|
|
362
387
|
const handle = {
|
|
363
388
|
...span,
|
|
@@ -366,12 +391,17 @@ export class HueClient {
|
|
|
366
391
|
};
|
|
367
392
|
if (input !== undefined)
|
|
368
393
|
handle.setInput(input);
|
|
394
|
+
if (systemInstructions !== undefined)
|
|
395
|
+
this.setContent(span.span, "gen_ai.system_instructions", systemInstructions);
|
|
396
|
+
if (tools !== undefined)
|
|
397
|
+
this.setContent(span.span, "gen_ai.tool.definitions", tools);
|
|
369
398
|
// recordMessages inside the callback copies this request metadata onto its log record.
|
|
370
399
|
const store = this.storage.getStore() ?? { context: span.context };
|
|
371
400
|
return this.storage.run({ ...store, model: metadata }, () => callback(handle));
|
|
372
401
|
}, {
|
|
373
402
|
sessionId,
|
|
374
403
|
userId,
|
|
404
|
+
workspaceId,
|
|
375
405
|
parentContext,
|
|
376
406
|
kind: SpanKind.CLIENT,
|
|
377
407
|
attributes: {
|
|
@@ -462,6 +492,8 @@ export class HueClient {
|
|
|
462
492
|
body["gen_ai.input.messages"] = messages.input;
|
|
463
493
|
if (messages.output !== undefined)
|
|
464
494
|
body["gen_ai.output.messages"] = messages.output;
|
|
495
|
+
if (messages.systemInstructions !== undefined)
|
|
496
|
+
body["gen_ai.system_instructions"] = messages.systemInstructions;
|
|
465
497
|
// Request metadata is inherited only when the record correlates with the enclosing helper
|
|
466
498
|
// scope; an unrelated explicit context carries caller-supplied values alone.
|
|
467
499
|
const enclosing = explicitContext === undefined || explicitContext === store?.context ? store : undefined;
|
|
@@ -492,6 +524,166 @@ export class HueClient {
|
|
|
492
524
|
this.transport.instrumentationFailure("logs");
|
|
493
525
|
}
|
|
494
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Records the tools a model provider executed itself while producing `response`, which no
|
|
529
|
+
* `hue.tool()` call saw: OpenAI Responses `mcp_call`, `web_search_call`, `file_search_call` and
|
|
530
|
+
* `code_interpreter_call` items, and Anthropic Messages `mcp_tool_use` / `server_tool_use`
|
|
531
|
+
* blocks with their result blocks. Each becomes an `execute_tool {name}` child span of the active
|
|
532
|
+
* (or given) context with `gen_ai.tool.type` `extension` and `gen_ai.tool.call.id`; MCP calls add
|
|
533
|
+
* `mcp.server.name` (the provider's label, or the `servers` entry for it). Arguments and results
|
|
534
|
+
* follow `captureContent`; a failed call carries `error.type` and ERROR status. An OpenAI
|
|
535
|
+
* `mcp_list_tools` item becomes a `tools/list` child span carrying that server's tools as
|
|
536
|
+
* `gen_ai.tool.definitions`. Call it inside `hue.model()` so the spans nest under the model call
|
|
537
|
+
* and `provider` defaults to its provider; pass `request` to record each server's host as
|
|
538
|
+
* `server.address`. The spans have no duration of their own: the provider ran the tools inside
|
|
539
|
+
* the model request. Unreadable items are skipped and counted; nothing is thrown.
|
|
540
|
+
*/
|
|
541
|
+
recordProviderToolCalls(response, options = {}) {
|
|
542
|
+
if (!this.enabled || this.closed)
|
|
543
|
+
return;
|
|
544
|
+
try {
|
|
545
|
+
const store = this.storage.getStore();
|
|
546
|
+
const provider = options.provider ?? hostedToolProvider(store?.model?.provider);
|
|
547
|
+
if (provider === undefined)
|
|
548
|
+
throw new TypeError("Unknown provider for hosted tool calls");
|
|
549
|
+
const parent = options.parentContext ?? store?.context ?? context.active();
|
|
550
|
+
const addresses = hostedServerAddresses(provider, options.request);
|
|
551
|
+
const activity = hostedToolActivity(provider, response);
|
|
552
|
+
if (activity.skipped > 0)
|
|
553
|
+
this.transport.instrumentationFailure("traces", undefined, activity.skipped);
|
|
554
|
+
const server = (label) => {
|
|
555
|
+
const attributes = {};
|
|
556
|
+
if (label === undefined)
|
|
557
|
+
return attributes;
|
|
558
|
+
const info = options.servers?.[label];
|
|
559
|
+
for (const [key, value] of [
|
|
560
|
+
["mcp.server.name", info?.name ?? label],
|
|
561
|
+
["mcp.server.version", info?.version],
|
|
562
|
+
["hue.mcp.provider", info?.provider],
|
|
563
|
+
["hue.mcp.surface", info?.surface],
|
|
564
|
+
]) {
|
|
565
|
+
if (value === undefined)
|
|
566
|
+
continue;
|
|
567
|
+
if (isLabel(value))
|
|
568
|
+
attributes[key] = value;
|
|
569
|
+
else
|
|
570
|
+
this.transport.instrumentationFailure();
|
|
571
|
+
}
|
|
572
|
+
const address = addresses.get(label);
|
|
573
|
+
if (address !== undefined)
|
|
574
|
+
attributes["server.address"] = address;
|
|
575
|
+
return attributes;
|
|
576
|
+
};
|
|
577
|
+
const fail = (span, type) => {
|
|
578
|
+
span.setAttribute("error.type", type);
|
|
579
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
580
|
+
};
|
|
581
|
+
for (const call of activity.calls) {
|
|
582
|
+
const span = this.tracer.startSpan(`execute_tool ${call.name}`, {
|
|
583
|
+
kind: SpanKind.INTERNAL,
|
|
584
|
+
attributes: {
|
|
585
|
+
"gen_ai.operation.name": "execute_tool",
|
|
586
|
+
"gen_ai.tool.name": call.name,
|
|
587
|
+
"gen_ai.tool.type": "extension",
|
|
588
|
+
...(call.callId === undefined ? {} : { "gen_ai.tool.call.id": call.callId }),
|
|
589
|
+
...server(call.server),
|
|
590
|
+
},
|
|
591
|
+
}, parent);
|
|
592
|
+
if (call.arguments !== undefined)
|
|
593
|
+
this.setContent(span, "gen_ai.tool.call.arguments", call.arguments);
|
|
594
|
+
if (call.result !== undefined)
|
|
595
|
+
this.setContent(span, "gen_ai.tool.call.result", call.result);
|
|
596
|
+
if (call.errorType !== undefined)
|
|
597
|
+
fail(span, call.errorType);
|
|
598
|
+
span.end();
|
|
599
|
+
}
|
|
600
|
+
for (const listing of activity.listings) {
|
|
601
|
+
const span = this.tracer.startSpan("tools/list", {
|
|
602
|
+
kind: SpanKind.INTERNAL,
|
|
603
|
+
attributes: { "mcp.method.name": "tools/list", ...server(listing.server) },
|
|
604
|
+
}, parent);
|
|
605
|
+
this.setContent(span, "gen_ai.tool.definitions", listing.definitions);
|
|
606
|
+
if (listing.errorType !== undefined)
|
|
607
|
+
fail(span, listing.errorType);
|
|
608
|
+
span.end();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
this.transport.instrumentationFailure();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Adds a `hue.file` event to the active (or given) span for a file the work read, received or
|
|
617
|
+
* produced: `hue.file.sha256`, `hue.file.role`, `hue.file.media_type`, `hue.file.size` when known
|
|
618
|
+
* and, when `captureContent` is true, `hue.file.name`. `data` is hashed and measured locally and
|
|
619
|
+
* never exported. The event is metadata, so it is recorded in both capture modes. An invalid
|
|
620
|
+
* record, or one without an active span, is omitted and counted, never thrown; an invalid name
|
|
621
|
+
* alone is omitted and counted while the rest is recorded.
|
|
622
|
+
*/
|
|
623
|
+
recordFile(file, explicitContext) {
|
|
624
|
+
if (!this.enabled || this.closed)
|
|
625
|
+
return;
|
|
626
|
+
try {
|
|
627
|
+
const span = trace.getSpan(explicitContext ?? this.storage.getStore()?.context ?? context.active());
|
|
628
|
+
if (!span?.isRecording())
|
|
629
|
+
throw new Error("File records require an active span");
|
|
630
|
+
const { role, mediaType, data, name } = file;
|
|
631
|
+
if (role !== "input" && role !== "attachment" && role !== "output")
|
|
632
|
+
throw new TypeError("Invalid file role");
|
|
633
|
+
if (!isTextLabel(mediaType))
|
|
634
|
+
throw new TypeError("Invalid media type");
|
|
635
|
+
let sha256 = typeof file.sha256 === "string" ? file.sha256.toLowerCase() : file.sha256;
|
|
636
|
+
let byteSize = file.byteSize;
|
|
637
|
+
if (data !== undefined) {
|
|
638
|
+
const bytes = typeof data === "string"
|
|
639
|
+
? (() => {
|
|
640
|
+
// Buffer.byteLength measures UTF-8 without allocating the copy that hashing would
|
|
641
|
+
// otherwise require. Reject before Buffer.from/createHash can retain large input.
|
|
642
|
+
if (data.length > MAX_FILE_DATA_BYTES ||
|
|
643
|
+
Buffer.byteLength(data, "utf8") > MAX_FILE_DATA_BYTES)
|
|
644
|
+
throw new RangeError("File data exceeds Hue's 25 MiB limit");
|
|
645
|
+
return Buffer.from(data, "utf8");
|
|
646
|
+
})()
|
|
647
|
+
: data instanceof Uint8Array
|
|
648
|
+
? data.byteLength <= MAX_FILE_DATA_BYTES
|
|
649
|
+
? data
|
|
650
|
+
: (() => {
|
|
651
|
+
throw new RangeError("File data exceeds Hue's 25 MiB limit");
|
|
652
|
+
})()
|
|
653
|
+
: undefined;
|
|
654
|
+
if (!bytes)
|
|
655
|
+
throw new TypeError("File data must be bytes or a string");
|
|
656
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
657
|
+
// A caller-supplied digest or size must describe the same bytes.
|
|
658
|
+
if ((sha256 !== undefined && sha256 !== digest) ||
|
|
659
|
+
(byteSize !== undefined && byteSize !== bytes.byteLength))
|
|
660
|
+
throw new TypeError("File digest or size does not match its data");
|
|
661
|
+
sha256 = digest;
|
|
662
|
+
byteSize = bytes.byteLength;
|
|
663
|
+
}
|
|
664
|
+
if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/.test(sha256))
|
|
665
|
+
throw new TypeError("A file needs a SHA-256 digest or its data");
|
|
666
|
+
if (byteSize !== undefined && (!Number.isSafeInteger(byteSize) || byteSize < 0))
|
|
667
|
+
throw new TypeError("Invalid file size");
|
|
668
|
+
const attributes = {
|
|
669
|
+
"hue.file.sha256": sha256,
|
|
670
|
+
"hue.file.role": role,
|
|
671
|
+
"hue.file.media_type": mediaType,
|
|
672
|
+
};
|
|
673
|
+
if (byteSize !== undefined)
|
|
674
|
+
attributes["hue.file.size"] = byteSize;
|
|
675
|
+
if (this.captureContent && name !== undefined) {
|
|
676
|
+
if (isTextLabel(name))
|
|
677
|
+
attributes["hue.file.name"] = name;
|
|
678
|
+
else
|
|
679
|
+
this.transport.instrumentationFailure();
|
|
680
|
+
}
|
|
681
|
+
span.addEvent("hue.file", attributes);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
this.transport.instrumentationFailure();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
495
687
|
setContent(span, key, value) {
|
|
496
688
|
if (!this.enabled || this.closed || !this.captureContent)
|
|
497
689
|
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]";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionInput, ActionResult, CoverageGapInput, CoverageGapResult, CreateRunInput, Environment, PublishableEnvironmentDefinition, EnvironmentIdentity, EnvironmentPage, EnvironmentPageOptions, EnvironmentRun, EnvironmentSummary, EnvironmentVersion, EnvironmentVersionSummary, FinishRunInput, SealedRun, StepPage, StepPageOptions } from "./types.js";
|
|
1
|
+
import type { ActionInput, ActionResult, CoverageGapInput, CoverageGapResult, CreateRunInput, Environment, PublishableEnvironmentDefinition, EnvironmentIdentity, EnvironmentPage, EnvironmentPageOptions, EnvironmentRun, EnvironmentSummary, EnvironmentVersion, EnvironmentVersionSummary, FinishRunInput, SealedRun, StepPage, StepPageOptions, WorldEvidence, WorldEvidenceOptions } from "./types.js";
|
|
2
2
|
/** Connection and retry options for {@link createEnvironmentClient}. */
|
|
3
3
|
export interface EnvironmentClientOptions {
|
|
4
4
|
/** Project service key sent as a bearer token; server-side only. */
|
|
@@ -14,9 +14,13 @@ export interface EnvironmentClientOptions {
|
|
|
14
14
|
export declare class HueEnvironmentError extends Error {
|
|
15
15
|
/** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
|
|
16
16
|
readonly status?: number | undefined;
|
|
17
|
+
/** Hue's `Retry-After` in milliseconds, bounded, when a 429 or 503 carried one. */
|
|
18
|
+
readonly retryAfterMs?: number | undefined;
|
|
17
19
|
constructor(
|
|
18
20
|
/** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
|
|
19
|
-
status?: number | undefined
|
|
21
|
+
status?: number | undefined,
|
|
22
|
+
/** Hue's `Retry-After` in milliseconds, bounded, when a 429 or 503 carried one. */
|
|
23
|
+
retryAfterMs?: number | undefined);
|
|
20
24
|
}
|
|
21
25
|
/** Typed client for authored environments, isolated runs and immutable journals. */
|
|
22
26
|
export declare class EnvironmentClient {
|
|
@@ -57,6 +61,13 @@ export declare class EnvironmentClient {
|
|
|
57
61
|
sealedAt: string | null;
|
|
58
62
|
stateDigest: string;
|
|
59
63
|
finalState?: import("./types.js").JsonValue;
|
|
64
|
+
worldId?: string;
|
|
65
|
+
lifecycle?: import("./types.js").WorldLifecycle;
|
|
66
|
+
completingUntil?: string | null;
|
|
67
|
+
traceExternalId?: string | null;
|
|
68
|
+
surfaces?: import("./types.js").WorldSurface[];
|
|
69
|
+
flags?: import("./types.js").WorldFlag[];
|
|
70
|
+
connection?: null;
|
|
60
71
|
validity: "environment_incomplete" | "not_assessed";
|
|
61
72
|
coverageGap: import("./types.js").CoverageGap | null;
|
|
62
73
|
}>;
|
|
@@ -68,6 +79,9 @@ export declare class EnvironmentClient {
|
|
|
68
79
|
listSteps(runId: string, page?: StepPageOptions): Promise<StepPage>;
|
|
69
80
|
/** Seals a world as completed or abandoned and freezes its evidence. */
|
|
70
81
|
finishRun(runId: string, input: FinishRunInput): Promise<SealedRun>;
|
|
82
|
+
/** Reads a sealed world's evaluator-only evidence with the project key; a world token never
|
|
83
|
+
* can. An open world answers 409 until it is sealed. */
|
|
84
|
+
getEvidence(runId: string, options?: WorldEvidenceOptions): Promise<WorldEvidence>;
|
|
71
85
|
}
|
|
72
86
|
/** Creates a typed simulated-environment client. */
|
|
73
87
|
export declare function createEnvironmentClient(options: EnvironmentClientOptions): EnvironmentClient;
|
|
@@ -3,17 +3,38 @@ import { aggregateBounds, json, uuid, valueBounds } from "../evals/json.js";
|
|
|
3
3
|
/** Sanitized environment API failure that never includes response text or credentials. */
|
|
4
4
|
export class HueEnvironmentError extends Error {
|
|
5
5
|
status;
|
|
6
|
+
retryAfterMs;
|
|
6
7
|
constructor(
|
|
7
8
|
/** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
|
|
8
|
-
status
|
|
9
|
+
status,
|
|
10
|
+
/** Hue's `Retry-After` in milliseconds, bounded, when a 429 or 503 carried one. */
|
|
11
|
+
retryAfterMs) {
|
|
9
12
|
super(status
|
|
10
13
|
? `Hue environment request failed (HTTP ${status})`
|
|
11
14
|
: "Hue environment connection or response failed");
|
|
12
15
|
this.status = status;
|
|
16
|
+
this.retryAfterMs = retryAfterMs;
|
|
13
17
|
this.name = "HueEnvironmentError";
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
|
|
21
|
+
/** A `Retry-After` longer than this waits this long: Hue asks for a second, never minutes. */
|
|
22
|
+
const MAX_RETRY_AFTER_MS = 10_000;
|
|
23
|
+
const TRACEPARENT = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/;
|
|
24
|
+
/** Version 00 with real identifiers: an all-zero trace or span ID is invalid, and Hue refuses it. */
|
|
25
|
+
function validTraceparent(value) {
|
|
26
|
+
if (!TRACEPARENT.test(value))
|
|
27
|
+
return false;
|
|
28
|
+
const [, traceId, spanId] = value.split("-");
|
|
29
|
+
return !/^0+$/.test(traceId) && !/^0+$/.test(spanId);
|
|
30
|
+
}
|
|
31
|
+
/** Whole seconds only, as Hue sends them; a date or garbage is ignored. */
|
|
32
|
+
function retryAfterMillis(response) {
|
|
33
|
+
const header = response.headers.get("retry-after");
|
|
34
|
+
if (header === null || !/^\d{1,6}$/.test(header.trim()))
|
|
35
|
+
return undefined;
|
|
36
|
+
return Math.min(Number(header.trim()) * 1000, MAX_RETRY_AFTER_MS);
|
|
37
|
+
}
|
|
17
38
|
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
18
39
|
const REQUEST_BOUNDS = { ...valueBounds, bytes: 1024 * 1024 };
|
|
19
40
|
/** The server bounds JSON inside each entity independently, then permits the parsed
|
|
@@ -62,7 +83,7 @@ export class EnvironmentClient {
|
|
|
62
83
|
}
|
|
63
84
|
if (!response.ok) {
|
|
64
85
|
await response.body?.cancel();
|
|
65
|
-
throw new HueEnvironmentError(response.status);
|
|
86
|
+
throw new HueEnvironmentError(response.status, response.status === 429 || response.status === 503 ? retryAfterMillis(response) : undefined);
|
|
66
87
|
}
|
|
67
88
|
try {
|
|
68
89
|
const reader = response.body?.getReader();
|
|
@@ -105,8 +126,10 @@ export class EnvironmentClient {
|
|
|
105
126
|
const recoverable = error.status === undefined || RETRYABLE.has(error.status);
|
|
106
127
|
if (!recoverable || attempt >= this.maxAttempts)
|
|
107
128
|
throw error;
|
|
129
|
+
// Hue's admission refusals say how long to wait; anything else backs off.
|
|
108
130
|
const backoff = Math.min(100 * 2 ** (attempt - 1), 2000);
|
|
109
|
-
|
|
131
|
+
const wait = error.retryAfterMs ?? backoff + Math.random() * backoff;
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
110
133
|
}
|
|
111
134
|
}
|
|
112
135
|
}
|
|
@@ -157,6 +180,13 @@ export class EnvironmentClient {
|
|
|
157
180
|
throw new RangeError("ttlSeconds must be 1–86400");
|
|
158
181
|
if (input.seed !== undefined && !/^[a-f0-9]{32}$/.test(input.seed))
|
|
159
182
|
throw new TypeError("Seed must be 32 lowercase hexadecimal characters");
|
|
183
|
+
if (input.traceparent !== undefined && !validTraceparent(input.traceparent))
|
|
184
|
+
throw new TypeError("traceparent must be a version-00 W3C trace context");
|
|
185
|
+
if (input.agentRevision !== undefined &&
|
|
186
|
+
(typeof input.agentRevision !== "string" ||
|
|
187
|
+
input.agentRevision.length < 1 ||
|
|
188
|
+
input.agentRevision.length > 256))
|
|
189
|
+
throw new RangeError("agentRevision must be 1–256 characters");
|
|
160
190
|
return this.request("POST", "/environment-runs", {
|
|
161
191
|
...input,
|
|
162
192
|
environmentVersionId: uuid(input.environmentVersionId),
|
|
@@ -202,6 +232,19 @@ export class EnvironmentClient {
|
|
|
202
232
|
finishRun(runId, input) {
|
|
203
233
|
return this.request("POST", `/environment-runs/${uuid(runId)}/finish`, input);
|
|
204
234
|
}
|
|
235
|
+
/** Reads a sealed world's evaluator-only evidence with the project key; a world token never
|
|
236
|
+
* can. An open world answers 409 until it is sealed. */
|
|
237
|
+
getEvidence(runId, options = {}) {
|
|
238
|
+
const query = new URLSearchParams();
|
|
239
|
+
if (options.section !== undefined) {
|
|
240
|
+
if (!["all", "start", "end", "diff", "ledger"].includes(options.section))
|
|
241
|
+
throw new TypeError("Evidence section must be all, start, end, diff or ledger");
|
|
242
|
+
query.set("section", options.section);
|
|
243
|
+
}
|
|
244
|
+
if (options.bodies !== undefined)
|
|
245
|
+
query.set("bodies", options.bodies ? "true" : "false");
|
|
246
|
+
return this.request("GET", `/environment-runs/${uuid(runId)}/evidence${query.size ? `?${query}` : ""}`);
|
|
247
|
+
}
|
|
205
248
|
}
|
|
206
249
|
/** Creates a typed simulated-environment client. */
|
|
207
250
|
export function createEnvironmentClient(options) {
|
|
@@ -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 = {};
|