@effect-agent/testing 0.1.0-beta.8 → 0.1.0-beta.81

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 (54) hide show
  1. package/dist/Certification.d.mts +105 -0
  2. package/dist/Certification.mjs +592 -0
  3. package/dist/Certification.mjs.map +1 -0
  4. package/dist/Chaos.d.mts +126 -0
  5. package/dist/Chaos.mjs +755 -0
  6. package/dist/Chaos.mjs.map +1 -0
  7. package/dist/CodeExecutorConformance.d.mts +33 -0
  8. package/dist/CodeExecutorConformance.mjs +231 -0
  9. package/dist/CodeExecutorConformance.mjs.map +1 -0
  10. package/dist/CodeExecutorSubstitute.d.mts +21 -0
  11. package/dist/CodeExecutorSubstitute.mjs +368 -0
  12. package/dist/CodeExecutorSubstitute.mjs.map +1 -0
  13. package/dist/DocsResearcher.d.mts +270 -0
  14. package/dist/DocsResearcher.mjs +490 -0
  15. package/dist/DocsResearcher.mjs.map +1 -0
  16. package/dist/ScriptedModel-DAvxIiud.d.mts +220 -0
  17. package/dist/ScriptedModel.d.mts +2 -0
  18. package/dist/ScriptedModel.mjs +155 -0
  19. package/dist/ScriptedModel.mjs.map +1 -0
  20. package/dist/TravelPlanner.d.mts +1665 -0
  21. package/dist/TravelPlanner.mjs +1963 -0
  22. package/dist/TravelPlanner.mjs.map +1 -0
  23. package/dist/deterministic-layers-Eka0fMZq.mjs +358 -0
  24. package/dist/deterministic-layers-Eka0fMZq.mjs.map +1 -0
  25. package/dist/index.d.mts +2 -3406
  26. package/dist/index.mjs +2 -4771
  27. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  28. package/package.json +1 -48
  29. package/src/{certification.ts → Certification.ts} +251 -117
  30. package/src/{chaos.ts → Chaos.ts} +246 -122
  31. package/src/{code-executor-conformance.ts → CodeExecutorConformance.ts} +29 -6
  32. package/src/{code-executor-substitute.ts → CodeExecutorSubstitute.ts} +112 -45
  33. package/src/{fixtures/docs-researcher/index.ts → DocsResearcher.ts} +68 -5
  34. package/src/{scripted-model.ts → ScriptedModel.ts} +22 -25
  35. package/src/TravelPlanner.ts +232 -0
  36. package/src/fixtures/docs-researcher/definition.ts +19 -10
  37. package/src/fixtures/docs-researcher/harness.ts +41 -30
  38. package/src/fixtures/docs-researcher/mcp.ts +49 -3
  39. package/src/fixtures/travel-planner/definition.ts +15 -2
  40. package/src/fixtures/travel-planner/deterministic-layers.ts +47 -4
  41. package/src/fixtures/travel-planner/phase2.ts +4 -3
  42. package/src/fixtures/travel-planner/phase3.ts +18 -41
  43. package/src/fixtures/travel-planner/phase4.ts +23 -36
  44. package/src/fixtures/travel-planner/phase5.ts +38 -41
  45. package/src/fixtures/travel-planner/phase6.ts +187 -84
  46. package/src/fixtures/travel-planner/phase7.ts +4 -102
  47. package/src/fixtures/travel-planner/scenarios.ts +3 -4
  48. package/src/fixtures/travel-planner/subagents-durable.ts +33 -57
  49. package/src/fixtures/travel-planner/subagents.ts +35 -13
  50. package/src/index.ts +1 -11
  51. package/dist/index.mjs.map +0 -1
  52. package/src/code-executor-conformance.d.ts +0 -30
  53. package/src/fixtures/travel-planner/index.ts +0 -11
  54. package/src/fixtures/warehouse/index.ts +0 -412
@@ -1,19 +1,21 @@
1
1
  import {
2
- CodeExecutionError,
3
2
  CodeExecutionHost,
4
3
  CodeExecutionLimits,
5
4
  CodeExecutionNamespace,
6
5
  CodeExecutionRequest,
7
- CodeExecutionResult,
8
6
  CodeExecutor,
9
- CodeHostCall,
10
7
  CodeHostCallFailure,
11
- CodeHostCallResult,
12
8
  CodeHostCallSuccess,
9
+ type CodeExecutionError,
10
+ type CodeExecutionResult,
11
+ type CodeHostCall,
12
+ type CodeHostCallResult,
13
+ } from "@effect-agent/sandbox/CodeExecutor";
14
+ import {
13
15
  NetworkAllowlist,
14
16
  NetworkDisabled,
15
- SandboxImplementation,
16
- } from "@effect-agent/sandbox";
17
+ type SandboxImplementation,
18
+ } from "@effect-agent/sandbox/Sandbox";
17
19
  import { Deferred, Duration, Effect, Fiber, Schema } from "effect";
18
20
 
19
21
  /**
@@ -88,12 +90,14 @@ const respondingHost = (
88
90
  respond: (call: CodeHostCall) => CodeHostCallResult,
89
91
  ): { readonly host: CodeExecutionHost["Service"]; readonly calls: Array<CodeHostCall> } => {
90
92
  const calls: Array<CodeHostCall> = [];
93
+
91
94
  return {
92
95
  calls,
93
96
  host: {
94
97
  call: (call) =>
95
98
  Effect.sync(() => {
96
99
  calls.push(call);
100
+
97
101
  return respond(call);
98
102
  }),
99
103
  },
@@ -106,6 +110,7 @@ const runPass = (
106
110
  ): Effect.Effect<CodeExecutionResult, CodeExecutionError, CodeExecutor> =>
107
111
  Effect.gen(function* () {
108
112
  const executor = yield* CodeExecutor;
113
+
109
114
  return yield* executor
110
115
  .execute(request)
111
116
  .pipe(Effect.provideService(CodeExecutionHost, CodeExecutionHost.of(host)));
@@ -134,6 +139,7 @@ const expectSuccess = (
134
139
  ),
135
140
  Effect.flatMap((result) => {
136
141
  const complaint = check(result);
142
+
137
143
  return complaint === undefined ? Effect.void : Effect.fail(violation(caseName, complaint));
138
144
  }),
139
145
  );
@@ -157,6 +163,7 @@ const expectFailure = (
157
163
  );
158
164
  }
159
165
  const complaint = check?.(error);
166
+
160
167
  return complaint === undefined ? Effect.void : Effect.fail(violation(caseName, complaint));
161
168
  }),
162
169
  );
@@ -165,6 +172,7 @@ export const codeExecutorConformanceCases = (
165
172
  options: CodeExecutorConformanceOptions,
166
173
  ): ReadonlyArray<CodeExecutorConformanceCase> => {
167
174
  const posture = options.implementation;
175
+
168
176
  return [
169
177
  {
170
178
  name: "TEST-015 executes bounded JSON computation and returns the program value",
@@ -184,9 +192,11 @@ export const codeExecutorConformanceCases = (
184
192
  name: "CAP-015 reports its isolation posture honestly in results and errors",
185
193
  run: Effect.gen(function* () {
186
194
  const caseName = "CAP-015 reports its isolation posture honestly in results and errors";
195
+
187
196
  const result = yield* runPass(makeRequest("async () => 1"), unusedHost).pipe(
188
197
  Effect.mapError((error) => violation(caseName, `expected success, got ${error._tag}`)),
189
198
  );
199
+
190
200
  if (
191
201
  result.implementation.isolation !== posture.isolation ||
192
202
  result.implementation.identity !== posture.identity
@@ -196,10 +206,12 @@ export const codeExecutorConformanceCases = (
196
206
  `result posture ${preview(result.implementation)} does not match the declared ${preview(posture)}`,
197
207
  );
198
208
  }
209
+
199
210
  const error = yield* runPass(makeRequest("async () => {"), unusedHost).pipe(
200
211
  Effect.flip,
201
212
  Effect.mapError(() => violation(caseName, "expected the invalid-source pass to fail")),
202
213
  );
214
+
203
215
  // Every expected execution failure carries the posture; an adapter
204
216
  // omitting the field must fail this case, not slip past a probe.
205
217
  if (
@@ -219,11 +231,13 @@ export const codeExecutorConformanceCases = (
219
231
  run: Effect.gen(function* () {
220
232
  const caseName =
221
233
  "TEST-015 routes host calls through the CodeExecutionHost in program order";
234
+
222
235
  const { host, calls } = respondingHost((call) =>
223
236
  call.method === "query"
224
237
  ? CodeHostCallSuccess.make({ value: { rows: [1, 2, 3] } })
225
238
  : CodeHostCallSuccess.make({ value: 3 }),
226
239
  );
240
+
227
241
  const result = yield* runPass(
228
242
  makeRequest(
229
243
  "async () => { const q = await warehouse.query({ sql: 'select' }); const c = await warehouse.count({ table: 't' }); return { rows: q.rows, count: c }; }",
@@ -235,10 +249,12 @@ export const codeExecutorConformanceCases = (
235
249
  violation(caseName, `expected success, got ${error._tag}: ${preview(error)}`),
236
250
  ),
237
251
  );
252
+
238
253
  if (JSON.stringify(result.value) !== JSON.stringify({ rows: [1, 2, 3], count: 3 })) {
239
254
  return yield* violation(caseName, `unexpected value ${preview(result.value)}`);
240
255
  }
241
256
  const observed = calls.map((call) => `${call.namespace}.${call.method}`);
257
+
242
258
  if (JSON.stringify(observed) !== JSON.stringify(["warehouse.query", "warehouse.count"])) {
243
259
  return yield* violation(caseName, `unexpected host call order ${preview(observed)}`);
244
260
  }
@@ -376,6 +392,7 @@ export const codeExecutorConformanceCases = (
376
392
  run: Effect.gen(function* () {
377
393
  const caseName = "TEST-015 fails typed when host calls exceed the executor cap";
378
394
  const { host, calls } = respondingHost(() => CodeHostCallSuccess.make({ value: null }));
395
+
379
396
  yield* expectFailure(
380
397
  caseName,
381
398
  makeRequest(
@@ -404,6 +421,8 @@ export const codeExecutorConformanceCases = (
404
421
  "TEST-015 fails typed on a host outcome outside the protocol schema",
405
422
  makeRequest("async () => warehouse.query({})", { namespaces: [warehouseNamespace] }),
406
423
  {
424
+ // Deliberately violate the compile-time host contract to verify that an adapter
425
+ // independently decodes the runtime protocol boundary.
407
426
  call: () => Effect.succeed({ bogus: true } as unknown as CodeHostCallResult),
408
427
  },
409
428
  "CodeExecutionProtocolError",
@@ -460,6 +479,7 @@ export const codeExecutorConformanceCases = (
460
479
  const caseName = "TEST-015 interruption reaches in-flight host calls and pass teardown";
461
480
  const started = yield* Deferred.make<void>();
462
481
  const witness = { hostCallInterrupted: false };
482
+
463
483
  const host: CodeExecutionHost["Service"] = {
464
484
  call: () =>
465
485
  Deferred.succeed(started, undefined).pipe(
@@ -471,16 +491,19 @@ export const codeExecutorConformanceCases = (
471
491
  ),
472
492
  ),
473
493
  };
494
+
474
495
  const fiber = yield* runPass(
475
496
  makeRequest("async () => warehouse.query({})", { namespaces: [warehouseNamespace] }),
476
497
  host,
477
498
  ).pipe(Effect.forkChild);
499
+
478
500
  // Guard against a broken adapter that settles the pass without ever
479
501
  // reaching the host: the case must report a violation, not hang.
480
502
  const winner = yield* Effect.raceFirst(
481
503
  Deferred.await(started).pipe(Effect.as("started" as const)),
482
504
  Fiber.join(fiber).pipe(Effect.exit, Effect.as("exited" as const)),
483
505
  );
506
+
484
507
  if (winner === "exited") {
485
508
  return yield* violation(
486
509
  caseName,
@@ -1,9 +1,6 @@
1
1
  import {
2
2
  CodeExecutionHost,
3
- CodeExecutionLimits,
4
- CodeExecutionNamespace,
5
3
  CodeExecutionProtocolError,
6
- CodeExecutionRequest,
7
4
  CodeExecutionResourceUse,
8
5
  CodeExecutionResult,
9
6
  CodeExecutionTimeoutError,
@@ -16,8 +13,11 @@ import {
16
13
  CodeOutputLimitError,
17
14
  CodeProgramFailedError,
18
15
  CodeSourceError,
19
- SandboxImplementation,
20
- } from "@effect-agent/sandbox";
16
+ type CodeExecutionLimits,
17
+ type CodeExecutionNamespace,
18
+ type CodeExecutionRequest,
19
+ } from "@effect-agent/sandbox/CodeExecutor";
20
+ import { SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
21
21
  import { Clock, Duration, Effect, Fiber, Layer, Option, Queue, Schema } from "effect";
22
22
 
23
23
  /**
@@ -91,6 +91,7 @@ const formatLogValue = (value: unknown): string => {
91
91
  if (typeof value === "string") {
92
92
  return value;
93
93
  }
94
+
94
95
  return JSON.stringify(value) ?? String(value);
95
96
  } catch {
96
97
  try {
@@ -104,17 +105,21 @@ const formatLogValue = (value: unknown): string => {
104
105
  const makeConsole = (capture: LogCapture, limits: CodeExecutionLimits) => {
105
106
  const write = (...values: ReadonlyArray<unknown>): void => {
106
107
  const joined = values.map(formatLogValue).join(" ");
108
+
107
109
  const line =
108
110
  joined.length > MAX_LOG_LINE_CHARACTERS
109
111
  ? `${joined.slice(0, MAX_LOG_LINE_CHARACTERS - 1)}…`
110
112
  : joined;
113
+
111
114
  const bytes = utf8ByteLength(line);
115
+
112
116
  if (capture.lines.length >= MAX_LOG_LINES || capture.bytes + bytes > limits.maxLogBytes) {
113
117
  throw new LogLimitSignal(capture.bytes + bytes);
114
118
  }
115
119
  capture.lines.push(line);
116
120
  capture.bytes += bytes;
117
121
  };
122
+
118
123
  return { debug: write, error: write, info: write, log: write, warn: write };
119
124
  };
120
125
 
@@ -131,18 +136,21 @@ const buildNamespaceObject = (
131
136
  offer: (pending: PendingHostCall) => void,
132
137
  ): Record<string, unknown> => {
133
138
  const methods: Record<string, unknown> = {};
139
+
134
140
  for (const method of namespace.methods) {
135
141
  methods[method] = (argument: unknown) =>
136
142
  new Promise((resolve, reject) => {
137
143
  offer({ namespace: namespace.name, method, argument, resolve, reject });
138
144
  });
139
145
  }
146
+
140
147
  return methods;
141
148
  };
142
149
 
143
150
  const boundedText = (value: unknown): string => {
144
151
  try {
145
152
  const text = value instanceof Error ? `${value.name}: ${value.message}` : formatLogValue(value);
153
+
146
154
  return text.slice(0, MAX_THROWN_CHARACTERS);
147
155
  } catch {
148
156
  return "[unserializable thrown value]";
@@ -158,24 +166,44 @@ const safeDecodeJson = (value: unknown): Option.Option<Schema.Json> => {
158
166
  }
159
167
  };
160
168
 
169
+ const decodeJsonText = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
170
+
171
+ /** Own serialized result text before bounding and decoding its detached JSON snapshot. */
172
+ const serializeJson = (value: unknown): Option.Option<string> => {
173
+ const decoded = safeDecodeJson(value);
174
+
175
+ if (Option.isNone(decoded)) return Option.none();
176
+ try {
177
+ return Option.fromUndefinedOr(JSON.stringify(decoded.value));
178
+ } catch {
179
+ return Option.none();
180
+ }
181
+ };
182
+
161
183
  const boundedThrown = (value: unknown): Schema.Json => {
162
184
  const decoded = safeDecodeJson(value);
185
+
163
186
  if (Option.isSome(decoded)) {
164
187
  try {
165
188
  const encoded = JSON.stringify(decoded.value);
189
+
166
190
  if (encoded !== undefined && encoded.length <= MAX_THROWN_CHARACTERS) {
167
- return decoded.value;
191
+ const snapshot = decodeJsonText(encoded);
192
+
193
+ if (Option.isSome(snapshot)) return snapshot.value;
168
194
  }
169
195
  } catch {
170
196
  // fall through to the bounded string form
171
197
  }
172
198
  }
199
+
173
200
  return boundedText(value);
174
201
  };
175
202
 
176
203
  const encodedJsonByteLength = (value: Schema.Json): number | undefined => {
177
204
  try {
178
205
  const encoded = JSON.stringify(value);
206
+
179
207
  return encoded === undefined ? undefined : utf8ByteLength(encoded);
180
208
  } catch {
181
209
  return undefined;
@@ -213,6 +241,7 @@ const validateRequest = (
213
241
  }
214
242
  const reservedNames = new Set<string>([...shadowedGlobals, "console"]);
215
243
  const seen = new Set<string>();
244
+
216
245
  for (const namespace of request.namespaces) {
217
246
  if (reservedNames.has(namespace.name) || seen.has(namespace.name)) {
218
247
  return yield* CodeExecutorUnsupportedError.make({
@@ -224,6 +253,7 @@ const validateRequest = (
224
253
  seen.add(namespace.name);
225
254
  }
226
255
  const sourceBytes = utf8ByteLength(request.source);
256
+
227
257
  if (sourceBytes > request.limits.maxSourceBytes) {
228
258
  return yield* CodeSourceError.make({
229
259
  implementation: inProcessCodeExecutorImplementation,
@@ -246,6 +276,7 @@ const serveHostCalls = (
246
276
  Effect.gen(function* () {
247
277
  while (true) {
248
278
  const pending = yield* Queue.take(queue);
279
+
249
280
  counter.calls += 1;
250
281
  if (counter.calls > limits.maxHostCalls) {
251
282
  return yield* CodeHostCallLimitError.make({
@@ -255,11 +286,13 @@ const serveHostCalls = (
255
286
  });
256
287
  }
257
288
  const argument = safeDecodeJson(pending.argument);
289
+
258
290
  if (Option.isNone(argument)) {
259
291
  pending.reject(new TypeError("host call arguments must be JSON values"));
260
292
  continue;
261
293
  }
262
294
  const argumentBytes = encodedJsonByteLength(argument.value);
295
+
263
296
  if (argumentBytes === undefined || argumentBytes > limits.maxHostCallArgumentBytes) {
264
297
  return yield* CodeOutputLimitError.make({
265
298
  implementation: inProcessCodeExecutorImplementation,
@@ -269,6 +302,7 @@ const serveHostCalls = (
269
302
  logs: [...capture.lines],
270
303
  });
271
304
  }
305
+
272
306
  const rawOutcome = yield* host.call(
273
307
  CodeHostCall.make({
274
308
  namespace: pending.namespace,
@@ -276,7 +310,9 @@ const serveHostCalls = (
276
310
  argument: argument.value,
277
311
  }),
278
312
  );
313
+
279
314
  const outcome = decodeHostOutcome(rawOutcome);
315
+
280
316
  if (Option.isNone(outcome)) {
281
317
  return yield* CodeExecutionProtocolError.make({
282
318
  implementation: inProcessCodeExecutorImplementation,
@@ -288,6 +324,7 @@ const serveHostCalls = (
288
324
  continue;
289
325
  }
290
326
  const resultBytes = encodedJsonByteLength(outcome.value.value);
327
+
291
328
  if (resultBytes === undefined || resultBytes > limits.maxHostCallResultBytes) {
292
329
  return yield* CodeOutputLimitError.make({
293
330
  implementation: inProcessCodeExecutorImplementation,
@@ -306,27 +343,37 @@ const classifyProgramFailure = (
306
343
  limits: CodeExecutionLimits,
307
344
  capture: LogCapture,
308
345
  ): CodeOutputLimitError | CodeSourceError | CodeProgramFailedError => {
309
- const inner = thrown instanceof EvaluationThrew ? thrown.inner : thrown;
310
- if (inner instanceof LogLimitSignal) {
311
- return CodeOutputLimitError.make({
312
- implementation: inProcessCodeExecutorImplementation,
313
- surface: "logs",
314
- limit: limits.maxLogBytes,
315
- observed: inner.observed,
316
- logs: [...capture.lines],
317
- });
318
- }
319
- if (inner instanceof NotAFunction) {
320
- return CodeSourceError.make({
321
- implementation: inProcessCodeExecutorImplementation,
322
- reason: "not-a-function",
323
- message: `The source expression evaluated to ${inner.actual}; it must evaluate to one async function`,
324
- });
346
+ let inner = thrown;
347
+ let reason: "threw" | "rejected" = "rejected";
348
+
349
+ try {
350
+ const evaluationThrew = thrown instanceof EvaluationThrew;
351
+
352
+ inner = evaluationThrew ? thrown.inner : thrown;
353
+ if (inner instanceof LogLimitSignal) {
354
+ return CodeOutputLimitError.make({
355
+ implementation: inProcessCodeExecutorImplementation,
356
+ surface: "logs",
357
+ limit: limits.maxLogBytes,
358
+ observed: inner.observed,
359
+ logs: [...capture.lines],
360
+ });
361
+ }
362
+ if (inner instanceof NotAFunction) {
363
+ return CodeSourceError.make({
364
+ implementation: inProcessCodeExecutorImplementation,
365
+ reason: "not-a-function",
366
+ message: `The source expression evaluated to ${inner.actual}; it must evaluate to one async function`,
367
+ });
368
+ }
369
+ // Async body throws become rejections: exception-like values read as `threw`,
370
+ // while plain values such as uncaught host failure envelopes read as `rejected`.
371
+ reason = evaluationThrew || inner instanceof Error ? "threw" : "rejected";
372
+ } catch {
373
+ // A program-owned Proxy can throw from instanceof's prototype lookup. Keep
374
+ // that failure inside the same guarded diagnostic boundary as its value.
325
375
  }
326
- // An async function converts a body-level `throw` into a rejection, so the
327
- // split is by value shape: exception-like values read as `threw`, plain
328
- // rejection values (an uncaught host failure envelope) read as `rejected`.
329
- const reason = thrown instanceof EvaluationThrew || inner instanceof Error ? "threw" : "rejected";
376
+
330
377
  return CodeProgramFailedError.make({
331
378
  implementation: inProcessCodeExecutorImplementation,
332
379
  reason,
@@ -346,6 +393,9 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
346
393
 
347
394
  const factory = yield* Effect.try({
348
395
  try: () =>
396
+ // This substitute intentionally evaluates authored test programs in-process and reports
397
+ // an `unisolated` posture. Real adapters own the security boundary and never use this path.
398
+ // oxlint-disable-next-line typescript/no-implied-eval
349
399
  new Function(
350
400
  ...shadowedGlobals,
351
401
  "console",
@@ -367,24 +417,32 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
367
417
  // rejected synchronously, so the queue stays bounded against hostile
368
418
  // programs.
369
419
  let issuedHostCalls = 0;
420
+
370
421
  const namespaceObjects = request.namespaces.map((namespace) =>
371
422
  buildNamespaceObject(namespace, (pending) => {
372
423
  issuedHostCalls += 1;
373
424
  if (issuedHostCalls > request.limits.maxHostCalls + 1) {
374
425
  pending.reject(new Error(`host-call limit of ${request.limits.maxHostCalls} exceeded`));
426
+
375
427
  return;
376
428
  }
377
429
  Queue.offerUnsafe(queue, pending);
378
430
  }),
379
431
  );
380
432
 
381
- const server = yield* serveHostCalls(host, queue, request.limits, capture, counter).pipe(
382
- Effect.forkScoped,
383
- );
433
+ const concurrency = request.limits.maxHostCallConcurrency ?? 4;
434
+
435
+ const server = yield* Effect.all(
436
+ Array.from({ length: concurrency }, () =>
437
+ serveHostCalls(host, queue, request.limits, capture, counter),
438
+ ),
439
+ { concurrency, discard: true },
440
+ ).pipe(Effect.andThen(Effect.never), Effect.forkScoped);
384
441
 
385
442
  const program = Effect.tryPromise({
386
443
  try: async () => {
387
444
  let candidate: unknown;
445
+
388
446
  try {
389
447
  candidate = factory(
390
448
  ...shadowedGlobals.map(() => undefined),
@@ -398,17 +456,20 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
398
456
  throw new EvaluationThrew(new NotAFunction(typeof candidate));
399
457
  }
400
458
  let outcome: unknown;
459
+
401
460
  try {
402
- outcome = (candidate as () => unknown)();
461
+ outcome = candidate();
403
462
  } catch (cause) {
404
463
  throw new EvaluationThrew(cause);
405
464
  }
465
+
406
466
  return await Promise.resolve(outcome);
407
467
  },
408
468
  catch: (thrown) => classifyProgramFailure(thrown, request.limits, capture),
409
469
  });
410
470
 
411
471
  const startedAt = yield* Clock.currentTimeMillis;
472
+
412
473
  // The wall-clock deadline interrupts only at asynchronous suspension
413
474
  // points: a synchronous runaway shares the host thread and cannot be
414
475
  // stopped in-process — exactly why the platform CPU enforcement cases
@@ -428,6 +489,7 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
428
489
  }),
429
490
  Effect.ensuring(Fiber.interrupt(server)),
430
491
  );
492
+
431
493
  const finishedAt = yield* Clock.currentTimeMillis;
432
494
 
433
495
  // An unawaited burst can outrun the server: the program may return before
@@ -442,31 +504,36 @@ const executeInProcess: CodeExecutorExecute = Effect.fn("InProcessCodeExecutor.e
442
504
  });
443
505
  }
444
506
 
445
- const value = yield* Schema.decodeUnknownEffect(Schema.Json)(returned).pipe(
446
- Effect.mapError(() =>
447
- CodeProgramFailedError.make({
448
- implementation: inProcessCodeExecutorImplementation,
449
- reason: "non-json-result",
450
- thrown: null,
451
- message: "The program must return a JSON value",
452
- logs: [...capture.lines],
453
- }),
454
- ),
455
- );
456
- const resultBytes = encodedJsonByteLength(value);
457
- if (resultBytes === undefined || resultBytes > request.limits.maxResultBytes) {
507
+ const nonJsonResult = () =>
508
+ CodeProgramFailedError.make({
509
+ implementation: inProcessCodeExecutorImplementation,
510
+ reason: "non-json-result",
511
+ thrown: null,
512
+ message: "The program must return a JSON value",
513
+ logs: [...capture.lines],
514
+ });
515
+
516
+ const encoded = serializeJson(returned);
517
+
518
+ if (Option.isNone(encoded)) return yield* nonJsonResult();
519
+ const resultBytes = utf8ByteLength(encoded.value);
520
+
521
+ if (resultBytes > request.limits.maxResultBytes) {
458
522
  return yield* CodeOutputLimitError.make({
459
523
  implementation: inProcessCodeExecutorImplementation,
460
524
  surface: "result",
461
525
  limit: request.limits.maxResultBytes,
462
- observed: resultBytes ?? 0,
526
+ observed: resultBytes,
463
527
  logs: [...capture.lines],
464
528
  });
465
529
  }
530
+ const decoded = decodeJsonText(encoded.value);
531
+
532
+ if (Option.isNone(decoded)) return yield* nonJsonResult();
466
533
 
467
534
  return CodeExecutionResult.make({
468
535
  implementation: inProcessCodeExecutorImplementation,
469
- value,
536
+ value: decoded.value,
470
537
  logs: [...capture.lines],
471
538
  resourceUse: CodeExecutionResourceUse.make({
472
539
  wallTime: Duration.millis(Math.max(0, finishedAt - startedAt)),
@@ -1,13 +1,15 @@
1
+ /** Shared Docs Researcher definitions and MCP delegation fixtures. */
2
+
1
3
  // ---------------------------------------------------------------------------
2
4
  // Docs Researcher fixture (P7 internal agent #3): S2 durable delegation on DN
3
- // with MCP-discovered content tools. See definition.ts / mcp.ts / harness.ts.
5
+ // with MCP-discovered content tools. See fixtures/docs-researcher/{definition,mcp,harness}.ts.
4
6
  //
5
7
  // Authoring friction note (WP7 input; real observations from writing this
6
8
  // agent):
7
9
  //
8
10
  // 1. MCP discovery and Agent authoring do not meet in the type system. The
9
11
  // connector's validated Toolkit arrives as `Toolkit.Any`, while
10
- // `Agent.define` needs the statically typed toolkit — so the binding
12
+ // `Agent.make` needs the statically typed toolkit — so the binding
11
13
  // between "what discovery served" and "what the child was authored
12
14
  // against" had to be re-proved by hand (`assertDiscoveryMatchesAuthoredToolkit`
13
15
  // re-deriving `Tool.getJsonSchema` on both sides). A framework helper that
@@ -29,6 +31,67 @@
29
31
  // exactly where a reviewer looks for them.
30
32
  // ---------------------------------------------------------------------------
31
33
 
32
- export * from "./definition.ts";
33
- export * from "./harness.ts";
34
- export * from "./mcp.ts";
34
+ export {
35
+ ResearchDocumentId,
36
+ BoundedSummary,
37
+ DocumentQuery,
38
+ ResearchDocument,
39
+ DocumentUnavailable,
40
+ DocumentLibrary,
41
+ FetchDocument,
42
+ DocContentToolkit,
43
+ docContentToolkitLayer,
44
+ docsDocumentBodySecret,
45
+ researchCorpusDocumentIds,
46
+ researchDocumentLookup,
47
+ researchDocumentFor,
48
+ documentBodyPhrase,
49
+ SummaryBrief,
50
+ DocumentSummary,
51
+ documentSummaryFor,
52
+ encodedDocumentSummary,
53
+ DocSummarizer,
54
+ SummaryRequest,
55
+ SummaryFinding,
56
+ DocumentSummaryFailed,
57
+ documentSummaryPolicy,
58
+ delegateDocumentSummary,
59
+ mapSummaryChildFailure,
60
+ docsSummarizerDigestStrings,
61
+ docsSummaryHandlersLayer,
62
+ ResearchRequest,
63
+ ResearchDigest,
64
+ docsCoordinatorConfidentialMarker,
65
+ docsMissionConfidentialMarker,
66
+ DocsResearcherToolkit,
67
+ DocsResearcher,
68
+ researchMissionRequest,
69
+ expectedResearchDigest,
70
+ } from "./fixtures/docs-researcher/definition.ts";
71
+
72
+ export {
73
+ docsResearcherDeploymentId,
74
+ docsResearcherProducerId,
75
+ docsResearcherPrincipal,
76
+ docsCoordinatorDigests,
77
+ docsSummarizerDigests,
78
+ docsResearcherSubmitOptions,
79
+ docsResearcherSubmitAgent,
80
+ summarizeCallId,
81
+ fetchCallId,
82
+ type DocsResearcherHarnessOptions,
83
+ type DocsResearcherHarness,
84
+ makeDocsResearcherHarness,
85
+ redactedDocumentPreview,
86
+ type DocsResearcherHarnessRequirements,
87
+ } from "./fixtures/docs-researcher/harness.ts";
88
+
89
+ export {
90
+ docsMcpRequest,
91
+ docsMcpIdentity,
92
+ docsMcpConnectorLayer,
93
+ docsMcpOversizedConnectorLayer,
94
+ docsMcpMismatchedConnectorLayer,
95
+ assertDiscoveryMatchesAuthoredToolkit,
96
+ DocsMcpDiscoveryEvidence,
97
+ } from "./fixtures/docs-researcher/mcp.ts";