@openpond/evals 0.7.5 → 0.8.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.
Files changed (89) hide show
  1. package/JAVASCRIPT_ENVIRONMENTS.md +47 -0
  2. package/LEARNING.md +30 -0
  3. package/dist/environment-execution-services-contract.js +27 -0
  4. package/dist/environment-execution-services.js +100 -0
  5. package/dist/javascript-environment-contract.js +13 -0
  6. package/dist/javascript-environment-node.js +4 -2
  7. package/dist/javascript-environment.js +4 -2
  8. package/dist/javascript-isolate-process-entry.js +2 -2
  9. package/dist/javascript-isolate-process-source.js +1 -1
  10. package/dist/javascript-isolate-process.js +6 -4
  11. package/dist/javascript-isolate.js +39 -30
  12. package/dist/javascript-verifier-worker-source.js +1 -1
  13. package/dist/learning/authoring-service.js +1 -1
  14. package/dist/learning/authoring.js +9 -0
  15. package/dist/learning/index.js +4 -0
  16. package/dist/learning/operations.js +3 -1
  17. package/dist/learning/repository.js +2 -0
  18. package/dist/learning/reward-authoring.js +120 -0
  19. package/dist/learning/reward-check-service.js +31 -0
  20. package/dist/learning/reward-check-worker.js +114 -0
  21. package/dist/learning/reward-checks.js +81 -0
  22. package/dist/learning/reward-fixture-executor.js +29 -0
  23. package/dist/learning/service.js +7 -0
  24. package/dist/rewards.js +3 -0
  25. package/dist/sql-execution-contract.js +52 -0
  26. package/dist/sql-execution-engine.js +135 -0
  27. package/dist/sql-execution-node.js +58 -0
  28. package/dist/sql-execution-process-entry.js +3 -0
  29. package/dist/sql-execution-process-source.js +1 -0
  30. package/dist/sql-wasm-binary.js +1 -0
  31. package/dist/types/environment-execution-services-contract.d.ts +74 -0
  32. package/dist/types/environment-execution-services-contract.d.ts.map +1 -0
  33. package/dist/types/environment-execution-services.d.ts +17 -0
  34. package/dist/types/environment-execution-services.d.ts.map +1 -0
  35. package/dist/types/evidence/conformance.d.ts +4 -4
  36. package/dist/types/evidence/contracts.d.ts +4 -4
  37. package/dist/types/javascript-environment-contract.d.ts +121 -0
  38. package/dist/types/javascript-environment-contract.d.ts.map +1 -1
  39. package/dist/types/javascript-environment-node.d.ts.map +1 -1
  40. package/dist/types/javascript-environment.d.ts +36 -0
  41. package/dist/types/javascript-environment.d.ts.map +1 -1
  42. package/dist/types/javascript-isolate-process.d.ts.map +1 -1
  43. package/dist/types/javascript-isolate.d.ts +4 -1
  44. package/dist/types/javascript-isolate.d.ts.map +1 -1
  45. package/dist/types/learning/admission.d.ts +12 -0
  46. package/dist/types/learning/admission.d.ts.map +1 -1
  47. package/dist/types/learning/authoring-service.d.ts +172 -0
  48. package/dist/types/learning/authoring-service.d.ts.map +1 -1
  49. package/dist/types/learning/authoring.d.ts +121 -0
  50. package/dist/types/learning/authoring.d.ts.map +1 -1
  51. package/dist/types/learning/contracts.d.ts +23 -11
  52. package/dist/types/learning/contracts.d.ts.map +1 -1
  53. package/dist/types/learning/index.d.ts +4 -0
  54. package/dist/types/learning/index.d.ts.map +1 -1
  55. package/dist/types/learning/operations.d.ts +134 -6
  56. package/dist/types/learning/operations.d.ts.map +1 -1
  57. package/dist/types/learning/repository.d.ts +114 -5
  58. package/dist/types/learning/repository.d.ts.map +1 -1
  59. package/dist/types/learning/reward-authoring.d.ts +18 -0
  60. package/dist/types/learning/reward-authoring.d.ts.map +1 -0
  61. package/dist/types/learning/reward-check-service.d.ts +17 -0
  62. package/dist/types/learning/reward-check-service.d.ts.map +1 -0
  63. package/dist/types/learning/reward-check-worker.d.ts +27 -0
  64. package/dist/types/learning/reward-check-worker.d.ts.map +1 -0
  65. package/dist/types/learning/reward-checks.d.ts +177 -0
  66. package/dist/types/learning/reward-checks.d.ts.map +1 -0
  67. package/dist/types/learning/reward-fixture-executor.d.ts +9 -0
  68. package/dist/types/learning/reward-fixture-executor.d.ts.map +1 -0
  69. package/dist/types/learning/service.d.ts.map +1 -1
  70. package/dist/types/learning/transport.d.ts +200 -28
  71. package/dist/types/learning/transport.d.ts.map +1 -1
  72. package/dist/types/rewards.d.ts +24 -0
  73. package/dist/types/rewards.d.ts.map +1 -1
  74. package/dist/types/sql-execution-contract.d.ts +72 -0
  75. package/dist/types/sql-execution-contract.d.ts.map +1 -0
  76. package/dist/types/sql-execution-engine.d.ts +4 -0
  77. package/dist/types/sql-execution-engine.d.ts.map +1 -0
  78. package/dist/types/sql-execution-node.d.ts +9 -0
  79. package/dist/types/sql-execution-node.d.ts.map +1 -0
  80. package/dist/types/sql-execution-process-entry.d.ts +2 -0
  81. package/dist/types/sql-execution-process-entry.d.ts.map +1 -0
  82. package/dist/types/sql-execution-process-source.d.ts +1 -0
  83. package/dist/types/sql-wasm-binary.d.ts +1 -0
  84. package/package.json +10 -1
  85. package/schemas/learning/v1/command-request.schema.json +412 -248
  86. package/schemas/learning/v1/draft.schema.json +96 -0
  87. package/schemas/learning/v1/read-request.schema.json +1 -0
  88. package/schemas/learning/v1/reward.schema.json +3 -0
  89. package/schemas/learning/v1/reward_check.schema.json +343 -0
@@ -63,3 +63,50 @@ is cancelled, waits for worker termination, and fails explicitly when that bound
63
63
  operation deadline is exceeded.
64
64
 
65
65
  The policy adapter must honor its supplied abort signal and settle after cancelling its own request. `environmentCleanupComplete` describes environment cleanup only; it is not a provider billing or infrastructure-cleanup receipt. Failed creation, collection, cancellation and cleanup remain explicit states, without fabricated scores.
66
+
67
+ ## Submitted SQL and JavaScript
68
+
69
+ An optional `executionServices` array on the immutable environment definition binds
70
+ an owner-controlled service to a named `step` tool or to `collect`. Omission preserves
71
+ existing definition hashes. Each service declares an identity, deadline and explicit
72
+ value references: `{ scope: "input" | "initialState" | "state" | "arguments", path: [...] }`.
73
+ References traverse own JSON properties only. The controller receives service results
74
+ as `services[id]`; candidates cannot supply that field.
75
+
76
+ A `sqlite.v1` binding declares `sql`, `snapshot`, `maxRows` and `maxResultBytes`.
77
+ Snapshots contain named tables, typed columns (`INTEGER`, `REAL`, `TEXT`, `BLOB`) and
78
+ array rows. SQLite executes exactly one statement in a fresh in-memory database.
79
+ The Node adapter enforces read-only authorization, a deterministic function allowlist,
80
+ a 32 MiB WASM memory maximum, value/SQL/result limits and a parent-owned deadline.
81
+ Writes, attachment, pragma changes, extensions and nondeterministic functions are
82
+ rejected. Results retain ordered column names and array rows, including duplicates.
83
+ SQL null is JSON null; exact large integers use `{ integer: "9223372036854775807" }`
84
+ and blobs use `{ blobBase64: "AP8=" }`. JSON numbers retain their ordinary IEEE-754
85
+ semantics. Use tagged integers when exact 64-bit storage is required.
86
+
87
+ A `javascript.v1` binding declares `source`, `cases`, `exportName`, `maxCases` and
88
+ `maxResultBytes`. Cases are owner-held objects with an `input` property. Each case
89
+ runs in a fresh QuickJS interpreter, and only its `input` crosses into the candidate.
90
+ Other properties, including expected results, remain with the owner. Put hidden cases
91
+ and expectations in private `initialState`, not task `input`: task input is part of
92
+ the policy transcript. The authored controller grades returned case values against
93
+ its own expectations. No host functions, module loader, filesystem or network are
94
+ exposed to candidate JavaScript.
95
+
96
+ Service outcomes are either completed results or explicit rejected submissions.
97
+ Syntax, authorization, output and engine-memory failures cannot manufacture a passing
98
+ answer. A service timeout produces a rejected outcome only after child termination;
99
+ owner cancellation and infrastructure setup failures still fail the operation.
100
+ Service preparation and the authored controller share the operation's overall budget
101
+ and its tracked promise. Collection remains unable to mutate session state, and
102
+ `destroy()` aborts and awaits all active work before running authored cleanup.
103
+
104
+ `executeJavaScriptEnvironmentInProcess` and `executeJavaScriptEnvironmentInWorker`
105
+ provide these services on Node hosts; service children always use Node processes.
106
+ The portable in-process interpreter supports candidate JavaScript and rejects a SQL
107
+ binding without a Node SQL executor. The standalone Node API is
108
+ `executeSqlInProcess({ request, timeoutMs, signal })` from
109
+ `@openpond/evals/sql-execution/node`; request/result schemas are available from
110
+ `@openpond/evals/sql-execution`. The SQLite WASM bytes are embedded in the trusted
111
+ child program, so execution survives installation and host bundling without resolving
112
+ an application-relative WASM file or loading submitted code as host JavaScript.
package/LEARNING.md CHANGED
@@ -79,6 +79,36 @@ for untrusted schemas and deliberately rejects unsupported capabilities.
79
79
 
80
80
  ## Host responsibilities
81
81
 
82
+ ### Reward fixture checks
83
+
84
+ Reward drafts may contain up to 50 named fixtures. JSON and numeric editor fields
85
+ remain strings while a draft is unfinished. `compileRewardAuthoring` is the shared
86
+ source/rubric compiler; `compileRewardFixtures` validates executable fixture input.
87
+ Published fixtures are an immutable evaluator-private `fixtureSetRef` asset included
88
+ in the Reward's `assets`, so editing the next release restores the same examples.
89
+
90
+ `queue_reward_check` takes an exact current Reward draft reference, timeout and
91
+ explicit spend ceiling. It stores a `reward_check` resource without publishing a
92
+ Reward, Task format, source or task evidence. The job binds the compiled Reward and
93
+ fixture hashes. Its worker resolves historical draft bytes even if another editor
94
+ saves or publishes a newer revision. Retry uses the same operation receipt; a
95
+ different request under that operation ID conflicts.
96
+
97
+ Hosts run `createRewardCheckWorker` with an execution adapter that records its
98
+ runtime/package identity and settles only after cleanup. The isolated adapter runs
99
+ portable deterministic checks and authored JavaScript with the supplied interpreter.
100
+ Human review remains pending; missing judge/learned-model adapters remain unavailable.
101
+ No fixture result implicitly calibrates a judge or qualifies a training runtime.
102
+ Per-fixture results distinguish a scored rejection, unavailable grading and grader
103
+ failure, and record whether each outcome matched its authored expectation.
104
+
105
+ Checks are indexed by Reward ID, retain their original draft reference and remain
106
+ readable after publication. Editing source or fixtures must mark prior checks as
107
+ historical. `cancel_reward_check` requests cancellation with revision concurrency;
108
+ the worker publishes terminal cancellation only after execution-owner cleanup.
109
+
110
+ ### Source and execution ownership
111
+
82
112
  Authored verifier code and rubrics use immutable `asset` resources. Publish source
83
113
  and its Reward with `publish_resources`; the service checks UTF-8 size, SHA-256,
84
114
  full reference identity, scope and evaluator visibility in the same transaction.
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { SqlExecutionRequestSchema } from "./sql-execution-contract.js";
3
+ /** Explicit owner-side paths; resolution never evaluates code or traverses prototypes. */
4
+ export const EnvironmentValueReferenceSchema = z.object({
5
+ scope: z.enum(["input", "initialState", "state", "arguments"]),
6
+ path: z.array(z.string().min(1).max(256)).max(16),
7
+ }).strict();
8
+ const common = {
9
+ id: z.string().min(1).max(128),
10
+ operation: z.enum(["step", "collect"]),
11
+ toolName: z.string().min(1).max(500).optional(),
12
+ timeoutMs: z.number().int().min(1).max(20_000),
13
+ };
14
+ export const EnvironmentExecutionServiceSchema = z.discriminatedUnion("kind", [
15
+ z.object({ ...common, kind: z.literal("sqlite.v1"), sql: EnvironmentValueReferenceSchema, snapshot: EnvironmentValueReferenceSchema,
16
+ maxRows: SqlExecutionRequestSchema.shape.maxRows, maxResultBytes: SqlExecutionRequestSchema.shape.maxResultBytes }).strict(),
17
+ z.object({ ...common, kind: z.literal("javascript.v1"), source: EnvironmentValueReferenceSchema, cases: EnvironmentValueReferenceSchema,
18
+ exportName: z.string().regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/).max(128), maxCases: z.number().int().min(1).max(32), maxResultBytes: z.number().int().min(256).max(262_144) }).strict(),
19
+ ]).superRefine((service, context) => {
20
+ if ((service.operation === "step") !== (service.toolName !== undefined))
21
+ context.addIssue({ code: "custom", path: ["toolName"], message: "Only step services require a declared tool name." });
22
+ if (service.operation === "collect") {
23
+ const references = service.kind === "sqlite.v1" ? [service.sql, service.snapshot] : [service.source, service.cases];
24
+ if (references.some(reference => reference.scope === "arguments"))
25
+ context.addIssue({ code: "custom", message: "Collection has no tool arguments." });
26
+ }
27
+ });
@@ -0,0 +1,100 @@
1
+ import { EnvironmentExecutionServiceSchema } from "./environment-execution-services-contract.js";
2
+ import { JavaScriptIsolateExecutionError } from "./javascript-isolate.js";
3
+ import { assertBoundedTaskJson } from "./task-schema.js";
4
+ import { assertSqlExecutionRequest } from "./sql-execution-contract.js";
5
+ function resolveValue(value, reference) {
6
+ let resolved = reference.scope === "arguments" ? value.action?.arguments : value[reference.scope];
7
+ for (const key of reference.path) {
8
+ if (!resolved || typeof resolved !== "object" || !Object.hasOwn(resolved, key))
9
+ throw new Error("environment_service_reference_missing");
10
+ resolved = resolved[key];
11
+ }
12
+ return resolved;
13
+ }
14
+ /** The single returned promise owns service work, controller execution and cancellation. */
15
+ export async function executeEnvironmentWithServices(input, runners) {
16
+ if (!input.executionServices?.length)
17
+ return runners.controller(input);
18
+ if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 30_000)
19
+ throw new Error("environment_timeout_invalid");
20
+ assertBoundedTaskJson(input.value, 4_194_304);
21
+ const services = input.executionServices.map(service => EnvironmentExecutionServiceSchema.parse(service));
22
+ if (services.length > 16)
23
+ throw new Error("environment_service_limit");
24
+ if (new Set(services.map(service => service.id)).size !== services.length)
25
+ throw new Error("environment_service_identity_collision");
26
+ const started = Date.now();
27
+ const controller = new AbortController();
28
+ const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
29
+ const timer = setTimeout(() => controller.abort(new Error("environment_timeout")), input.timeoutMs);
30
+ try {
31
+ const results = Object.create(null);
32
+ for (const service of services) {
33
+ signal.throwIfAborted();
34
+ if (service.operation !== input.operation || (service.operation === "step" && service.toolName !== input.value.action?.name))
35
+ throw new Error("environment_service_operation_mismatch");
36
+ const budget = Math.min(service.timeoutMs, input.timeoutMs - (Date.now() - started));
37
+ if (budget < 1)
38
+ throw new Error("environment_timeout");
39
+ const deadline = new AbortController();
40
+ const serviceSignal = AbortSignal.any([signal, deadline.signal]);
41
+ const serviceTimer = setTimeout(() => deadline.abort(new Error("environment_service_timeout")), budget);
42
+ const serviceStarted = Date.now();
43
+ try {
44
+ if (service.kind === "sqlite.v1") {
45
+ if (!runners.sql)
46
+ throw new Error("environment_sql_executor_required");
47
+ const sql = resolveValue(input.value, service.sql);
48
+ if (typeof sql !== "string" || !sql.length || new TextEncoder().encode(sql).byteLength > 65_536 || sql.includes("\0")) {
49
+ results[service.id] = { status: "rejected", code: "invalid_submission" };
50
+ continue;
51
+ }
52
+ const request = assertSqlExecutionRequest({ schemaVersion: "openpond.sqlExecution.v1", snapshot: resolveValue(input.value, service.snapshot), sql, maxRows: service.maxRows, maxResultBytes: service.maxResultBytes });
53
+ results[service.id] = await runners.sql({ request, timeoutMs: input.timeoutMs, signal: serviceSignal });
54
+ }
55
+ else {
56
+ const source = resolveValue(input.value, service.source), cases = resolveValue(input.value, service.cases);
57
+ if (typeof source !== "string" || new TextEncoder().encode(source).byteLength > 524_288) {
58
+ results[service.id] = { status: "rejected", code: "invalid_submission" };
59
+ continue;
60
+ }
61
+ if (!Array.isArray(cases) || !cases.length || cases.length > service.maxCases || cases.some(item => !item || typeof item !== "object" || !Object.hasOwn(item, "input")))
62
+ throw new Error("environment_service_cases_invalid");
63
+ const values = [];
64
+ for (const item of cases) {
65
+ const remaining = budget - (Date.now() - serviceStarted);
66
+ if (remaining < 1) {
67
+ deadline.abort(new Error("environment_service_timeout"));
68
+ serviceSignal.throwIfAborted();
69
+ }
70
+ // Hidden expected results and all other case fields remain with the owner.
71
+ values.push(await runners.candidate({ source, exportName: service.exportName, value: structuredClone(item.input), timeoutMs: Math.max(1, remaining), maxResultBytes: service.maxResultBytes, deterministic: true, errorPrefix: "candidate", signal: serviceSignal }));
72
+ if (new TextEncoder().encode(JSON.stringify({ status: "completed", cases: values })).byteLength > service.maxResultBytes)
73
+ throw new JavaScriptIsolateExecutionError("candidate_result_too_large");
74
+ }
75
+ results[service.id] = { status: "completed", cases: values };
76
+ }
77
+ }
78
+ catch (error) {
79
+ signal.throwIfAborted();
80
+ if (deadline.signal.aborted)
81
+ results[service.id] = { status: "rejected", code: "timeout" };
82
+ else if (error instanceof JavaScriptIsolateExecutionError)
83
+ results[service.id] = { status: "rejected", code: "execution_failed" };
84
+ else
85
+ throw error;
86
+ }
87
+ finally {
88
+ clearTimeout(serviceTimer);
89
+ }
90
+ }
91
+ signal.throwIfAborted();
92
+ const remaining = input.timeoutMs - (Date.now() - started);
93
+ if (remaining < 1)
94
+ throw new Error("environment_timeout");
95
+ return await runners.controller({ ...input, executionServices: undefined, value: { ...input.value, services: results }, timeoutMs: remaining, signal });
96
+ }
97
+ finally {
98
+ clearTimeout(timer);
99
+ }
100
+ }
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { ImmutableAssetRefSchema, ToolDeclarationSchema, contentHash } from "@openpond/harness";
3
3
  import { assertBoundedTaskJson, validateTaskSchema } from "./task-schema-validation.js";
4
+ import { EnvironmentExecutionServiceSchema } from "./environment-execution-services-contract.js";
5
+ export * from "./environment-execution-services-contract.js";
4
6
  export const JavaScriptEnvironmentDefinitionContentSchema = z.object({
5
7
  schemaVersion: z.literal("openpond.javascriptEnvironment.v1"),
6
8
  id: z.string().trim().min(1).max(500),
@@ -11,6 +13,7 @@ export const JavaScriptEnvironmentDefinitionContentSchema = z.object({
11
13
  maxStateBytes: z.number().int().min(1).max(1_048_576),
12
14
  maxObservationBytes: z.number().int().min(1).max(262_144),
13
15
  operationTimeoutMs: z.number().int().min(1).max(30_000),
16
+ executionServices: z.array(EnvironmentExecutionServiceSchema).min(1).max(16).optional(),
14
17
  }).strict().superRefine((value, context) => {
15
18
  if (new Set(value.tools.map(tool => tool.name)).size !== value.tools.length)
16
19
  context.addIssue({ code: "custom", path: ["tools"], message: "Environment tool names must be unique." });
@@ -20,6 +23,16 @@ export const JavaScriptEnvironmentDefinitionContentSchema = z.object({
20
23
  if (tool.inputSchemaHash !== contentHash(tool.inputSchema))
21
24
  context.addIssue({ code: "custom", path: ["tools", index, "inputSchemaHash"], message: "Tool schema identity differs from its declared schema." });
22
25
  });
26
+ const services = value.executionServices ?? [];
27
+ if (new Set(services.map(service => service.id)).size !== services.length)
28
+ context.addIssue({ code: "custom", path: ["executionServices"], message: "Execution service identities must be unique." });
29
+ services.forEach((service, index) => {
30
+ const tool = value.tools.find(tool => tool.name === service.toolName);
31
+ if (service.operation === "step" && !tool)
32
+ context.addIssue({ code: "custom", path: ["executionServices", index, "toolName"], message: "Execution services must target a declared tool." });
33
+ if (service.timeoutMs >= Math.min(value.operationTimeoutMs, tool?.timeoutMs ?? value.operationTimeoutMs))
34
+ context.addIssue({ code: "custom", path: ["executionServices", index, "timeoutMs"], message: "Service budgets must leave time for the authored controller." });
35
+ });
23
36
  });
24
37
  export const JavaScriptEnvironmentDefinitionSchema = JavaScriptEnvironmentDefinitionContentSchema.safeExtend({ contentHash: z.string().regex(/^[a-f0-9]{64}$/) });
25
38
  export const JavaScriptEnvironmentOperationSchema = z.enum(["create", "reset", "step", "collect", "destroy"]);
@@ -1,13 +1,15 @@
1
1
  import { JavaScriptEnvironmentOperationSchema, JavaScriptEnvironmentResultSchema } from "./javascript-environment-contract.js";
2
2
  import { executeJavaScriptIsolateInWorker } from "./javascript-isolate-node.js";
3
3
  import { executeJavaScriptIsolateInProcess } from "./javascript-isolate-process.js";
4
+ import { executeEnvironmentWithServices } from "./environment-execution-services.js";
5
+ import { executeSqlInProcess } from "./sql-execution-node.js";
4
6
  /** The execution owner awaits termination before recording a step or cleanup. */
5
7
  export async function executeJavaScriptEnvironmentInWorker(input) {
6
8
  const operation = JavaScriptEnvironmentOperationSchema.parse(input.operation);
7
- return JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInWorker({ ...input, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" }));
9
+ return executeEnvironmentWithServices(input, { sql: executeSqlInProcess, candidate: executeJavaScriptIsolateInProcess, controller: async (prepared) => JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInWorker({ ...prepared, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" })) });
8
10
  }
9
11
  /** Uses a Node child process independently of the execution owner's runtime. */
10
12
  export async function executeJavaScriptEnvironmentInProcess(input) {
11
13
  const operation = JavaScriptEnvironmentOperationSchema.parse(input.operation);
12
- return JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInProcess({ ...input, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" }));
14
+ return executeEnvironmentWithServices(input, { sql: executeSqlInProcess, candidate: executeJavaScriptIsolateInProcess, controller: async (prepared) => JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInProcess({ ...prepared, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" })) });
13
15
  }
@@ -2,12 +2,13 @@ import { contentHash } from "@openpond/harness";
2
2
  import { verifyLearningTextAsset } from "./learning/assets.js";
3
3
  import { assertBoundedTaskJson, validateTaskValue } from "./task-schema.js";
4
4
  import { executeJavaScriptIsolate } from "./javascript-isolate.js";
5
+ import { executeEnvironmentWithServices } from "./environment-execution-services.js";
5
6
  import { assertJavaScriptEnvironmentDefinition, JavaScriptEnvironmentOperationSchema, JavaScriptEnvironmentResultSchema } from "./javascript-environment-contract.js";
6
7
  export * from "./javascript-environment-contract.js";
7
8
  /** Pure, bounded module execution. Server hosts should provide the worker runner. */
8
9
  export async function executeJavaScriptEnvironment(input) {
9
10
  const operation = JavaScriptEnvironmentOperationSchema.parse(input.operation);
10
- return JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolate({ ...input, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" }));
11
+ return executeEnvironmentWithServices(input, { candidate: executeJavaScriptIsolate, controller: async (prepared) => JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolate({ ...prepared, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" })) });
11
12
  }
12
13
  export class JavaScriptEnvironmentActionError extends Error {
13
14
  code;
@@ -59,7 +60,8 @@ export async function createJavaScriptEnvironmentSession(input) {
59
60
  throw new JavaScriptEnvironmentActionError("unknown_tool");
60
61
  if (action && tool && !validateTaskValue(tool.inputSchema, action.arguments).valid)
61
62
  throw new JavaScriptEnvironmentActionError("invalid_arguments");
62
- const promise = execute({ source, operation, value: { input: taskInput, initialState, seed, state: structuredClone(state), sequence: steps, action: action ? structuredClone(action) : null }, timeoutMs: Math.min(definition.operationTimeoutMs, tool?.timeoutMs ?? definition.operationTimeoutMs), signal });
63
+ const executionServices = definition.executionServices?.filter(service => service.operation === operation && (operation !== "step" || service.toolName === action?.name));
64
+ const promise = execute({ source, operation, value: { input: taskInput, initialState, seed, state: structuredClone(state), sequence: steps, action: action ? structuredClone(action) : null }, timeoutMs: Math.min(definition.operationTimeoutMs, tool?.timeoutMs ?? definition.operationTimeoutMs), signal, ...(executionServices?.length ? { executionServices } : {}) });
63
65
  active = promise;
64
66
  const result = JavaScriptEnvironmentResultSchema.parse(await promise);
65
67
  signal.throwIfAborted();
@@ -1,8 +1,8 @@
1
- import { executeJavaScriptIsolate } from "./javascript-isolate.js";
1
+ import { executeJavaScriptIsolate, JavaScriptIsolateExecutionError } from "./javascript-isolate.js";
2
2
  // Only this bundled, trusted program runs in Node. Authored source is interpreter data.
3
3
  const owner = globalThis;
4
4
  const input = owner.__openpondIsolateInput;
5
5
  delete owner.__openpondIsolateInput;
6
6
  if (!input)
7
7
  throw new Error("JavaScript process requires its execution owner.");
8
- void executeJavaScriptIsolate(input).then((result) => process.stdout.write(JSON.stringify({ ok: true, result })), (error) => process.stdout.write(JSON.stringify({ ok: false, error: (error instanceof Error ? error.message : "JavaScript execution failed.").slice(0, 8_192) })));
8
+ void executeJavaScriptIsolate(input).then((result) => process.stdout.write(JSON.stringify({ ok: true, result })), (error) => process.stdout.write(JSON.stringify({ ok: false, executionError: error instanceof JavaScriptIsolateExecutionError, error: (error instanceof Error ? error.message : "JavaScript execution failed.").slice(0, 8_192) })));