@odla-ai/harness 0.10.4 → 0.11.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.
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/code-runtime-cli.ts
5
5
  var import_node_os4 = require("os");
6
- var import_promises13 = require("fs/promises");
6
+ var import_promises14 = require("fs/promises");
7
7
 
8
8
  // src/code-runtime-client-validation.ts
9
9
  var import_code = require("@odla-ai/camel/code");
@@ -422,40 +422,40 @@ async function readBoundedResponse(body, maximum) {
422
422
  return result;
423
423
  }
424
424
 
425
- // src/code-runtime.ts
426
- var CODE_RUNTIME_PROTOCOL_VERSION = 3;
427
- async function runCodeRuntimeHeartbeatLoop(options) {
428
- const heartbeatMs = options.heartbeatMs ?? 15e3;
429
- if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
430
- throw new TypeError("heartbeatMs must be an integer from 1000 to 300000");
431
- }
432
- let retryMs = 1e3;
433
- do {
434
- if (options.signal?.aborted) return;
425
+ // src/code-runtime-overload.ts
426
+ var OVERLOAD_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3];
427
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["control_plane_overloaded", "registry_overloaded", "transport_unavailable"]);
428
+ function overloadedControlFailure(cause) {
429
+ if (!cause || typeof cause !== "object") return false;
430
+ const failure = cause;
431
+ return failure.status === 503 && typeof failure.code === "string" && RETRYABLE_CODES.has(failure.code);
432
+ }
433
+ async function withOverloadRetry(call, wait2, onRetry = () => void 0) {
434
+ for (let attempt = 0; ; attempt += 1) {
435
435
  try {
436
- const snapshot = await options.control.heartbeat(options.runtimeVersion, options.capabilities);
437
- await options.onSnapshot?.(snapshot);
438
- retryMs = 1e3;
439
- if (options.once) return;
440
- await wait(heartbeatMs, options.signal);
441
- } catch (error) {
442
- if (options.signal?.aborted) return;
443
- if (options.once || !retryableControlFailure(error)) throw error;
444
- await options.onRetry?.(error, retryMs);
445
- await wait(retryMs, options.signal);
446
- retryMs = Math.min(retryMs * 2, 3e4);
436
+ return await call();
437
+ } catch (cause) {
438
+ const delayMs = OVERLOAD_RETRY_DELAYS_MS[attempt];
439
+ if (delayMs === void 0 || !overloadedControlFailure(cause)) throw cause;
440
+ await onRetry(cause, delayMs);
441
+ await wait2(delayMs);
447
442
  }
448
- } while (!options.signal?.aborted);
443
+ }
449
444
  }
445
+
446
+ // src/code-runtime-reconciler.ts
447
+ var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
450
448
  var CodeRuntimeReconciler = class {
451
- constructor(control, engine, onDiagnostic) {
449
+ constructor(control, engine, onDiagnostic, options = {}) {
452
450
  this.control = control;
453
451
  this.engine = engine;
454
452
  this.onDiagnostic = onDiagnostic;
453
+ this.options = options;
455
454
  }
456
455
  control;
457
456
  engine;
458
457
  onDiagnostic;
458
+ options;
459
459
  results = /* @__PURE__ */ new Map();
460
460
  async reconcile(snapshot) {
461
461
  for (const command of snapshot.commands) {
@@ -474,7 +474,15 @@ var CodeRuntimeReconciler = class {
474
474
  await this.control.acknowledge(command.commandId, completed.result);
475
475
  if (!completed.notified) {
476
476
  try {
477
- await this.engine.acknowledged?.(command, completed.result);
477
+ await withOverloadRetry(
478
+ async () => {
479
+ await this.engine.acknowledged?.(command, completed.result);
480
+ },
481
+ (ms) => (this.options.wait ?? sleep)(ms),
482
+ (cause, delayMs) => this.onDiagnostic?.(
483
+ `command ${command.commandId} acknowledged handling waits ${delayMs}ms for an overloaded control plane \xB7 ${cause instanceof Error ? cause.message : String(cause)}`
484
+ )
485
+ );
478
486
  } catch (error) {
479
487
  this.onDiagnostic?.(
480
488
  `command ${command.commandId} acknowledged handling failed \xB7 ${error instanceof Error ? error.message : String(error)}`
@@ -485,6 +493,32 @@ var CodeRuntimeReconciler = class {
485
493
  }
486
494
  }
487
495
  };
496
+
497
+ // src/code-runtime.ts
498
+ var CODE_RUNTIME_PROTOCOL_VERSION = 3;
499
+ async function runCodeRuntimeHeartbeatLoop(options) {
500
+ const heartbeatMs = options.heartbeatMs ?? 15e3;
501
+ if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
502
+ throw new TypeError("heartbeatMs must be an integer from 1000 to 300000");
503
+ }
504
+ let retryMs = 1e3;
505
+ do {
506
+ if (options.signal?.aborted) return;
507
+ try {
508
+ const snapshot = await options.control.heartbeat(options.runtimeVersion, options.capabilities);
509
+ await options.onSnapshot?.(snapshot);
510
+ retryMs = 1e3;
511
+ if (options.once) return;
512
+ await wait(heartbeatMs, options.signal);
513
+ } catch (error) {
514
+ if (options.signal?.aborted) return;
515
+ if (options.once || !retryableControlFailure(error)) throw error;
516
+ await options.onRetry?.(error, retryMs);
517
+ await wait(retryMs, options.signal);
518
+ retryMs = Math.min(retryMs * 2, 3e4);
519
+ }
520
+ } while (!options.signal?.aborted);
521
+ }
488
522
  function retryableControlFailure(value) {
489
523
  if (!value || typeof value !== "object") return false;
490
524
  const failure = value;
@@ -1412,8 +1446,8 @@ var CodeRuntimeCheckpointManager = class {
1412
1446
  trustedBaseDigest: active.trustedBaseDigest,
1413
1447
  planningInputDigest: active.planningInputDigest,
1414
1448
  conversationRefs: active.conversationRefs,
1415
- fallbackPolicyDigest: this.options.fallbackPolicyDigest,
1416
- recipes: this.options.recipes,
1449
+ fallbackPolicyDigest: active.buildPolicyDigest ?? this.options.fallbackPolicyDigest,
1450
+ recipes: active.recipes ?? this.options.recipes,
1417
1451
  recipeExecutor: this.options.recipeExecutor,
1418
1452
  review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
1419
1453
  });
@@ -2354,13 +2388,19 @@ function createCodeRuntimeSessionSkillLoader(control) {
2354
2388
  ...tool.acceptsTaint === void 0 ? {} : { acceptsTaint: tool.acceptsTaint },
2355
2389
  handler: async (input, context) => {
2356
2390
  if (!context.toolCallId) throw new TypeError("collaboration tool call identity is required");
2357
- return execute2(command.sessionId, {
2358
- commandId: command.commandId,
2359
- toolCallId: context.toolCallId,
2360
- skill: manifest.name,
2361
- tool: tool.name,
2362
- input
2363
- }, context.signal);
2391
+ try {
2392
+ return await execute2(command.sessionId, {
2393
+ commandId: command.commandId,
2394
+ toolCallId: context.toolCallId,
2395
+ skill: manifest.name,
2396
+ tool: tool.name,
2397
+ input
2398
+ }, context.signal);
2399
+ } catch (cause) {
2400
+ if (context.signal?.aborted) throw cause;
2401
+ const detail = (cause instanceof Error ? cause.message : String(cause)).slice(0, 500);
2402
+ return { content: `Tool "${tool.name}" failed: ${detail}`, isError: true };
2403
+ }
2364
2404
  }
2365
2405
  }))
2366
2406
  }));
@@ -2378,23 +2418,7 @@ async function sessionSkillsFor(options, command) {
2378
2418
  }
2379
2419
 
2380
2420
  // src/code-runtime-inference.ts
2381
- var OVERLOAD_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3];
2382
- var RETRYABLE_CODES = /* @__PURE__ */ new Set(["control_plane_overloaded", "registry_overloaded", "transport_unavailable"]);
2383
- function overloadedControlFailure(cause) {
2384
- return cause instanceof CodeRuntimeControlError && cause.status === 503 && RETRYABLE_CODES.has(cause.code);
2385
- }
2386
- async function inferWithBackoff(infer, wait2, onRetry) {
2387
- for (let attempt = 0; ; attempt += 1) {
2388
- try {
2389
- return await infer();
2390
- } catch (cause) {
2391
- const delayMs = OVERLOAD_RETRY_DELAYS_MS[attempt];
2392
- if (delayMs === void 0 || !overloadedControlFailure(cause)) throw cause;
2393
- await onRetry(cause, delayMs);
2394
- await wait2(delayMs);
2395
- }
2396
- }
2397
- }
2421
+ var inferWithBackoff = (infer, wait2, onRetry) => withOverloadRetry(infer, wait2, (cause, delayMs) => onRetry(cause, delayMs));
2398
2422
  async function handleCodeRuntimeInference(input) {
2399
2423
  const { command, request, state } = input;
2400
2424
  const startedAt = Date.now();
@@ -3806,6 +3830,96 @@ function observeCodeRuntimeSessionSkills(command, skills, emit) {
3806
3830
  }));
3807
3831
  }
3808
3832
 
3833
+ // src/code-repository-recipes.ts
3834
+ var import_promises13 = require("fs/promises");
3835
+ var import_node_path12 = require("path");
3836
+ var REPOSITORY_RECIPES_FILE = "odla.recipes.json";
3837
+ var MAX_RECIPES = 16;
3838
+ var DEFAULT_TIMEOUT_MS = 12e4;
3839
+ var MAX_TIMEOUT_MS = 15 * 6e4;
3840
+ var ID2 = /^[A-Za-z0-9._:-]{1,120}$/;
3841
+ var RECIPE_FIELDS = /* @__PURE__ */ new Set(["id", "command", "timeoutMs"]);
3842
+ var fault = (detail) => new TypeError(`${REPOSITORY_RECIPES_FILE} is malformed: ${detail}`);
3843
+ function parseRepositoryRecipes(text2, envelope) {
3844
+ let parsed;
3845
+ try {
3846
+ parsed = JSON.parse(text2);
3847
+ } catch {
3848
+ throw fault("not valid JSON");
3849
+ }
3850
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw fault("the document must be an object");
3851
+ const document = parsed;
3852
+ if (document.version !== 1) throw fault("version must be 1");
3853
+ const entries = document.recipes;
3854
+ if (!Array.isArray(entries) || entries.length < 1) throw fault("recipes must be a non-empty array");
3855
+ if (entries.length > MAX_RECIPES) throw fault(`at most ${MAX_RECIPES} recipes may be declared`);
3856
+ const recipes = [];
3857
+ const seen = /* @__PURE__ */ new Set();
3858
+ entries.forEach((entry, index) => {
3859
+ const label = `recipe ${index + 1}`;
3860
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw fault(`${label} must be an object`);
3861
+ const row = entry;
3862
+ const unknown = Object.keys(row).filter((key) => !RECIPE_FIELDS.has(key));
3863
+ if (unknown.length) throw fault(`${label} has unsupported field ${unknown[0]}; the host owns image and resource limits`);
3864
+ if (typeof row.id !== "string" || !ID2.test(row.id)) throw fault(`${label} needs an id of 1 to 120 letters, digits, . _ : or -`);
3865
+ if (seen.has(row.id)) throw fault(`${label} repeats id ${row.id}`);
3866
+ seen.add(row.id);
3867
+ if (!Array.isArray(row.command) || row.command.length < 1 || row.command.some((part) => typeof part !== "string" || !part)) {
3868
+ throw fault(`${label} (${row.id}) needs command as a non-empty array of non-empty strings`);
3869
+ }
3870
+ const timeoutMs = row.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3871
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
3872
+ throw fault(`${label} (${row.id}) timeoutMs must be an integer from 1 to ${MAX_TIMEOUT_MS}`);
3873
+ }
3874
+ const recipe2 = {
3875
+ id: row.id,
3876
+ command: [...row.command],
3877
+ timeoutMs,
3878
+ image: envelope.image,
3879
+ maxOutputBytes: envelope.maxOutputBytes,
3880
+ cpus: envelope.cpus,
3881
+ memory: envelope.memory,
3882
+ pids: envelope.pids
3883
+ };
3884
+ try {
3885
+ assertCodeBuildRecipe(recipe2);
3886
+ } catch (cause) {
3887
+ throw fault(`${label} (${row.id}) is not a runnable recipe: ${cause instanceof Error ? cause.message : String(cause)}`);
3888
+ }
3889
+ recipes.push(recipe2);
3890
+ });
3891
+ return recipes;
3892
+ }
3893
+ async function readRepositoryRecipes(baselineDir, envelope) {
3894
+ let text2;
3895
+ try {
3896
+ text2 = await (0, import_promises13.readFile)((0, import_node_path12.join)(baselineDir, REPOSITORY_RECIPES_FILE), "utf8");
3897
+ } catch (cause) {
3898
+ if (cause.code === "ENOENT") return null;
3899
+ throw cause;
3900
+ }
3901
+ return parseRepositoryRecipes(text2, envelope);
3902
+ }
3903
+ async function resolveCodeRecipes(baselineDir, release, envelope) {
3904
+ const declared = envelope ? await readRepositoryRecipes(baselineDir, envelope) : null;
3905
+ return declared ? { recipes: declared, source: "repository" } : { recipes: release, source: "release" };
3906
+ }
3907
+ function describeCodeRecipes(resolved) {
3908
+ const ids = resolved.recipes.map((recipe2) => recipe2.id).join(", ");
3909
+ return resolved.source === "repository" ? `Verification recipes: declared by the repository in ${REPOSITORY_RECIPES_FILE} \xB7 ${ids}` : `Verification recipes: release-owned (the repository declares none in ${REPOSITORY_RECIPES_FILE}) \xB7 ${ids}`;
3910
+ }
3911
+ async function sessionRecipesFor(workspace, options, resume) {
3912
+ const resolved = await resolveCodeRecipes(workspace.baselineDir, options.recipes, options.repositoryRecipes ?? null).catch(async (cause) => {
3913
+ await workspace.cleanup();
3914
+ throw cause;
3915
+ });
3916
+ return {
3917
+ ...resolved,
3918
+ buildPolicyDigest: digestRuntimeValue(JSON.stringify(resolved.recipes)),
3919
+ note: !resume && resolved.source === "repository" ? describeCodeRecipes(resolved) : null
3920
+ };
3921
+ }
3922
+
3809
3923
  // src/code-runtime-engine.ts
3810
3924
  var TheseusRuntimeEngine = class {
3811
3925
  constructor(options) {
@@ -3857,6 +3971,7 @@ var TheseusRuntimeEngine = class {
3857
3971
  control: this.options.control,
3858
3972
  ...this.options.localSource ? { localSource: this.options.localSource } : {}
3859
3973
  });
3974
+ const resolvedRecipes = await sessionRecipesFor(workspace, this.options, resume);
3860
3975
  const abort = new AbortController(), startGate = codeRuntimeAcknowledgementGate(abort.signal);
3861
3976
  const conversationRefs = [];
3862
3977
  const active = {
@@ -3865,6 +3980,8 @@ var TheseusRuntimeEngine = class {
3865
3980
  conversationRefs,
3866
3981
  acknowledged: false,
3867
3982
  startGate,
3983
+ recipes: resolvedRecipes.recipes,
3984
+ buildPolicyDigest: resolvedRecipes.buildPolicyDigest,
3868
3985
  role: metadata.role,
3869
3986
  readOnly: metadata.readOnly,
3870
3987
  title: metadata.title,
@@ -3882,6 +3999,7 @@ var TheseusRuntimeEngine = class {
3882
3999
  done: Promise.resolve(null)
3883
4000
  };
3884
4001
  this.#active.set(command.sessionId, active);
4002
+ if (resolvedRecipes.note) await this.#event(command, { type: "message", actor: "system", body: resolvedRecipes.note }, conversationRefs);
3885
4003
  if (requestedLocal) {
3886
4004
  await this.#event(command, {
3887
4005
  type: "message",
@@ -3917,7 +4035,7 @@ var TheseusRuntimeEngine = class {
3917
4035
  const active = await this.#takeOver(command, "pursue requires an active Code session");
3918
4036
  active.done = startGoalPursuit({
3919
4037
  spec,
3920
- recipes: this.options.recipes,
4038
+ recipes: active.recipes,
3921
4039
  recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
3922
4040
  workspace: active.workspace,
3923
4041
  baseCommitSha: active.baseCommitSha,
@@ -3996,7 +4114,7 @@ var TheseusRuntimeEngine = class {
3996
4114
  async #runAttempt(command, metadata, active) {
3997
4115
  const lease = fakeCodeLease(command, metadata);
3998
4116
  const broker = this.#observed(command, active, createCodeRuntimeToolBroker({
3999
- recipes: this.options.recipes,
4117
+ recipes: active.recipes,
4000
4118
  engine: this.options.engine,
4001
4119
  recipeAuthorization: this.options.recipeAuthorization
4002
4120
  }, lease, metadata.role));
@@ -4022,7 +4140,7 @@ var TheseusRuntimeEngine = class {
4022
4140
  // A review-role session is read-only and still runs the proof; a planner
4023
4141
  // marked read-only by its payload runs nothing.
4024
4142
  recipes: metadata.role === "review" || !metadata.readOnly,
4025
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
4143
+ recipeIds: active.recipes.map((recipe2) => recipe2.id),
4026
4144
  ...extraSkills.length ? { extraSkills } : {}
4027
4145
  });
4028
4146
  const closing = result.finalText.trim();
@@ -4131,7 +4249,7 @@ function parse(argv) {
4131
4249
  };
4132
4250
  }
4133
4251
  async function readPolicy(path) {
4134
- const value = JSON.parse(await (0, import_promises13.readFile)(path, "utf8"));
4252
+ const value = JSON.parse(await (0, import_promises14.readFile)(path, "utf8"));
4135
4253
  if (!value || Object.keys(value).some((key) => !["recipes", "recipeAuthorization"].includes(key)) || !Array.isArray(value.recipes) || !value.recipes.length || value.recipeAuthorization !== void 0 && value.recipeAuthorization !== "registered_recipe" && value.recipeAuthorization !== "exact_approval") throw new TypeError("invalid Code build policy file");
4136
4254
  const recipes = value.recipes;
4137
4255
  for (const recipe2 of recipes) assertCodeBuildRecipe(recipe2);