@graphorin/agent 0.5.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 +7 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/errors/index.d.ts +170 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +204 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/evaluator-optimizer/index.d.ts +91 -0
- package/dist/evaluator-optimizer/index.d.ts.map +1 -0
- package/dist/evaluator-optimizer/index.js +85 -0
- package/dist/evaluator-optimizer/index.js.map +1 -0
- package/dist/factory.d.ts +13 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +1853 -0
- package/dist/factory.js.map +1 -0
- package/dist/fallback/index.d.ts +52 -0
- package/dist/fallback/index.d.ts.map +1 -0
- package/dist/fallback/index.js +53 -0
- package/dist/fallback/index.js.map +1 -0
- package/dist/fanout/index.d.ts +142 -0
- package/dist/fanout/index.d.ts.map +1 -0
- package/dist/fanout/index.js +252 -0
- package/dist/fanout/index.js.map +1 -0
- package/dist/filters/index.d.ts +137 -0
- package/dist/filters/index.d.ts.map +1 -0
- package/dist/filters/index.js +273 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/ids.js +46 -0
- package/dist/internal/ids.js.map +1 -0
- package/dist/internal/usage-accumulator.js +62 -0
- package/dist/internal/usage-accumulator.js.map +1 -0
- package/dist/lateral-leak/causality-monitor.d.ts +97 -0
- package/dist/lateral-leak/causality-monitor.d.ts.map +1 -0
- package/dist/lateral-leak/causality-monitor.js +139 -0
- package/dist/lateral-leak/causality-monitor.js.map +1 -0
- package/dist/lateral-leak/index.d.ts +4 -0
- package/dist/lateral-leak/index.js +5 -0
- package/dist/lateral-leak/merge-guard.d.ts +89 -0
- package/dist/lateral-leak/merge-guard.d.ts.map +1 -0
- package/dist/lateral-leak/merge-guard.js +65 -0
- package/dist/lateral-leak/merge-guard.js.map +1 -0
- package/dist/lateral-leak/protocol-guard.d.ts +76 -0
- package/dist/lateral-leak/protocol-guard.d.ts.map +1 -0
- package/dist/lateral-leak/protocol-guard.js +147 -0
- package/dist/lateral-leak/protocol-guard.js.map +1 -0
- package/dist/preferred-model/index.d.ts +53 -0
- package/dist/preferred-model/index.d.ts.map +1 -0
- package/dist/preferred-model/index.js +141 -0
- package/dist/preferred-model/index.js.map +1 -0
- package/dist/progress/index.d.ts +62 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/index.js +150 -0
- package/dist/progress/index.js.map +1 -0
- package/dist/run-state/index.d.ts +152 -0
- package/dist/run-state/index.d.ts.map +1 -0
- package/dist/run-state/index.js +311 -0
- package/dist/run-state/index.js.map +1 -0
- package/dist/tooling/adapters.js +154 -0
- package/dist/tooling/adapters.js.map +1 -0
- package/dist/tooling/catalogue.js +37 -0
- package/dist/tooling/catalogue.js.map +1 -0
- package/dist/tooling/dataflow.js +99 -0
- package/dist/tooling/dataflow.js.map +1 -0
- package/dist/tooling/registry-build.js +85 -0
- package/dist/tooling/registry-build.js.map +1 -0
- package/dist/types.d.ts +413 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +115 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ContentOriginKind, MergeGuardConfig, TrustClass } from "../lateral-leak/merge-guard.js";
|
|
2
|
+
import { AgentEvent } from "@graphorin/core";
|
|
3
|
+
|
|
4
|
+
//#region src/fanout/index.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-child budget. Defaults derived from the canonical 2026
|
|
8
|
+
* scaling-rule table for agent fan-out workloads.
|
|
9
|
+
*
|
|
10
|
+
* @stable
|
|
11
|
+
*/
|
|
12
|
+
interface PerChildBudget {
|
|
13
|
+
/**
|
|
14
|
+
* Max `usage.totalTokens` per child. Enforced **post-hoc** and only
|
|
15
|
+
* for usage-reporting children (an `invoke` that resolves to a full
|
|
16
|
+
* `AgentResult` — e.g. `() => child.run(input)`); a child returning a
|
|
17
|
+
* plain value reports `tokensUsed: 0` and this cap cannot fire.
|
|
18
|
+
*/
|
|
19
|
+
readonly tokens?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Max tool calls per child. Same usage-reporting contract as
|
|
22
|
+
* {@link PerChildBudget.tokens} (counted from `state.steps`).
|
|
23
|
+
*/
|
|
24
|
+
readonly toolCalls?: number;
|
|
25
|
+
/** Wall-clock cap, enforced for every child via a race timer. */
|
|
26
|
+
readonly durationMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Built-in merge-strategy taxonomy.
|
|
30
|
+
*
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
type MergeStrategy<TOutput = unknown> = {
|
|
34
|
+
readonly kind: 'concat';
|
|
35
|
+
readonly separator?: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: 'first-success';
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: 'judge-merge';
|
|
40
|
+
readonly judge: (children: ReadonlyArray<ChildResult<TOutput>>) => Promise<TOutput>;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: 'custom';
|
|
43
|
+
readonly merge: (children: ReadonlyArray<ChildResult<TOutput>>) => Promise<TOutput>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Per-child outcome surfaced on
|
|
47
|
+
* {@link FanOutResult.children}. Failed-child isolation: a child
|
|
48
|
+
* that throws produces a `ChildResult` with `status: 'failed'` —
|
|
49
|
+
* never an exception thrown from the fan-out call itself.
|
|
50
|
+
*
|
|
51
|
+
* @stable
|
|
52
|
+
*/
|
|
53
|
+
interface ChildResult<TOutput = unknown> {
|
|
54
|
+
readonly agentId: string;
|
|
55
|
+
readonly status: 'completed' | 'failed' | 'budget-exceeded' | 'cancelled';
|
|
56
|
+
readonly output?: TOutput;
|
|
57
|
+
readonly error?: {
|
|
58
|
+
readonly message: string;
|
|
59
|
+
readonly code: string;
|
|
60
|
+
};
|
|
61
|
+
readonly tokensUsed: number;
|
|
62
|
+
readonly toolCallCount: number;
|
|
63
|
+
readonly durationMs: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Aggregate result returned by `Agent.fanOut(...)`.
|
|
67
|
+
*
|
|
68
|
+
* @stable
|
|
69
|
+
*/
|
|
70
|
+
interface FanOutResult<TOutput = unknown> {
|
|
71
|
+
readonly fanOutId: string;
|
|
72
|
+
readonly output: TOutput;
|
|
73
|
+
readonly children: ReadonlyArray<ChildResult<TOutput>>;
|
|
74
|
+
readonly mergeDurationMs: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Per-call options accepted by `Agent.fanOut(...)`.
|
|
78
|
+
*
|
|
79
|
+
* @stable
|
|
80
|
+
*/
|
|
81
|
+
interface FanOutOptions<TOutput = unknown> {
|
|
82
|
+
/**
|
|
83
|
+
* The sub-agents to invoke. Each entry is invoked as a function
|
|
84
|
+
* returning a `Promise<TOutput>` — the fan-out helper does not
|
|
85
|
+
* impose an `Agent` shape on the children so the runtime can
|
|
86
|
+
* adapt any callable surface.
|
|
87
|
+
*/
|
|
88
|
+
readonly children: ReadonlyArray<{
|
|
89
|
+
readonly agentId: string;
|
|
90
|
+
/**
|
|
91
|
+
* Child callable. Resolve to a plain `TOutput`, or to a full
|
|
92
|
+
* `AgentResult` (e.g. `() => childAgent.run(input)`) — the fan-out
|
|
93
|
+
* detects the result envelope structurally (`output` + numeric
|
|
94
|
+
* `usage.totalTokens` + `state`), unwraps `output`, and harvests
|
|
95
|
+
* `tokensUsed` / `toolCallCount` so per-child budgets can enforce.
|
|
96
|
+
*/
|
|
97
|
+
readonly invoke: () => Promise<TOutput>;
|
|
98
|
+
/** Trust-class for the merge guard (default `'loopback'`). */
|
|
99
|
+
readonly trustClass?: TrustClass;
|
|
100
|
+
/** Content-origin for the merge guard (default `'built-in'`). */
|
|
101
|
+
readonly origin?: ContentOriginKind;
|
|
102
|
+
/** Rolling trust adjustment in `[0,1]` (default `1`). */
|
|
103
|
+
readonly historyAdjustment?: number;
|
|
104
|
+
}>;
|
|
105
|
+
/** Default `4` per the canonical 2026 production lesson. */
|
|
106
|
+
readonly maxConcurrentChildren?: number;
|
|
107
|
+
/** Per-child budget; default unset. */
|
|
108
|
+
readonly perBudget?: PerChildBudget;
|
|
109
|
+
/** Default `{ kind: 'concat' }`. */
|
|
110
|
+
readonly mergeStrategy?: MergeStrategy<TOutput>;
|
|
111
|
+
readonly signal?: AbortSignal;
|
|
112
|
+
/** Optional callback for per-child completion observability. */
|
|
113
|
+
readonly onChildResult?: (result: ChildResult<TOutput>) => void;
|
|
114
|
+
/** Optional event emitter for `agent.fanout.spawned / merged`. */
|
|
115
|
+
readonly emit?: (event: AgentEvent) => void;
|
|
116
|
+
/**
|
|
117
|
+
* Sideways-injection merge guard (AG-7): on `'judge-merge'` the
|
|
118
|
+
* fan-out scores each child's source trust and contribution weight
|
|
119
|
+
* against the judge's merged output; a biased merge emits
|
|
120
|
+
* `agent.lateral-leak.detected` (vector `sideways-injection`) and —
|
|
121
|
+
* under `strictness: 'detect-and-block'` — throws
|
|
122
|
+
* {@link MergeBlockedError}.
|
|
123
|
+
*/
|
|
124
|
+
readonly mergeGuard?: MergeGuardConfig;
|
|
125
|
+
/** Identifiers required to populate the events. */
|
|
126
|
+
readonly runId: string;
|
|
127
|
+
readonly sessionId: string;
|
|
128
|
+
readonly agentId: string;
|
|
129
|
+
/** Default — generated from `runId + Date.now()`. */
|
|
130
|
+
readonly fanOutId?: string;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Run a fan-out and produce the aggregate {@link FanOutResult}.
|
|
134
|
+
* Pure with respect to side effects — the runtime emits events /
|
|
135
|
+
* audit rows / counter increments via the supplied `emit` callback.
|
|
136
|
+
*
|
|
137
|
+
* @stable
|
|
138
|
+
*/
|
|
139
|
+
declare function runFanOut<TOutput>(opts: FanOutOptions<TOutput>): Promise<FanOutResult<TOutput>>;
|
|
140
|
+
//#endregion
|
|
141
|
+
export { ChildResult, FanOutOptions, FanOutResult, MergeStrategy, PerChildBudget, runFanOut };
|
|
142
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/fanout/index.ts"],"sourcesContent":[],"mappings":";;;;;;AA0EA;AAeA;;;;AAGqB,UA5DJ,cAAA,CA4DI;EAAa;AASlC;;;;;EAOqB,SAAA,MAAA,CAAA,EAAA,MAAA;EAoBE;;;;EAKyB,SAAA,SAAA,CAAA,EAAA,MAAA;EAAZ;EAEV,SAAA,UAAA,CAAA,EAAA,MAAA;;;AAkM1B;;;;AAEW,KArRC,aAqRD,CAAA,UAAA,OAAA,CAAA,GAAA;EAAR,SAAA,IAAA,EAAA,QAAA;EAAO,SAAA,SAAA,CAAA,EAAA,MAAA;;;;;6BAhRuB,cAAc,YAAY,cAAc,QAAQ;;;6BAIhD,cAAc,YAAY,cAAc,QAAQ;;;;;;;;;;UAWhE;;;oBAGG;;;;;;;;;;;;;;UAYH;;mBAEE;qBACE,cAAc,YAAY;;;;;;;;UAS9B;;;;;;;qBAOI;;;;;;;;;2BASM,QAAQ;;0BAET;;sBAEJ;;;;;;;uBAOC;;2BAEI,cAAc;oBACrB;;oCAEgB,YAAY;;0BAEtB;;;;;;;;;wBASF;;;;;;;;;;;;;;;iBAyLF,yBACd,cAAc,WACnB,QAAQ,aAAa"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { MergeBlockedError } from "../errors/index.js";
|
|
2
|
+
import { computeSourceTrust, evaluateMerge } from "../lateral-leak/merge-guard.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
//#region src/fanout/index.ts
|
|
6
|
+
/**
|
|
7
|
+
* Agent-step-level fan-out — `Agent.fanOut(...)` convenience that
|
|
8
|
+
* spawns N sub-agents in parallel under a bounded-fanout cap with
|
|
9
|
+
* per-child budgets and four built-in merge strategies.
|
|
10
|
+
*
|
|
11
|
+
* Boundary discipline against the workflow `Dispatch(...)`
|
|
12
|
+
* primitive: fan-out is **agent-step-level inline result** (children
|
|
13
|
+
* share parent `RunContext` lineage; result consumed by parent's
|
|
14
|
+
* continuing loop within one or few `agent.run(...)` calls).
|
|
15
|
+
* `Dispatch(...)` is workflow-step-level checkpointed durable graph.
|
|
16
|
+
* The two compose orthogonally.
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_MAX_CONCURRENT_CHILDREN = 4;
|
|
21
|
+
/**
|
|
22
|
+
* Structurally detect a full `AgentResult` returned by a child
|
|
23
|
+
* `invoke` (AG-16): an object carrying `output`, a numeric
|
|
24
|
+
* `usage.totalTokens`, and a `state` with string `id`/`status`. The
|
|
25
|
+
* three-field match makes accidental collision with a user `TOutput`
|
|
26
|
+
* negligible; children whose genuine output looks like this should
|
|
27
|
+
* resolve a plain value instead.
|
|
28
|
+
*/
|
|
29
|
+
function harvestAgentResult(value) {
|
|
30
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
31
|
+
const v = value;
|
|
32
|
+
if (!("output" in v)) return void 0;
|
|
33
|
+
if (typeof v.usage?.totalTokens !== "number") return void 0;
|
|
34
|
+
if (typeof v.state?.id !== "string" || typeof v.state.status !== "string") return void 0;
|
|
35
|
+
let toolCallCount = 0;
|
|
36
|
+
if (Array.isArray(v.state.steps)) for (const step of v.state.steps) {
|
|
37
|
+
const calls = step.toolCalls;
|
|
38
|
+
if (Array.isArray(calls)) toolCallCount += calls.length;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
output: v.output,
|
|
42
|
+
tokensUsed: v.usage.totalTokens,
|
|
43
|
+
toolCallCount
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whitespace-token overlap of a child's output against the merged
|
|
48
|
+
* output, in `[0,1]` — the contribution-weight estimate the merge
|
|
49
|
+
* guard's docstring contracts (each child scored independently; a
|
|
50
|
+
* judge parroting one child verbatim yields ~1.0 for that child).
|
|
51
|
+
*/
|
|
52
|
+
function contributionWeight(childText, mergedText) {
|
|
53
|
+
if (childText.length === 0 || mergedText.length === 0) return 0;
|
|
54
|
+
const mergedTokens = mergedText.split(/\s+/).filter((t) => t.length > 0);
|
|
55
|
+
if (mergedTokens.length === 0) return 0;
|
|
56
|
+
const childTokens = new Set(childText.split(/\s+/).filter((t) => t.length > 0));
|
|
57
|
+
let hits = 0;
|
|
58
|
+
for (const token of mergedTokens) if (childTokens.has(token)) hits += 1;
|
|
59
|
+
return hits / mergedTokens.length;
|
|
60
|
+
}
|
|
61
|
+
async function runWithSemaphore(children, cap, signal, perBudget, onChildResult) {
|
|
62
|
+
const results = new Array(children.length);
|
|
63
|
+
let cursor = 0;
|
|
64
|
+
const running = /* @__PURE__ */ new Set();
|
|
65
|
+
const limit = Math.max(1, Math.min(cap, children.length));
|
|
66
|
+
const launchOne = (index) => {
|
|
67
|
+
const child = children[index];
|
|
68
|
+
if (child === void 0) return Promise.resolve();
|
|
69
|
+
const start = Date.now();
|
|
70
|
+
const exec = async () => {
|
|
71
|
+
if (signal?.aborted) return {
|
|
72
|
+
agentId: child.agentId,
|
|
73
|
+
status: "cancelled",
|
|
74
|
+
tokensUsed: 0,
|
|
75
|
+
toolCallCount: 0,
|
|
76
|
+
durationMs: 0
|
|
77
|
+
};
|
|
78
|
+
let timer;
|
|
79
|
+
try {
|
|
80
|
+
const timedPromise = perBudget?.durationMs !== void 0 ? new Promise((_, reject) => {
|
|
81
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("budget-exceeded:durationMs")), perBudget.durationMs);
|
|
82
|
+
}) : void 0;
|
|
83
|
+
const raw = await (timedPromise ? Promise.race([child.invoke(), timedPromise]) : child.invoke());
|
|
84
|
+
const report = harvestAgentResult(raw);
|
|
85
|
+
const output = report === void 0 ? raw : report.output;
|
|
86
|
+
const tokensUsed = report?.tokensUsed ?? 0;
|
|
87
|
+
const toolCallCount = report?.toolCallCount ?? 0;
|
|
88
|
+
const exceeded = (perBudget?.tokens !== void 0 && tokensUsed > perBudget.tokens ? `tokens ${tokensUsed} > ${perBudget.tokens}` : void 0) ?? (perBudget?.toolCalls !== void 0 && toolCallCount > perBudget.toolCalls ? `toolCalls ${toolCallCount} > ${perBudget.toolCalls}` : void 0);
|
|
89
|
+
if (exceeded !== void 0) return {
|
|
90
|
+
agentId: child.agentId,
|
|
91
|
+
status: "budget-exceeded",
|
|
92
|
+
error: {
|
|
93
|
+
message: `budget-exceeded: ${exceeded}`,
|
|
94
|
+
code: "budget-exceeded"
|
|
95
|
+
},
|
|
96
|
+
tokensUsed,
|
|
97
|
+
toolCallCount,
|
|
98
|
+
durationMs: Date.now() - start
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
agentId: child.agentId,
|
|
102
|
+
status: "completed",
|
|
103
|
+
output,
|
|
104
|
+
tokensUsed,
|
|
105
|
+
toolCallCount,
|
|
106
|
+
durationMs: Date.now() - start
|
|
107
|
+
};
|
|
108
|
+
} catch (cause) {
|
|
109
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
110
|
+
const aborted = signal?.aborted;
|
|
111
|
+
const status = message.startsWith("budget-exceeded") ? "budget-exceeded" : aborted ? "cancelled" : "failed";
|
|
112
|
+
return {
|
|
113
|
+
agentId: child.agentId,
|
|
114
|
+
status,
|
|
115
|
+
error: {
|
|
116
|
+
message,
|
|
117
|
+
code: status
|
|
118
|
+
},
|
|
119
|
+
tokensUsed: 0,
|
|
120
|
+
toolCallCount: 0,
|
|
121
|
+
durationMs: Date.now() - start
|
|
122
|
+
};
|
|
123
|
+
} finally {
|
|
124
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const promise = exec().then((r) => {
|
|
128
|
+
results[index] = r;
|
|
129
|
+
onChildResult?.(r);
|
|
130
|
+
});
|
|
131
|
+
running.add(promise);
|
|
132
|
+
promise.finally(() => running.delete(promise));
|
|
133
|
+
return promise;
|
|
134
|
+
};
|
|
135
|
+
while (cursor < children.length || running.size > 0) {
|
|
136
|
+
while (running.size < limit && cursor < children.length) {
|
|
137
|
+
launchOne(cursor);
|
|
138
|
+
cursor += 1;
|
|
139
|
+
}
|
|
140
|
+
if (running.size > 0) await Promise.race(running);
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Run a fan-out and produce the aggregate {@link FanOutResult}.
|
|
146
|
+
* Pure with respect to side effects — the runtime emits events /
|
|
147
|
+
* audit rows / counter increments via the supplied `emit` callback.
|
|
148
|
+
*
|
|
149
|
+
* @stable
|
|
150
|
+
*/
|
|
151
|
+
async function runFanOut(opts) {
|
|
152
|
+
const fanOutId = opts.fanOutId ?? `fanout-${opts.runId.slice(-8)}-${Date.now().toString(36)}`;
|
|
153
|
+
const merge = opts.mergeStrategy ?? { kind: "concat" };
|
|
154
|
+
const cap = opts.maxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN;
|
|
155
|
+
opts.emit?.({
|
|
156
|
+
type: "agent.fanout.spawned",
|
|
157
|
+
runId: opts.runId,
|
|
158
|
+
sessionId: opts.sessionId,
|
|
159
|
+
agentId: opts.agentId,
|
|
160
|
+
fanOutId,
|
|
161
|
+
childCount: opts.children.length,
|
|
162
|
+
mergeStrategyKind: merge.kind,
|
|
163
|
+
spawnedAtIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
164
|
+
});
|
|
165
|
+
const results = await runWithSemaphore(opts.children, cap, opts.signal, opts.perBudget, opts.onChildResult);
|
|
166
|
+
const mergeStart = Date.now();
|
|
167
|
+
let merged;
|
|
168
|
+
switch (merge.kind) {
|
|
169
|
+
case "concat": {
|
|
170
|
+
const sep = merge.separator ?? "\n\n---\n\n";
|
|
171
|
+
const parts = [];
|
|
172
|
+
for (const r of results) if (r.status === "completed" && r.output !== void 0) parts.push(typeof r.output === "string" ? r.output : JSON.stringify(r.output));
|
|
173
|
+
else if (r.status === "failed" || r.status === "budget-exceeded" || r.status === "cancelled") parts.push(`[${r.status}: ${r.agentId}]`);
|
|
174
|
+
merged = parts.join(sep);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "first-success":
|
|
178
|
+
merged = results.find((r) => r.status === "completed")?.output ?? "";
|
|
179
|
+
break;
|
|
180
|
+
case "judge-merge":
|
|
181
|
+
merged = await merge.judge(results);
|
|
182
|
+
if (opts.mergeGuard !== void 0 && opts.mergeGuard.strictness !== "off") {
|
|
183
|
+
const mergedText = typeof merged === "string" ? merged : JSON.stringify(merged);
|
|
184
|
+
const overrides = opts.mergeGuard.sourceTrustOverrides ?? {};
|
|
185
|
+
const verdict = evaluateMerge(results.map((r, i) => {
|
|
186
|
+
const c = opts.children[i];
|
|
187
|
+
const childText = r.output === void 0 ? "" : typeof r.output === "string" ? r.output : JSON.stringify(r.output);
|
|
188
|
+
return {
|
|
189
|
+
agentId: r.agentId,
|
|
190
|
+
sourceTrust: computeSourceTrust({
|
|
191
|
+
agentId: r.agentId,
|
|
192
|
+
trustClass: c?.trustClass ?? "loopback",
|
|
193
|
+
origin: c?.origin ?? "built-in",
|
|
194
|
+
...c?.historyAdjustment !== void 0 ? { historyAdjustment: c.historyAdjustment } : {}
|
|
195
|
+
}, overrides),
|
|
196
|
+
contributionWeight: contributionWeight(childText, mergedText)
|
|
197
|
+
};
|
|
198
|
+
}), opts.mergeGuard);
|
|
199
|
+
if (verdict.biased) {
|
|
200
|
+
opts.emit?.({
|
|
201
|
+
type: "agent.lateral-leak.detected",
|
|
202
|
+
runId: opts.runId,
|
|
203
|
+
sessionId: opts.sessionId,
|
|
204
|
+
agentId: opts.agentId,
|
|
205
|
+
vector: "sideways-injection",
|
|
206
|
+
severity: verdict.decision === "block" ? "block" : "warn",
|
|
207
|
+
causalityChain: verdict.offendingChild === void 0 ? [] : [verdict.offendingChild],
|
|
208
|
+
messageContentSha256: createHash("sha256").update(mergedText, "utf8").digest("hex"),
|
|
209
|
+
decision: verdict.decision === "block" ? "block" : verdict.decision === "flag" ? "flag" : "detect",
|
|
210
|
+
detectedAtIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
211
|
+
});
|
|
212
|
+
if (verdict.decision === "block") throw new MergeBlockedError(fanOutId, `low-trust child '${verdict.offendingChild}' (sourceTrust ${verdict.sourceTrust?.toFixed(2)}) contributes ${((verdict.contributionWeight ?? 0) * 100).toFixed(0)}% of the merged output`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
case "custom":
|
|
217
|
+
merged = await merge.merge(results);
|
|
218
|
+
break;
|
|
219
|
+
default: merged = "";
|
|
220
|
+
}
|
|
221
|
+
const mergeDurationMs = Date.now() - mergeStart;
|
|
222
|
+
const childMetadata = results.map((r) => ({
|
|
223
|
+
agentId: r.agentId,
|
|
224
|
+
status: r.status,
|
|
225
|
+
tokensUsed: r.tokensUsed,
|
|
226
|
+
toolCallCount: r.toolCallCount,
|
|
227
|
+
durationMs: r.durationMs
|
|
228
|
+
}));
|
|
229
|
+
const successfulChildCount = results.filter((r) => r.status === "completed").length;
|
|
230
|
+
opts.emit?.({
|
|
231
|
+
type: "agent.fanout.merged",
|
|
232
|
+
runId: opts.runId,
|
|
233
|
+
sessionId: opts.sessionId,
|
|
234
|
+
agentId: opts.agentId,
|
|
235
|
+
fanOutId,
|
|
236
|
+
childCount: opts.children.length,
|
|
237
|
+
successfulChildCount,
|
|
238
|
+
mergeStrategyKind: merge.kind,
|
|
239
|
+
mergeDurationMs,
|
|
240
|
+
childMetadata
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
fanOutId,
|
|
244
|
+
output: merged,
|
|
245
|
+
children: results,
|
|
246
|
+
mergeDurationMs
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
//#endregion
|
|
251
|
+
export { runFanOut };
|
|
252
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["results: ChildResult<TOutput>[]","running: Set<Promise<void>>","timer: ReturnType<typeof setTimeout> | undefined","status: ChildResult<TOutput>['status']","merge: MergeStrategy<TOutput>","merged: TOutput","parts: string[]","childMetadata: FanOutChildMetadata[]"],"sources":["../../src/fanout/index.ts"],"sourcesContent":["/**\n * Agent-step-level fan-out — `Agent.fanOut(...)` convenience that\n * spawns N sub-agents in parallel under a bounded-fanout cap with\n * per-child budgets and four built-in merge strategies.\n *\n * Boundary discipline against the workflow `Dispatch(...)`\n * primitive: fan-out is **agent-step-level inline result** (children\n * share parent `RunContext` lineage; result consumed by parent's\n * continuing loop within one or few `agent.run(...)` calls).\n * `Dispatch(...)` is workflow-step-level checkpointed durable graph.\n * The two compose orthogonally.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport type { AgentEvent, FanOutChildMetadata } from '@graphorin/core';\nimport { MergeBlockedError } from '../errors/index.js';\nimport {\n type ContentOriginKind,\n computeSourceTrust,\n evaluateMerge,\n type MergeGuardConfig,\n type TrustClass,\n} from '../lateral-leak/merge-guard.js';\n\n/**\n * Per-child budget. Defaults derived from the canonical 2026\n * scaling-rule table for agent fan-out workloads.\n *\n * @stable\n */\nexport interface PerChildBudget {\n /**\n * Max `usage.totalTokens` per child. Enforced **post-hoc** and only\n * for usage-reporting children (an `invoke` that resolves to a full\n * `AgentResult` — e.g. `() => child.run(input)`); a child returning a\n * plain value reports `tokensUsed: 0` and this cap cannot fire.\n */\n readonly tokens?: number;\n /**\n * Max tool calls per child. Same usage-reporting contract as\n * {@link PerChildBudget.tokens} (counted from `state.steps`).\n */\n readonly toolCalls?: number;\n /** Wall-clock cap, enforced for every child via a race timer. */\n readonly durationMs?: number;\n}\n\n/**\n * Built-in merge-strategy taxonomy.\n *\n * @stable\n */\nexport type MergeStrategy<TOutput = unknown> =\n | { readonly kind: 'concat'; readonly separator?: string }\n | { readonly kind: 'first-success' }\n | {\n readonly kind: 'judge-merge';\n readonly judge: (children: ReadonlyArray<ChildResult<TOutput>>) => Promise<TOutput>;\n }\n | {\n readonly kind: 'custom';\n readonly merge: (children: ReadonlyArray<ChildResult<TOutput>>) => Promise<TOutput>;\n };\n\n/**\n * Per-child outcome surfaced on\n * {@link FanOutResult.children}. Failed-child isolation: a child\n * that throws produces a `ChildResult` with `status: 'failed'` —\n * never an exception thrown from the fan-out call itself.\n *\n * @stable\n */\nexport interface ChildResult<TOutput = unknown> {\n readonly agentId: string;\n readonly status: 'completed' | 'failed' | 'budget-exceeded' | 'cancelled';\n readonly output?: TOutput;\n readonly error?: { readonly message: string; readonly code: string };\n readonly tokensUsed: number;\n readonly toolCallCount: number;\n readonly durationMs: number;\n}\n\n/**\n * Aggregate result returned by `Agent.fanOut(...)`.\n *\n * @stable\n */\nexport interface FanOutResult<TOutput = unknown> {\n readonly fanOutId: string;\n readonly output: TOutput;\n readonly children: ReadonlyArray<ChildResult<TOutput>>;\n readonly mergeDurationMs: number;\n}\n\n/**\n * Per-call options accepted by `Agent.fanOut(...)`.\n *\n * @stable\n */\nexport interface FanOutOptions<TOutput = unknown> {\n /**\n * The sub-agents to invoke. Each entry is invoked as a function\n * returning a `Promise<TOutput>` — the fan-out helper does not\n * impose an `Agent` shape on the children so the runtime can\n * adapt any callable surface.\n */\n readonly children: ReadonlyArray<{\n readonly agentId: string;\n /**\n * Child callable. Resolve to a plain `TOutput`, or to a full\n * `AgentResult` (e.g. `() => childAgent.run(input)`) — the fan-out\n * detects the result envelope structurally (`output` + numeric\n * `usage.totalTokens` + `state`), unwraps `output`, and harvests\n * `tokensUsed` / `toolCallCount` so per-child budgets can enforce.\n */\n readonly invoke: () => Promise<TOutput>;\n /** Trust-class for the merge guard (default `'loopback'`). */\n readonly trustClass?: TrustClass;\n /** Content-origin for the merge guard (default `'built-in'`). */\n readonly origin?: ContentOriginKind;\n /** Rolling trust adjustment in `[0,1]` (default `1`). */\n readonly historyAdjustment?: number;\n }>;\n /** Default `4` per the canonical 2026 production lesson. */\n readonly maxConcurrentChildren?: number;\n /** Per-child budget; default unset. */\n readonly perBudget?: PerChildBudget;\n /** Default `{ kind: 'concat' }`. */\n readonly mergeStrategy?: MergeStrategy<TOutput>;\n readonly signal?: AbortSignal;\n /** Optional callback for per-child completion observability. */\n readonly onChildResult?: (result: ChildResult<TOutput>) => void;\n /** Optional event emitter for `agent.fanout.spawned / merged`. */\n readonly emit?: (event: AgentEvent) => void;\n /**\n * Sideways-injection merge guard (AG-7): on `'judge-merge'` the\n * fan-out scores each child's source trust and contribution weight\n * against the judge's merged output; a biased merge emits\n * `agent.lateral-leak.detected` (vector `sideways-injection`) and —\n * under `strictness: 'detect-and-block'` — throws\n * {@link MergeBlockedError}.\n */\n readonly mergeGuard?: MergeGuardConfig;\n /** Identifiers required to populate the events. */\n readonly runId: string;\n readonly sessionId: string;\n readonly agentId: string;\n /** Default — generated from `runId + Date.now()`. */\n readonly fanOutId?: string;\n}\n\nconst DEFAULT_MAX_CONCURRENT_CHILDREN = 4;\n\n/**\n * Structurally detect a full `AgentResult` returned by a child\n * `invoke` (AG-16): an object carrying `output`, a numeric\n * `usage.totalTokens`, and a `state` with string `id`/`status`. The\n * three-field match makes accidental collision with a user `TOutput`\n * negligible; children whose genuine output looks like this should\n * resolve a plain value instead.\n */\nfunction harvestAgentResult(\n value: unknown,\n):\n | { readonly output: unknown; readonly tokensUsed: number; readonly toolCallCount: number }\n | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const v = value as {\n readonly output?: unknown;\n readonly usage?: { readonly totalTokens?: unknown };\n readonly state?: { readonly id?: unknown; readonly status?: unknown; readonly steps?: unknown };\n };\n if (!('output' in v)) return undefined;\n if (typeof v.usage?.totalTokens !== 'number') return undefined;\n if (typeof v.state?.id !== 'string' || typeof v.state.status !== 'string') return undefined;\n let toolCallCount = 0;\n if (Array.isArray(v.state.steps)) {\n for (const step of v.state.steps) {\n const calls = (step as { readonly toolCalls?: unknown }).toolCalls;\n if (Array.isArray(calls)) toolCallCount += calls.length;\n }\n }\n return { output: v.output, tokensUsed: v.usage.totalTokens, toolCallCount };\n}\n\n/**\n * Whitespace-token overlap of a child's output against the merged\n * output, in `[0,1]` — the contribution-weight estimate the merge\n * guard's docstring contracts (each child scored independently; a\n * judge parroting one child verbatim yields ~1.0 for that child).\n */\nfunction contributionWeight(childText: string, mergedText: string): number {\n if (childText.length === 0 || mergedText.length === 0) return 0;\n const mergedTokens = mergedText.split(/\\s+/).filter((t) => t.length > 0);\n if (mergedTokens.length === 0) return 0;\n const childTokens = new Set(childText.split(/\\s+/).filter((t) => t.length > 0));\n let hits = 0;\n for (const token of mergedTokens) {\n if (childTokens.has(token)) hits += 1;\n }\n return hits / mergedTokens.length;\n}\n\nasync function runWithSemaphore<TOutput>(\n children: ReadonlyArray<FanOutOptions<TOutput>['children'][number]>,\n cap: number,\n signal: AbortSignal | undefined,\n perBudget: PerChildBudget | undefined,\n onChildResult?: (r: ChildResult<TOutput>) => void,\n): Promise<ReadonlyArray<ChildResult<TOutput>>> {\n const results: ChildResult<TOutput>[] = new Array(children.length);\n let cursor = 0;\n const running: Set<Promise<void>> = new Set();\n const limit = Math.max(1, Math.min(cap, children.length));\n\n const launchOne = (index: number): Promise<void> => {\n const child = children[index];\n if (child === undefined) return Promise.resolve();\n const start = Date.now();\n const exec = async (): Promise<ChildResult<TOutput>> => {\n if (signal?.aborted) {\n return {\n agentId: child.agentId,\n status: 'cancelled',\n tokensUsed: 0,\n toolCallCount: 0,\n durationMs: 0,\n };\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const timedPromise =\n perBudget?.durationMs !== undefined\n ? new Promise<TOutput>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error('budget-exceeded:durationMs')),\n perBudget.durationMs,\n );\n })\n : undefined;\n const racePromise = timedPromise\n ? Promise.race([child.invoke(), timedPromise])\n : child.invoke();\n const raw = await racePromise;\n // AG-16: a child resolving to a full AgentResult reports its\n // real usage — unwrap the output and harvest the counters.\n const report = harvestAgentResult(raw);\n const output = (report === undefined ? raw : report.output) as TOutput;\n const tokensUsed = report?.tokensUsed ?? 0;\n const toolCallCount = report?.toolCallCount ?? 0;\n // Post-hoc budget enforcement (only fires for usage-reporting\n // children): the over-budget output is withheld from the merge.\n const exceeded =\n (perBudget?.tokens !== undefined && tokensUsed > perBudget.tokens\n ? `tokens ${tokensUsed} > ${perBudget.tokens}`\n : undefined) ??\n (perBudget?.toolCalls !== undefined && toolCallCount > perBudget.toolCalls\n ? `toolCalls ${toolCallCount} > ${perBudget.toolCalls}`\n : undefined);\n if (exceeded !== undefined) {\n return {\n agentId: child.agentId,\n status: 'budget-exceeded',\n error: { message: `budget-exceeded: ${exceeded}`, code: 'budget-exceeded' },\n tokensUsed,\n toolCallCount,\n durationMs: Date.now() - start,\n };\n }\n return {\n agentId: child.agentId,\n status: 'completed',\n output,\n tokensUsed,\n toolCallCount,\n durationMs: Date.now() - start,\n };\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n const aborted = signal?.aborted;\n const status: ChildResult<TOutput>['status'] = message.startsWith('budget-exceeded')\n ? 'budget-exceeded'\n : aborted\n ? 'cancelled'\n : 'failed';\n return {\n agentId: child.agentId,\n status,\n error: { message, code: status },\n tokensUsed: 0,\n toolCallCount: 0,\n durationMs: Date.now() - start,\n };\n } finally {\n // AG-16: the duration timer must die on EVERY path — leaving it\n // armed on rejection held the event loop open.\n if (timer !== undefined) clearTimeout(timer);\n }\n };\n const promise = exec().then((r) => {\n results[index] = r;\n onChildResult?.(r);\n });\n running.add(promise);\n promise.finally(() => running.delete(promise));\n return promise;\n };\n\n while (cursor < children.length || running.size > 0) {\n while (running.size < limit && cursor < children.length) {\n void launchOne(cursor);\n cursor += 1;\n }\n if (running.size > 0) {\n await Promise.race(running);\n }\n }\n return results;\n}\n\n/**\n * Run a fan-out and produce the aggregate {@link FanOutResult}.\n * Pure with respect to side effects — the runtime emits events /\n * audit rows / counter increments via the supplied `emit` callback.\n *\n * @stable\n */\nexport async function runFanOut<TOutput>(\n opts: FanOutOptions<TOutput>,\n): Promise<FanOutResult<TOutput>> {\n const fanOutId = opts.fanOutId ?? `fanout-${opts.runId.slice(-8)}-${Date.now().toString(36)}`;\n const merge: MergeStrategy<TOutput> = opts.mergeStrategy ?? {\n kind: 'concat',\n };\n const cap = opts.maxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN;\n\n opts.emit?.({\n type: 'agent.fanout.spawned',\n runId: opts.runId,\n sessionId: opts.sessionId,\n agentId: opts.agentId,\n fanOutId,\n childCount: opts.children.length,\n mergeStrategyKind: merge.kind,\n spawnedAtIso: new Date().toISOString(),\n });\n\n const results = await runWithSemaphore<TOutput>(\n opts.children,\n cap,\n opts.signal,\n opts.perBudget,\n opts.onChildResult,\n );\n\n const mergeStart = Date.now();\n let merged: TOutput;\n switch (merge.kind) {\n case 'concat': {\n const sep = merge.separator ?? '\\n\\n---\\n\\n';\n const parts: string[] = [];\n for (const r of results) {\n if (r.status === 'completed' && r.output !== undefined) {\n parts.push(typeof r.output === 'string' ? r.output : JSON.stringify(r.output));\n } else if (\n r.status === 'failed' ||\n r.status === 'budget-exceeded' ||\n r.status === 'cancelled'\n ) {\n parts.push(`[${r.status}: ${r.agentId}]`);\n }\n }\n merged = parts.join(sep) as unknown as TOutput;\n break;\n }\n case 'first-success': {\n const first = results.find((r) => r.status === 'completed');\n merged = first?.output ?? ('' as unknown as TOutput);\n break;\n }\n case 'judge-merge': {\n merged = await merge.judge(results);\n // AG-7: the sideways-injection merge guard scores each child's\n // source trust × contribution weight against the judge's merged\n // output. A biased merge emits `agent.lateral-leak.detected`;\n // 'detect-and-block' refuses the merge entirely.\n if (opts.mergeGuard !== undefined && opts.mergeGuard.strictness !== 'off') {\n const mergedText = typeof merged === 'string' ? merged : JSON.stringify(merged);\n const overrides = opts.mergeGuard.sourceTrustOverrides ?? {};\n const perChild = results.map((r, i) => {\n const c = opts.children[i];\n const childText =\n r.output === undefined\n ? ''\n : typeof r.output === 'string'\n ? r.output\n : JSON.stringify(r.output);\n return {\n agentId: r.agentId,\n sourceTrust: computeSourceTrust(\n {\n agentId: r.agentId,\n trustClass: c?.trustClass ?? 'loopback',\n origin: c?.origin ?? 'built-in',\n ...(c?.historyAdjustment !== undefined\n ? { historyAdjustment: c.historyAdjustment }\n : {}),\n },\n overrides,\n ),\n contributionWeight: contributionWeight(childText, mergedText),\n };\n });\n const verdict = evaluateMerge(perChild, opts.mergeGuard);\n if (verdict.biased) {\n opts.emit?.({\n type: 'agent.lateral-leak.detected',\n runId: opts.runId,\n sessionId: opts.sessionId,\n agentId: opts.agentId,\n vector: 'sideways-injection',\n severity: verdict.decision === 'block' ? 'block' : 'warn',\n causalityChain: verdict.offendingChild === undefined ? [] : [verdict.offendingChild],\n messageContentSha256: createHash('sha256').update(mergedText, 'utf8').digest('hex'),\n decision:\n verdict.decision === 'block'\n ? 'block'\n : verdict.decision === 'flag'\n ? 'flag'\n : 'detect',\n detectedAtIso: new Date().toISOString(),\n });\n if (verdict.decision === 'block') {\n throw new MergeBlockedError(\n fanOutId,\n `low-trust child '${verdict.offendingChild}' (sourceTrust ${verdict.sourceTrust?.toFixed(2)}) contributes ${(\n (verdict.contributionWeight ?? 0) * 100\n ).toFixed(0)}% of the merged output`,\n );\n }\n }\n }\n break;\n }\n case 'custom':\n merged = await merge.merge(results);\n break;\n default: {\n const _exhaustive: never = merge;\n void _exhaustive;\n merged = '' as unknown as TOutput;\n }\n }\n const mergeDurationMs = Date.now() - mergeStart;\n\n const childMetadata: FanOutChildMetadata[] = results.map((r) => ({\n agentId: r.agentId,\n status: r.status,\n tokensUsed: r.tokensUsed,\n toolCallCount: r.toolCallCount,\n durationMs: r.durationMs,\n }));\n const successfulChildCount = results.filter((r) => r.status === 'completed').length;\n\n opts.emit?.({\n type: 'agent.fanout.merged',\n runId: opts.runId,\n sessionId: opts.sessionId,\n agentId: opts.agentId,\n fanOutId,\n childCount: opts.children.length,\n successfulChildCount,\n mergeStrategyKind: merge.kind,\n mergeDurationMs,\n childMetadata,\n });\n\n return {\n fanOutId,\n output: merged,\n children: results,\n mergeDurationMs,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { HandoffFilter, HandoffInputFilterDescriptor, Sensitivity } from "@graphorin/core";
|
|
2
|
+
|
|
3
|
+
//#region src/filters/index.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A `HandoffFilter` paired with the serializable descriptor that
|
|
7
|
+
* round-trips through the JSONL session export. Authors of custom
|
|
8
|
+
* filters return one of these via `filters.custom({...})`.
|
|
9
|
+
*
|
|
10
|
+
* @stable
|
|
11
|
+
*/
|
|
12
|
+
interface DescribedFilter extends HandoffFilter {
|
|
13
|
+
readonly descriptor: HandoffInputFilterDescriptor;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Keep the parent's system prompt and the last `n` non-system
|
|
17
|
+
* messages. Default `n = 10` per DEC-146 / RB-40 security-first
|
|
18
|
+
* compose.
|
|
19
|
+
*
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
declare function lastN(n?: number): DescribedFilter;
|
|
23
|
+
/**
|
|
24
|
+
* Keep only the parent's system prompt and the most recent user
|
|
25
|
+
* message. Useful for simple sub-agents that only need the question.
|
|
26
|
+
*
|
|
27
|
+
* @stable
|
|
28
|
+
*/
|
|
29
|
+
declare function lastUser(): DescribedFilter;
|
|
30
|
+
/**
|
|
31
|
+
* The full unfiltered history. Discouraged — security-conscious
|
|
32
|
+
* callers should pick {@link lastN} or {@link bySensitivity} instead
|
|
33
|
+
* (a sub-agent rarely needs the parent's entire conversation).
|
|
34
|
+
*
|
|
35
|
+
* @stable
|
|
36
|
+
*/
|
|
37
|
+
declare function full(): DescribedFilter;
|
|
38
|
+
/**
|
|
39
|
+
* Replace the parent's history with a single system message carrying
|
|
40
|
+
* the supplied summary. Used by callers that wire in an LLM-based
|
|
41
|
+
* summarizer outside the framework.
|
|
42
|
+
*
|
|
43
|
+
* @stable
|
|
44
|
+
*/
|
|
45
|
+
declare function summary(text: string): DescribedFilter;
|
|
46
|
+
/**
|
|
47
|
+
* Drop messages whose effective sensitivity ceiling exceeds
|
|
48
|
+
* `maxTier`. Messages without sensitivity metadata default to
|
|
49
|
+
* `'public'` and are always kept.
|
|
50
|
+
*
|
|
51
|
+
* The framework currently records sensitivity at the
|
|
52
|
+
* `MessageContent` part level via the `inboundTrust` / `secret`
|
|
53
|
+
* annotations. v0.1 ships a coarse-grained heuristic: a message is
|
|
54
|
+
* kept iff every text part's content does not contain the literal
|
|
55
|
+
* `[REDACTED:secret]` token AND every part's annotated sensitivity
|
|
56
|
+
* is acceptable to `maxTier`. Operators that need a stricter
|
|
57
|
+
* filter compose the function with `stripSensitiveOutputs()` or a
|
|
58
|
+
* custom predicate.
|
|
59
|
+
*
|
|
60
|
+
* @stable
|
|
61
|
+
*/
|
|
62
|
+
declare function bySensitivity(args?: {
|
|
63
|
+
readonly maxTier?: Sensitivity;
|
|
64
|
+
}): DescribedFilter;
|
|
65
|
+
/**
|
|
66
|
+
* Strip every `ReasoningContent` part from each message. Always
|
|
67
|
+
* applied at the handoff boundary (the `compose(...)` helper appends
|
|
68
|
+
* this filter automatically).
|
|
69
|
+
*
|
|
70
|
+
* @stable
|
|
71
|
+
*/
|
|
72
|
+
declare function stripReasoning(): DescribedFilter;
|
|
73
|
+
/**
|
|
74
|
+
* Strip tool messages whose `content` carries the literal token
|
|
75
|
+
* `[REDACTED:secret]` or whose `secret` annotation marks the body as
|
|
76
|
+
* sensitive. Conservative-by-design: the agent runtime tags
|
|
77
|
+
* sensitive tool outputs at session-write time so this filter has
|
|
78
|
+
* stable bytes to scan against.
|
|
79
|
+
*
|
|
80
|
+
* @stable
|
|
81
|
+
*/
|
|
82
|
+
declare function stripSensitiveOutputs(): DescribedFilter;
|
|
83
|
+
/**
|
|
84
|
+
* Drop every assistant `toolCalls` array AND every `tool` message.
|
|
85
|
+
* Useful when a sub-agent should only see the textual conversation.
|
|
86
|
+
*
|
|
87
|
+
* @stable
|
|
88
|
+
*/
|
|
89
|
+
declare function stripToolCalls(): DescribedFilter;
|
|
90
|
+
/**
|
|
91
|
+
* Compose multiple filters left-to-right. The composer **always**
|
|
92
|
+
* appends `stripReasoning()` at the end so reasoning content never
|
|
93
|
+
* crosses a handoff boundary regardless of caller intent.
|
|
94
|
+
*
|
|
95
|
+
* @stable
|
|
96
|
+
*/
|
|
97
|
+
declare function compose(...filters: ReadonlyArray<HandoffFilter>): DescribedFilter;
|
|
98
|
+
/**
|
|
99
|
+
* Wrap a caller-supplied function as a {@link DescribedFilter} with
|
|
100
|
+
* the canonical `'custom'` descriptor.
|
|
101
|
+
*
|
|
102
|
+
* @stable
|
|
103
|
+
*/
|
|
104
|
+
declare function custom(fn: HandoffFilter, meta?: Readonly<Record<string, unknown>>): DescribedFilter;
|
|
105
|
+
/**
|
|
106
|
+
* The canonical default applied by the agent runtime to every
|
|
107
|
+
* `Agent.toTool(...)` and `handoff(...)` invocation when the caller
|
|
108
|
+
* does not supply an explicit filter.
|
|
109
|
+
*
|
|
110
|
+
* @stable
|
|
111
|
+
*/
|
|
112
|
+
declare function defaultHandoffFilter(): DescribedFilter;
|
|
113
|
+
/**
|
|
114
|
+
* Pure `HandoffInputFilterDescriptor` for callers that just need the
|
|
115
|
+
* descriptor without instantiating the runtime function (e.g. the
|
|
116
|
+
* sessions package's lenient-forward-parse path).
|
|
117
|
+
*
|
|
118
|
+
* @stable
|
|
119
|
+
*/
|
|
120
|
+
declare const FILTER_KIND_CUSTOM: HandoffInputFilterDescriptor;
|
|
121
|
+
/** Aggregate module export. */
|
|
122
|
+
declare const filters: {
|
|
123
|
+
lastN: typeof lastN;
|
|
124
|
+
lastUser: typeof lastUser;
|
|
125
|
+
full: typeof full;
|
|
126
|
+
summary: typeof summary;
|
|
127
|
+
bySensitivity: typeof bySensitivity;
|
|
128
|
+
stripReasoning: typeof stripReasoning;
|
|
129
|
+
stripSensitiveOutputs: typeof stripSensitiveOutputs;
|
|
130
|
+
stripToolCalls: typeof stripToolCalls;
|
|
131
|
+
compose: typeof compose;
|
|
132
|
+
custom: typeof custom;
|
|
133
|
+
defaultHandoffFilter: typeof defaultHandoffFilter;
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
export { DescribedFilter, FILTER_KIND_CUSTOM, bySensitivity, compose, custom, defaultHandoffFilter, filters, full, lastN, lastUser, stripReasoning, stripSensitiveOutputs, stripToolCalls, summary };
|
|
137
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/filters/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAgUA;AAWA;AAGA;UA1RiB,eAAA,SAAwB;uBAClB;;;;;;;;;iBAmCP,KAAA,cAAe;;;;;;;iBA0Bf,QAAA,CAAA,GAAY;;;;;;;;iBAgCZ,IAAA,CAAA,GAAQ;;;;;;;;iBAWR,OAAA,gBAAuB;;;;;;;;;;;;;;;;;iBA4BvB,aAAA;qBAAyC;IAAqB;;;;;;;;iBA0B9D,cAAA,CAAA,GAAkB;;;;;;;;;;iBAelB,qBAAA,CAAA,GAAyB;;;;;;;iBA4BzB,cAAA,CAAA,GAAkB;;;;;;;;iBA0BlB,OAAA,aAAoB,cAAc,iBAAiB;;;;;;;iBA0BnD,MAAA,KACV,sBACG,SAAS,2BACf;;;;;;;;iBAWa,oBAAA,CAAA,GAAwB;;;;;;;;cAW3B,oBAAoB;;cAGpB"}
|