@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +31 -11
  3. package/dist/errors/index.d.ts +17 -4
  4. package/dist/errors/index.d.ts.map +1 -1
  5. package/dist/errors/index.js +19 -3
  6. package/dist/errors/index.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +153 -1930
  9. package/dist/factory.js.map +1 -1
  10. package/dist/fanout/index.d.ts +13 -1
  11. package/dist/fanout/index.d.ts.map +1 -1
  12. package/dist/fanout/index.js +13 -4
  13. package/dist/fanout/index.js.map +1 -1
  14. package/dist/index.d.ts +7 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +11 -9
  17. package/dist/index.js.map +1 -1
  18. package/dist/lateral-leak/index.js +1 -1
  19. package/dist/package.js +6 -0
  20. package/dist/package.js.map +1 -0
  21. package/dist/run-state/index.d.ts +32 -6
  22. package/dist/run-state/index.d.ts.map +1 -1
  23. package/dist/run-state/index.js +46 -22
  24. package/dist/run-state/index.js.map +1 -1
  25. package/dist/runtime/agent-surface.js +122 -0
  26. package/dist/runtime/agent-surface.js.map +1 -0
  27. package/dist/runtime/agent-to-tool.d.ts +51 -0
  28. package/dist/runtime/agent-to-tool.d.ts.map +1 -0
  29. package/dist/runtime/agent-to-tool.js +145 -0
  30. package/dist/runtime/agent-to-tool.js.map +1 -0
  31. package/dist/runtime/approvals.js +0 -0
  32. package/dist/runtime/approvals.js.map +1 -0
  33. package/dist/runtime/dispatch.js +108 -0
  34. package/dist/runtime/dispatch.js.map +1 -0
  35. package/dist/runtime/executor-wiring.js +128 -0
  36. package/dist/runtime/executor-wiring.js.map +1 -0
  37. package/dist/runtime/fallback-chain.js +139 -0
  38. package/dist/runtime/fallback-chain.js.map +1 -0
  39. package/dist/runtime/handoff.js +307 -0
  40. package/dist/runtime/handoff.js.map +1 -0
  41. package/dist/runtime/messages.d.ts +22 -0
  42. package/dist/runtime/messages.d.ts.map +1 -0
  43. package/dist/runtime/messages.js +204 -0
  44. package/dist/runtime/messages.js.map +1 -0
  45. package/dist/runtime/provider-events.js +117 -0
  46. package/dist/runtime/provider-events.js.map +1 -0
  47. package/dist/runtime/run-compaction.js +210 -0
  48. package/dist/runtime/run-compaction.js.map +1 -0
  49. package/dist/runtime/run-finish.js +48 -0
  50. package/dist/runtime/run-finish.js.map +1 -0
  51. package/dist/runtime/run-gates.js +336 -0
  52. package/dist/runtime/run-gates.js.map +1 -0
  53. package/dist/runtime/run-init.js +81 -0
  54. package/dist/runtime/run-init.js.map +1 -0
  55. package/dist/runtime/run-input.js +46 -0
  56. package/dist/runtime/run-input.js.map +1 -0
  57. package/dist/runtime/step-catalogue.js +173 -0
  58. package/dist/runtime/step-catalogue.js.map +1 -0
  59. package/dist/runtime/tool-call-walk.js +189 -0
  60. package/dist/runtime/tool-call-walk.js.map +1 -0
  61. package/dist/runtime/tool-wiring.js +159 -0
  62. package/dist/runtime/tool-wiring.js.map +1 -0
  63. package/dist/tooling/adapters.js +1 -1
  64. package/dist/tooling/dataflow.js +1 -1
  65. package/dist/tooling/policy.js +2 -0
  66. package/dist/tooling/policy.js.map +1 -1
  67. package/dist/types.d.ts +63 -13
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +20 -20
  70. package/src/errors/index.ts +320 -0
  71. package/src/evaluator-optimizer/index.ts +212 -0
  72. package/src/factory.ts +957 -0
  73. package/src/fallback/index.ts +108 -0
  74. package/src/fanout/index.ts +523 -0
  75. package/src/filters/index.ts +347 -0
  76. package/src/index.ts +180 -0
  77. package/src/internal/ids.ts +46 -0
  78. package/src/internal/usage-accumulator.ts +90 -0
  79. package/src/lateral-leak/causality-monitor.ts +221 -0
  80. package/src/lateral-leak/index.ts +35 -0
  81. package/src/lateral-leak/merge-guard.ts +151 -0
  82. package/src/lateral-leak/protocol-guard.ts +222 -0
  83. package/src/preferred-model/index.ts +210 -0
  84. package/src/progress/index.ts +238 -0
  85. package/src/run-state/index.ts +607 -0
  86. package/src/runtime/agent-surface.ts +218 -0
  87. package/src/runtime/agent-to-tool.ts +323 -0
  88. package/src/runtime/approvals.ts +0 -0
  89. package/src/runtime/dispatch.ts +183 -0
  90. package/src/runtime/executor-wiring.ts +331 -0
  91. package/src/runtime/fallback-chain.ts +250 -0
  92. package/src/runtime/handoff.ts +428 -0
  93. package/src/runtime/messages.ts +309 -0
  94. package/src/runtime/provider-events.ts +175 -0
  95. package/src/runtime/run-compaction.ts +288 -0
  96. package/src/runtime/run-finish.ts +93 -0
  97. package/src/runtime/run-gates.ts +419 -0
  98. package/src/runtime/run-init.ts +169 -0
  99. package/src/runtime/run-input.ts +102 -0
  100. package/src/runtime/step-catalogue.ts +338 -0
  101. package/src/runtime/tool-call-walk.ts +301 -0
  102. package/src/runtime/tool-wiring.ts +218 -0
  103. package/src/testing/replay-provider.ts +121 -0
  104. package/src/tooling/adapters.ts +403 -0
  105. package/src/tooling/catalogue.ts +36 -0
  106. package/src/tooling/dataflow.ts +171 -0
  107. package/src/tooling/plan.ts +123 -0
  108. package/src/tooling/policy.ts +67 -0
  109. package/src/tooling/registry-build.ts +191 -0
  110. 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
+ }