@graphorin/agent 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +12 -12
  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 -4
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -3
  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/preferred-model/index.d.ts +2 -2
  37. package/dist/preferred-model/index.js +2 -2
  38. package/dist/preferred-model/index.js.map +1 -1
  39. package/dist/progress/index.js +2 -2
  40. package/dist/progress/index.js.map +1 -1
  41. package/dist/run-state/index.d.ts +7 -5
  42. package/dist/run-state/index.d.ts.map +1 -1
  43. package/dist/run-state/index.js +33 -4
  44. package/dist/run-state/index.js.map +1 -1
  45. package/dist/testing/replay-provider.d.ts +18 -0
  46. package/dist/testing/replay-provider.d.ts.map +1 -0
  47. package/dist/testing/replay-provider.js +100 -0
  48. package/dist/testing/replay-provider.js.map +1 -0
  49. package/dist/tooling/adapters.js +6 -6
  50. package/dist/tooling/adapters.js.map +1 -1
  51. package/dist/tooling/catalogue.js +1 -1
  52. package/dist/tooling/catalogue.js.map +1 -1
  53. package/dist/tooling/dataflow.js +19 -3
  54. package/dist/tooling/dataflow.js.map +1 -1
  55. package/dist/tooling/plan.js +92 -0
  56. package/dist/tooling/plan.js.map +1 -0
  57. package/dist/tooling/policy.js +45 -0
  58. package/dist/tooling/policy.js.map +1 -0
  59. package/dist/tooling/registry-build.js +1 -1
  60. package/dist/tooling/registry-build.js.map +1 -1
  61. package/dist/types.d.ts +189 -18
  62. package/dist/types.d.ts.map +1 -1
  63. package/package.json +11 -8
@@ -1 +1 @@
1
- {"version":3,"file":"causality-monitor.js","names":["DEFAULT_DENIAL_PATTERNS: ReadonlyArray<RegExp>","#chain","matchedPattern: string | undefined"],"sources":["../../src/lateral-leak/causality-monitor.ts"],"sourcesContent":["/**\n * `CausalityMonitor` lateral-leak defense primitive that maintains\n * a per-`RunContext` `causalityChain` of bounded depth and refuses\n * to let an assistant message reference information about a denied\n * earlier action.\n *\n * The monitor is opt-in: agents declare `causalityMonitor:\n * { strictness, denialPatterns?, ... }` on `createAgent({...})`.\n * When the field is absent, the runtime instantiates a no-op monitor\n * that records nothing and never flags.\n *\n * @packageDocumentation\n */\n\nimport type { LateralLeakVector } from '@graphorin/core';\n\n/**\n * Operator-tunable strictness level. Default `'detect-and-flag'`\n * for cloud-tier providers; `'detect'` for loopback providers;\n * `'off'` for v0.1-alpha backward compatibility.\n *\n * @stable\n */\nexport type CausalityMonitorStrictness = 'off' | 'detect' | 'detect-and-flag' | 'detect-and-block';\n\n/**\n * Per-agent configuration accepted by `createAgent({ causalityMonitor })`.\n *\n * @stable\n */\nexport interface CausalityMonitorConfig {\n readonly strictness: CausalityMonitorStrictness;\n /** Maximum depth of the chain. Default `32`. */\n readonly maxChainDepth?: number;\n /** Operator-extensible denial patterns. */\n readonly denialPatterns?: ReadonlyArray<RegExp>;\n /**\n * When `true`, emit the chain on every `checkMessage(...)` call\n * (high-cardinality; opt-in for compliance audits). Default\n * `false` only emit on detected leaks.\n */\n readonly auditAllChains?: boolean;\n}\n\n/**\n * Default denial-pattern catalogue. The agent runtime extends this\n * list when the operator supplies their own patterns.\n *\n * @stable\n */\nexport const DEFAULT_DENIAL_PATTERNS: ReadonlyArray<RegExp> = [\n /SecretAccessDenied/i,\n /ToolApprovalDenied/i,\n /HandoffPermissionDenied/i,\n /SandboxViolation/i,\n];\n\n/** Default chain depth when not specified. */\nexport const DEFAULT_MAX_CHAIN_DEPTH = 32;\n\n/**\n * Result returned by {@link CausalityMonitor.checkMessage}.\n *\n * @stable\n */\nexport interface CausalityMonitorCheck {\n readonly leakDetected: boolean;\n readonly severity: 'info' | 'warn' | 'block';\n readonly causalityChain: ReadonlyArray<string>;\n readonly matchedPattern?: string;\n readonly decision: 'detect' | 'flag' | 'strip' | 'block';\n readonly vector: LateralLeakVector;\n}\n\n/**\n * In-memory primitive instantiated per `RunContext`. Bounded-depth\n * append discipline keeps the memory footprint trivial even on long\n * runs.\n *\n * @stable\n */\nexport class CausalityMonitor {\n readonly strictness: CausalityMonitorStrictness;\n readonly maxChainDepth: number;\n readonly denialPatterns: ReadonlyArray<RegExp>;\n readonly auditAllChains: boolean;\n #chain: string[] = [];\n\n constructor(cfg: CausalityMonitorConfig) {\n this.strictness = cfg.strictness;\n this.maxChainDepth = cfg.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;\n this.denialPatterns = cfg.denialPatterns\n ? [...DEFAULT_DENIAL_PATTERNS, ...cfg.denialPatterns]\n : DEFAULT_DENIAL_PATTERNS;\n this.auditAllChains = cfg.auditAllChains ?? false;\n }\n\n /** Snapshot the current causality chain. */\n get chain(): ReadonlyArray<string> {\n return this.#chain.slice();\n }\n\n /**\n * Append an entry to the causality chain, dropping the oldest\n * when the chain exceeds `maxChainDepth`. Bounded-length, no PII,\n * no secret values entries are short opaque strings like\n * `tool:slack-notify`, `tool.error:SecretAccessDenied`,\n * `subagent:research-east`, `compaction:auto-trigger`.\n */\n recordCall(entry: string): void {\n if (this.strictness === 'off') return;\n if (entry.length === 0) return;\n this.#chain.push(entry);\n if (this.#chain.length > this.maxChainDepth) {\n this.#chain.shift();\n }\n }\n\n /** Reset the chain e.g. on `agent.run` boundary. */\n reset(): void {\n this.#chain = [];\n }\n\n /**\n * Inspect a candidate assistant-visible string and return whether\n * the lateral-leak defense should fire. Pure decision based on\n * the current chain + the operator-extensible denial patterns.\n */\n checkMessage(content: string): CausalityMonitorCheck {\n if (this.strictness === 'off') {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: [],\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n const chainHasDenial = this.#chain.some((entry) =>\n this.denialPatterns.some((pat) => pat.test(entry)),\n );\n if (!chainHasDenial) {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n let matchedPattern: string | undefined;\n for (const pat of this.denialPatterns) {\n if (pat.test(content)) {\n matchedPattern = pat.source;\n break;\n }\n }\n const leakDetected = matchedPattern !== undefined;\n if (!leakDetected) {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n switch (this.strictness) {\n case 'detect':\n return {\n leakDetected: true,\n severity: 'info',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'detect',\n vector: 'causality-laundering',\n };\n case 'detect-and-flag':\n return {\n leakDetected: true,\n severity: 'warn',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'flag',\n vector: 'causality-laundering',\n };\n case 'detect-and-block':\n return {\n leakDetected: true,\n severity: 'block',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'block',\n vector: 'causality-laundering',\n };\n default: {\n const _exhaustive: never = this.strictness;\n void _exhaustive;\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n }\n }\n\n /**\n * Drain the chain to the audit log on `agent.run` completion or\n * `agent.abort`. The runtime supplies the audit emitter the\n * primitive itself is storage-agnostic.\n */\n flush(reason: 'agent.run.complete' | 'agent.abort'): {\n readonly chain: ReadonlyArray<string>;\n readonly reason: 'agent.run.complete' | 'agent.abort';\n } {\n return { chain: this.chain, reason };\n }\n}\n"],"mappings":";;;;;;;AAkDA,MAAaA,0BAAiD;CAC5D;CACA;CACA;CACA;CACD;;AAGD,MAAa,0BAA0B;;;;;;;;AAuBvC,IAAa,mBAAb,MAA8B;CAC5B,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CACT,SAAmB,EAAE;CAErB,YAAY,KAA6B;AACvC,OAAK,aAAa,IAAI;AACtB,OAAK,gBAAgB,IAAI,iBAAiB;AAC1C,OAAK,iBAAiB,IAAI,iBACtB,CAAC,GAAG,yBAAyB,GAAG,IAAI,eAAe,GACnD;AACJ,OAAK,iBAAiB,IAAI,kBAAkB;;;CAI9C,IAAI,QAA+B;AACjC,SAAO,MAAKC,MAAO,OAAO;;;;;;;;;CAU5B,WAAW,OAAqB;AAC9B,MAAI,KAAK,eAAe,MAAO;AAC/B,MAAI,MAAM,WAAW,EAAG;AACxB,QAAKA,MAAO,KAAK,MAAM;AACvB,MAAI,MAAKA,MAAO,SAAS,KAAK,cAC5B,OAAKA,MAAO,OAAO;;;CAKvB,QAAc;AACZ,QAAKA,QAAS,EAAE;;;;;;;CAQlB,aAAa,SAAwC;AACnD,MAAI,KAAK,eAAe,MACtB,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,EAAE;GAClB,UAAU;GACV,QAAQ;GACT;AAKH,MAAI,CAHmB,MAAKA,MAAO,MAAM,UACvC,KAAK,eAAe,MAAM,QAAQ,IAAI,KAAK,MAAM,CAAC,CACnD,CAEC,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,KAAK;GACrB,UAAU;GACV,QAAQ;GACT;EAEH,IAAIC;AACJ,OAAK,MAAM,OAAO,KAAK,eACrB,KAAI,IAAI,KAAK,QAAQ,EAAE;AACrB,oBAAiB,IAAI;AACrB;;AAIJ,MAAI,EADiB,mBAAmB,QAEtC,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,KAAK;GACrB,UAAU;GACV,QAAQ;GACT;AAEH,UAAQ,KAAK,YAAb;GACE,KAAK,SACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH,KAAK,kBACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH,KAAK,mBACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH;AAC6B,SAAK;AAEhC,WAAO;KACL,cAAc;KACd,UAAU;KACV,gBAAgB,KAAK;KACrB,UAAU;KACV,QAAQ;KACT;;;;;;;;CAUP,MAAM,QAGJ;AACA,SAAO;GAAE,OAAO,KAAK;GAAO;GAAQ"}
1
+ {"version":3,"file":"causality-monitor.js","names":["DEFAULT_DENIAL_PATTERNS: ReadonlyArray<RegExp>","#chain","matchedPattern: string | undefined"],"sources":["../../src/lateral-leak/causality-monitor.ts"],"sourcesContent":["/**\n * `CausalityMonitor` - lateral-leak defense primitive that maintains\n * a per-`RunContext` `causalityChain` of bounded depth and refuses\n * to let an assistant message reference information about a denied\n * earlier action.\n *\n * The monitor is opt-in: agents declare `causalityMonitor:\n * { strictness, denialPatterns?, ... }` on `createAgent({...})`.\n * When the field is absent, the runtime instantiates a no-op monitor\n * that records nothing and never flags.\n *\n * @packageDocumentation\n */\n\nimport type { LateralLeakVector } from '@graphorin/core';\n\n/**\n * Operator-tunable strictness level. Default `'detect-and-flag'`\n * for cloud-tier providers; `'detect'` for loopback providers;\n * `'off'` for v0.1-alpha backward compatibility.\n *\n * @stable\n */\nexport type CausalityMonitorStrictness = 'off' | 'detect' | 'detect-and-flag' | 'detect-and-block';\n\n/**\n * Per-agent configuration accepted by `createAgent({ causalityMonitor })`.\n *\n * @stable\n */\nexport interface CausalityMonitorConfig {\n readonly strictness: CausalityMonitorStrictness;\n /** Maximum depth of the chain. Default `32`. */\n readonly maxChainDepth?: number;\n /** Operator-extensible denial patterns. */\n readonly denialPatterns?: ReadonlyArray<RegExp>;\n /**\n * When `true`, emit the chain on every `checkMessage(...)` call\n * (high-cardinality; opt-in for compliance audits). Default\n * `false` - only emit on detected leaks.\n */\n readonly auditAllChains?: boolean;\n}\n\n/**\n * Default denial-pattern catalogue. The agent runtime extends this\n * list when the operator supplies their own patterns.\n *\n * @stable\n */\nexport const DEFAULT_DENIAL_PATTERNS: ReadonlyArray<RegExp> = [\n /SecretAccessDenied/i,\n /ToolApprovalDenied/i,\n /HandoffPermissionDenied/i,\n /SandboxViolation/i,\n];\n\n/** Default chain depth when not specified. */\nexport const DEFAULT_MAX_CHAIN_DEPTH = 32;\n\n/**\n * Result returned by {@link CausalityMonitor.checkMessage}.\n *\n * @stable\n */\nexport interface CausalityMonitorCheck {\n readonly leakDetected: boolean;\n readonly severity: 'info' | 'warn' | 'block';\n readonly causalityChain: ReadonlyArray<string>;\n readonly matchedPattern?: string;\n readonly decision: 'detect' | 'flag' | 'strip' | 'block';\n readonly vector: LateralLeakVector;\n}\n\n/**\n * In-memory primitive instantiated per `RunContext`. Bounded-depth\n * append discipline keeps the memory footprint trivial even on long\n * runs.\n *\n * @stable\n */\nexport class CausalityMonitor {\n readonly strictness: CausalityMonitorStrictness;\n readonly maxChainDepth: number;\n readonly denialPatterns: ReadonlyArray<RegExp>;\n readonly auditAllChains: boolean;\n #chain: string[] = [];\n\n constructor(cfg: CausalityMonitorConfig) {\n this.strictness = cfg.strictness;\n this.maxChainDepth = cfg.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;\n this.denialPatterns = cfg.denialPatterns\n ? [...DEFAULT_DENIAL_PATTERNS, ...cfg.denialPatterns]\n : DEFAULT_DENIAL_PATTERNS;\n this.auditAllChains = cfg.auditAllChains ?? false;\n }\n\n /** Snapshot the current causality chain. */\n get chain(): ReadonlyArray<string> {\n return this.#chain.slice();\n }\n\n /**\n * Append an entry to the causality chain, dropping the oldest\n * when the chain exceeds `maxChainDepth`. Bounded-length, no PII,\n * no secret values - entries are short opaque strings like\n * `tool:slack-notify`, `tool.error:SecretAccessDenied`,\n * `subagent:research-east`, `compaction:auto-trigger`.\n */\n recordCall(entry: string): void {\n if (this.strictness === 'off') return;\n if (entry.length === 0) return;\n this.#chain.push(entry);\n if (this.#chain.length > this.maxChainDepth) {\n this.#chain.shift();\n }\n }\n\n /** Reset the chain - e.g. on `agent.run` boundary. */\n reset(): void {\n this.#chain = [];\n }\n\n /**\n * Inspect a candidate assistant-visible string and return whether\n * the lateral-leak defense should fire. Pure decision based on\n * the current chain + the operator-extensible denial patterns.\n */\n checkMessage(content: string): CausalityMonitorCheck {\n if (this.strictness === 'off') {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: [],\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n const chainHasDenial = this.#chain.some((entry) =>\n this.denialPatterns.some((pat) => pat.test(entry)),\n );\n if (!chainHasDenial) {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n let matchedPattern: string | undefined;\n for (const pat of this.denialPatterns) {\n if (pat.test(content)) {\n matchedPattern = pat.source;\n break;\n }\n }\n const leakDetected = matchedPattern !== undefined;\n if (!leakDetected) {\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n switch (this.strictness) {\n case 'detect':\n return {\n leakDetected: true,\n severity: 'info',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'detect',\n vector: 'causality-laundering',\n };\n case 'detect-and-flag':\n return {\n leakDetected: true,\n severity: 'warn',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'flag',\n vector: 'causality-laundering',\n };\n case 'detect-and-block':\n return {\n leakDetected: true,\n severity: 'block',\n causalityChain: this.chain,\n ...(matchedPattern !== undefined ? { matchedPattern } : {}),\n decision: 'block',\n vector: 'causality-laundering',\n };\n default: {\n const _exhaustive: never = this.strictness;\n void _exhaustive;\n return {\n leakDetected: false,\n severity: 'info',\n causalityChain: this.chain,\n decision: 'detect',\n vector: 'causality-laundering',\n };\n }\n }\n }\n\n /**\n * Drain the chain to the audit log on `agent.run` completion or\n * `agent.abort`. The runtime supplies the audit emitter - the\n * primitive itself is storage-agnostic.\n */\n flush(reason: 'agent.run.complete' | 'agent.abort'): {\n readonly chain: ReadonlyArray<string>;\n readonly reason: 'agent.run.complete' | 'agent.abort';\n } {\n return { chain: this.chain, reason };\n }\n}\n"],"mappings":";;;;;;;AAkDA,MAAaA,0BAAiD;CAC5D;CACA;CACA;CACA;CACD;;AAGD,MAAa,0BAA0B;;;;;;;;AAuBvC,IAAa,mBAAb,MAA8B;CAC5B,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CACT,SAAmB,EAAE;CAErB,YAAY,KAA6B;AACvC,OAAK,aAAa,IAAI;AACtB,OAAK,gBAAgB,IAAI,iBAAiB;AAC1C,OAAK,iBAAiB,IAAI,iBACtB,CAAC,GAAG,yBAAyB,GAAG,IAAI,eAAe,GACnD;AACJ,OAAK,iBAAiB,IAAI,kBAAkB;;;CAI9C,IAAI,QAA+B;AACjC,SAAO,MAAKC,MAAO,OAAO;;;;;;;;;CAU5B,WAAW,OAAqB;AAC9B,MAAI,KAAK,eAAe,MAAO;AAC/B,MAAI,MAAM,WAAW,EAAG;AACxB,QAAKA,MAAO,KAAK,MAAM;AACvB,MAAI,MAAKA,MAAO,SAAS,KAAK,cAC5B,OAAKA,MAAO,OAAO;;;CAKvB,QAAc;AACZ,QAAKA,QAAS,EAAE;;;;;;;CAQlB,aAAa,SAAwC;AACnD,MAAI,KAAK,eAAe,MACtB,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,EAAE;GAClB,UAAU;GACV,QAAQ;GACT;AAKH,MAAI,CAHmB,MAAKA,MAAO,MAAM,UACvC,KAAK,eAAe,MAAM,QAAQ,IAAI,KAAK,MAAM,CAAC,CACnD,CAEC,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,KAAK;GACrB,UAAU;GACV,QAAQ;GACT;EAEH,IAAIC;AACJ,OAAK,MAAM,OAAO,KAAK,eACrB,KAAI,IAAI,KAAK,QAAQ,EAAE;AACrB,oBAAiB,IAAI;AACrB;;AAIJ,MAAI,EADiB,mBAAmB,QAEtC,QAAO;GACL,cAAc;GACd,UAAU;GACV,gBAAgB,KAAK;GACrB,UAAU;GACV,QAAQ;GACT;AAEH,UAAQ,KAAK,YAAb;GACE,KAAK,SACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH,KAAK,kBACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH,KAAK,mBACH,QAAO;IACL,cAAc;IACd,UAAU;IACV,gBAAgB,KAAK;IACrB,GAAI,mBAAmB,SAAY,EAAE,gBAAgB,GAAG,EAAE;IAC1D,UAAU;IACV,QAAQ;IACT;GACH;AAC6B,SAAK;AAEhC,WAAO;KACL,cAAc;KACd,UAAU;KACV,gBAAgB,KAAK;KACrB,UAAU;KACV,QAAQ;KACT;;;;;;;;CAUP,MAAM,QAGJ;AACA,SAAO;GAAE,OAAO,KAAK;GAAO;GAAQ"}
@@ -1,6 +1,6 @@
1
1
  //#region src/lateral-leak/merge-guard.d.ts
2
2
  /**
3
- * `MergeAgentSidewaysInjectionGuard` layered on top of the
3
+ * `MergeAgentSidewaysInjectionGuard` - layered on top of the
4
4
  * `Agent.fanOut(...)` `'judge-merge'` strategy. Computes a
5
5
  * per-child `sourceTrust` score in `[0.0, 1.0]` and flags merges
6
6
  * where a low-trust child's contribution-weight exceeds the
@@ -69,7 +69,7 @@ interface MergeBiasDecision {
69
69
  readonly decision: 'pass-through' | 'flag' | 'block';
70
70
  }
71
71
  /**
72
- * Evaluate whether the merge is biased a child with
72
+ * Evaluate whether the merge is biased - a child with
73
73
  * `sourceTrust < lowTrustThreshold` contributing more than
74
74
  * `maxLowTrustWeight` of the merged output.
75
75
  *
@@ -27,7 +27,7 @@ function computeSourceTrust(input, overrides = {}) {
27
27
  return Math.max(0, Math.min(1, baseline * provenance * history));
28
28
  }
29
29
  /**
30
- * Evaluate whether the merge is biased a child with
30
+ * Evaluate whether the merge is biased - a child with
31
31
  * `sourceTrust < lowTrustThreshold` contributing more than
32
32
  * `maxLowTrustWeight` of the merged output.
33
33
  *
@@ -1 +1 @@
1
- {"version":3,"file":"merge-guard.js","names":["TRUST_CLASS_BASELINE: Record<TrustClass, number>","ORIGIN_MULTIPLIER: Record<ContentOriginKind, number>","decision: MergeBiasDecision['decision']"],"sources":["../../src/lateral-leak/merge-guard.ts"],"sourcesContent":["/**\n * `MergeAgentSidewaysInjectionGuard` layered on top of the\n * `Agent.fanOut(...)` `'judge-merge'` strategy. Computes a\n * per-child `sourceTrust` score in `[0.0, 1.0]` and flags merges\n * where a low-trust child's contribution-weight exceeds the\n * configured `maxLowTrustWeight` threshold.\n *\n * @packageDocumentation\n */\n\n/**\n * Trust-class baseline used by the guard's `sourceTrust`\n * computation. Mirrors the DEC-149 trust-class taxonomy from\n * `@graphorin/provider`.\n *\n * @stable\n */\nexport type TrustClass = 'loopback' | 'public-tls' | 'public-mtls' | 'untrusted-skill';\n\nconst TRUST_CLASS_BASELINE: Record<TrustClass, number> = {\n loopback: 0.9,\n 'public-tls': 0.7,\n 'public-mtls': 0.8,\n 'untrusted-skill': 0.3,\n};\n\n/**\n * Tool-source provenance multiplier. Mirrors the `ContentOrigin`\n * annotation from `@graphorin/memory.context-engine`.\n *\n * @stable\n */\nexport type ContentOriginKind =\n | 'built-in'\n | 'user-defined'\n | 'trusted-skill'\n | 'untrusted-skill'\n | 'mcp'\n | 'web-search';\n\nconst ORIGIN_MULTIPLIER: Record<ContentOriginKind, number> = {\n 'built-in': 1.0,\n 'user-defined': 0.9,\n 'trusted-skill': 0.85,\n 'untrusted-skill': 0.4,\n mcp: 0.7,\n 'web-search': 0.5,\n};\n\n/**\n * Per-agent guard configuration accepted by\n * `createAgent({ mergeGuard })`.\n *\n * @stable\n */\nexport interface MergeGuardConfig {\n readonly strictness: 'off' | 'detect' | 'detect-and-flag' | 'detect-and-block';\n /** Default `0.3`. */\n readonly maxLowTrustWeight?: number;\n /** Default `0.5`. */\n readonly lowTrustThreshold?: number;\n /** Operator overrides for known agent ids. */\n readonly sourceTrustOverrides?: Readonly<Record<string, number>>;\n}\n\n/**\n * Per-child input descriptor for {@link computeSourceTrust}.\n *\n * @stable\n */\nexport interface ChildTrustInput {\n readonly agentId: string;\n readonly trustClass: TrustClass;\n readonly origin: ContentOriginKind;\n /** Rolling trust score in `[0.0, 1.0]`. Defaults to `1.0`. */\n readonly historyAdjustment?: number;\n}\n\n/**\n * Compose `baseline * provenance * historyAdjustment` and clamp.\n *\n * @stable\n */\nexport function computeSourceTrust(\n input: ChildTrustInput,\n overrides: Readonly<Record<string, number>> = {},\n): number {\n const override = overrides[input.agentId];\n if (override !== undefined) return Math.max(0, Math.min(1, override));\n const baseline = TRUST_CLASS_BASELINE[input.trustClass];\n const provenance = ORIGIN_MULTIPLIER[input.origin];\n const history = input.historyAdjustment ?? 1.0;\n return Math.max(0, Math.min(1, baseline * provenance * history));\n}\n\n/**\n * Pure decision returned by {@link evaluateMerge}.\n *\n * @stable\n */\nexport interface MergeBiasDecision {\n readonly biased: boolean;\n readonly offendingChild?: string;\n readonly contributionWeight?: number;\n readonly sourceTrust?: number;\n readonly decision: 'pass-through' | 'flag' | 'block';\n}\n\n/**\n * Evaluate whether the merge is biased a child with\n * `sourceTrust < lowTrustThreshold` contributing more than\n * `maxLowTrustWeight` of the merged output.\n *\n * Inputs are pre-computed per-child trust scores together with the\n * estimated contribution weights (token-count overlap between each\n * child's output and the merged output, normalized to sum to ~1.0).\n *\n * @stable\n */\nexport function evaluateMerge(\n perChild: ReadonlyArray<{\n readonly agentId: string;\n readonly sourceTrust: number;\n readonly contributionWeight: number;\n }>,\n cfg: MergeGuardConfig,\n): MergeBiasDecision {\n if (cfg.strictness === 'off') {\n return { biased: false, decision: 'pass-through' };\n }\n const lowTrustThreshold = cfg.lowTrustThreshold ?? 0.5;\n const maxLowTrustWeight = cfg.maxLowTrustWeight ?? 0.3;\n for (const child of perChild) {\n if (child.sourceTrust < lowTrustThreshold && child.contributionWeight > maxLowTrustWeight) {\n const decision: MergeBiasDecision['decision'] =\n cfg.strictness === 'detect-and-block'\n ? 'block'\n : cfg.strictness === 'detect-and-flag'\n ? 'flag'\n : 'pass-through';\n return {\n biased: true,\n offendingChild: child.agentId,\n contributionWeight: child.contributionWeight,\n sourceTrust: child.sourceTrust,\n decision,\n };\n }\n }\n return { biased: false, decision: 'pass-through' };\n}\n"],"mappings":";AAmBA,MAAMA,uBAAmD;CACvD,UAAU;CACV,cAAc;CACd,eAAe;CACf,mBAAmB;CACpB;AAgBD,MAAMC,oBAAuD;CAC3D,YAAY;CACZ,gBAAgB;CAChB,iBAAiB;CACjB,mBAAmB;CACnB,KAAK;CACL,cAAc;CACf;;;;;;AAoCD,SAAgB,mBACd,OACA,YAA8C,EAAE,EACxC;CACR,MAAM,WAAW,UAAU,MAAM;AACjC,KAAI,aAAa,OAAW,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;CACrE,MAAM,WAAW,qBAAqB,MAAM;CAC5C,MAAM,aAAa,kBAAkB,MAAM;CAC3C,MAAM,UAAU,MAAM,qBAAqB;AAC3C,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,WAAW,aAAa,QAAQ,CAAC;;;;;;;;;;;;;AA2BlE,SAAgB,cACd,UAKA,KACmB;AACnB,KAAI,IAAI,eAAe,MACrB,QAAO;EAAE,QAAQ;EAAO,UAAU;EAAgB;CAEpD,MAAM,oBAAoB,IAAI,qBAAqB;CACnD,MAAM,oBAAoB,IAAI,qBAAqB;AACnD,MAAK,MAAM,SAAS,SAClB,KAAI,MAAM,cAAc,qBAAqB,MAAM,qBAAqB,mBAAmB;EACzF,MAAMC,WACJ,IAAI,eAAe,qBACf,UACA,IAAI,eAAe,oBACjB,SACA;AACR,SAAO;GACL,QAAQ;GACR,gBAAgB,MAAM;GACtB,oBAAoB,MAAM;GAC1B,aAAa,MAAM;GACnB;GACD;;AAGL,QAAO;EAAE,QAAQ;EAAO,UAAU;EAAgB"}
1
+ {"version":3,"file":"merge-guard.js","names":["TRUST_CLASS_BASELINE: Record<TrustClass, number>","ORIGIN_MULTIPLIER: Record<ContentOriginKind, number>","decision: MergeBiasDecision['decision']"],"sources":["../../src/lateral-leak/merge-guard.ts"],"sourcesContent":["/**\n * `MergeAgentSidewaysInjectionGuard` - layered on top of the\n * `Agent.fanOut(...)` `'judge-merge'` strategy. Computes a\n * per-child `sourceTrust` score in `[0.0, 1.0]` and flags merges\n * where a low-trust child's contribution-weight exceeds the\n * configured `maxLowTrustWeight` threshold.\n *\n * @packageDocumentation\n */\n\n/**\n * Trust-class baseline used by the guard's `sourceTrust`\n * computation. Mirrors the DEC-149 trust-class taxonomy from\n * `@graphorin/provider`.\n *\n * @stable\n */\nexport type TrustClass = 'loopback' | 'public-tls' | 'public-mtls' | 'untrusted-skill';\n\nconst TRUST_CLASS_BASELINE: Record<TrustClass, number> = {\n loopback: 0.9,\n 'public-tls': 0.7,\n 'public-mtls': 0.8,\n 'untrusted-skill': 0.3,\n};\n\n/**\n * Tool-source provenance multiplier. Mirrors the `ContentOrigin`\n * annotation from `@graphorin/memory.context-engine`.\n *\n * @stable\n */\nexport type ContentOriginKind =\n | 'built-in'\n | 'user-defined'\n | 'trusted-skill'\n | 'untrusted-skill'\n | 'mcp'\n | 'web-search';\n\nconst ORIGIN_MULTIPLIER: Record<ContentOriginKind, number> = {\n 'built-in': 1.0,\n 'user-defined': 0.9,\n 'trusted-skill': 0.85,\n 'untrusted-skill': 0.4,\n mcp: 0.7,\n 'web-search': 0.5,\n};\n\n/**\n * Per-agent guard configuration accepted by\n * `createAgent({ mergeGuard })`.\n *\n * @stable\n */\nexport interface MergeGuardConfig {\n readonly strictness: 'off' | 'detect' | 'detect-and-flag' | 'detect-and-block';\n /** Default `0.3`. */\n readonly maxLowTrustWeight?: number;\n /** Default `0.5`. */\n readonly lowTrustThreshold?: number;\n /** Operator overrides for known agent ids. */\n readonly sourceTrustOverrides?: Readonly<Record<string, number>>;\n}\n\n/**\n * Per-child input descriptor for {@link computeSourceTrust}.\n *\n * @stable\n */\nexport interface ChildTrustInput {\n readonly agentId: string;\n readonly trustClass: TrustClass;\n readonly origin: ContentOriginKind;\n /** Rolling trust score in `[0.0, 1.0]`. Defaults to `1.0`. */\n readonly historyAdjustment?: number;\n}\n\n/**\n * Compose `baseline * provenance * historyAdjustment` and clamp.\n *\n * @stable\n */\nexport function computeSourceTrust(\n input: ChildTrustInput,\n overrides: Readonly<Record<string, number>> = {},\n): number {\n const override = overrides[input.agentId];\n if (override !== undefined) return Math.max(0, Math.min(1, override));\n const baseline = TRUST_CLASS_BASELINE[input.trustClass];\n const provenance = ORIGIN_MULTIPLIER[input.origin];\n const history = input.historyAdjustment ?? 1.0;\n return Math.max(0, Math.min(1, baseline * provenance * history));\n}\n\n/**\n * Pure decision returned by {@link evaluateMerge}.\n *\n * @stable\n */\nexport interface MergeBiasDecision {\n readonly biased: boolean;\n readonly offendingChild?: string;\n readonly contributionWeight?: number;\n readonly sourceTrust?: number;\n readonly decision: 'pass-through' | 'flag' | 'block';\n}\n\n/**\n * Evaluate whether the merge is biased - a child with\n * `sourceTrust < lowTrustThreshold` contributing more than\n * `maxLowTrustWeight` of the merged output.\n *\n * Inputs are pre-computed per-child trust scores together with the\n * estimated contribution weights (token-count overlap between each\n * child's output and the merged output, normalized to sum to ~1.0).\n *\n * @stable\n */\nexport function evaluateMerge(\n perChild: ReadonlyArray<{\n readonly agentId: string;\n readonly sourceTrust: number;\n readonly contributionWeight: number;\n }>,\n cfg: MergeGuardConfig,\n): MergeBiasDecision {\n if (cfg.strictness === 'off') {\n return { biased: false, decision: 'pass-through' };\n }\n const lowTrustThreshold = cfg.lowTrustThreshold ?? 0.5;\n const maxLowTrustWeight = cfg.maxLowTrustWeight ?? 0.3;\n for (const child of perChild) {\n if (child.sourceTrust < lowTrustThreshold && child.contributionWeight > maxLowTrustWeight) {\n const decision: MergeBiasDecision['decision'] =\n cfg.strictness === 'detect-and-block'\n ? 'block'\n : cfg.strictness === 'detect-and-flag'\n ? 'flag'\n : 'pass-through';\n return {\n biased: true,\n offendingChild: child.agentId,\n contributionWeight: child.contributionWeight,\n sourceTrust: child.sourceTrust,\n decision,\n };\n }\n }\n return { biased: false, decision: 'pass-through' };\n}\n"],"mappings":";AAmBA,MAAMA,uBAAmD;CACvD,UAAU;CACV,cAAc;CACd,eAAe;CACf,mBAAmB;CACpB;AAgBD,MAAMC,oBAAuD;CAC3D,YAAY;CACZ,gBAAgB;CAChB,iBAAiB;CACjB,mBAAmB;CACnB,KAAK;CACL,cAAc;CACf;;;;;;AAoCD,SAAgB,mBACd,OACA,YAA8C,EAAE,EACxC;CACR,MAAM,WAAW,UAAU,MAAM;AACjC,KAAI,aAAa,OAAW,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;CACrE,MAAM,WAAW,qBAAqB,MAAM;CAC5C,MAAM,aAAa,kBAAkB,MAAM;CAC3C,MAAM,UAAU,MAAM,qBAAqB;AAC3C,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,WAAW,aAAa,QAAQ,CAAC;;;;;;;;;;;;;AA2BlE,SAAgB,cACd,UAKA,KACmB;AACnB,KAAI,IAAI,eAAe,MACrB,QAAO;EAAE,QAAQ;EAAO,UAAU;EAAgB;CAEpD,MAAM,oBAAoB,IAAI,qBAAqB;CACnD,MAAM,oBAAoB,IAAI,qBAAqB;AACnD,MAAK,MAAM,SAAS,SAClB,KAAI,MAAM,cAAc,qBAAqB,MAAM,qBAAqB,mBAAmB;EACzF,MAAMC,WACJ,IAAI,eAAe,qBACf,UACA,IAAI,eAAe,oBACjB,SACA;AACR,SAAO;GACL,QAAQ;GACR,gBAAgB,MAAM;GACtB,oBAAoB,MAAM;GAC1B,aAAa,MAAM;GACnB;GACD;;AAGL,QAAO;EAAE,QAAQ;EAAO,UAAU;EAAgB"}
@@ -1,6 +1,6 @@
1
1
  //#region src/lateral-leak/protocol-guard.d.ts
2
2
  /**
3
- * Protocol/header injection guard escapes control characters in
3
+ * Protocol/header injection guard - escapes control characters in
4
4
  * tool result bodies before they cross internal-service delivery
5
5
  * boundaries (SSE, WebSocket, REST body, HTTP header, audit row).
6
6
  *
@@ -13,7 +13,7 @@
13
13
  * - `'ws' | 'rest-body'` → `'replace'` (replace with the Unicode
14
14
  * replacement character `\uFFFD`).
15
15
  * - `'audit'` → `'strict'` (regulated deployments).
16
- * - `'reject'` is operator opt-in the guard throws
16
+ * - `'reject'` is operator opt-in - the guard throws
17
17
  * {@link ProtocolInjectionRejectError} when control characters are
18
18
  * detected.
19
19
  *
@@ -46,7 +46,7 @@ interface ProtocolGuardConfig {
46
46
  readonly audit?: ProtocolEscapePolicy;
47
47
  }
48
48
  /**
49
- * Resolved policy lookup. Pure function no side effects.
49
+ * Resolved policy lookup. Pure function - no side effects.
50
50
  *
51
51
  * @stable
52
52
  */
@@ -66,7 +66,7 @@ interface GuardOutcome {
66
66
  }
67
67
  /**
68
68
  * Apply the configured escape policy to a single string body. Pure
69
- * never mutates inputs.
69
+ * - never mutates inputs.
70
70
  *
71
71
  * @stable
72
72
  */
@@ -2,7 +2,7 @@ import { ProtocolInjectionRejectError } from "../errors/index.js";
2
2
 
3
3
  //#region src/lateral-leak/protocol-guard.ts
4
4
  /**
5
- * Protocol/header injection guard escapes control characters in
5
+ * Protocol/header injection guard - escapes control characters in
6
6
  * tool result bodies before they cross internal-service delivery
7
7
  * boundaries (SSE, WebSocket, REST body, HTTP header, audit row).
8
8
  *
@@ -15,14 +15,14 @@ import { ProtocolInjectionRejectError } from "../errors/index.js";
15
15
  * - `'ws' | 'rest-body'` → `'replace'` (replace with the Unicode
16
16
  * replacement character `\uFFFD`).
17
17
  * - `'audit'` → `'strict'` (regulated deployments).
18
- * - `'reject'` is operator opt-in the guard throws
18
+ * - `'reject'` is operator opt-in - the guard throws
19
19
  * {@link ProtocolInjectionRejectError} when control characters are
20
20
  * detected.
21
21
  *
22
22
  * @packageDocumentation
23
23
  */
24
24
  /**
25
- * Resolved policy lookup. Pure function no side effects.
25
+ * Resolved policy lookup. Pure function - no side effects.
26
26
  *
27
27
  * @stable
28
28
  */
@@ -94,7 +94,7 @@ function findMatchedPattern(input) {
94
94
  }
95
95
  /**
96
96
  * Apply the configured escape policy to a single string body. Pure
97
- * never mutates inputs.
97
+ * - never mutates inputs.
98
98
  *
99
99
  * @stable
100
100
  */
@@ -1 +1 @@
1
- {"version":3,"file":"protocol-guard.js","names":["codePoints: number[]"],"sources":["../../src/lateral-leak/protocol-guard.ts"],"sourcesContent":["/**\n * Protocol/header injection guard escapes control characters in\n * tool result bodies before they cross internal-service delivery\n * boundaries (SSE, WebSocket, REST body, HTTP header, audit row).\n *\n * The default policy mirrors the deployment-posture matrix from the\n * lateral-leak design (DEC-171 / suggested ADR-059):\n *\n * - `'sse' | 'http-header'` → `'strict'` (escape control chars to\n * `\\xNN` hex literals; the safest default for cleartext frame\n * protocols).\n * - `'ws' | 'rest-body'` → `'replace'` (replace with the Unicode\n * replacement character `\\uFFFD`).\n * - `'audit'` → `'strict'` (regulated deployments).\n * - `'reject'` is operator opt-in the guard throws\n * {@link ProtocolInjectionRejectError} when control characters are\n * detected.\n *\n * @packageDocumentation\n */\n\nimport { ProtocolInjectionRejectError } from '../errors/index.js';\n\n/**\n * Per-boundary identifier used by the runtime when calling the\n * guard.\n *\n * @stable\n */\nexport type ProtocolBoundary = 'sse' | 'http-header' | 'ws' | 'rest-body' | 'audit';\n\n/**\n * Per-boundary escape policy.\n *\n * @stable\n */\nexport type ProtocolEscapePolicy = 'strict' | 'replace' | 'reject';\n\n/**\n * Configurable per-boundary policy table. Operators may override\n * specific boundaries via `Agent.protocolGuard?: { ... }`.\n *\n * @stable\n */\nexport interface ProtocolGuardConfig {\n readonly sse?: ProtocolEscapePolicy;\n readonly httpHeader?: ProtocolEscapePolicy;\n readonly ws?: ProtocolEscapePolicy;\n readonly restBody?: ProtocolEscapePolicy;\n readonly audit?: ProtocolEscapePolicy;\n}\n\n/**\n * Resolved policy lookup. Pure function no side effects.\n *\n * @stable\n */\nexport function resolvePolicy(\n boundary: ProtocolBoundary,\n cfg: ProtocolGuardConfig = {},\n): ProtocolEscapePolicy {\n switch (boundary) {\n case 'sse':\n return cfg.sse ?? 'strict';\n case 'http-header':\n return cfg.httpHeader ?? 'strict';\n case 'ws':\n return cfg.ws ?? 'replace';\n case 'rest-body':\n return cfg.restBody ?? 'replace';\n case 'audit':\n return cfg.audit ?? 'strict';\n default: {\n const _exhaustive: never = boundary;\n void _exhaustive;\n return 'strict';\n }\n }\n}\n\n// Control-character catalogue used to detect injection attempts at\n// the framework's outbound delivery boundaries. Building the regex\n// programmatically avoids embedding control bytes in the source —\n// the bytes are exactly the ones a malicious tool result might\n// inject to escape framing on cleartext protocols.\nfunction buildControlCharsRegex(): RegExp {\n const codePoints: number[] = [];\n for (let n = 0; n < 0x20; n++) {\n if (n === 0x09 || n === 0x0a || n === 0x0d) continue;\n codePoints.push(n);\n }\n codePoints.push(0x7f);\n const cls = codePoints.map((c) => `\\\\x${c.toString(16).padStart(2, '0')}`).join('');\n return new RegExp(`[${cls}]`, 'g');\n}\nconst CONTROL_CHARS_RE = buildControlCharsRegex();\nconst SSE_FRAME_RE = /\\r?\\n\\r?\\n/g;\nconst HEADER_CRLF_RE = /\\r\\n/g;\n\n/**\n * Outcome of {@link guardOutboundContent}.\n *\n * @stable\n */\nexport interface GuardOutcome {\n readonly content: string;\n readonly escapedCharCount: number;\n readonly matchedPattern?: string;\n readonly decision: 'pass-through' | 'escaped' | 'replaced' | 'rejected';\n readonly boundary: ProtocolBoundary;\n readonly policy: ProtocolEscapePolicy;\n}\n\nfunction escapeStrict(\n input: string,\n boundary: ProtocolBoundary,\n): { content: string; count: number } {\n let count = 0;\n let out = input.replace(CONTROL_CHARS_RE, (ch) => {\n count += 1;\n return `\\\\x${ch.charCodeAt(0).toString(16).padStart(2, '0')}`;\n });\n if (boundary === 'sse') {\n out = out.replace(SSE_FRAME_RE, (m) => {\n count += 1;\n return m.replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r');\n });\n }\n if (boundary === 'http-header' || boundary === 'audit') {\n out = out.replace(HEADER_CRLF_RE, () => {\n count += 1;\n return '\\\\r\\\\n';\n });\n }\n return { content: out, count };\n}\n\nfunction replaceUFFFD(input: string): { content: string; count: number } {\n let count = 0;\n const out = input.replace(CONTROL_CHARS_RE, () => {\n count += 1;\n return '\\uFFFD';\n });\n return { content: out, count };\n}\n\nfunction findMatchedPattern(input: string): string | undefined {\n if (CONTROL_CHARS_RE.test(input)) {\n CONTROL_CHARS_RE.lastIndex = 0;\n return 'control-char';\n }\n if (SSE_FRAME_RE.test(input)) {\n SSE_FRAME_RE.lastIndex = 0;\n return 'sse-frame';\n }\n if (HEADER_CRLF_RE.test(input)) {\n HEADER_CRLF_RE.lastIndex = 0;\n return 'header-crlf';\n }\n return undefined;\n}\n\n/**\n * Apply the configured escape policy to a single string body. Pure\n * never mutates inputs.\n *\n * @stable\n */\nexport function guardOutboundContent(\n input: string,\n boundary: ProtocolBoundary,\n cfg: ProtocolGuardConfig = {},\n): GuardOutcome {\n const policy = resolvePolicy(boundary, cfg);\n const matchedPattern = findMatchedPattern(input);\n if (matchedPattern === undefined) {\n return {\n content: input,\n escapedCharCount: 0,\n decision: 'pass-through',\n boundary,\n policy,\n };\n }\n switch (policy) {\n case 'strict': {\n const r = escapeStrict(input, boundary);\n return {\n content: r.content,\n escapedCharCount: r.count,\n matchedPattern,\n decision: 'escaped',\n boundary,\n policy,\n };\n }\n case 'replace': {\n const r = replaceUFFFD(input);\n return {\n content: r.content,\n escapedCharCount: r.count,\n matchedPattern,\n decision: 'replaced',\n boundary,\n policy,\n };\n }\n case 'reject':\n throw new ProtocolInjectionRejectError(boundary, matchedPattern);\n default: {\n const _exhaustive: never = policy;\n void _exhaustive;\n return {\n content: input,\n escapedCharCount: 0,\n decision: 'pass-through',\n boundary,\n policy,\n };\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,cACd,UACA,MAA2B,EAAE,EACP;AACtB,SAAQ,UAAR;EACE,KAAK,MACH,QAAO,IAAI,OAAO;EACpB,KAAK,cACH,QAAO,IAAI,cAAc;EAC3B,KAAK,KACH,QAAO,IAAI,MAAM;EACnB,KAAK,YACH,QAAO,IAAI,YAAY;EACzB,KAAK,QACH,QAAO,IAAI,SAAS;EACtB,QAGE,QAAO;;;AAUb,SAAS,yBAAiC;CACxC,MAAMA,aAAuB,EAAE;AAC/B,MAAK,IAAI,IAAI,GAAG,IAAI,IAAM,KAAK;AAC7B,MAAI,MAAM,KAAQ,MAAM,MAAQ,MAAM,GAAM;AAC5C,aAAW,KAAK,EAAE;;AAEpB,YAAW,KAAK,IAAK;CACrB,MAAM,MAAM,WAAW,KAAK,MAAM,MAAM,EAAE,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG;AACnF,QAAO,IAAI,OAAO,IAAI,IAAI,IAAI,IAAI;;AAEpC,MAAM,mBAAmB,wBAAwB;AACjD,MAAM,eAAe;AACrB,MAAM,iBAAiB;AAgBvB,SAAS,aACP,OACA,UACoC;CACpC,IAAI,QAAQ;CACZ,IAAI,MAAM,MAAM,QAAQ,mBAAmB,OAAO;AAChD,WAAS;AACT,SAAO,MAAM,GAAG,WAAW,EAAE,CAAC,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI;GAC3D;AACF,KAAI,aAAa,MACf,OAAM,IAAI,QAAQ,eAAe,MAAM;AACrC,WAAS;AACT,SAAO,EAAE,QAAQ,OAAO,MAAM,CAAC,QAAQ,OAAO,MAAM;GACpD;AAEJ,KAAI,aAAa,iBAAiB,aAAa,QAC7C,OAAM,IAAI,QAAQ,sBAAsB;AACtC,WAAS;AACT,SAAO;GACP;AAEJ,QAAO;EAAE,SAAS;EAAK;EAAO;;AAGhC,SAAS,aAAa,OAAmD;CACvE,IAAI,QAAQ;AAKZ,QAAO;EAAE,SAJG,MAAM,QAAQ,wBAAwB;AAChD,YAAS;AACT,UAAO;IACP;EACqB;EAAO;;AAGhC,SAAS,mBAAmB,OAAmC;AAC7D,KAAI,iBAAiB,KAAK,MAAM,EAAE;AAChC,mBAAiB,YAAY;AAC7B,SAAO;;AAET,KAAI,aAAa,KAAK,MAAM,EAAE;AAC5B,eAAa,YAAY;AACzB,SAAO;;AAET,KAAI,eAAe,KAAK,MAAM,EAAE;AAC9B,iBAAe,YAAY;AAC3B,SAAO;;;;;;;;;AAWX,SAAgB,qBACd,OACA,UACA,MAA2B,EAAE,EACf;CACd,MAAM,SAAS,cAAc,UAAU,IAAI;CAC3C,MAAM,iBAAiB,mBAAmB,MAAM;AAChD,KAAI,mBAAmB,OACrB,QAAO;EACL,SAAS;EACT,kBAAkB;EAClB,UAAU;EACV;EACA;EACD;AAEH,SAAQ,QAAR;EACE,KAAK,UAAU;GACb,MAAM,IAAI,aAAa,OAAO,SAAS;AACvC,UAAO;IACL,SAAS,EAAE;IACX,kBAAkB,EAAE;IACpB;IACA,UAAU;IACV;IACA;IACD;;EAEH,KAAK,WAAW;GACd,MAAM,IAAI,aAAa,MAAM;AAC7B,UAAO;IACL,SAAS,EAAE;IACX,kBAAkB,EAAE;IACpB;IACA,UAAU;IACV;IACA;IACD;;EAEH,KAAK,SACH,OAAM,IAAI,6BAA6B,UAAU,eAAe;EAClE,QAGE,QAAO;GACL,SAAS;GACT,kBAAkB;GAClB,UAAU;GACV;GACA;GACD"}
1
+ {"version":3,"file":"protocol-guard.js","names":["codePoints: number[]"],"sources":["../../src/lateral-leak/protocol-guard.ts"],"sourcesContent":["/**\n * Protocol/header injection guard - escapes control characters in\n * tool result bodies before they cross internal-service delivery\n * boundaries (SSE, WebSocket, REST body, HTTP header, audit row).\n *\n * The default policy mirrors the deployment-posture matrix from the\n * lateral-leak design (DEC-171 / suggested ADR-059):\n *\n * - `'sse' | 'http-header'` → `'strict'` (escape control chars to\n * `\\xNN` hex literals; the safest default for cleartext frame\n * protocols).\n * - `'ws' | 'rest-body'` → `'replace'` (replace with the Unicode\n * replacement character `\\uFFFD`).\n * - `'audit'` → `'strict'` (regulated deployments).\n * - `'reject'` is operator opt-in - the guard throws\n * {@link ProtocolInjectionRejectError} when control characters are\n * detected.\n *\n * @packageDocumentation\n */\n\nimport { ProtocolInjectionRejectError } from '../errors/index.js';\n\n/**\n * Per-boundary identifier used by the runtime when calling the\n * guard.\n *\n * @stable\n */\nexport type ProtocolBoundary = 'sse' | 'http-header' | 'ws' | 'rest-body' | 'audit';\n\n/**\n * Per-boundary escape policy.\n *\n * @stable\n */\nexport type ProtocolEscapePolicy = 'strict' | 'replace' | 'reject';\n\n/**\n * Configurable per-boundary policy table. Operators may override\n * specific boundaries via `Agent.protocolGuard?: { ... }`.\n *\n * @stable\n */\nexport interface ProtocolGuardConfig {\n readonly sse?: ProtocolEscapePolicy;\n readonly httpHeader?: ProtocolEscapePolicy;\n readonly ws?: ProtocolEscapePolicy;\n readonly restBody?: ProtocolEscapePolicy;\n readonly audit?: ProtocolEscapePolicy;\n}\n\n/**\n * Resolved policy lookup. Pure function - no side effects.\n *\n * @stable\n */\nexport function resolvePolicy(\n boundary: ProtocolBoundary,\n cfg: ProtocolGuardConfig = {},\n): ProtocolEscapePolicy {\n switch (boundary) {\n case 'sse':\n return cfg.sse ?? 'strict';\n case 'http-header':\n return cfg.httpHeader ?? 'strict';\n case 'ws':\n return cfg.ws ?? 'replace';\n case 'rest-body':\n return cfg.restBody ?? 'replace';\n case 'audit':\n return cfg.audit ?? 'strict';\n default: {\n const _exhaustive: never = boundary;\n void _exhaustive;\n return 'strict';\n }\n }\n}\n\n// Control-character catalogue used to detect injection attempts at\n// the framework's outbound delivery boundaries. Building the regex\n// programmatically avoids embedding control bytes in the source -\n// the bytes are exactly the ones a malicious tool result might\n// inject to escape framing on cleartext protocols.\nfunction buildControlCharsRegex(): RegExp {\n const codePoints: number[] = [];\n for (let n = 0; n < 0x20; n++) {\n if (n === 0x09 || n === 0x0a || n === 0x0d) continue;\n codePoints.push(n);\n }\n codePoints.push(0x7f);\n const cls = codePoints.map((c) => `\\\\x${c.toString(16).padStart(2, '0')}`).join('');\n return new RegExp(`[${cls}]`, 'g');\n}\nconst CONTROL_CHARS_RE = buildControlCharsRegex();\nconst SSE_FRAME_RE = /\\r?\\n\\r?\\n/g;\nconst HEADER_CRLF_RE = /\\r\\n/g;\n\n/**\n * Outcome of {@link guardOutboundContent}.\n *\n * @stable\n */\nexport interface GuardOutcome {\n readonly content: string;\n readonly escapedCharCount: number;\n readonly matchedPattern?: string;\n readonly decision: 'pass-through' | 'escaped' | 'replaced' | 'rejected';\n readonly boundary: ProtocolBoundary;\n readonly policy: ProtocolEscapePolicy;\n}\n\nfunction escapeStrict(\n input: string,\n boundary: ProtocolBoundary,\n): { content: string; count: number } {\n let count = 0;\n let out = input.replace(CONTROL_CHARS_RE, (ch) => {\n count += 1;\n return `\\\\x${ch.charCodeAt(0).toString(16).padStart(2, '0')}`;\n });\n if (boundary === 'sse') {\n out = out.replace(SSE_FRAME_RE, (m) => {\n count += 1;\n return m.replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r');\n });\n }\n if (boundary === 'http-header' || boundary === 'audit') {\n out = out.replace(HEADER_CRLF_RE, () => {\n count += 1;\n return '\\\\r\\\\n';\n });\n }\n return { content: out, count };\n}\n\nfunction replaceUFFFD(input: string): { content: string; count: number } {\n let count = 0;\n const out = input.replace(CONTROL_CHARS_RE, () => {\n count += 1;\n return '\\uFFFD';\n });\n return { content: out, count };\n}\n\nfunction findMatchedPattern(input: string): string | undefined {\n if (CONTROL_CHARS_RE.test(input)) {\n CONTROL_CHARS_RE.lastIndex = 0;\n return 'control-char';\n }\n if (SSE_FRAME_RE.test(input)) {\n SSE_FRAME_RE.lastIndex = 0;\n return 'sse-frame';\n }\n if (HEADER_CRLF_RE.test(input)) {\n HEADER_CRLF_RE.lastIndex = 0;\n return 'header-crlf';\n }\n return undefined;\n}\n\n/**\n * Apply the configured escape policy to a single string body. Pure\n * - never mutates inputs.\n *\n * @stable\n */\nexport function guardOutboundContent(\n input: string,\n boundary: ProtocolBoundary,\n cfg: ProtocolGuardConfig = {},\n): GuardOutcome {\n const policy = resolvePolicy(boundary, cfg);\n const matchedPattern = findMatchedPattern(input);\n if (matchedPattern === undefined) {\n return {\n content: input,\n escapedCharCount: 0,\n decision: 'pass-through',\n boundary,\n policy,\n };\n }\n switch (policy) {\n case 'strict': {\n const r = escapeStrict(input, boundary);\n return {\n content: r.content,\n escapedCharCount: r.count,\n matchedPattern,\n decision: 'escaped',\n boundary,\n policy,\n };\n }\n case 'replace': {\n const r = replaceUFFFD(input);\n return {\n content: r.content,\n escapedCharCount: r.count,\n matchedPattern,\n decision: 'replaced',\n boundary,\n policy,\n };\n }\n case 'reject':\n throw new ProtocolInjectionRejectError(boundary, matchedPattern);\n default: {\n const _exhaustive: never = policy;\n void _exhaustive;\n return {\n content: input,\n escapedCharCount: 0,\n decision: 'pass-through',\n boundary,\n policy,\n };\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,cACd,UACA,MAA2B,EAAE,EACP;AACtB,SAAQ,UAAR;EACE,KAAK,MACH,QAAO,IAAI,OAAO;EACpB,KAAK,cACH,QAAO,IAAI,cAAc;EAC3B,KAAK,KACH,QAAO,IAAI,MAAM;EACnB,KAAK,YACH,QAAO,IAAI,YAAY;EACzB,KAAK,QACH,QAAO,IAAI,SAAS;EACtB,QAGE,QAAO;;;AAUb,SAAS,yBAAiC;CACxC,MAAMA,aAAuB,EAAE;AAC/B,MAAK,IAAI,IAAI,GAAG,IAAI,IAAM,KAAK;AAC7B,MAAI,MAAM,KAAQ,MAAM,MAAQ,MAAM,GAAM;AAC5C,aAAW,KAAK,EAAE;;AAEpB,YAAW,KAAK,IAAK;CACrB,MAAM,MAAM,WAAW,KAAK,MAAM,MAAM,EAAE,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG;AACnF,QAAO,IAAI,OAAO,IAAI,IAAI,IAAI,IAAI;;AAEpC,MAAM,mBAAmB,wBAAwB;AACjD,MAAM,eAAe;AACrB,MAAM,iBAAiB;AAgBvB,SAAS,aACP,OACA,UACoC;CACpC,IAAI,QAAQ;CACZ,IAAI,MAAM,MAAM,QAAQ,mBAAmB,OAAO;AAChD,WAAS;AACT,SAAO,MAAM,GAAG,WAAW,EAAE,CAAC,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI;GAC3D;AACF,KAAI,aAAa,MACf,OAAM,IAAI,QAAQ,eAAe,MAAM;AACrC,WAAS;AACT,SAAO,EAAE,QAAQ,OAAO,MAAM,CAAC,QAAQ,OAAO,MAAM;GACpD;AAEJ,KAAI,aAAa,iBAAiB,aAAa,QAC7C,OAAM,IAAI,QAAQ,sBAAsB;AACtC,WAAS;AACT,SAAO;GACP;AAEJ,QAAO;EAAE,SAAS;EAAK;EAAO;;AAGhC,SAAS,aAAa,OAAmD;CACvE,IAAI,QAAQ;AAKZ,QAAO;EAAE,SAJG,MAAM,QAAQ,wBAAwB;AAChD,YAAS;AACT,UAAO;IACP;EACqB;EAAO;;AAGhC,SAAS,mBAAmB,OAAmC;AAC7D,KAAI,iBAAiB,KAAK,MAAM,EAAE;AAChC,mBAAiB,YAAY;AAC7B,SAAO;;AAET,KAAI,aAAa,KAAK,MAAM,EAAE;AAC5B,eAAa,YAAY;AACzB,SAAO;;AAET,KAAI,eAAe,KAAK,MAAM,EAAE;AAC9B,iBAAe,YAAY;AAC3B,SAAO;;;;;;;;;AAWX,SAAgB,qBACd,OACA,UACA,MAA2B,EAAE,EACf;CACd,MAAM,SAAS,cAAc,UAAU,IAAI;CAC3C,MAAM,iBAAiB,mBAAmB,MAAM;AAChD,KAAI,mBAAmB,OACrB,QAAO;EACL,SAAS;EACT,kBAAkB;EAClB,UAAU;EACV;EACA;EACD;AAEH,SAAQ,QAAR;EACE,KAAK,UAAU;GACb,MAAM,IAAI,aAAa,OAAO,SAAS;AACvC,UAAO;IACL,SAAS,EAAE;IACX,kBAAkB,EAAE;IACpB;IACA,UAAU;IACV;IACA;IACD;;EAEH,KAAK,WAAW;GACd,MAAM,IAAI,aAAa,MAAM;AAC7B,UAAO;IACL,SAAS,EAAE;IACX,kBAAkB,EAAE;IACpB;IACA,UAAU;IACV;IACA;IACD;;EAEH,KAAK,SACH,OAAM,IAAI,6BAA6B,UAAU,eAAe;EAClE,QAGE,QAAO;GACL,SAAS;GACT,kBAAkB;GAClB,UAAU;GACV;GACA;GACD"}
@@ -29,7 +29,7 @@ interface ResolvePreferredModelInput {
29
29
  /**
30
30
  * Pick the highest-cost tier across the supplied per-tool hints.
31
31
  * Explicit `ModelSpec` entries are treated as the highest tier
32
- * (`'smart'`) for tie-breaking the conservative-correctness rule
32
+ * (`'smart'`) for tie-breaking - the conservative-correctness rule
33
33
  * documented in DEC-169 / suggested ADR-057.
34
34
  *
35
35
  * Returns the picked hint together with the original `ModelSpec`
@@ -43,7 +43,7 @@ declare function pickTopTierAcrossTools(hints: ReadonlyArray<ModelHint | ModelSp
43
43
  } | undefined;
44
44
  /**
45
45
  * Walk the precedence ladder and return the resolved provider for a
46
- * single agent step. Pure function no side effects.
46
+ * single agent step. Pure function - no side effects.
47
47
  *
48
48
  * @stable
49
49
  */
@@ -18,7 +18,7 @@ function modelIdFromSpec(spec) {
18
18
  /**
19
19
  * Pick the highest-cost tier across the supplied per-tool hints.
20
20
  * Explicit `ModelSpec` entries are treated as the highest tier
21
- * (`'smart'`) for tie-breaking the conservative-correctness rule
21
+ * (`'smart'`) for tie-breaking - the conservative-correctness rule
22
22
  * documented in DEC-169 / suggested ADR-057.
23
23
  *
24
24
  * Returns the picked hint together with the original `ModelSpec`
@@ -57,7 +57,7 @@ function pickTopTierAcrossTools(hints) {
57
57
  }
58
58
  /**
59
59
  * Walk the precedence ladder and return the resolved provider for a
60
- * single agent step. Pure function no side effects.
60
+ * single agent step. Pure function - no side effects.
61
61
  *
62
62
  * @stable
63
63
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["TIER_ORDER: Record<ModelHint, number>","bestSpec: ModelSpec | undefined","bestHint: ModelHint | undefined","agent"],"sources":["../../src/preferred-model/index.ts"],"sourcesContent":["/**\n * Per-tool / per-agent preferred-model resolution. Pure functions\n * consulted by the agent loop AFTER the model has decided which\n * tool(s) to call but BEFORE `provider.stream(...)` is invoked.\n *\n * The four-step precedence ladder (highest wins):\n *\n * 1. `prepareStep({ provider })` the operator's explicit per-step\n * override always wins.\n * 2. `Tool.preferredModel` the tool author's per-tool hint.\n * Only the tools the model actually CALLED on the previous step\n * are consulted (AG-15) an advertised-but-uncalled hint never\n * escalates the run. Multi-tool ties resolve to the highest cost\n * tier (`'smart' > 'balanced' > 'fast'`; explicit `ModelSpec` is\n * treated as the highest tier).\n * 3. `Agent.preferredModel?` the per-agent default.\n * 4. `Agent` default `provider` the v0.1-alpha behaviour.\n *\n * Cost-tier resolution against `Agent.modelTierMap` is documented\n * as a hint: when the requested tier is unmapped, the resolver\n * falls through to the next precedence step rather than throwing.\n *\n * @packageDocumentation\n */\n\nimport type { ModelHint, ModelSpec, Provider } from '@graphorin/core';\n\nconst TIER_ORDER: Record<ModelHint, number> = { fast: 0, balanced: 1, smart: 2 };\n\n/**\n * Result returned by {@link resolvePreferredModel}.\n *\n * @stable\n */\nexport interface PreferredModelResolution {\n readonly resolvedProvider: Provider;\n readonly resolvedModelId: string;\n readonly source: 'prepare-step' | 'tier-map' | 'spec' | 'agent-preferred' | 'fallthrough-default';\n readonly hintApplied?: ModelHint;\n readonly fallthroughReason?:\n | 'tier-not-mapped'\n | 'provider-unavailable'\n | 'override-takes-precedence';\n}\n\n/**\n * Pure inputs to {@link resolvePreferredModel}.\n *\n * @stable\n */\nexport interface ResolvePreferredModelInput {\n readonly prepareStepProvider?: Provider;\n readonly toolPreferredModels: ReadonlyArray<ModelHint | ModelSpec | undefined>;\n readonly agentPreferredModel?: ModelHint | ModelSpec;\n readonly agentDefaultProvider: Provider;\n readonly modelTierMap?: Partial<Record<ModelHint, ModelSpec>>;\n}\n\nfunction isModelHint(value: ModelHint | ModelSpec | undefined): value is ModelHint {\n return value === 'fast' || value === 'balanced' || value === 'smart';\n}\n\nfunction specToProvider(spec: ModelSpec): Provider {\n if ('provider' in spec) return spec.provider as Provider;\n return spec as Provider;\n}\n\nfunction modelIdFromSpec(spec: ModelSpec): string {\n if ('provider' in spec) return spec.model;\n return (spec as Provider).modelId;\n}\n\n/**\n * Pick the highest-cost tier across the supplied per-tool hints.\n * Explicit `ModelSpec` entries are treated as the highest tier\n * (`'smart'`) for tie-breaking the conservative-correctness rule\n * documented in DEC-169 / suggested ADR-057.\n *\n * Returns the picked hint together with the original `ModelSpec`\n * (when an explicit spec won the tie-break).\n *\n * @stable\n */\nexport function pickTopTierAcrossTools(\n hints: ReadonlyArray<ModelHint | ModelSpec | undefined>,\n): { readonly hint: ModelHint; readonly spec?: ModelSpec } | undefined {\n let bestRank = -1;\n let bestSpec: ModelSpec | undefined;\n let bestHint: ModelHint | undefined;\n for (const h of hints) {\n if (h === undefined) continue;\n if (isModelHint(h)) {\n const rank = TIER_ORDER[h];\n if (rank > bestRank) {\n bestRank = rank;\n bestSpec = undefined;\n bestHint = h;\n }\n } else {\n // Explicit `ModelSpec` -> treat as the highest tier.\n const rank = TIER_ORDER.smart;\n if (rank >= bestRank) {\n bestRank = rank;\n bestSpec = h;\n bestHint = 'smart';\n }\n }\n }\n if (bestHint === undefined) return undefined;\n if (bestSpec !== undefined) return { hint: bestHint, spec: bestSpec };\n return { hint: bestHint };\n}\n\n/**\n * Walk the precedence ladder and return the resolved provider for a\n * single agent step. Pure function no side effects.\n *\n * @stable\n */\nexport function resolvePreferredModel(input: ResolvePreferredModelInput): PreferredModelResolution {\n if (input.prepareStepProvider !== undefined) {\n return {\n resolvedProvider: input.prepareStepProvider,\n resolvedModelId: input.prepareStepProvider.modelId,\n source: 'prepare-step',\n };\n }\n const top = pickTopTierAcrossTools(input.toolPreferredModels);\n if (top !== undefined) {\n if (top.spec !== undefined) {\n return {\n resolvedProvider: specToProvider(top.spec),\n resolvedModelId: modelIdFromSpec(top.spec),\n source: 'spec',\n };\n }\n const mapped = input.modelTierMap?.[top.hint];\n if (mapped !== undefined) {\n return {\n resolvedProvider: specToProvider(mapped),\n resolvedModelId: modelIdFromSpec(mapped),\n source: 'tier-map',\n hintApplied: top.hint,\n };\n }\n // Fall through to agent-preferred / default.\n const agent = input.agentPreferredModel;\n if (agent !== undefined) {\n if (isModelHint(agent)) {\n const agentMapped = input.modelTierMap?.[agent];\n if (agentMapped !== undefined) {\n return {\n resolvedProvider: specToProvider(agentMapped),\n resolvedModelId: modelIdFromSpec(agentMapped),\n source: 'agent-preferred',\n hintApplied: agent,\n fallthroughReason: 'tier-not-mapped',\n };\n }\n } else {\n return {\n resolvedProvider: specToProvider(agent),\n resolvedModelId: modelIdFromSpec(agent),\n source: 'agent-preferred',\n fallthroughReason: 'tier-not-mapped',\n };\n }\n }\n return {\n resolvedProvider: input.agentDefaultProvider,\n resolvedModelId: input.agentDefaultProvider.modelId,\n source: 'fallthrough-default',\n hintApplied: top.hint,\n fallthroughReason: 'tier-not-mapped',\n };\n }\n // No per-tool hints. Honour `Agent.preferredModel` next.\n const agent = input.agentPreferredModel;\n if (agent !== undefined) {\n if (isModelHint(agent)) {\n const mapped = input.modelTierMap?.[agent];\n if (mapped !== undefined) {\n return {\n resolvedProvider: specToProvider(mapped),\n resolvedModelId: modelIdFromSpec(mapped),\n source: 'agent-preferred',\n hintApplied: agent,\n };\n }\n // Agent-default tier not mapped: fall through.\n return {\n resolvedProvider: input.agentDefaultProvider,\n resolvedModelId: input.agentDefaultProvider.modelId,\n source: 'fallthrough-default',\n hintApplied: agent,\n fallthroughReason: 'tier-not-mapped',\n };\n }\n return {\n resolvedProvider: specToProvider(agent),\n resolvedModelId: modelIdFromSpec(agent),\n source: 'agent-preferred',\n };\n }\n return {\n resolvedProvider: input.agentDefaultProvider,\n resolvedModelId: input.agentDefaultProvider.modelId,\n source: 'fallthrough-default',\n };\n}\n"],"mappings":";AA2BA,MAAMA,aAAwC;CAAE,MAAM;CAAG,UAAU;CAAG,OAAO;CAAG;AA+BhF,SAAS,YAAY,OAA8D;AACjF,QAAO,UAAU,UAAU,UAAU,cAAc,UAAU;;AAG/D,SAAS,eAAe,MAA2B;AACjD,KAAI,cAAc,KAAM,QAAO,KAAK;AACpC,QAAO;;AAGT,SAAS,gBAAgB,MAAyB;AAChD,KAAI,cAAc,KAAM,QAAO,KAAK;AACpC,QAAQ,KAAkB;;;;;;;;;;;;;AAc5B,SAAgB,uBACd,OACqE;CACrE,IAAI,WAAW;CACf,IAAIC;CACJ,IAAIC;AACJ,MAAK,MAAM,KAAK,OAAO;AACrB,MAAI,MAAM,OAAW;AACrB,MAAI,YAAY,EAAE,EAAE;GAClB,MAAM,OAAO,WAAW;AACxB,OAAI,OAAO,UAAU;AACnB,eAAW;AACX,eAAW;AACX,eAAW;;SAER;GAEL,MAAM,OAAO,WAAW;AACxB,OAAI,QAAQ,UAAU;AACpB,eAAW;AACX,eAAW;AACX,eAAW;;;;AAIjB,KAAI,aAAa,OAAW,QAAO;AACnC,KAAI,aAAa,OAAW,QAAO;EAAE,MAAM;EAAU,MAAM;EAAU;AACrE,QAAO,EAAE,MAAM,UAAU;;;;;;;;AAS3B,SAAgB,sBAAsB,OAA6D;AACjG,KAAI,MAAM,wBAAwB,OAChC,QAAO;EACL,kBAAkB,MAAM;EACxB,iBAAiB,MAAM,oBAAoB;EAC3C,QAAQ;EACT;CAEH,MAAM,MAAM,uBAAuB,MAAM,oBAAoB;AAC7D,KAAI,QAAQ,QAAW;AACrB,MAAI,IAAI,SAAS,OACf,QAAO;GACL,kBAAkB,eAAe,IAAI,KAAK;GAC1C,iBAAiB,gBAAgB,IAAI,KAAK;GAC1C,QAAQ;GACT;EAEH,MAAM,SAAS,MAAM,eAAe,IAAI;AACxC,MAAI,WAAW,OACb,QAAO;GACL,kBAAkB,eAAe,OAAO;GACxC,iBAAiB,gBAAgB,OAAO;GACxC,QAAQ;GACR,aAAa,IAAI;GAClB;EAGH,MAAMC,UAAQ,MAAM;AACpB,MAAIA,YAAU,OACZ,KAAI,YAAYA,QAAM,EAAE;GACtB,MAAM,cAAc,MAAM,eAAeA;AACzC,OAAI,gBAAgB,OAClB,QAAO;IACL,kBAAkB,eAAe,YAAY;IAC7C,iBAAiB,gBAAgB,YAAY;IAC7C,QAAQ;IACR,aAAaA;IACb,mBAAmB;IACpB;QAGH,QAAO;GACL,kBAAkB,eAAeA,QAAM;GACvC,iBAAiB,gBAAgBA,QAAM;GACvC,QAAQ;GACR,mBAAmB;GACpB;AAGL,SAAO;GACL,kBAAkB,MAAM;GACxB,iBAAiB,MAAM,qBAAqB;GAC5C,QAAQ;GACR,aAAa,IAAI;GACjB,mBAAmB;GACpB;;CAGH,MAAM,QAAQ,MAAM;AACpB,KAAI,UAAU,QAAW;AACvB,MAAI,YAAY,MAAM,EAAE;GACtB,MAAM,SAAS,MAAM,eAAe;AACpC,OAAI,WAAW,OACb,QAAO;IACL,kBAAkB,eAAe,OAAO;IACxC,iBAAiB,gBAAgB,OAAO;IACxC,QAAQ;IACR,aAAa;IACd;AAGH,UAAO;IACL,kBAAkB,MAAM;IACxB,iBAAiB,MAAM,qBAAqB;IAC5C,QAAQ;IACR,aAAa;IACb,mBAAmB;IACpB;;AAEH,SAAO;GACL,kBAAkB,eAAe,MAAM;GACvC,iBAAiB,gBAAgB,MAAM;GACvC,QAAQ;GACT;;AAEH,QAAO;EACL,kBAAkB,MAAM;EACxB,iBAAiB,MAAM,qBAAqB;EAC5C,QAAQ;EACT"}
1
+ {"version":3,"file":"index.js","names":["TIER_ORDER: Record<ModelHint, number>","bestSpec: ModelSpec | undefined","bestHint: ModelHint | undefined","agent"],"sources":["../../src/preferred-model/index.ts"],"sourcesContent":["/**\n * Per-tool / per-agent preferred-model resolution. Pure functions\n * consulted by the agent loop AFTER the model has decided which\n * tool(s) to call but BEFORE `provider.stream(...)` is invoked.\n *\n * The four-step precedence ladder (highest wins):\n *\n * 1. `prepareStep({ provider })` - the operator's explicit per-step\n * override always wins.\n * 2. `Tool.preferredModel` - the tool author's per-tool hint.\n * Only the tools the model actually CALLED on the previous step\n * are consulted (AG-15) - an advertised-but-uncalled hint never\n * escalates the run. Multi-tool ties resolve to the highest cost\n * tier (`'smart' > 'balanced' > 'fast'`; explicit `ModelSpec` is\n * treated as the highest tier).\n * 3. `Agent.preferredModel?` - the per-agent default.\n * 4. `Agent` default `provider` - the v0.1-alpha behaviour.\n *\n * Cost-tier resolution against `Agent.modelTierMap` is documented\n * as a hint: when the requested tier is unmapped, the resolver\n * falls through to the next precedence step rather than throwing.\n *\n * @packageDocumentation\n */\n\nimport type { ModelHint, ModelSpec, Provider } from '@graphorin/core';\n\nconst TIER_ORDER: Record<ModelHint, number> = { fast: 0, balanced: 1, smart: 2 };\n\n/**\n * Result returned by {@link resolvePreferredModel}.\n *\n * @stable\n */\nexport interface PreferredModelResolution {\n readonly resolvedProvider: Provider;\n readonly resolvedModelId: string;\n readonly source: 'prepare-step' | 'tier-map' | 'spec' | 'agent-preferred' | 'fallthrough-default';\n readonly hintApplied?: ModelHint;\n readonly fallthroughReason?:\n | 'tier-not-mapped'\n | 'provider-unavailable'\n | 'override-takes-precedence';\n}\n\n/**\n * Pure inputs to {@link resolvePreferredModel}.\n *\n * @stable\n */\nexport interface ResolvePreferredModelInput {\n readonly prepareStepProvider?: Provider;\n readonly toolPreferredModels: ReadonlyArray<ModelHint | ModelSpec | undefined>;\n readonly agentPreferredModel?: ModelHint | ModelSpec;\n readonly agentDefaultProvider: Provider;\n readonly modelTierMap?: Partial<Record<ModelHint, ModelSpec>>;\n}\n\nfunction isModelHint(value: ModelHint | ModelSpec | undefined): value is ModelHint {\n return value === 'fast' || value === 'balanced' || value === 'smart';\n}\n\nfunction specToProvider(spec: ModelSpec): Provider {\n if ('provider' in spec) return spec.provider as Provider;\n return spec as Provider;\n}\n\nfunction modelIdFromSpec(spec: ModelSpec): string {\n if ('provider' in spec) return spec.model;\n return (spec as Provider).modelId;\n}\n\n/**\n * Pick the highest-cost tier across the supplied per-tool hints.\n * Explicit `ModelSpec` entries are treated as the highest tier\n * (`'smart'`) for tie-breaking - the conservative-correctness rule\n * documented in DEC-169 / suggested ADR-057.\n *\n * Returns the picked hint together with the original `ModelSpec`\n * (when an explicit spec won the tie-break).\n *\n * @stable\n */\nexport function pickTopTierAcrossTools(\n hints: ReadonlyArray<ModelHint | ModelSpec | undefined>,\n): { readonly hint: ModelHint; readonly spec?: ModelSpec } | undefined {\n let bestRank = -1;\n let bestSpec: ModelSpec | undefined;\n let bestHint: ModelHint | undefined;\n for (const h of hints) {\n if (h === undefined) continue;\n if (isModelHint(h)) {\n const rank = TIER_ORDER[h];\n if (rank > bestRank) {\n bestRank = rank;\n bestSpec = undefined;\n bestHint = h;\n }\n } else {\n // Explicit `ModelSpec` -> treat as the highest tier.\n const rank = TIER_ORDER.smart;\n if (rank >= bestRank) {\n bestRank = rank;\n bestSpec = h;\n bestHint = 'smart';\n }\n }\n }\n if (bestHint === undefined) return undefined;\n if (bestSpec !== undefined) return { hint: bestHint, spec: bestSpec };\n return { hint: bestHint };\n}\n\n/**\n * Walk the precedence ladder and return the resolved provider for a\n * single agent step. Pure function - no side effects.\n *\n * @stable\n */\nexport function resolvePreferredModel(input: ResolvePreferredModelInput): PreferredModelResolution {\n if (input.prepareStepProvider !== undefined) {\n return {\n resolvedProvider: input.prepareStepProvider,\n resolvedModelId: input.prepareStepProvider.modelId,\n source: 'prepare-step',\n };\n }\n const top = pickTopTierAcrossTools(input.toolPreferredModels);\n if (top !== undefined) {\n if (top.spec !== undefined) {\n return {\n resolvedProvider: specToProvider(top.spec),\n resolvedModelId: modelIdFromSpec(top.spec),\n source: 'spec',\n };\n }\n const mapped = input.modelTierMap?.[top.hint];\n if (mapped !== undefined) {\n return {\n resolvedProvider: specToProvider(mapped),\n resolvedModelId: modelIdFromSpec(mapped),\n source: 'tier-map',\n hintApplied: top.hint,\n };\n }\n // Fall through to agent-preferred / default.\n const agent = input.agentPreferredModel;\n if (agent !== undefined) {\n if (isModelHint(agent)) {\n const agentMapped = input.modelTierMap?.[agent];\n if (agentMapped !== undefined) {\n return {\n resolvedProvider: specToProvider(agentMapped),\n resolvedModelId: modelIdFromSpec(agentMapped),\n source: 'agent-preferred',\n hintApplied: agent,\n fallthroughReason: 'tier-not-mapped',\n };\n }\n } else {\n return {\n resolvedProvider: specToProvider(agent),\n resolvedModelId: modelIdFromSpec(agent),\n source: 'agent-preferred',\n fallthroughReason: 'tier-not-mapped',\n };\n }\n }\n return {\n resolvedProvider: input.agentDefaultProvider,\n resolvedModelId: input.agentDefaultProvider.modelId,\n source: 'fallthrough-default',\n hintApplied: top.hint,\n fallthroughReason: 'tier-not-mapped',\n };\n }\n // No per-tool hints. Honour `Agent.preferredModel` next.\n const agent = input.agentPreferredModel;\n if (agent !== undefined) {\n if (isModelHint(agent)) {\n const mapped = input.modelTierMap?.[agent];\n if (mapped !== undefined) {\n return {\n resolvedProvider: specToProvider(mapped),\n resolvedModelId: modelIdFromSpec(mapped),\n source: 'agent-preferred',\n hintApplied: agent,\n };\n }\n // Agent-default tier not mapped: fall through.\n return {\n resolvedProvider: input.agentDefaultProvider,\n resolvedModelId: input.agentDefaultProvider.modelId,\n source: 'fallthrough-default',\n hintApplied: agent,\n fallthroughReason: 'tier-not-mapped',\n };\n }\n return {\n resolvedProvider: specToProvider(agent),\n resolvedModelId: modelIdFromSpec(agent),\n source: 'agent-preferred',\n };\n }\n return {\n resolvedProvider: input.agentDefaultProvider,\n resolvedModelId: input.agentDefaultProvider.modelId,\n source: 'fallthrough-default',\n };\n}\n"],"mappings":";AA2BA,MAAMA,aAAwC;CAAE,MAAM;CAAG,UAAU;CAAG,OAAO;CAAG;AA+BhF,SAAS,YAAY,OAA8D;AACjF,QAAO,UAAU,UAAU,UAAU,cAAc,UAAU;;AAG/D,SAAS,eAAe,MAA2B;AACjD,KAAI,cAAc,KAAM,QAAO,KAAK;AACpC,QAAO;;AAGT,SAAS,gBAAgB,MAAyB;AAChD,KAAI,cAAc,KAAM,QAAO,KAAK;AACpC,QAAQ,KAAkB;;;;;;;;;;;;;AAc5B,SAAgB,uBACd,OACqE;CACrE,IAAI,WAAW;CACf,IAAIC;CACJ,IAAIC;AACJ,MAAK,MAAM,KAAK,OAAO;AACrB,MAAI,MAAM,OAAW;AACrB,MAAI,YAAY,EAAE,EAAE;GAClB,MAAM,OAAO,WAAW;AACxB,OAAI,OAAO,UAAU;AACnB,eAAW;AACX,eAAW;AACX,eAAW;;SAER;GAEL,MAAM,OAAO,WAAW;AACxB,OAAI,QAAQ,UAAU;AACpB,eAAW;AACX,eAAW;AACX,eAAW;;;;AAIjB,KAAI,aAAa,OAAW,QAAO;AACnC,KAAI,aAAa,OAAW,QAAO;EAAE,MAAM;EAAU,MAAM;EAAU;AACrE,QAAO,EAAE,MAAM,UAAU;;;;;;;;AAS3B,SAAgB,sBAAsB,OAA6D;AACjG,KAAI,MAAM,wBAAwB,OAChC,QAAO;EACL,kBAAkB,MAAM;EACxB,iBAAiB,MAAM,oBAAoB;EAC3C,QAAQ;EACT;CAEH,MAAM,MAAM,uBAAuB,MAAM,oBAAoB;AAC7D,KAAI,QAAQ,QAAW;AACrB,MAAI,IAAI,SAAS,OACf,QAAO;GACL,kBAAkB,eAAe,IAAI,KAAK;GAC1C,iBAAiB,gBAAgB,IAAI,KAAK;GAC1C,QAAQ;GACT;EAEH,MAAM,SAAS,MAAM,eAAe,IAAI;AACxC,MAAI,WAAW,OACb,QAAO;GACL,kBAAkB,eAAe,OAAO;GACxC,iBAAiB,gBAAgB,OAAO;GACxC,QAAQ;GACR,aAAa,IAAI;GAClB;EAGH,MAAMC,UAAQ,MAAM;AACpB,MAAIA,YAAU,OACZ,KAAI,YAAYA,QAAM,EAAE;GACtB,MAAM,cAAc,MAAM,eAAeA;AACzC,OAAI,gBAAgB,OAClB,QAAO;IACL,kBAAkB,eAAe,YAAY;IAC7C,iBAAiB,gBAAgB,YAAY;IAC7C,QAAQ;IACR,aAAaA;IACb,mBAAmB;IACpB;QAGH,QAAO;GACL,kBAAkB,eAAeA,QAAM;GACvC,iBAAiB,gBAAgBA,QAAM;GACvC,QAAQ;GACR,mBAAmB;GACpB;AAGL,SAAO;GACL,kBAAkB,MAAM;GACxB,iBAAiB,MAAM,qBAAqB;GAC5C,QAAQ;GACR,aAAa,IAAI;GACjB,mBAAmB;GACpB;;CAGH,MAAM,QAAQ,MAAM;AACpB,KAAI,UAAU,QAAW;AACvB,MAAI,YAAY,MAAM,EAAE;GACtB,MAAM,SAAS,MAAM,eAAe;AACpC,OAAI,WAAW,OACb,QAAO;IACL,kBAAkB,eAAe,OAAO;IACxC,iBAAiB,gBAAgB,OAAO;IACxC,QAAQ;IACR,aAAa;IACd;AAGH,UAAO;IACL,kBAAkB,MAAM;IACxB,iBAAiB,MAAM,qBAAqB;IAC5C,QAAQ;IACR,aAAa;IACb,mBAAmB;IACpB;;AAEH,SAAO;GACL,kBAAkB,eAAe,MAAM;GACvC,iBAAiB,gBAAgB,MAAM;GACvC,QAAQ;GACT;;AAEH,QAAO;EACL,kBAAkB,MAAM;EACxB,iBAAiB,MAAM,qBAAqB;EAC5C,QAAQ;EACT"}
@@ -12,12 +12,12 @@ import { join } from "node:path";
12
12
  *
13
13
  * Cross-session continuity flow:
14
14
  *
15
- * 1. The current agent calls `agent.progress.write(content)`
15
+ * 1. The current agent calls `agent.progress.write(content)` -
16
16
  * runtime persists the file and queues the
17
17
  * `agent.progress.written` event (drained into the active or
18
18
  * next consumed stream).
19
19
  * 2. A sibling / future agent calls
20
- * `agent.progress.read({ runId: priorRunId })` runtime
20
+ * `agent.progress.read({ runId: priorRunId })` - runtime
21
21
  * discovers existing files (no implicit auto-discovery; the
22
22
  * operator must supply the `runId` cursor).
23
23
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["defaultSensitivity: Sensitivity","redact: (s: string) => string","entries: string[]","out: ProgressArtifactRef[]"],"sources":["../../src/progress/index.ts"],"sourcesContent":["/**\n * Structured progress-artifact IO. Persists UTF-8 text artifacts\n * under `<artifactRoot>/<runId>/progress/<role>.<seqPadded>.txt`\n * via atomic-write `.tmp + rename` discipline.\n *\n * Cross-session continuity flow:\n *\n * 1. The current agent calls `agent.progress.write(content)` —\n * runtime persists the file and queues the\n * `agent.progress.written` event (drained into the active or\n * next consumed stream).\n * 2. A sibling / future agent calls\n * `agent.progress.read({ runId: priorRunId })` — runtime\n * discovers existing files (no implicit auto-discovery; the\n * operator must supply the `runId` cursor).\n *\n * Auto-discovery across runs is deferred to v0.2.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport { mkdir, readdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport type { ProgressArtifactRef, Sensitivity } from '@graphorin/core';\nimport { ProgressWriteError } from '../errors/index.js';\n\nconst DEFAULT_ARTIFACT_DIR = 'graphorin-progress';\nconst SEQ_PAD_WIDTH = 3;\n\n/**\n * Optional configuration accepted by {@link createProgressIO}.\n *\n * @stable\n */\nexport interface ProgressIOConfig {\n /** Filesystem root under which `<runId>/progress/<role>.<seq>.txt` files live. */\n readonly artifactRoot?: string;\n /** Default `Sensitivity` applied when the caller does not override. */\n readonly defaultSensitivity?: Sensitivity;\n /** Optional redaction transform applied to content before write. */\n readonly redact?: (content: string) => string;\n}\n\n/**\n * Per-call options for {@link ProgressIO.write}.\n *\n * @stable\n */\nexport interface ProgressWriteOptions {\n readonly role?: string;\n /** Explicit sequence number; default auto-increments per `(runId, role)`. */\n readonly seq?: number;\n readonly sensitivity?: Sensitivity;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Per-call options for {@link ProgressIO.read}.\n *\n * @stable\n */\nexport interface ProgressReadOptions {\n readonly runId?: string;\n readonly role?: string;\n /** Skip artifacts whose `seq <= sinceSeq`. */\n readonly sinceSeq?: number;\n /** Default `100`. */\n readonly maxArtifacts?: number;\n}\n\n/**\n * Public surface returned by {@link createProgressIO}. Used by the\n * agent runtime to back `agent.progress.write / read`.\n *\n * @stable\n */\nexport interface ProgressIO {\n write(\n runId: string,\n content: string,\n options?: ProgressWriteOptions,\n ): Promise<ProgressArtifactRef>;\n read(\n currentRunId: string,\n options?: ProgressReadOptions,\n ): Promise<ReadonlyArray<ProgressArtifactRef>>;\n rootFor(runId: string): string;\n}\n\ninterface InternalSeqState {\n next: number;\n}\n\nconst seqWidth = (n: number): string => String(n).padStart(SEQ_PAD_WIDTH, '0');\n\nconst sha256Hex = (input: string): string =>\n createHash('sha256').update(input, 'utf8').digest('hex');\n\n/**\n * Build a {@link ProgressIO} bound to a particular artifact root.\n *\n * @stable\n */\nexport function createProgressIO(config: ProgressIOConfig = {}): ProgressIO {\n const root = config.artifactRoot ?? join(tmpdir(), DEFAULT_ARTIFACT_DIR);\n const defaultSensitivity: Sensitivity = config.defaultSensitivity ?? 'internal';\n const redact: (s: string) => string = config.redact ?? ((s) => s);\n const seqState = new Map<string, InternalSeqState>();\n\n const directoryFor = (runId: string): string => join(root, runId, 'progress');\n\n const seqKey = (runId: string, role: string): string => `${runId}::${role}`;\n\n const ensureSeqState = async (runId: string, role: string): Promise<InternalSeqState> => {\n const key = seqKey(runId, role);\n let state = seqState.get(key);\n if (state !== undefined) return state;\n state = { next: 1 };\n try {\n const dir = directoryFor(runId);\n const entries = await readdir(dir);\n let max = 0;\n const prefix = `${role}.`;\n for (const entry of entries) {\n if (!entry.startsWith(prefix) || !entry.endsWith('.txt')) continue;\n const seqStr = entry.slice(prefix.length, entry.length - 4);\n const n = Number(seqStr);\n if (Number.isFinite(n) && n > max) max = n;\n }\n state.next = max + 1;\n } catch {\n // Directory does not exist yet — first write will create it.\n }\n seqState.set(key, state);\n return state;\n };\n\n const write = async (\n runId: string,\n rawContent: string,\n options: ProgressWriteOptions = {},\n ): Promise<ProgressArtifactRef> => {\n const role = options.role ?? 'agent';\n const sensitivity = options.sensitivity ?? defaultSensitivity;\n const tags = options.tags ?? [];\n const seqSource = await ensureSeqState(runId, role);\n const seq = options.seq ?? seqSource.next;\n seqSource.next = Math.max(seqSource.next, seq + 1);\n\n const content = redact(rawContent);\n const dir = directoryFor(runId);\n const filename = `${role}.${seqWidth(seq)}.txt`;\n const finalPath = join(dir, filename);\n const tmpPath = `${finalPath}.tmp`;\n\n try {\n await mkdir(dir, { recursive: true });\n await writeFile(tmpPath, content, { encoding: 'utf8' });\n await rename(tmpPath, finalPath);\n } catch (cause) {\n try {\n await unlink(tmpPath);\n } catch {\n // Best-effort cleanup; the write error is the operator-facing signal.\n }\n throw new ProgressWriteError(finalPath, cause);\n }\n\n const stats = await stat(finalPath);\n const sha256 = sha256Hex(content);\n return {\n path: finalPath,\n role,\n seq,\n sizeBytes: stats.size,\n sensitivity,\n ...(tags.length > 0 ? { tags } : {}),\n writtenAtIso: new Date().toISOString(),\n sha256,\n };\n };\n\n const read = async (\n currentRunId: string,\n options: ProgressReadOptions = {},\n ): Promise<ReadonlyArray<ProgressArtifactRef>> => {\n const runId = options.runId ?? currentRunId;\n const max = options.maxArtifacts ?? 100;\n const sinceSeq = options.sinceSeq ?? 0;\n const dir = directoryFor(runId);\n let entries: string[];\n try {\n entries = await readdir(dir);\n } catch {\n return [];\n }\n const out: ProgressArtifactRef[] = [];\n const role = options.role;\n for (const entry of entries) {\n if (!entry.endsWith('.txt')) continue;\n const dot = entry.indexOf('.');\n if (dot < 0) continue;\n const entryRole = entry.slice(0, dot);\n const seqStr = entry.slice(dot + 1, entry.length - 4);\n const seq = Number(seqStr);\n if (!Number.isFinite(seq)) continue;\n if (role !== undefined && entryRole !== role) continue;\n if (seq <= sinceSeq) continue;\n const fullPath = join(dir, entry);\n const stats = await stat(fullPath);\n const content = await readFile(fullPath, { encoding: 'utf8' });\n const sha256 = sha256Hex(content);\n out.push({\n path: fullPath,\n role: entryRole,\n seq,\n sizeBytes: stats.size,\n sensitivity: 'internal',\n writtenAtIso: stats.mtime.toISOString(),\n sha256,\n });\n if (out.length >= max) break;\n }\n out.sort((a, b) => {\n if (a.role !== b.role) return a.role.localeCompare(b.role);\n return a.seq - b.seq;\n });\n return out;\n };\n\n return {\n write,\n read,\n rootFor: (runId) => directoryFor(runId),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["defaultSensitivity: Sensitivity","redact: (s: string) => string","entries: string[]","out: ProgressArtifactRef[]"],"sources":["../../src/progress/index.ts"],"sourcesContent":["/**\n * Structured progress-artifact IO. Persists UTF-8 text artifacts\n * under `<artifactRoot>/<runId>/progress/<role>.<seqPadded>.txt`\n * via atomic-write `.tmp + rename` discipline.\n *\n * Cross-session continuity flow:\n *\n * 1. The current agent calls `agent.progress.write(content)` -\n * runtime persists the file and queues the\n * `agent.progress.written` event (drained into the active or\n * next consumed stream).\n * 2. A sibling / future agent calls\n * `agent.progress.read({ runId: priorRunId })` - runtime\n * discovers existing files (no implicit auto-discovery; the\n * operator must supply the `runId` cursor).\n *\n * Auto-discovery across runs is deferred to v0.2.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport { mkdir, readdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport type { ProgressArtifactRef, Sensitivity } from '@graphorin/core';\nimport { ProgressWriteError } from '../errors/index.js';\n\nconst DEFAULT_ARTIFACT_DIR = 'graphorin-progress';\nconst SEQ_PAD_WIDTH = 3;\n\n/**\n * Optional configuration accepted by {@link createProgressIO}.\n *\n * @stable\n */\nexport interface ProgressIOConfig {\n /** Filesystem root under which `<runId>/progress/<role>.<seq>.txt` files live. */\n readonly artifactRoot?: string;\n /** Default `Sensitivity` applied when the caller does not override. */\n readonly defaultSensitivity?: Sensitivity;\n /** Optional redaction transform applied to content before write. */\n readonly redact?: (content: string) => string;\n}\n\n/**\n * Per-call options for {@link ProgressIO.write}.\n *\n * @stable\n */\nexport interface ProgressWriteOptions {\n readonly role?: string;\n /** Explicit sequence number; default auto-increments per `(runId, role)`. */\n readonly seq?: number;\n readonly sensitivity?: Sensitivity;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Per-call options for {@link ProgressIO.read}.\n *\n * @stable\n */\nexport interface ProgressReadOptions {\n readonly runId?: string;\n readonly role?: string;\n /** Skip artifacts whose `seq <= sinceSeq`. */\n readonly sinceSeq?: number;\n /** Default `100`. */\n readonly maxArtifacts?: number;\n}\n\n/**\n * Public surface returned by {@link createProgressIO}. Used by the\n * agent runtime to back `agent.progress.write / read`.\n *\n * @stable\n */\nexport interface ProgressIO {\n write(\n runId: string,\n content: string,\n options?: ProgressWriteOptions,\n ): Promise<ProgressArtifactRef>;\n read(\n currentRunId: string,\n options?: ProgressReadOptions,\n ): Promise<ReadonlyArray<ProgressArtifactRef>>;\n rootFor(runId: string): string;\n}\n\ninterface InternalSeqState {\n next: number;\n}\n\nconst seqWidth = (n: number): string => String(n).padStart(SEQ_PAD_WIDTH, '0');\n\nconst sha256Hex = (input: string): string =>\n createHash('sha256').update(input, 'utf8').digest('hex');\n\n/**\n * Build a {@link ProgressIO} bound to a particular artifact root.\n *\n * @stable\n */\nexport function createProgressIO(config: ProgressIOConfig = {}): ProgressIO {\n const root = config.artifactRoot ?? join(tmpdir(), DEFAULT_ARTIFACT_DIR);\n const defaultSensitivity: Sensitivity = config.defaultSensitivity ?? 'internal';\n const redact: (s: string) => string = config.redact ?? ((s) => s);\n const seqState = new Map<string, InternalSeqState>();\n\n const directoryFor = (runId: string): string => join(root, runId, 'progress');\n\n const seqKey = (runId: string, role: string): string => `${runId}::${role}`;\n\n const ensureSeqState = async (runId: string, role: string): Promise<InternalSeqState> => {\n const key = seqKey(runId, role);\n let state = seqState.get(key);\n if (state !== undefined) return state;\n state = { next: 1 };\n try {\n const dir = directoryFor(runId);\n const entries = await readdir(dir);\n let max = 0;\n const prefix = `${role}.`;\n for (const entry of entries) {\n if (!entry.startsWith(prefix) || !entry.endsWith('.txt')) continue;\n const seqStr = entry.slice(prefix.length, entry.length - 4);\n const n = Number(seqStr);\n if (Number.isFinite(n) && n > max) max = n;\n }\n state.next = max + 1;\n } catch {\n // Directory does not exist yet - first write will create it.\n }\n seqState.set(key, state);\n return state;\n };\n\n const write = async (\n runId: string,\n rawContent: string,\n options: ProgressWriteOptions = {},\n ): Promise<ProgressArtifactRef> => {\n const role = options.role ?? 'agent';\n const sensitivity = options.sensitivity ?? defaultSensitivity;\n const tags = options.tags ?? [];\n const seqSource = await ensureSeqState(runId, role);\n const seq = options.seq ?? seqSource.next;\n seqSource.next = Math.max(seqSource.next, seq + 1);\n\n const content = redact(rawContent);\n const dir = directoryFor(runId);\n const filename = `${role}.${seqWidth(seq)}.txt`;\n const finalPath = join(dir, filename);\n const tmpPath = `${finalPath}.tmp`;\n\n try {\n await mkdir(dir, { recursive: true });\n await writeFile(tmpPath, content, { encoding: 'utf8' });\n await rename(tmpPath, finalPath);\n } catch (cause) {\n try {\n await unlink(tmpPath);\n } catch {\n // Best-effort cleanup; the write error is the operator-facing signal.\n }\n throw new ProgressWriteError(finalPath, cause);\n }\n\n const stats = await stat(finalPath);\n const sha256 = sha256Hex(content);\n return {\n path: finalPath,\n role,\n seq,\n sizeBytes: stats.size,\n sensitivity,\n ...(tags.length > 0 ? { tags } : {}),\n writtenAtIso: new Date().toISOString(),\n sha256,\n };\n };\n\n const read = async (\n currentRunId: string,\n options: ProgressReadOptions = {},\n ): Promise<ReadonlyArray<ProgressArtifactRef>> => {\n const runId = options.runId ?? currentRunId;\n const max = options.maxArtifacts ?? 100;\n const sinceSeq = options.sinceSeq ?? 0;\n const dir = directoryFor(runId);\n let entries: string[];\n try {\n entries = await readdir(dir);\n } catch {\n return [];\n }\n const out: ProgressArtifactRef[] = [];\n const role = options.role;\n for (const entry of entries) {\n if (!entry.endsWith('.txt')) continue;\n const dot = entry.indexOf('.');\n if (dot < 0) continue;\n const entryRole = entry.slice(0, dot);\n const seqStr = entry.slice(dot + 1, entry.length - 4);\n const seq = Number(seqStr);\n if (!Number.isFinite(seq)) continue;\n if (role !== undefined && entryRole !== role) continue;\n if (seq <= sinceSeq) continue;\n const fullPath = join(dir, entry);\n const stats = await stat(fullPath);\n const content = await readFile(fullPath, { encoding: 'utf8' });\n const sha256 = sha256Hex(content);\n out.push({\n path: fullPath,\n role: entryRole,\n seq,\n sizeBytes: stats.size,\n sensitivity: 'internal',\n writtenAtIso: stats.mtime.toISOString(),\n sha256,\n });\n if (out.length >= max) break;\n }\n out.sort((a, b) => {\n if (a.role !== b.role) return a.role.localeCompare(b.role);\n return a.seq - b.seq;\n });\n return out;\n };\n\n return {\n write,\n read,\n rootFor: (runId) => directoryFor(runId),\n };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { CompletedToolCall, HandoffRecord, Message, RunState, RunStateUsageByModel, RunStatus, RunStep, RunTaintSummary, ToolApproval, Usage } from "@graphorin/core";
1
+ import { CompletedToolCall, HandoffRecord, Message, RunState, RunStateUsageByModel, RunStatus, RunStep, RunTaintSummary, TodoItem, ToolApproval, Usage } from "@graphorin/core";
2
2
 
3
3
  //#region src/run-state/index.d.ts
4
4
 
@@ -38,6 +38,8 @@ interface SerializedRunState {
38
38
  readonly taintSummary?: RunTaintSummary;
39
39
  /** AG-19: deferred tools promoted by `tool_search` this run. */
40
40
  readonly promotedTools?: ReadonlyArray<string>;
41
+ /** D6: journaled structured plan/todo list. */
42
+ readonly todos?: ReadonlyArray<TodoItem>;
41
43
  readonly startedAt: string;
42
44
  readonly finishedAt?: string;
43
45
  readonly error?: {
@@ -55,8 +57,8 @@ interface SerializeRunStateOptions {
55
57
  /**
56
58
  * Deep-redact secret-named keys (`apiKey`, `authorization`,
57
59
  * `bearerToken` / `accessToken` / `refreshToken`, `password`,
58
- * `secret`, …) anywhere in the snapshot tool results and messages
59
- * included replacing their values with `'[redacted]'`. Defaults to
60
+ * `secret`, …) anywhere in the snapshot - tool results and messages
61
+ * included - replacing their values with `'[redacted]'`. Defaults to
60
62
  * `false` for the round-trip canonical helper; the agent runtime
61
63
  * passes `true` when persisting through the checkpoint store
62
64
  * (AG-23). Redaction is best-effort by key name: secrets stored
@@ -73,7 +75,7 @@ interface SerializeRunStateOptions {
73
75
  declare function serializeRunState(state: RunState, options?: SerializeRunStateOptions): SerializedRunState;
74
76
  /**
75
77
  * Render the canonical JSON string representation of the supplied
76
- * {@link RunState}. `JSON.stringify(serializeRunState(state))`
78
+ * {@link RunState}. `JSON.stringify(serializeRunState(state))` -
77
79
  * provided as a convenience.
78
80
  *
79
81
  * @stable
@@ -124,7 +126,7 @@ declare function createInitialRunState(args: {
124
126
  }): RunState;
125
127
  /**
126
128
  * Append a per-model usage entry to {@link RunState.usageByModel}.
127
- * Mutates the supplied state in place used by the agent runtime's
129
+ * Mutates the supplied state in place - used by the agent runtime's
128
130
  * per-step retry loop. Pure callers that need an immutable update
129
131
  * should clone the state first.
130
132
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/run-state/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AA4D0B,cA5Bb,wBA4Ba,EAAA,yBAAA;;;;AAe1B;AAoBA;AACS,cAzDI,gCAAA,GAyDJ,CAAA;;;;AAmFT;AAEC;AAiCD;AAoJgB,UA3TC,kBAAA,CA2T8C;EAkB/C,SAAA,OAAA,EAAA,OA5UW,wBAkVf;EAyBI,SAAA,EAAA,EAAA,MAAa;EA8Bb,SAAA,OAAA,EAAA,MAAA;EA+BA,SAAA,cAAA,EAAA,MAA2B;EAAQ,SAAA,SAAA,EAAA,MAAA;EAAyB,SAAA,MAAA,CAAA,EAAA,MAAA;EAAd,SAAA,MAAA,EAla3C,SAka2C;EAAa,SAAA,KAAA,EAjazD,aAiayD,CAja3C,OAia2C,CAAA;qBAhatD,cAAc;6BACN,cAAc;qBACtB,cAAc;kBACjB;0BACQ;;0BAEA;;2BAEC;;;;;;;;;;;;;;UAWV,wBAAA;;;;;;;;;;;;;;;;;;;iBAoBD,iBAAA,QACP,oBACE,2BACR;;;;;;;;iBAiFa,cAAA,QAAsB,oBAAoB;UAIhD,kBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+BM,mBAAA,6BAA+C,qBAA0B;;iBAoJzE,gBAAA,+BAA+C,qBAAqB;;;;;;;;iBAkBpE,qBAAA;;;;;;IAMZ;;;;;;;;;iBAyBY,aAAA,QAAqB,kCAAkC;;;;;;;;iBA8BvD,yBAAA,UAAmC,mCAAmC;;;;;;;;iBA+BtE,2BAAA,QAAmC,WAAW,cAAc"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/run-state/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AA6D0B,cA5Bb,wBA4Ba,EAAA,yBAAA;;;;;;AAiBT,cAtCJ,gCAAA,GAsC4B,CAAA;AAoBzC;;;;;AAqFA;AAIU,UA3IO,kBAAA,CA2IW;EA+BZ,SAAA,OAAA,EAAA,OAzKW,wBAyKoC;EAgL/C,SAAA,EAAA,EAAA,MAAgB;EAkBhB,SAAA,OAAA,EAAA,MAAqB;EA+BrB,SAAA,cAAa,EAAA,MAAQ;EAwCrB,SAAA,SAAA,EAAA,MAAA;EA6CA,SAAA,MAAA,CAAA,EAAA,MAAA;EAAmC,SAAA,MAAA,EAzdhC,SAydgC;EAAyB,SAAA,KAAA,EAxd1D,aAwd0D,CAxd5C,OAwd4C,CAAA;EAAd,SAAA,QAAA,EAvdzC,aAudyC,CAvd3B,OAud2B,CAAA;EAAa,SAAA,gBAAA,EAtd9C,aAsd8C,CAtdhC,YAsdgC,CAAA;qBArdtD,cAAc;kBACjB;0BACQ;;0BAEA;;2BAEC;;mBAER,cAAc;;;;;;;;;;;;;;UAWhB,wBAAA;;;;;;;;;;;;;;;;;;;iBAoBD,iBAAA,QACP,oBACE,2BACR;;;;;;;;iBAkFa,cAAA,QAAsB,oBAAoB;UAIhD,kBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+BM,mBAAA,6BAA+C,qBAA0B;;iBAgLzE,gBAAA,+BAA+C,qBAAqB;;;;;;;;iBAkBpE,qBAAA;;;;;;IAMZ;;;;;;;;;iBAyBY,aAAA,QAAqB,kCAAkC;;;;;;;;iBAwCvD,yBAAA,UAAmC,mCAAmC;;;;;;;;iBA6CtE,2BAAA,QAAmC,WAAW,cAAc"}
@@ -37,6 +37,7 @@ function serializeRunState(state, options = {}) {
37
37
  ...state.usageByModel !== void 0 ? { usageByModel: state.usageByModel } : {},
38
38
  ...state.taintSummary !== void 0 ? { taintSummary: state.taintSummary } : {},
39
39
  ...state.promotedTools !== void 0 ? { promotedTools: state.promotedTools } : {},
40
+ ...state.todos !== void 0 ? { todos: state.todos } : {},
40
41
  startedAt: state.startedAt,
41
42
  ...state.finishedAt !== void 0 ? { finishedAt: state.finishedAt } : {},
42
43
  ...state.error !== void 0 ? { error: state.error } : {}
@@ -79,7 +80,7 @@ function redactJsonString(content) {
79
80
  }
80
81
  /**
81
82
  * Render the canonical JSON string representation of the supplied
82
- * {@link RunState}. `JSON.stringify(serializeRunState(state))`
83
+ * {@link RunState}. `JSON.stringify(serializeRunState(state))` -
83
84
  * provided as a convenience.
84
85
  *
85
86
  * @stable
@@ -136,6 +137,8 @@ function deserializeRunState(payload, options = {}) {
136
137
  completionTokens: Number(usageRaw.completionTokens ?? 0),
137
138
  totalTokens: Number(usageRaw.totalTokens ?? 0),
138
139
  ...usageRaw.reasoningTokens !== void 0 ? { reasoningTokens: Number(usageRaw.reasoningTokens) } : {},
140
+ ...usageRaw.cachedReadTokens !== void 0 ? { cachedReadTokens: Number(usageRaw.cachedReadTokens) } : {},
141
+ ...usageRaw.cacheWriteTokens !== void 0 ? { cacheWriteTokens: Number(usageRaw.cacheWriteTokens) } : {},
139
142
  ...isRecord(usageRaw.cost) ? { cost: {
140
143
  amount: Number(usageRaw.cost.amount ?? 0),
141
144
  currency: String(usageRaw.cost.currency ?? "USD")
@@ -162,6 +165,8 @@ function deserializeRunState(payload, options = {}) {
162
165
  totalTokens: Number(entryRaw.totalTokens ?? 0),
163
166
  attemptCount: Number(entryRaw.attemptCount ?? 1),
164
167
  ...entryRaw.reasoningTokens !== void 0 ? { reasoningTokens: Number(entryRaw.reasoningTokens) } : {},
168
+ ...entryRaw.cachedReadTokens !== void 0 ? { cachedReadTokens: Number(entryRaw.cachedReadTokens) } : {},
169
+ ...entryRaw.cacheWriteTokens !== void 0 ? { cacheWriteTokens: Number(entryRaw.cacheWriteTokens) } : {},
165
170
  ...isRecord(entryRaw.cost) ? { cost: {
166
171
  amount: Number(entryRaw.cost.amount ?? 0),
167
172
  currency: String(entryRaw.cost.currency ?? "USD")
@@ -179,13 +184,20 @@ function deserializeRunState(payload, options = {}) {
179
184
  let taintSummary;
180
185
  if (isRecord(payload.taintSummary)) {
181
186
  const ts = payload.taintSummary;
187
+ const tileHashes = Array.isArray(ts.spanTileHashes) ? ts.spanTileHashes.filter((h) => typeof h === "string") : void 0;
182
188
  taintSummary = {
183
189
  untrustedSeen: ts.untrustedSeen === true,
184
190
  sensitiveSeen: ts.sensitiveSeen === true,
185
- untrustedSourceKinds: Array.isArray(ts.untrustedSourceKinds) ? ts.untrustedSourceKinds.filter((k) => typeof k === "string") : []
191
+ untrustedSourceKinds: Array.isArray(ts.untrustedSourceKinds) ? ts.untrustedSourceKinds.filter((k) => typeof k === "string") : [],
192
+ ...tileHashes !== void 0 && tileHashes.length > 0 ? { spanTileHashes: tileHashes } : {}
186
193
  };
187
194
  }
188
195
  const promotedTools = Array.isArray(payload.promotedTools) ? payload.promotedTools.filter((t) => typeof t === "string") : void 0;
196
+ const todos = Array.isArray(payload.todos) ? payload.todos.filter(isRecord).filter((t) => typeof t.id === "string" && typeof t.content === "string" && (t.status === "pending" || t.status === "in_progress" || t.status === "completed")).map((t) => ({
197
+ id: t.id,
198
+ content: t.content,
199
+ status: t.status
200
+ })) : void 0;
189
201
  return {
190
202
  id,
191
203
  agentId,
@@ -201,6 +213,7 @@ function deserializeRunState(payload, options = {}) {
201
213
  ...usageByModel !== void 0 ? { usageByModel } : {},
202
214
  ...taintSummary !== void 0 ? { taintSummary } : {},
203
215
  ...promotedTools !== void 0 ? { promotedTools } : {},
216
+ ...todos !== void 0 ? { todos } : {},
204
217
  startedAt,
205
218
  ...typeof finishedAtRaw === "string" ? { finishedAt: finishedAtRaw } : {},
206
219
  ...error !== void 0 ? { error } : {}
@@ -241,7 +254,7 @@ function createInitialRunState(args) {
241
254
  }
242
255
  /**
243
256
  * Append a per-model usage entry to {@link RunState.usageByModel}.
244
- * Mutates the supplied state in place used by the agent runtime's
257
+ * Mutates the supplied state in place - used by the agent runtime's
245
258
  * per-step retry loop. Pure callers that need an immutable update
246
259
  * should clone the state first.
247
260
  *
@@ -255,6 +268,8 @@ function addModelUsage(state, modelId, delta) {
255
268
  completionTokens: prev.completionTokens + delta.completionTokens,
256
269
  totalTokens: prev.totalTokens + delta.totalTokens,
257
270
  ...delta.reasoningTokens !== void 0 || prev.reasoningTokens !== void 0 ? { reasoningTokens: (prev.reasoningTokens ?? 0) + (delta.reasoningTokens ?? 0) } : {},
271
+ ...delta.cachedReadTokens !== void 0 || prev.cachedReadTokens !== void 0 ? { cachedReadTokens: (prev.cachedReadTokens ?? 0) + (delta.cachedReadTokens ?? 0) } : {},
272
+ ...delta.cacheWriteTokens !== void 0 || prev.cacheWriteTokens !== void 0 ? { cacheWriteTokens: (prev.cacheWriteTokens ?? 0) + (delta.cacheWriteTokens ?? 0) } : {},
258
273
  attemptCount: prev.attemptCount + 1,
259
274
  ...prev.cost !== void 0 ? { cost: prev.cost } : {}
260
275
  } : {
@@ -276,7 +291,11 @@ function aggregateUsageFromByModel(byModel) {
276
291
  let ct = 0;
277
292
  let tt = 0;
278
293
  let rt = 0;
294
+ let cr = 0;
295
+ let cw = 0;
279
296
  let hasReasoning = false;
297
+ let hasCachedRead = false;
298
+ let hasCacheWrite = false;
280
299
  for (const entry of Object.values(byModel)) {
281
300
  pt += entry.promptTokens;
282
301
  ct += entry.completionTokens;
@@ -285,12 +304,22 @@ function aggregateUsageFromByModel(byModel) {
285
304
  rt += entry.reasoningTokens;
286
305
  hasReasoning = true;
287
306
  }
307
+ if (entry.cachedReadTokens !== void 0) {
308
+ cr += entry.cachedReadTokens;
309
+ hasCachedRead = true;
310
+ }
311
+ if (entry.cacheWriteTokens !== void 0) {
312
+ cw += entry.cacheWriteTokens;
313
+ hasCacheWrite = true;
314
+ }
288
315
  }
289
316
  return {
290
317
  promptTokens: pt,
291
318
  completionTokens: ct,
292
319
  totalTokens: tt,
293
- ...hasReasoning ? { reasoningTokens: rt } : {}
320
+ ...hasReasoning ? { reasoningTokens: rt } : {},
321
+ ...hasCachedRead ? { cachedReadTokens: cr } : {},
322
+ ...hasCacheWrite ? { cacheWriteTokens: cw } : {}
294
323
  };
295
324
  }
296
325
  /**