@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/delivery-evidence.ts
CHANGED
|
@@ -1,85 +1,236 @@
|
|
|
1
1
|
/** Portable SHA-256 for public evidence, shared by Bun and the historical browser.
|
|
2
2
|
* This is an integrity join, not a signature or native delivery acknowledgment. */
|
|
3
3
|
export function evidenceDigest(text: string): string {
|
|
4
|
-
const input = new TextEncoder().encode(text),
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
const input = new TextEncoder().encode(text),
|
|
5
|
+
bytes = new Uint8Array(Math.ceil((input.length + 9) / 64) * 64);
|
|
6
|
+
bytes.set(input);
|
|
7
|
+
bytes[input.length] = 128;
|
|
8
|
+
const view = new DataView(bytes.buffer),
|
|
9
|
+
bits = input.length * 8;
|
|
10
|
+
view.setUint32(bytes.length - 8, Math.floor(bits / 4294967296));
|
|
11
|
+
view.setUint32(bytes.length - 4, bits >>> 0);
|
|
12
|
+
const h = [
|
|
13
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
14
|
+
];
|
|
15
|
+
const k = [
|
|
16
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
17
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
18
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
19
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
20
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
21
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
22
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
23
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
24
|
+
];
|
|
25
|
+
const rr = (v: number, n: number) => (v >>> n) | (v << (32 - n)),
|
|
26
|
+
w = new Uint32Array(64);
|
|
11
27
|
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
12
28
|
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + 4 * i);
|
|
13
|
-
for (let i = 16; i < 64; i++) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
for (let i = 16; i < 64; i++) {
|
|
30
|
+
const x = w[i - 15]!,
|
|
31
|
+
y = w[i - 2]!;
|
|
32
|
+
w[i] =
|
|
33
|
+
w[i - 16]! +
|
|
34
|
+
(rr(x, 7) ^ rr(x, 18) ^ (x >>> 3)) +
|
|
35
|
+
w[i - 7]! +
|
|
36
|
+
(rr(y, 17) ^ rr(y, 19) ^ (y >>> 10));
|
|
37
|
+
}
|
|
38
|
+
let [a, b, c, d, e, f, g, j] = h as [
|
|
39
|
+
number,
|
|
40
|
+
number,
|
|
41
|
+
number,
|
|
42
|
+
number,
|
|
43
|
+
number,
|
|
44
|
+
number,
|
|
45
|
+
number,
|
|
46
|
+
number,
|
|
47
|
+
];
|
|
48
|
+
for (let i = 0; i < 64; i++) {
|
|
49
|
+
const t = (j + (rr(e, 6) ^ rr(e, 11) ^ rr(e, 25)) + ((e & f) ^ (~e & g)) + k[i]! + w[i]!) | 0;
|
|
50
|
+
const u = ((rr(a, 2) ^ rr(a, 13) ^ rr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) | 0;
|
|
51
|
+
j = g;
|
|
52
|
+
g = f;
|
|
53
|
+
f = e;
|
|
54
|
+
e = (d + t) | 0;
|
|
55
|
+
d = c;
|
|
56
|
+
c = b;
|
|
57
|
+
b = a;
|
|
58
|
+
a = (t + u) | 0;
|
|
59
|
+
}
|
|
60
|
+
[a, b, c, d, e, f, g, j].forEach((v, i) => {
|
|
61
|
+
h[i] = (h[i]! + v) >>> 0;
|
|
62
|
+
});
|
|
18
63
|
}
|
|
19
|
-
return h.map(v=>v.toString(16).padStart(8,
|
|
64
|
+
return h.map((v) => v.toString(16).padStart(8, '0')).join('');
|
|
20
65
|
}
|
|
21
66
|
|
|
22
|
-
export interface DeliveryOwner {
|
|
67
|
+
export interface DeliveryOwner {
|
|
68
|
+
threadId: string;
|
|
69
|
+
messageId: string;
|
|
70
|
+
projectId?: string;
|
|
71
|
+
}
|
|
23
72
|
export interface ProviderBoundaryReceipt {
|
|
24
|
-
schema: 1;
|
|
73
|
+
schema: 1;
|
|
74
|
+
owner: DeliveryOwner;
|
|
75
|
+
assemblyHash: string;
|
|
76
|
+
messagesHash: string;
|
|
25
77
|
/** Exactly one initial system message starts with the assembled context. */
|
|
26
78
|
systemMessageIndex: number;
|
|
27
79
|
}
|
|
28
80
|
// Fixed top-level field order over the preserved serialization, never the current cache.
|
|
29
81
|
export function assemblyDigest(i: any): string {
|
|
30
|
-
return evidenceDigest(
|
|
31
|
-
|
|
82
|
+
return evidenceDigest(
|
|
83
|
+
JSON.stringify([
|
|
84
|
+
i.threadId,
|
|
85
|
+
i.projectId ?? null,
|
|
86
|
+
i.messageId,
|
|
87
|
+
i.executorContext,
|
|
88
|
+
i.userMessage,
|
|
89
|
+
i.layers,
|
|
90
|
+
i.blocks,
|
|
91
|
+
i.decoration,
|
|
92
|
+
]),
|
|
93
|
+
);
|
|
32
94
|
}
|
|
33
95
|
export function boundaryReceipt(i: any, owner: DeliveryOwner): ProviderBoundaryReceipt {
|
|
34
|
-
const matches = i.providerMessages.flatMap((m: any, n: number) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
96
|
+
const matches = i.providerMessages.flatMap((m: any, n: number) =>
|
|
97
|
+
m.role === 'system' &&
|
|
98
|
+
typeof m.content === 'string' &&
|
|
99
|
+
typeof i.executorContext === 'string' &&
|
|
100
|
+
i.executorContext.length > 0 &&
|
|
101
|
+
(m.content === i.executorContext || m.content.startsWith(i.executorContext + '\n\n'))
|
|
102
|
+
? [n]
|
|
103
|
+
: [],
|
|
104
|
+
);
|
|
105
|
+
return {
|
|
106
|
+
schema: 1,
|
|
107
|
+
owner: { ...owner },
|
|
108
|
+
assemblyHash: assemblyDigest(i),
|
|
109
|
+
messagesHash: evidenceDigest(JSON.stringify(i.providerMessages)),
|
|
110
|
+
systemMessageIndex: matches.length === 1 ? matches[0] : -1,
|
|
111
|
+
};
|
|
39
112
|
}
|
|
40
113
|
export interface ExpertDeliveryProof {
|
|
41
|
-
domain: string;
|
|
42
|
-
|
|
114
|
+
domain: string;
|
|
115
|
+
layerId: string;
|
|
116
|
+
assessedRevision: number;
|
|
117
|
+
deliveredRevision: number;
|
|
118
|
+
deliveredHash: string;
|
|
119
|
+
relation: 'assessed' | 'advanced';
|
|
120
|
+
content: string;
|
|
43
121
|
}
|
|
44
122
|
/** One validation contract used by inspection, the runner and the copy-only audit. */
|
|
45
|
-
export function verifyExpertDelivery(
|
|
46
|
-
|
|
123
|
+
export function verifyExpertDelivery(
|
|
124
|
+
i: any,
|
|
125
|
+
delivery: any,
|
|
126
|
+
domain: string,
|
|
127
|
+
expected: DeliveryOwner,
|
|
128
|
+
): ExpertDeliveryProof {
|
|
129
|
+
const fail = () => {
|
|
130
|
+
throw Error('Recorded expert delivery invalid');
|
|
131
|
+
};
|
|
47
132
|
const one = (rows: any, predicate: (row: any) => boolean): any => {
|
|
48
|
-
if (!Array.isArray(rows)) return fail();
|
|
133
|
+
if (!Array.isArray(rows)) return fail();
|
|
134
|
+
const found = rows.filter(predicate);
|
|
135
|
+
return found.length === 1 ? found[0] : fail();
|
|
49
136
|
};
|
|
50
|
-
const owner = (x: any) =>
|
|
137
|
+
const owner = (x: any) =>
|
|
138
|
+
x?.threadId === expected.threadId && x?.projectId === expected.projectId;
|
|
51
139
|
const receipt = i?.providerBoundary;
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
140
|
+
if (
|
|
141
|
+
!expected.threadId ||
|
|
142
|
+
!expected.messageId ||
|
|
143
|
+
!owner(i) ||
|
|
144
|
+
i.messageId !== expected.messageId ||
|
|
145
|
+
!owner(i.decoration?.input?.currentMessage) ||
|
|
146
|
+
i.decoration.input.currentMessage.messageId !== expected.messageId ||
|
|
147
|
+
receipt?.schema !== 1 ||
|
|
148
|
+
!owner(receipt.owner) ||
|
|
149
|
+
receipt.owner.messageId !== expected.messageId ||
|
|
150
|
+
receipt.assemblyHash !== assemblyDigest(i) ||
|
|
151
|
+
!Array.isArray(i.providerMessages) ||
|
|
152
|
+
receipt.messagesHash !== evidenceDigest(JSON.stringify(i.providerMessages))
|
|
153
|
+
)
|
|
154
|
+
fail();
|
|
57
155
|
const system = i.providerMessages[receipt.systemMessageIndex];
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
156
|
+
if (
|
|
157
|
+
!Number.isSafeInteger(receipt.systemMessageIndex) ||
|
|
158
|
+
system?.role !== 'system' ||
|
|
159
|
+
typeof system.content !== 'string' ||
|
|
160
|
+
!i.executorContext ||
|
|
161
|
+
!(system.content === i.executorContext || system.content.startsWith(i.executorContext + '\n\n'))
|
|
162
|
+
)
|
|
163
|
+
fail();
|
|
164
|
+
const p = one(i.decoration?.participants, (p) => p?.id === domain),
|
|
165
|
+
id = `thread-knowledge:${domain}`;
|
|
166
|
+
const e = one(delivery?.layers, (e) => e?.id === id),
|
|
167
|
+
l = one(i.layers, (l) => l?.id === id);
|
|
62
168
|
if (new Set(delivery.layers.map((row: any) => row?.id)).size !== delivery.layers.length) fail();
|
|
63
169
|
for (const row of delivery.layers.filter((x: any) => x?.domain === domain && x.id !== id)) {
|
|
64
|
-
const cache = one(i.layers, l => l?.id === row.id);
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
170
|
+
const cache = one(i.layers, (l) => l?.id === row.id);
|
|
171
|
+
if (
|
|
172
|
+
!owner(cache) ||
|
|
173
|
+
!owner(row) ||
|
|
174
|
+
cache.included !== true ||
|
|
175
|
+
row.assessedRevision !== undefined ||
|
|
176
|
+
row.relation !== undefined ||
|
|
177
|
+
row.assessedHash !== p.provenance?.cacheHash ||
|
|
178
|
+
row.deliveredHash !== cache.hash ||
|
|
179
|
+
row.drift !== (row.assessedHash !== row.deliveredHash)
|
|
180
|
+
)
|
|
181
|
+
fail();
|
|
68
182
|
}
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
183
|
+
if (
|
|
184
|
+
delivery.layers.filter((e: any) => e?.domain === domain && e?.assessedRevision !== undefined)
|
|
185
|
+
.length !== 1
|
|
186
|
+
)
|
|
187
|
+
fail();
|
|
188
|
+
const v = l.version,
|
|
189
|
+
ar = p.provenance?.threadKnowledgeRevision,
|
|
190
|
+
ah = p.provenance?.threadKnowledgeHash,
|
|
191
|
+
r = e.deliveredRevision;
|
|
192
|
+
if (
|
|
193
|
+
!owner(l) ||
|
|
194
|
+
l.included !== true ||
|
|
195
|
+
l.segment !== 'thread-knowledge' ||
|
|
196
|
+
l.definitionId !== id ||
|
|
197
|
+
!owner(v) ||
|
|
198
|
+
v?.domain !== domain ||
|
|
199
|
+
e.domain !== domain ||
|
|
200
|
+
!owner(e) ||
|
|
201
|
+
!Number.isSafeInteger(ar) ||
|
|
202
|
+
ar < 0 ||
|
|
203
|
+
!Number.isSafeInteger(r) ||
|
|
204
|
+
r < 0 ||
|
|
205
|
+
e.assessedRevision !== ar ||
|
|
206
|
+
e.assessedHash !== ah ||
|
|
207
|
+
typeof ah !== 'string' ||
|
|
208
|
+
typeof p.segments?.threadKnowledge !== 'string' ||
|
|
209
|
+
typeof l.content !== 'string' ||
|
|
210
|
+
e.deliveredHash !== l.hash ||
|
|
211
|
+
v.hash !== l.hash ||
|
|
212
|
+
v.revision !== r ||
|
|
213
|
+
e.drift !== (e.assessedHash !== e.deliveredHash) ||
|
|
214
|
+
(e.relation === 'assessed'
|
|
215
|
+
? r !== ar || ah !== l.hash
|
|
216
|
+
: e.relation === 'advanced'
|
|
217
|
+
? r <= ar
|
|
218
|
+
: true)
|
|
219
|
+
)
|
|
220
|
+
fail();
|
|
221
|
+
const b = one(i.blocks, (b) => b?.source === id && b.kind === 'thread-knowledge');
|
|
222
|
+
if (b.text !== l.content || b.hash !== l.hash || b.id !== `thread-knowledge:${id}:${l.hash}`)
|
|
223
|
+
fail();
|
|
81
224
|
// Intended assembled block, not a matching quotation in the user message.
|
|
82
|
-
const body = i.blocks.map((b: any) => b.text).join(
|
|
225
|
+
const body = i.blocks.map((b: any) => b.text).join('\n\n');
|
|
83
226
|
if (body !== i.executorContext) fail();
|
|
84
|
-
return {
|
|
227
|
+
return {
|
|
228
|
+
domain,
|
|
229
|
+
layerId: id,
|
|
230
|
+
assessedRevision: ar,
|
|
231
|
+
deliveredRevision: r,
|
|
232
|
+
deliveredHash: l.hash,
|
|
233
|
+
relation: e.relation,
|
|
234
|
+
content: l.content,
|
|
235
|
+
};
|
|
85
236
|
}
|
package/src/event-stream.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
5
|
-
import type {
|
|
1
|
+
import type { ActionPrompt } from './action-prompt';
|
|
2
|
+
import type { LifecycleEvent } from './cache-lifecycle';
|
|
3
|
+
import type { DispatchContext } from './middleware';
|
|
4
|
+
import type { Signal } from './signal';
|
|
5
|
+
import type { Dispatch } from './thread';
|
|
6
6
|
|
|
7
7
|
/** Session lifecycle events (minimal type for event stream). */
|
|
8
8
|
export interface SessionEvent {
|
|
9
|
-
readonly type:
|
|
9
|
+
readonly type: 'thread:added' | 'thread:removed' | 'thread:spawned';
|
|
10
10
|
readonly threadId: string;
|
|
11
11
|
readonly parentId?: string;
|
|
12
12
|
readonly timestamp: number;
|
|
@@ -16,14 +16,28 @@ export interface SessionEvent {
|
|
|
16
16
|
* Every observable event in the system, tagged by origin.
|
|
17
17
|
*/
|
|
18
18
|
export type StreamEvent =
|
|
19
|
-
| {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
| {
|
|
20
|
+
kind: 'journal';
|
|
21
|
+
threadId: string;
|
|
22
|
+
turnId: string | null;
|
|
23
|
+
projectId?: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
scope?: 'phase' | 'learning';
|
|
26
|
+
outcome?: 'durable' | 'failed';
|
|
27
|
+
}
|
|
28
|
+
| { kind: 'layer'; threadId: string; event: LifecycleEvent }
|
|
29
|
+
| { kind: 'dispatch'; threadId: string; dispatch: Dispatch }
|
|
30
|
+
| { kind: 'signal'; threadId: string; signal: Signal }
|
|
31
|
+
| { kind: 'prompt'; threadId: string; prompt: ActionPrompt }
|
|
32
|
+
| { kind: 'session'; event: SessionEvent }
|
|
33
|
+
| { kind: 'middleware'; threadId: string; phase: 'before' | 'after'; context: DispatchContext }
|
|
34
|
+
| {
|
|
35
|
+
kind: 'error';
|
|
36
|
+
source: string;
|
|
37
|
+
message: string;
|
|
38
|
+
severity: 'error' | 'warn';
|
|
39
|
+
timestamp: number;
|
|
40
|
+
};
|
|
27
41
|
|
|
28
42
|
/**
|
|
29
43
|
* Unified event stream across all threads and sessions.
|
|
@@ -56,7 +70,7 @@ export class EventStream {
|
|
|
56
70
|
try {
|
|
57
71
|
listener(event);
|
|
58
72
|
} catch (err) {
|
|
59
|
-
console.warn(
|
|
73
|
+
console.warn('[EventStream] listener error:', (err as Error).message ?? err);
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
76
|
}
|
|
@@ -72,7 +86,7 @@ export class EventStream {
|
|
|
72
86
|
|
|
73
87
|
/** Get recent events, optionally filtered by kind and/or thread. */
|
|
74
88
|
recent(opts?: {
|
|
75
|
-
kind?: StreamEvent[
|
|
89
|
+
kind?: StreamEvent['kind'];
|
|
76
90
|
threadId?: string;
|
|
77
91
|
limit?: number;
|
|
78
92
|
}): ReadonlyArray<StreamEvent> {
|
|
@@ -84,8 +98,8 @@ export class EventStream {
|
|
|
84
98
|
|
|
85
99
|
if (opts?.threadId) {
|
|
86
100
|
events = events.filter((e) => {
|
|
87
|
-
if (e.kind ===
|
|
88
|
-
return
|
|
101
|
+
if (e.kind === 'session') return true; // session events are global
|
|
102
|
+
return 'threadId' in e && e.threadId === opts.threadId;
|
|
89
103
|
});
|
|
90
104
|
}
|
|
91
105
|
|
|
@@ -94,8 +108,8 @@ export class EventStream {
|
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
/** Push an error event — surfaced as a toast in the viewer UI. */
|
|
97
|
-
pushError(source: string, message: string, severity:
|
|
98
|
-
this.push({ kind:
|
|
111
|
+
pushError(source: string, message: string, severity: 'error' | 'warn' = 'error'): void {
|
|
112
|
+
this.push({ kind: 'error', source, message, severity, timestamp: Date.now() });
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
/** Clear history. */
|
package/src/executor.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type { AssembledContext, ContextStack, LayerFilter } from
|
|
4
|
-
import { buildInjectionArtifact, type InjectionArtifact, type MessageDecoration } from
|
|
5
|
-
import type {
|
|
6
|
-
import type { ToolCallObservation } from
|
|
7
|
-
import type {
|
|
1
|
+
import { type AgentConfig, BaseAgent, type ExecutionResult } from './base-agent';
|
|
2
|
+
import { computeHash, REQUIRED_CONTEXT_BLOCKED } from './context-layer';
|
|
3
|
+
import type { AssembledContext, ContextStack, LayerFilter } from './context-stack';
|
|
4
|
+
import { buildInjectionArtifact, type InjectionArtifact, type MessageDecoration } from './messages';
|
|
5
|
+
import type { NativeEvidence, NativeObservation } from './native-evidence';
|
|
6
|
+
import type { ToolCallObservation } from './tools';
|
|
7
|
+
import type { LLMMessage } from './types';
|
|
8
8
|
|
|
9
9
|
/** Runtime metadata passed to executor handlers at dispatch time. */
|
|
10
10
|
export interface ExecuteMeta {
|
|
@@ -44,21 +44,17 @@ export interface ExecuteMeta {
|
|
|
44
44
|
export type ExecuteHandler<TPayload, TResult> = (
|
|
45
45
|
context: string,
|
|
46
46
|
payload: TPayload,
|
|
47
|
-
meta?: ExecuteMeta
|
|
47
|
+
meta?: ExecuteMeta,
|
|
48
48
|
) => Promise<TResult>;
|
|
49
49
|
|
|
50
|
-
export interface ExecutorConfig<TPayload = unknown, TResult = unknown>
|
|
51
|
-
extends AgentConfig {
|
|
50
|
+
export interface ExecutorConfig<TPayload = unknown, TResult = unknown> extends AgentConfig {
|
|
52
51
|
handler: ExecuteHandler<TPayload, TResult>;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
/**
|
|
56
55
|
* An Executor takes context + payload, goes and does work, returns full results.
|
|
57
56
|
*/
|
|
58
|
-
export class Executor<TPayload = unknown, TResult = unknown> extends BaseAgent<
|
|
59
|
-
TPayload,
|
|
60
|
-
TResult
|
|
61
|
-
> {
|
|
57
|
+
export class Executor<TPayload = unknown, TResult = unknown> extends BaseAgent<TPayload, TResult> {
|
|
62
58
|
private _handler: ExecuteHandler<TPayload, TResult>;
|
|
63
59
|
|
|
64
60
|
constructor(config: ExecutorConfig<TPayload, TResult>) {
|
|
@@ -73,7 +69,7 @@ export class Executor<TPayload = unknown, TResult = unknown> extends BaseAgent<
|
|
|
73
69
|
async run(
|
|
74
70
|
payload: TPayload,
|
|
75
71
|
filterOverride?: LayerFilter,
|
|
76
|
-
meta?: ExecuteMeta
|
|
72
|
+
meta?: ExecuteMeta,
|
|
77
73
|
): Promise<ExecutionResult<TResult>> {
|
|
78
74
|
// Decoration composed by pre-message middleware (parallel domain advice)
|
|
79
75
|
// rides in on annotations and is appended after the assembled layers, so
|
|
@@ -83,37 +79,54 @@ export class Executor<TPayload = unknown, TResult = unknown> extends BaseAgent<
|
|
|
83
79
|
const base = this.assembleContext(filterOverride);
|
|
84
80
|
const blocks = decoration?.blocks.length ? [...base.blocks, ...decoration.blocks] : base.blocks;
|
|
85
81
|
const assembled: AssembledContext = decoration?.blocks.length
|
|
86
|
-
? { blocks, text: blocks.map((block) => block.text).join(
|
|
82
|
+
? { blocks, text: blocks.map((block) => block.text).join('\n\n') }
|
|
87
83
|
: base;
|
|
88
84
|
const context = assembled.text;
|
|
89
85
|
const contextHash = computeHash(context);
|
|
90
86
|
const capturedAt = Date.now();
|
|
91
|
-
const included = new Set(assembled.blocks.map(block => block.id));
|
|
92
|
-
const layers = this._stack.layers.map(layer => ({
|
|
87
|
+
const included = new Set(assembled.blocks.map((block) => block.id));
|
|
88
|
+
const layers = this._stack.layers.map((layer) => ({
|
|
93
89
|
...layer.snapshotInstance(meta?.threadId),
|
|
94
90
|
id: layer.id,
|
|
95
91
|
prompt: layer.prompt,
|
|
96
|
-
sourceIds: layer.sources.map(source => source.id),
|
|
92
|
+
sourceIds: layer.sources.map((source) => source.id),
|
|
97
93
|
included: included.has(layer.id),
|
|
98
94
|
}));
|
|
99
|
-
const userMessage = typeof payload ===
|
|
95
|
+
const userMessage = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
100
96
|
const injection = buildInjectionArtifact({
|
|
101
97
|
userMessage,
|
|
102
98
|
assembled: base,
|
|
103
99
|
decoration,
|
|
104
|
-
layerFreshness: Object.fromEntries(
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
layerFreshness: Object.fromEntries(
|
|
101
|
+
layers.map((layer) => [
|
|
102
|
+
layer.id,
|
|
103
|
+
{
|
|
104
|
+
state: layer.state,
|
|
105
|
+
lastWarmed: layer.lastWarmed,
|
|
106
|
+
},
|
|
107
|
+
]),
|
|
108
|
+
),
|
|
107
109
|
});
|
|
108
110
|
let providerMessages: LLMMessage[] | undefined;
|
|
109
111
|
let providerBoundary: ProviderBoundaryReceipt | undefined;
|
|
110
|
-
const capture = (): InjectionArtifact => ({
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
const capture = (): InjectionArtifact => ({
|
|
113
|
+
...injection,
|
|
114
|
+
capturedAt,
|
|
115
|
+
threadId: meta?.threadId,
|
|
116
|
+
projectId: meta?.projectId,
|
|
117
|
+
messageId: meta?.messageId,
|
|
118
|
+
providerBoundary,
|
|
119
|
+
executorContext: context,
|
|
120
|
+
providerMessages,
|
|
121
|
+
layers,
|
|
122
|
+
...(plan !== undefined ? { plan } : {}),
|
|
123
|
+
});
|
|
114
124
|
const observe = (callback: () => void) => {
|
|
115
|
-
try {
|
|
116
|
-
|
|
125
|
+
try {
|
|
126
|
+
callback();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn('[Executor] input evidence observer failed:', error);
|
|
129
|
+
}
|
|
117
130
|
};
|
|
118
131
|
const record = () => observe(() => meta?.recordInjection?.(structuredClone(capture())));
|
|
119
132
|
// These are in-memory preparation observations, not durable writes or
|
|
@@ -122,27 +135,43 @@ export class Executor<TPayload = unknown, TResult = unknown> extends BaseAgent<
|
|
|
122
135
|
// Shared layers may finish a coalesced load for a later request. Preserve
|
|
123
136
|
// the actual prepared evidence above, but never send it as this request's
|
|
124
137
|
// selection. No retry/replay is implied by refusing this provider call.
|
|
125
|
-
const mismatched = layers.filter(layer => {
|
|
138
|
+
const mismatched = layers.filter((layer) => {
|
|
126
139
|
if (!layer.included || !layer.selection) return false;
|
|
127
140
|
const selectedFor = layer.selection.currentMessage;
|
|
128
141
|
// Standalone legacy sources have no request-identity contract. A flow
|
|
129
142
|
// plan does: even its unidentified loads must not replace identified work.
|
|
130
143
|
if (!selectedFor && plan === undefined) return false;
|
|
131
144
|
const expectedId = meta?.messageId && meta.threadId ? meta.messageId : undefined;
|
|
132
|
-
return
|
|
133
|
-
|
|
145
|
+
return (
|
|
146
|
+
selectedFor?.messageId !== expectedId ||
|
|
147
|
+
(selectedFor !== undefined &&
|
|
148
|
+
(selectedFor.threadId !== meta?.threadId || selectedFor.projectId !== meta?.projectId))
|
|
149
|
+
);
|
|
134
150
|
});
|
|
135
151
|
if (mismatched.length) {
|
|
136
|
-
throw new Error(
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Memory selection changed before provider execution: logical message ownership differs for layer(s) ${mismatched.map((l) => l.id).join(', ')}`,
|
|
154
|
+
);
|
|
137
155
|
}
|
|
138
156
|
// A selecting source that could not carry required context is a reason to
|
|
139
157
|
// stop, not a note for the model. Refuse before any provider call; the
|
|
140
158
|
// recorded artifact above keeps the conflict and record ids inspectable.
|
|
141
|
-
const blocked = layers
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
const blocked = layers
|
|
160
|
+
.filter((layer) => layer.included)
|
|
161
|
+
.flatMap((layer) =>
|
|
162
|
+
(layer.selection?.sources ?? []).flatMap((source) =>
|
|
163
|
+
source.report.conflicts
|
|
164
|
+
.filter((conflict) => conflict.kind === REQUIRED_CONTEXT_BLOCKED)
|
|
165
|
+
.map(
|
|
166
|
+
(conflict) =>
|
|
167
|
+
`layer ${layer.id}, source ${source.sourceId}: ${conflict.ids.join(', ')}`,
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
144
171
|
if (blocked.length) {
|
|
145
|
-
throw new Error(
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Required memory context blocked before provider execution: pinned records exceed the selection cap and no scoped retrieval is available (${blocked.join('; ')})`,
|
|
174
|
+
);
|
|
146
175
|
}
|
|
147
176
|
let output: TResult;
|
|
148
177
|
let native: NativeEvidence | undefined;
|
|
@@ -150,14 +179,22 @@ export class Executor<TPayload = unknown, TResult = unknown> extends BaseAgent<
|
|
|
150
179
|
try {
|
|
151
180
|
output = await this._handler(context, payload, {
|
|
152
181
|
...meta,
|
|
153
|
-
recordNative:
|
|
154
|
-
|
|
182
|
+
recordNative: (evidence) => {
|
|
183
|
+
if (recording) {
|
|
184
|
+
native = evidence;
|
|
185
|
+
observe(() => meta?.recordNative?.(evidence));
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
recordProviderInput: (messages) => {
|
|
155
189
|
// The initial boundary input must not be replaced by tool followups.
|
|
156
190
|
if (providerMessages === undefined) {
|
|
157
191
|
providerMessages = structuredClone(messages);
|
|
158
|
-
if (meta?.threadId && meta.messageId)
|
|
159
|
-
|
|
160
|
-
|
|
192
|
+
if (meta?.threadId && meta.messageId)
|
|
193
|
+
providerBoundary = boundaryReceipt(capture(), {
|
|
194
|
+
threadId: meta.threadId,
|
|
195
|
+
messageId: meta.messageId,
|
|
196
|
+
...(meta.projectId ? { projectId: meta.projectId } : {}),
|
|
197
|
+
});
|
|
161
198
|
}
|
|
162
199
|
record();
|
|
163
200
|
observe(() => meta?.recordProviderInput?.(structuredClone(messages)));
|
|
@@ -182,4 +219,5 @@ export class Executor<TPayload = unknown, TResult = unknown> extends BaseAgent<
|
|
|
182
219
|
};
|
|
183
220
|
}
|
|
184
221
|
}
|
|
185
|
-
|
|
222
|
+
|
|
223
|
+
import { boundaryReceipt, type ProviderBoundaryReceipt } from './delivery-evidence';
|