@hue-run/sdk 0.11.4 → 0.12.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/EVALUATIONS.md +25 -9
- package/dist/cli/eval.js +17 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +36 -24
- package/dist/evals/client.d.ts +25 -5
- package/dist/evals/client.js +93 -15
- package/dist/evals/environment-target.d.ts +15 -8
- package/dist/evals/environment-target.js +61 -21
- package/dist/evals/local-worker.d.ts +38 -3
- package/dist/evals/local-worker.js +158 -34
- package/dist/evals/runner.d.ts +24 -0
- package/dist/evals/runner.js +154 -12
- package/dist/evals.d.ts +3 -3
- package/dist/evals.js +2 -2
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +76 -15
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { HueEnvironmentError, isTransientEnvironmentError, } from "../environmen
|
|
|
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";
|
|
6
|
-
import { TargetCancelledError, TargetOutcomeUncertainError } from "./runner.js";
|
|
6
|
+
import { TargetCancelledError, TargetOutcomeUncertainError, TargetTimeoutError } from "./runner.js";
|
|
7
7
|
export function requestedAttemptV2(options) {
|
|
8
8
|
const requested = options.requestedProviders !== undefined;
|
|
9
9
|
const selected = options.mcpSurface !== undefined;
|
|
@@ -47,6 +47,9 @@ export function pinRequestedAttemptV2(requested, config) {
|
|
|
47
47
|
export const MAX_GRACE_WAIT_MS = 10_000;
|
|
48
48
|
export const SEAL_POLL_MS = 250;
|
|
49
49
|
export const SEAL_WAIT_MS = 30_000;
|
|
50
|
+
/** How long past the world's `expiresAt` the target is given before it is stopped: room for a
|
|
51
|
+
* last call in flight at the deadline and for clock skew between the worker and Hue. */
|
|
52
|
+
export const DEADLINE_MARGIN_MS = 5_000;
|
|
50
53
|
const SEAL_TIMING = {
|
|
51
54
|
maxGraceWaitMs: MAX_GRACE_WAIT_MS,
|
|
52
55
|
pollMs: SEAL_POLL_MS,
|
|
@@ -145,21 +148,28 @@ export function caseTraceparent(span) {
|
|
|
145
148
|
}
|
|
146
149
|
const gatewayStates = new Map();
|
|
147
150
|
/**
|
|
148
|
-
* Whether the deployment serves the simulation gateway, from
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
151
|
+
* Whether the deployment serves the simulation gateway, from a credential-free probe of the
|
|
152
|
+
* gateway's reserved health path with no app named (`/api/sim/_hue/health`), so the answer does
|
|
153
|
+
* not depend on which apps the deployment mirrors. The gateway answers it on its own host with
|
|
154
|
+
* its health, 200 with `gateway: "simulation"`, and on the application host with a refusal that
|
|
155
|
+
* carries `x-hue-diagnostic` (`unmirrored_provider`: the reserved segment is no app); either is
|
|
156
|
+
* on, since only the enabled gateway writes that header. The disabled handler's empty 404 (no
|
|
157
|
+
* `x-hue-diagnostic`) is off, and anything else (a network failure, a timeout, a redirect, another
|
|
158
|
+
* status or body) is unknown. Probed only after a create was refused; on and off are remembered
|
|
159
|
+
* per origin, unknown is probed again next time.
|
|
153
160
|
*/
|
|
154
161
|
export function gatewayState(baseUrl, fetchImpl = fetch) {
|
|
155
162
|
const origin = new URL(baseUrl).origin;
|
|
156
163
|
const remembered = gatewayStates.get(origin);
|
|
157
164
|
if (remembered)
|
|
158
165
|
return remembered;
|
|
159
|
-
const probe = fetchImpl(`${origin}/api/sim/
|
|
166
|
+
const probe = fetchImpl(`${origin}/api/sim/_hue/health`, {
|
|
167
|
+
redirect: "error",
|
|
168
|
+
signal: AbortSignal.timeout(5_000),
|
|
169
|
+
})
|
|
160
170
|
.then(async (response) => {
|
|
161
171
|
if (response.status === 404)
|
|
162
|
-
return response.headers.has("x-hue-diagnostic") ? "
|
|
172
|
+
return response.headers.has("x-hue-diagnostic") ? "on" : "off";
|
|
163
173
|
if (!response.ok)
|
|
164
174
|
return "unknown";
|
|
165
175
|
const body = (await response.json());
|
|
@@ -307,20 +317,50 @@ export async function runEnvironmentTarget(options) {
|
|
|
307
317
|
if (options.signal?.aborted)
|
|
308
318
|
throw new TargetCancelledError();
|
|
309
319
|
await progress({ type: "target_started", environmentRunId: run.id });
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
// The world's deadline bounds the target: once the world expires its mirrors refuse every
|
|
321
|
+
// call, so an agent still running can achieve nothing more. The target is told through its
|
|
322
|
+
// signal a moment after the deadline and, whether or not it listens, its case ends as a
|
|
323
|
+
// `TargetTimeout` and the world is sealed abandoned; a promise that never settles is left
|
|
324
|
+
// behind rather than holding the run open for good.
|
|
325
|
+
const expiresAt = Date.parse(run.expiresAt);
|
|
326
|
+
const deadline = new AbortController();
|
|
327
|
+
let timer;
|
|
328
|
+
const timedOut = new Promise((_, reject) => {
|
|
329
|
+
if (!Number.isFinite(expiresAt))
|
|
330
|
+
return;
|
|
331
|
+
timer = setTimeout(() => {
|
|
332
|
+
// The timeout is decided before the target is told: a target that rejects with its
|
|
333
|
+
// own abort error on the signal must not win the race and be saved as its own error.
|
|
334
|
+
const error = new TargetTimeoutError(run.id, run.expiresAt);
|
|
335
|
+
reject(error);
|
|
336
|
+
deadline.abort(error);
|
|
337
|
+
}, Math.max(0, expiresAt + DEADLINE_MARGIN_MS - Date.now()));
|
|
323
338
|
});
|
|
339
|
+
let output;
|
|
340
|
+
try {
|
|
341
|
+
output = await Promise.race([
|
|
342
|
+
options.target(options.inputs, {
|
|
343
|
+
config: context.config,
|
|
344
|
+
item: context.item,
|
|
345
|
+
executionId: context.executionId,
|
|
346
|
+
environmentRunId: run.id,
|
|
347
|
+
trace: { traceId: context.span.traceId, spanId: context.span.spanId },
|
|
348
|
+
tools,
|
|
349
|
+
...(world ? { world } : {}),
|
|
350
|
+
...(mcp ? { mcp } : {}),
|
|
351
|
+
...(connectionBundle ? { connectionBundle } : {}),
|
|
352
|
+
files: structuredClone(context.files),
|
|
353
|
+
outputDirectory: context.outputDirectory,
|
|
354
|
+
signal: options.signal
|
|
355
|
+
? AbortSignal.any([options.signal, deadline.signal])
|
|
356
|
+
: deadline.signal,
|
|
357
|
+
}),
|
|
358
|
+
timedOut,
|
|
359
|
+
]);
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
clearTimeout(timer);
|
|
363
|
+
}
|
|
324
364
|
await seal(options.environmentClient, run.id, context.executionId, "completed", timing);
|
|
325
365
|
finalized = true;
|
|
326
366
|
await Promise.resolve(progress({ type: "world_sealed", environmentRunId: run.id })).catch(() => undefined);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { HueClient } from "../client.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type EnvironmentClient } from "../environment/client.js";
|
|
3
3
|
import type { EnvironmentTool } from "../environment/tools.js";
|
|
4
4
|
import type { WorldHandoff } from "../environment/types.js";
|
|
5
5
|
import type { ActualAgentManifestInputV2, AttemptConnectionBundleV2, RequestedAttemptProviderV2 } from "./attempt.js";
|
|
6
6
|
import type { EvaluationClient } from "./client.js";
|
|
7
7
|
import { type RunExperimentTargetContext, type RunnerReport, type TelemetryNotAccepted } from "./runner.js";
|
|
8
|
-
import type { ExperimentCase, JsonValue, LocalAgentRegistration, LocalFile, LocalScorer, TargetResult } from "./types.js";
|
|
8
|
+
import type { ExperimentCase, JsonValue, LocalAgentClaim, LocalAgentRegistration, LocalFile, LocalScorer, TargetResult } from "./types.js";
|
|
9
9
|
/** Capability strings a registration declares; Hue offers only matching cases. */
|
|
10
10
|
export declare const localAgentCapabilities: {
|
|
11
11
|
/** Cases pinned to a hosted synthetic world. */
|
|
@@ -58,6 +58,9 @@ export interface LocalAgentTargetContext {
|
|
|
58
58
|
/** Private directory for this case, removed after it; return generated files with
|
|
59
59
|
* `withFiles`. */
|
|
60
60
|
outputDirectory: string;
|
|
61
|
+
/** Ends when the worker stops or the world's deadline passes; a target that listens stops
|
|
62
|
+
* its work, and one that does not is left behind when its case ends as a `TargetTimeout`. */
|
|
63
|
+
signal?: AbortSignal;
|
|
61
64
|
}
|
|
62
65
|
/** Candidate-visible context for one queued ordinary case run directly on this machine. */
|
|
63
66
|
export interface LocalAgentDirectContext {
|
|
@@ -109,8 +112,27 @@ export interface RunLocalAgentOptions {
|
|
|
109
112
|
pollIntervalMillis?: number;
|
|
110
113
|
/** Stops polling cooperatively; does not cancel an active callback. */
|
|
111
114
|
signal?: AbortSignal;
|
|
112
|
-
/**
|
|
115
|
+
/** Stop after this many runs settle: completed, or given up as `attention`. Useful for
|
|
116
|
+
* one-shot jobs and deterministic acceptance. Omit to keep polling. */
|
|
113
117
|
maxRuns?: number;
|
|
118
|
+
/** How many times one run is resumed after a transient failure (a request Hue refused
|
|
119
|
+
* transiently past the client's own retries, a world seal not read back, a lost network,
|
|
120
|
+
* telemetry not accepted) before the worker gives it up as `attention`. Default 5, range
|
|
121
|
+
* 1–20. A failure that would recur (an uncertain target outcome, an unserializable output, a
|
|
122
|
+
* refusal Hue decided on, an input the SDK refuses) is attention at once. */
|
|
123
|
+
maxRunAttempts?: number;
|
|
124
|
+
/** The lifetime of each world the worker creates for a case, in seconds (1–86400); the
|
|
125
|
+
* server's default, an hour, applies when omitted. The target is stopped when the world
|
|
126
|
+
* expires: its case ends as a `TargetTimeout` error and the world is sealed abandoned. */
|
|
127
|
+
worldTtlSeconds?: number;
|
|
128
|
+
/** Called when a poll (registration or claim) fails transiently; the worker waits and polls
|
|
129
|
+
* again. A refusal it cannot recover from (a revoked key, a disabled agent) is thrown. */
|
|
130
|
+
onPollError?(error: unknown): void | Promise<void>;
|
|
131
|
+
/** Called when a claimed run's attempt fails: `retry` says the worker keeps the claim and
|
|
132
|
+
* resumes it after `waitMillis`; `attention` says the run was given up and needs a project
|
|
133
|
+
* member (requeue or cancel); `refused` says Hue no longer accepts this worker's outcome for
|
|
134
|
+
* the run (it was released or cancelled meanwhile). */
|
|
135
|
+
onRunFailed?(failure: LocalRunFailure): void | Promise<void>;
|
|
114
136
|
/** Emit a one-time `DeprecationWarning` when the deployment serves a legacy world; on by default. */
|
|
115
137
|
deprecationWarnings?: boolean;
|
|
116
138
|
/** Opt into the experiment's immutable V2 provider profile. These three values are
|
|
@@ -151,6 +173,19 @@ export declare function registeredCapabilities(options: {
|
|
|
151
173
|
/** The ordinary-case callback, when supplied. */
|
|
152
174
|
directTarget?: unknown;
|
|
153
175
|
}): string[];
|
|
176
|
+
/** Why a claimed run's attempt ended without completing, and what the worker did about it. */
|
|
177
|
+
export interface LocalRunFailure extends LocalAgentClaim {
|
|
178
|
+
/** What failed. */
|
|
179
|
+
error: unknown;
|
|
180
|
+
/** Failed attempts of this run in this process, this one included; 0 when not counted. */
|
|
181
|
+
attempt: number;
|
|
182
|
+
/** `retry`: the claim is kept and the run resumes after `waitMillis`. `attention`: the run
|
|
183
|
+
* was given up for a project member to requeue or cancel. `refused`: Hue no longer accepts
|
|
184
|
+
* this worker's outcome for the run, which was released or cancelled meanwhile. */
|
|
185
|
+
outcome: "retry" | "attention" | "refused";
|
|
186
|
+
/** The wait before the run resumes, for `retry`. */
|
|
187
|
+
waitMillis?: number;
|
|
188
|
+
}
|
|
154
189
|
/**
|
|
155
190
|
* Starts an outbound-only worker for one fixed local agent entry point. Hue chooses
|
|
156
191
|
* only the registered key/revision; no command or source is received from the cloud.
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isTransientEnvironmentError } from "../environment/client.js";
|
|
4
|
+
import { HueApiError, isTransientApiError } from "./client.js";
|
|
5
|
+
import { HueExportError } from "../transport.js";
|
|
3
6
|
import { CheckpointStore, checkpointPath, checkpointSegment } from "./checkpoint.js";
|
|
4
7
|
import { pinRequestedAttemptV2, requestedAttemptV2, runEnvironmentTarget, } from "./environment-target.js";
|
|
5
|
-
import { runExperiment,
|
|
8
|
+
import { runExperiment, } from "./runner.js";
|
|
6
9
|
/** Capability strings a registration declares; Hue offers only matching cases. */
|
|
7
10
|
export const localAgentCapabilities = {
|
|
8
11
|
/** Cases pinned to a hosted synthetic world. */
|
|
@@ -50,6 +53,7 @@ function localAgentTargetContext(context) {
|
|
|
50
53
|
: {}),
|
|
51
54
|
files: structuredClone(context.files),
|
|
52
55
|
outputDirectory: context.outputDirectory,
|
|
56
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
59
|
/** The registration's capabilities: explicit values plus one per supplied callback. */
|
|
@@ -98,15 +102,22 @@ function wait(milliseconds, signal) {
|
|
|
98
102
|
signal?.addEventListener("abort", aborted, { once: true });
|
|
99
103
|
});
|
|
100
104
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
/** A failure worth resuming: one that changes on its own. Hue or a world not answering or
|
|
106
|
+
* refusing transiently past the client's retries, telemetry not accepted at the flush, or a run
|
|
107
|
+
* of such failures across concurrent cases. Everything else, an outcome unsafe to resume (the
|
|
108
|
+
* target may have run without a saved outcome, an output that cannot be saved), a saved outcome
|
|
109
|
+
* whose trace was never acknowledged (a resume does not export it again), a refusal Hue decided
|
|
110
|
+
* on, an input the SDK refuses (a case file whose bytes differ from its manifest), would recur on
|
|
111
|
+
* the next attempt and is given up as attention at once. */
|
|
112
|
+
function resumable(error, seen = new Set()) {
|
|
113
|
+
if (isTransientApiError(error) ||
|
|
114
|
+
isTransientEnvironmentError(error) ||
|
|
115
|
+
error instanceof HueExportError)
|
|
105
116
|
return true;
|
|
106
|
-
if (!(error instanceof AggregateError) || seen.has(error))
|
|
117
|
+
if (!(error instanceof AggregateError) || seen.has(error) || !error.errors.length)
|
|
107
118
|
return false;
|
|
108
119
|
seen.add(error);
|
|
109
|
-
return error.errors.
|
|
120
|
+
return error.errors.every((nested) => resumable(nested, seen));
|
|
110
121
|
}
|
|
111
122
|
/**
|
|
112
123
|
* Starts an outbound-only worker for one fixed local agent entry point. Hue chooses
|
|
@@ -119,10 +130,33 @@ export async function runLocalAgent(options) {
|
|
|
119
130
|
const maxRuns = options.maxRuns ?? Number.POSITIVE_INFINITY;
|
|
120
131
|
if (!(maxRuns === Number.POSITIVE_INFINITY || (Number.isInteger(maxRuns) && maxRuns > 0)))
|
|
121
132
|
throw new RangeError("maxRuns must be a positive integer");
|
|
133
|
+
const maxRunAttempts = options.maxRunAttempts ?? 5;
|
|
134
|
+
if (!Number.isInteger(maxRunAttempts) || maxRunAttempts < 1 || maxRunAttempts > 20)
|
|
135
|
+
throw new RangeError("maxRunAttempts must be 1–20");
|
|
136
|
+
if (options.worldTtlSeconds !== undefined &&
|
|
137
|
+
(!Number.isInteger(options.worldTtlSeconds) ||
|
|
138
|
+
options.worldTtlSeconds < 1 ||
|
|
139
|
+
options.worldTtlSeconds > 86_400))
|
|
140
|
+
throw new RangeError("worldTtlSeconds must be 1–86400");
|
|
122
141
|
if (options.environmentClient.baseUrl !== options.client.baseUrl)
|
|
123
142
|
throw new Error("Environments and evaluations must use the same Hue origin");
|
|
124
143
|
const directory = resolve(options.checkpointDirectory);
|
|
125
|
-
|
|
144
|
+
// The first read is polled like the claims that follow: Hue not answering at startup is not
|
|
145
|
+
// a reason for the worker to exit; a refusal Hue decided on still is.
|
|
146
|
+
let project;
|
|
147
|
+
for (let failures = 0;;) {
|
|
148
|
+
try {
|
|
149
|
+
project = await options.client.checkConnection();
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!isTransientApiError(error) || options.signal?.aborted)
|
|
154
|
+
throw error;
|
|
155
|
+
failures++;
|
|
156
|
+
await options.onPollError?.(error);
|
|
157
|
+
await wait(Math.min(interval * 2 ** Math.min(failures, 6), 60_000), options.signal);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
126
160
|
const store = await CheckpointStore.acquire(directory, {
|
|
127
161
|
kind: "local-agent-worker",
|
|
128
162
|
projectId: project.id,
|
|
@@ -138,28 +172,87 @@ export async function runLocalAgent(options) {
|
|
|
138
172
|
workerId = randomUUID();
|
|
139
173
|
await store.write("worker-id", workerId);
|
|
140
174
|
}
|
|
141
|
-
let
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
let settled = 0;
|
|
176
|
+
/** Consecutive poll failures, for the backoff between polls; reset by a poll that answers. */
|
|
177
|
+
let pollFailures = 0;
|
|
178
|
+
/** Consecutive failed attempts per claimed run; a run is given up at `maxRunAttempts`. */
|
|
179
|
+
const attempts = new Map();
|
|
180
|
+
const bump = (claim) => {
|
|
181
|
+
const attempt = (attempts.get(claim.runId) ?? 0) + 1;
|
|
182
|
+
attempts.set(claim.runId, attempt);
|
|
183
|
+
return attempt;
|
|
184
|
+
};
|
|
185
|
+
/** The wait before the run is resumed: the poll interval doubled per attempt, at most a minute. */
|
|
186
|
+
const backoff = (attempt) => Math.min(interval * 2 ** Math.min(attempt, 6), 60_000);
|
|
187
|
+
/** Gives the run up as attention with the failure's name; a refusal to record it (the run
|
|
188
|
+
* was released or cancelled meanwhile) is reported as such and changes nothing else. */
|
|
189
|
+
const giveUp = async (claim, error) => {
|
|
190
|
+
const failureType = error instanceof Error ? error.name.slice(0, 200) : "WorkerError";
|
|
191
|
+
let outcome = "attention";
|
|
192
|
+
try {
|
|
193
|
+
await options.client.completeLocalAgentRun({
|
|
194
|
+
runId: claim.runId,
|
|
195
|
+
workerId,
|
|
196
|
+
state: "attention",
|
|
197
|
+
failureType,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (completion) {
|
|
201
|
+
if (completion instanceof HueApiError && completion.status === 409)
|
|
202
|
+
outcome = "refused";
|
|
203
|
+
// A transient failure past the client's retries leaves the run claimed; the next claim
|
|
204
|
+
// returns it and the attempt count gives it up again. A refusal Hue decided on (a
|
|
205
|
+
// revoked key, a disabled agent) is what needs fixing, and is thrown as itself.
|
|
206
|
+
else if (isTransientApiError(completion))
|
|
207
|
+
return false;
|
|
208
|
+
else
|
|
209
|
+
throw completion;
|
|
210
|
+
}
|
|
211
|
+
const attempt = attempts.get(claim.runId) ?? 0;
|
|
212
|
+
attempts.delete(claim.runId);
|
|
213
|
+
settled++;
|
|
214
|
+
await options.onRunFailed?.({ ...claim, error, attempt, outcome });
|
|
215
|
+
return true;
|
|
216
|
+
};
|
|
217
|
+
while (settled < maxRuns && !options.signal?.aborted) {
|
|
218
|
+
let claim;
|
|
219
|
+
try {
|
|
220
|
+
const agent = await options.client.registerLocalAgent({
|
|
221
|
+
...options.agent,
|
|
222
|
+
capabilities,
|
|
223
|
+
scorerDigests: options.agent.scorerDigests ??
|
|
224
|
+
options.scorers?.map((item) => item.definition.sourceDigest) ??
|
|
225
|
+
[],
|
|
226
|
+
});
|
|
227
|
+
claim = await options.client.claimLocalAgentRun({ agentId: agent.id, workerId });
|
|
228
|
+
pollFailures = 0;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
// A transient failure past the client's own retries (Hue restarting, a network blip)
|
|
232
|
+
// does not end the worker: it waits, longer each time, and polls again. A refusal Hue
|
|
233
|
+
// decided on (a revoked key, a disabled agent, a changed registration) is thrown.
|
|
234
|
+
if (!isTransientApiError(error))
|
|
235
|
+
throw error;
|
|
236
|
+
pollFailures++;
|
|
237
|
+
await options.onPollError?.(error);
|
|
238
|
+
await wait(Math.min(interval * 2 ** Math.min(pollFailures, 6), 60_000), options.signal);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
151
241
|
if (!claim) {
|
|
152
242
|
await wait(interval, options.signal);
|
|
153
243
|
continue;
|
|
154
244
|
}
|
|
245
|
+
// A claim naming an experiment id the checkpoint store refuses is a protocol violation,
|
|
246
|
+
// not an operational failure: it is thrown here, before the attempt, and never retried.
|
|
247
|
+
const experimentId = checkpointSegment(claim.experimentId, "experiment id");
|
|
155
248
|
const heartbeat = setInterval(() => {
|
|
156
249
|
void options.client
|
|
157
250
|
.heartbeatLocalAgentRun({ runId: claim.runId, workerId })
|
|
158
251
|
.catch(() => undefined);
|
|
159
252
|
}, Math.min(15_000, Math.max(1_000, interval)));
|
|
160
253
|
let experimentFinished = false;
|
|
254
|
+
let queueCompleted = false;
|
|
161
255
|
try {
|
|
162
|
-
const experimentId = checkpointSegment(claim.experimentId, "experiment id");
|
|
163
256
|
const requested = requestedConfiguration
|
|
164
257
|
? pinRequestedAttemptV2(requestedConfiguration, (await options.client.getExperiment(experimentId)).config)
|
|
165
258
|
: undefined;
|
|
@@ -201,6 +294,7 @@ export async function runLocalAgent(options) {
|
|
|
201
294
|
inputs,
|
|
202
295
|
context,
|
|
203
296
|
requested,
|
|
297
|
+
ttlSeconds: options.worldTtlSeconds,
|
|
204
298
|
// The registered revision is the agent revision under test.
|
|
205
299
|
agentRevision: options.agent.revision,
|
|
206
300
|
deprecationWarnings: options.deprecationWarnings,
|
|
@@ -218,24 +312,54 @@ export async function runLocalAgent(options) {
|
|
|
218
312
|
workerId,
|
|
219
313
|
state: "completed",
|
|
220
314
|
});
|
|
315
|
+
queueCompleted = true;
|
|
316
|
+
attempts.delete(claim.runId);
|
|
317
|
+
settled++;
|
|
221
318
|
await options.onCompleted?.(report);
|
|
222
|
-
completed++;
|
|
223
319
|
}
|
|
224
320
|
catch (error) {
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
321
|
+
// The run is completed in the queue: a failure after that is the caller's own
|
|
322
|
+
// (`onCompleted` threw) and is thrown as itself, not reported as a retry that cannot be.
|
|
323
|
+
if (queueCompleted)
|
|
324
|
+
throw error;
|
|
325
|
+
if (experimentFinished && error instanceof HueApiError && error.status === 409) {
|
|
326
|
+
// The experiment finished, but Hue no longer accepts this worker's completion: the
|
|
327
|
+
// run was released or cancelled meanwhile. Its outcome is recorded; nothing to resume.
|
|
328
|
+
attempts.delete(claim.runId);
|
|
329
|
+
settled++;
|
|
330
|
+
await options.onRunFailed?.({ ...claim, error, attempt: 0, outcome: "refused" });
|
|
331
|
+
}
|
|
332
|
+
else if (!experimentFinished && !resumable(error)) {
|
|
333
|
+
// A failure that would recur: unsafe to resume, refused by Hue, or an input the SDK
|
|
334
|
+
// refuses. A project member decides; the worker goes on to other runs. When Hue cannot
|
|
335
|
+
// record that for `maxRunAttempts` polls, the worker stops with the error: it can
|
|
336
|
+
// reach neither an outcome nor a record of one.
|
|
337
|
+
if (!(await giveUp(claim, error))) {
|
|
338
|
+
const attempt = bump(claim);
|
|
339
|
+
if (attempt >= maxRunAttempts)
|
|
340
|
+
throw error;
|
|
341
|
+
await wait(backoff(attempt), options.signal);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
// A transient failure: the outcome so far is checkpointed, and the claim stays with
|
|
346
|
+
// this worker, so the next claim returns the same run and the experiment resumes where
|
|
347
|
+
// it stopped. After `maxRunAttempts` such failures the run is given up as attention;
|
|
348
|
+
// when even that cannot be recorded, the worker stops with the error.
|
|
349
|
+
const attempt = bump(claim);
|
|
350
|
+
if (attempt >= maxRunAttempts) {
|
|
351
|
+
if (!(await giveUp(claim, error))) {
|
|
352
|
+
if (attempt >= 2 * maxRunAttempts)
|
|
353
|
+
throw error;
|
|
354
|
+
await wait(backoff(attempt), options.signal);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
const waitMillis = backoff(attempt);
|
|
359
|
+
await options.onRunFailed?.({ ...claim, error, attempt, outcome: "retry", waitMillis });
|
|
360
|
+
await wait(waitMillis, options.signal);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
239
363
|
}
|
|
240
364
|
finally {
|
|
241
365
|
clearInterval(heartbeat);
|
package/dist/evals/runner.d.ts
CHANGED
|
@@ -29,6 +29,30 @@ export declare class OutcomeSerializationError extends Error {
|
|
|
29
29
|
export declare class TargetCancelledError extends Error {
|
|
30
30
|
constructor();
|
|
31
31
|
}
|
|
32
|
+
/** Thrown by the environment target when the target did not return before its world's deadline.
|
|
33
|
+
* The world is sealed abandoned and the attempt saved as an `error` of type `TargetTimeout`, so a
|
|
34
|
+
* hung agent ends its case instead of holding the run open. */
|
|
35
|
+
export declare class TargetTimeoutError extends Error {
|
|
36
|
+
/** The world whose deadline passed. */
|
|
37
|
+
readonly environmentRunId: string;
|
|
38
|
+
/** The world's `expiresAt`. */
|
|
39
|
+
readonly expiresAt: string;
|
|
40
|
+
constructor(
|
|
41
|
+
/** The world whose deadline passed. */
|
|
42
|
+
environmentRunId: string,
|
|
43
|
+
/** The world's `expiresAt`. */
|
|
44
|
+
expiresAt: string);
|
|
45
|
+
}
|
|
46
|
+
/** The target's outcome is saved but the telemetry it requires was never acknowledged on an
|
|
47
|
+
* earlier attempt. Restore or export the trace, or complete with omitted evidence through the
|
|
48
|
+
* client; the target is never rerun. It is this case's own state, not the run's. */
|
|
49
|
+
export declare class TraceExportUnacknowledgedError extends Error {
|
|
50
|
+
/** The execution whose saved outcome waits for its trace. */
|
|
51
|
+
readonly executionId: string;
|
|
52
|
+
constructor(
|
|
53
|
+
/** The execution whose saved outcome waits for its trace. */
|
|
54
|
+
executionId: string);
|
|
55
|
+
}
|
|
32
56
|
/** Thrown when the target or world may have committed but acknowledgement is unavailable. */
|
|
33
57
|
export declare class TargetOutcomeUncertainError extends Error {
|
|
34
58
|
/** Execution whose target outcome must never be replayed automatically. */
|