@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.
- 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 -1
- 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 +252 -218
- package/src/intervention.ts +21 -12
- package/src/messages.ts +73 -62
- package/src/middleware.ts +9 -19
- package/src/model-registry.ts +1645 -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/intervention.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
101
|
+
await thread.signals.emit({
|
|
93
102
|
id: `sig_${intervention.id}`,
|
|
94
|
-
kind:
|
|
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 {
|
|
2
|
-
import { computeHash } from
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
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
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
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:
|
|
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:
|
|
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:
|
|
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: {
|
|
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<{
|
|
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?:
|
|
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:
|
|
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(
|
|
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<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 ===
|
|
159
|
-
if (block.role ===
|
|
160
|
-
if (block.id ===
|
|
161
|
-
return
|
|
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
|
|
176
|
+
return 'domain-knowledge';
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
function blockTitle(kind: InjectionSegmentKind): string {
|
|
167
180
|
switch (kind) {
|
|
168
|
-
case
|
|
169
|
-
return
|
|
170
|
-
case
|
|
171
|
-
return
|
|
172
|
-
case
|
|
173
|
-
return
|
|
174
|
-
case
|
|
175
|
-
return
|
|
176
|
-
case
|
|
177
|
-
return
|
|
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[
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
260
|
+
sections.push(grouped.map((block) => block.text).join('\n\n'));
|
|
250
261
|
}
|
|
251
262
|
|
|
252
|
-
const text = sections.join(
|
|
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 ===
|
|
287
|
+
if (block.role === 'system') {
|
|
277
288
|
systemParts.push(block.text);
|
|
278
|
-
} else if (block.role ===
|
|
289
|
+
} else if (block.role === 'layer') {
|
|
279
290
|
systemParts.push(`[${block.id}]: ${block.text}`);
|
|
280
|
-
} else if (block.role ===
|
|
291
|
+
} else if (block.role === 'content') {
|
|
281
292
|
systemParts.push(block.text);
|
|
282
293
|
}
|
|
283
294
|
}
|
|
284
295
|
|
|
285
|
-
messages.push({ role:
|
|
296
|
+
messages.push({ role: 'system', content: systemParts.join('\n\n') });
|
|
286
297
|
}
|
|
287
298
|
|
|
288
|
-
messages.push({ role:
|
|
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 ===
|
|
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(
|
|
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
|
|
2
|
-
import type { ContextStackView } from
|
|
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 =
|
|
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:
|
|
64
|
+
this._middleware.push({ id, tier: 'always', fn });
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
/** Register conditional middleware with a predicate. */
|
|
71
|
-
useWhen(
|
|
72
|
-
id:
|
|
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 ===
|
|
83
|
+
if (m.tier === 'always') return true;
|
|
94
84
|
return m.when ? m.when(ctx) : false;
|
|
95
85
|
});
|
|
96
86
|
|