@purista/harness 1.2.1 → 1.2.3

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 (46) hide show
  1. package/dist/agents/index.d.ts +1 -0
  2. package/dist/agents/index.js +276 -141
  3. package/dist/errors/catalog.d.ts +4 -3
  4. package/dist/harness/defineHarness.d.ts +45 -4
  5. package/dist/harness/defineHarness.js +51 -2
  6. package/dist/index.d.ts +1 -1
  7. package/dist/memory/sandbox/index.js +7 -1
  8. package/dist/models/registry.d.ts +10 -3
  9. package/dist/models/registry.js +45 -3
  10. package/dist/ports/base-model-provider.js +2 -0
  11. package/dist/ports/capabilities.d.ts +2 -0
  12. package/dist/ports/harness-context.d.ts +1 -0
  13. package/dist/ports/model-provider.d.ts +4 -0
  14. package/dist/ports/state.d.ts +6 -0
  15. package/dist/runtime/abort.d.ts +5 -0
  16. package/dist/runtime/abort.js +33 -0
  17. package/dist/runtime/durable.d.ts +2 -0
  18. package/dist/runtime/durable.js +6 -2
  19. package/dist/runtime/sessionDurable.d.ts +49 -0
  20. package/dist/runtime/sessionDurable.js +135 -0
  21. package/dist/runtime/steps.d.ts +19 -1
  22. package/dist/runtime/steps.js +21 -3
  23. package/dist/sandbox/index.d.ts +34 -0
  24. package/dist/sandbox/index.js +40 -3
  25. package/dist/sessions/index.d.ts +15 -2
  26. package/dist/sessions/index.js +336 -105
  27. package/dist/skills/index.js +19 -6
  28. package/dist/state/in-memory.d.ts +1 -0
  29. package/dist/state/in-memory.js +15 -0
  30. package/dist/telemetry/shim.js +9 -4
  31. package/dist/testing/durableWorkspaceStoreContract.d.ts +1 -1
  32. package/dist/testing/durableWorkspaceStoreContract.js +64 -28
  33. package/dist/tools/index.d.ts +2 -0
  34. package/dist/tools/index.js +15 -1
  35. package/dist/tools/mcp/runner.js +11 -6
  36. package/dist/tools/mcp/stdio.js +170 -1
  37. package/dist/ulid/index.d.ts +6 -1
  38. package/dist/ulid/index.js +31 -13
  39. package/dist/version.d.ts +2 -0
  40. package/dist/version.js +2 -0
  41. package/dist/workflows/index.js +7 -1
  42. package/dist/workspace/in-memory.d.ts +9 -10
  43. package/dist/workspace/in-memory.js +191 -48
  44. package/package.json +1 -1
  45. package/dist/harness/errors.d.ts +0 -62
  46. package/dist/harness/errors.js +0 -67
@@ -3,6 +3,8 @@ import { ulid } from '../ulid/index.js';
3
3
  import { runDefaultAgent } from '../agents/index.js';
4
4
  import { runWorkflow } from '../workflows/index.js';
5
5
  import { createMemoryFacade, createSessionMemory } from '../ports/memory.js';
6
+ import { beginDurableWorkflow, DURABLE_RUN_ID_PATTERN, isExecutableDurableRuntime } from '../runtime/sessionDurable.js';
7
+ import { HarnessConfigError } from '../errors/catalog.js';
6
8
  import { loadSkillsSync } from '../skills/index.js';
7
9
  import { createModelRegistry } from '../models/registry.js';
8
10
  import { createMetrics, createTelemetryShim } from '../telemetry/index.js';
@@ -11,6 +13,82 @@ const NEVER_ABORT_SIGNAL = new AbortController().signal;
11
13
  function now() {
12
14
  return new Date().toISOString();
13
15
  }
16
+ const STREAM_MAX_BUFFERED_EVENTS = 1024;
17
+ const STREAM_TERMINAL_EVENT_TYPES = new Set(['run.finished', 'agent.finished']);
18
+ /**
19
+ * Relay run events from an in-process run to a stream consumer.
20
+ *
21
+ * The unread events live in a bounded queue: consumed events are removed (no
22
+ * growing cursor over a shared array), and on overflow the oldest non-terminal
23
+ * unread event is dropped and counted, so a slow consumer never silently skips
24
+ * an unread event. Delivery is promise-notified rather than time-polled, so
25
+ * there is no fixed per-event latency or periodic timer.
26
+ */
27
+ export async function* relayRunEvents(run) {
28
+ const queue = [];
29
+ let dropped = 0;
30
+ let liveRunId = 'unknown';
31
+ let done = false;
32
+ let failure;
33
+ let wake;
34
+ const notify = () => {
35
+ const resolve = wake;
36
+ wake = undefined;
37
+ resolve?.();
38
+ };
39
+ const result = run((event) => {
40
+ if ('runId' in event)
41
+ liveRunId = event.runId;
42
+ if (queue.length >= STREAM_MAX_BUFFERED_EVENTS) {
43
+ const dropIndex = queue.findIndex((candidate) => !STREAM_TERMINAL_EVENT_TYPES.has(candidate.type));
44
+ if (dropIndex >= 0) {
45
+ queue.splice(dropIndex, 1);
46
+ dropped += 1;
47
+ }
48
+ }
49
+ queue.push(event);
50
+ notify();
51
+ return Promise.resolve();
52
+ })
53
+ .catch((error) => {
54
+ failure = error;
55
+ return undefined;
56
+ })
57
+ .finally(() => {
58
+ done = true;
59
+ notify();
60
+ });
61
+ try {
62
+ while (true) {
63
+ if (dropped > 0) {
64
+ const droppedCount = dropped;
65
+ dropped = 0;
66
+ yield { type: 'stream.overflow', runId: liveRunId, at: now(), dropped: droppedCount };
67
+ }
68
+ while (queue.length > 0) {
69
+ yield queue.shift();
70
+ // Surface a fresh overflow notice promptly between events.
71
+ if (dropped > 0)
72
+ break;
73
+ }
74
+ if (queue.length === 0 && dropped === 0) {
75
+ if (done) {
76
+ break;
77
+ }
78
+ // No await between the empty check and installing `wake`, so a producer
79
+ // push cannot be lost between them.
80
+ await new Promise((resolve) => {
81
+ wake = resolve;
82
+ });
83
+ }
84
+ }
85
+ }
86
+ finally {
87
+ await result.catch(() => undefined);
88
+ }
89
+ if (failure)
90
+ throw failure;
91
+ }
14
92
  function validateInvokeOptions(opts) {
15
93
  if (opts?.historyWindow !== undefined && opts.historyWindow < 0) {
16
94
  throw new ValidationError('Invoke options are invalid.', { where: 'invoke_options', issues: { historyWindow: opts.historyWindow } });
@@ -30,6 +108,12 @@ function normalizeMessage(message, sessionId) {
30
108
  export function createSessionHarness(definition) {
31
109
  const resolvedSkills = loadSkillsSync(definition.skills);
32
110
  const sessionStates = new Map();
111
+ // In-flight session-state creations, memoized so concurrent first-time callers
112
+ // share one sandbox open (no orphaned sessions) and one SessionState object
113
+ // (so the synchronous busy check/set below serializes runs correctly).
114
+ const sessionStateOpenings = new Map();
115
+ // Stable per-harness-instance worker id used as the default durable lease owner.
116
+ const durableWorkerId = `worker_${ulid()}`;
33
117
  const contentCaptureMode = resolveContentCaptureMode(definition.telemetry);
34
118
  const telemetry = withTelemetryFlavor(definition.telemetryShim ?? createTelemetryShim(), definition.telemetry);
35
119
  const adapterMetrics = createMetrics(telemetry, { 'harness.name': definition.name });
@@ -45,6 +129,7 @@ export function createSessionHarness(definition) {
45
129
  toolTimeoutMs: definition.defaults.toolTimeoutMs ?? 120_000,
46
130
  skillTimeoutMs: definition.defaults.skillTimeoutMs ?? 60_000,
47
131
  modelTimeoutMs: definition.defaults.modelTimeoutMs ?? 300_000,
132
+ maxParallelToolCalls: definition.defaults.maxParallelToolCalls ?? 8,
48
133
  ...(definition.defaults.historyWindow !== undefined ? { historyWindow: definition.defaults.historyWindow } : {})
49
134
  }
50
135
  };
@@ -56,24 +141,36 @@ export function createSessionHarness(definition) {
56
141
  if (existing) {
57
142
  return existing;
58
143
  }
144
+ const createdAt = now();
59
145
  const created = {
60
146
  id: sessionId,
61
- createdAt: now(),
62
- updatedAt: now(),
147
+ createdAt,
148
+ updatedAt: createdAt,
63
149
  runCount: 0
64
150
  };
65
151
  await definition.state.upsertSession(created);
66
152
  return created;
67
153
  }
68
- async function getSessionState(sessionId) {
154
+ function getSessionState(sessionId) {
69
155
  const existing = sessionStates.get(sessionId);
70
156
  if (existing) {
71
- return existing;
157
+ return Promise.resolve(existing);
72
158
  }
73
- const sandboxSession = await definition.sandbox.open({ sessionId, runId: `init_${ulid()}` });
74
- const created = { busy: false, sandboxSession, mountedSkills: new Set() };
75
- sessionStates.set(sessionId, created);
76
- return created;
159
+ const pending = sessionStateOpenings.get(sessionId);
160
+ if (pending) {
161
+ return pending;
162
+ }
163
+ const opening = (async () => {
164
+ const sandboxSession = await definition.sandbox.open({ sessionId, runId: `init_${ulid()}` });
165
+ const created = { busy: false, sandboxSession, mountedSkills: new Set() };
166
+ sessionStates.set(sessionId, created);
167
+ sessionStateOpenings.delete(sessionId);
168
+ return created;
169
+ })();
170
+ // Let a failed open be retried instead of caching the rejection forever.
171
+ opening.catch(() => sessionStateOpenings.delete(sessionId));
172
+ sessionStateOpenings.set(sessionId, opening);
173
+ return opening;
77
174
  }
78
175
  async function appendEvents(runId, events) {
79
176
  try {
@@ -141,6 +238,21 @@ export function createSessionHarness(definition) {
141
238
  function memoryFacade(opts) {
142
239
  return createMemoryFacade(memoryOptions(opts.sessionId, opts.sandboxSession, opts.signal, opts));
143
240
  }
241
+ /**
242
+ * Validates `opts.durable` and returns the executable durable runtime, or
243
+ * `undefined` for an ephemeral run. Throws before any run record is created.
244
+ */
245
+ function resolveDurableRuntime(opts) {
246
+ if (!opts?.durable)
247
+ return undefined;
248
+ if (!DURABLE_RUN_ID_PATTERN.test(opts.durable.runId)) {
249
+ throw new ValidationError('Durable run id is invalid.', { where: 'invoke_options', issues: { 'durable.runId': opts.durable.runId } });
250
+ }
251
+ if (!isExecutableDurableRuntime(definition.runtime)) {
252
+ throw new HarnessConfigError('Durable execution requires an executable .runtime(...) adapter.', { reason: 'durable_runtime_required', path: 'runtime' });
253
+ }
254
+ return definition.runtime;
255
+ }
144
256
  return {
145
257
  inspect() {
146
258
  return definition.inspection;
@@ -202,14 +314,21 @@ export function createSessionHarness(definition) {
202
314
  throw new ValidationError('Session history replacement failed validation.', { where: 'session_history', issues: { message } }, error);
203
315
  }
204
316
  });
205
- await definition.state.clearMessages(sessionId);
206
- if (parsed.length > 0) {
207
- await definition.state.appendMessages(sessionId, parsed);
317
+ if (definition.state.replaceMessages) {
318
+ await definition.state.replaceMessages(sessionId, parsed);
319
+ }
320
+ else {
321
+ // Non-atomic fallback for adapters without atomic replace.
322
+ await definition.state.clearMessages(sessionId);
323
+ if (parsed.length > 0) {
324
+ await definition.state.appendMessages(sessionId, parsed);
325
+ }
208
326
  }
209
327
  },
210
328
  async close() {
211
329
  await definition.state.closeSession(sessionId);
212
330
  sessionStates.delete(sessionId);
331
+ sessionStateOpenings.delete(sessionId);
213
332
  await state.sandboxSession.close();
214
333
  }
215
334
  };
@@ -248,51 +367,13 @@ export function createSessionHarness(definition) {
248
367
  $infer: {}
249
368
  };
250
369
  async function* streamAgentCall(sessionId, agentId, agent, input, opts) {
251
- const buffer = [];
252
- const maxBufferedEvents = 1024;
253
- let dropped = 0;
254
- let done = false;
255
- let failure;
256
- let liveRunId = 'unknown';
257
- const result = runAgentCall(sessionId, agentId, agent, input, opts, (event) => {
258
- if ('runId' in event)
259
- liveRunId = event.runId;
260
- if (buffer.length >= maxBufferedEvents) {
261
- const dropIndex = buffer.findIndex((candidate) => candidate.type !== 'run.finished');
262
- if (dropIndex >= 0) {
263
- buffer.splice(dropIndex, 1);
264
- dropped += 1;
265
- }
266
- }
267
- buffer.push(event);
268
- return Promise.resolve();
269
- }).catch((error) => {
270
- failure = error;
271
- return undefined;
272
- }).finally(() => {
273
- done = true;
274
- });
275
- let cursor = 0;
276
- while (true) {
277
- if (dropped > 0) {
278
- yield { type: 'stream.overflow', runId: liveRunId, at: now(), dropped };
279
- dropped = 0;
280
- }
281
- while (cursor < buffer.length) {
282
- yield buffer[cursor];
283
- cursor += 1;
284
- }
285
- if (done) {
286
- await result.catch(() => undefined);
287
- if (failure)
288
- throw failure;
289
- return;
290
- }
291
- await new Promise((resolve) => setTimeout(resolve, 5));
292
- }
370
+ yield* relayRunEvents((onEvent) => runAgentCall(sessionId, agentId, agent, input, opts, onEvent));
293
371
  }
294
372
  async function runAgentCall(sessionId, agentId, agent, input, opts, onEvent) {
295
373
  validateInvokeOptions(opts);
374
+ if (opts?.durable) {
375
+ throw new ValidationError('Durable execution is only supported for workflow runs.', { where: 'invoke_options', issues: { durable: 'agent_run' } });
376
+ }
296
377
  if (opts?.signal?.aborted) {
297
378
  throw new OperationCancelledError('Run was cancelled before start.', { scope: 'run' });
298
379
  }
@@ -352,7 +433,12 @@ export function createSessionHarness(definition) {
352
433
  input,
353
434
  history: await definition.state.listMessages(sessionId),
354
435
  agent,
355
- models: modelRegistry,
436
+ models: withRunEventModelRegistry(modelRegistry, {
437
+ harnessName: definition.name,
438
+ sessionId,
439
+ runId,
440
+ agentId
441
+ }, emit),
356
442
  skills: resolvedSkills,
357
443
  customTools: definition.tools,
358
444
  mcpRegistry,
@@ -363,6 +449,7 @@ export function createSessionHarness(definition) {
363
449
  maxSteps: definition.defaults.agentMaxIterations ?? 16,
364
450
  signal: runSignal.signal,
365
451
  toolTimeoutMs: definition.defaults.toolTimeoutMs ?? 120_000,
452
+ maxParallelToolCalls: definition.defaults.maxParallelToolCalls ?? 8,
366
453
  logger: definition.logger,
367
454
  telemetry,
368
455
  emitEvent: emit,
@@ -418,51 +505,11 @@ export function createSessionHarness(definition) {
418
505
  }
419
506
  }
420
507
  async function* streamWorkflowCall(sessionId, workflowId, workflow, input, opts) {
421
- const buffer = [];
422
- const maxBufferedEvents = 1024;
423
- let dropped = 0;
424
- let done = false;
425
- let failure;
426
- let liveRunId = 'unknown';
427
- const result = runWorkflowCall(sessionId, workflowId, workflow, input, opts, (event) => {
428
- if ('runId' in event)
429
- liveRunId = event.runId;
430
- if (buffer.length >= maxBufferedEvents) {
431
- const dropIndex = buffer.findIndex((candidate) => candidate.type !== 'run.finished');
432
- if (dropIndex >= 0) {
433
- buffer.splice(dropIndex, 1);
434
- dropped += 1;
435
- }
436
- }
437
- buffer.push(event);
438
- return Promise.resolve();
439
- }).catch((error) => {
440
- failure = error;
441
- return undefined;
442
- }).finally(() => {
443
- done = true;
444
- });
445
- let cursor = 0;
446
- while (true) {
447
- if (dropped > 0) {
448
- yield { type: 'stream.overflow', runId: liveRunId, at: now(), dropped };
449
- dropped = 0;
450
- }
451
- while (cursor < buffer.length) {
452
- yield buffer[cursor];
453
- cursor += 1;
454
- }
455
- if (done) {
456
- await result.catch(() => undefined);
457
- if (failure)
458
- throw failure;
459
- return;
460
- }
461
- await new Promise((resolve) => setTimeout(resolve, 5));
462
- }
508
+ yield* relayRunEvents((onEvent) => runWorkflowCall(sessionId, workflowId, workflow, input, opts, onEvent));
463
509
  }
464
510
  async function runWorkflowCall(sessionId, workflowId, workflow, input, opts, onEvent) {
465
511
  validateInvokeOptions(opts);
512
+ const durableRuntime = resolveDurableRuntime(opts);
466
513
  if (opts?.signal?.aborted) {
467
514
  throw new OperationCancelledError('Run was cancelled before start.', { scope: 'run' });
468
515
  }
@@ -473,7 +520,7 @@ export function createSessionHarness(definition) {
473
520
  }
474
521
  state.busy = true;
475
522
  const startedAt = now();
476
- const runId = ulid();
523
+ const runId = opts?.durable ? opts.durable.runId : ulid();
477
524
  const memory = memoryFacade({
478
525
  sessionId,
479
526
  runId,
@@ -503,7 +550,22 @@ export function createSessionHarness(definition) {
503
550
  state.busy = false;
504
551
  throw error;
505
552
  }
553
+ let durableBinding;
506
554
  try {
555
+ if (durableRuntime && opts?.durable) {
556
+ durableBinding = await beginDurableWorkflow({
557
+ runtime: durableRuntime,
558
+ ...(definition.workspaceStore ? { workspaceStore: definition.workspaceStore } : {}),
559
+ durable: opts.durable,
560
+ defaultWorkerId: durableWorkerId,
561
+ sessionId,
562
+ workflowId,
563
+ input: input,
564
+ signal: runSignal.signal,
565
+ logger: definition.logger,
566
+ harnessName: definition.name
567
+ });
568
+ }
507
569
  const result = await withIncomingTraceContext(telemetry, opts, definition.logger, async () => telemetry.span('harness.session.prompt', {
508
570
  'harness.name': definition.name,
509
571
  'harness.session.id': sessionId,
@@ -528,10 +590,16 @@ export function createSessionHarness(definition) {
528
590
  signal: runSignal.signal,
529
591
  runId,
530
592
  sessionId,
531
- models: modelRegistry,
593
+ models: withRunEventModelRegistry(modelRegistry, {
594
+ harnessName: definition.name,
595
+ sessionId,
596
+ runId,
597
+ workflowId
598
+ }, emit),
532
599
  metadata: opts?.metadata ?? {},
533
600
  metrics: workflowMetrics,
534
601
  memory,
602
+ step: durableBinding ? durableBinding.step : passthroughStep,
535
603
  agents: Object.fromEntries(Object.entries(definition.agents).map(([agentId, agent]) => [
536
604
  agentId,
537
605
  async (agentInput, agentOpts) => {
@@ -557,7 +625,13 @@ export function createSessionHarness(definition) {
557
625
  input: agentInput,
558
626
  history: await definition.state.listMessages(sessionId),
559
627
  agent: agent,
560
- models: modelRegistry,
628
+ models: withRunEventModelRegistry(modelRegistry, {
629
+ harnessName: definition.name,
630
+ sessionId,
631
+ runId,
632
+ workflowId,
633
+ agentId
634
+ }, emit),
561
635
  skills: resolvedSkills,
562
636
  customTools: definition.tools,
563
637
  mcpRegistry,
@@ -568,6 +642,7 @@ export function createSessionHarness(definition) {
568
642
  maxSteps: definition.defaults.agentMaxIterations ?? 16,
569
643
  signal: agentSignal.signal,
570
644
  toolTimeoutMs: definition.defaults.toolTimeoutMs ?? 120_000,
645
+ maxParallelToolCalls: definition.defaults.maxParallelToolCalls ?? 8,
571
646
  logger: definition.logger,
572
647
  telemetry,
573
648
  emitEvent: emit,
@@ -597,6 +672,9 @@ export function createSessionHarness(definition) {
597
672
  }));
598
673
  }));
599
674
  const finishedAt = now();
675
+ if (durableBinding) {
676
+ await guardDurableStep({ sessionId, runId, workflowId, operation: 'finish_success' }, () => durableBinding.finishSuccess(result));
677
+ }
600
678
  const runFinished = { type: 'run.finished', runId, at: finishedAt, output: result };
601
679
  await emit(runFinished);
602
680
  await definition.state.finishRun(runId, { status: 'succeeded', finishedAt, output: result });
@@ -608,6 +686,9 @@ export function createSessionHarness(definition) {
608
686
  const finalError = normalizeRunError(error, runSignal.signal);
609
687
  const finishedAt = now();
610
688
  const serialized = serializeError(finalError);
689
+ if (durableBinding && finalError instanceof OperationCancelledError) {
690
+ await guardDurableStep({ sessionId, runId, workflowId, operation: 'finish_cancelled' }, () => durableBinding.finishCancelled(finalError));
691
+ }
611
692
  const log = finalError instanceof OperationCancelledError ? definition.logger.warn.bind(definition.logger) : definition.logger.error.bind(definition.logger);
612
693
  log('Harness workflow run failed.', {
613
694
  harness: definition.name,
@@ -637,10 +718,41 @@ export function createSessionHarness(definition) {
637
718
  throw finalError;
638
719
  }
639
720
  finally {
721
+ // Releases the lease for a non-cancel failure so a retry with the same run
722
+ // id can resume; a no-op once the run was settled (success/cancel).
723
+ if (durableBinding)
724
+ await durableBinding.dispose();
640
725
  runSignal.cleanup();
641
726
  state.busy = false;
642
727
  }
643
728
  }
729
+ /** Pass-through step used when a workflow runs without durable execution. */
730
+ function passthroughStep(_stepId, fn) {
731
+ return fn();
732
+ }
733
+ /**
734
+ * Runs a durable finalization side effect (runtime finish / workspace lifecycle)
735
+ * without ever masking the primary run outcome (spec 21 §16.1 step 7).
736
+ */
737
+ async function guardDurableStep(args, step) {
738
+ try {
739
+ await step();
740
+ }
741
+ catch (error) {
742
+ telemetry.recordCounter('harness.runs.durable_errors', 1, {
743
+ harness: definition.name,
744
+ 'harness.run.durable.operation': args.operation
745
+ });
746
+ definition.logger.error('Durable finalization step failed; preserving run outcome.', {
747
+ harness: definition.name,
748
+ session_id: args.sessionId,
749
+ run_id: args.runId,
750
+ workflow_id: args.workflowId,
751
+ operation: args.operation,
752
+ error: serializeError(error)
753
+ });
754
+ }
755
+ }
644
756
  async function terminalizeFailedRun(args) {
645
757
  await runFailureTerminalizationStep(args, 'emit_run_finished', args.emitRunFinished);
646
758
  await runFailureTerminalizationStep(args, 'finish_run', args.finishRun);
@@ -668,6 +780,100 @@ export function createSessionHarness(definition) {
668
780
  }
669
781
  }
670
782
  }
783
+ function withRunEventModelRegistry(models, context, emitEvent) {
784
+ return Object.fromEntries(Object.entries(models).map(([alias, handle]) => [alias, withRunEventModelHandle(alias, handle, context, emitEvent)]));
785
+ }
786
+ function withRunEventModelHandle(alias, handle, context, emitEvent) {
787
+ if (!handle || typeof handle !== 'object')
788
+ return handle;
789
+ const source = handle;
790
+ const wrapped = { ...source };
791
+ for (const method of ['text', 'object', 'embed', 'rerank']) {
792
+ const fn = source[method];
793
+ if (typeof fn !== 'function')
794
+ continue;
795
+ wrapped[method] = (req, signal, ctx) => fn.call(source, req, signal, mergeModelRunContext(context, ctx));
796
+ }
797
+ const textStream = source['textStream'];
798
+ if (typeof textStream === 'function') {
799
+ wrapped['textStream'] = (req, signal, ctx) => {
800
+ const streamContext = modelStreamRunContext(context, ctx, alias);
801
+ return emitTextStreamRunEvents(textStream.call(source, req, signal, streamContext), streamContext, emitEvent);
802
+ };
803
+ }
804
+ const objectStream = source['objectStream'];
805
+ if (typeof objectStream === 'function') {
806
+ wrapped['objectStream'] = (req, signal, ctx) => {
807
+ const streamContext = modelStreamRunContext(context, ctx, alias);
808
+ return emitObjectStreamRunEvents(objectStream.call(source, req, signal, streamContext), streamContext, emitEvent);
809
+ };
810
+ }
811
+ return wrapped;
812
+ }
813
+ function mergeModelRunContext(context, override) {
814
+ return { ...context, ...(override ?? {}) };
815
+ }
816
+ function modelStreamRunContext(context, override, alias) {
817
+ const merged = mergeModelRunContext(context, override);
818
+ return {
819
+ ...merged,
820
+ modelAlias: alias,
821
+ ...(merged.emitRunEvents === true ? { streamId: `model_${ulid()}` } : {})
822
+ };
823
+ }
824
+ async function* emitTextStreamRunEvents(stream, context, emitEvent) {
825
+ for await (const chunk of stream) {
826
+ if (context.emitRunEvents === true && isTextDeltaChunk(chunk)) {
827
+ await emitEvent({
828
+ type: 'model.delta',
829
+ runId: context.runId,
830
+ ...(context.agentId ? { agentId: context.agentId } : {}),
831
+ ...(context.workflowId ? { workflowId: context.workflowId } : {}),
832
+ ...(context.modelAlias ? { modelAlias: context.modelAlias } : {}),
833
+ streamId: context.streamId,
834
+ delta: chunk.text
835
+ });
836
+ }
837
+ yield chunk;
838
+ }
839
+ }
840
+ async function* emitObjectStreamRunEvents(stream, context, emitEvent) {
841
+ for await (const chunk of stream) {
842
+ if (context.emitRunEvents === true && isObjectPartialChunk(chunk)) {
843
+ await emitEvent({
844
+ type: 'model.object.partial',
845
+ runId: context.runId,
846
+ ...(context.agentId ? { agentId: context.agentId } : {}),
847
+ ...(context.workflowId ? { workflowId: context.workflowId } : {}),
848
+ ...(context.modelAlias ? { modelAlias: context.modelAlias } : {}),
849
+ streamId: context.streamId,
850
+ partial: chunk.partial
851
+ });
852
+ }
853
+ else if (context.emitRunEvents === true && isObjectFinishChunk(chunk)) {
854
+ await emitEvent({
855
+ type: 'model.object',
856
+ runId: context.runId,
857
+ ...(context.agentId ? { agentId: context.agentId } : {}),
858
+ ...(context.workflowId ? { workflowId: context.workflowId } : {}),
859
+ ...(context.modelAlias ? { modelAlias: context.modelAlias } : {}),
860
+ ...(context.streamId ? { streamId: context.streamId } : {}),
861
+ object: chunk.object,
862
+ ...(chunk.usage ? { usage: chunk.usage } : {})
863
+ });
864
+ }
865
+ yield chunk;
866
+ }
867
+ }
868
+ function isTextDeltaChunk(chunk) {
869
+ return Boolean(chunk && typeof chunk === 'object' && chunk.kind === 'delta' && typeof chunk.text === 'string');
870
+ }
871
+ function isObjectPartialChunk(chunk) {
872
+ return Boolean(chunk && typeof chunk === 'object' && chunk.kind === 'partial');
873
+ }
874
+ function isObjectFinishChunk(chunk) {
875
+ return Boolean(chunk && typeof chunk === 'object' && chunk.kind === 'finish' && Object.prototype.hasOwnProperty.call(chunk, 'object'));
876
+ }
671
877
  function configureHarnessAdapters(context, models, state, sandbox, memory, tools) {
672
878
  const seen = new Set();
673
879
  for (const alias of Object.values(models)) {
@@ -844,12 +1050,12 @@ function sanitizeEventForPersistence(event) {
844
1050
  case 'model.message':
845
1051
  return { agentId: event.agentId, message: '[redacted]' };
846
1052
  case 'model.delta':
847
- return { agentId: event.agentId, delta: '[redacted]' };
1053
+ return { ...modelStreamEventMeta(event), delta: '[redacted]' };
848
1054
  case 'model.object.partial':
849
- return { ...(event.agentId ? { agentId: event.agentId } : {}), partial: '[redacted]' };
1055
+ return { ...modelStreamEventMeta(event), partial: '[redacted]' };
850
1056
  case 'model.object':
851
1057
  return {
852
- ...(event.agentId ? { agentId: event.agentId } : {}),
1058
+ ...modelStreamEventMeta(event),
853
1059
  object: '[redacted]',
854
1060
  ...(event.usage ? { usage: event.usage } : {})
855
1061
  };
@@ -869,8 +1075,22 @@ function sanitizeEventForPersistence(event) {
869
1075
  };
870
1076
  case 'stream.overflow':
871
1077
  return { dropped: event.dropped };
1078
+ default: {
1079
+ // Exhaustiveness guard: adding a RunEvent variant without updating this
1080
+ // sanitizer becomes a compile error instead of silently persisting undefined.
1081
+ event;
1082
+ return {};
1083
+ }
872
1084
  }
873
1085
  }
1086
+ function modelStreamEventMeta(event) {
1087
+ return {
1088
+ ...(event.agentId ? { agentId: event.agentId } : {}),
1089
+ ...(event.workflowId ? { workflowId: event.workflowId } : {}),
1090
+ ...(event.modelAlias ? { modelAlias: event.modelAlias } : {}),
1091
+ ...(event.streamId ? { streamId: event.streamId } : {})
1092
+ };
1093
+ }
874
1094
  function isJsonRecord(value) {
875
1095
  return value !== null && typeof value === 'object' && !Array.isArray(value);
876
1096
  }
@@ -891,9 +1111,11 @@ function normalizeSerializedRunError(error) {
891
1111
  }
892
1112
  function createRunSignal(parent, timeoutMs) {
893
1113
  const controller = new AbortController();
894
- const relay = () => controller.abort(parent?.reason);
1114
+ const relay = () => controller.abort(runAbortReason(parent?.reason));
895
1115
  if (parent)
896
1116
  parent.addEventListener('abort', relay, { once: true });
1117
+ if (parent?.aborted)
1118
+ relay();
897
1119
  const timeout = timeoutMs && timeoutMs > 0
898
1120
  ? setTimeout(() => controller.abort(new OperationTimeoutError('Run timed out.', { scope: 'run', timeout_ms: timeoutMs })), timeoutMs)
899
1121
  : undefined;
@@ -911,10 +1133,14 @@ function combineSignals(primary, secondary) {
911
1133
  if (!secondary)
912
1134
  return { signal: primary, cleanup: () => undefined };
913
1135
  const controller = new AbortController();
914
- const relayPrimary = () => controller.abort(primary.reason);
915
- const relaySecondary = () => controller.abort(secondary.reason);
1136
+ const relayPrimary = () => controller.abort(runAbortReason(primary.reason));
1137
+ const relaySecondary = () => controller.abort(runAbortReason(secondary.reason));
916
1138
  primary.addEventListener('abort', relayPrimary, { once: true });
917
1139
  secondary.addEventListener('abort', relaySecondary, { once: true });
1140
+ if (primary.aborted)
1141
+ relayPrimary();
1142
+ else if (secondary.aborted)
1143
+ relaySecondary();
918
1144
  return {
919
1145
  signal: controller.signal,
920
1146
  cleanup: () => {
@@ -923,3 +1149,8 @@ function combineSignals(primary, secondary) {
923
1149
  }
924
1150
  };
925
1151
  }
1152
+ function runAbortReason(reason) {
1153
+ if (reason instanceof OperationCancelledError || reason instanceof OperationTimeoutError)
1154
+ return reason;
1155
+ return new OperationCancelledError('Run was cancelled.', { scope: 'run' }, reason);
1156
+ }