@graphorin/agent 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +31 -11
- package/dist/errors/index.d.ts +17 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +19 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +153 -1930
- package/dist/factory.js.map +1 -1
- package/dist/fanout/index.d.ts +13 -1
- package/dist/fanout/index.d.ts.map +1 -1
- package/dist/fanout/index.js +13 -4
- package/dist/fanout/index.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/lateral-leak/index.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/run-state/index.d.ts +32 -6
- package/dist/run-state/index.d.ts.map +1 -1
- package/dist/run-state/index.js +46 -22
- package/dist/run-state/index.js.map +1 -1
- package/dist/runtime/agent-surface.js +122 -0
- package/dist/runtime/agent-surface.js.map +1 -0
- package/dist/runtime/agent-to-tool.d.ts +51 -0
- package/dist/runtime/agent-to-tool.d.ts.map +1 -0
- package/dist/runtime/agent-to-tool.js +145 -0
- package/dist/runtime/agent-to-tool.js.map +1 -0
- package/dist/runtime/approvals.js +0 -0
- package/dist/runtime/approvals.js.map +1 -0
- package/dist/runtime/dispatch.js +108 -0
- package/dist/runtime/dispatch.js.map +1 -0
- package/dist/runtime/executor-wiring.js +128 -0
- package/dist/runtime/executor-wiring.js.map +1 -0
- package/dist/runtime/fallback-chain.js +139 -0
- package/dist/runtime/fallback-chain.js.map +1 -0
- package/dist/runtime/handoff.js +307 -0
- package/dist/runtime/handoff.js.map +1 -0
- package/dist/runtime/messages.d.ts +22 -0
- package/dist/runtime/messages.d.ts.map +1 -0
- package/dist/runtime/messages.js +204 -0
- package/dist/runtime/messages.js.map +1 -0
- package/dist/runtime/provider-events.js +117 -0
- package/dist/runtime/provider-events.js.map +1 -0
- package/dist/runtime/run-compaction.js +210 -0
- package/dist/runtime/run-compaction.js.map +1 -0
- package/dist/runtime/run-finish.js +48 -0
- package/dist/runtime/run-finish.js.map +1 -0
- package/dist/runtime/run-gates.js +336 -0
- package/dist/runtime/run-gates.js.map +1 -0
- package/dist/runtime/run-init.js +81 -0
- package/dist/runtime/run-init.js.map +1 -0
- package/dist/runtime/run-input.js +46 -0
- package/dist/runtime/run-input.js.map +1 -0
- package/dist/runtime/step-catalogue.js +173 -0
- package/dist/runtime/step-catalogue.js.map +1 -0
- package/dist/runtime/tool-call-walk.js +189 -0
- package/dist/runtime/tool-call-walk.js.map +1 -0
- package/dist/runtime/tool-wiring.js +159 -0
- package/dist/runtime/tool-wiring.js.map +1 -0
- package/dist/tooling/adapters.js +1 -1
- package/dist/tooling/dataflow.js +1 -1
- package/dist/tooling/policy.js +2 -0
- package/dist/tooling/policy.js.map +1 -1
- package/dist/types.d.ts +63 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/errors/index.ts +320 -0
- package/src/evaluator-optimizer/index.ts +212 -0
- package/src/factory.ts +957 -0
- package/src/fallback/index.ts +108 -0
- package/src/fanout/index.ts +523 -0
- package/src/filters/index.ts +347 -0
- package/src/index.ts +180 -0
- package/src/internal/ids.ts +46 -0
- package/src/internal/usage-accumulator.ts +90 -0
- package/src/lateral-leak/causality-monitor.ts +221 -0
- package/src/lateral-leak/index.ts +35 -0
- package/src/lateral-leak/merge-guard.ts +151 -0
- package/src/lateral-leak/protocol-guard.ts +222 -0
- package/src/preferred-model/index.ts +210 -0
- package/src/progress/index.ts +238 -0
- package/src/run-state/index.ts +607 -0
- package/src/runtime/agent-surface.ts +218 -0
- package/src/runtime/agent-to-tool.ts +323 -0
- package/src/runtime/approvals.ts +0 -0
- package/src/runtime/dispatch.ts +183 -0
- package/src/runtime/executor-wiring.ts +331 -0
- package/src/runtime/fallback-chain.ts +250 -0
- package/src/runtime/handoff.ts +428 -0
- package/src/runtime/messages.ts +309 -0
- package/src/runtime/provider-events.ts +175 -0
- package/src/runtime/run-compaction.ts +288 -0
- package/src/runtime/run-finish.ts +93 -0
- package/src/runtime/run-gates.ts +419 -0
- package/src/runtime/run-init.ts +169 -0
- package/src/runtime/run-input.ts +102 -0
- package/src/runtime/step-catalogue.ts +338 -0
- package/src/runtime/tool-call-walk.ts +301 -0
- package/src/runtime/tool-wiring.ts +218 -0
- package/src/testing/replay-provider.ts +121 -0
- package/src/tooling/adapters.ts +403 -0
- package/src/tooling/catalogue.ts +36 -0
- package/src/tooling/dataflow.ts +171 -0
- package/src/tooling/plan.ts +123 -0
- package/src/tooling/policy.ts +67 -0
- package/src/tooling/registry-build.ts +191 -0
- package/src/types.ts +696 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RunState` JSON serialization and rehydration.
|
|
3
|
+
*
|
|
4
|
+
* The on-disk shape carries an explicit `version` field so future
|
|
5
|
+
* schema bumps can detect older payloads. v0.1 ships
|
|
6
|
+
* `'graphorin-run-state/1.0'` - additive fields that older readers
|
|
7
|
+
* do not understand are ignored under the lenient-forward-parse
|
|
8
|
+
* discipline.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
CompletedToolCall,
|
|
15
|
+
HandoffRecord,
|
|
16
|
+
RunState,
|
|
17
|
+
RunStateUsageByModel,
|
|
18
|
+
RunStatus,
|
|
19
|
+
RunTaintSummary,
|
|
20
|
+
TodoItem,
|
|
21
|
+
ToolApproval,
|
|
22
|
+
Usage,
|
|
23
|
+
WireMessage,
|
|
24
|
+
WireRunState,
|
|
25
|
+
WireRunStep,
|
|
26
|
+
} from '@graphorin/core';
|
|
27
|
+
import { fromJsonSafeRunState, toJsonSafeRunState, zeroUsage } from '@graphorin/core';
|
|
28
|
+
import { RunStateMalformedError, RunStateVersionUnsupportedError } from '../errors/index.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Canonical schema id for serialized {@link RunState} payloads.
|
|
32
|
+
*
|
|
33
|
+
* 1.2 encodes binary message/tool-outcome payloads (`Uint8Array | URL`)
|
|
34
|
+
* through the core `WireRunState` projection (base64 / href envelopes)
|
|
35
|
+
* instead of letting `JSON.stringify` corrupt them. 1.0/1.1 payloads
|
|
36
|
+
* remain readable; their corrupted numeric-key byte objects are
|
|
37
|
+
* repaired best-effort on rehydration.
|
|
38
|
+
*
|
|
39
|
+
* @stable
|
|
40
|
+
*/
|
|
41
|
+
export const RUN_STATE_SCHEMA_VERSION = 'graphorin-run-state/1.2' as const;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reader-supported schema id range. Major version 1 only for v0.1.
|
|
45
|
+
*
|
|
46
|
+
* @stable
|
|
47
|
+
*/
|
|
48
|
+
export const RUN_STATE_SCHEMA_MAJOR_SUPPORTED = 1;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* On-disk payload returned by {@link serializeRunState} and accepted
|
|
52
|
+
* by {@link deserializeRunState}. The shape is JSON-stable: binary
|
|
53
|
+
* message/tool-outcome payloads appear in their `WireMessage` /
|
|
54
|
+
* `WireRunStep` (base64 / href envelope) form.
|
|
55
|
+
*
|
|
56
|
+
* @stable
|
|
57
|
+
*/
|
|
58
|
+
export interface SerializedRunState {
|
|
59
|
+
readonly version: typeof RUN_STATE_SCHEMA_VERSION;
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly agentId: string;
|
|
62
|
+
readonly currentAgentId: string;
|
|
63
|
+
readonly sessionId: string;
|
|
64
|
+
readonly userId?: string;
|
|
65
|
+
readonly status: RunStatus;
|
|
66
|
+
readonly steps: ReadonlyArray<WireRunStep>;
|
|
67
|
+
readonly messages: ReadonlyArray<WireMessage>;
|
|
68
|
+
readonly pendingApprovals: ReadonlyArray<ToolApproval>;
|
|
69
|
+
readonly handoffs: ReadonlyArray<HandoffRecord>;
|
|
70
|
+
readonly usage: Usage;
|
|
71
|
+
readonly usageByModel?: RunStateUsageByModel;
|
|
72
|
+
/** AG-19: coarse data-flow taint summary (no untrusted text). */
|
|
73
|
+
readonly taintSummary?: RunTaintSummary;
|
|
74
|
+
/** AG-19: deferred tools promoted by `tool_search` this run. */
|
|
75
|
+
readonly promotedTools?: ReadonlyArray<string>;
|
|
76
|
+
/** D6: journaled structured plan/todo list. */
|
|
77
|
+
readonly todos?: ReadonlyArray<TodoItem>;
|
|
78
|
+
/**
|
|
79
|
+
* W-001: parked sub-agent runs. Each child snapshot is itself a full
|
|
80
|
+
* `SerializedRunState` - version-stamped and secret-redacted
|
|
81
|
+
* recursively, to any nesting depth.
|
|
82
|
+
*/
|
|
83
|
+
readonly pendingSubRuns?: ReadonlyArray<SerializedPendingSubRun>;
|
|
84
|
+
readonly startedAt: string;
|
|
85
|
+
readonly finishedAt?: string;
|
|
86
|
+
readonly error?: { readonly message: string; readonly code: string; readonly details?: unknown };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Serialized twin of core's `PendingSubRun` (W-001): the parked child
|
|
91
|
+
* state travels as its own versioned {@link SerializedRunState}.
|
|
92
|
+
*
|
|
93
|
+
* @stable
|
|
94
|
+
*/
|
|
95
|
+
export interface SerializedPendingSubRun {
|
|
96
|
+
readonly toolCallId: string;
|
|
97
|
+
readonly toolName: string;
|
|
98
|
+
readonly targetAgentName: string;
|
|
99
|
+
readonly state: SerializedRunState;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Options accepted by {@link serializeRunState}.
|
|
104
|
+
*
|
|
105
|
+
* @stable
|
|
106
|
+
*/
|
|
107
|
+
export interface SerializeRunStateOptions {
|
|
108
|
+
/**
|
|
109
|
+
* Deep-redact secret-named keys (`apiKey`, `authorization`,
|
|
110
|
+
* `bearerToken` / `accessToken` / `refreshToken`, `password`,
|
|
111
|
+
* `secret`, …) anywhere in the snapshot - tool results and messages
|
|
112
|
+
* included - replacing their values with `'[redacted]'`. Defaults to
|
|
113
|
+
* `false` for the round-trip canonical helper; the agent runtime
|
|
114
|
+
* passes `true` when persisting through the checkpoint store
|
|
115
|
+
* (AG-23). Redaction is best-effort by key name: secrets stored
|
|
116
|
+
* under unrelated keys are not detected.
|
|
117
|
+
*/
|
|
118
|
+
readonly stripTracingApiKey?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render a JSON-stable snapshot of the supplied {@link RunState}.
|
|
123
|
+
* The returned value is plain JSON (no `Map`, `Set`, `Date`, ...).
|
|
124
|
+
*
|
|
125
|
+
* @stable
|
|
126
|
+
*/
|
|
127
|
+
export function serializeRunState(
|
|
128
|
+
state: RunState,
|
|
129
|
+
options: SerializeRunStateOptions = {},
|
|
130
|
+
): SerializedRunState {
|
|
131
|
+
// W-004: binary payloads (Uint8Array | URL in messages and tool-outcome
|
|
132
|
+
// contentParts) must be projected to their JSON-safe wire form BEFORE
|
|
133
|
+
// the detach stringify below, or they corrupt silently.
|
|
134
|
+
const wire = toJsonSafeRunState(state);
|
|
135
|
+
// W-001: each parked child state serializes through THIS function
|
|
136
|
+
// recursively (from the live `state`, not the wire projection) so
|
|
137
|
+
// every nested snapshot carries its own `version` stamp and passes
|
|
138
|
+
// the same secret redaction - a naive stringify would smuggle an
|
|
139
|
+
// unversioned live child through the checkpoint.
|
|
140
|
+
const pendingSubRuns =
|
|
141
|
+
state.pendingSubRuns !== undefined && state.pendingSubRuns.length > 0
|
|
142
|
+
? state.pendingSubRuns.map(
|
|
143
|
+
(sub): SerializedPendingSubRun => ({
|
|
144
|
+
toolCallId: sub.toolCallId,
|
|
145
|
+
toolName: sub.toolName,
|
|
146
|
+
targetAgentName: sub.targetAgentName,
|
|
147
|
+
state: serializeRunState(sub.state, options),
|
|
148
|
+
}),
|
|
149
|
+
)
|
|
150
|
+
: undefined;
|
|
151
|
+
const out: SerializedRunState = {
|
|
152
|
+
version: RUN_STATE_SCHEMA_VERSION,
|
|
153
|
+
id: wire.id,
|
|
154
|
+
agentId: wire.agentId,
|
|
155
|
+
currentAgentId: wire.currentAgentId,
|
|
156
|
+
sessionId: wire.sessionId,
|
|
157
|
+
...(wire.userId !== undefined ? { userId: wire.userId } : {}),
|
|
158
|
+
status: wire.status,
|
|
159
|
+
steps: wire.steps,
|
|
160
|
+
messages: wire.messages,
|
|
161
|
+
pendingApprovals: wire.pendingApprovals,
|
|
162
|
+
handoffs: wire.handoffs,
|
|
163
|
+
usage: wire.usage,
|
|
164
|
+
...(wire.usageByModel !== undefined ? { usageByModel: wire.usageByModel } : {}),
|
|
165
|
+
...(wire.taintSummary !== undefined ? { taintSummary: wire.taintSummary } : {}),
|
|
166
|
+
...(wire.promotedTools !== undefined ? { promotedTools: wire.promotedTools } : {}),
|
|
167
|
+
...(wire.todos !== undefined ? { todos: wire.todos } : {}),
|
|
168
|
+
...(pendingSubRuns !== undefined ? { pendingSubRuns } : {}),
|
|
169
|
+
startedAt: wire.startedAt,
|
|
170
|
+
...(wire.finishedAt !== undefined ? { finishedAt: wire.finishedAt } : {}),
|
|
171
|
+
...(wire.error !== undefined ? { error: wire.error } : {}),
|
|
172
|
+
};
|
|
173
|
+
// AG-23: the snapshot must be DETACHED from the live MutableRunState -
|
|
174
|
+
// a post-suspend mutation must never reach an already-persisted
|
|
175
|
+
// checkpoint. The JSON round-trip doubles as the plain-JSON guarantee
|
|
176
|
+
// this function documents (no Map/Set/Date survive it).
|
|
177
|
+
const detached = JSON.parse(JSON.stringify(out)) as SerializedRunState;
|
|
178
|
+
if (options.stripTracingApiKey === true) {
|
|
179
|
+
redactSecretKeysInPlace(detached);
|
|
180
|
+
}
|
|
181
|
+
return detached;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Key names whose values are redacted by
|
|
186
|
+
* `serializeRunState(state, { stripTracingApiKey: true })`.
|
|
187
|
+
*/
|
|
188
|
+
const SECRET_KEY_PATTERN =
|
|
189
|
+
/^(api[-_]?key|tracing[-_]?api[-_]?key|x-api-key|authorization|bearer[-_]?token|access[-_]?token|refresh[-_]?token|secret|password|passphrase)$/i;
|
|
190
|
+
|
|
191
|
+
const REDACTED = '[redacted]';
|
|
192
|
+
|
|
193
|
+
function redactSecretKeysInPlace(value: unknown): void {
|
|
194
|
+
if (typeof value !== 'object' || value === null) return;
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
for (const item of value) redactSecretKeysInPlace(item);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const record = value as Record<string, unknown>;
|
|
200
|
+
// Tool messages embed the tool output as a JSON STRING in `content` -
|
|
201
|
+
// parse, redact, and re-stringify so a secret inside the string form
|
|
202
|
+
// is caught too.
|
|
203
|
+
if (record.role === 'tool' && typeof record.content === 'string') {
|
|
204
|
+
record.content = redactJsonString(record.content);
|
|
205
|
+
}
|
|
206
|
+
for (const [key, inner] of Object.entries(record)) {
|
|
207
|
+
if (SECRET_KEY_PATTERN.test(key)) {
|
|
208
|
+
record[key] = REDACTED;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
redactSecretKeysInPlace(inner);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function redactJsonString(content: string): string {
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(content) as unknown;
|
|
218
|
+
if (typeof parsed !== 'object' || parsed === null) return content;
|
|
219
|
+
redactSecretKeysInPlace(parsed);
|
|
220
|
+
return JSON.stringify(parsed);
|
|
221
|
+
} catch {
|
|
222
|
+
return content;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Render the canonical JSON string representation of the supplied
|
|
228
|
+
* {@link RunState}. `JSON.stringify(serializeRunState(state))` -
|
|
229
|
+
* provided as a convenience.
|
|
230
|
+
*
|
|
231
|
+
* @stable
|
|
232
|
+
*/
|
|
233
|
+
export function runStateToJSON(state: RunState, options?: SerializeRunStateOptions): string {
|
|
234
|
+
return JSON.stringify(serializeRunState(state, options));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface DeserializeOptions {
|
|
238
|
+
/**
|
|
239
|
+
* Synthesize `usageByModel` from a v0.1-alpha state that omits
|
|
240
|
+
* the field. Defaults to `true` so callers can rehydrate older
|
|
241
|
+
* states without explicit migration.
|
|
242
|
+
*/
|
|
243
|
+
readonly synthesizeUsageByModel?: boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Logger callback for one-time INFO messages emitted on
|
|
246
|
+
* backwards-compat synthesis. Defaults to a no-op.
|
|
247
|
+
*/
|
|
248
|
+
readonly logger?: (message: string) => void;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
252
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Rehydrate a {@link RunState} from the on-disk payload. Throws
|
|
257
|
+
* {@link RunStateVersionUnsupportedError} when the payload version
|
|
258
|
+
* is from a future major; throws
|
|
259
|
+
* {@link RunStateMalformedError} when the payload is structurally
|
|
260
|
+
* invalid.
|
|
261
|
+
*
|
|
262
|
+
* Backwards-compat: a payload that omits `usageByModel` is accepted
|
|
263
|
+
* and the field is synthesized from the aggregate `usage` with
|
|
264
|
+
* `attemptCount: 1` for the primary model.
|
|
265
|
+
*
|
|
266
|
+
* @stable
|
|
267
|
+
*/
|
|
268
|
+
export function deserializeRunState(payload: unknown, options: DeserializeOptions = {}): RunState {
|
|
269
|
+
if (!isRecord(payload)) {
|
|
270
|
+
throw new RunStateMalformedError('payload must be an object');
|
|
271
|
+
}
|
|
272
|
+
const versionRaw = payload.version;
|
|
273
|
+
if (typeof versionRaw !== 'string') {
|
|
274
|
+
throw new RunStateMalformedError("missing 'version' field");
|
|
275
|
+
}
|
|
276
|
+
const versionMatch = /^graphorin-run-state\/(\d+)\.(\d+)$/.exec(versionRaw);
|
|
277
|
+
if (versionMatch === null) {
|
|
278
|
+
throw new RunStateMalformedError(`unrecognized version '${versionRaw}'`);
|
|
279
|
+
}
|
|
280
|
+
const major = Number(versionMatch[1]);
|
|
281
|
+
if (major > RUN_STATE_SCHEMA_MAJOR_SUPPORTED) {
|
|
282
|
+
throw new RunStateVersionUnsupportedError(versionRaw, RUN_STATE_SCHEMA_VERSION);
|
|
283
|
+
}
|
|
284
|
+
const requiredString = (key: string): string => {
|
|
285
|
+
const v = payload[key];
|
|
286
|
+
if (typeof v !== 'string') {
|
|
287
|
+
throw new RunStateMalformedError(`missing string '${key}' field`);
|
|
288
|
+
}
|
|
289
|
+
return v;
|
|
290
|
+
};
|
|
291
|
+
const requiredArray = <T>(key: string): T[] => {
|
|
292
|
+
const v = payload[key];
|
|
293
|
+
if (!Array.isArray(v)) {
|
|
294
|
+
throw new RunStateMalformedError(`missing array '${key}' field`);
|
|
295
|
+
}
|
|
296
|
+
return v as T[];
|
|
297
|
+
};
|
|
298
|
+
const id = requiredString('id');
|
|
299
|
+
const agentId = requiredString('agentId');
|
|
300
|
+
const currentAgentId =
|
|
301
|
+
typeof payload.currentAgentId === 'string' ? payload.currentAgentId : agentId;
|
|
302
|
+
const sessionId = requiredString('sessionId');
|
|
303
|
+
const status = requiredString('status') as RunStatus;
|
|
304
|
+
const steps = requiredArray<WireRunStep>('steps');
|
|
305
|
+
const messages = requiredArray<WireMessage>('messages');
|
|
306
|
+
const pendingApprovals = requiredArray<ToolApproval>('pendingApprovals');
|
|
307
|
+
const handoffs = requiredArray<HandoffRecord>('handoffs');
|
|
308
|
+
const usageRaw = payload.usage;
|
|
309
|
+
if (!isRecord(usageRaw)) {
|
|
310
|
+
throw new RunStateMalformedError("missing object 'usage' field");
|
|
311
|
+
}
|
|
312
|
+
const usage: Usage = {
|
|
313
|
+
promptTokens: Number(usageRaw.promptTokens ?? 0),
|
|
314
|
+
completionTokens: Number(usageRaw.completionTokens ?? 0),
|
|
315
|
+
totalTokens: Number(usageRaw.totalTokens ?? 0),
|
|
316
|
+
...(usageRaw.reasoningTokens !== undefined
|
|
317
|
+
? { reasoningTokens: Number(usageRaw.reasoningTokens) }
|
|
318
|
+
: {}),
|
|
319
|
+
...(usageRaw.cachedReadTokens !== undefined
|
|
320
|
+
? { cachedReadTokens: Number(usageRaw.cachedReadTokens) }
|
|
321
|
+
: {}),
|
|
322
|
+
...(usageRaw.cacheWriteTokens !== undefined
|
|
323
|
+
? { cacheWriteTokens: Number(usageRaw.cacheWriteTokens) }
|
|
324
|
+
: {}),
|
|
325
|
+
...(isRecord(usageRaw.cost)
|
|
326
|
+
? {
|
|
327
|
+
cost: {
|
|
328
|
+
amount: Number(usageRaw.cost.amount ?? 0),
|
|
329
|
+
currency: String(usageRaw.cost.currency ?? 'USD'),
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
: {}),
|
|
333
|
+
};
|
|
334
|
+
const startedAt = requiredString('startedAt');
|
|
335
|
+
const userIdRaw = payload.userId;
|
|
336
|
+
const finishedAtRaw = payload.finishedAt;
|
|
337
|
+
const errorRaw = payload.error;
|
|
338
|
+
let error: RunState['error'];
|
|
339
|
+
if (isRecord(errorRaw)) {
|
|
340
|
+
error = {
|
|
341
|
+
message: typeof errorRaw.message === 'string' ? errorRaw.message : 'unknown',
|
|
342
|
+
code: typeof errorRaw.code === 'string' ? errorRaw.code : 'unknown',
|
|
343
|
+
...(errorRaw.details !== undefined ? { details: errorRaw.details } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Backwards-compat synthesis for usageByModel.
|
|
348
|
+
let usageByModel: RunStateUsageByModel | undefined;
|
|
349
|
+
if (isRecord(payload.usageByModel)) {
|
|
350
|
+
const m: Record<string, Usage & { attemptCount: number }> = {};
|
|
351
|
+
for (const [modelId, entryRaw] of Object.entries(payload.usageByModel)) {
|
|
352
|
+
if (!isRecord(entryRaw)) continue;
|
|
353
|
+
m[modelId] = {
|
|
354
|
+
promptTokens: Number(entryRaw.promptTokens ?? 0),
|
|
355
|
+
completionTokens: Number(entryRaw.completionTokens ?? 0),
|
|
356
|
+
totalTokens: Number(entryRaw.totalTokens ?? 0),
|
|
357
|
+
attemptCount: Number(entryRaw.attemptCount ?? 1),
|
|
358
|
+
...(entryRaw.reasoningTokens !== undefined
|
|
359
|
+
? { reasoningTokens: Number(entryRaw.reasoningTokens) }
|
|
360
|
+
: {}),
|
|
361
|
+
...(entryRaw.cachedReadTokens !== undefined
|
|
362
|
+
? { cachedReadTokens: Number(entryRaw.cachedReadTokens) }
|
|
363
|
+
: {}),
|
|
364
|
+
...(entryRaw.cacheWriteTokens !== undefined
|
|
365
|
+
? { cacheWriteTokens: Number(entryRaw.cacheWriteTokens) }
|
|
366
|
+
: {}),
|
|
367
|
+
...(isRecord(entryRaw.cost)
|
|
368
|
+
? {
|
|
369
|
+
cost: {
|
|
370
|
+
amount: Number(entryRaw.cost.amount ?? 0),
|
|
371
|
+
currency: String(entryRaw.cost.currency ?? 'USD'),
|
|
372
|
+
},
|
|
373
|
+
}
|
|
374
|
+
: {}),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
usageByModel = m;
|
|
378
|
+
} else if (options.synthesizeUsageByModel !== false && Number.isFinite(usage.totalTokens)) {
|
|
379
|
+
usageByModel = {
|
|
380
|
+
[agentId]: { ...usage, attemptCount: 1 },
|
|
381
|
+
};
|
|
382
|
+
options.logger?.(
|
|
383
|
+
`[graphorin/agent] RunState v0.1-alpha synthesis: per-model breakdown derived from aggregate usage for agent '${agentId}'.`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// AG-19: rehydrate the coarse taint summary + promoted-tool set (additive;
|
|
388
|
+
// absent on older v1.0 payloads).
|
|
389
|
+
let taintSummary: RunTaintSummary | undefined;
|
|
390
|
+
if (isRecord(payload.taintSummary)) {
|
|
391
|
+
const ts = payload.taintSummary;
|
|
392
|
+
const tileHashes = Array.isArray(ts.spanTileHashes)
|
|
393
|
+
? ts.spanTileHashes.filter((h): h is string => typeof h === 'string')
|
|
394
|
+
: undefined;
|
|
395
|
+
taintSummary = {
|
|
396
|
+
untrustedSeen: ts.untrustedSeen === true,
|
|
397
|
+
sensitiveSeen: ts.sensitiveSeen === true,
|
|
398
|
+
untrustedSourceKinds: Array.isArray(ts.untrustedSourceKinds)
|
|
399
|
+
? ts.untrustedSourceKinds.filter((k): k is string => typeof k === 'string')
|
|
400
|
+
: [],
|
|
401
|
+
...(tileHashes !== undefined && tileHashes.length > 0 ? { spanTileHashes: tileHashes } : {}),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const promotedTools = Array.isArray(payload.promotedTools)
|
|
405
|
+
? payload.promotedTools.filter((t): t is string => typeof t === 'string')
|
|
406
|
+
: undefined;
|
|
407
|
+
const todos = Array.isArray(payload.todos)
|
|
408
|
+
? payload.todos
|
|
409
|
+
.filter(isRecord)
|
|
410
|
+
.filter(
|
|
411
|
+
(t): t is { id: string; content: string; status: TodoItem['status'] } =>
|
|
412
|
+
typeof t.id === 'string' &&
|
|
413
|
+
typeof t.content === 'string' &&
|
|
414
|
+
(t.status === 'pending' || t.status === 'in_progress' || t.status === 'completed'),
|
|
415
|
+
)
|
|
416
|
+
.map((t) => ({ id: t.id, content: t.content, status: t.status }))
|
|
417
|
+
: undefined;
|
|
418
|
+
|
|
419
|
+
const wire: WireRunState = {
|
|
420
|
+
id,
|
|
421
|
+
agentId,
|
|
422
|
+
currentAgentId,
|
|
423
|
+
sessionId,
|
|
424
|
+
...(typeof userIdRaw === 'string' ? { userId: userIdRaw } : {}),
|
|
425
|
+
status,
|
|
426
|
+
steps,
|
|
427
|
+
messages,
|
|
428
|
+
pendingApprovals,
|
|
429
|
+
handoffs,
|
|
430
|
+
usage,
|
|
431
|
+
...(usageByModel !== undefined ? { usageByModel } : {}),
|
|
432
|
+
...(taintSummary !== undefined ? { taintSummary } : {}),
|
|
433
|
+
...(promotedTools !== undefined ? { promotedTools } : {}),
|
|
434
|
+
...(todos !== undefined ? { todos } : {}),
|
|
435
|
+
startedAt,
|
|
436
|
+
...(typeof finishedAtRaw === 'string' ? { finishedAt: finishedAtRaw } : {}),
|
|
437
|
+
...(error !== undefined ? { error } : {}),
|
|
438
|
+
};
|
|
439
|
+
void zeroUsage; // kept available; some callers prefer zeroUsage as a default.
|
|
440
|
+
// W-004: decode base64/href envelopes back into Uint8Array/URL. Legacy
|
|
441
|
+
// 1.0/1.1 payloads whose binary fields were stringify-corrupted into
|
|
442
|
+
// numeric-key objects are repaired best-effort inside the codec.
|
|
443
|
+
const rehydrated = fromJsonSafeRunState(wire);
|
|
444
|
+
// W-001: rehydrate each parked child through THIS function recursively
|
|
445
|
+
// (the child payload is its own versioned SerializedRunState).
|
|
446
|
+
if (Array.isArray(payload.pendingSubRuns) && payload.pendingSubRuns.length > 0) {
|
|
447
|
+
const subs = payload.pendingSubRuns
|
|
448
|
+
.filter(isRecord)
|
|
449
|
+
.filter(
|
|
450
|
+
(s): s is Record<string, unknown> & { toolCallId: string; toolName: string } =>
|
|
451
|
+
typeof s.toolCallId === 'string' && typeof s.toolName === 'string',
|
|
452
|
+
)
|
|
453
|
+
.map((s) => ({
|
|
454
|
+
toolCallId: s.toolCallId,
|
|
455
|
+
toolName: s.toolName,
|
|
456
|
+
targetAgentName: typeof s.targetAgentName === 'string' ? s.targetAgentName : s.toolName,
|
|
457
|
+
state: deserializeRunState(s.state, options),
|
|
458
|
+
}));
|
|
459
|
+
if (subs.length > 0) {
|
|
460
|
+
(rehydrated as { pendingSubRuns?: RunState['pendingSubRuns'] }).pendingSubRuns = subs;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return rehydrated;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Convenience JSON-string parser pairing with {@link runStateToJSON}. */
|
|
467
|
+
export function runStateFromJSON(serialized: string, options?: DeserializeOptions): RunState {
|
|
468
|
+
let parsed: unknown;
|
|
469
|
+
try {
|
|
470
|
+
parsed = JSON.parse(serialized);
|
|
471
|
+
} catch (cause) {
|
|
472
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
473
|
+
throw new RunStateMalformedError(`invalid JSON: ${message}`);
|
|
474
|
+
}
|
|
475
|
+
return deserializeRunState(parsed, options);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Build a fresh, minimal {@link RunState} for a new run. Helper used
|
|
480
|
+
* by `createAgent({...})` so consumers can construct deterministic
|
|
481
|
+
* run state in tests.
|
|
482
|
+
*
|
|
483
|
+
* @stable
|
|
484
|
+
*/
|
|
485
|
+
export function createInitialRunState(args: {
|
|
486
|
+
readonly id: string;
|
|
487
|
+
readonly agentId: string;
|
|
488
|
+
readonly sessionId: string;
|
|
489
|
+
readonly userId?: string;
|
|
490
|
+
readonly startedAt?: string;
|
|
491
|
+
}): RunState {
|
|
492
|
+
return {
|
|
493
|
+
id: args.id,
|
|
494
|
+
agentId: args.agentId,
|
|
495
|
+
currentAgentId: args.agentId,
|
|
496
|
+
sessionId: args.sessionId,
|
|
497
|
+
...(args.userId !== undefined ? { userId: args.userId } : {}),
|
|
498
|
+
status: 'running',
|
|
499
|
+
steps: [],
|
|
500
|
+
messages: [],
|
|
501
|
+
pendingApprovals: [],
|
|
502
|
+
handoffs: [],
|
|
503
|
+
usage: zeroUsage(),
|
|
504
|
+
startedAt: args.startedAt ?? new Date().toISOString(),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Append a per-model usage entry to {@link RunState.usageByModel}.
|
|
510
|
+
* Mutates the supplied state in place - used by the agent runtime's
|
|
511
|
+
* per-step retry loop. Pure callers that need an immutable update
|
|
512
|
+
* should clone the state first.
|
|
513
|
+
*
|
|
514
|
+
* @stable
|
|
515
|
+
*/
|
|
516
|
+
export function addModelUsage(state: RunState, modelId: string, delta: Usage): void {
|
|
517
|
+
const current = (state.usageByModel ?? {}) as Record<string, Usage & { attemptCount: number }>;
|
|
518
|
+
const prev = current[modelId];
|
|
519
|
+
const merged: Usage & { attemptCount: number } = prev
|
|
520
|
+
? {
|
|
521
|
+
promptTokens: prev.promptTokens + delta.promptTokens,
|
|
522
|
+
completionTokens: prev.completionTokens + delta.completionTokens,
|
|
523
|
+
totalTokens: prev.totalTokens + delta.totalTokens,
|
|
524
|
+
...(delta.reasoningTokens !== undefined || prev.reasoningTokens !== undefined
|
|
525
|
+
? {
|
|
526
|
+
reasoningTokens: (prev.reasoningTokens ?? 0) + (delta.reasoningTokens ?? 0),
|
|
527
|
+
}
|
|
528
|
+
: {}),
|
|
529
|
+
...(delta.cachedReadTokens !== undefined || prev.cachedReadTokens !== undefined
|
|
530
|
+
? {
|
|
531
|
+
cachedReadTokens: (prev.cachedReadTokens ?? 0) + (delta.cachedReadTokens ?? 0),
|
|
532
|
+
}
|
|
533
|
+
: {}),
|
|
534
|
+
...(delta.cacheWriteTokens !== undefined || prev.cacheWriteTokens !== undefined
|
|
535
|
+
? {
|
|
536
|
+
cacheWriteTokens: (prev.cacheWriteTokens ?? 0) + (delta.cacheWriteTokens ?? 0),
|
|
537
|
+
}
|
|
538
|
+
: {}),
|
|
539
|
+
attemptCount: prev.attemptCount + 1,
|
|
540
|
+
...(prev.cost !== undefined ? { cost: prev.cost } : {}),
|
|
541
|
+
}
|
|
542
|
+
: { ...delta, attemptCount: 1 };
|
|
543
|
+
current[modelId] = merged;
|
|
544
|
+
// Mutate via the writable typing - `usageByModel` is declared
|
|
545
|
+
// optional; the runtime owns the field's lifecycle.
|
|
546
|
+
(state as { usageByModel?: RunStateUsageByModel }).usageByModel = current;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Recompute the aggregate usage from `usageByModel`. Returns the
|
|
551
|
+
* sum that callers can compare against `state.usage` to verify the
|
|
552
|
+
* per-step retry loop maintained the documented invariant.
|
|
553
|
+
*
|
|
554
|
+
* @stable
|
|
555
|
+
*/
|
|
556
|
+
export function aggregateUsageFromByModel(byModel: RunStateUsageByModel | undefined): Usage {
|
|
557
|
+
if (byModel === undefined) return zeroUsage();
|
|
558
|
+
let pt = 0;
|
|
559
|
+
let ct = 0;
|
|
560
|
+
let tt = 0;
|
|
561
|
+
let rt = 0;
|
|
562
|
+
let cr = 0;
|
|
563
|
+
let cw = 0;
|
|
564
|
+
let hasReasoning = false;
|
|
565
|
+
let hasCachedRead = false;
|
|
566
|
+
let hasCacheWrite = false;
|
|
567
|
+
for (const entry of Object.values(byModel)) {
|
|
568
|
+
pt += entry.promptTokens;
|
|
569
|
+
ct += entry.completionTokens;
|
|
570
|
+
tt += entry.totalTokens;
|
|
571
|
+
if (entry.reasoningTokens !== undefined) {
|
|
572
|
+
rt += entry.reasoningTokens;
|
|
573
|
+
hasReasoning = true;
|
|
574
|
+
}
|
|
575
|
+
if (entry.cachedReadTokens !== undefined) {
|
|
576
|
+
cr += entry.cachedReadTokens;
|
|
577
|
+
hasCachedRead = true;
|
|
578
|
+
}
|
|
579
|
+
if (entry.cacheWriteTokens !== undefined) {
|
|
580
|
+
cw += entry.cacheWriteTokens;
|
|
581
|
+
hasCacheWrite = true;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
promptTokens: pt,
|
|
586
|
+
completionTokens: ct,
|
|
587
|
+
totalTokens: tt,
|
|
588
|
+
...(hasReasoning ? { reasoningTokens: rt } : {}),
|
|
589
|
+
...(hasCachedRead ? { cachedReadTokens: cr } : {}),
|
|
590
|
+
...(hasCacheWrite ? { cacheWriteTokens: cw } : {}),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* The "tools used" surface of a completed run. Cheap to compute
|
|
596
|
+
* from `RunState.steps`; surfaced as a stand-alone helper for
|
|
597
|
+
* Phase 17 example apps and operator-facing dashboards.
|
|
598
|
+
*
|
|
599
|
+
* @stable
|
|
600
|
+
*/
|
|
601
|
+
export function completedToolCallsFromState(state: RunState): ReadonlyArray<CompletedToolCall> {
|
|
602
|
+
const out: CompletedToolCall[] = [];
|
|
603
|
+
for (const step of state.steps) {
|
|
604
|
+
for (const tc of step.toolCalls) out.push(tc);
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
}
|