@graphorin/agent 0.6.0 → 0.7.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/CHANGELOG.md +96 -0
- package/README.md +31 -11
- package/dist/errors/index.d.ts +17 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +19 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +153 -1930
- package/dist/factory.js.map +1 -1
- package/dist/fanout/index.d.ts +13 -1
- package/dist/fanout/index.d.ts.map +1 -1
- package/dist/fanout/index.js +13 -4
- package/dist/fanout/index.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/lateral-leak/index.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/run-state/index.d.ts +32 -6
- package/dist/run-state/index.d.ts.map +1 -1
- package/dist/run-state/index.js +46 -22
- package/dist/run-state/index.js.map +1 -1
- package/dist/runtime/agent-surface.js +122 -0
- package/dist/runtime/agent-surface.js.map +1 -0
- package/dist/runtime/agent-to-tool.d.ts +51 -0
- package/dist/runtime/agent-to-tool.d.ts.map +1 -0
- package/dist/runtime/agent-to-tool.js +145 -0
- package/dist/runtime/agent-to-tool.js.map +1 -0
- package/dist/runtime/approvals.js +0 -0
- package/dist/runtime/approvals.js.map +1 -0
- package/dist/runtime/dispatch.js +108 -0
- package/dist/runtime/dispatch.js.map +1 -0
- package/dist/runtime/executor-wiring.js +128 -0
- package/dist/runtime/executor-wiring.js.map +1 -0
- package/dist/runtime/fallback-chain.js +139 -0
- package/dist/runtime/fallback-chain.js.map +1 -0
- package/dist/runtime/handoff.js +307 -0
- package/dist/runtime/handoff.js.map +1 -0
- package/dist/runtime/messages.d.ts +22 -0
- package/dist/runtime/messages.d.ts.map +1 -0
- package/dist/runtime/messages.js +204 -0
- package/dist/runtime/messages.js.map +1 -0
- package/dist/runtime/provider-events.js +117 -0
- package/dist/runtime/provider-events.js.map +1 -0
- package/dist/runtime/run-compaction.js +210 -0
- package/dist/runtime/run-compaction.js.map +1 -0
- package/dist/runtime/run-finish.js +48 -0
- package/dist/runtime/run-finish.js.map +1 -0
- package/dist/runtime/run-gates.js +336 -0
- package/dist/runtime/run-gates.js.map +1 -0
- package/dist/runtime/run-init.js +81 -0
- package/dist/runtime/run-init.js.map +1 -0
- package/dist/runtime/run-input.js +46 -0
- package/dist/runtime/run-input.js.map +1 -0
- package/dist/runtime/step-catalogue.js +173 -0
- package/dist/runtime/step-catalogue.js.map +1 -0
- package/dist/runtime/tool-call-walk.js +189 -0
- package/dist/runtime/tool-call-walk.js.map +1 -0
- package/dist/runtime/tool-wiring.js +159 -0
- package/dist/runtime/tool-wiring.js.map +1 -0
- package/dist/tooling/adapters.js +1 -1
- package/dist/tooling/dataflow.js +1 -1
- package/dist/tooling/policy.js +2 -0
- package/dist/tooling/policy.js.map +1 -1
- package/dist/types.d.ts +63 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/errors/index.ts +320 -0
- package/src/evaluator-optimizer/index.ts +212 -0
- package/src/factory.ts +957 -0
- package/src/fallback/index.ts +108 -0
- package/src/fanout/index.ts +523 -0
- package/src/filters/index.ts +347 -0
- package/src/index.ts +180 -0
- package/src/internal/ids.ts +46 -0
- package/src/internal/usage-accumulator.ts +90 -0
- package/src/lateral-leak/causality-monitor.ts +221 -0
- package/src/lateral-leak/index.ts +35 -0
- package/src/lateral-leak/merge-guard.ts +151 -0
- package/src/lateral-leak/protocol-guard.ts +222 -0
- package/src/preferred-model/index.ts +210 -0
- package/src/progress/index.ts +238 -0
- package/src/run-state/index.ts +607 -0
- package/src/runtime/agent-surface.ts +218 -0
- package/src/runtime/agent-to-tool.ts +323 -0
- package/src/runtime/approvals.ts +0 -0
- package/src/runtime/dispatch.ts +183 -0
- package/src/runtime/executor-wiring.ts +331 -0
- package/src/runtime/fallback-chain.ts +250 -0
- package/src/runtime/handoff.ts +428 -0
- package/src/runtime/messages.ts +309 -0
- package/src/runtime/provider-events.ts +175 -0
- package/src/runtime/run-compaction.ts +288 -0
- package/src/runtime/run-finish.ts +93 -0
- package/src/runtime/run-gates.ts +419 -0
- package/src/runtime/run-init.ts +169 -0
- package/src/runtime/run-input.ts +102 -0
- package/src/runtime/step-catalogue.ts +338 -0
- package/src/runtime/tool-call-walk.ts +301 -0
- package/src/runtime/tool-wiring.ts +218 -0
- package/src/testing/replay-provider.ts +121 -0
- package/src/tooling/adapters.ts +403 -0
- package/src/tooling/catalogue.ts +36 -0
- package/src/tooling/dataflow.ts +171 -0
- package/src/tooling/plan.ts +123 -0
- package/src/tooling/policy.ts +67 -0
- package/src/tooling/registry-build.ts +191 -0
- package/src/types.ts +696 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handoff filter library - a small set of pure, composable functions
|
|
3
|
+
* that take the parent agent's message history and return a filtered
|
|
4
|
+
* subset suitable for forwarding to a child agent.
|
|
5
|
+
*
|
|
6
|
+
* Every filter pairs a `HandoffFilter` runtime function with a
|
|
7
|
+
* serializable {@link HandoffInputFilterDescriptor} so the JSONL
|
|
8
|
+
* session export (`@graphorin/sessions`) can replay the filter stack
|
|
9
|
+
* even after the runtime implementations evolve.
|
|
10
|
+
*
|
|
11
|
+
* Reasoning content is **always** stripped at the handoff boundary -
|
|
12
|
+
* `filters.compose(...)` guarantees `stripReasoning()` runs last so a
|
|
13
|
+
* caller-supplied filter cannot accidentally forward reasoning to a
|
|
14
|
+
* child agent. This is the confidentiality + token-economy default
|
|
15
|
+
* documented in the agent-loop reference.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
HandoffFilter,
|
|
22
|
+
HandoffInputFilterDescriptor,
|
|
23
|
+
Message,
|
|
24
|
+
Sensitivity,
|
|
25
|
+
} from '@graphorin/core';
|
|
26
|
+
import { SENSITIVITY_ORDER } from '@graphorin/core';
|
|
27
|
+
|
|
28
|
+
const SENSITIVITY_RANK: Record<Sensitivity, number> = {
|
|
29
|
+
public: 0,
|
|
30
|
+
internal: 1,
|
|
31
|
+
secret: 2,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return `true` iff the message's effective sensitivity is at most
|
|
36
|
+
* `maxTier` (a strictly weaker check than core's
|
|
37
|
+
* `acceptsSensitivity` because the filter library does not have
|
|
38
|
+
* the provider's `acceptsSensitivity[]` array - only a ceiling).
|
|
39
|
+
*/
|
|
40
|
+
function withinTier(record: Sensitivity, maxTier: Sensitivity): boolean {
|
|
41
|
+
return SENSITIVITY_RANK[record] <= SENSITIVITY_RANK[maxTier];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void SENSITIVITY_ORDER;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A `HandoffFilter` paired with the serializable descriptor that
|
|
48
|
+
* round-trips through the JSONL session export. Authors of custom
|
|
49
|
+
* filters return one of these via `filters.custom({...})`.
|
|
50
|
+
*
|
|
51
|
+
* @stable
|
|
52
|
+
*/
|
|
53
|
+
export interface DescribedFilter extends HandoffFilter {
|
|
54
|
+
readonly descriptor: HandoffInputFilterDescriptor;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function makeFilter(fn: HandoffFilter, descriptor: HandoffInputFilterDescriptor): DescribedFilter {
|
|
58
|
+
const wrapped = ((history: readonly Message[]): readonly Message[] =>
|
|
59
|
+
fn(history)) as DescribedFilter;
|
|
60
|
+
Object.defineProperty(wrapped, 'descriptor', { value: descriptor, enumerable: true });
|
|
61
|
+
return wrapped;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isReasoningPart(part: unknown): boolean {
|
|
65
|
+
if (typeof part !== 'object' || part === null) return false;
|
|
66
|
+
const t = (part as { readonly type?: unknown }).type;
|
|
67
|
+
return t === 'reasoning';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stripReasoningFromMessage(msg: Message): Message {
|
|
71
|
+
if (msg.role === 'system' || msg.role === 'tool') return msg;
|
|
72
|
+
const content = msg.content;
|
|
73
|
+
if (typeof content === 'string') return msg;
|
|
74
|
+
const filtered = content.filter((part) => !isReasoningPart(part));
|
|
75
|
+
if (filtered.length === content.length) return msg;
|
|
76
|
+
if (msg.role === 'assistant') {
|
|
77
|
+
return { ...msg, content: filtered };
|
|
78
|
+
}
|
|
79
|
+
return { ...msg, content: filtered };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Keep the parent's system prompt and the last `n` non-system
|
|
84
|
+
* messages. Default `n = 10` per DEC-146 / RB-40 security-first
|
|
85
|
+
* compose.
|
|
86
|
+
*
|
|
87
|
+
* @stable
|
|
88
|
+
*/
|
|
89
|
+
export function lastN(n = 10): DescribedFilter {
|
|
90
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
91
|
+
throw new RangeError(`filters.lastN: n must be a positive integer (got ${String(n)})`);
|
|
92
|
+
}
|
|
93
|
+
return makeFilter(
|
|
94
|
+
(history) => {
|
|
95
|
+
const out: Message[] = [];
|
|
96
|
+
for (const msg of history) {
|
|
97
|
+
if (msg.role === 'system') {
|
|
98
|
+
out.push(msg);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const nonSystem: Message[] = history.filter((m) => m.role !== 'system');
|
|
102
|
+
const tail = nonSystem.slice(Math.max(0, nonSystem.length - n));
|
|
103
|
+
return [...out, ...tail];
|
|
104
|
+
},
|
|
105
|
+
{ kind: 'last-n', meta: { n } },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keep only the parent's system prompt and the most recent user
|
|
111
|
+
* message. Useful for simple sub-agents that only need the question.
|
|
112
|
+
*
|
|
113
|
+
* @stable
|
|
114
|
+
*/
|
|
115
|
+
export function lastUser(): DescribedFilter {
|
|
116
|
+
return makeFilter(
|
|
117
|
+
(history) => {
|
|
118
|
+
const out: Message[] = [];
|
|
119
|
+
let lastUserIdx = -1;
|
|
120
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
121
|
+
const msg = history[i];
|
|
122
|
+
if (msg && msg.role === 'user') {
|
|
123
|
+
lastUserIdx = i;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const msg of history) {
|
|
128
|
+
if (msg.role === 'system') out.push(msg);
|
|
129
|
+
}
|
|
130
|
+
if (lastUserIdx >= 0) {
|
|
131
|
+
const m = history[lastUserIdx];
|
|
132
|
+
if (m) out.push(m);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
},
|
|
136
|
+
{ kind: 'last-user' },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The full unfiltered history. Discouraged - security-conscious
|
|
142
|
+
* callers should pick {@link lastN} or {@link bySensitivity} instead
|
|
143
|
+
* (a sub-agent rarely needs the parent's entire conversation).
|
|
144
|
+
*
|
|
145
|
+
* @stable
|
|
146
|
+
*/
|
|
147
|
+
export function full(): DescribedFilter {
|
|
148
|
+
return makeFilter((history) => history.slice(), { kind: 'full' });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Replace the parent's history with a single system message carrying
|
|
153
|
+
* the supplied summary. Used by callers that wire in an LLM-based
|
|
154
|
+
* summarizer outside the framework.
|
|
155
|
+
*
|
|
156
|
+
* @stable
|
|
157
|
+
*/
|
|
158
|
+
export function summary(text: string): DescribedFilter {
|
|
159
|
+
return makeFilter(
|
|
160
|
+
() => [
|
|
161
|
+
{
|
|
162
|
+
role: 'system',
|
|
163
|
+
content: `[Summary of parent conversation]\n${text}`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
{ kind: 'summary', meta: { summaryLength: text.length } },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Drop messages whose effective sensitivity ceiling exceeds
|
|
172
|
+
* `maxTier`. Messages without sensitivity metadata default to
|
|
173
|
+
* `'public'` and are always kept.
|
|
174
|
+
*
|
|
175
|
+
* The framework currently records sensitivity at the
|
|
176
|
+
* `MessageContent` part level via the `inboundTrust` / `secret`
|
|
177
|
+
* annotations. v0.1 ships a coarse-grained heuristic: a message is
|
|
178
|
+
* kept iff every text part's content does not contain the literal
|
|
179
|
+
* `[REDACTED:secret]` token AND every part's annotated sensitivity
|
|
180
|
+
* is acceptable to `maxTier`. Operators that need a stricter
|
|
181
|
+
* filter compose the function with `stripSensitiveOutputs()` or a
|
|
182
|
+
* custom predicate.
|
|
183
|
+
*
|
|
184
|
+
* @stable
|
|
185
|
+
*/
|
|
186
|
+
export function bySensitivity(args: { readonly maxTier?: Sensitivity } = {}): DescribedFilter {
|
|
187
|
+
const maxTier: Sensitivity = args.maxTier ?? 'public';
|
|
188
|
+
return makeFilter(
|
|
189
|
+
(history) => {
|
|
190
|
+
const out: Message[] = [];
|
|
191
|
+
for (const msg of history) {
|
|
192
|
+
const content = msg.role === 'system' ? msg.content : msg.content;
|
|
193
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content);
|
|
194
|
+
if (text.includes('[REDACTED:secret]') && !withinTier('secret', maxTier)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
out.push(msg);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
},
|
|
201
|
+
{ kind: 'sensitivity-filter', meta: { maxTier } },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Strip every `ReasoningContent` part from each message. Always
|
|
207
|
+
* applied at the handoff boundary (the `compose(...)` helper appends
|
|
208
|
+
* this filter automatically).
|
|
209
|
+
*
|
|
210
|
+
* @stable
|
|
211
|
+
*/
|
|
212
|
+
export function stripReasoning(): DescribedFilter {
|
|
213
|
+
return makeFilter((history) => history.map(stripReasoningFromMessage), {
|
|
214
|
+
kind: 'strip-reasoning',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Strip tool messages whose `content` carries the literal token
|
|
220
|
+
* `[REDACTED:secret]` or whose `secret` annotation marks the body as
|
|
221
|
+
* sensitive. Conservative-by-design: the agent runtime tags
|
|
222
|
+
* sensitive tool outputs at session-write time so this filter has
|
|
223
|
+
* stable bytes to scan against.
|
|
224
|
+
*
|
|
225
|
+
* @stable
|
|
226
|
+
*/
|
|
227
|
+
export function stripSensitiveOutputs(): DescribedFilter {
|
|
228
|
+
return makeFilter(
|
|
229
|
+
(history) => {
|
|
230
|
+
const out: Message[] = [];
|
|
231
|
+
let stripped = 0;
|
|
232
|
+
for (const msg of history) {
|
|
233
|
+
if (msg.role === 'tool') {
|
|
234
|
+
const text = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
235
|
+
if (text.includes('[REDACTED:')) {
|
|
236
|
+
stripped += 1;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
out.push(msg);
|
|
241
|
+
}
|
|
242
|
+
void stripped;
|
|
243
|
+
return out;
|
|
244
|
+
},
|
|
245
|
+
{ kind: 'strip-sensitive-outputs' },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Drop every assistant `toolCalls` array AND every `tool` message.
|
|
251
|
+
* Useful when a sub-agent should only see the textual conversation.
|
|
252
|
+
*
|
|
253
|
+
* @stable
|
|
254
|
+
*/
|
|
255
|
+
export function stripToolCalls(): DescribedFilter {
|
|
256
|
+
return makeFilter(
|
|
257
|
+
(history) => {
|
|
258
|
+
const out: Message[] = [];
|
|
259
|
+
for (const msg of history) {
|
|
260
|
+
if (msg.role === 'tool') continue;
|
|
261
|
+
if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
262
|
+
const { toolCalls: _toolCalls, ...rest } = msg;
|
|
263
|
+
out.push(rest);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
out.push(msg);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
},
|
|
270
|
+
{ kind: 'strip-tool-calls' },
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Compose multiple filters left-to-right. The composer **always**
|
|
276
|
+
* appends `stripReasoning()` at the end so reasoning content never
|
|
277
|
+
* crosses a handoff boundary regardless of caller intent.
|
|
278
|
+
*
|
|
279
|
+
* @stable
|
|
280
|
+
*/
|
|
281
|
+
export function compose(...filters: ReadonlyArray<HandoffFilter>): DescribedFilter {
|
|
282
|
+
const stripper = stripReasoning();
|
|
283
|
+
const ordered: ReadonlyArray<HandoffFilter> = [...filters, stripper];
|
|
284
|
+
const descriptors: HandoffInputFilterDescriptor[] = filters.map((f) => {
|
|
285
|
+
const desc = (f as DescribedFilter).descriptor;
|
|
286
|
+
return desc ?? { kind: 'custom' };
|
|
287
|
+
});
|
|
288
|
+
descriptors.push(stripper.descriptor);
|
|
289
|
+
return makeFilter(
|
|
290
|
+
(history) => {
|
|
291
|
+
let current: readonly Message[] = history;
|
|
292
|
+
for (const filter of ordered) {
|
|
293
|
+
current = filter(current);
|
|
294
|
+
}
|
|
295
|
+
return current;
|
|
296
|
+
},
|
|
297
|
+
{ kind: 'compose', meta: { steps: descriptors } },
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Wrap a caller-supplied function as a {@link DescribedFilter} with
|
|
303
|
+
* the canonical `'custom'` descriptor.
|
|
304
|
+
*
|
|
305
|
+
* @stable
|
|
306
|
+
*/
|
|
307
|
+
export function custom(
|
|
308
|
+
fn: HandoffFilter,
|
|
309
|
+
meta?: Readonly<Record<string, unknown>>,
|
|
310
|
+
): DescribedFilter {
|
|
311
|
+
return makeFilter(fn, meta !== undefined ? { kind: 'custom', meta } : { kind: 'custom' });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The canonical default applied by the agent runtime to every
|
|
316
|
+
* `Agent.toTool(...)` and `handoff(...)` invocation when the caller
|
|
317
|
+
* does not supply an explicit filter.
|
|
318
|
+
*
|
|
319
|
+
* @stable
|
|
320
|
+
*/
|
|
321
|
+
export function defaultHandoffFilter(): DescribedFilter {
|
|
322
|
+
return compose(lastN(10), stripSensitiveOutputs());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Pure `HandoffInputFilterDescriptor` for callers that just need the
|
|
327
|
+
* descriptor without instantiating the runtime function (e.g. the
|
|
328
|
+
* sessions package's lenient-forward-parse path).
|
|
329
|
+
*
|
|
330
|
+
* @stable
|
|
331
|
+
*/
|
|
332
|
+
export const FILTER_KIND_CUSTOM: HandoffInputFilterDescriptor = { kind: 'custom' };
|
|
333
|
+
|
|
334
|
+
/** Aggregate module export. */
|
|
335
|
+
export const filters = {
|
|
336
|
+
lastN,
|
|
337
|
+
lastUser,
|
|
338
|
+
full,
|
|
339
|
+
summary,
|
|
340
|
+
bySensitivity,
|
|
341
|
+
stripReasoning,
|
|
342
|
+
stripSensitiveOutputs,
|
|
343
|
+
stripToolCalls,
|
|
344
|
+
compose,
|
|
345
|
+
custom,
|
|
346
|
+
defaultHandoffFilter,
|
|
347
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@graphorin/agent` - agent runtime for the Graphorin framework.
|
|
3
|
+
*
|
|
4
|
+
* The package owns:
|
|
5
|
+
*
|
|
6
|
+
* - The `createAgent({...})` factory that wires the typed
|
|
7
|
+
* `model -> tool calls -> model` loop, the streaming event
|
|
8
|
+
* surface, the steering / followUp queues, durable HITL
|
|
9
|
+
* approvals, multi-agent handoffs (`Agent.toTool`, the filter
|
|
10
|
+
* library, secrets isolation), the agent-level model fallback
|
|
11
|
+
* chain, the per-step compaction lifecycle, the per-tool
|
|
12
|
+
* preferred-model resolution, the structured progress-artifact
|
|
13
|
+
* APIs, and the lateral-leak defense layer.
|
|
14
|
+
* - `runStateToJSON / runStateFromJSON` helpers for caller-managed
|
|
15
|
+
* durable HITL.
|
|
16
|
+
* - The handoff filter library.
|
|
17
|
+
* - The agent-step-level fan-out helpers (`runFanOut`,
|
|
18
|
+
* `evaluatorOptimizer`, the progress IO surface).
|
|
19
|
+
* - Pure decision functions consulted by the loop
|
|
20
|
+
* (`isAgentFallbackEligible`, `resolvePreferredModel`).
|
|
21
|
+
* - Lateral-leak primitives (`CausalityMonitor`,
|
|
22
|
+
* `MergeAgentSidewaysInjectionGuard` decision helpers,
|
|
23
|
+
* protocol-injection guard).
|
|
24
|
+
*
|
|
25
|
+
* The full documentation lives in the package `README.md`.
|
|
26
|
+
*
|
|
27
|
+
* @packageDocumentation
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Canonical version constant, derived from `package.json` at build time. */
|
|
31
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
32
|
+
|
|
33
|
+
export const VERSION: string = pkg.version;
|
|
34
|
+
|
|
35
|
+
// AG-2 / SDF-4: the canonical guardrail contract lives in
|
|
36
|
+
// `@graphorin/security`; re-exported here for config ergonomics.
|
|
37
|
+
export type {
|
|
38
|
+
GuardrailContext,
|
|
39
|
+
GuardrailDefinition,
|
|
40
|
+
GuardrailResult,
|
|
41
|
+
InputGuardrail,
|
|
42
|
+
OutputGuardrail,
|
|
43
|
+
} from '@graphorin/security/guardrails';
|
|
44
|
+
export {
|
|
45
|
+
AgentResolutionError,
|
|
46
|
+
AgentRuntimeError,
|
|
47
|
+
type AgentRuntimeErrorCode,
|
|
48
|
+
EvaluatorOptimizerConfigError,
|
|
49
|
+
InvalidAgentConfigError,
|
|
50
|
+
InvalidPreferredModelError,
|
|
51
|
+
MergeBlockedError,
|
|
52
|
+
MultipleHandoffsInStepError,
|
|
53
|
+
ProgressWriteError,
|
|
54
|
+
ProtocolInjectionRejectError,
|
|
55
|
+
ProviderMiddlewareOrderError,
|
|
56
|
+
RunStateMalformedError,
|
|
57
|
+
RunStateVersionUnsupportedError,
|
|
58
|
+
SubAgentResumeTargetNotFoundError,
|
|
59
|
+
ToolNotFoundError,
|
|
60
|
+
} from './errors/index.js';
|
|
61
|
+
export {
|
|
62
|
+
type EvaluatorCallable,
|
|
63
|
+
type EvaluatorOptimizerOptions,
|
|
64
|
+
type EvaluatorOptimizerOutcome,
|
|
65
|
+
type EvaluatorOutcome,
|
|
66
|
+
evaluatorOptimizer,
|
|
67
|
+
type GeneratorCallable,
|
|
68
|
+
type Rubric,
|
|
69
|
+
} from './evaluator-optimizer/index.js';
|
|
70
|
+
export { createAgent } from './factory.js';
|
|
71
|
+
export {
|
|
72
|
+
type AgentFallbackEligibility,
|
|
73
|
+
type AgentFallbackPolicy,
|
|
74
|
+
type AgentFallbackReason,
|
|
75
|
+
isAgentFallbackEligible,
|
|
76
|
+
} from './fallback/index.js';
|
|
77
|
+
export {
|
|
78
|
+
type ChildResult,
|
|
79
|
+
type FanOutOptions,
|
|
80
|
+
type FanOutResult,
|
|
81
|
+
type MergeStrategy,
|
|
82
|
+
type PerChildBudget,
|
|
83
|
+
runFanOut,
|
|
84
|
+
} from './fanout/index.js';
|
|
85
|
+
export {
|
|
86
|
+
bySensitivity,
|
|
87
|
+
compose,
|
|
88
|
+
custom,
|
|
89
|
+
type DescribedFilter,
|
|
90
|
+
defaultHandoffFilter,
|
|
91
|
+
FILTER_KIND_CUSTOM,
|
|
92
|
+
filters,
|
|
93
|
+
full,
|
|
94
|
+
lastN,
|
|
95
|
+
lastUser,
|
|
96
|
+
stripReasoning,
|
|
97
|
+
stripSensitiveOutputs,
|
|
98
|
+
stripToolCalls,
|
|
99
|
+
summary,
|
|
100
|
+
} from './filters/index.js';
|
|
101
|
+
export {
|
|
102
|
+
CausalityMonitor,
|
|
103
|
+
type CausalityMonitorCheck,
|
|
104
|
+
type CausalityMonitorConfig,
|
|
105
|
+
type CausalityMonitorStrictness,
|
|
106
|
+
type ChildTrustInput,
|
|
107
|
+
type ContentOriginKind,
|
|
108
|
+
computeSourceTrust,
|
|
109
|
+
DEFAULT_DENIAL_PATTERNS,
|
|
110
|
+
DEFAULT_MAX_CHAIN_DEPTH,
|
|
111
|
+
evaluateMerge,
|
|
112
|
+
type GuardOutcome,
|
|
113
|
+
guardOutboundContent,
|
|
114
|
+
type MergeBiasDecision,
|
|
115
|
+
type MergeGuardConfig,
|
|
116
|
+
type ProtocolBoundary,
|
|
117
|
+
type ProtocolEscapePolicy,
|
|
118
|
+
type ProtocolGuardConfig,
|
|
119
|
+
resolvePolicy,
|
|
120
|
+
type TrustClass,
|
|
121
|
+
} from './lateral-leak/index.js';
|
|
122
|
+
export {
|
|
123
|
+
type PreferredModelResolution,
|
|
124
|
+
pickTopTierAcrossTools,
|
|
125
|
+
type ResolvePreferredModelInput,
|
|
126
|
+
resolvePreferredModel,
|
|
127
|
+
} from './preferred-model/index.js';
|
|
128
|
+
export {
|
|
129
|
+
createProgressIO,
|
|
130
|
+
type ProgressIO,
|
|
131
|
+
type ProgressIOConfig,
|
|
132
|
+
type ProgressReadOptions,
|
|
133
|
+
type ProgressWriteOptions,
|
|
134
|
+
} from './progress/index.js';
|
|
135
|
+
export {
|
|
136
|
+
addModelUsage,
|
|
137
|
+
aggregateUsageFromByModel,
|
|
138
|
+
completedToolCallsFromState,
|
|
139
|
+
createInitialRunState,
|
|
140
|
+
deserializeRunState,
|
|
141
|
+
RUN_STATE_SCHEMA_MAJOR_SUPPORTED,
|
|
142
|
+
RUN_STATE_SCHEMA_VERSION,
|
|
143
|
+
runStateFromJSON,
|
|
144
|
+
runStateToJSON,
|
|
145
|
+
type SerializedPendingSubRun,
|
|
146
|
+
type SerializedRunState,
|
|
147
|
+
type SerializeRunStateOptions,
|
|
148
|
+
serializeRunState,
|
|
149
|
+
} from './run-state/index.js';
|
|
150
|
+
export {
|
|
151
|
+
getSubAgentToolRefs,
|
|
152
|
+
SUBAGENT_TOOL,
|
|
153
|
+
type SubAgentFoldTaint,
|
|
154
|
+
type SubAgentToolRefs,
|
|
155
|
+
} from './runtime/agent-to-tool.js';
|
|
156
|
+
export { foldChildRunUsage } from './runtime/messages.js';
|
|
157
|
+
export {
|
|
158
|
+
createReplayProvider,
|
|
159
|
+
type ReplayProviderOptions,
|
|
160
|
+
} from './testing/replay-provider.js';
|
|
161
|
+
export type {
|
|
162
|
+
AbortOptions,
|
|
163
|
+
Agent,
|
|
164
|
+
AgentCallOptions,
|
|
165
|
+
AgentCapability,
|
|
166
|
+
AgentConfig,
|
|
167
|
+
AgentInput,
|
|
168
|
+
AgentToToolOptions,
|
|
169
|
+
CompactionApiResult,
|
|
170
|
+
CompactOptions,
|
|
171
|
+
HandoffEntry,
|
|
172
|
+
OutputSpec,
|
|
173
|
+
PostCompactionHook,
|
|
174
|
+
PrepareStepHook,
|
|
175
|
+
PrepareStepOverrides,
|
|
176
|
+
ResponseVerifier,
|
|
177
|
+
ResumeDirective,
|
|
178
|
+
SkillsRegistryLike,
|
|
179
|
+
VerifierResult,
|
|
180
|
+
} from './types.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny id helpers used by the agent runtime. URL-safe Base32 alphabet,
|
|
3
|
+
* monotonic timestamp prefix to keep ids loosely sortable.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
11
|
+
|
|
12
|
+
function pad(value: string, width: number): string {
|
|
13
|
+
return value.padStart(width, '0');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 7-character Base32 tail backed by 35 bits of CSPRNG entropy. We read
|
|
18
|
+
* 5 random bytes (40 bits) and consume 5 bits per emitted character
|
|
19
|
+
* MSB-first; the leftover bits are discarded. Sourcing from
|
|
20
|
+
* `randomBytes` (uniform over `[0, 256)`) guarantees a uniform
|
|
21
|
+
* distribution over `ALPHABET` - no modulo bias.
|
|
22
|
+
*/
|
|
23
|
+
function randomTail(): string {
|
|
24
|
+
const buf = randomBytes(5);
|
|
25
|
+
let bitBuffer = 0;
|
|
26
|
+
let bitCount = 0;
|
|
27
|
+
let cursor = 0;
|
|
28
|
+
let out = '';
|
|
29
|
+
for (let i = 0; i < 7; i++) {
|
|
30
|
+
while (bitCount < 5) {
|
|
31
|
+
bitBuffer = (bitBuffer << 8) | (buf[cursor++] ?? 0);
|
|
32
|
+
bitCount += 8;
|
|
33
|
+
}
|
|
34
|
+
bitCount -= 5;
|
|
35
|
+
const idx = (bitBuffer >>> bitCount) & 0x1f;
|
|
36
|
+
out += ALPHABET[idx];
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Generate a fresh, sortable, URL-safe identifier. */
|
|
42
|
+
export function newId(prefix?: string): string {
|
|
43
|
+
const ts = pad(Date.now().toString(32).toUpperCase(), 9);
|
|
44
|
+
const id = `${ts}${randomTail()}`;
|
|
45
|
+
return prefix ? `${prefix}_${id}` : id;
|
|
46
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal {@link UsageAccumulator} implementation used by the agent
|
|
3
|
+
* runtime when the consumer does not pass a custom one through
|
|
4
|
+
* `RunContext.usage`. Tracks per-model breakdown and a running
|
|
5
|
+
* aggregate.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ModelUsage, Usage, UsageAccumulator, UsageSnapshot } from '@graphorin/core';
|
|
11
|
+
|
|
12
|
+
export class InMemoryUsageAccumulator implements UsageAccumulator {
|
|
13
|
+
#aggregate: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
14
|
+
readonly #byModel = new Map<string, ModelUsage>();
|
|
15
|
+
|
|
16
|
+
get total(): Usage {
|
|
17
|
+
return this.#aggregate;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get byModel(): ReadonlyMap<string, ModelUsage> {
|
|
21
|
+
return this.#byModel;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
add(modelId: string, usage: Usage): void {
|
|
25
|
+
const prev = this.#byModel.get(modelId);
|
|
26
|
+
if (prev === undefined) {
|
|
27
|
+
this.#byModel.set(modelId, {
|
|
28
|
+
modelId,
|
|
29
|
+
promptTokens: usage.promptTokens,
|
|
30
|
+
completionTokens: usage.completionTokens,
|
|
31
|
+
totalTokens: usage.totalTokens,
|
|
32
|
+
callCount: 1,
|
|
33
|
+
...(usage.reasoningTokens !== undefined ? { reasoningTokens: usage.reasoningTokens } : {}),
|
|
34
|
+
...(usage.cachedReadTokens !== undefined
|
|
35
|
+
? { cachedReadTokens: usage.cachedReadTokens }
|
|
36
|
+
: {}),
|
|
37
|
+
...(usage.cacheWriteTokens !== undefined
|
|
38
|
+
? { cacheWriteTokens: usage.cacheWriteTokens }
|
|
39
|
+
: {}),
|
|
40
|
+
...(usage.cost !== undefined ? { cost: usage.cost } : {}),
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
const merged: ModelUsage = {
|
|
44
|
+
modelId,
|
|
45
|
+
promptTokens: prev.promptTokens + usage.promptTokens,
|
|
46
|
+
completionTokens: prev.completionTokens + usage.completionTokens,
|
|
47
|
+
totalTokens: prev.totalTokens + usage.totalTokens,
|
|
48
|
+
callCount: prev.callCount + 1,
|
|
49
|
+
...sumOptional('reasoningTokens', prev.reasoningTokens, usage.reasoningTokens),
|
|
50
|
+
...sumOptional('cachedReadTokens', prev.cachedReadTokens, usage.cachedReadTokens),
|
|
51
|
+
...sumOptional('cacheWriteTokens', prev.cacheWriteTokens, usage.cacheWriteTokens),
|
|
52
|
+
};
|
|
53
|
+
this.#byModel.set(modelId, merged);
|
|
54
|
+
}
|
|
55
|
+
this.#aggregate = {
|
|
56
|
+
promptTokens: this.#aggregate.promptTokens + usage.promptTokens,
|
|
57
|
+
completionTokens: this.#aggregate.completionTokens + usage.completionTokens,
|
|
58
|
+
totalTokens: this.#aggregate.totalTokens + usage.totalTokens,
|
|
59
|
+
...sumOptional('reasoningTokens', this.#aggregate.reasoningTokens, usage.reasoningTokens),
|
|
60
|
+
...sumOptional('cachedReadTokens', this.#aggregate.cachedReadTokens, usage.cachedReadTokens),
|
|
61
|
+
...sumOptional('cacheWriteTokens', this.#aggregate.cacheWriteTokens, usage.cacheWriteTokens),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reset(): void {
|
|
66
|
+
this.#aggregate = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
67
|
+
this.#byModel.clear();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
snapshot(): UsageSnapshot {
|
|
71
|
+
return {
|
|
72
|
+
total: { ...this.#aggregate },
|
|
73
|
+
byModel: Array.from(this.#byModel.values()).map((m) => ({ ...m })),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sum an optional token field: present in the output only when at least
|
|
80
|
+
* one side carries it (so a run that never saw the field keeps the same
|
|
81
|
+
* serialized shape as before the field existed).
|
|
82
|
+
*/
|
|
83
|
+
function sumOptional<K extends string>(
|
|
84
|
+
key: K,
|
|
85
|
+
a: number | undefined,
|
|
86
|
+
b: number | undefined,
|
|
87
|
+
): Partial<Record<K, number>> {
|
|
88
|
+
if (a === undefined && b === undefined) return {};
|
|
89
|
+
return { [key]: (a ?? 0) + (b ?? 0) } as Partial<Record<K, number>>;
|
|
90
|
+
}
|