@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.
@@ -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 its credential-free health
149
- * endpoint: 200 with `gateway: "simulation"` is on, the disabled handler's empty 404 (no
150
- * `x-hue-diagnostic`) is off, and anything else (a network failure, a timeout, a redirect, a
151
- * refusal carrying a diagnostic, another status or body) is unknown. Probed only after a create
152
- * was refused; on and off are remembered per origin, unknown is probed again next time.
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/gmailmcp.googleapis.com/_hue/health`, { redirect: "error", signal: AbortSignal.timeout(5_000) })
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") ? "unknown" : "off";
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
- const output = await options.target(options.inputs, {
311
- config: context.config,
312
- item: context.item,
313
- executionId: context.executionId,
314
- environmentRunId: run.id,
315
- trace: { traceId: context.span.traceId, spanId: context.span.spanId },
316
- tools,
317
- ...(world ? { world } : {}),
318
- ...(mcp ? { mcp } : {}),
319
- ...(connectionBundle ? { connectionBundle } : {}),
320
- files: structuredClone(context.files),
321
- outputDirectory: context.outputDirectory,
322
- signal: options.signal,
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 { EnvironmentClient } from "../environment/client.js";
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
- /** Useful for one-shot jobs and deterministic acceptance. Omit to keep polling. */
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, OutcomeSerializationError, TargetOutcomeUncertainError, UncertainExecutionError, } from "./runner.js";
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
- function needsAttention(error, seen = new Set()) {
102
- if (error instanceof TargetOutcomeUncertainError ||
103
- error instanceof UncertainExecutionError ||
104
- error instanceof OutcomeSerializationError)
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.some((nested) => needsAttention(nested, seen));
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
- const project = await options.client.checkConnection();
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 completed = 0;
142
- while (completed < maxRuns && !options.signal?.aborted) {
143
- const agent = await options.client.registerLocalAgent({
144
- ...options.agent,
145
- capabilities,
146
- scorerDigests: options.agent.scorerDigests ??
147
- options.scorers?.map((item) => item.definition.sourceDigest) ??
148
- [],
149
- });
150
- const claim = await options.client.claimLocalAgentRun({ agentId: agent.id, workerId });
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
- // A durable outcome can still need completion/result uploads. Leave operational
226
- // failures claimed so the same worker can resume them through its checkpoints.
227
- // Attention is terminal in the queue and is reserved for explicit unsafe-to-resume
228
- // outcomes that require operator intervention.
229
- if (!experimentFinished && needsAttention(error))
230
- await options.client
231
- .completeLocalAgentRun({
232
- runId: claim.runId,
233
- workerId,
234
- state: "attention",
235
- failureType: error instanceof Error ? error.name.slice(0, 200) : "WorkerError",
236
- })
237
- .catch(() => undefined);
238
- throw error;
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);
@@ -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. */