@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,311 @@
|
|
|
1
|
+
import { RunStateMalformedError, RunStateVersionUnsupportedError } from "../errors/index.js";
|
|
2
|
+
import { zeroUsage } from "@graphorin/core";
|
|
3
|
+
|
|
4
|
+
//#region src/run-state/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Canonical schema id for serialized {@link RunState} payloads.
|
|
7
|
+
*
|
|
8
|
+
* @stable
|
|
9
|
+
*/
|
|
10
|
+
const RUN_STATE_SCHEMA_VERSION = "graphorin-run-state/1.1";
|
|
11
|
+
/**
|
|
12
|
+
* Reader-supported schema id range. Major version 1 only for v0.1.
|
|
13
|
+
*
|
|
14
|
+
* @stable
|
|
15
|
+
*/
|
|
16
|
+
const RUN_STATE_SCHEMA_MAJOR_SUPPORTED = 1;
|
|
17
|
+
/**
|
|
18
|
+
* Render a JSON-stable snapshot of the supplied {@link RunState}.
|
|
19
|
+
* The returned value is plain JSON (no `Map`, `Set`, `Date`, ...).
|
|
20
|
+
*
|
|
21
|
+
* @stable
|
|
22
|
+
*/
|
|
23
|
+
function serializeRunState(state, options = {}) {
|
|
24
|
+
const out = {
|
|
25
|
+
version: RUN_STATE_SCHEMA_VERSION,
|
|
26
|
+
id: state.id,
|
|
27
|
+
agentId: state.agentId,
|
|
28
|
+
currentAgentId: state.currentAgentId,
|
|
29
|
+
sessionId: state.sessionId,
|
|
30
|
+
...state.userId !== void 0 ? { userId: state.userId } : {},
|
|
31
|
+
status: state.status,
|
|
32
|
+
steps: state.steps,
|
|
33
|
+
messages: state.messages,
|
|
34
|
+
pendingApprovals: state.pendingApprovals,
|
|
35
|
+
handoffs: state.handoffs,
|
|
36
|
+
usage: state.usage,
|
|
37
|
+
...state.usageByModel !== void 0 ? { usageByModel: state.usageByModel } : {},
|
|
38
|
+
...state.taintSummary !== void 0 ? { taintSummary: state.taintSummary } : {},
|
|
39
|
+
...state.promotedTools !== void 0 ? { promotedTools: state.promotedTools } : {},
|
|
40
|
+
startedAt: state.startedAt,
|
|
41
|
+
...state.finishedAt !== void 0 ? { finishedAt: state.finishedAt } : {},
|
|
42
|
+
...state.error !== void 0 ? { error: state.error } : {}
|
|
43
|
+
};
|
|
44
|
+
const detached = JSON.parse(JSON.stringify(out));
|
|
45
|
+
if (options.stripTracingApiKey === true) redactSecretKeysInPlace(detached);
|
|
46
|
+
return detached;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Key names whose values are redacted by
|
|
50
|
+
* `serializeRunState(state, { stripTracingApiKey: true })`.
|
|
51
|
+
*/
|
|
52
|
+
const SECRET_KEY_PATTERN = /^(api[-_]?key|tracing[-_]?api[-_]?key|x-api-key|authorization|bearer[-_]?token|access[-_]?token|refresh[-_]?token|secret|password|passphrase)$/i;
|
|
53
|
+
const REDACTED = "[redacted]";
|
|
54
|
+
function redactSecretKeysInPlace(value) {
|
|
55
|
+
if (typeof value !== "object" || value === null) return;
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
for (const item of value) redactSecretKeysInPlace(item);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const record = value;
|
|
61
|
+
if (record.role === "tool" && typeof record.content === "string") record.content = redactJsonString(record.content);
|
|
62
|
+
for (const [key, inner] of Object.entries(record)) {
|
|
63
|
+
if (SECRET_KEY_PATTERN.test(key)) {
|
|
64
|
+
record[key] = REDACTED;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
redactSecretKeysInPlace(inner);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function redactJsonString(content) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(content);
|
|
73
|
+
if (typeof parsed !== "object" || parsed === null) return content;
|
|
74
|
+
redactSecretKeysInPlace(parsed);
|
|
75
|
+
return JSON.stringify(parsed);
|
|
76
|
+
} catch {
|
|
77
|
+
return content;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Render the canonical JSON string representation of the supplied
|
|
82
|
+
* {@link RunState}. `JSON.stringify(serializeRunState(state))` —
|
|
83
|
+
* provided as a convenience.
|
|
84
|
+
*
|
|
85
|
+
* @stable
|
|
86
|
+
*/
|
|
87
|
+
function runStateToJSON(state, options) {
|
|
88
|
+
return JSON.stringify(serializeRunState(state, options));
|
|
89
|
+
}
|
|
90
|
+
function isRecord(v) {
|
|
91
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Rehydrate a {@link RunState} from the on-disk payload. Throws
|
|
95
|
+
* {@link RunStateVersionUnsupportedError} when the payload version
|
|
96
|
+
* is from a future major; throws
|
|
97
|
+
* {@link RunStateMalformedError} when the payload is structurally
|
|
98
|
+
* invalid.
|
|
99
|
+
*
|
|
100
|
+
* Backwards-compat: a payload that omits `usageByModel` is accepted
|
|
101
|
+
* and the field is synthesized from the aggregate `usage` with
|
|
102
|
+
* `attemptCount: 1` for the primary model.
|
|
103
|
+
*
|
|
104
|
+
* @stable
|
|
105
|
+
*/
|
|
106
|
+
function deserializeRunState(payload, options = {}) {
|
|
107
|
+
if (!isRecord(payload)) throw new RunStateMalformedError("payload must be an object");
|
|
108
|
+
const versionRaw = payload.version;
|
|
109
|
+
if (typeof versionRaw !== "string") throw new RunStateMalformedError("missing 'version' field");
|
|
110
|
+
const versionMatch = /^graphorin-run-state\/(\d+)\.(\d+)$/.exec(versionRaw);
|
|
111
|
+
if (versionMatch === null) throw new RunStateMalformedError(`unrecognized version '${versionRaw}'`);
|
|
112
|
+
if (Number(versionMatch[1]) > RUN_STATE_SCHEMA_MAJOR_SUPPORTED) throw new RunStateVersionUnsupportedError(versionRaw, RUN_STATE_SCHEMA_VERSION);
|
|
113
|
+
const requiredString = (key) => {
|
|
114
|
+
const v = payload[key];
|
|
115
|
+
if (typeof v !== "string") throw new RunStateMalformedError(`missing string '${key}' field`);
|
|
116
|
+
return v;
|
|
117
|
+
};
|
|
118
|
+
const requiredArray = (key) => {
|
|
119
|
+
const v = payload[key];
|
|
120
|
+
if (!Array.isArray(v)) throw new RunStateMalformedError(`missing array '${key}' field`);
|
|
121
|
+
return v;
|
|
122
|
+
};
|
|
123
|
+
const id = requiredString("id");
|
|
124
|
+
const agentId = requiredString("agentId");
|
|
125
|
+
const currentAgentId = typeof payload.currentAgentId === "string" ? payload.currentAgentId : agentId;
|
|
126
|
+
const sessionId = requiredString("sessionId");
|
|
127
|
+
const status = requiredString("status");
|
|
128
|
+
const steps = requiredArray("steps");
|
|
129
|
+
const messages = requiredArray("messages");
|
|
130
|
+
const pendingApprovals = requiredArray("pendingApprovals");
|
|
131
|
+
const handoffs = requiredArray("handoffs");
|
|
132
|
+
const usageRaw = payload.usage;
|
|
133
|
+
if (!isRecord(usageRaw)) throw new RunStateMalformedError("missing object 'usage' field");
|
|
134
|
+
const usage = {
|
|
135
|
+
promptTokens: Number(usageRaw.promptTokens ?? 0),
|
|
136
|
+
completionTokens: Number(usageRaw.completionTokens ?? 0),
|
|
137
|
+
totalTokens: Number(usageRaw.totalTokens ?? 0),
|
|
138
|
+
...usageRaw.reasoningTokens !== void 0 ? { reasoningTokens: Number(usageRaw.reasoningTokens) } : {},
|
|
139
|
+
...isRecord(usageRaw.cost) ? { cost: {
|
|
140
|
+
amount: Number(usageRaw.cost.amount ?? 0),
|
|
141
|
+
currency: String(usageRaw.cost.currency ?? "USD")
|
|
142
|
+
} } : {}
|
|
143
|
+
};
|
|
144
|
+
const startedAt = requiredString("startedAt");
|
|
145
|
+
const userIdRaw = payload.userId;
|
|
146
|
+
const finishedAtRaw = payload.finishedAt;
|
|
147
|
+
const errorRaw = payload.error;
|
|
148
|
+
let error;
|
|
149
|
+
if (isRecord(errorRaw)) error = {
|
|
150
|
+
message: typeof errorRaw.message === "string" ? errorRaw.message : "unknown",
|
|
151
|
+
code: typeof errorRaw.code === "string" ? errorRaw.code : "unknown",
|
|
152
|
+
...errorRaw.details !== void 0 ? { details: errorRaw.details } : {}
|
|
153
|
+
};
|
|
154
|
+
let usageByModel;
|
|
155
|
+
if (isRecord(payload.usageByModel)) {
|
|
156
|
+
const m = {};
|
|
157
|
+
for (const [modelId, entryRaw] of Object.entries(payload.usageByModel)) {
|
|
158
|
+
if (!isRecord(entryRaw)) continue;
|
|
159
|
+
m[modelId] = {
|
|
160
|
+
promptTokens: Number(entryRaw.promptTokens ?? 0),
|
|
161
|
+
completionTokens: Number(entryRaw.completionTokens ?? 0),
|
|
162
|
+
totalTokens: Number(entryRaw.totalTokens ?? 0),
|
|
163
|
+
attemptCount: Number(entryRaw.attemptCount ?? 1),
|
|
164
|
+
...entryRaw.reasoningTokens !== void 0 ? { reasoningTokens: Number(entryRaw.reasoningTokens) } : {},
|
|
165
|
+
...isRecord(entryRaw.cost) ? { cost: {
|
|
166
|
+
amount: Number(entryRaw.cost.amount ?? 0),
|
|
167
|
+
currency: String(entryRaw.cost.currency ?? "USD")
|
|
168
|
+
} } : {}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
usageByModel = m;
|
|
172
|
+
} else if (options.synthesizeUsageByModel !== false && Number.isFinite(usage.totalTokens)) {
|
|
173
|
+
usageByModel = { [agentId]: {
|
|
174
|
+
...usage,
|
|
175
|
+
attemptCount: 1
|
|
176
|
+
} };
|
|
177
|
+
options.logger?.(`[graphorin/agent] RunState v0.1-alpha synthesis: per-model breakdown derived from aggregate usage for agent '${agentId}'.`);
|
|
178
|
+
}
|
|
179
|
+
let taintSummary;
|
|
180
|
+
if (isRecord(payload.taintSummary)) {
|
|
181
|
+
const ts = payload.taintSummary;
|
|
182
|
+
taintSummary = {
|
|
183
|
+
untrustedSeen: ts.untrustedSeen === true,
|
|
184
|
+
sensitiveSeen: ts.sensitiveSeen === true,
|
|
185
|
+
untrustedSourceKinds: Array.isArray(ts.untrustedSourceKinds) ? ts.untrustedSourceKinds.filter((k) => typeof k === "string") : []
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const promotedTools = Array.isArray(payload.promotedTools) ? payload.promotedTools.filter((t) => typeof t === "string") : void 0;
|
|
189
|
+
return {
|
|
190
|
+
id,
|
|
191
|
+
agentId,
|
|
192
|
+
currentAgentId,
|
|
193
|
+
sessionId,
|
|
194
|
+
...typeof userIdRaw === "string" ? { userId: userIdRaw } : {},
|
|
195
|
+
status,
|
|
196
|
+
steps,
|
|
197
|
+
messages,
|
|
198
|
+
pendingApprovals,
|
|
199
|
+
handoffs,
|
|
200
|
+
usage,
|
|
201
|
+
...usageByModel !== void 0 ? { usageByModel } : {},
|
|
202
|
+
...taintSummary !== void 0 ? { taintSummary } : {},
|
|
203
|
+
...promotedTools !== void 0 ? { promotedTools } : {},
|
|
204
|
+
startedAt,
|
|
205
|
+
...typeof finishedAtRaw === "string" ? { finishedAt: finishedAtRaw } : {},
|
|
206
|
+
...error !== void 0 ? { error } : {}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Convenience JSON-string parser pairing with {@link runStateToJSON}. */
|
|
210
|
+
function runStateFromJSON(serialized, options) {
|
|
211
|
+
let parsed;
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(serialized);
|
|
214
|
+
} catch (cause) {
|
|
215
|
+
throw new RunStateMalformedError(`invalid JSON: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
216
|
+
}
|
|
217
|
+
return deserializeRunState(parsed, options);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build a fresh, minimal {@link RunState} for a new run. Helper used
|
|
221
|
+
* by `createAgent({...})` so consumers can construct deterministic
|
|
222
|
+
* run state in tests.
|
|
223
|
+
*
|
|
224
|
+
* @stable
|
|
225
|
+
*/
|
|
226
|
+
function createInitialRunState(args) {
|
|
227
|
+
return {
|
|
228
|
+
id: args.id,
|
|
229
|
+
agentId: args.agentId,
|
|
230
|
+
currentAgentId: args.agentId,
|
|
231
|
+
sessionId: args.sessionId,
|
|
232
|
+
...args.userId !== void 0 ? { userId: args.userId } : {},
|
|
233
|
+
status: "running",
|
|
234
|
+
steps: [],
|
|
235
|
+
messages: [],
|
|
236
|
+
pendingApprovals: [],
|
|
237
|
+
handoffs: [],
|
|
238
|
+
usage: zeroUsage(),
|
|
239
|
+
startedAt: args.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Append a per-model usage entry to {@link RunState.usageByModel}.
|
|
244
|
+
* Mutates the supplied state in place — used by the agent runtime's
|
|
245
|
+
* per-step retry loop. Pure callers that need an immutable update
|
|
246
|
+
* should clone the state first.
|
|
247
|
+
*
|
|
248
|
+
* @stable
|
|
249
|
+
*/
|
|
250
|
+
function addModelUsage(state, modelId, delta) {
|
|
251
|
+
const current = state.usageByModel ?? {};
|
|
252
|
+
const prev = current[modelId];
|
|
253
|
+
current[modelId] = prev ? {
|
|
254
|
+
promptTokens: prev.promptTokens + delta.promptTokens,
|
|
255
|
+
completionTokens: prev.completionTokens + delta.completionTokens,
|
|
256
|
+
totalTokens: prev.totalTokens + delta.totalTokens,
|
|
257
|
+
...delta.reasoningTokens !== void 0 || prev.reasoningTokens !== void 0 ? { reasoningTokens: (prev.reasoningTokens ?? 0) + (delta.reasoningTokens ?? 0) } : {},
|
|
258
|
+
attemptCount: prev.attemptCount + 1,
|
|
259
|
+
...prev.cost !== void 0 ? { cost: prev.cost } : {}
|
|
260
|
+
} : {
|
|
261
|
+
...delta,
|
|
262
|
+
attemptCount: 1
|
|
263
|
+
};
|
|
264
|
+
state.usageByModel = current;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Recompute the aggregate usage from `usageByModel`. Returns the
|
|
268
|
+
* sum that callers can compare against `state.usage` to verify the
|
|
269
|
+
* per-step retry loop maintained the documented invariant.
|
|
270
|
+
*
|
|
271
|
+
* @stable
|
|
272
|
+
*/
|
|
273
|
+
function aggregateUsageFromByModel(byModel) {
|
|
274
|
+
if (byModel === void 0) return zeroUsage();
|
|
275
|
+
let pt = 0;
|
|
276
|
+
let ct = 0;
|
|
277
|
+
let tt = 0;
|
|
278
|
+
let rt = 0;
|
|
279
|
+
let hasReasoning = false;
|
|
280
|
+
for (const entry of Object.values(byModel)) {
|
|
281
|
+
pt += entry.promptTokens;
|
|
282
|
+
ct += entry.completionTokens;
|
|
283
|
+
tt += entry.totalTokens;
|
|
284
|
+
if (entry.reasoningTokens !== void 0) {
|
|
285
|
+
rt += entry.reasoningTokens;
|
|
286
|
+
hasReasoning = true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
promptTokens: pt,
|
|
291
|
+
completionTokens: ct,
|
|
292
|
+
totalTokens: tt,
|
|
293
|
+
...hasReasoning ? { reasoningTokens: rt } : {}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The "tools used" surface of a completed run. Cheap to compute
|
|
298
|
+
* from `RunState.steps`; surfaced as a stand-alone helper for
|
|
299
|
+
* Phase 17 example apps and operator-facing dashboards.
|
|
300
|
+
*
|
|
301
|
+
* @stable
|
|
302
|
+
*/
|
|
303
|
+
function completedToolCallsFromState(state) {
|
|
304
|
+
const out = [];
|
|
305
|
+
for (const step of state.steps) for (const tc of step.toolCalls) out.push(tc);
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
//#endregion
|
|
310
|
+
export { RUN_STATE_SCHEMA_MAJOR_SUPPORTED, RUN_STATE_SCHEMA_VERSION, addModelUsage, aggregateUsageFromByModel, completedToolCallsFromState, createInitialRunState, deserializeRunState, runStateFromJSON, runStateToJSON, serializeRunState };
|
|
311
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["out: SerializedRunState","usage: Usage","error: RunState['error']","usageByModel: RunStateUsageByModel | undefined","m: Record<string, Usage & { attemptCount: number }>","taintSummary: RunTaintSummary | undefined","parsed: unknown","out: CompletedToolCall[]"],"sources":["../../src/run-state/index.ts"],"sourcesContent":["/**\n * `RunState` JSON serialization and rehydration.\n *\n * The on-disk shape carries an explicit `version` field so future\n * schema bumps can detect older payloads. v0.1 ships\n * `'graphorin-run-state/1.0'` — additive fields that older readers\n * do not understand are ignored under the lenient-forward-parse\n * discipline.\n *\n * @packageDocumentation\n */\n\nimport type {\n CompletedToolCall,\n HandoffRecord,\n Message,\n RunState,\n RunStateUsageByModel,\n RunStatus,\n RunStep,\n RunTaintSummary,\n ToolApproval,\n Usage,\n} from '@graphorin/core';\nimport { zeroUsage } from '@graphorin/core';\nimport { RunStateMalformedError, RunStateVersionUnsupportedError } from '../errors/index.js';\n\n/**\n * Canonical schema id for serialized {@link RunState} payloads.\n *\n * @stable\n */\nexport const RUN_STATE_SCHEMA_VERSION = 'graphorin-run-state/1.1' as const;\n\n/**\n * Reader-supported schema id range. Major version 1 only for v0.1.\n *\n * @stable\n */\nexport const RUN_STATE_SCHEMA_MAJOR_SUPPORTED = 1;\n\n/**\n * On-disk payload returned by {@link serializeRunState} and accepted\n * by {@link deserializeRunState}. The shape is JSON-stable.\n *\n * @stable\n */\nexport interface SerializedRunState {\n readonly version: typeof RUN_STATE_SCHEMA_VERSION;\n readonly id: string;\n readonly agentId: string;\n readonly currentAgentId: string;\n readonly sessionId: string;\n readonly userId?: string;\n readonly status: RunStatus;\n readonly steps: ReadonlyArray<RunStep>;\n readonly messages: ReadonlyArray<Message>;\n readonly pendingApprovals: ReadonlyArray<ToolApproval>;\n readonly handoffs: ReadonlyArray<HandoffRecord>;\n readonly usage: Usage;\n readonly usageByModel?: RunStateUsageByModel;\n /** AG-19: coarse data-flow taint summary (no untrusted text). */\n readonly taintSummary?: RunTaintSummary;\n /** AG-19: deferred tools promoted by `tool_search` this run. */\n readonly promotedTools?: ReadonlyArray<string>;\n readonly startedAt: string;\n readonly finishedAt?: string;\n readonly error?: { readonly message: string; readonly code: string; readonly details?: unknown };\n}\n\n/**\n * Options accepted by {@link serializeRunState}.\n *\n * @stable\n */\nexport interface SerializeRunStateOptions {\n /**\n * Deep-redact secret-named keys (`apiKey`, `authorization`,\n * `bearerToken` / `accessToken` / `refreshToken`, `password`,\n * `secret`, …) anywhere in the snapshot — tool results and messages\n * included — replacing their values with `'[redacted]'`. Defaults to\n * `false` for the round-trip canonical helper; the agent runtime\n * passes `true` when persisting through the checkpoint store\n * (AG-23). Redaction is best-effort by key name: secrets stored\n * under unrelated keys are not detected.\n */\n readonly stripTracingApiKey?: boolean;\n}\n\n/**\n * Render a JSON-stable snapshot of the supplied {@link RunState}.\n * The returned value is plain JSON (no `Map`, `Set`, `Date`, ...).\n *\n * @stable\n */\nexport function serializeRunState(\n state: RunState,\n options: SerializeRunStateOptions = {},\n): SerializedRunState {\n const out: SerializedRunState = {\n version: RUN_STATE_SCHEMA_VERSION,\n id: state.id,\n agentId: state.agentId,\n currentAgentId: state.currentAgentId,\n sessionId: state.sessionId,\n ...(state.userId !== undefined ? { userId: state.userId } : {}),\n status: state.status,\n steps: state.steps,\n messages: state.messages,\n pendingApprovals: state.pendingApprovals,\n handoffs: state.handoffs,\n usage: state.usage,\n ...(state.usageByModel !== undefined ? { usageByModel: state.usageByModel } : {}),\n ...(state.taintSummary !== undefined ? { taintSummary: state.taintSummary } : {}),\n ...(state.promotedTools !== undefined ? { promotedTools: state.promotedTools } : {}),\n startedAt: state.startedAt,\n ...(state.finishedAt !== undefined ? { finishedAt: state.finishedAt } : {}),\n ...(state.error !== undefined ? { error: state.error } : {}),\n };\n // AG-23: the snapshot must be DETACHED from the live MutableRunState —\n // a post-suspend mutation must never reach an already-persisted\n // checkpoint. The JSON round-trip doubles as the plain-JSON guarantee\n // this function documents (no Map/Set/Date survive it).\n const detached = JSON.parse(JSON.stringify(out)) as SerializedRunState;\n if (options.stripTracingApiKey === true) {\n redactSecretKeysInPlace(detached);\n }\n return detached;\n}\n\n/**\n * Key names whose values are redacted by\n * `serializeRunState(state, { stripTracingApiKey: true })`.\n */\nconst SECRET_KEY_PATTERN =\n /^(api[-_]?key|tracing[-_]?api[-_]?key|x-api-key|authorization|bearer[-_]?token|access[-_]?token|refresh[-_]?token|secret|password|passphrase)$/i;\n\nconst REDACTED = '[redacted]';\n\nfunction redactSecretKeysInPlace(value: unknown): void {\n if (typeof value !== 'object' || value === null) return;\n if (Array.isArray(value)) {\n for (const item of value) redactSecretKeysInPlace(item);\n return;\n }\n const record = value as Record<string, unknown>;\n // Tool messages embed the tool output as a JSON STRING in `content` —\n // parse, redact, and re-stringify so a secret inside the string form\n // is caught too.\n if (record.role === 'tool' && typeof record.content === 'string') {\n record.content = redactJsonString(record.content);\n }\n for (const [key, inner] of Object.entries(record)) {\n if (SECRET_KEY_PATTERN.test(key)) {\n record[key] = REDACTED;\n continue;\n }\n redactSecretKeysInPlace(inner);\n }\n}\n\nfunction redactJsonString(content: string): string {\n try {\n const parsed = JSON.parse(content) as unknown;\n if (typeof parsed !== 'object' || parsed === null) return content;\n redactSecretKeysInPlace(parsed);\n return JSON.stringify(parsed);\n } catch {\n return content;\n }\n}\n\n/**\n * Render the canonical JSON string representation of the supplied\n * {@link RunState}. `JSON.stringify(serializeRunState(state))` —\n * provided as a convenience.\n *\n * @stable\n */\nexport function runStateToJSON(state: RunState, options?: SerializeRunStateOptions): string {\n return JSON.stringify(serializeRunState(state, options));\n}\n\ninterface DeserializeOptions {\n /**\n * Synthesize `usageByModel` from a v0.1-alpha state that omits\n * the field. Defaults to `true` so callers can rehydrate older\n * states without explicit migration.\n */\n readonly synthesizeUsageByModel?: boolean;\n /**\n * Logger callback for one-time INFO messages emitted on\n * backwards-compat synthesis. Defaults to a no-op.\n */\n readonly logger?: (message: string) => void;\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/**\n * Rehydrate a {@link RunState} from the on-disk payload. Throws\n * {@link RunStateVersionUnsupportedError} when the payload version\n * is from a future major; throws\n * {@link RunStateMalformedError} when the payload is structurally\n * invalid.\n *\n * Backwards-compat: a payload that omits `usageByModel` is accepted\n * and the field is synthesized from the aggregate `usage` with\n * `attemptCount: 1` for the primary model.\n *\n * @stable\n */\nexport function deserializeRunState(payload: unknown, options: DeserializeOptions = {}): RunState {\n if (!isRecord(payload)) {\n throw new RunStateMalformedError('payload must be an object');\n }\n const versionRaw = payload.version;\n if (typeof versionRaw !== 'string') {\n throw new RunStateMalformedError(\"missing 'version' field\");\n }\n const versionMatch = /^graphorin-run-state\\/(\\d+)\\.(\\d+)$/.exec(versionRaw);\n if (versionMatch === null) {\n throw new RunStateMalformedError(`unrecognized version '${versionRaw}'`);\n }\n const major = Number(versionMatch[1]);\n if (major > RUN_STATE_SCHEMA_MAJOR_SUPPORTED) {\n throw new RunStateVersionUnsupportedError(versionRaw, RUN_STATE_SCHEMA_VERSION);\n }\n const requiredString = (key: string): string => {\n const v = payload[key];\n if (typeof v !== 'string') {\n throw new RunStateMalformedError(`missing string '${key}' field`);\n }\n return v;\n };\n const requiredArray = <T>(key: string): T[] => {\n const v = payload[key];\n if (!Array.isArray(v)) {\n throw new RunStateMalformedError(`missing array '${key}' field`);\n }\n return v as T[];\n };\n const id = requiredString('id');\n const agentId = requiredString('agentId');\n const currentAgentId =\n typeof payload.currentAgentId === 'string' ? payload.currentAgentId : agentId;\n const sessionId = requiredString('sessionId');\n const status = requiredString('status') as RunStatus;\n const steps = requiredArray<RunStep>('steps');\n const messages = requiredArray<Message>('messages');\n const pendingApprovals = requiredArray<ToolApproval>('pendingApprovals');\n const handoffs = requiredArray<HandoffRecord>('handoffs');\n const usageRaw = payload.usage;\n if (!isRecord(usageRaw)) {\n throw new RunStateMalformedError(\"missing object 'usage' field\");\n }\n const usage: Usage = {\n promptTokens: Number(usageRaw.promptTokens ?? 0),\n completionTokens: Number(usageRaw.completionTokens ?? 0),\n totalTokens: Number(usageRaw.totalTokens ?? 0),\n ...(usageRaw.reasoningTokens !== undefined\n ? { reasoningTokens: Number(usageRaw.reasoningTokens) }\n : {}),\n ...(isRecord(usageRaw.cost)\n ? {\n cost: {\n amount: Number(usageRaw.cost.amount ?? 0),\n currency: String(usageRaw.cost.currency ?? 'USD'),\n },\n }\n : {}),\n };\n const startedAt = requiredString('startedAt');\n const userIdRaw = payload.userId;\n const finishedAtRaw = payload.finishedAt;\n const errorRaw = payload.error;\n let error: RunState['error'];\n if (isRecord(errorRaw)) {\n error = {\n message: typeof errorRaw.message === 'string' ? errorRaw.message : 'unknown',\n code: typeof errorRaw.code === 'string' ? errorRaw.code : 'unknown',\n ...(errorRaw.details !== undefined ? { details: errorRaw.details } : {}),\n };\n }\n\n // Backwards-compat synthesis for usageByModel.\n let usageByModel: RunStateUsageByModel | undefined;\n if (isRecord(payload.usageByModel)) {\n const m: Record<string, Usage & { attemptCount: number }> = {};\n for (const [modelId, entryRaw] of Object.entries(payload.usageByModel)) {\n if (!isRecord(entryRaw)) continue;\n m[modelId] = {\n promptTokens: Number(entryRaw.promptTokens ?? 0),\n completionTokens: Number(entryRaw.completionTokens ?? 0),\n totalTokens: Number(entryRaw.totalTokens ?? 0),\n attemptCount: Number(entryRaw.attemptCount ?? 1),\n ...(entryRaw.reasoningTokens !== undefined\n ? { reasoningTokens: Number(entryRaw.reasoningTokens) }\n : {}),\n ...(isRecord(entryRaw.cost)\n ? {\n cost: {\n amount: Number(entryRaw.cost.amount ?? 0),\n currency: String(entryRaw.cost.currency ?? 'USD'),\n },\n }\n : {}),\n };\n }\n usageByModel = m;\n } else if (options.synthesizeUsageByModel !== false && Number.isFinite(usage.totalTokens)) {\n usageByModel = {\n [agentId]: { ...usage, attemptCount: 1 },\n };\n options.logger?.(\n `[graphorin/agent] RunState v0.1-alpha synthesis: per-model breakdown derived from aggregate usage for agent '${agentId}'.`,\n );\n }\n\n // AG-19: rehydrate the coarse taint summary + promoted-tool set (additive;\n // absent on older v1.0 payloads).\n let taintSummary: RunTaintSummary | undefined;\n if (isRecord(payload.taintSummary)) {\n const ts = payload.taintSummary;\n taintSummary = {\n untrustedSeen: ts.untrustedSeen === true,\n sensitiveSeen: ts.sensitiveSeen === true,\n untrustedSourceKinds: Array.isArray(ts.untrustedSourceKinds)\n ? ts.untrustedSourceKinds.filter((k): k is string => typeof k === 'string')\n : [],\n };\n }\n const promotedTools = Array.isArray(payload.promotedTools)\n ? payload.promotedTools.filter((t): t is string => typeof t === 'string')\n : undefined;\n\n const out: RunState = {\n id,\n agentId,\n currentAgentId,\n sessionId,\n ...(typeof userIdRaw === 'string' ? { userId: userIdRaw } : {}),\n status,\n steps,\n messages,\n pendingApprovals,\n handoffs,\n usage,\n ...(usageByModel !== undefined ? { usageByModel } : {}),\n ...(taintSummary !== undefined ? { taintSummary } : {}),\n ...(promotedTools !== undefined ? { promotedTools } : {}),\n startedAt,\n ...(typeof finishedAtRaw === 'string' ? { finishedAt: finishedAtRaw } : {}),\n ...(error !== undefined ? { error } : {}),\n };\n void zeroUsage; // kept available; some callers prefer zeroUsage as a default.\n return out;\n}\n\n/** Convenience JSON-string parser pairing with {@link runStateToJSON}. */\nexport function runStateFromJSON(serialized: string, options?: DeserializeOptions): RunState {\n let parsed: unknown;\n try {\n parsed = JSON.parse(serialized);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n throw new RunStateMalformedError(`invalid JSON: ${message}`);\n }\n return deserializeRunState(parsed, options);\n}\n\n/**\n * Build a fresh, minimal {@link RunState} for a new run. Helper used\n * by `createAgent({...})` so consumers can construct deterministic\n * run state in tests.\n *\n * @stable\n */\nexport function createInitialRunState(args: {\n readonly id: string;\n readonly agentId: string;\n readonly sessionId: string;\n readonly userId?: string;\n readonly startedAt?: string;\n}): RunState {\n return {\n id: args.id,\n agentId: args.agentId,\n currentAgentId: args.agentId,\n sessionId: args.sessionId,\n ...(args.userId !== undefined ? { userId: args.userId } : {}),\n status: 'running',\n steps: [],\n messages: [],\n pendingApprovals: [],\n handoffs: [],\n usage: zeroUsage(),\n startedAt: args.startedAt ?? new Date().toISOString(),\n };\n}\n\n/**\n * Append a per-model usage entry to {@link RunState.usageByModel}.\n * Mutates the supplied state in place — used by the agent runtime's\n * per-step retry loop. Pure callers that need an immutable update\n * should clone the state first.\n *\n * @stable\n */\nexport function addModelUsage(state: RunState, modelId: string, delta: Usage): void {\n const current = (state.usageByModel ?? {}) as Record<string, Usage & { attemptCount: number }>;\n const prev = current[modelId];\n const merged: Usage & { attemptCount: number } = prev\n ? {\n promptTokens: prev.promptTokens + delta.promptTokens,\n completionTokens: prev.completionTokens + delta.completionTokens,\n totalTokens: prev.totalTokens + delta.totalTokens,\n ...(delta.reasoningTokens !== undefined || prev.reasoningTokens !== undefined\n ? {\n reasoningTokens: (prev.reasoningTokens ?? 0) + (delta.reasoningTokens ?? 0),\n }\n : {}),\n attemptCount: prev.attemptCount + 1,\n ...(prev.cost !== undefined ? { cost: prev.cost } : {}),\n }\n : { ...delta, attemptCount: 1 };\n current[modelId] = merged;\n // Mutate via the writable typing — `usageByModel` is declared\n // optional; the runtime owns the field's lifecycle.\n (state as { usageByModel?: RunStateUsageByModel }).usageByModel = current;\n}\n\n/**\n * Recompute the aggregate usage from `usageByModel`. Returns the\n * sum that callers can compare against `state.usage` to verify the\n * per-step retry loop maintained the documented invariant.\n *\n * @stable\n */\nexport function aggregateUsageFromByModel(byModel: RunStateUsageByModel | undefined): Usage {\n if (byModel === undefined) return zeroUsage();\n let pt = 0;\n let ct = 0;\n let tt = 0;\n let rt = 0;\n let hasReasoning = false;\n for (const entry of Object.values(byModel)) {\n pt += entry.promptTokens;\n ct += entry.completionTokens;\n tt += entry.totalTokens;\n if (entry.reasoningTokens !== undefined) {\n rt += entry.reasoningTokens;\n hasReasoning = true;\n }\n }\n return {\n promptTokens: pt,\n completionTokens: ct,\n totalTokens: tt,\n ...(hasReasoning ? { reasoningTokens: rt } : {}),\n };\n}\n\n/**\n * The \"tools used\" surface of a completed run. Cheap to compute\n * from `RunState.steps`; surfaced as a stand-alone helper for\n * Phase 17 example apps and operator-facing dashboards.\n *\n * @stable\n */\nexport function completedToolCallsFromState(state: RunState): ReadonlyArray<CompletedToolCall> {\n const out: CompletedToolCall[] = [];\n for (const step of state.steps) {\n for (const tc of step.toolCalls) out.push(tc);\n }\n return out;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { countTokensHeuristic } from "@graphorin/tools/result";
|
|
2
|
+
import { createGuard } from "@graphorin/security/guard";
|
|
3
|
+
import "@graphorin/security/sandbox";
|
|
4
|
+
import { parseSecretRef, resolveSecret } from "@graphorin/security/secrets";
|
|
5
|
+
|
|
6
|
+
//#region src/tooling/adapters.ts
|
|
7
|
+
/**
|
|
8
|
+
* Adapter A — build the executor's `secretResolver` hook.
|
|
9
|
+
*
|
|
10
|
+
* This is a thin **value resolver**. The executor's secrets accessor
|
|
11
|
+
* (`@graphorin/tools` `tool-context.ts`) already (1) enforces the
|
|
12
|
+
* per-tool `secretsAllowed` ACL via `enforceSecretAcl(key)` inside a
|
|
13
|
+
* `withChildToolSecretsContext(...)` scope and (2) maps a `null` result
|
|
14
|
+
* onto the optional/required-secret error contract. This adapter must
|
|
15
|
+
* therefore **not** re-implement ACL — it only resolves a key to a
|
|
16
|
+
* value.
|
|
17
|
+
*/
|
|
18
|
+
function buildSecretResolver(options = {}) {
|
|
19
|
+
const backend = options.backend ?? (async (key) => resolveSecret(parseSecretRef(key)));
|
|
20
|
+
return Object.freeze({ resolve: backend });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Adapter C — build the executor's `memoryGuardFactory` (+ an optional
|
|
24
|
+
* `memoryRegionReader`) from the agent's configured `Memory`.
|
|
25
|
+
*
|
|
26
|
+
* When `memory` is `undefined`, the factory returns `null` for every
|
|
27
|
+
* tier and no reader is supplied — the executor degrades to its
|
|
28
|
+
* audit-only baseline (the guard step is skipped).
|
|
29
|
+
*/
|
|
30
|
+
function buildMemoryGuard(memory, options = {}) {
|
|
31
|
+
if (memory === void 0) return Object.freeze({ memoryGuardFactory: () => null });
|
|
32
|
+
const memoryGuardFactory = (tier) => {
|
|
33
|
+
switch (tier) {
|
|
34
|
+
case "pure":
|
|
35
|
+
case "side-effecting-no-memory": return createGuard({ tier });
|
|
36
|
+
case "unknown": return createGuard({
|
|
37
|
+
tier,
|
|
38
|
+
...options.auditOnly ?? {}
|
|
39
|
+
});
|
|
40
|
+
case "untrusted": return createGuard({
|
|
41
|
+
tier,
|
|
42
|
+
...options.strictFull ?? {}
|
|
43
|
+
});
|
|
44
|
+
case "memory-aware": return options.apiBoundary === void 0 ? null : createGuard({
|
|
45
|
+
tier,
|
|
46
|
+
...options.apiBoundary
|
|
47
|
+
});
|
|
48
|
+
default: return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return Object.freeze(options.regionReader === void 0 ? { memoryGuardFactory } : {
|
|
52
|
+
memoryGuardFactory,
|
|
53
|
+
memoryRegionReader: options.regionReader
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Construct a `MemoryRegionReader` from an explicit region list and a
|
|
58
|
+
* read function. WI-03 uses this to bind the agent's `Memory` tiers to
|
|
59
|
+
* named regions once a run scope exists; exposed here so the reader
|
|
60
|
+
* contract is unit-testable in isolation.
|
|
61
|
+
*/
|
|
62
|
+
function createMemoryRegionReader(regions, read) {
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
regions: Object.freeze([...regions]),
|
|
65
|
+
read
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Adapter D — build the **synchronous** token counter the executor's
|
|
70
|
+
* truncation pipeline requires.
|
|
71
|
+
*
|
|
72
|
+
* DESIGN DECISION (impedance mismatch §1.6.1): `@graphorin/tools`'
|
|
73
|
+
* `TokenCounter.count(text): number` is synchronous, while
|
|
74
|
+
* `@graphorin/provider`'s `createDefaultCounter().count(input): Promise<number>`
|
|
75
|
+
* is asynchronous (and also accepts `Message[]`). The truncation path
|
|
76
|
+
* runs inside `executeBatch` and must not `await`, so we never wire the
|
|
77
|
+
* provider's async counter here. We default to the tools heuristic
|
|
78
|
+
* (deterministic, offline) and allow a *synchronous* tokenizer (e.g.
|
|
79
|
+
* js-tiktoken's `encode`) to be injected when a caller wants
|
|
80
|
+
* vendor-accurate counts without blocking. The async provider counter
|
|
81
|
+
* stays reserved for budget accounting elsewhere (WI-09), not for
|
|
82
|
+
* executor truncation.
|
|
83
|
+
*/
|
|
84
|
+
function buildToolTokenCounter(options = {}) {
|
|
85
|
+
const { tokenize } = options;
|
|
86
|
+
if (tokenize === void 0) return countTokensHeuristic;
|
|
87
|
+
return Object.freeze({ count: (text) => tokenize(text).length });
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Adapter E — bridge the executor's `streamingSink` callback to an
|
|
91
|
+
* async iterator via a bounded queue.
|
|
92
|
+
*/
|
|
93
|
+
function createExecutorEventBridge(options = {}) {
|
|
94
|
+
const queueDepth = options.queueDepth ?? 256;
|
|
95
|
+
const buffer = [];
|
|
96
|
+
let pending = null;
|
|
97
|
+
let closed = false;
|
|
98
|
+
let dropped = 0;
|
|
99
|
+
const sink = (event) => {
|
|
100
|
+
if (closed) return;
|
|
101
|
+
if (pending !== null) {
|
|
102
|
+
const resolve = pending;
|
|
103
|
+
pending = null;
|
|
104
|
+
resolve({
|
|
105
|
+
value: event,
|
|
106
|
+
done: false
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
buffer.push(event);
|
|
111
|
+
if (buffer.length > queueDepth) {
|
|
112
|
+
buffer.shift();
|
|
113
|
+
dropped += 1;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
async function* drain() {
|
|
117
|
+
for (;;) {
|
|
118
|
+
if (buffer.length > 0) {
|
|
119
|
+
const event = buffer.shift();
|
|
120
|
+
if (event !== void 0) yield event;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (closed) return;
|
|
124
|
+
const next = await new Promise((resolve) => {
|
|
125
|
+
pending = resolve;
|
|
126
|
+
});
|
|
127
|
+
if (next.done === true) return;
|
|
128
|
+
yield next.value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const close = () => {
|
|
132
|
+
closed = true;
|
|
133
|
+
if (pending !== null) {
|
|
134
|
+
const resolve = pending;
|
|
135
|
+
pending = null;
|
|
136
|
+
resolve({
|
|
137
|
+
value: void 0,
|
|
138
|
+
done: true
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
sink,
|
|
144
|
+
drain,
|
|
145
|
+
close,
|
|
146
|
+
get dropped() {
|
|
147
|
+
return dropped;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
153
|
+
export { buildMemoryGuard, buildSecretResolver, buildToolTokenCounter, createExecutorEventBridge, createMemoryRegionReader };
|
|
154
|
+
//# sourceMappingURL=adapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.js","names":["backend: SecretBackend","memoryGuardFactory: MemoryGuardFactory","buffer: T[]","pending: ((result: IteratorResult<T, undefined>) => void) | null"],"sources":["../../src/tooling/adapters.ts"],"sourcesContent":["/**\n * Integration adapters (A–E) that let the `@graphorin/tools` executor\n * consume the agent's `@graphorin/security` / `@graphorin/memory` /\n * `@graphorin/provider` surfaces.\n *\n * These are built and unit-tested in isolation (WI-01). The run loop\n * wires them into `createToolExecutor(...)` in a later work item\n * (WI-03); nothing here is exported from the package entrypoint yet.\n *\n * Every adapter is typed against the executor's `ExecutorOptions` via\n * indexed access, so the values they produce are guaranteed assignable\n * to the hooks `createToolExecutor(...)` expects — without depending on\n * which individual hook types `@graphorin/tools` happens to re-export.\n *\n * @packageDocumentation\n */\n\nimport type { Memory } from '@graphorin/memory';\nimport {\n type ApiBoundaryGuardOptions,\n type AuditOnlyGuardOptions,\n createGuard,\n type StrictFullGuardOptions,\n} from '@graphorin/security/guard';\nimport {\n createDockerSandbox,\n createIsolatedVMSandbox,\n createWorkerThreadsSandbox,\n type SandboxImpl,\n} from '@graphorin/security/sandbox';\nimport { parseSecretRef, resolveSecret } from '@graphorin/security/secrets';\nimport type { ExecutorOptions } from '@graphorin/tools/executor';\nimport { countTokensHeuristic } from '@graphorin/tools/result';\n\n// --- Executor hook shapes (derived from `ExecutorOptions`) ------------------\n// Indexed access keeps these in lock-step with what the executor expects.\n\n/** The executor's secret-resolver hook: `{ resolve(key): Promise<SecretValue | null> }`. */\ntype SecretResolver = NonNullable<ExecutorOptions['secretResolver']>;\n/** A resolved `SecretValue`, or `null` when the backing store has no value. */\ntype SecretValueOrNull = Awaited<ReturnType<SecretResolver['resolve']>>;\n/** The executor's sandbox-dispatch resolver: `(policy) => Sandbox | null`. */\ntype SandboxResolver = NonNullable<ExecutorOptions['sandboxResolver']>;\n/** The executor's memory-guard factory: `(tier) => MemoryModificationGuard | null`. */\ntype MemoryGuardFactory = NonNullable<ExecutorOptions['memoryGuardFactory']>;\n/** The discriminator the guard factory receives (`'pure' | … | 'untrusted'`). */\ntype MemoryGuardTier = Parameters<MemoryGuardFactory>[0];\n/** A guard instance, or `null` when the tier needs no guard / cannot be built. */\ntype MemoryModificationGuardOrNull = ReturnType<MemoryGuardFactory>;\n/** The reader the guard hashes pre/post execution: `{ regions; read(region) }`. */\ntype MemoryRegionReader = NonNullable<ExecutorOptions['memoryRegionReader']>;\n/** The executor's **synchronous** truncation token counter: `{ count(text): number }`. */\ntype ToolTokenCounter = NonNullable<ExecutorOptions['tokenCounter']>;\n/** A value the executor's `streamingSink` receives (a `tool.execute.*` event). */\ntype ExecutorEvent = Parameters<NonNullable<ExecutorOptions['streamingSink']>>[0];\n\n// ---------------------------------------------------------------------------\n// Adapter A — `secretResolver`\n// ---------------------------------------------------------------------------\n\n/**\n * Backend that turns a secret reference into a resolved value (or\n * `null` when the backing store represents \"absent\" as null). Defaults\n * to `@graphorin/security`'s global resolver registry\n * (`resolveSecret(parseSecretRef(key))`), which **rejects** on a\n * malformed ref / unknown scheme / resolution failure and never returns\n * null — those rejections surface as tool errors through the executor's\n * secrets accessor.\n */\nexport type SecretBackend = (key: string) => Promise<SecretValueOrNull>;\n\n/** Options for {@link buildSecretResolver}. */\nexport interface SecretResolverOptions {\n /** Resolution backend. Defaults to `resolveSecret(parseSecretRef(key))`. */\n readonly backend?: SecretBackend;\n}\n\n/**\n * Adapter A — build the executor's `secretResolver` hook.\n *\n * This is a thin **value resolver**. The executor's secrets accessor\n * (`@graphorin/tools` `tool-context.ts`) already (1) enforces the\n * per-tool `secretsAllowed` ACL via `enforceSecretAcl(key)` inside a\n * `withChildToolSecretsContext(...)` scope and (2) maps a `null` result\n * onto the optional/required-secret error contract. This adapter must\n * therefore **not** re-implement ACL — it only resolves a key to a\n * value.\n */\nexport function buildSecretResolver(options: SecretResolverOptions = {}): SecretResolver {\n const backend: SecretBackend =\n options.backend ?? (async (key) => resolveSecret(parseSecretRef(key)));\n return Object.freeze({ resolve: backend });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter B — `sandboxResolver`\n// ---------------------------------------------------------------------------\n\n/** Isolation kinds the default resolver knows how to construct. */\ntype IsolatedKind = 'worker-threads' | 'isolated-vm' | 'docker';\n\n/** Factory producing a concrete sandbox for a given isolation kind. */\nexport type SandboxFactory = () => SandboxImpl;\n\n/** Per-kind sandbox factory overrides (e.g. fakes in tests, custom adapters). */\nexport type SandboxFactoryMap = Partial<Record<IsolatedKind, SandboxFactory>>;\n\n/** Options for {@link buildSandboxResolver}. */\nexport interface SandboxResolverOptions {\n /**\n * Per-kind factory overrides. Defaults wire the three out-of-process\n * `@graphorin/security` adapters. Tests inject fakes so unit runs\n * never spawn real worker threads / containers (offline; R4). The\n * factories are invoked **lazily** — never at build time — so the\n * production default costs nothing until a tool actually needs that\n * isolation kind.\n */\n readonly factories?: SandboxFactoryMap;\n}\n\n/**\n * Adapter B — build the executor's `sandboxResolver`.\n *\n * Maps a `ResolvedSandboxPolicy.kind` to a cached `SandboxImpl`,\n * lazily constructing **one instance per kind**. Returns `null` for\n * `'none'` (run the tool inline) and for any kind without a registered\n * factory (custom kinds need a custom resolver).\n */\nexport function buildSandboxResolver(options: SandboxResolverOptions = {}): SandboxResolver {\n const factories: Record<IsolatedKind, SandboxFactory> = {\n 'worker-threads': options.factories?.['worker-threads'] ?? (() => createWorkerThreadsSandbox()),\n 'isolated-vm': options.factories?.['isolated-vm'] ?? (() => createIsolatedVMSandbox()),\n docker: options.factories?.docker ?? (() => createDockerSandbox()),\n };\n // Look up factories by the (open) policy kind. `SandboxKind` carries a\n // `(string & {})` member that defeats literal narrowing, so we index a\n // string-keyed view: a miss (incl. `'none'` and custom kinds) runs inline.\n const lookup = factories as Record<string, SandboxFactory | undefined>;\n const cache = new Map<string, SandboxImpl>();\n\n return (policy) => {\n const kind = policy.kind;\n const factory = lookup[kind];\n if (factory === undefined) {\n return null;\n }\n let impl = cache.get(kind);\n if (impl === undefined) {\n impl = factory();\n cache.set(kind, impl);\n }\n return impl;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Adapter C — `memoryGuardFactory` + `memoryRegionReader`\n// ---------------------------------------------------------------------------\n\n/** Options for {@link buildMemoryGuard}. */\nexport interface MemoryGuardOptions {\n /**\n * Options for the `'memory-aware'` API-boundary guard. The guard\n * requires an operation allowlist + a recorder of observed\n * `<scope>.<op>` calls, both supplied by the runtime once a run scope\n * exists (WI-03). When omitted, the factory returns `null` for the\n * `'memory-aware'` tier and the executor skips the guard.\n */\n readonly apiBoundary?: ApiBoundaryGuardOptions;\n /** Options for the `'unknown'` (audit-only) guard. */\n readonly auditOnly?: AuditOnlyGuardOptions;\n /** Options for the `'untrusted'` (strict-full) guard. */\n readonly strictFull?: StrictFullGuardOptions;\n /**\n * The region reader the guard hashes pre/post execution. The reader\n * is **scope-bound** (it materialises memory regions for a specific\n * run scope), so the runtime supplies it in WI-03 — the `Memory`\n * facade exposes no scope-free read surface. When omitted, the\n * executor skips the snapshot/verify cycle (it only runs the guard\n * when the reader is present).\n */\n readonly regionReader?: MemoryRegionReader;\n}\n\n/** The executor wiring produced by {@link buildMemoryGuard}. */\nexport interface MemoryGuardWiring {\n readonly memoryGuardFactory: MemoryGuardFactory;\n readonly memoryRegionReader?: MemoryRegionReader;\n}\n\n/**\n * Adapter C — build the executor's `memoryGuardFactory` (+ an optional\n * `memoryRegionReader`) from the agent's configured `Memory`.\n *\n * When `memory` is `undefined`, the factory returns `null` for every\n * tier and no reader is supplied — the executor degrades to its\n * audit-only baseline (the guard step is skipped).\n */\nexport function buildMemoryGuard(\n memory: Memory | undefined,\n options: MemoryGuardOptions = {},\n): MemoryGuardWiring {\n if (memory === undefined) {\n return Object.freeze({ memoryGuardFactory: () => null });\n }\n\n const memoryGuardFactory: MemoryGuardFactory = (\n tier: MemoryGuardTier,\n ): MemoryModificationGuardOrNull => {\n switch (tier) {\n case 'pure':\n case 'side-effecting-no-memory':\n return createGuard({ tier });\n case 'unknown':\n return createGuard({ tier, ...(options.auditOnly ?? {}) });\n case 'untrusted':\n return createGuard({ tier, ...(options.strictFull ?? {}) });\n case 'memory-aware':\n return options.apiBoundary === undefined\n ? null\n : createGuard({ tier, ...options.apiBoundary });\n default:\n return null;\n }\n };\n\n return Object.freeze(\n options.regionReader === undefined\n ? { memoryGuardFactory }\n : { memoryGuardFactory, memoryRegionReader: options.regionReader },\n );\n}\n\n/**\n * Construct a `MemoryRegionReader` from an explicit region list and a\n * read function. WI-03 uses this to bind the agent's `Memory` tiers to\n * named regions once a run scope exists; exposed here so the reader\n * contract is unit-testable in isolation.\n */\nexport function createMemoryRegionReader(\n regions: ReadonlyArray<string>,\n read: (region: string) => Promise<Uint8Array | string>,\n): MemoryRegionReader {\n return Object.freeze({ regions: Object.freeze([...regions]), read });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter D — `tokenCounter` (the sync/async impedance mismatch)\n// ---------------------------------------------------------------------------\n\n/** A synchronous tokenizer (e.g. js-tiktoken's `encode`) returning a token array. */\nexport type SyncTokenize = (text: string) => { readonly length: number };\n\n/** Options for {@link buildToolTokenCounter}. */\nexport interface ToolTokenCounterOptions {\n /**\n * Optional synchronous tokenizer. When provided, the token count is\n * `tokenize(text).length`. Defaults to the `@graphorin/tools`\n * heuristic (4 chars/token).\n */\n readonly tokenize?: SyncTokenize;\n}\n\n/**\n * Adapter D — build the **synchronous** token counter the executor's\n * truncation pipeline requires.\n *\n * DESIGN DECISION (impedance mismatch §1.6.1): `@graphorin/tools`'\n * `TokenCounter.count(text): number` is synchronous, while\n * `@graphorin/provider`'s `createDefaultCounter().count(input): Promise<number>`\n * is asynchronous (and also accepts `Message[]`). The truncation path\n * runs inside `executeBatch` and must not `await`, so we never wire the\n * provider's async counter here. We default to the tools heuristic\n * (deterministic, offline) and allow a *synchronous* tokenizer (e.g.\n * js-tiktoken's `encode`) to be injected when a caller wants\n * vendor-accurate counts without blocking. The async provider counter\n * stays reserved for budget accounting elsewhere (WI-09), not for\n * executor truncation.\n */\nexport function buildToolTokenCounter(options: ToolTokenCounterOptions = {}): ToolTokenCounter {\n const { tokenize } = options;\n if (tokenize === undefined) {\n return countTokensHeuristic;\n }\n return Object.freeze({\n count: (text: string): number => tokenize(text).length,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter E — `streamingSink` → `AgentEvent` stream bridge\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createExecutorEventBridge}. */\nexport interface ExecutorEventBridgeOptions {\n /**\n * Maximum buffered events before backpressure drops the **oldest**\n * event (preserving the most-recent window). Defaults to `256`, matching\n * the executor's `streamingEventQueueDepth` default. Under normal\n * operation the consumer drains faster than the producer and nothing\n * is ever dropped; this bound is a safety valve.\n */\n readonly queueDepth?: number;\n}\n\n/**\n * Bridges the executor's synchronous `streamingSink` callback into an\n * async iterable the run loop can `for await` over.\n *\n * Single-producer / single-consumer: `sink` is the producer (called by\n * the executor); one `drain()` iterator is the consumer (the loop). It\n * is generic over the event type (defaulting to the executor's\n * `ExecutorEvent`) so the queue mechanics can be exercised in isolation.\n */\nexport interface ExecutorEventBridge<T = ExecutorEvent> {\n /**\n * Synchronous sink handed to the executor's `streamingSink`. Never\n * throws and never blocks; events emitted after {@link close} are\n * dropped.\n */\n readonly sink: (event: T) => void;\n /**\n * Async iterable yielding events in arrival order. Completes once\n * {@link close} has been called **and** the buffer is drained.\n */\n drain(): AsyncIterableIterator<T>;\n /** Signal end-of-stream; `drain()` finishes after the buffer drains. */\n close(): void;\n /** Count of events dropped under backpressure (oldest-dropped). */\n readonly dropped: number;\n}\n\n/**\n * Adapter E — bridge the executor's `streamingSink` callback to an\n * async iterator via a bounded queue.\n */\nexport function createExecutorEventBridge<T = ExecutorEvent>(\n options: ExecutorEventBridgeOptions = {},\n): ExecutorEventBridge<T> {\n const queueDepth = options.queueDepth ?? 256;\n const buffer: T[] = [];\n let pending: ((result: IteratorResult<T, undefined>) => void) | null = null;\n let closed = false;\n let dropped = 0;\n\n const sink = (event: T): void => {\n if (closed) {\n return;\n }\n if (pending !== null) {\n // A consumer is parked — hand the event off directly.\n const resolve = pending;\n pending = null;\n resolve({ value: event, done: false });\n return;\n }\n buffer.push(event);\n if (buffer.length > queueDepth) {\n buffer.shift();\n dropped += 1;\n }\n };\n\n async function* drain(): AsyncIterableIterator<T> {\n for (;;) {\n if (buffer.length > 0) {\n const event = buffer.shift();\n if (event !== undefined) {\n yield event;\n }\n continue;\n }\n if (closed) {\n return;\n }\n const next = await new Promise<IteratorResult<T, undefined>>((resolve) => {\n pending = resolve;\n });\n if (next.done === true) {\n return;\n }\n yield next.value;\n }\n }\n\n const close = (): void => {\n closed = true;\n if (pending !== null) {\n const resolve = pending;\n pending = null;\n resolve({ value: undefined, done: true });\n }\n };\n\n return Object.freeze({\n sink,\n drain,\n close,\n get dropped(): number {\n return dropped;\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAwFA,SAAgB,oBAAoB,UAAiC,EAAE,EAAkB;CACvF,MAAMA,UACJ,QAAQ,YAAY,OAAO,QAAQ,cAAc,eAAe,IAAI,CAAC;AACvE,QAAO,OAAO,OAAO,EAAE,SAAS,SAAS,CAAC;;;;;;;;;;AA2G5C,SAAgB,iBACd,QACA,UAA8B,EAAE,EACb;AACnB,KAAI,WAAW,OACb,QAAO,OAAO,OAAO,EAAE,0BAA0B,MAAM,CAAC;CAG1D,MAAMC,sBACJ,SACkC;AAClC,UAAQ,MAAR;GACE,KAAK;GACL,KAAK,2BACH,QAAO,YAAY,EAAE,MAAM,CAAC;GAC9B,KAAK,UACH,QAAO,YAAY;IAAE;IAAM,GAAI,QAAQ,aAAa,EAAE;IAAG,CAAC;GAC5D,KAAK,YACH,QAAO,YAAY;IAAE;IAAM,GAAI,QAAQ,cAAc,EAAE;IAAG,CAAC;GAC7D,KAAK,eACH,QAAO,QAAQ,gBAAgB,SAC3B,OACA,YAAY;IAAE;IAAM,GAAG,QAAQ;IAAa,CAAC;GACnD,QACE,QAAO;;;AAIb,QAAO,OAAO,OACZ,QAAQ,iBAAiB,SACrB,EAAE,oBAAoB,GACtB;EAAE;EAAoB,oBAAoB,QAAQ;EAAc,CACrE;;;;;;;;AASH,SAAgB,yBACd,SACA,MACoB;AACpB,QAAO,OAAO,OAAO;EAAE,SAAS,OAAO,OAAO,CAAC,GAAG,QAAQ,CAAC;EAAE;EAAM,CAAC;;;;;;;;;;;;;;;;;;AAoCtE,SAAgB,sBAAsB,UAAmC,EAAE,EAAoB;CAC7F,MAAM,EAAE,aAAa;AACrB,KAAI,aAAa,OACf,QAAO;AAET,QAAO,OAAO,OAAO,EACnB,QAAQ,SAAyB,SAAS,KAAK,CAAC,QACjD,CAAC;;;;;;AAkDJ,SAAgB,0BACd,UAAsC,EAAE,EAChB;CACxB,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAMC,SAAc,EAAE;CACtB,IAAIC,UAAmE;CACvE,IAAI,SAAS;CACb,IAAI,UAAU;CAEd,MAAM,QAAQ,UAAmB;AAC/B,MAAI,OACF;AAEF,MAAI,YAAY,MAAM;GAEpB,MAAM,UAAU;AAChB,aAAU;AACV,WAAQ;IAAE,OAAO;IAAO,MAAM;IAAO,CAAC;AACtC;;AAEF,SAAO,KAAK,MAAM;AAClB,MAAI,OAAO,SAAS,YAAY;AAC9B,UAAO,OAAO;AACd,cAAW;;;CAIf,gBAAgB,QAAkC;AAChD,WAAS;AACP,OAAI,OAAO,SAAS,GAAG;IACrB,MAAM,QAAQ,OAAO,OAAO;AAC5B,QAAI,UAAU,OACZ,OAAM;AAER;;AAEF,OAAI,OACF;GAEF,MAAM,OAAO,MAAM,IAAI,SAAuC,YAAY;AACxE,cAAU;KACV;AACF,OAAI,KAAK,SAAS,KAChB;AAEF,SAAM,KAAK;;;CAIf,MAAM,cAAoB;AACxB,WAAS;AACT,MAAI,YAAY,MAAM;GACpB,MAAM,UAAU;AAChB,aAAU;AACV,WAAQ;IAAE,OAAO;IAAW,MAAM;IAAM,CAAC;;;AAI7C,QAAO,OAAO,OAAO;EACnB;EACA;EACA;EACA,IAAI,UAAkB;AACpB,UAAO;;EAEV,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/tooling/catalogue.ts
|
|
2
|
+
/**
|
|
3
|
+
* A7 (SOTA): prompt-cache-aware tool-catalogue ordering.
|
|
4
|
+
*
|
|
5
|
+
* The per-step catalogue is `[...eagerTools, ...promotedTools, ...handoffTools]`.
|
|
6
|
+
* The eager prefix is already stable (the registry lists in insertion order), so
|
|
7
|
+
* the only source of churn is the promoted section: building it from the
|
|
8
|
+
* registry's `listDeferred` order means a tool promoted on a later step can sort
|
|
9
|
+
* BEFORE one promoted earlier, shifting the serialized suffix and invalidating
|
|
10
|
+
* the provider's prompt-cache prefix on every subsequent step.
|
|
11
|
+
*
|
|
12
|
+
* `orderPromotedTools` instead emits promoted tools in PROMOTION order (the
|
|
13
|
+
* insertion order of the promoted-name set), so a newly promoted tool always
|
|
14
|
+
* joins the END — the catalogue grows append-only and earlier promotions keep
|
|
15
|
+
* their byte position. A long-running assistant re-pays only for the new tail.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Resolve `promotedNames` (insertion-ordered) to the matching `deferred`
|
|
21
|
+
* entries, preserving promotion order. Names absent from the pool (e.g. a tool
|
|
22
|
+
* removed mid-run) are skipped.
|
|
23
|
+
*/
|
|
24
|
+
function orderPromotedTools(promotedNames, deferred) {
|
|
25
|
+
const byName = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const tool of deferred) byName.set(tool.name, tool);
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const name of promotedNames) {
|
|
29
|
+
const tool = byName.get(name);
|
|
30
|
+
if (tool !== void 0) out.push(tool);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { orderPromotedTools };
|
|
37
|
+
//# sourceMappingURL=catalogue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalogue.js","names":["out: T[]"],"sources":["../../src/tooling/catalogue.ts"],"sourcesContent":["/**\n * A7 (SOTA): prompt-cache-aware tool-catalogue ordering.\n *\n * The per-step catalogue is `[...eagerTools, ...promotedTools, ...handoffTools]`.\n * The eager prefix is already stable (the registry lists in insertion order), so\n * the only source of churn is the promoted section: building it from the\n * registry's `listDeferred` order means a tool promoted on a later step can sort\n * BEFORE one promoted earlier, shifting the serialized suffix and invalidating\n * the provider's prompt-cache prefix on every subsequent step.\n *\n * `orderPromotedTools` instead emits promoted tools in PROMOTION order (the\n * insertion order of the promoted-name set), so a newly promoted tool always\n * joins the END — the catalogue grows append-only and earlier promotions keep\n * their byte position. A long-running assistant re-pays only for the new tail.\n *\n * @packageDocumentation\n */\n\n/**\n * Resolve `promotedNames` (insertion-ordered) to the matching `deferred`\n * entries, preserving promotion order. Names absent from the pool (e.g. a tool\n * removed mid-run) are skipped.\n */\nexport function orderPromotedTools<T extends { readonly name: string }>(\n promotedNames: Iterable<string>,\n deferred: ReadonlyArray<T>,\n): ReadonlyArray<T> {\n const byName = new Map<string, T>();\n for (const tool of deferred) byName.set(tool.name, tool);\n const out: T[] = [];\n for (const name of promotedNames) {\n const tool = byName.get(name);\n if (tool !== undefined) out.push(tool);\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,mBACd,eACA,UACkB;CAClB,MAAM,yBAAS,IAAI,KAAgB;AACnC,MAAK,MAAM,QAAQ,SAAU,QAAO,IAAI,KAAK,MAAM,KAAK;CACxD,MAAMA,MAAW,EAAE;AACnB,MAAK,MAAM,QAAQ,eAAe;EAChC,MAAM,OAAO,OAAO,IAAI,KAAK;AAC7B,MAAI,SAAS,OAAW,KAAI,KAAK,KAAK;;AAExC,QAAO"}
|