@hue-run/sdk 0.1.5 → 0.2.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.
Files changed (56) hide show
  1. package/ENVIRONMENTS.md +182 -0
  2. package/EVALUATIONS.md +12 -0
  3. package/README.md +192 -18
  4. package/dist/ai-sdk.d.ts +9 -1
  5. package/dist/ai-sdk.js +34 -8
  6. package/dist/client.d.ts +121 -6
  7. package/dist/client.js +329 -56
  8. package/dist/config.d.ts +11 -2
  9. package/dist/config.js +36 -7
  10. package/dist/environment/client.d.ts +73 -0
  11. package/dist/environment/client.js +209 -0
  12. package/dist/environment/tools.d.ts +30 -0
  13. package/dist/environment/tools.js +24 -0
  14. package/dist/environment/types.d.ts +429 -0
  15. package/dist/environment/types.js +1 -0
  16. package/dist/environment.d.ts +5 -0
  17. package/dist/environment.js +2 -0
  18. package/dist/evals/attempt.d.ts +454 -0
  19. package/dist/evals/attempt.js +687 -0
  20. package/dist/evals/client.d.ts +99 -5
  21. package/dist/evals/client.js +136 -7
  22. package/dist/evals/environment-evidence.d.ts +6 -0
  23. package/dist/evals/environment-evidence.js +123 -0
  24. package/dist/evals/environment-json.d.ts +3 -0
  25. package/dist/evals/environment-json.js +76 -0
  26. package/dist/evals/json.d.ts +9 -1
  27. package/dist/evals/json.js +14 -6
  28. package/dist/evals/runner.d.ts +61 -2
  29. package/dist/evals/runner.js +71 -9
  30. package/dist/evals/scorer-publication.d.ts +2 -0
  31. package/dist/evals/scorer-publication.js +84 -0
  32. package/dist/evals/scorers.d.ts +11 -0
  33. package/dist/evals/scorers.js +56 -5
  34. package/dist/evals/simulation.d.ts +184 -0
  35. package/dist/evals/simulation.js +603 -0
  36. package/dist/evals/types.d.ts +304 -0
  37. package/dist/evals.d.ts +5 -1
  38. package/dist/evals.js +3 -1
  39. package/dist/experimental-telemetry.d.ts +8 -0
  40. package/dist/experimental-telemetry.js +13 -0
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.js +2 -0
  43. package/dist/managed.d.ts +51 -1
  44. package/dist/managed.js +11 -1
  45. package/dist/privacy.d.ts +2 -0
  46. package/dist/privacy.js +16 -1
  47. package/dist/receipt.d.ts +12 -1
  48. package/dist/receipt.js +10 -1
  49. package/dist/safety.d.ts +1 -2
  50. package/dist/snapshot.js +4 -0
  51. package/dist/transport.d.ts +41 -9
  52. package/dist/transport.js +80 -22
  53. package/dist/types.d.ts +144 -8
  54. package/dist/version.d.ts +2 -0
  55. package/dist/version.js +3 -0
  56. package/package.json +51 -15
@@ -0,0 +1,73 @@
1
+ import type { ActionInput, ActionResult, CoverageGapInput, CoverageGapResult, CreateRunInput, Environment, EnvironmentDefinition, EnvironmentIdentity, EnvironmentPage, EnvironmentPageOptions, EnvironmentRun, EnvironmentSummary, EnvironmentVersion, EnvironmentVersionSummary, FinishRunInput, SealedRun, StepPage, StepPageOptions } from "./types.js";
2
+ /** Connection and retry options for {@link createEnvironmentClient}. */
3
+ export interface EnvironmentClientOptions {
4
+ /** Project service key sent as a bearer token; server-side only. */
5
+ apiKey: string;
6
+ /** Hue origin; defaults to `https://app.hue.run`. */
7
+ baseUrl?: string;
8
+ /** Per-request deadline in milliseconds. */
9
+ timeoutMillis?: number;
10
+ /** Attempts for idempotent run mutations, 1–10; defaults to 4. */
11
+ maxAttempts?: number;
12
+ }
13
+ /** Sanitized environment API failure that never includes response text or credentials. */
14
+ export declare class HueEnvironmentError extends Error {
15
+ /** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
16
+ readonly status?: number | undefined;
17
+ constructor(
18
+ /** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
19
+ status?: number | undefined);
20
+ }
21
+ /** Typed client for authored environments, isolated runs and immutable journals. */
22
+ export declare class EnvironmentClient {
23
+ /** Validated Hue origin. */
24
+ readonly baseUrl: string;
25
+ private readonly apiKey;
26
+ private readonly timeoutMillis;
27
+ private readonly maxAttempts;
28
+ constructor(options: EnvironmentClientOptions);
29
+ private send;
30
+ private request;
31
+ private requestOnce;
32
+ private page;
33
+ /** Creates an environment identity; this non-idempotent registry write is not retried. */
34
+ createEnvironment(input: EnvironmentIdentity): Promise<EnvironmentSummary>;
35
+ /** Lists active environment identities. */
36
+ listEnvironments(page?: EnvironmentPageOptions): Promise<EnvironmentPage>;
37
+ /** Reads one environment and its immutable version summaries. */
38
+ getEnvironment(id: string): Promise<Environment>;
39
+ /** Publishes an immutable definition; this non-idempotent registry write is not retried. */
40
+ publishVersion(environmentId: string, definition: EnvironmentDefinition): Promise<EnvironmentVersionSummary>;
41
+ /** Reads a full immutable environment version and generated action catalog. */
42
+ getVersion(id: string): Promise<EnvironmentVersion>;
43
+ /** Creates or recovers one fresh isolated world using a stable idempotency key. */
44
+ createRun(input: CreateRunInput): Promise<EnvironmentRun>;
45
+ /** Reads authoritative current or sealed world state. */
46
+ getRun(runId: string): Promise<{
47
+ id: string;
48
+ environmentVersionId: string;
49
+ executionId: string | null;
50
+ seed: string;
51
+ status: "open" | "completed" | "abandoned" | "expired";
52
+ stepCount: number;
53
+ maxSteps: number;
54
+ clockNs: string;
55
+ expiresAt: string;
56
+ createdAt: string;
57
+ sealedAt: string | null;
58
+ stateDigest: string;
59
+ finalState?: import("./types.js").JsonValue;
60
+ validity: "environment_incomplete" | "not_assessed";
61
+ coverageGap: import("./types.js").CoverageGap | null;
62
+ }>;
63
+ /** Record a known coverage gap with durable identity; retries reuse the exact request. */
64
+ recordCoverageGap(runId: string, input: CoverageGapInput): Promise<CoverageGapResult>;
65
+ /** Invokes an action; repeating an invocation identity replays its recorded result. */
66
+ act(runId: string, input: ActionInput): Promise<ActionResult>;
67
+ /** Pages the immutable journal by step ordinal. */
68
+ listSteps(runId: string, page?: StepPageOptions): Promise<StepPage>;
69
+ /** Seals a world as completed or abandoned and freezes its evidence. */
70
+ finishRun(runId: string, input: FinishRunInput): Promise<SealedRun>;
71
+ }
72
+ /** Creates a typed simulated-environment client. */
73
+ export declare function createEnvironmentClient(options: EnvironmentClientOptions): EnvironmentClient;
@@ -0,0 +1,209 @@
1
+ import { validateOptions } from "../config.js";
2
+ import { aggregateBounds, json, uuid, valueBounds } from "../evals/json.js";
3
+ /** Sanitized environment API failure that never includes response text or credentials. */
4
+ export class HueEnvironmentError extends Error {
5
+ status;
6
+ constructor(
7
+ /** HTTP status when Hue answered; absent for transport, timeout or parse failure. */
8
+ status) {
9
+ super(status
10
+ ? `Hue environment request failed (HTTP ${status})`
11
+ : "Hue environment connection or response failed");
12
+ this.status = status;
13
+ this.name = "HueEnvironmentError";
14
+ }
15
+ }
16
+ const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
17
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
18
+ const REQUEST_BOUNDS = { ...valueBounds, bytes: 1024 * 1024 };
19
+ /** The server bounds JSON inside each entity independently, then permits the parsed
20
+ * definition to contain up to 240 KB. The publication wrapper adds one node/depth
21
+ * level and a small fixed byte prefix. Aggregate bounds cover every server-valid
22
+ * definition without relaxing action/run request envelopes.
23
+ */
24
+ const ENVIRONMENT_PUBLICATION_BOUNDS = aggregateBounds(240_000 + 32);
25
+ /** Typed client for authored environments, isolated runs and immutable journals. */
26
+ export class EnvironmentClient {
27
+ /** Validated Hue origin. */
28
+ baseUrl;
29
+ apiKey;
30
+ timeoutMillis;
31
+ maxAttempts;
32
+ constructor(options) {
33
+ const validated = validateOptions({
34
+ ...options,
35
+ serviceName: "hue-environments",
36
+ captureContent: false,
37
+ });
38
+ this.baseUrl = validated.baseUrl;
39
+ this.apiKey = validated.apiKey;
40
+ this.timeoutMillis = validated.timeoutMillis;
41
+ const attempts = options.maxAttempts ?? 4;
42
+ if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10)
43
+ throw new RangeError("maxAttempts must be 1–10");
44
+ this.maxAttempts = attempts;
45
+ }
46
+ async send(method, path, payload) {
47
+ let response;
48
+ try {
49
+ response = await fetch(`${this.baseUrl}/api/v1${path}`, {
50
+ method,
51
+ headers: {
52
+ Authorization: `Bearer ${this.apiKey}`,
53
+ ...(payload ? { "Content-Type": "application/json" } : {}),
54
+ },
55
+ body: payload,
56
+ redirect: "error",
57
+ signal: AbortSignal.timeout(this.timeoutMillis),
58
+ });
59
+ }
60
+ catch {
61
+ throw new HueEnvironmentError();
62
+ }
63
+ if (!response.ok) {
64
+ await response.body?.cancel();
65
+ throw new HueEnvironmentError(response.status);
66
+ }
67
+ try {
68
+ const reader = response.body?.getReader();
69
+ if (!reader)
70
+ throw new Error("Missing response");
71
+ const chunks = [];
72
+ let size = 0;
73
+ try {
74
+ for (;;) {
75
+ const { done, value } = await reader.read();
76
+ if (done)
77
+ break;
78
+ size += value.byteLength;
79
+ if (size > MAX_RESPONSE_BYTES)
80
+ throw new Error("Oversized response");
81
+ chunks.push(value);
82
+ }
83
+ }
84
+ finally {
85
+ await reader.cancel();
86
+ }
87
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
88
+ }
89
+ catch {
90
+ throw new HueEnvironmentError();
91
+ }
92
+ }
93
+ async request(method, path, body) {
94
+ // Serialize once: a body this client cannot encode is a caller error that no retry fixes.
95
+ const payload = body === undefined
96
+ ? undefined
97
+ : JSON.stringify(json(Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined)), REQUEST_BOUNDS));
98
+ for (let attempt = 1;; attempt++) {
99
+ try {
100
+ return await this.send(method, path, payload);
101
+ }
102
+ catch (error) {
103
+ if (!(error instanceof HueEnvironmentError))
104
+ throw error;
105
+ const recoverable = error.status === undefined || RETRYABLE.has(error.status);
106
+ if (!recoverable || attempt >= this.maxAttempts)
107
+ throw error;
108
+ const backoff = Math.min(100 * 2 ** (attempt - 1), 2000);
109
+ await new Promise((resolve) => setTimeout(resolve, backoff + Math.random() * backoff));
110
+ }
111
+ }
112
+ }
113
+ requestOnce(method, path, body, bounds = REQUEST_BOUNDS) {
114
+ const payload = body === undefined
115
+ ? undefined
116
+ : JSON.stringify(json(Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined)), bounds));
117
+ return this.send(method, path, payload);
118
+ }
119
+ page(options = {}) {
120
+ const query = new URLSearchParams();
121
+ if (options.after)
122
+ query.set("after", uuid(options.after));
123
+ if (options.limit !== undefined) {
124
+ if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 100)
125
+ throw new RangeError("Page limit must be 1–100");
126
+ query.set("limit", String(options.limit));
127
+ }
128
+ return query.size ? `?${query}` : "";
129
+ }
130
+ /** Creates an environment identity; this non-idempotent registry write is not retried. */
131
+ createEnvironment(input) {
132
+ return this.requestOnce("POST", "/environments", input);
133
+ }
134
+ /** Lists active environment identities. */
135
+ listEnvironments(page) {
136
+ return this.request("GET", `/environments${this.page(page)}`);
137
+ }
138
+ /** Reads one environment and its immutable version summaries. */
139
+ getEnvironment(id) {
140
+ return this.request("GET", `/environments/${uuid(id)}`);
141
+ }
142
+ /** Publishes an immutable definition; this non-idempotent registry write is not retried. */
143
+ publishVersion(environmentId, definition) {
144
+ return this.requestOnce("POST", `/environments/${uuid(environmentId)}/versions`, { definition }, ENVIRONMENT_PUBLICATION_BOUNDS);
145
+ }
146
+ /** Reads a full immutable environment version and generated action catalog. */
147
+ getVersion(id) {
148
+ return this.request("GET", `/environment-versions/${uuid(id)}`);
149
+ }
150
+ /** Creates or recovers one fresh isolated world using a stable idempotency key. */
151
+ createRun(input) {
152
+ if (input.maxSteps !== undefined &&
153
+ (!Number.isInteger(input.maxSteps) || input.maxSteps < 1 || input.maxSteps > 500))
154
+ throw new RangeError("maxSteps must be 1–500");
155
+ if (input.ttlSeconds !== undefined &&
156
+ (!Number.isInteger(input.ttlSeconds) || input.ttlSeconds < 1 || input.ttlSeconds > 86_400))
157
+ throw new RangeError("ttlSeconds must be 1–86400");
158
+ if (input.seed !== undefined && !/^[a-f0-9]{32}$/.test(input.seed))
159
+ throw new TypeError("Seed must be 32 lowercase hexadecimal characters");
160
+ return this.request("POST", "/environment-runs", {
161
+ ...input,
162
+ environmentVersionId: uuid(input.environmentVersionId),
163
+ ...(input.executionId === undefined ? {} : { executionId: uuid(input.executionId) }),
164
+ });
165
+ }
166
+ /** Reads authoritative current or sealed world state. */
167
+ async getRun(runId) {
168
+ const run = await this.request("GET", `/environment-runs/${uuid(runId)}`);
169
+ return { validity: "not_assessed", coverageGap: null, ...run };
170
+ }
171
+ /** Record a known coverage gap with durable identity; retries reuse the exact request. */
172
+ recordCoverageGap(runId, input) {
173
+ uuid(input.idempotencyKey);
174
+ if (!input.args || typeof input.args !== "object" || Array.isArray(input.args))
175
+ throw new TypeError("Coverage gap arguments must be a JSON object");
176
+ json(input.args, { ...valueBounds, bytes: 16_000 });
177
+ return this.request("POST", `/environment-runs/${uuid(runId)}/coverage-gap`, input);
178
+ }
179
+ /** Invokes an action; repeating an invocation identity replays its recorded result. */
180
+ act(runId, input) {
181
+ return this.request("POST", `/environment-runs/${uuid(runId)}/actions`, {
182
+ ...input,
183
+ args: input.args ?? {},
184
+ });
185
+ }
186
+ /** Pages the immutable journal by step ordinal. */
187
+ listSteps(runId, page = {}) {
188
+ const query = new URLSearchParams();
189
+ if (page.after !== undefined) {
190
+ if (!Number.isInteger(page.after) || page.after < -1)
191
+ throw new RangeError("Step cursor must be an ordinal");
192
+ query.set("after", String(page.after));
193
+ }
194
+ if (page.limit !== undefined) {
195
+ if (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > 100)
196
+ throw new RangeError("Page limit must be 1–100");
197
+ query.set("limit", String(page.limit));
198
+ }
199
+ return this.request("GET", `/environment-runs/${uuid(runId)}/steps${query.size ? `?${query}` : ""}`);
200
+ }
201
+ /** Seals a world as completed or abandoned and freezes its evidence. */
202
+ finishRun(runId, input) {
203
+ return this.request("POST", `/environment-runs/${uuid(runId)}/finish`, input);
204
+ }
205
+ }
206
+ /** Creates a typed simulated-environment client. */
207
+ export function createEnvironmentClient(options) {
208
+ return new EnvironmentClient(options);
209
+ }
@@ -0,0 +1,30 @@
1
+ import type { Context } from "@opentelemetry/api";
2
+ import type { HueClient } from "../client.js";
3
+ import type { EnvironmentClient } from "./client.js";
4
+ import type { ActionSchema, EnvironmentRun, JsonValue, Observation } from "./types.js";
5
+ /** Framework-neutral callable generated from one environment action. */
6
+ export interface EnvironmentTool {
7
+ /** Tool name. */
8
+ name: string;
9
+ /** Agent-visible description. */
10
+ description?: string;
11
+ /** JSON Schema input contract. */
12
+ inputSchema: ActionSchema;
13
+ /** Executes the action and resolves with the world's observation. */
14
+ execute(args?: Record<string, JsonValue>): Promise<Observation>;
15
+ }
16
+ /** Dependencies and tracing context for {@link bindEnvironmentTools}. */
17
+ export interface BindEnvironmentToolsOptions {
18
+ /** Hue client used to record each action as tool telemetry. */
19
+ hue: HueClient;
20
+ /** Environment client bound to the same Hue origin. */
21
+ client: Pick<EnvironmentClient, "act">;
22
+ /** Fresh environment run whose closed catalog becomes callables. */
23
+ run: EnvironmentRun;
24
+ /** Optional parent span context for generated tool spans. */
25
+ parentContext?: Context;
26
+ /** Optional durable invocation-ID factory for caller-owned resume state. */
27
+ invocationId?(action: string): string;
28
+ }
29
+ /** Binds a run's generated catalog to plain local callables without changing the agent framework. */
30
+ export declare function bindEnvironmentTools(options: BindEnvironmentToolsOptions): Record<string, EnvironmentTool>;
@@ -0,0 +1,24 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /** Binds a run's generated catalog to plain local callables without changing the agent framework. */
3
+ export function bindEnvironmentTools(options) {
4
+ const tools = {};
5
+ for (const action of options.run.actions) {
6
+ tools[action.name] = {
7
+ name: action.name,
8
+ ...(action.description === undefined ? {} : { description: action.description }),
9
+ inputSchema: action.inputSchema,
10
+ execute: async (args = {}) => {
11
+ const invocationId = options.invocationId?.(action.name) ?? randomUUID();
12
+ const execute = async () => (await options.client.act(options.run.id, {
13
+ invocationId,
14
+ action: action.name,
15
+ args,
16
+ })).observation;
17
+ return options.hue.tool(action.name, args, execute, {
18
+ parentContext: options.parentContext,
19
+ });
20
+ },
21
+ };
22
+ }
23
+ return tools;
24
+ }