@graphorin/agent 0.5.0 → 0.6.1

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 (65) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +41 -21
  3. package/dist/errors/index.d.ts +1 -1
  4. package/dist/errors/index.js +1 -1
  5. package/dist/errors/index.js.map +1 -1
  6. package/dist/evaluator-optimizer/index.d.ts +1 -1
  7. package/dist/evaluator-optimizer/index.js.map +1 -1
  8. package/dist/factory.d.ts.map +1 -1
  9. package/dist/factory.js +517 -108
  10. package/dist/factory.js.map +1 -1
  11. package/dist/fallback/index.d.ts +1 -1
  12. package/dist/fallback/index.js.map +1 -1
  13. package/dist/fanout/index.d.ts +8 -8
  14. package/dist/fanout/index.js +3 -3
  15. package/dist/fanout/index.js.map +1 -1
  16. package/dist/filters/index.d.ts +1 -1
  17. package/dist/filters/index.js +2 -2
  18. package/dist/filters/index.js.map +1 -1
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +6 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/internal/ids.js +1 -1
  24. package/dist/internal/ids.js.map +1 -1
  25. package/dist/internal/usage-accumulator.js +17 -2
  26. package/dist/internal/usage-accumulator.js.map +1 -1
  27. package/dist/lateral-leak/causality-monitor.d.ts +4 -4
  28. package/dist/lateral-leak/causality-monitor.js +3 -3
  29. package/dist/lateral-leak/causality-monitor.js.map +1 -1
  30. package/dist/lateral-leak/merge-guard.d.ts +2 -2
  31. package/dist/lateral-leak/merge-guard.js +1 -1
  32. package/dist/lateral-leak/merge-guard.js.map +1 -1
  33. package/dist/lateral-leak/protocol-guard.d.ts +4 -4
  34. package/dist/lateral-leak/protocol-guard.js +4 -4
  35. package/dist/lateral-leak/protocol-guard.js.map +1 -1
  36. package/dist/package.js +6 -0
  37. package/dist/package.js.map +1 -0
  38. package/dist/preferred-model/index.d.ts +2 -2
  39. package/dist/preferred-model/index.js +2 -2
  40. package/dist/preferred-model/index.js.map +1 -1
  41. package/dist/progress/index.js +2 -2
  42. package/dist/progress/index.js.map +1 -1
  43. package/dist/run-state/index.d.ts +7 -5
  44. package/dist/run-state/index.d.ts.map +1 -1
  45. package/dist/run-state/index.js +33 -4
  46. package/dist/run-state/index.js.map +1 -1
  47. package/dist/testing/replay-provider.d.ts +18 -0
  48. package/dist/testing/replay-provider.d.ts.map +1 -0
  49. package/dist/testing/replay-provider.js +100 -0
  50. package/dist/testing/replay-provider.js.map +1 -0
  51. package/dist/tooling/adapters.js +6 -6
  52. package/dist/tooling/adapters.js.map +1 -1
  53. package/dist/tooling/catalogue.js +1 -1
  54. package/dist/tooling/catalogue.js.map +1 -1
  55. package/dist/tooling/dataflow.js +19 -3
  56. package/dist/tooling/dataflow.js.map +1 -1
  57. package/dist/tooling/plan.js +92 -0
  58. package/dist/tooling/plan.js.map +1 -0
  59. package/dist/tooling/policy.js +45 -0
  60. package/dist/tooling/policy.js.map +1 -0
  61. package/dist/tooling/registry-build.js +1 -1
  62. package/dist/tooling/registry-build.js.map +1 -1
  63. package/dist/types.d.ts +189 -18
  64. package/dist/types.d.ts.map +1 -1
  65. package/package.json +11 -8
@@ -1 +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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"}
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 TodoItem,\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 /** D6: journaled structured plan/todo list. */\n readonly todos?: ReadonlyArray<TodoItem>;\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 ...(state.todos !== undefined ? { todos: state.todos } : {}),\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 ...(usageRaw.cachedReadTokens !== undefined\n ? { cachedReadTokens: Number(usageRaw.cachedReadTokens) }\n : {}),\n ...(usageRaw.cacheWriteTokens !== undefined\n ? { cacheWriteTokens: Number(usageRaw.cacheWriteTokens) }\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 ...(entryRaw.cachedReadTokens !== undefined\n ? { cachedReadTokens: Number(entryRaw.cachedReadTokens) }\n : {}),\n ...(entryRaw.cacheWriteTokens !== undefined\n ? { cacheWriteTokens: Number(entryRaw.cacheWriteTokens) }\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 const tileHashes = Array.isArray(ts.spanTileHashes)\n ? ts.spanTileHashes.filter((h): h is string => typeof h === 'string')\n : undefined;\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 ...(tileHashes !== undefined && tileHashes.length > 0 ? { spanTileHashes: tileHashes } : {}),\n };\n }\n const promotedTools = Array.isArray(payload.promotedTools)\n ? payload.promotedTools.filter((t): t is string => typeof t === 'string')\n : undefined;\n const todos = Array.isArray(payload.todos)\n ? payload.todos\n .filter(isRecord)\n .filter(\n (t): t is { id: string; content: string; status: TodoItem['status'] } =>\n typeof t.id === 'string' &&\n typeof t.content === 'string' &&\n (t.status === 'pending' || t.status === 'in_progress' || t.status === 'completed'),\n )\n .map((t) => ({ id: t.id, content: t.content, status: t.status }))\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 ...(todos !== undefined ? { todos } : {}),\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 ...(delta.cachedReadTokens !== undefined || prev.cachedReadTokens !== undefined\n ? {\n cachedReadTokens: (prev.cachedReadTokens ?? 0) + (delta.cachedReadTokens ?? 0),\n }\n : {}),\n ...(delta.cacheWriteTokens !== undefined || prev.cacheWriteTokens !== undefined\n ? {\n cacheWriteTokens: (prev.cacheWriteTokens ?? 0) + (delta.cacheWriteTokens ?? 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 cr = 0;\n let cw = 0;\n let hasReasoning = false;\n let hasCachedRead = false;\n let hasCacheWrite = 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 if (entry.cachedReadTokens !== undefined) {\n cr += entry.cachedReadTokens;\n hasCachedRead = true;\n }\n if (entry.cacheWriteTokens !== undefined) {\n cw += entry.cacheWriteTokens;\n hasCacheWrite = true;\n }\n }\n return {\n promptTokens: pt,\n completionTokens: ct,\n totalTokens: tt,\n ...(hasReasoning ? { reasoningTokens: rt } : {}),\n ...(hasCachedRead ? { cachedReadTokens: cr } : {}),\n ...(hasCacheWrite ? { cacheWriteTokens: cw } : {}),\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,18 @@
1
+ import { Provider, RunState } from "@graphorin/core";
2
+
3
+ //#region src/testing/replay-provider.d.ts
4
+
5
+ /** Options for {@link createReplayProvider}. */
6
+ interface ReplayProviderOptions {
7
+ /** Provider name for spans/logs. Default `'replay'`. */
8
+ readonly name?: string;
9
+ }
10
+ /**
11
+ * Build a Provider that serves the journaled step responses in order.
12
+ *
13
+ * @stable
14
+ */
15
+ declare function createReplayProvider(state: RunState, options?: ReplayProviderOptions): Provider;
16
+ //#endregion
17
+ export { ReplayProviderOptions, createReplayProvider };
18
+ //# sourceMappingURL=replay-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-provider.d.ts","names":[],"sources":["../../src/testing/replay-provider.ts"],"sourcesContent":[],"mappings":";;;;;UA0BiB,qBAAA;;;;;;;;;iBAUD,oBAAA,QACP,oBACE,wBACR"}
@@ -0,0 +1,100 @@
1
+ //#region src/testing/replay-provider.ts
2
+ /**
3
+ * Build a Provider that serves the journaled step responses in order.
4
+ *
5
+ * @stable
6
+ */
7
+ function createReplayProvider(state, options = {}) {
8
+ const recorded = [];
9
+ for (const step of state.steps) if (step.providerResponse !== void 0) recorded.push(step.providerResponse);
10
+ if (recorded.length === 0) throw new TypeError("createReplayProvider: RunState carries no journaled provider responses - run the original agent with recordProviderResponses: true.");
11
+ let cursor = 0;
12
+ const modelId = recorded[0]?.modelId ?? "replay";
13
+ function nextRecorded() {
14
+ const entry = recorded[cursor];
15
+ cursor += 1;
16
+ return entry;
17
+ }
18
+ return {
19
+ name: options.name ?? "replay",
20
+ modelId,
21
+ capabilities: {
22
+ streaming: true,
23
+ toolCalling: true,
24
+ parallelToolCalls: true,
25
+ multimodal: false,
26
+ structuredOutput: true,
27
+ reasoning: false,
28
+ contextWindow: 1e6,
29
+ maxOutput: 1e6
30
+ },
31
+ async *stream(_req) {
32
+ const entry = nextRecorded();
33
+ if (entry === void 0) {
34
+ yield {
35
+ type: "error",
36
+ error: {
37
+ kind: "unknown",
38
+ message: `replay exhausted after ${recorded.length} recorded step(s) - the replayed run diverged from the original`
39
+ }
40
+ };
41
+ return;
42
+ }
43
+ yield {
44
+ type: "stream-start",
45
+ metadata: {
46
+ providerName: "replay",
47
+ modelId: entry.modelId
48
+ }
49
+ };
50
+ if (entry.text !== void 0 && entry.text.length > 0) yield {
51
+ type: "text-delta",
52
+ delta: entry.text
53
+ };
54
+ for (const call of entry.toolCalls ?? []) {
55
+ yield {
56
+ type: "tool-call-start",
57
+ toolCallId: call.toolCallId,
58
+ toolName: call.toolName
59
+ };
60
+ yield {
61
+ type: "tool-call-input-delta",
62
+ toolCallId: call.toolCallId,
63
+ argsDelta: JSON.stringify(call.args)
64
+ };
65
+ yield {
66
+ type: "tool-call-end",
67
+ toolCallId: call.toolCallId,
68
+ finalArgs: call.args
69
+ };
70
+ }
71
+ yield {
72
+ type: "finish",
73
+ finishReason: (entry.toolCalls?.length ?? 0) > 0 ? "tool-calls" : "stop",
74
+ usage: {
75
+ promptTokens: 0,
76
+ completionTokens: 0,
77
+ totalTokens: 0
78
+ }
79
+ };
80
+ },
81
+ async generate(_req) {
82
+ const entry = nextRecorded();
83
+ if (entry === void 0) throw new Error(`replay exhausted after ${recorded.length} recorded step(s) - the replayed run diverged from the original`);
84
+ return {
85
+ ...entry.text !== void 0 ? { text: entry.text } : {},
86
+ ...entry.toolCalls !== void 0 && entry.toolCalls.length > 0 ? { toolCalls: entry.toolCalls.map((c) => ({ ...c })) } : {},
87
+ usage: {
88
+ promptTokens: 0,
89
+ completionTokens: 0,
90
+ totalTokens: 0
91
+ },
92
+ finishReason: (entry.toolCalls?.length ?? 0) > 0 ? "tool-calls" : "stop"
93
+ };
94
+ }
95
+ };
96
+ }
97
+
98
+ //#endregion
99
+ export { createReplayProvider };
100
+ //# sourceMappingURL=replay-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-provider.js","names":["recorded: RunStepProviderResponse[]"],"sources":["../../src/testing/replay-provider.ts"],"sourcesContent":["/**\n * `createReplayProvider(state)` - a Provider that replays the model\n * responses journaled on `RunState.steps[].providerResponse` (C3, opt-in\n * via `AgentConfig.recordProviderResponses`). Re-running the same input\n * against a replay provider reproduces the original run deterministically\n * with zero live model calls - the mocked-completion harness that gives\n * agent loops reproducible integration tests.\n *\n * The provider is strict by design: it throws when the state carries no\n * journaled responses, and emits an error event when the replayed run\n * asks for more steps than were recorded (a divergence, not a fixture\n * gap - fail loudly instead of hallucinating a response).\n *\n * @packageDocumentation\n */\n\nimport type {\n Provider,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n RunState,\n RunStepProviderResponse,\n} from '@graphorin/core';\n\n/** Options for {@link createReplayProvider}. */\nexport interface ReplayProviderOptions {\n /** Provider name for spans/logs. Default `'replay'`. */\n readonly name?: string;\n}\n\n/**\n * Build a Provider that serves the journaled step responses in order.\n *\n * @stable\n */\nexport function createReplayProvider(\n state: RunState,\n options: ReplayProviderOptions = {},\n): Provider {\n const recorded: RunStepProviderResponse[] = [];\n for (const step of state.steps) {\n if (step.providerResponse !== undefined) recorded.push(step.providerResponse);\n }\n if (recorded.length === 0) {\n throw new TypeError(\n 'createReplayProvider: RunState carries no journaled provider responses - ' +\n 'run the original agent with recordProviderResponses: true.',\n );\n }\n let cursor = 0;\n const modelId = recorded[0]?.modelId ?? 'replay';\n\n function nextRecorded(): RunStepProviderResponse | undefined {\n const entry = recorded[cursor];\n cursor += 1;\n return entry;\n }\n\n return {\n name: options.name ?? 'replay',\n modelId,\n capabilities: {\n streaming: true,\n toolCalling: true,\n parallelToolCalls: true,\n multimodal: false,\n structuredOutput: true,\n reasoning: false,\n contextWindow: 1_000_000,\n maxOutput: 1_000_000,\n },\n async *stream(_req: ProviderRequest): AsyncIterable<ProviderEvent> {\n const entry = nextRecorded();\n if (entry === undefined) {\n yield {\n type: 'error',\n error: {\n kind: 'unknown',\n message: `replay exhausted after ${recorded.length} recorded step(s) - the replayed run diverged from the original`,\n },\n };\n return;\n }\n yield { type: 'stream-start', metadata: { providerName: 'replay', modelId: entry.modelId } };\n if (entry.text !== undefined && entry.text.length > 0) {\n yield { type: 'text-delta', delta: entry.text };\n }\n for (const call of entry.toolCalls ?? []) {\n yield { type: 'tool-call-start', toolCallId: call.toolCallId, toolName: call.toolName };\n yield {\n type: 'tool-call-input-delta',\n toolCallId: call.toolCallId,\n argsDelta: JSON.stringify(call.args),\n };\n yield { type: 'tool-call-end', toolCallId: call.toolCallId, finalArgs: call.args };\n }\n yield {\n type: 'finish',\n finishReason: (entry.toolCalls?.length ?? 0) > 0 ? 'tool-calls' : 'stop',\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n };\n },\n async generate(_req: ProviderRequest): Promise<ProviderResponse> {\n const entry = nextRecorded();\n if (entry === undefined) {\n throw new Error(\n `replay exhausted after ${recorded.length} recorded step(s) - the replayed run diverged from the original`,\n );\n }\n return {\n ...(entry.text !== undefined ? { text: entry.text } : {}),\n ...(entry.toolCalls !== undefined && entry.toolCalls.length > 0\n ? { toolCalls: entry.toolCalls.map((c) => ({ ...c })) }\n : {}),\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n finishReason: (entry.toolCalls?.length ?? 0) > 0 ? 'tool-calls' : 'stop',\n };\n },\n };\n}\n"],"mappings":";;;;;;AAoCA,SAAgB,qBACd,OACA,UAAiC,EAAE,EACzB;CACV,MAAMA,WAAsC,EAAE;AAC9C,MAAK,MAAM,QAAQ,MAAM,MACvB,KAAI,KAAK,qBAAqB,OAAW,UAAS,KAAK,KAAK,iBAAiB;AAE/E,KAAI,SAAS,WAAW,EACtB,OAAM,IAAI,UACR,sIAED;CAEH,IAAI,SAAS;CACb,MAAM,UAAU,SAAS,IAAI,WAAW;CAExC,SAAS,eAAoD;EAC3D,MAAM,QAAQ,SAAS;AACvB,YAAU;AACV,SAAO;;AAGT,QAAO;EACL,MAAM,QAAQ,QAAQ;EACtB;EACA,cAAc;GACZ,WAAW;GACX,aAAa;GACb,mBAAmB;GACnB,YAAY;GACZ,kBAAkB;GAClB,WAAW;GACX,eAAe;GACf,WAAW;GACZ;EACD,OAAO,OAAO,MAAqD;GACjE,MAAM,QAAQ,cAAc;AAC5B,OAAI,UAAU,QAAW;AACvB,UAAM;KACJ,MAAM;KACN,OAAO;MACL,MAAM;MACN,SAAS,0BAA0B,SAAS,OAAO;MACpD;KACF;AACD;;AAEF,SAAM;IAAE,MAAM;IAAgB,UAAU;KAAE,cAAc;KAAU,SAAS,MAAM;KAAS;IAAE;AAC5F,OAAI,MAAM,SAAS,UAAa,MAAM,KAAK,SAAS,EAClD,OAAM;IAAE,MAAM;IAAc,OAAO,MAAM;IAAM;AAEjD,QAAK,MAAM,QAAQ,MAAM,aAAa,EAAE,EAAE;AACxC,UAAM;KAAE,MAAM;KAAmB,YAAY,KAAK;KAAY,UAAU,KAAK;KAAU;AACvF,UAAM;KACJ,MAAM;KACN,YAAY,KAAK;KACjB,WAAW,KAAK,UAAU,KAAK,KAAK;KACrC;AACD,UAAM;KAAE,MAAM;KAAiB,YAAY,KAAK;KAAY,WAAW,KAAK;KAAM;;AAEpF,SAAM;IACJ,MAAM;IACN,eAAe,MAAM,WAAW,UAAU,KAAK,IAAI,eAAe;IAClE,OAAO;KAAE,cAAc;KAAG,kBAAkB;KAAG,aAAa;KAAG;IAChE;;EAEH,MAAM,SAAS,MAAkD;GAC/D,MAAM,QAAQ,cAAc;AAC5B,OAAI,UAAU,OACZ,OAAM,IAAI,MACR,0BAA0B,SAAS,OAAO,iEAC3C;AAEH,UAAO;IACL,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;IACxD,GAAI,MAAM,cAAc,UAAa,MAAM,UAAU,SAAS,IAC1D,EAAE,WAAW,MAAM,UAAU,KAAK,OAAO,EAAE,GAAG,GAAG,EAAE,EAAE,GACrD,EAAE;IACN,OAAO;KAAE,cAAc;KAAG,kBAAkB;KAAG,aAAa;KAAG;IAC/D,eAAe,MAAM,WAAW,UAAU,KAAK,IAAI,eAAe;IACnE;;EAEJ"}
@@ -5,14 +5,14 @@ import { parseSecretRef, resolveSecret } from "@graphorin/security/secrets";
5
5
 
6
6
  //#region src/tooling/adapters.ts
7
7
  /**
8
- * Adapter A build the executor's `secretResolver` hook.
8
+ * Adapter A - build the executor's `secretResolver` hook.
9
9
  *
10
10
  * This is a thin **value resolver**. The executor's secrets accessor
11
11
  * (`@graphorin/tools` `tool-context.ts`) already (1) enforces the
12
12
  * per-tool `secretsAllowed` ACL via `enforceSecretAcl(key)` inside a
13
13
  * `withChildToolSecretsContext(...)` scope and (2) maps a `null` result
14
14
  * onto the optional/required-secret error contract. This adapter must
15
- * therefore **not** re-implement ACL it only resolves a key to a
15
+ * therefore **not** re-implement ACL - it only resolves a key to a
16
16
  * value.
17
17
  */
18
18
  function buildSecretResolver(options = {}) {
@@ -20,11 +20,11 @@ function buildSecretResolver(options = {}) {
20
20
  return Object.freeze({ resolve: backend });
21
21
  }
22
22
  /**
23
- * Adapter C build the executor's `memoryGuardFactory` (+ an optional
23
+ * Adapter C - build the executor's `memoryGuardFactory` (+ an optional
24
24
  * `memoryRegionReader`) from the agent's configured `Memory`.
25
25
  *
26
26
  * When `memory` is `undefined`, the factory returns `null` for every
27
- * tier and no reader is supplied the executor degrades to its
27
+ * tier and no reader is supplied - the executor degrades to its
28
28
  * audit-only baseline (the guard step is skipped).
29
29
  */
30
30
  function buildMemoryGuard(memory, options = {}) {
@@ -66,7 +66,7 @@ function createMemoryRegionReader(regions, read) {
66
66
  });
67
67
  }
68
68
  /**
69
- * Adapter D build the **synchronous** token counter the executor's
69
+ * Adapter D - build the **synchronous** token counter the executor's
70
70
  * truncation pipeline requires.
71
71
  *
72
72
  * DESIGN DECISION (impedance mismatch §1.6.1): `@graphorin/tools`'
@@ -87,7 +87,7 @@ function buildToolTokenCounter(options = {}) {
87
87
  return Object.freeze({ count: (text) => tokenize(text).length });
88
88
  }
89
89
  /**
90
- * Adapter E bridge the executor's `streamingSink` callback to an
90
+ * Adapter E - bridge the executor's `streamingSink` callback to an
91
91
  * async iterator via a bounded queue.
92
92
  */
93
93
  function createExecutorEventBridge(options = {}) {
@@ -1 +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"}
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"}
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * `orderPromotedTools` instead emits promoted tools in PROMOTION order (the
13
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
14
+ * joins the END - the catalogue grows append-only and earlier promotions keep
15
15
  * their byte position. A long-running assistant re-pays only for the new tail.
16
16
  *
17
17
  * @packageDocumentation
@@ -1 +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"}
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"}
@@ -16,8 +16,13 @@ import { createDataFlowPolicy, createTaintLedger, deriveTaintLabel } from "@grap
16
16
  *
17
17
  * Because the agent builds one executor that serves the whole run (and the
18
18
  * code-mode quiet executor too), the guard keeps a per-run ledger map.
19
- * Taint state is in-memory and run-scoped; like `tool_search` promotion it
20
- * is **not** persisted across a suspend/resume. The map is bounded by
19
+ * The live ledger (with its verbatim-probe spans) is in-memory and
20
+ * run-scoped, but its summary survives suspend/resume: the agent persists
21
+ * `snapshotLedger()` onto `RunState.taintSummary` on every exit and
22
+ * re-seeds via `seedLedger()` on resume (AG-19 / agent-08). The summary
23
+ * carries the coarse flags plus ONE-WAY span-tile hashes (C6) - never
24
+ * untrusted text - so the verbatim probe re-arms for pre-suspend content
25
+ * at tile granularity while live spans keep full sensitivity. The map is bounded by
21
26
  * insertion-order eviction so a long-lived agent never leaks ledgers.
22
27
  *
23
28
  * @packageDocumentation
@@ -74,14 +79,25 @@ function buildDataFlowGuard(config) {
74
79
  },
75
80
  record(input) {
76
81
  const ledger = ledgerFor(input.runContext.runId);
77
- const label = deriveTaintLabel({
82
+ const derived = deriveTaintLabel({
78
83
  trustClass: input.trustClass,
79
84
  ...input.source !== void 0 ? { source: input.source } : {},
80
85
  ...input.sensitivity !== void 0 ? { sensitivity: input.sensitivity } : {},
81
86
  ...config.sensitiveTiers !== void 0 ? { sensitiveTiers: config.sensitiveTiers } : {}
82
87
  });
88
+ const override = input.taintOverride;
89
+ const label = override === void 0 ? derived : {
90
+ ...derived,
91
+ untrusted: derived.untrusted || override.untrusted === true,
92
+ sensitive: derived.sensitive || override.sensitive === true,
93
+ ...override.untrusted === true && override.sourceKind !== void 0 ? { sourceKind: override.sourceKind } : {}
94
+ };
83
95
  ledger.recordOutput(label, input.outputText);
84
96
  },
97
+ recordAssistant(runId, text) {
98
+ if (text.length === 0) return;
99
+ ledgerFor(runId).recordAssistantOutput?.(text);
100
+ },
85
101
  snapshotLedger(runId) {
86
102
  return ledgers.get(runId)?.snapshot();
87
103
  },
@@ -1 +1 @@
1
- {"version":3,"file":"dataflow.js","names":[],"sources":["../../src/tooling/dataflow.ts"],"sourcesContent":["/**\n * Adapter: build the executor's {@link DataFlowGuard} from a\n * {@link DataFlowPolicyConfig} (WI-12 / P1-3).\n *\n * Bridges `@graphorin/security`'s pure taint engine (policy + per-run\n * ledger + provenance derivation) to the `@graphorin/tools` executor hook.\n * The executor calls {@link DataFlowGuard.inspect} as a sink gate and\n * {@link DataFlowGuard.record} after every successful output; this adapter\n * routes each call to the right run's {@link TaintLedger} and maps the\n * security {@link DataFlowDecision} onto the executor's\n * {@link DataFlowVerdict} (so the executor takes no security dependency).\n *\n * Because the agent builds one executor that serves the whole run (and the\n * code-mode quiet executor too), the guard keeps a per-run ledger map.\n * Taint state is in-memory and run-scoped; like `tool_search` promotion it\n * is **not** persisted across a suspend/resume. The map is bounded by\n * insertion-order eviction so a long-lived agent never leaks ledgers.\n *\n * @packageDocumentation\n */\n\nimport {\n createDataFlowPolicy,\n createTaintLedger,\n type DataFlowPolicyConfig,\n deriveTaintLabel,\n type TaintLedger,\n type TaintLedgerSnapshot,\n} from '@graphorin/security/dataflow';\nimport { containsPii } from '@graphorin/security/guardrails';\nimport type {\n DataFlowGuard,\n DataFlowInspectInput,\n DataFlowRecordInput,\n DataFlowVerdict,\n} from '@graphorin/tools/executor';\n\n/** Max concurrent run ledgers retained before evicting the oldest. */\nconst MAX_TRACKED_RUNS = 128;\n\n/** Serialize tool-call args to text for the verbatim-carry probe. */\nfunction stringifyArgs(value: unknown): string {\n if (typeof value === 'string') return value;\n try {\n return JSON.stringify(value) ?? '';\n } catch {\n return '';\n }\n}\n\n/**\n * Build a {@link DataFlowGuard} backed by `@graphorin/security`'s taint\n * engine. Returns a guard whose `inspect`/`record` operate on a per-run\n * ledger keyed by `runContext.runId`.\n *\n * @internal\n */\n/**\n * The executor's {@link DataFlowGuard} plus AG-19 ledger-persistence hooks the\n * agent uses to carry the coarse taint summary across a suspend/resume.\n */\nexport interface DataFlowGuardWithLedgers extends DataFlowGuard {\n /** Read a run's coarse taint summary to persist on suspend (or `undefined`). */\n snapshotLedger(runId: string): TaintLedgerSnapshot | undefined;\n /** Pre-seed a run's ledger from a persisted summary on resume. */\n seedLedger(runId: string, summary: TaintLedgerSnapshot): void;\n}\n\nexport function buildDataFlowGuard(config: DataFlowPolicyConfig): DataFlowGuardWithLedgers {\n const policy = createDataFlowPolicy(config);\n // FIDES-lattice (SDF-8): when treatPiiAsSensitive is on, feed the PII catalogue\n // into the ledger so PII reads arm the trifecta `sensitive` leg. Default off.\n const ledgerOpts = {\n ...(config.minSpanLength !== undefined ? { minSpanLength: config.minSpanLength } : {}),\n ...(config.treatPiiAsSensitive === true ? { piiSensitivity: containsPii } : {}),\n };\n const ledgers = new Map<string, TaintLedger>();\n\n function ledgerFor(runId: string): TaintLedger {\n const existing = ledgers.get(runId);\n if (existing !== undefined) return existing;\n const ledger = createTaintLedger(ledgerOpts);\n ledgers.set(runId, ledger);\n while (ledgers.size > MAX_TRACKED_RUNS) {\n const oldest = ledgers.keys().next().value;\n if (oldest === undefined) break;\n ledgers.delete(oldest);\n }\n return ledger;\n }\n\n return {\n inspect(input: DataFlowInspectInput): DataFlowVerdict {\n const ledger = ledgerFor(input.runContext.runId);\n const probe = ledger.inspectArgs(stringifyArgs(input.args));\n const decision = policy.evaluate({\n toolName: input.toolName,\n sideEffectClass: input.sideEffectClass,\n carriesUntrustedVerbatim: probe.carriesUntrustedVerbatim,\n untrustedSeen: ledger.untrustedSeen,\n sensitiveSeen: ledger.sensitiveSeen,\n sourceKinds:\n probe.matchedSourceKinds.length > 0\n ? probe.matchedSourceKinds\n : ledger.untrustedSourceKinds,\n });\n if (decision.action === 'allow') return { action: 'allow' };\n return {\n action: decision.action,\n flow: decision.flow,\n reason: decision.reason,\n sourceKinds: decision.sourceKinds,\n };\n },\n\n record(input: DataFlowRecordInput): void {\n const ledger = ledgerFor(input.runContext.runId);\n const label = deriveTaintLabel({\n trustClass: input.trustClass,\n ...(input.source !== undefined ? { source: input.source } : {}),\n ...(input.sensitivity !== undefined ? { sensitivity: input.sensitivity } : {}),\n // SDF-8: widen the trifecta `sensitive` leg to the operator's\n // configured tiers (default secret-only ⇒ byte-identical).\n ...(config.sensitiveTiers !== undefined ? { sensitiveTiers: config.sensitiveTiers } : {}),\n });\n ledger.recordOutput(label, input.outputText);\n },\n\n // AG-19: snapshot/rehydrate the run's coarse taint summary across a\n // suspend/resume so the sink gate is not silently un-gated on the HITL\n // boundary. Only the load-bearing flags cross the boundary never spans.\n snapshotLedger(runId: string): TaintLedgerSnapshot | undefined {\n return ledgers.get(runId)?.snapshot();\n },\n seedLedger(runId: string, summary: TaintLedgerSnapshot): void {\n ledgers.set(runId, createTaintLedger({ ...ledgerOpts, initial: summary }));\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,MAAM,mBAAmB;;AAGzB,SAAS,cAAc,OAAwB;AAC7C,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI;AACF,SAAO,KAAK,UAAU,MAAM,IAAI;SAC1B;AACN,SAAO;;;AAsBX,SAAgB,mBAAmB,QAAwD;CACzF,MAAM,SAAS,qBAAqB,OAAO;CAG3C,MAAM,aAAa;EACjB,GAAI,OAAO,kBAAkB,SAAY,EAAE,eAAe,OAAO,eAAe,GAAG,EAAE;EACrF,GAAI,OAAO,wBAAwB,OAAO,EAAE,gBAAgB,aAAa,GAAG,EAAE;EAC/E;CACD,MAAM,0BAAU,IAAI,KAA0B;CAE9C,SAAS,UAAU,OAA4B;EAC7C,MAAM,WAAW,QAAQ,IAAI,MAAM;AACnC,MAAI,aAAa,OAAW,QAAO;EACnC,MAAM,SAAS,kBAAkB,WAAW;AAC5C,UAAQ,IAAI,OAAO,OAAO;AAC1B,SAAO,QAAQ,OAAO,kBAAkB;GACtC,MAAM,SAAS,QAAQ,MAAM,CAAC,MAAM,CAAC;AACrC,OAAI,WAAW,OAAW;AAC1B,WAAQ,OAAO,OAAO;;AAExB,SAAO;;AAGT,QAAO;EACL,QAAQ,OAA8C;GACpD,MAAM,SAAS,UAAU,MAAM,WAAW,MAAM;GAChD,MAAM,QAAQ,OAAO,YAAY,cAAc,MAAM,KAAK,CAAC;GAC3D,MAAM,WAAW,OAAO,SAAS;IAC/B,UAAU,MAAM;IAChB,iBAAiB,MAAM;IACvB,0BAA0B,MAAM;IAChC,eAAe,OAAO;IACtB,eAAe,OAAO;IACtB,aACE,MAAM,mBAAmB,SAAS,IAC9B,MAAM,qBACN,OAAO;IACd,CAAC;AACF,OAAI,SAAS,WAAW,QAAS,QAAO,EAAE,QAAQ,SAAS;AAC3D,UAAO;IACL,QAAQ,SAAS;IACjB,MAAM,SAAS;IACf,QAAQ,SAAS;IACjB,aAAa,SAAS;IACvB;;EAGH,OAAO,OAAkC;GACvC,MAAM,SAAS,UAAU,MAAM,WAAW,MAAM;GAChD,MAAM,QAAQ,iBAAiB;IAC7B,YAAY,MAAM;IAClB,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,QAAQ,GAAG,EAAE;IAC9D,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;IAG7E,GAAI,OAAO,mBAAmB,SAAY,EAAE,gBAAgB,OAAO,gBAAgB,GAAG,EAAE;IACzF,CAAC;AACF,UAAO,aAAa,OAAO,MAAM,WAAW;;EAM9C,eAAe,OAAgD;AAC7D,UAAO,QAAQ,IAAI,MAAM,EAAE,UAAU;;EAEvC,WAAW,OAAe,SAAoC;AAC5D,WAAQ,IAAI,OAAO,kBAAkB;IAAE,GAAG;IAAY,SAAS;IAAS,CAAC,CAAC;;EAE7E"}
1
+ {"version":3,"file":"dataflow.js","names":[],"sources":["../../src/tooling/dataflow.ts"],"sourcesContent":["/**\n * Adapter: build the executor's {@link DataFlowGuard} from a\n * {@link DataFlowPolicyConfig} (WI-12 / P1-3).\n *\n * Bridges `@graphorin/security`'s pure taint engine (policy + per-run\n * ledger + provenance derivation) to the `@graphorin/tools` executor hook.\n * The executor calls {@link DataFlowGuard.inspect} as a sink gate and\n * {@link DataFlowGuard.record} after every successful output; this adapter\n * routes each call to the right run's {@link TaintLedger} and maps the\n * security {@link DataFlowDecision} onto the executor's\n * {@link DataFlowVerdict} (so the executor takes no security dependency).\n *\n * Because the agent builds one executor that serves the whole run (and the\n * code-mode quiet executor too), the guard keeps a per-run ledger map.\n * The live ledger (with its verbatim-probe spans) is in-memory and\n * run-scoped, but its summary survives suspend/resume: the agent persists\n * `snapshotLedger()` onto `RunState.taintSummary` on every exit and\n * re-seeds via `seedLedger()` on resume (AG-19 / agent-08). The summary\n * carries the coarse flags plus ONE-WAY span-tile hashes (C6) - never\n * untrusted text - so the verbatim probe re-arms for pre-suspend content\n * at tile granularity while live spans keep full sensitivity. The map is bounded by\n * insertion-order eviction so a long-lived agent never leaks ledgers.\n *\n * @packageDocumentation\n */\n\nimport {\n createDataFlowPolicy,\n createTaintLedger,\n type DataFlowPolicyConfig,\n deriveTaintLabel,\n type TaintLedger,\n type TaintLedgerSnapshot,\n} from '@graphorin/security/dataflow';\nimport { containsPii } from '@graphorin/security/guardrails';\nimport type {\n DataFlowGuard,\n DataFlowInspectInput,\n DataFlowRecordInput,\n DataFlowVerdict,\n} from '@graphorin/tools/executor';\n\n/** Max concurrent run ledgers retained before evicting the oldest. */\nconst MAX_TRACKED_RUNS = 128;\n\n/** Serialize tool-call args to text for the verbatim-carry probe. */\nfunction stringifyArgs(value: unknown): string {\n if (typeof value === 'string') return value;\n try {\n return JSON.stringify(value) ?? '';\n } catch {\n return '';\n }\n}\n\n/**\n * Build a {@link DataFlowGuard} backed by `@graphorin/security`'s taint\n * engine. Returns a guard whose `inspect`/`record` operate on a per-run\n * ledger keyed by `runContext.runId`.\n *\n * @internal\n */\n/**\n * The executor's {@link DataFlowGuard} plus AG-19 ledger-persistence hooks the\n * agent uses to carry the coarse taint summary across a suspend/resume.\n */\nexport interface DataFlowGuardWithLedgers extends DataFlowGuard {\n /** Read a run's coarse taint summary to persist on suspend (or `undefined`). */\n snapshotLedger(runId: string): TaintLedgerSnapshot | undefined;\n /** Pre-seed a run's ledger from a persisted summary on resume. */\n seedLedger(runId: string, summary: TaintLedgerSnapshot): void;\n /**\n * C6: record the model's own step output as derived-untrusted (no-op on\n * an untainted run). Makes the verbatim probe catch args the model\n * copied from its own untrusted-derived phrasing, and feeds the\n * `derivedTaint: 'strict'` policy leg.\n */\n recordAssistant(runId: string, text: string): void;\n}\n\nexport function buildDataFlowGuard(config: DataFlowPolicyConfig): DataFlowGuardWithLedgers {\n const policy = createDataFlowPolicy(config);\n // FIDES-lattice (SDF-8): when treatPiiAsSensitive is on, feed the PII catalogue\n // into the ledger so PII reads arm the trifecta `sensitive` leg. Default off.\n const ledgerOpts = {\n ...(config.minSpanLength !== undefined ? { minSpanLength: config.minSpanLength } : {}),\n ...(config.treatPiiAsSensitive === true ? { piiSensitivity: containsPii } : {}),\n };\n const ledgers = new Map<string, TaintLedger>();\n\n function ledgerFor(runId: string): TaintLedger {\n const existing = ledgers.get(runId);\n if (existing !== undefined) return existing;\n const ledger = createTaintLedger(ledgerOpts);\n ledgers.set(runId, ledger);\n while (ledgers.size > MAX_TRACKED_RUNS) {\n const oldest = ledgers.keys().next().value;\n if (oldest === undefined) break;\n ledgers.delete(oldest);\n }\n return ledger;\n }\n\n return {\n inspect(input: DataFlowInspectInput): DataFlowVerdict {\n const ledger = ledgerFor(input.runContext.runId);\n const probe = ledger.inspectArgs(stringifyArgs(input.args));\n const decision = policy.evaluate({\n toolName: input.toolName,\n sideEffectClass: input.sideEffectClass,\n carriesUntrustedVerbatim: probe.carriesUntrustedVerbatim,\n untrustedSeen: ledger.untrustedSeen,\n sensitiveSeen: ledger.sensitiveSeen,\n sourceKinds:\n probe.matchedSourceKinds.length > 0\n ? probe.matchedSourceKinds\n : ledger.untrustedSourceKinds,\n });\n if (decision.action === 'allow') return { action: 'allow' };\n return {\n action: decision.action,\n flow: decision.flow,\n reason: decision.reason,\n sourceKinds: decision.sourceKinds,\n };\n },\n\n record(input: DataFlowRecordInput): void {\n const ledger = ledgerFor(input.runContext.runId);\n const derived = deriveTaintLabel({\n trustClass: input.trustClass,\n ...(input.source !== undefined ? { source: input.source } : {}),\n ...(input.sensitivity !== undefined ? { sensitivity: input.sensitivity } : {}),\n // SDF-8: widen the trifecta `sensitive` leg to the operator's\n // configured tiers (default secret-only ⇒ byte-identical).\n ...(config.sensitiveTiers !== undefined ? { sensitiveTiers: config.sensitiveTiers } : {}),\n });\n // C6: a ToolReturn taint override only ever WIDENS the derived\n // label - a first-party recall tool can mark quarantined content\n // untrusted, but nothing can launder an untrusted tool's output.\n const override = input.taintOverride;\n const label =\n override === undefined\n ? derived\n : {\n ...derived,\n untrusted: derived.untrusted || override.untrusted === true,\n sensitive: derived.sensitive || override.sensitive === true,\n ...(override.untrusted === true && override.sourceKind !== undefined\n ? { sourceKind: override.sourceKind }\n : {}),\n };\n ledger.recordOutput(label, input.outputText);\n },\n\n recordAssistant(runId: string, text: string): void {\n if (text.length === 0) return;\n ledgerFor(runId).recordAssistantOutput?.(text);\n },\n\n // AG-19: snapshot/rehydrate the run's coarse taint summary across a\n // suspend/resume so the sink gate is not silently un-gated on the HITL\n // boundary. Only the load-bearing flags cross the boundary - never spans.\n snapshotLedger(runId: string): TaintLedgerSnapshot | undefined {\n return ledgers.get(runId)?.snapshot();\n },\n seedLedger(runId: string, summary: TaintLedgerSnapshot): void {\n ledgers.set(runId, createTaintLedger({ ...ledgerOpts, initial: summary }));\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,mBAAmB;;AAGzB,SAAS,cAAc,OAAwB;AAC7C,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI;AACF,SAAO,KAAK,UAAU,MAAM,IAAI;SAC1B;AACN,SAAO;;;AA6BX,SAAgB,mBAAmB,QAAwD;CACzF,MAAM,SAAS,qBAAqB,OAAO;CAG3C,MAAM,aAAa;EACjB,GAAI,OAAO,kBAAkB,SAAY,EAAE,eAAe,OAAO,eAAe,GAAG,EAAE;EACrF,GAAI,OAAO,wBAAwB,OAAO,EAAE,gBAAgB,aAAa,GAAG,EAAE;EAC/E;CACD,MAAM,0BAAU,IAAI,KAA0B;CAE9C,SAAS,UAAU,OAA4B;EAC7C,MAAM,WAAW,QAAQ,IAAI,MAAM;AACnC,MAAI,aAAa,OAAW,QAAO;EACnC,MAAM,SAAS,kBAAkB,WAAW;AAC5C,UAAQ,IAAI,OAAO,OAAO;AAC1B,SAAO,QAAQ,OAAO,kBAAkB;GACtC,MAAM,SAAS,QAAQ,MAAM,CAAC,MAAM,CAAC;AACrC,OAAI,WAAW,OAAW;AAC1B,WAAQ,OAAO,OAAO;;AAExB,SAAO;;AAGT,QAAO;EACL,QAAQ,OAA8C;GACpD,MAAM,SAAS,UAAU,MAAM,WAAW,MAAM;GAChD,MAAM,QAAQ,OAAO,YAAY,cAAc,MAAM,KAAK,CAAC;GAC3D,MAAM,WAAW,OAAO,SAAS;IAC/B,UAAU,MAAM;IAChB,iBAAiB,MAAM;IACvB,0BAA0B,MAAM;IAChC,eAAe,OAAO;IACtB,eAAe,OAAO;IACtB,aACE,MAAM,mBAAmB,SAAS,IAC9B,MAAM,qBACN,OAAO;IACd,CAAC;AACF,OAAI,SAAS,WAAW,QAAS,QAAO,EAAE,QAAQ,SAAS;AAC3D,UAAO;IACL,QAAQ,SAAS;IACjB,MAAM,SAAS;IACf,QAAQ,SAAS;IACjB,aAAa,SAAS;IACvB;;EAGH,OAAO,OAAkC;GACvC,MAAM,SAAS,UAAU,MAAM,WAAW,MAAM;GAChD,MAAM,UAAU,iBAAiB;IAC/B,YAAY,MAAM;IAClB,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,QAAQ,GAAG,EAAE;IAC9D,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;IAG7E,GAAI,OAAO,mBAAmB,SAAY,EAAE,gBAAgB,OAAO,gBAAgB,GAAG,EAAE;IACzF,CAAC;GAIF,MAAM,WAAW,MAAM;GACvB,MAAM,QACJ,aAAa,SACT,UACA;IACE,GAAG;IACH,WAAW,QAAQ,aAAa,SAAS,cAAc;IACvD,WAAW,QAAQ,aAAa,SAAS,cAAc;IACvD,GAAI,SAAS,cAAc,QAAQ,SAAS,eAAe,SACvD,EAAE,YAAY,SAAS,YAAY,GACnC,EAAE;IACP;AACP,UAAO,aAAa,OAAO,MAAM,WAAW;;EAG9C,gBAAgB,OAAe,MAAoB;AACjD,OAAI,KAAK,WAAW,EAAG;AACvB,aAAU,MAAM,CAAC,wBAAwB,KAAK;;EAMhD,eAAe,OAAgD;AAC7D,UAAO,QAAQ,IAAI,MAAM,EAAE,UAAU;;EAEvC,WAAW,OAAe,SAAoC;AAC5D,WAAQ,IAAI,OAAO,kBAAkB;IAAE,GAAG;IAAY,SAAS;IAAS,CAAC,CAAC;;EAE7E"}
@@ -0,0 +1,92 @@
1
+ //#region src/tooling/plan.ts
2
+ /** Stable name of the built-in plan tool. */
3
+ const PLAN_TOOL_NAME = "update_plan";
4
+ const STATUS_VALUES = [
5
+ "pending",
6
+ "in_progress",
7
+ "completed"
8
+ ];
9
+ const planInputSchema = {
10
+ parse: (v) => v,
11
+ safeParse: (v) => {
12
+ if (typeof v !== "object" || v === null || !Array.isArray(v.todos)) return {
13
+ success: false,
14
+ error: /* @__PURE__ */ new Error("expected { todos: TodoItem[] }")
15
+ };
16
+ const todos = v.todos;
17
+ for (const t of todos) if (typeof t !== "object" || t === null || typeof t.id !== "string" || typeof t.content !== "string" || !STATUS_VALUES.includes(t.status)) return {
18
+ success: false,
19
+ error: /* @__PURE__ */ new Error("invalid TodoItem")
20
+ };
21
+ return {
22
+ success: true,
23
+ data: v
24
+ };
25
+ },
26
+ toJSON: () => ({
27
+ type: "object",
28
+ properties: { todos: {
29
+ type: "array",
30
+ items: {
31
+ type: "object",
32
+ properties: {
33
+ id: { type: "string" },
34
+ content: { type: "string" },
35
+ status: {
36
+ type: "string",
37
+ enum: [...STATUS_VALUES]
38
+ }
39
+ },
40
+ required: [
41
+ "id",
42
+ "content",
43
+ "status"
44
+ ]
45
+ }
46
+ } },
47
+ required: ["todos"]
48
+ })
49
+ };
50
+ /**
51
+ * Build the plan tool. `applyTodos` writes the validated list into the
52
+ * active RunState (the factory closes over `activeRunState`). Pure
53
+ * read-only-ish: it mutates run bookkeeping, not the outside world, so
54
+ * it is `sideEffectClass: 'read-only'` (never a data-flow sink).
55
+ */
56
+ function createPlanTool(applyTodos) {
57
+ return {
58
+ name: PLAN_TOOL_NAME,
59
+ description: "Record or update your working plan as a checklist of todos (TodoWrite-style, full replace). Each item has an id, content, and status (pending | in_progress | completed). Keep exactly ONE item in_progress at a time; mark items completed as you finish them. The plan is journaled and recited back to you each turn - use it to stay on task across a long run.",
60
+ inputSchema: planInputSchema,
61
+ sideEffectClass: "read-only",
62
+ async execute(input) {
63
+ const todos = input.todos.map((t) => ({
64
+ id: t.id,
65
+ content: t.content,
66
+ status: t.status
67
+ }));
68
+ applyTodos(todos);
69
+ return {
70
+ count: todos.length,
71
+ completed: todos.filter((t) => t.status === "completed").length
72
+ };
73
+ }
74
+ };
75
+ }
76
+ /**
77
+ * Render the attention-recitation block for the current plan. Returns
78
+ * `null` for an empty plan (nothing to recite). The block is appended to
79
+ * the per-step request copy near the END so it rides the last
80
+ * cache anchor and never busts the stable prompt prefix.
81
+ *
82
+ * @stable
83
+ */
84
+ function renderPlanRecitation(todos) {
85
+ if (todos === void 0 || todos.length === 0) return null;
86
+ const mark = (s) => s === "completed" ? "[x]" : s === "in_progress" ? "[~]" : "[ ]";
87
+ return `<plan reminder="stay on task; keep one item in progress">\n${todos.map((t) => `${mark(t.status)} ${t.content}`).join("\n")}\n</plan>`;
88
+ }
89
+
90
+ //#endregion
91
+ export { PLAN_TOOL_NAME, createPlanTool, renderPlanRecitation };
92
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.js","names":["todos: TodoItem[]"],"sources":["../../src/tooling/plan.ts"],"sourcesContent":["/**\n * D6 structured plan / todo tool + attention recitation. The tool is a\n * TodoWrite-style status-flip mutation over the agent's own working\n * plan, journaled in `RunState.todos` so it survives suspend/resume.\n * Attention recitation renders the current plan back into the prompt\n * near the context end each turn (request-only, cache-layout-aware) so\n * the model keeps its objective in focus on long runs (Manus todo.md /\n * lost-in-the-middle evidence).\n *\n * The tool lives in `@graphorin/agent` (not `@graphorin/tools`) because\n * it mutates run state: its `execute` writes through a factory-supplied\n * callback into the active `RunState`.\n *\n * @packageDocumentation\n */\n\nimport type { TodoItem, Tool } from '@graphorin/core';\n\n/** Stable name of the built-in plan tool. */\nexport const PLAN_TOOL_NAME = 'update_plan';\n\nconst STATUS_VALUES = ['pending', 'in_progress', 'completed'] as const;\n\ninterface PlanToolInput {\n readonly todos: ReadonlyArray<{\n readonly id: string;\n readonly content: string;\n readonly status: 'pending' | 'in_progress' | 'completed';\n }>;\n}\n\ninterface PlanToolOutput {\n readonly count: number;\n readonly completed: number;\n}\n\nconst planInputSchema = {\n parse: (v: unknown): PlanToolInput => v as PlanToolInput,\n safeParse: (v: unknown) => {\n if (typeof v !== 'object' || v === null || !Array.isArray((v as { todos?: unknown }).todos)) {\n return { success: false as const, error: new Error('expected { todos: TodoItem[] }') };\n }\n const todos = (v as { todos: unknown[] }).todos;\n for (const t of todos) {\n if (\n typeof t !== 'object' ||\n t === null ||\n typeof (t as { id?: unknown }).id !== 'string' ||\n typeof (t as { content?: unknown }).content !== 'string' ||\n !STATUS_VALUES.includes(\n (t as { status?: unknown }).status as (typeof STATUS_VALUES)[number],\n )\n ) {\n return { success: false as const, error: new Error('invalid TodoItem') };\n }\n }\n return { success: true as const, data: v as PlanToolInput };\n },\n toJSON: (): Record<string, unknown> => ({\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n content: { type: 'string' },\n status: { type: 'string', enum: [...STATUS_VALUES] },\n },\n required: ['id', 'content', 'status'],\n },\n },\n },\n required: ['todos'],\n }),\n} as unknown as Tool<PlanToolInput, PlanToolOutput>['inputSchema'];\n\n/**\n * Build the plan tool. `applyTodos` writes the validated list into the\n * active RunState (the factory closes over `activeRunState`). Pure\n * read-only-ish: it mutates run bookkeeping, not the outside world, so\n * it is `sideEffectClass: 'read-only'` (never a data-flow sink).\n */\nexport function createPlanTool(\n applyTodos: (todos: ReadonlyArray<TodoItem>) => void,\n): Tool<PlanToolInput, PlanToolOutput> {\n return {\n name: PLAN_TOOL_NAME,\n description:\n 'Record or update your working plan as a checklist of todos (TodoWrite-style, full replace). Each item has an id, content, and status (pending | in_progress | completed). Keep exactly ONE item in_progress at a time; mark items completed as you finish them. The plan is journaled and recited back to you each turn - use it to stay on task across a long run.',\n inputSchema: planInputSchema,\n sideEffectClass: 'read-only',\n async execute(input): Promise<PlanToolOutput> {\n const todos: TodoItem[] = input.todos.map((t) => ({\n id: t.id,\n content: t.content,\n status: t.status,\n }));\n applyTodos(todos);\n return {\n count: todos.length,\n completed: todos.filter((t) => t.status === 'completed').length,\n };\n },\n } as Tool<PlanToolInput, PlanToolOutput>;\n}\n\n/**\n * Render the attention-recitation block for the current plan. Returns\n * `null` for an empty plan (nothing to recite). The block is appended to\n * the per-step request copy near the END so it rides the last\n * cache anchor and never busts the stable prompt prefix.\n *\n * @stable\n */\nexport function renderPlanRecitation(todos: ReadonlyArray<TodoItem> | undefined): string | null {\n if (todos === undefined || todos.length === 0) return null;\n const mark = (s: TodoItem['status']): string =>\n s === 'completed' ? '[x]' : s === 'in_progress' ? '[~]' : '[ ]';\n const lines = todos.map((t) => `${mark(t.status)} ${t.content}`);\n return `<plan reminder=\"stay on task; keep one item in progress\">\\n${lines.join('\\n')}\\n</plan>`;\n}\n"],"mappings":";;AAmBA,MAAa,iBAAiB;AAE9B,MAAM,gBAAgB;CAAC;CAAW;CAAe;CAAY;AAe7D,MAAM,kBAAkB;CACtB,QAAQ,MAA8B;CACtC,YAAY,MAAe;AACzB,MAAI,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAS,EAA0B,MAAM,CACzF,QAAO;GAAE,SAAS;GAAgB,uBAAO,IAAI,MAAM,iCAAiC;GAAE;EAExF,MAAM,QAAS,EAA2B;AAC1C,OAAK,MAAM,KAAK,MACd,KACE,OAAO,MAAM,YACb,MAAM,QACN,OAAQ,EAAuB,OAAO,YACtC,OAAQ,EAA4B,YAAY,YAChD,CAAC,cAAc,SACZ,EAA2B,OAC7B,CAED,QAAO;GAAE,SAAS;GAAgB,uBAAO,IAAI,MAAM,mBAAmB;GAAE;AAG5E,SAAO;GAAE,SAAS;GAAe,MAAM;GAAoB;;CAE7D,eAAwC;EACtC,MAAM;EACN,YAAY,EACV,OAAO;GACL,MAAM;GACN,OAAO;IACL,MAAM;IACN,YAAY;KACV,IAAI,EAAE,MAAM,UAAU;KACtB,SAAS,EAAE,MAAM,UAAU;KAC3B,QAAQ;MAAE,MAAM;MAAU,MAAM,CAAC,GAAG,cAAc;MAAE;KACrD;IACD,UAAU;KAAC;KAAM;KAAW;KAAS;IACtC;GACF,EACF;EACD,UAAU,CAAC,QAAQ;EACpB;CACF;;;;;;;AAQD,SAAgB,eACd,YACqC;AACrC,QAAO;EACL,MAAM;EACN,aACE;EACF,aAAa;EACb,iBAAiB;EACjB,MAAM,QAAQ,OAAgC;GAC5C,MAAMA,QAAoB,MAAM,MAAM,KAAK,OAAO;IAChD,IAAI,EAAE;IACN,SAAS,EAAE;IACX,QAAQ,EAAE;IACX,EAAE;AACH,cAAW,MAAM;AACjB,UAAO;IACL,OAAO,MAAM;IACb,WAAW,MAAM,QAAQ,MAAM,EAAE,WAAW,YAAY,CAAC;IAC1D;;EAEJ;;;;;;;;;;AAWH,SAAgB,qBAAqB,OAA2D;AAC9F,KAAI,UAAU,UAAa,MAAM,WAAW,EAAG,QAAO;CACtD,MAAM,QAAQ,MACZ,MAAM,cAAc,QAAQ,MAAM,gBAAgB,QAAQ;AAE5D,QAAO,8DADO,MAAM,KAAK,MAAM,GAAG,KAAK,EAAE,OAAO,CAAC,GAAG,EAAE,UAAU,CACW,KAAK,KAAK,CAAC"}