@inixiative/foundry-core 0.2.0 → 0.3.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.
package/src/harness.ts CHANGED
@@ -1,18 +1,15 @@
1
- import { Thread, type FanResult, type BackgroundHandle } from "./thread";
2
- import type { ExecutionResult } from "./base-agent";
3
- import type { Decision } from "./decider";
4
- import type { Classification } from "./classifier";
5
- import type { Route } from "./router";
6
- import type { LayerFilter } from "./context-stack";
7
- import type { ContextLayer } from "./context-layer";
8
- import { Trace } from "./trace";
9
- import { BoundedSet } from "./bounded-set";
10
- import { newId } from "./id";
11
- import type {
12
- InvocationCondition,
13
- AgentModeConfig,
14
- LayerModeConfig,
15
- } from "./types";
1
+ import type { ExecutionResult } from './base-agent';
2
+ import { BoundedSet } from './bounded-set';
3
+ import type { ClarificationResult, ClarifyPayload } from './clarifier';
4
+ import type { Classification } from './classifier';
5
+ import type { ContextLayer } from './context-layer';
6
+ import type { LayerFilter } from './context-stack';
7
+ import type { Decision } from './decider';
8
+ import { newId } from './id';
9
+ import type { Route } from './router';
10
+ import type { BackgroundHandle, FanResult, Thread } from './thread';
11
+ import { Trace } from './trace';
12
+ import type { AgentModeConfig, InvocationCondition, LayerModeConfig } from './types';
16
13
 
17
14
  export interface Message<T = unknown> {
18
15
  readonly id: string;
@@ -24,6 +21,8 @@ export interface HarnessResult<T = unknown> {
24
21
  readonly messageId: string;
25
22
  readonly classification?: Decision<Classification>;
26
23
  readonly route?: Decision<Route>;
24
+ /** Set when a clarify stage ran; when `needed`, execution was skipped. */
25
+ readonly clarification?: ClarificationResult;
27
26
  readonly result: ExecutionResult<T>;
28
27
  readonly trace: Trace;
29
28
  readonly timestamp: number;
@@ -55,16 +54,16 @@ export interface PipelineStep<TIn = unknown, TOut = unknown> {
55
54
 
56
55
  export interface FlowStage {
57
56
  /** Agent ID for this stage. Use "routed" to use route.destination. */
58
- agentId: string | "routed";
57
+ agentId: string | 'routed';
59
58
  /** Pipeline role (for tracing and UI). */
60
- role: "classify" | "route" | "execute" | "enrich" | "guard" | "observe";
59
+ role: 'classify' | 'route' | 'clarify' | 'execute' | 'enrich' | 'guard' | 'observe';
61
60
  /**
62
61
  * When this stage runs:
63
62
  * - "always": every request
64
63
  * - "on-demand": only when explicitly requested by middleware or prior stage
65
64
  * - "conditional": when condition matches classification/route context
66
65
  */
67
- invocation: "always" | "on-demand" | "conditional";
66
+ invocation: 'always' | 'on-demand' | 'conditional';
68
67
  /** Condition for conditional invocation. */
69
68
  condition?: InvocationCondition;
70
69
  /**
@@ -134,7 +133,7 @@ export function matchesCondition(
134
133
  // ---------------------------------------------------------------------------
135
134
 
136
135
  interface SendOptions {
137
- nativeObservation?: import("./thread").DispatchOptions["nativeObservation"];
136
+ nativeObservation?: import('./thread').DispatchOptions['nativeObservation'];
138
137
  onDelta?: (text: string) => void;
139
138
  /** Final trace for this attempt, independent of the bounded history cache. */
140
139
  onTrace?: (trace: Trace) => void;
@@ -167,8 +166,10 @@ export class Harness {
167
166
  private _explicitFlow: FlowConfig | null = null;
168
167
 
169
168
  // Agent/layer invocation metadata (from config)
170
- private _agentModes: Map<string, { invocation: string; condition?: InvocationCondition }> = new Map();
171
- private _layerModes: Map<string, { activation: string; condition?: InvocationCondition }> = new Map();
169
+ private _agentModes: Map<string, { invocation: string; condition?: InvocationCondition }> =
170
+ new Map();
171
+ private _layerModes: Map<string, { activation: string; condition?: InvocationCondition }> =
172
+ new Map();
172
173
 
173
174
  private _pipeline: PipelineStep[] = [];
174
175
  private _traces: Trace[] = [];
@@ -184,9 +185,8 @@ export class Harness {
184
185
  this.thread = thread;
185
186
  this._maxTraces = opts?.maxTraces ?? 1000;
186
187
 
187
- this._dedupCapacity = opts?.dedup === true ? 1000
188
- : typeof opts?.dedup === "number" ? opts.dedup
189
- : 0;
188
+ this._dedupCapacity =
189
+ opts?.dedup === true ? 1000 : typeof opts?.dedup === 'number' ? opts.dedup : 0;
190
190
  this._seenMessages = new BoundedSet<string>(Math.max(this._dedupCapacity, 1));
191
191
  }
192
192
 
@@ -226,10 +226,10 @@ export class Harness {
226
226
  layers: Record<string, LayerModeConfig>,
227
227
  ): void {
228
228
  for (const [id, cfg] of Object.entries(agents)) {
229
- this.setAgentMode(id, cfg.invocation ?? "always", cfg.condition);
229
+ this.setAgentMode(id, cfg.invocation ?? 'always', cfg.condition);
230
230
  }
231
231
  for (const [id, cfg] of Object.entries(layers)) {
232
- this.setLayerMode(id, cfg.activation ?? "always", cfg.condition);
232
+ this.setLayerMode(id, cfg.activation ?? 'always', cfg.condition);
233
233
  }
234
234
  }
235
235
 
@@ -274,10 +274,7 @@ export class Harness {
274
274
 
275
275
  // -- Entry point --
276
276
 
277
- async send<T>(
278
- message: Message<T>,
279
- opts?: SendOptions,
280
- ): Promise<HarnessResult> {
277
+ async send<T>(message: Message<T>, opts?: SendOptions): Promise<HarnessResult> {
281
278
  // Idempotency: return cached result for duplicate messages
282
279
  if (this._dedupCapacity > 0 && this._seenMessages.has(message.id)) {
283
280
  const cached = this._resultCache.get(message.id);
@@ -311,11 +308,8 @@ export class Harness {
311
308
  */
312
309
  async *sendStream<T>(
313
310
  message: Message<T>,
314
- opts?: Pick<SendOptions, "onTrace" | "nativeObservation">,
315
- ): AsyncGenerator<
316
- | { kind: "delta"; text: string }
317
- | { kind: "done"; result: HarnessResult }
318
- > {
311
+ opts?: Pick<SendOptions, 'onTrace' | 'nativeObservation'>,
312
+ ): AsyncGenerator<{ kind: 'delta'; text: string } | { kind: 'done'; result: HarnessResult }> {
319
313
  // Bounded buffer so deltas emitted while we're yielding don't get lost.
320
314
  const pending: string[] = [];
321
315
  let resolveWaiter: (() => void) | null = null;
@@ -335,25 +329,36 @@ export class Harness {
335
329
  let failed = false;
336
330
 
337
331
  const runPromise = this.send(message, { ...opts, onDelta })
338
- .then((r) => { finalResult = r; })
339
- .catch((e) => { failed = true; thrown = e; })
332
+ .then((r) => {
333
+ finalResult = r;
334
+ })
335
+ .catch((e) => {
336
+ failed = true;
337
+ thrown = e;
338
+ })
340
339
  .finally(() => {
341
340
  done = true;
342
- if (resolveWaiter) { const r = resolveWaiter; resolveWaiter = null; r(); }
341
+ if (resolveWaiter) {
342
+ const r = resolveWaiter;
343
+ resolveWaiter = null;
344
+ r();
345
+ }
343
346
  });
344
347
 
345
348
  while (!done || pending.length > 0) {
346
349
  if (pending.length > 0) {
347
- yield { kind: "delta", text: pending.shift()! };
350
+ yield { kind: 'delta', text: pending.shift()! };
348
351
  continue;
349
352
  }
350
353
  if (done) break;
351
- await new Promise<void>((r) => { resolveWaiter = r; });
354
+ await new Promise<void>((r) => {
355
+ resolveWaiter = r;
356
+ });
352
357
  }
353
358
 
354
359
  await runPromise;
355
360
  if (failed) throw thrown;
356
- if (finalResult) yield { kind: "done", result: finalResult };
361
+ if (finalResult) yield { kind: 'done', result: finalResult };
357
362
  }
358
363
 
359
364
  /**
@@ -368,22 +373,20 @@ export class Harness {
368
373
  route?: Decision<Route>,
369
374
  requestedLayers?: ReadonlySet<string>,
370
375
  ): LayerFilter {
371
- const routeSlice = route?.value?.contextSlice
372
- ? new Set(route.value.contextSlice)
373
- : null;
376
+ const routeSlice = route?.value?.contextSlice ? new Set(route.value.contextSlice) : null;
374
377
 
375
378
  return (layer: ContextLayer) => {
376
379
  const mode = this._layerModes.get(layer.id);
377
- const activation = mode?.activation ?? "always";
380
+ const activation = mode?.activation ?? 'always';
378
381
 
379
382
  switch (activation) {
380
- case "always":
383
+ case 'always':
381
384
  return true;
382
385
 
383
- case "conditional":
386
+ case 'conditional':
384
387
  return matchesCondition(mode?.condition, classification, route);
385
388
 
386
- case "on-demand":
389
+ case 'on-demand':
387
390
  // Included if router requested it via contextSlice, or middleware requested it
388
391
  if (routeSlice?.has(layer.id)) return true;
389
392
  if (requestedLayers?.has(layer.id)) return true;
@@ -411,8 +414,8 @@ export class Harness {
411
414
  if (this._classifierId) {
412
415
  stages.push({
413
416
  agentId: this._classifierId,
414
- role: "classify",
415
- invocation: "always",
417
+ role: 'classify',
418
+ invocation: 'always',
416
419
  });
417
420
  }
418
421
 
@@ -420,8 +423,8 @@ export class Harness {
420
423
  if (this._routerId) {
421
424
  stages.push({
422
425
  agentId: this._routerId,
423
- role: "route",
424
- invocation: "always",
426
+ role: 'route',
427
+ invocation: 'always',
425
428
  });
426
429
  }
427
430
 
@@ -432,27 +435,27 @@ export class Harness {
432
435
  if (agentId === this._routerId) continue;
433
436
  if (agentId === this._defaultExecutorId) continue;
434
437
 
435
- if (mode.invocation === "conditional") {
438
+ if (mode.invocation === 'conditional') {
436
439
  stages.push({
437
440
  agentId,
438
- role: "enrich",
439
- invocation: "conditional",
441
+ role: 'enrich',
442
+ invocation: 'conditional',
440
443
  condition: mode.condition,
441
444
  });
442
- } else if (mode.invocation === "on-demand") {
445
+ } else if (mode.invocation === 'on-demand') {
443
446
  stages.push({
444
447
  agentId,
445
- role: "enrich",
446
- invocation: "on-demand",
448
+ role: 'enrich',
449
+ invocation: 'on-demand',
447
450
  });
448
451
  }
449
452
  }
450
453
 
451
454
  // Execute stage (uses routed destination or default)
452
455
  stages.push({
453
- agentId: "routed",
454
- role: "execute",
455
- invocation: "always",
456
+ agentId: 'routed',
457
+ role: 'execute',
458
+ invocation: 'always',
456
459
  });
457
460
 
458
461
  return {
@@ -468,60 +471,125 @@ export class Harness {
468
471
  flow: FlowConfig,
469
472
  opts?: SendOptions,
470
473
  ): Promise<HarnessResult> {
471
- const trace = new Trace(message.id, { input: structuredClone(message.payload), threadId: this.thread.id });
474
+ const trace = new Trace(message.id, {
475
+ input: structuredClone(message.payload),
476
+ threadId: this.thread.id,
477
+ });
472
478
  const invokedAgents: Array<{ id: string; mode: string }> = [];
473
479
 
474
480
  const requestedAgents = new Set<string>();
475
481
  const requestedLayers = new Set<string>();
476
482
  const stageResults = new Map<string, ExecutionResult>();
477
- const data = new Map<string, unknown>();
478
483
 
479
484
  let classification: Decision<Classification> | undefined;
480
485
  let route: Decision<Route> | undefined;
486
+ let clarification: ClarificationResult | undefined;
481
487
  let lastExecuteAgentId: string | undefined;
482
488
 
483
- // Build request context for middleware
484
- const reqCtx: RequestContext = {
485
- get classification() { return classification; },
486
- get route() { return route; },
487
- requestAgent: (id) => requestedAgents.add(id),
488
- requestLayer: (id) => requestedLayers.add(id),
489
- get requestedAgents() { return requestedAgents; },
490
- get requestedLayers() { return requestedLayers; },
491
- get stageResults() { return stageResults; },
492
- data,
493
- };
494
-
495
489
  try {
496
- // Run each stage in order
490
+ // Decision stages (classify, route) read the same frozen input concurrently: neither waits for
491
+ // the other, and the router receives the message, not the classification. Their results are
492
+ // applied in configured stage order, so the outcome does not depend on which answered first.
493
+ const decisionStages = flow.stages.filter(
494
+ (stage) =>
495
+ !stage.background &&
496
+ stage.invocation === 'always' &&
497
+ (stage.role === 'classify' || stage.role === 'route') &&
498
+ stage.agentId !== 'routed' &&
499
+ this.thread.getAgent(stage.agentId),
500
+ );
501
+ if (decisionStages.length) {
502
+ const layerFilter = this.buildLayerFilter(undefined, undefined, requestedLayers);
503
+ const settled = await Promise.allSettled(
504
+ decisionStages.map(async (stage) => {
505
+ const span = trace.startDetached(`${stage.role}:${stage.agentId}`, stage.role as any, {
506
+ agentId: stage.agentId,
507
+ threadId: this.thread.id,
508
+ input: message.payload,
509
+ annotations: { invocation: stage.invocation },
510
+ });
511
+ try {
512
+ const result = await this.thread.dispatch(
513
+ stage.agentId,
514
+ message.payload,
515
+ layerFilter,
516
+ {
517
+ role: stage.role,
518
+ messageId: message.id,
519
+ invocation: stage.invocation,
520
+ recordInjection: (artifact) => {
521
+ span.annotations.injection = artifact;
522
+ },
523
+ nativeObservation: opts?.nativeObservation,
524
+ recordNative: (evidence) => {
525
+ span.annotations.native = evidence;
526
+ },
527
+ recordCompleted: (output) => {
528
+ span.annotations.executorCompletion = { output };
529
+ },
530
+ },
531
+ );
532
+ if (result.meta?.injection) span.annotations.injection = result.meta.injection;
533
+ trace.endSpan(span, result.output);
534
+ return result;
535
+ } catch (error) {
536
+ trace.endSpan(span, undefined, error);
537
+ throw error;
538
+ }
539
+ }),
540
+ );
541
+ for (const [index, stage] of decisionStages.entries()) {
542
+ const outcome = settled[index]!;
543
+ if (outcome.status === 'rejected') throw outcome.reason;
544
+ invokedAgents.push({ id: stage.agentId, mode: stage.invocation });
545
+ stageResults.set(stage.agentId, outcome.value);
546
+ if (stage.role === 'classify') {
547
+ classification = outcome.value.output as Decision<Classification>;
548
+ if (classification?.value) {
549
+ await this.thread.signals.emit({
550
+ id: newId('sig-classify'),
551
+ kind: 'classification',
552
+ source: `harness:${stage.agentId}`,
553
+ content: classification.value,
554
+ confidence: classification.confidence,
555
+ timestamp: Date.now(),
556
+ });
557
+ }
558
+ } else route = outcome.value.output as Decision<Route>;
559
+ }
560
+ }
561
+ const concurrent = new Set(decisionStages);
562
+
563
+ // Run the remaining stages in order
497
564
  for (const stage of flow.stages) {
565
+ if (concurrent.has(stage)) continue;
498
566
  // Should this stage run?
499
- if (stage.invocation === "on-demand") {
500
- const resolvedId = stage.agentId === "routed"
501
- ? route?.value?.destination
502
- : stage.agentId;
567
+ if (stage.invocation === 'on-demand') {
568
+ const resolvedId = stage.agentId === 'routed' ? route?.value?.destination : stage.agentId;
503
569
  if (!resolvedId || !requestedAgents.has(resolvedId)) continue;
504
570
  }
505
571
 
506
- if (stage.invocation === "conditional") {
572
+ if (stage.invocation === 'conditional') {
507
573
  if (!matchesCondition(stage.condition, classification, route)) continue;
508
574
  }
509
575
 
510
576
  // Resolve agent ID
511
- const agentId = stage.agentId === "routed"
512
- ? (route?.value?.destination ?? flow.defaultExecutor)
513
- : stage.agentId;
577
+ const agentId =
578
+ stage.agentId === 'routed'
579
+ ? (route?.value?.destination ?? flow.defaultExecutor)
580
+ : stage.agentId;
514
581
 
515
582
  if (!agentId) {
516
- if (stage.role === "execute") {
583
+ if (stage.role === 'execute') {
517
584
  throw new Error(
518
- "No target agent: router returned no destination and no default executor is configured",
585
+ 'No target agent: router returned no destination and no default executor is configured',
519
586
  );
520
587
  }
521
588
  continue;
522
589
  }
523
590
  if (!this.thread.getAgent(agentId)) {
524
- if (stage.role === "execute") throw new Error(`Execution target "${agentId}" is not registered`);
591
+ if (stage.role === 'execute')
592
+ throw new Error(`Execution target "${agentId}" is not registered`);
525
593
  continue;
526
594
  }
527
595
 
@@ -533,12 +601,17 @@ export class Harness {
533
601
  const bgStart = performance.now();
534
602
  const msgId = message.id;
535
603
  const bgStage = stage;
536
- const { promise } = this.thread.dispatchBackground(agentId, message.payload, layerFilter, {
537
- role: stage.role,
538
- messageId: message.id,
539
- invocation: "background",
540
- });
541
- invokedAgents.push({ id: agentId, mode: "background" });
604
+ const { promise } = this.thread.dispatchBackground(
605
+ agentId,
606
+ message.payload,
607
+ layerFilter,
608
+ {
609
+ role: stage.role,
610
+ messageId: message.id,
611
+ invocation: 'background',
612
+ },
613
+ );
614
+ invokedAgents.push({ id: agentId, mode: 'background' });
542
615
 
543
616
  // Wire callback — runs when the background dispatch completes
544
617
  promise.then((result) => {
@@ -562,69 +635,90 @@ export class Harness {
562
635
  annotations: { invocation: stage.invocation },
563
636
  });
564
637
 
565
- // Route stage receives classification context; others get raw payload
566
- const stagePayload = stage.role === "route" && classification
567
- ? { payload: message.payload, classification: classification.value }
568
- : message.payload;
638
+ // A conditional route stage runs after classification and receives it; clarify stages also
639
+ // receive the classification; others get raw payload
640
+ const stagePayload =
641
+ stage.role === 'route' && classification
642
+ ? { payload: message.payload, classification: classification.value }
643
+ : stage.role === 'clarify'
644
+ ? ({
645
+ message:
646
+ typeof message.payload === 'string'
647
+ ? message.payload
648
+ : JSON.stringify(message.payload),
649
+ classification: classification?.value ?? { category: 'unknown' },
650
+ } satisfies ClarifyPayload)
651
+ : message.payload;
569
652
 
570
653
  // The thread observes this dispatch once at its boundary; the stage
571
654
  // only contributes correlation. Only the execute stage streams deltas:
572
655
  // classifier/router/enricher output isn't user-visible content.
573
656
  const result = await this.thread.dispatch(agentId, stagePayload, layerFilter, {
574
- onDelta: stage.role === "execute" ? opts?.onDelta : undefined,
657
+ onDelta: stage.role === 'execute' ? opts?.onDelta : undefined,
575
658
  role: stage.role,
576
659
  messageId: message.id,
577
660
  invocation: stage.invocation,
578
- recordInjection: artifact => { stageSpan.annotations.injection = artifact; },
661
+ recordInjection: (artifact) => {
662
+ stageSpan.annotations.injection = artifact;
663
+ },
579
664
  nativeObservation: opts?.nativeObservation,
580
- recordNative: evidence => { stageSpan.annotations.native = evidence; },
581
- recordCompleted: output => { stageSpan.annotations.executorCompletion = { output }; },
665
+ recordNative: (evidence) => {
666
+ stageSpan.annotations.native = evidence;
667
+ },
668
+ recordCompleted: (output) => {
669
+ stageSpan.annotations.executorCompletion = { output };
670
+ },
582
671
  });
583
672
  invokedAgents.push({ id: agentId, mode: stage.invocation });
584
673
  stageResults.set(agentId, result);
585
674
 
586
675
  // Track the last execute-role agent for final result extraction
587
- if (stage.role === "execute") {
676
+ if (stage.role === 'execute') {
588
677
  lastExecuteAgentId = agentId;
589
678
  }
590
679
 
591
680
  // Capture classification/route from appropriate stages
592
- if (stage.role === "classify") {
681
+ if (stage.role === 'classify') {
593
682
  classification = result.output as Decision<Classification>;
594
683
  if (classification?.value) {
595
684
  await this.thread.signals.emit({
596
- id: newId("sig-classify"),
597
- kind: "classification",
685
+ id: newId('sig-classify'),
686
+ kind: 'classification',
598
687
  source: `harness:${agentId}`,
599
688
  content: classification.value,
600
689
  confidence: classification.confidence,
601
690
  timestamp: Date.now(),
602
691
  });
603
692
  }
604
- } else if (stage.role === "route") {
693
+ } else if (stage.role === 'route') {
605
694
  route = result.output as Decision<Route>;
695
+ } else if (stage.role === 'clarify') {
696
+ clarification = (result.output as Decision<ClarificationResult>).value;
606
697
  }
607
698
 
608
699
  if (trace.current && result.meta?.injection) {
609
700
  trace.current.annotations.injection = result.meta.injection;
610
701
  }
611
702
  trace.end(result.output);
703
+
704
+ // Circuit breaker: an underspecified request returns questions instead of executing
705
+ if (clarification?.needed) break;
612
706
  }
613
707
 
614
708
  // Run any remaining on-demand agents requested by middleware
615
- for (const agentId of requestedAgents) {
709
+ for (const agentId of clarification?.needed ? [] : requestedAgents) {
616
710
  const alreadyRan = invokedAgents.some((a) => a.id === agentId);
617
711
  if (alreadyRan) continue;
618
712
  if (!this.thread.getAgent(agentId)) continue;
619
713
 
620
714
  const layerFilter = this.buildLayerFilter(classification, route, requestedLayers);
621
- trace.start(`on-demand:${agentId}`, "enrich", { agentId });
715
+ trace.start(`on-demand:${agentId}`, 'enrich', { agentId });
622
716
  const result = await this.thread.dispatch(agentId, message.payload, layerFilter, {
623
- role: "enrich",
717
+ role: 'enrich',
624
718
  messageId: message.id,
625
- invocation: "on-demand",
719
+ invocation: 'on-demand',
626
720
  });
627
- invokedAgents.push({ id: agentId, mode: "on-demand" });
721
+ invokedAgents.push({ id: agentId, mode: 'on-demand' });
628
722
  stageResults.set(agentId, result);
629
723
  trace.end(result.output);
630
724
  }
@@ -633,10 +727,11 @@ export class Harness {
633
727
  this._recordTrace(trace, opts?.onTrace);
634
728
 
635
729
  // Final result: from last execute-role agent, or last invoked agent
636
- const finalAgentId = lastExecuteAgentId
637
- ?? invokedAgents[invokedAgents.length - 1]?.id;
638
- const finalResult = (finalAgentId ? stageResults.get(finalAgentId) : undefined)
639
- ?? { output: null, contextHash: "" } as ExecutionResult;
730
+ const finalAgentId = lastExecuteAgentId ?? invokedAgents[invokedAgents.length - 1]?.id;
731
+ const finalResult = clarification?.needed
732
+ ? ({ output: null, contextHash: '' } as ExecutionResult)
733
+ : ((finalAgentId ? stageResults.get(finalAgentId) : undefined) ??
734
+ ({ output: null, contextHash: '' } as ExecutionResult));
640
735
 
641
736
  const layerFilter = this.buildLayerFilter(classification, route, requestedLayers);
642
737
  const activeLayers = this.thread.stack.layers.filter(layerFilter).map((l) => l.id);
@@ -645,6 +740,7 @@ export class Harness {
645
740
  messageId: message.id,
646
741
  classification,
647
742
  route,
743
+ clarification,
648
744
  result: finalResult,
649
745
  trace,
650
746
  timestamp: Date.now(),
@@ -652,7 +748,8 @@ export class Harness {
652
748
  activeLayers,
653
749
  };
654
750
  } catch (err) {
655
- const error = err instanceof Error ? { name: err.name, message: err.message } : { message: String(err) };
751
+ const error =
752
+ err instanceof Error ? { name: err.name, message: err.message } : { message: String(err) };
656
753
  while (trace.current) trace.end(undefined, error);
657
754
  trace.finish();
658
755
  this._recordTrace(trace, opts?.onTrace);
@@ -670,11 +767,7 @@ export class Harness {
670
767
  return this.thread.dispatch(agentId, payload, filterOverride);
671
768
  }
672
769
 
673
- async fan<T>(
674
- agentIds: string[],
675
- payload: T,
676
- filterOverride?: LayerFilter,
677
- ): Promise<FanResult[]> {
770
+ async fan<T>(agentIds: string[], payload: T, filterOverride?: LayerFilter): Promise<FanResult[]> {
678
771
  return this.thread.fan(agentIds, payload, filterOverride);
679
772
  }
680
773
 
@@ -686,15 +779,18 @@ export class Harness {
686
779
  route?: Decision<Route>,
687
780
  ): Promise<ExecutionResult> {
688
781
  const layerFilter = this.buildLayerFilter(classification, route);
689
- return this.thread.dispatch(agentId, payload, layerFilter, { role: "enrich", invocation: "on-demand" });
782
+ return this.thread.dispatch(agentId, payload, layerFilter, {
783
+ role: 'enrich',
784
+ invocation: 'on-demand',
785
+ });
690
786
  }
691
787
 
692
788
  async runPipeline<T>(input: T, trace?: Trace): Promise<unknown> {
693
789
  let current: unknown = input;
694
- const t = trace ?? new Trace("pipeline");
790
+ const t = trace ?? new Trace('pipeline');
695
791
 
696
792
  for (const step of this._pipeline) {
697
- t.start(step.id, "middleware", { input: current });
793
+ t.start(step.id, 'middleware', { input: current });
698
794
  current = await step.run(current, this, t);
699
795
  t.end(current);
700
796
  }
@@ -743,7 +839,10 @@ export class Harness {
743
839
  if (this._traces.length > this._maxTraces) {
744
840
  this._traces.shift();
745
841
  }
746
- try { onTrace?.(trace); }
747
- catch (error) { console.warn("[Harness] trace observer failed:", error); }
842
+ try {
843
+ onTrace?.(trace);
844
+ } catch (error) {
845
+ console.warn('[Harness] trace observer failed:', error);
846
+ }
748
847
  }
749
848
  }