@inixiative/foundry-core 0.2.0 → 0.4.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.
@@ -1,6 +1,12 @@
1
- import type { Signal, SignalBus } from "./signal";
2
- import type { Trace, Span } from "./trace";
3
- import { newId } from "./id";
1
+ import { newId } from './id';
2
+ import type { Signal, SignalBus } from './signal';
3
+ import type { Span, Trace } from './trace';
4
+
5
+ /** The thread an intervention targets: its id and its own signal bus. */
6
+ export interface InterventionThread {
7
+ readonly id: string;
8
+ readonly signals: SignalBus;
9
+ }
4
10
 
5
11
  /**
6
12
  * An intervention — a manual override from a human operator.
@@ -20,6 +26,9 @@ export interface Intervention {
20
26
  readonly id: string;
21
27
  readonly timestamp: number;
22
28
 
29
+ /** The thread the correction was made in; its signal bus receives the correction. */
30
+ readonly threadId: string;
31
+
23
32
  /** What trace this intervention targets. */
24
33
  readonly traceId: string;
25
34
 
@@ -44,7 +53,7 @@ export interface Intervention {
44
53
  *
45
54
  * When an operator overrides a decision, the InterventionLog:
46
55
  * - Records the intervention
47
- * - Emits a correction signal on the signal bus
56
+ * - Emits a correction signal on the bus of the thread it was made in
48
57
  * - Provides the intervention history for the UI
49
58
  *
50
59
  * The correction signal flows through the normal signal pipeline —
@@ -53,28 +62,28 @@ export interface Intervention {
53
62
  */
54
63
  export class InterventionLog {
55
64
  private _interventions: Intervention[] = [];
56
- private _signals: SignalBus;
57
65
  private _maxHistory: number;
58
66
 
59
- constructor(signals: SignalBus, maxHistory: number = 1000) {
60
- this._signals = signals;
67
+ constructor(maxHistory: number = 1000) {
61
68
  this._maxHistory = maxHistory;
62
69
  }
63
70
 
64
71
  /**
65
- * Record an intervention and emit a correction signal.
72
+ * Record an intervention and emit a correction signal on the thread's bus.
66
73
  */
67
74
  async intervene(
75
+ thread: InterventionThread,
68
76
  traceId: string,
69
77
  spanId: string,
70
78
  actual: unknown,
71
79
  correction: unknown,
72
80
  operator: string,
73
- reason?: string
81
+ reason?: string,
74
82
  ): Promise<Intervention> {
75
83
  const intervention: Intervention = {
76
- id: newId("int"),
84
+ id: newId('int'),
77
85
  timestamp: Date.now(),
86
+ threadId: thread.id,
78
87
  traceId,
79
88
  spanId,
80
89
  actual,
@@ -89,9 +98,9 @@ export class InterventionLog {
89
98
  }
90
99
 
91
100
  // Emit as a correction signal so the system learns
92
- await this._signals.emit({
101
+ await thread.signals.emit({
93
102
  id: `sig_${intervention.id}`,
94
- kind: "correction",
103
+ kind: 'correction',
95
104
  source: `operator:${operator}`,
96
105
  content: {
97
106
  traceId,
package/src/messages.ts CHANGED
@@ -1,14 +1,15 @@
1
- import type { AssembledContext, PromptBlock } from "./context-stack";
2
- import { computeHash } from "./context-layer";
3
- import type { LayerInstanceState, LogicalMessageIdentity } from "./context-layer";
4
- import type { LLMMessage } from "./types";
1
+ import type { LayerInstanceState, LogicalMessageIdentity } from './context-layer';
2
+ import { computeHash } from './context-layer';
3
+ import type { AssembledContext, PromptBlock } from './context-stack';
4
+ import type { ServedModel } from './native-evidence';
5
+ import type { LLMMessage } from './types';
5
6
 
6
7
  export type InjectionSegmentKind =
7
- | "instructions"
8
- | "domain-knowledge"
9
- | "thread-knowledge"
10
- | "routing"
11
- | "guard-findings";
8
+ | 'instructions'
9
+ | 'domain-knowledge'
10
+ | 'thread-knowledge'
11
+ | 'routing'
12
+ | 'guard-findings';
12
13
 
13
14
  export interface InjectionBlock {
14
15
  readonly id: string;
@@ -47,26 +48,36 @@ export interface DecorationParticipant {
47
48
  /** What one participant supplied to its provider at one phase, or why it made no call. */
48
49
  export type ParticipantRequest =
49
50
  | {
50
- readonly status: "supplied";
51
+ readonly status: 'supplied';
51
52
  readonly phase: string;
52
53
  readonly providerId: string;
53
54
  readonly messages: readonly LLMMessage[];
54
55
  readonly capturedAt: number;
56
+ /** The model and effort that answered, when the provider answered before the phase sealed. */
57
+ readonly served?: ServedModel;
55
58
  }
56
- | { readonly status: "not-sent"; readonly phase: string; readonly reason: string }
59
+ | { readonly status: 'not-sent'; readonly phase: string; readonly reason: string }
57
60
  /** A call may have been made, but its input was never observed (e.g. the caller threw first). Not "not-sent". */
58
- | { readonly status: "unobserved"; readonly phase: string; readonly reason: string };
61
+ | { readonly status: 'unobserved'; readonly phase: string; readonly reason: string };
59
62
 
60
63
  /**
61
64
  * The composed, immutable decoration delivered alongside a user message:
62
65
  * the blocks appended to the executor context plus who contributed them.
63
66
  */
64
67
  export interface MessageDecoration {
65
- readonly input: { readonly hash: string; readonly capturedAt: number; readonly currentMessage?: LogicalMessageIdentity };
68
+ readonly input: {
69
+ readonly hash: string;
70
+ readonly capturedAt: number;
71
+ readonly currentMessage?: LogicalMessageIdentity;
72
+ };
66
73
  readonly blocks: PromptBlock[];
67
74
  readonly participants: DecorationParticipant[];
68
75
  readonly omissions: Array<{ readonly id: string; readonly reason: string }>;
69
- readonly conflicts: Array<{ readonly kind: string; readonly id: string; readonly detail: string }>;
76
+ readonly conflicts: Array<{
77
+ readonly kind: string;
78
+ readonly id: string;
79
+ readonly detail: string;
80
+ }>;
70
81
  }
71
82
 
72
83
  /**
@@ -93,7 +104,7 @@ export interface DeliveryRecord {
93
104
  * How the delivered snapshot relates to the assessed one: the same revision, a later revision committed
94
105
  * between assessment and assembly, no owner mark on the delivered bytes, or an inconsistent pair.
95
106
  */
96
- readonly relation?: "assessed" | "advanced" | "unknown" | "inconsistent";
107
+ readonly relation?: 'assessed' | 'advanced' | 'unknown' | 'inconsistent';
97
108
  /** True when the layer changed between assessment and delivery. */
98
109
  readonly drift: boolean;
99
110
  }>;
@@ -105,7 +116,7 @@ export interface DeliveryRecord {
105
116
  * `settled` and `timeout` are retained for historical artifacts.
106
117
  */
107
118
  readonly learningBarrier?: {
108
- readonly outcome: "none" | "settled" | "timeout" | "pending" | "closed";
119
+ readonly outcome: 'none' | 'settled' | 'timeout' | 'pending' | 'closed';
109
120
  readonly closed?: ReadonlyArray<{ domain: string; reason: string }>;
110
121
  readonly waitedMs: number;
111
122
  readonly pending: Array<{ readonly domain: string; readonly reviews: number }>;
@@ -119,7 +130,7 @@ export interface InjectionArtifact {
119
130
  readonly projectId?: string;
120
131
  readonly messageId?: string;
121
132
  /** Integrity-bound initial provider callback; never a native acknowledgment. */
122
- readonly providerBoundary?: import("./delivery-evidence").ProviderBoundaryReceipt;
133
+ readonly providerBoundary?: import('./delivery-evidence').ProviderBoundaryReceipt;
123
134
  readonly userMessage: string;
124
135
  readonly blocks: InjectionBlock[];
125
136
  /** Decoration delivered with this turn, when a decorator ran. */
@@ -132,12 +143,14 @@ export interface InjectionArtifact {
132
143
  readonly executorContext?: string;
133
144
  /** Initial provider messages, before native-runtime wrapping or tool followups. */
134
145
  readonly providerMessages?: LLMMessage[];
135
- readonly layers?: Array<LayerInstanceState & {
136
- id: string;
137
- prompt?: string;
138
- sourceIds: string[];
139
- included: boolean;
140
- }>;
146
+ readonly layers?: Array<
147
+ LayerInstanceState & {
148
+ id: string;
149
+ prompt?: string;
150
+ sourceIds: string[];
151
+ included: boolean;
152
+ }
153
+ >;
141
154
  }
142
155
 
143
156
  export interface BuildInjectionArtifactOpts {
@@ -155,26 +168,26 @@ function estimateTextTokens(text: string): number {
155
168
  }
156
169
 
157
170
  function segmentKindForBlock(block: PromptBlock): InjectionSegmentKind {
158
- if (block.role === "system") return "instructions";
159
- if (block.role === "layer") return "instructions";
160
- if (block.id === "__thread-state" || block.id === "thread-state" || block.id === "memory") {
161
- return "thread-knowledge";
171
+ if (block.role === 'system') return 'instructions';
172
+ if (block.role === 'layer') return 'instructions';
173
+ if (block.id === '__thread-state' || block.id === 'thread-state' || block.id === 'memory') {
174
+ return 'thread-knowledge';
162
175
  }
163
- return "domain-knowledge";
176
+ return 'domain-knowledge';
164
177
  }
165
178
 
166
179
  function blockTitle(kind: InjectionSegmentKind): string {
167
180
  switch (kind) {
168
- case "routing":
169
- return "Tags / Routing";
170
- case "instructions":
171
- return "Instructions";
172
- case "domain-knowledge":
173
- return "Domain Knowledge";
174
- case "thread-knowledge":
175
- return "Thread State";
176
- case "guard-findings":
177
- return "Prior Guard Findings";
181
+ case 'routing':
182
+ return 'Tags / Routing';
183
+ case 'instructions':
184
+ return 'Instructions';
185
+ case 'domain-knowledge':
186
+ return 'Domain Knowledge';
187
+ case 'thread-knowledge':
188
+ return 'Thread State';
189
+ case 'guard-findings':
190
+ return 'Prior Guard Findings';
178
191
  }
179
192
  }
180
193
 
@@ -182,7 +195,7 @@ function makeBlock(
182
195
  kind: InjectionSegmentKind,
183
196
  source: string,
184
197
  text: string,
185
- freshness?: InjectionBlock["freshness"],
198
+ freshness?: InjectionBlock['freshness'],
186
199
  ): InjectionBlock {
187
200
  return {
188
201
  id: `${kind}:${source}:${computeHash(text)}`,
@@ -206,7 +219,7 @@ export function buildInjectionArtifact(opts: BuildInjectionArtifactOpts): Inject
206
219
  const blocks: InjectionBlock[] = [];
207
220
 
208
221
  if (opts.routing?.trim()) {
209
- blocks.push(makeBlock("routing", "route", opts.routing.trim()));
222
+ blocks.push(makeBlock('routing', 'route', opts.routing.trim()));
210
223
  }
211
224
 
212
225
  for (const block of [...opts.assembled.blocks, ...(opts.decoration?.blocks ?? [])]) {
@@ -214,18 +227,16 @@ export function buildInjectionArtifact(opts: BuildInjectionArtifactOpts): Inject
214
227
  // An explicit segment always wins; the id heuristic only classifies plain layers.
215
228
  const kind = (block.segment as InjectionSegmentKind | undefined) ?? segmentKindForBlock(block);
216
229
  const source = block.source ?? block.id ?? block.role;
217
- blocks.push(makeBlock(kind, source, block.text, block.id ? opts.layerFreshness?.[block.id] : undefined));
230
+ blocks.push(
231
+ makeBlock(kind, source, block.text, block.id ? opts.layerFreshness?.[block.id] : undefined),
232
+ );
218
233
  }
219
234
 
220
235
  if (opts.guardFindings?.trim()) {
221
- blocks.push(makeBlock("guard-findings", "guards", opts.guardFindings.trim()));
236
+ blocks.push(makeBlock('guard-findings', 'guards', opts.guardFindings.trim()));
222
237
  }
223
238
 
224
- const sections = [
225
- "# User Message",
226
- opts.userMessage,
227
- "# Foundry Injection",
228
- ];
239
+ const sections = ['# User Message', opts.userMessage, '# Foundry Injection'];
229
240
 
230
241
  const byKind = new Map<InjectionSegmentKind, InjectionBlock[]>();
231
242
  for (const block of blocks) {
@@ -235,21 +246,21 @@ export function buildInjectionArtifact(opts: BuildInjectionArtifactOpts): Inject
235
246
  }
236
247
 
237
248
  const order: InjectionSegmentKind[] = [
238
- "routing",
239
- "instructions",
240
- "domain-knowledge",
241
- "thread-knowledge",
242
- "guard-findings",
249
+ 'routing',
250
+ 'instructions',
251
+ 'domain-knowledge',
252
+ 'thread-knowledge',
253
+ 'guard-findings',
243
254
  ];
244
255
 
245
256
  for (const kind of order) {
246
257
  const grouped = byKind.get(kind);
247
258
  if (!grouped?.length) continue;
248
259
  sections.push(`## ${blockTitle(kind)}`);
249
- sections.push(grouped.map((block) => block.text).join("\n\n"));
260
+ sections.push(grouped.map((block) => block.text).join('\n\n'));
250
261
  }
251
262
 
252
- const text = sections.join("\n\n");
263
+ const text = sections.join('\n\n');
253
264
  return {
254
265
  userMessage: opts.userMessage,
255
266
  blocks,
@@ -265,7 +276,7 @@ export function buildInjectionArtifact(opts: BuildInjectionArtifactOpts): Inject
265
276
  */
266
277
  export function assembledToMessages(
267
278
  assembled: AssembledContext,
268
- userPayload: string
279
+ userPayload: string,
269
280
  ): LLMMessage[] {
270
281
  const messages: LLMMessage[] = [];
271
282
 
@@ -273,19 +284,19 @@ export function assembledToMessages(
273
284
  const systemParts: string[] = [];
274
285
 
275
286
  for (const block of assembled.blocks) {
276
- if (block.role === "system") {
287
+ if (block.role === 'system') {
277
288
  systemParts.push(block.text);
278
- } else if (block.role === "layer") {
289
+ } else if (block.role === 'layer') {
279
290
  systemParts.push(`[${block.id}]: ${block.text}`);
280
- } else if (block.role === "content") {
291
+ } else if (block.role === 'content') {
281
292
  systemParts.push(block.text);
282
293
  }
283
294
  }
284
295
 
285
- messages.push({ role: "system", content: systemParts.join("\n\n") });
296
+ messages.push({ role: 'system', content: systemParts.join('\n\n') });
286
297
  }
287
298
 
288
- messages.push({ role: "user", content: userPayload });
299
+ messages.push({ role: 'user', content: userPayload });
289
300
 
290
301
  return messages;
291
302
  }
@@ -301,7 +312,7 @@ export function splitSystemMessage(messages: LLMMessage[]): {
301
312
  const turns: LLMMessage[] = [];
302
313
 
303
314
  for (const msg of messages) {
304
- if (msg.role === "system") {
315
+ if (msg.role === 'system') {
305
316
  systemParts.push(msg.content);
306
317
  } else {
307
318
  turns.push(msg);
@@ -309,7 +320,7 @@ export function splitSystemMessage(messages: LLMMessage[]): {
309
320
  }
310
321
 
311
322
  return {
312
- system: systemParts.length > 0 ? systemParts.join("\n\n") : undefined,
323
+ system: systemParts.length > 0 ? systemParts.join('\n\n') : undefined,
313
324
  turns,
314
325
  };
315
326
  }
package/src/middleware.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { ExecutionResult } from "./base-agent";
2
- import type { ContextStackView } from "./context-stack";
1
+ import type { ExecutionResult } from './base-agent';
2
+ import type { ContextStackView } from './context-stack';
3
3
 
4
4
  export interface DispatchContext<TPayload = unknown> {
5
5
  readonly agentId: string;
@@ -28,10 +28,7 @@ export interface DispatchOutcome<TResult = unknown> {
28
28
 
29
29
  export type MiddlewareNext = () => Promise<ExecutionResult>;
30
30
 
31
- export type Middleware = (
32
- ctx: DispatchContext,
33
- next: MiddlewareNext
34
- ) => Promise<ExecutionResult>;
31
+ export type Middleware = (ctx: DispatchContext, next: MiddlewareNext) => Promise<ExecutionResult>;
35
32
 
36
33
  /**
37
34
  * When a middleware should run.
@@ -40,7 +37,7 @@ export type Middleware = (
40
37
  * - "conditional": runs only when its `when` predicate returns true
41
38
  * (deep classification, expensive enrichment, guardrails)
42
39
  */
43
- export type MiddlewareTier = "always" | "conditional";
40
+ export type MiddlewareTier = 'always' | 'conditional';
44
41
 
45
42
  export interface MiddlewareEntry {
46
43
  readonly id: string;
@@ -64,16 +61,12 @@ export class MiddlewareChain {
64
61
 
65
62
  /** Register always-on middleware. */
66
63
  use(id: string, fn: Middleware): void {
67
- this._middleware.push({ id, tier: "always", fn });
64
+ this._middleware.push({ id, tier: 'always', fn });
68
65
  }
69
66
 
70
67
  /** Register conditional middleware with a predicate. */
71
- useWhen(
72
- id: string,
73
- when: (ctx: DispatchContext) => boolean,
74
- fn: Middleware
75
- ): void {
76
- this._middleware.push({ id, tier: "conditional", fn, when });
68
+ useWhen(id: string, when: (ctx: DispatchContext) => boolean, fn: Middleware): void {
69
+ this._middleware.push({ id, tier: 'conditional', fn, when });
77
70
  }
78
71
 
79
72
  remove(id: string): boolean {
@@ -84,13 +77,10 @@ export class MiddlewareChain {
84
77
  }
85
78
 
86
79
  /** Execute the chain. Conditional middleware only runs when its predicate matches. */
87
- async execute(
88
- ctx: DispatchContext,
89
- handler: MiddlewareNext
90
- ): Promise<ExecutionResult> {
80
+ async execute(ctx: DispatchContext, handler: MiddlewareNext): Promise<ExecutionResult> {
91
81
  // Build the active stack for this request
92
82
  const active = this._middleware.filter((m) => {
93
- if (m.tier === "always") return true;
83
+ if (m.tier === 'always') return true;
94
84
  return m.when ? m.when(ctx) : false;
95
85
  });
96
86