@hue-run/sdk 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLI.md +3 -3
- package/ENVIRONMENTS.md +34 -4
- package/dist/client.js +10 -20
- package/dist/environment/client.d.ts +10 -2
- package/dist/environment/client.js +43 -12
- package/dist/environment/types.d.ts +30 -10
- 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/provider-tools.js +30 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CLI.md
CHANGED
|
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
|
|
|
42
42
|
The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
|
|
43
43
|
`capture_content=False`. For a supported application, setup installs the dependency and adds the
|
|
44
44
|
managed import and middleware registration to the existing entrypoint; an unreferenced helper is
|
|
45
|
-
not a completed integration. TypeScript uses `@hue-run/sdk@0.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.9.0`, `@opentelemetry/api@1.9.1` and
|
|
46
46
|
`@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
|
|
47
47
|
Content capture requires an ordinary account-managed key and a later explicit application decision.
|
|
48
48
|
|
|
@@ -305,7 +305,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
|
|
|
305
305
|
# Set project to an existing supported fixture; use the same directory on resume.
|
|
306
306
|
project=/absolute/path/to/supported-fixture
|
|
307
307
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
308
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
308
|
+
--archive .artifacts/typescript/hue-run-sdk-0.9.0.tgz \
|
|
309
309
|
--origin https://STAGING_ORIGIN \
|
|
310
310
|
--project "$project" --command setup \
|
|
311
311
|
--evidence .context/setup-staging-before-claim.json
|
|
@@ -316,7 +316,7 @@ the private local handoff and finish the real browser claim, then reconcile the
|
|
|
316
316
|
|
|
317
317
|
```sh
|
|
318
318
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
319
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
319
|
+
--archive .artifacts/typescript/hue-run-sdk-0.9.0.tgz \
|
|
320
320
|
--origin https://STAGING_ORIGIN \
|
|
321
321
|
--project "$project" --command claim \
|
|
322
322
|
--evidence .context/setup-staging-after-claim.json
|
package/ENVIRONMENTS.md
CHANGED
|
@@ -126,6 +126,18 @@ Hue canonicalizes valid synthetic-principal UUIDs to lowercase, and repository r
|
|
|
126
126
|
the same before comparing immutable digests, so casing-only UUID changes reuse the stored
|
|
127
127
|
version without dropping provider bindings.
|
|
128
128
|
|
|
129
|
+
Each Gmail provider instance names its mailbox carrier in `configuration`
|
|
130
|
+
(`GmailMailboxConfiguration`, narrowed by `kind`). `gmail_mailbox/v1` binds `messagesCollection`
|
|
131
|
+
and `draftsCollection` in the simplified shape Hue's `hue.gmail.*` actions read.
|
|
132
|
+
`gmail_mailbox/v2` adds `labelsCollection` and binds all three in the entity shapes the Gmail
|
|
133
|
+
mirrors serve: messages may also carry `cc`, `bcc`, `htmlBody`, `snippet`, attachment metadata,
|
|
134
|
+
`sizeEstimate`, `historyId` and `raw`, each draft names a `DRAFT`-labelled message, and threads
|
|
135
|
+
are derived from each message's `threadId`. Only the simulation gateway serves it: the pinned
|
|
136
|
+
provider-profile preflight above reports `profile_unavailable` for a `gmail_mailbox/v2`
|
|
137
|
+
instance. Its `mailboxAddress` must be the synthetic owner `owner@example.test` (compared
|
|
138
|
+
case-insensitively) or publication is refused, and a definition whose provider instances all use
|
|
139
|
+
it may publish with `actions: []`.
|
|
140
|
+
|
|
129
141
|
```ts
|
|
130
142
|
const definition = {
|
|
131
143
|
kind: "repository" as const,
|
|
@@ -230,22 +242,38 @@ try {
|
|
|
230
242
|
await spawnAgent({ env: child, mcpConfigPath: config.path });
|
|
231
243
|
} finally {
|
|
232
244
|
await config.dispose();
|
|
233
|
-
await environmentClient.finishRun(run.id, {
|
|
245
|
+
const finished = await environmentClient.finishRun(run.id, {
|
|
234
246
|
idempotencyKey: `execution:${executionId}:completed`,
|
|
235
247
|
status: "completed",
|
|
236
248
|
});
|
|
249
|
+
const graceEnd = Date.parse(finished.completingUntil ?? "") || Date.now();
|
|
250
|
+
await new Promise((resolve) =>
|
|
251
|
+
setTimeout(resolve, Math.min(10_000, Math.max(0, graceEnd - Date.now()))),
|
|
252
|
+
);
|
|
253
|
+
const sealDeadline = Date.now() + 30_000;
|
|
254
|
+
let sealed = false;
|
|
255
|
+
while (Date.now() < sealDeadline) {
|
|
256
|
+
if ((await environmentClient.getRun(run.id)).status !== "open") {
|
|
257
|
+
sealed = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
261
|
+
}
|
|
262
|
+
if (!sealed) throw new Error("World was not sealed after its completion grace");
|
|
237
263
|
}
|
|
238
264
|
```
|
|
239
265
|
|
|
240
266
|
`runSimulation`, `runLocalAgent` and `hue eval` do this for you: they create the world with the
|
|
241
267
|
execution, the stable key, the case span's context and the agent revision, pass the handoff as
|
|
242
|
-
`context.world`, and finish before returning so telemetry is flushed and the
|
|
243
|
-
afterwards. For one compatibility release `context.mcp` is the world's first MCP mirror with the
|
|
268
|
+
`context.world`, and finish and wait for the seal before returning so telemetry is flushed and the
|
|
269
|
+
execution completed afterwards. For one compatibility release `context.mcp` is the world's first MCP mirror with the
|
|
244
270
|
world token, so an adapter that read `HUE_MCP_URL` and `HUE_MCP_TOKEN` keeps working;
|
|
245
271
|
`agentEnvironment` sets those names too unless `legacyMcpVariables: false`. A gateway world binds
|
|
246
272
|
no Hue-native `tools` (Hue refuses them); a world created while the gateway is off keeps its tools
|
|
247
273
|
and the `hue_sim_` capability and emits a one-time `DeprecationWarning`.
|
|
248
274
|
|
|
275
|
+
A refusal such as 409 `simulation_gateway_required` exposes its validated server code as `HueEnvironmentError.diagnostic`.
|
|
276
|
+
|
|
249
277
|
`agentEnvironment` removes Hue control-plane credentials from the child by default: `HUE_API_KEY`,
|
|
250
278
|
`HUE_MCP_KEY` and any variable whose value is a `hue_sk_`, `hue_mcp_` or `hue_attempt_`
|
|
251
279
|
credential. Pass `includeHueCredentials: true` only for an agent that must call Hue's own API.
|
|
@@ -253,7 +281,9 @@ Nothing in these helpers logs the token; keep it out of your own logs and checkp
|
|
|
253
281
|
|
|
254
282
|
Finish answers `lifecycle: "completing"` with `sealedAt: null` for a gateway world: the seal
|
|
255
283
|
follows a 5 s grace so in-flight writes land, and a late finish answers 409, which the helpers
|
|
256
|
-
treat as the seal they can no longer change.
|
|
284
|
+
treat as the seal they can no longer change. Completing an execution while the world is open
|
|
285
|
+
returns 409, so direct clients read the run after the finish response's `completingUntil`
|
|
286
|
+
until it is no longer open; the helper waits through transient failures. `getEvidence(runId, { section, bodies })` reads the
|
|
257
287
|
sealed world's evaluator-only evidence (start and end state, the diff, the call ledger, coverage,
|
|
258
288
|
fingerprint) with the project key; a world token can never read it. The client honors Hue's
|
|
259
289
|
`Retry-After` on 429 and 503.
|
package/dist/client.js
CHANGED
|
@@ -40,7 +40,7 @@ function identifier(value) {
|
|
|
40
40
|
throw new TypeError("Session/user/workspace identifiers must contain 1–4096 valid characters");
|
|
41
41
|
return value;
|
|
42
42
|
}
|
|
43
|
-
/** A usable metadata label:
|
|
43
|
+
/** A usable metadata label: non-blank, at most 256 UTF-16 code units, with no NUL or unpaired surrogate. */
|
|
44
44
|
function isLabel(value) {
|
|
45
45
|
return (typeof value === "string" &&
|
|
46
46
|
value.trim() !== "" &&
|
|
@@ -48,18 +48,6 @@ function isLabel(value) {
|
|
|
48
48
|
!value.includes("\u0000") &&
|
|
49
49
|
value.isWellFormed());
|
|
50
50
|
}
|
|
51
|
-
/** A source label uses the stricter wire-safe validation without changing existing labels. */
|
|
52
|
-
function isSourceLabel(value) {
|
|
53
|
-
return (typeof value === "string" &&
|
|
54
|
-
value.trim() !== "" &&
|
|
55
|
-
value.length <= 256 &&
|
|
56
|
-
!value.includes("\u0000") &&
|
|
57
|
-
value.isWellFormed());
|
|
58
|
-
}
|
|
59
|
-
/** A label that is also free of NUL and unpaired surrogates, which export would reject. */
|
|
60
|
-
function isTextLabel(value) {
|
|
61
|
-
return isLabel(value) && !value.includes("\u0000") && value.isWellFormed();
|
|
62
|
-
}
|
|
63
51
|
/**
|
|
64
52
|
* Runs `work` exactly once with `active` as OpenTelemetry's current context, so instrumentations
|
|
65
53
|
* that use the global API parent under the Hue span. Without a registered context manager
|
|
@@ -330,11 +318,11 @@ export class HueClient {
|
|
|
330
318
|
"gen_ai.operation.name": "execute_tool",
|
|
331
319
|
"gen_ai.tool.name": name,
|
|
332
320
|
};
|
|
333
|
-
const stamp = (key, value
|
|
321
|
+
const stamp = (key, value) => {
|
|
334
322
|
if (value === undefined)
|
|
335
323
|
return;
|
|
336
324
|
// A blank or non-string label is omitted and counted; the tool call itself still runs.
|
|
337
|
-
if (
|
|
325
|
+
if (isLabel(value))
|
|
338
326
|
attributes[key] = value;
|
|
339
327
|
else if (this.enabled && !this.closed)
|
|
340
328
|
this.transport.instrumentationFailure();
|
|
@@ -342,8 +330,8 @@ export class HueClient {
|
|
|
342
330
|
stamp("gen_ai.tool.call.id", options.callId);
|
|
343
331
|
stamp("mcp.server.name", options.mcp?.name);
|
|
344
332
|
stamp("mcp.server.version", options.mcp?.version);
|
|
345
|
-
stamp("hue.mcp.provider", options.mcp?.provider
|
|
346
|
-
stamp("hue.mcp.surface", options.mcp?.surface
|
|
333
|
+
stamp("hue.mcp.provider", options.mcp?.provider);
|
|
334
|
+
stamp("hue.mcp.surface", options.mcp?.surface);
|
|
347
335
|
return this.withSpan(`execute_tool ${name}`, async ({ span }) => {
|
|
348
336
|
this.setContent(span, "gen_ai.tool.call.arguments", input);
|
|
349
337
|
const result = await execute();
|
|
@@ -555,7 +543,9 @@ export class HueClient {
|
|
|
555
543
|
const attributes = {};
|
|
556
544
|
if (label === undefined)
|
|
557
545
|
return attributes;
|
|
558
|
-
const info = options.servers
|
|
546
|
+
const info = options.servers && Object.hasOwn(options.servers, label)
|
|
547
|
+
? options.servers[label]
|
|
548
|
+
: undefined;
|
|
559
549
|
for (const [key, value] of [
|
|
560
550
|
["mcp.server.name", info?.name ?? label],
|
|
561
551
|
["mcp.server.version", info?.version],
|
|
@@ -630,7 +620,7 @@ export class HueClient {
|
|
|
630
620
|
const { role, mediaType, data, name } = file;
|
|
631
621
|
if (role !== "input" && role !== "attachment" && role !== "output")
|
|
632
622
|
throw new TypeError("Invalid file role");
|
|
633
|
-
if (!
|
|
623
|
+
if (!isLabel(mediaType))
|
|
634
624
|
throw new TypeError("Invalid media type");
|
|
635
625
|
let sha256 = typeof file.sha256 === "string" ? file.sha256.toLowerCase() : file.sha256;
|
|
636
626
|
let byteSize = file.byteSize;
|
|
@@ -673,7 +663,7 @@ export class HueClient {
|
|
|
673
663
|
if (byteSize !== undefined)
|
|
674
664
|
attributes["hue.file.size"] = byteSize;
|
|
675
665
|
if (this.captureContent && name !== undefined) {
|
|
676
|
-
if (
|
|
666
|
+
if (isLabel(name))
|
|
677
667
|
attributes["hue.file.name"] = name;
|
|
678
668
|
else
|
|
679
669
|
this.transport.instrumentationFailure();
|
|
@@ -16,12 +16,18 @@ export declare class HueEnvironmentError extends Error {
|
|
|
16
16
|
readonly status?: number | undefined;
|
|
17
17
|
/** Hue's `Retry-After` in milliseconds, bounded, when a 429 or 503 carried one. */
|
|
18
18
|
readonly retryAfterMs?: number | undefined;
|
|
19
|
+
/** The server's `X-Hue-Diagnostic` code, when it is a short snake-case value. */
|
|
20
|
+
readonly diagnostic?: string | undefined;
|
|
19
21
|
constructor(
|
|
20
22
|
/** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
|
|
21
23
|
status?: number | undefined,
|
|
22
24
|
/** Hue's `Retry-After` in milliseconds, bounded, when a 429 or 503 carried one. */
|
|
23
|
-
retryAfterMs?: number | undefined
|
|
25
|
+
retryAfterMs?: number | undefined,
|
|
26
|
+
/** The server's `X-Hue-Diagnostic` code, when it is a short snake-case value. */
|
|
27
|
+
diagnostic?: string | undefined);
|
|
24
28
|
}
|
|
29
|
+
/** A connection failure or a status the client retries; seal polling continues through these. */
|
|
30
|
+
export declare function isTransientEnvironmentError(error: unknown): boolean;
|
|
25
31
|
/** Typed client for authored environments, isolated runs and immutable journals. */
|
|
26
32
|
export declare class EnvironmentClient {
|
|
27
33
|
/** Validated Hue origin. */
|
|
@@ -47,7 +53,9 @@ export declare class EnvironmentClient {
|
|
|
47
53
|
/** Creates or recovers one fresh isolated world using a stable idempotency key. */
|
|
48
54
|
createRun(input: CreateRunInput): Promise<EnvironmentRun>;
|
|
49
55
|
/** Reads authoritative current or sealed world state. */
|
|
50
|
-
getRun(runId: string
|
|
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. */
|
|
@@ -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"> {
|
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. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { HueEnvironmentError } from "../environment/client.js";
|
|
2
|
+
import { HueEnvironmentError, isTransientEnvironmentError, } from "../environment/client.js";
|
|
3
3
|
import { bindEnvironmentTools } from "../environment/tools.js";
|
|
4
4
|
import { legacyMcpCapability, worldHandoff } from "../environment/world.js";
|
|
5
5
|
import { actualAgentManifestV2, attemptBaselineV2, projectMcpConnectionV2, requestedAttemptProvidersV2, validateAttemptConnectionBundleV2, } from "./attempt.js";
|
|
@@ -42,22 +42,73 @@ export function pinRequestedAttemptV2(requested, config) {
|
|
|
42
42
|
}
|
|
43
43
|
return { ...requested, expectedAgentManifestDigest: baseline.data.expectedAgentManifestDigest };
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
45
|
+
/** The completion grace is five seconds; cap an unexpectedly distant timestamp and let reads
|
|
46
|
+
* force the seal after the grace. */
|
|
47
|
+
export const MAX_GRACE_WAIT_MS = 10_000;
|
|
48
|
+
export const SEAL_POLL_MS = 250;
|
|
49
|
+
export const SEAL_WAIT_MS = 30_000;
|
|
50
|
+
/** Finish, then wait for the authoritative run to leave open. */
|
|
46
51
|
async function seal(client, runId, executionId, status) {
|
|
52
|
+
let completingUntil;
|
|
47
53
|
try {
|
|
48
|
-
await client.finishRun(runId, {
|
|
54
|
+
const finished = await client.finishRun(runId, {
|
|
49
55
|
idempotencyKey: `execution:${executionId}:${status}`,
|
|
50
56
|
status,
|
|
51
57
|
});
|
|
58
|
+
if (finished.lifecycle === "completing")
|
|
59
|
+
completingUntil = finished.completingUntil ?? null;
|
|
52
60
|
}
|
|
53
61
|
catch (error) {
|
|
54
|
-
// A gateway world answers 409 once it is completing, sealed or expired
|
|
55
|
-
// outcome the caller wanted or the one it can no longer change.
|
|
62
|
+
// A gateway world answers 409 once it is completing, sealed or expired.
|
|
56
63
|
const recovered = await client.getRun(runId).catch(() => undefined);
|
|
57
64
|
if (recovered?.status !== status &&
|
|
58
65
|
recovered?.status !== "expired" &&
|
|
59
66
|
!(recovered?.status === "open" && recovered.lifecycle === "completing"))
|
|
60
67
|
throw new TargetOutcomeUncertainError(executionId, { cause: error });
|
|
68
|
+
if (recovered?.status === "open")
|
|
69
|
+
completingUntil = recovered.completingUntil ?? null;
|
|
70
|
+
}
|
|
71
|
+
if (completingUntil !== undefined) {
|
|
72
|
+
try {
|
|
73
|
+
await awaitSeal(client, runId, completingUntil);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new TargetOutcomeUncertainError(executionId, { cause: error });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Wait out a completing world's grace, then read until the server seals it. */
|
|
81
|
+
async function awaitSeal(client, runId, completingUntil) {
|
|
82
|
+
const graceEnd = Date.parse(completingUntil ?? "");
|
|
83
|
+
let wait = Number.isFinite(graceEnd)
|
|
84
|
+
? Math.min(Math.max(0, graceEnd - Date.now()), MAX_GRACE_WAIT_MS)
|
|
85
|
+
: 0;
|
|
86
|
+
const deadline = performance.now() + wait + SEAL_WAIT_MS;
|
|
87
|
+
for (;;) {
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
89
|
+
const remaining = deadline - performance.now();
|
|
90
|
+
if (remaining <= 0)
|
|
91
|
+
throw new Error(`World ${runId} was not sealed after its completion grace`);
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timer = setTimeout(() => controller.abort(), remaining);
|
|
94
|
+
try {
|
|
95
|
+
if ((await client.getRun(runId, { signal: controller.signal })).status !== "open")
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!isTransientEnvironmentError(error))
|
|
100
|
+
throw error;
|
|
101
|
+
if (performance.now() >= deadline)
|
|
102
|
+
throw new Error(`World ${runId} was not sealed after its completion grace`, {
|
|
103
|
+
cause: error,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
if (performance.now() >= deadline)
|
|
110
|
+
throw new Error(`World ${runId} was not sealed after its completion grace`);
|
|
111
|
+
wait = SEAL_POLL_MS;
|
|
61
112
|
}
|
|
62
113
|
}
|
|
63
114
|
const deprecations = new Set();
|
|
@@ -10,7 +10,7 @@ export interface ScenarioPins {
|
|
|
10
10
|
datasetId: string;
|
|
11
11
|
/** Pinned dataset version; frozen only when `saved` is true. */
|
|
12
12
|
datasetVersionId: string;
|
|
13
|
-
/** Pinned scorer versions; a Scenario
|
|
13
|
+
/** Pinned scorer versions; a Scenario's publication lists them, its outcome scorer first. */
|
|
14
14
|
scorerVersionIds: string[];
|
|
15
15
|
/** Pinned simulated-world version, or `null` when the selection does not pin one. */
|
|
16
16
|
environmentVersionId: string | null;
|
package/dist/evals/scenarios.js
CHANGED
|
@@ -74,7 +74,10 @@ async function pinsFromScenario(client, scenario, dataset) {
|
|
|
74
74
|
name: dataset.name,
|
|
75
75
|
datasetId: scenario.publication.datasetId,
|
|
76
76
|
datasetVersionId: version.id,
|
|
77
|
-
|
|
77
|
+
// Publications list every pin, the outcome scorer first; older ones carry the single field.
|
|
78
|
+
scorerVersionIds: scenario.publication.scorerVersionIds?.length
|
|
79
|
+
? [...new Set(scenario.publication.scorerVersionIds)]
|
|
80
|
+
: [scenario.publication.scorerVersionId],
|
|
78
81
|
environmentVersionId: scenario.publication.environmentVersionId,
|
|
79
82
|
saved: version.frozenAt !== null,
|
|
80
83
|
revision: version.revision,
|
package/dist/evals/types.d.ts
CHANGED
|
@@ -879,8 +879,13 @@ export interface CaseConversionPublication {
|
|
|
879
879
|
environmentVersionId: string;
|
|
880
880
|
/** Scorer identity of the published outcome checks. */
|
|
881
881
|
scorerId: string;
|
|
882
|
-
/** Immutable scorer version pinned by the Scenario. */
|
|
882
|
+
/** Immutable scorer version pinned by the Scenario for its outcome checks. */
|
|
883
883
|
scorerVersionId: string;
|
|
884
|
+
/**
|
|
885
|
+
* Every immutable scorer version the Scenario pins, `scorerVersionId` first. Absent on
|
|
886
|
+
* Scenarios published before publications listed their pins.
|
|
887
|
+
*/
|
|
888
|
+
scorerVersionIds?: string[];
|
|
884
889
|
}
|
|
885
890
|
/** A Scenario read by {@link EvaluationClient.getCaseConversion}. Extra server fields are ignored. */
|
|
886
891
|
export interface CaseConversion extends Partial<Omit<CaseConversionSummary, "id" | "status">> {
|
package/dist/provider-tools.js
CHANGED
|
@@ -5,6 +5,30 @@ const MAX_PROVIDER_SERVERS = 512;
|
|
|
5
5
|
function isItem(value) {
|
|
6
6
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
7
|
}
|
|
8
|
+
/** Only provider-executed tool items contribute to invalid-item diagnostics when a response is
|
|
9
|
+
* truncated; messages and reasoning are harmless response content. */
|
|
10
|
+
function isProviderToolItem(provider, value) {
|
|
11
|
+
if (!isItem(value) || typeof value.type !== "string")
|
|
12
|
+
return false;
|
|
13
|
+
if (provider === "openai")
|
|
14
|
+
return (value.type === "mcp_call" ||
|
|
15
|
+
value.type === "mcp_list_tools" ||
|
|
16
|
+
value.type === "web_search_call" ||
|
|
17
|
+
value.type === "file_search_call" ||
|
|
18
|
+
value.type === "code_interpreter_call");
|
|
19
|
+
return value.type === "mcp_tool_use" || value.type === "server_tool_use";
|
|
20
|
+
}
|
|
21
|
+
function countProviderToolItems(provider, items, start = 0) {
|
|
22
|
+
let count = 0;
|
|
23
|
+
for (const key of Object.keys(items)) {
|
|
24
|
+
const index = Number(key);
|
|
25
|
+
if (!Number.isInteger(index) || index < start || index >= items.length || String(index) !== key)
|
|
26
|
+
continue;
|
|
27
|
+
if (isProviderToolItem(provider, items[index]))
|
|
28
|
+
count++;
|
|
29
|
+
}
|
|
30
|
+
return count;
|
|
31
|
+
}
|
|
8
32
|
function text(value) {
|
|
9
33
|
return typeof value === "string" &&
|
|
10
34
|
value.trim() !== "" &&
|
|
@@ -14,6 +38,9 @@ function text(value) {
|
|
|
14
38
|
? value
|
|
15
39
|
: undefined;
|
|
16
40
|
}
|
|
41
|
+
function errorCode(value) {
|
|
42
|
+
return typeof value === "string" && /^[a-z0-9_]{1,64}$/.test(value) ? value : "error";
|
|
43
|
+
}
|
|
17
44
|
/** MCP arguments arrive as a JSON string; record the structure when it parses, else the text. */
|
|
18
45
|
function jsonArguments(value) {
|
|
19
46
|
if (typeof value !== "string")
|
|
@@ -30,7 +57,7 @@ function jsonArguments(value) {
|
|
|
30
57
|
/** OpenAI Responses `output` items. Built-in tools are named by their kind; MCP calls by tool. */
|
|
31
58
|
function openaiCalls(items, activity) {
|
|
32
59
|
const count = Math.min(items.length, MAX_PROVIDER_ITEMS);
|
|
33
|
-
activity.skipped += items
|
|
60
|
+
activity.skipped += countProviderToolItems("openai", items, count);
|
|
34
61
|
for (let index = 0; index < count; index++) {
|
|
35
62
|
const item = items[index];
|
|
36
63
|
if (!isItem(item))
|
|
@@ -121,7 +148,7 @@ function anthropicCalls(blocks, activity) {
|
|
|
121
148
|
const results = new Map();
|
|
122
149
|
const count = Math.min(blocks.length, MAX_PROVIDER_ITEMS);
|
|
123
150
|
const truncated = blocks.length > MAX_PROVIDER_ITEMS;
|
|
124
|
-
activity.skipped += blocks
|
|
151
|
+
activity.skipped += countProviderToolItems("anthropic", blocks, count);
|
|
125
152
|
for (let index = 0; index < count; index++) {
|
|
126
153
|
const block = blocks[index];
|
|
127
154
|
if (isItem(block) &&
|
|
@@ -152,7 +179,7 @@ function anthropicCalls(blocks, activity) {
|
|
|
152
179
|
if (result?.is_error === true)
|
|
153
180
|
errorType = "mcp_error";
|
|
154
181
|
else if (isItem(content) && typeof content.type === "string" && content.type.endsWith("_error"))
|
|
155
|
-
errorType =
|
|
182
|
+
errorType = errorCode(content.error_code);
|
|
156
183
|
activity.calls.push({
|
|
157
184
|
name,
|
|
158
185
|
callId,
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Package version shared by the instrumentation scope and the export User-Agent. */
|
|
2
|
-
export declare const sdkVersion = "0.
|
|
2
|
+
export declare const sdkVersion = "0.9.0";
|
package/dist/version.js
CHANGED