@inixiative/foundry-core 0.1.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/package.json +1 -6
- package/src/action-prompt.ts +47 -28
- package/src/adapters/file-memory.ts +314 -122
- package/src/adapters/http-memory.ts +19 -24
- package/src/adapters/index.ts +4 -4
- package/src/adapters/markdown-docs.ts +15 -15
- package/src/adapters/sqlite-memory.ts +29 -27
- package/src/base-agent.ts +5 -7
- package/src/cache-lifecycle.ts +14 -14
- package/src/capability.ts +76 -83
- package/src/clarifier.ts +46 -0
- package/src/classifier.ts +6 -9
- package/src/context-layer.ts +125 -78
- package/src/context-stack.ts +15 -14
- package/src/credentials.ts +4 -2
- package/src/decider.ts +7 -8
- package/src/delivery-evidence.ts +206 -55
- package/src/event-stream.ts +34 -20
- package/src/executor.ts +81 -43
- package/src/harness.ts +225 -126
- package/src/hooks.ts +26 -26
- package/src/hydrator.ts +6 -8
- package/src/id.ts +3 -3
- package/src/index.ts +248 -218
- package/src/intervention.ts +21 -12
- package/src/messages.ts +73 -62
- package/src/middleware.ts +9 -19
- package/src/model-registry.ts +1587 -0
- package/src/native-evidence.ts +124 -30
- package/src/permission-middleware.ts +16 -28
- package/src/retry.ts +6 -6
- package/src/router.ts +5 -5
- package/src/signal.ts +16 -11
- package/src/thread.ts +138 -61
- package/src/token-counts.ts +9 -1
- package/src/token-tracker.ts +43 -34
- package/src/tools.ts +138 -95
- package/src/trace.ts +44 -23
- package/src/types.ts +33 -24
package/src/harness.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
import type { Classification } from
|
|
5
|
-
import type {
|
|
6
|
-
import type { LayerFilter } from
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
|
|
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 |
|
|
57
|
+
agentId: string | 'routed';
|
|
59
58
|
/** Pipeline role (for tracing and UI). */
|
|
60
|
-
role:
|
|
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:
|
|
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(
|
|
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 }> =
|
|
171
|
-
|
|
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 =
|
|
188
|
-
: typeof 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 ??
|
|
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 ??
|
|
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,
|
|
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) => {
|
|
339
|
-
|
|
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) {
|
|
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:
|
|
350
|
+
yield { kind: 'delta', text: pending.shift()! };
|
|
348
351
|
continue;
|
|
349
352
|
}
|
|
350
353
|
if (done) break;
|
|
351
|
-
await new Promise<void>((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:
|
|
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 ??
|
|
380
|
+
const activation = mode?.activation ?? 'always';
|
|
378
381
|
|
|
379
382
|
switch (activation) {
|
|
380
|
-
case
|
|
383
|
+
case 'always':
|
|
381
384
|
return true;
|
|
382
385
|
|
|
383
|
-
case
|
|
386
|
+
case 'conditional':
|
|
384
387
|
return matchesCondition(mode?.condition, classification, route);
|
|
385
388
|
|
|
386
|
-
case
|
|
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:
|
|
415
|
-
invocation:
|
|
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:
|
|
424
|
-
invocation:
|
|
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 ===
|
|
438
|
+
if (mode.invocation === 'conditional') {
|
|
436
439
|
stages.push({
|
|
437
440
|
agentId,
|
|
438
|
-
role:
|
|
439
|
-
invocation:
|
|
441
|
+
role: 'enrich',
|
|
442
|
+
invocation: 'conditional',
|
|
440
443
|
condition: mode.condition,
|
|
441
444
|
});
|
|
442
|
-
} else if (mode.invocation ===
|
|
445
|
+
} else if (mode.invocation === 'on-demand') {
|
|
443
446
|
stages.push({
|
|
444
447
|
agentId,
|
|
445
|
-
role:
|
|
446
|
-
invocation:
|
|
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:
|
|
454
|
-
role:
|
|
455
|
-
invocation:
|
|
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, {
|
|
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
|
-
//
|
|
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 ===
|
|
500
|
-
const resolvedId = stage.agentId ===
|
|
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 ===
|
|
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 =
|
|
512
|
-
|
|
513
|
-
|
|
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 ===
|
|
583
|
+
if (stage.role === 'execute') {
|
|
517
584
|
throw new Error(
|
|
518
|
-
|
|
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 ===
|
|
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(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
//
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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 ===
|
|
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 => {
|
|
661
|
+
recordInjection: (artifact) => {
|
|
662
|
+
stageSpan.annotations.injection = artifact;
|
|
663
|
+
},
|
|
579
664
|
nativeObservation: opts?.nativeObservation,
|
|
580
|
-
recordNative: evidence => {
|
|
581
|
-
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
597
|
-
kind:
|
|
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 ===
|
|
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}`,
|
|
715
|
+
trace.start(`on-demand:${agentId}`, 'enrich', { agentId });
|
|
622
716
|
const result = await this.thread.dispatch(agentId, message.payload, layerFilter, {
|
|
623
|
-
role:
|
|
717
|
+
role: 'enrich',
|
|
624
718
|
messageId: message.id,
|
|
625
|
-
invocation:
|
|
719
|
+
invocation: 'on-demand',
|
|
626
720
|
});
|
|
627
|
-
invokedAgents.push({ id: agentId, mode:
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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 =
|
|
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, {
|
|
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(
|
|
790
|
+
const t = trace ?? new Trace('pipeline');
|
|
695
791
|
|
|
696
792
|
for (const step of this._pipeline) {
|
|
697
|
-
t.start(step.id,
|
|
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 {
|
|
747
|
-
|
|
842
|
+
try {
|
|
843
|
+
onTrace?.(trace);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.warn('[Harness] trace observer failed:', error);
|
|
846
|
+
}
|
|
748
847
|
}
|
|
749
848
|
}
|