@caupulican/pi-agent-core 0.81.2 → 0.81.3

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 (44) hide show
  1. package/dist/compaction/compaction.d.ts +11 -2
  2. package/dist/compaction/compaction.d.ts.map +1 -1
  3. package/dist/compaction/compaction.js +258 -106
  4. package/dist/compaction/compaction.js.map +1 -1
  5. package/dist/compaction/extraction.d.ts +15 -0
  6. package/dist/compaction/extraction.d.ts.map +1 -0
  7. package/dist/compaction/extraction.js +342 -0
  8. package/dist/compaction/extraction.js.map +1 -0
  9. package/dist/compaction/index.d.ts +4 -0
  10. package/dist/compaction/index.d.ts.map +1 -1
  11. package/dist/compaction/index.js +4 -0
  12. package/dist/compaction/index.js.map +1 -1
  13. package/dist/compaction/loop.d.ts +52 -0
  14. package/dist/compaction/loop.d.ts.map +1 -0
  15. package/dist/compaction/loop.js +154 -0
  16. package/dist/compaction/loop.js.map +1 -0
  17. package/dist/compaction/token-budget.d.ts +12 -0
  18. package/dist/compaction/token-budget.d.ts.map +1 -0
  19. package/dist/compaction/token-budget.js +52 -0
  20. package/dist/compaction/token-budget.js.map +1 -0
  21. package/dist/compaction/utils.d.ts +1 -1
  22. package/dist/compaction/utils.d.ts.map +1 -1
  23. package/dist/compaction/utils.js +45 -2
  24. package/dist/compaction/utils.js.map +1 -1
  25. package/dist/compaction/verification.d.ts +20 -0
  26. package/dist/compaction/verification.d.ts.map +1 -0
  27. package/dist/compaction/verification.js +169 -0
  28. package/dist/compaction/verification.js.map +1 -0
  29. package/dist/reliability/classifier.d.ts +2 -0
  30. package/dist/reliability/classifier.d.ts.map +1 -1
  31. package/dist/reliability/classifier.js +18 -1
  32. package/dist/reliability/classifier.js.map +1 -1
  33. package/dist/reliability/index.d.ts +1 -0
  34. package/dist/reliability/index.d.ts.map +1 -1
  35. package/dist/reliability/index.js +1 -0
  36. package/dist/reliability/index.js.map +1 -1
  37. package/dist/reliability/provider-signatures.d.ts +10 -0
  38. package/dist/reliability/provider-signatures.d.ts.map +1 -0
  39. package/dist/reliability/provider-signatures.js +11 -0
  40. package/dist/reliability/provider-signatures.js.map +1 -0
  41. package/dist/reliability/retry-controller.d.ts.map +1 -1
  42. package/dist/reliability/retry-controller.js +1 -0
  43. package/dist/reliability/retry-controller.js.map +1 -1
  44. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop.js","sourceRoot":"","sources":["../../src/compaction/loop.ts"],"names":[],"mappings":"AA2CA,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,MAAM,cAAc,GAAG,CAAC,CAAC;AACzB,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAwB,EAAkC;IACjG,IAAI,SAAS,GAAG,OAAO,CAAC;IACxB,IAAI,UAA6C,CAAC;IAClD,IAAI,kBAAsC,CAAC;IAC3C,IAAI,aAA2C,CAAC;IAChD,IAAI,cAAc,GAAG,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC;IACzG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,cAAc,IAAI,CAAC,EAAE,CAAC;QAC7D,cAAc,GAAG,mBAAmB,CAAC;IACtC,CAAC;IAED,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,UAAU,EAAE,KAAK,EAAE,EAAE,CAAC;QAClD,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YAC1B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC;QACjE,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAChC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,IAAI,KAAK,YAAY,EAAE,CAAC;YAC3E,IAAI,aAAa,EAAE,CAAC;gBACnB,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC;YACtE,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;QACtD,CAAC;QAED,MAAM,cAAc,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAChD,IAAI,cAAc,IAAI,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAClD,OAAO;gBACN,IAAI,EAAE,MAAM;gBACZ,MAAM,EACL,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,IAAI,KAAK,YAAY;oBACpE,CAAC,CAAC,mBAAmB;oBACrB,CAAC,CAAC,kBAAkB;aACtB,CAAC;QACH,CAAC;QAED,MAAM,cAAc,GAAG,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;QACvF,MAAM,MAAM,GAAG,wBAAwB,CAAC,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,kBAAkB,CAAC,CAAC;QACxG,kBAAkB,GAAG,cAAc,CAAC;QACpC,UAAU,GAAG,MAAM,CAAC;QAEpB,IAAI,MAAM,CAAC,iBAAiB,IAAI,KAAK,GAAG,cAAc,EAAE,CAAC;YACxD,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;gBAC1B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC;YACjE,CAAC;YACD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,4BAA4B,EAAE,CAAC,CAAC;YAC9E,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACzB,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACnD,CAAC;QAED,IAAI,SAAuB,CAAC;QAC5B,IAAI,CAAC;YACJ,SAAS,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACR,SAAS,GAAG,aAAa,CAAC;YAC1B,IAAI,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACzE,SAAS;QACV,CAAC;QACD,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACvB,SAAS,GAAG,aAAa,CAAC;YAC1B,IAAI,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACzE,SAAS;QACV,CAAC;QAED,IAAI,MAAwB,CAAC;QAC7B,IAAI,CAAC;YACJ,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAC1C,MAAM,EACN,SAAS,CAAC,KAAK,EACf,SAAS,CAAC,MAAM,EAChB,SAAS,CAAC,OAAO,EACjB,MAAM,CACN,CAAC,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;YACnC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC7B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;YAC7D,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACzE,SAAS;QACV,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YAC1B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAC7D,CAAC;QACD,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACzB,aAAa,GAAG,MAAM,CAAC;QAEvB,MAAM,aAAa,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC/C,IAAI,aAAa,IAAI,IAAI,CAAC,mBAAmB,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;YACpE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACnD,CAAC;QAED,SAAS,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,6BAA6B,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAAA,CACrF;AAED,SAAS,iBAAiB,CACzB,KAAa,EACb,KAAa,EACb,UAA6C,EAC7C,cAAsB,EACE;IACxB,IAAI,KAAK,IAAI,UAAU,EAAE,CAAC;QACzB,OAAO;YACN,SAAS,EAAE,UAAU,EAAE,SAAS,IAAI,SAAS;YAC7C,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,gBAAgB,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/F,OAAO,EAAE,IAAI;YACb,iBAAiB,EAAE,IAAI;SACvB,CAAC;IACH,CAAC;IAED,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO;YACN,SAAS,EAAE,OAAO;YAClB,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,cAAc,CAAC;YAC7C,OAAO,EAAE,KAAK;YACd,iBAAiB,EAAE,KAAK;SACxB,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAA0B;QACrC,GAAG,UAAU;QACb,iBAAiB,EAAE,KAAK;KACxB,CAAC;IAEF,IAAI,KAAK,KAAK,aAAa,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;QACxD,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;IAC9B,CAAC;SAAM,IAAI,KAAK,KAAK,gBAAgB,EAAE,CAAC;QACvC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IACvB,CAAC;SAAM,IAAI,KAAK,KAAK,qBAAqB,EAAE,CAAC;QAC5C,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,MAAM,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC;IACpF,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd;AAED,SAAS,wBAAwB,CAChC,MAA6B,EAC7B,UAA6C,EAC7C,cAAsB,EACtB,kBAAsC,EACd;IACxB,IACC,CAAC,UAAU;QACX,MAAM,CAAC,iBAAiB;QACxB,CAAC,kBAAkB,KAAK,SAAS,IAAI,cAAc,GAAG,kBAAkB,CAAC,EACxE,CAAC;QACF,OAAO,MAAM,CAAC;IACf,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;QACrC,OAAO,MAAM,CAAC;IACf,CAAC;IAED,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9E,IAAI,gBAAgB,KAAK,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAClD,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACvD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACrC,CAAC;IACD,OAAO,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;AAAA,CACpF;AAED,SAAS,UAAU,CAAC,CAAwB,EAAE,CAAwB,EAAW;IAChF,OAAO,CACN,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS;QAC3B,CAAC,CAAC,gBAAgB,KAAK,CAAC,CAAC,gBAAgB;QACzC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO;QACvB,CAAC,CAAC,iBAAiB,KAAK,CAAC,CAAC,iBAAiB,CAC3C,CAAC;AAAA,CACF;AAED,SAAS,eAAe,CAAC,KAAc,EAAU;IAChD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAChG,IAAI,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;QAAE,OAAO,aAAa,CAAC;IAC1D,IAAI,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;QAAE,OAAO,gBAAgB,CAAC;IAChE,IAAI,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IACnG,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;QAC/F,OAAO,aAAa,CAAC;IACtB,OAAO,iBAAiB,CAAC;AAAA,CACzB","sourcesContent":["import type { Model } from \"@caupulican/pi-ai\";\nimport type { SessionEntry } from \"../session/session-manager.ts\";\nimport type { CompactionResult } from \"./compaction.ts\";\n\nexport interface CompactionCycleParams {\n\tmodelTier: \"cheap\" | \"session\";\n\tkeepRecentTokens: number;\n\tchunked: boolean;\n\tdeterministicOnly: boolean;\n}\n\nexport interface ModelAndAuth {\n\tmodel: Model<any>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tfailure?: string;\n}\n\nexport interface CompactionLoopDeps {\n\tmeasureLiveTokens(): number;\n\tgetTriggerThreshold(): number;\n\tgetMargin(): number;\n\tgetBranch(): SessionEntry[];\n\tresolveModelAndAuth(modelTier: CompactionCycleParams[\"modelTier\"]): Promise<ModelAndAuth>;\n\tsummarizeAndVerify(\n\t\tparams: CompactionCycleParams,\n\t\tmodel: Model<any>,\n\t\tapiKey: string | undefined,\n\t\theaders: Record<string, string> | undefined,\n\t\tbranch: SessionEntry[],\n\t): Promise<{ result: CompactionResult }>;\n\tbuildDeterministicCheckpoint(): Promise<{ result: CompactionResult }> | { result: CompactionResult };\n\tapply(result: CompactionResult): Promise<void> | void;\n\tonTransition(info: { cycle: number; from: string; cause: string }): void;\n\tgetBaseKeepRecentTokens?(): number;\n\tsignal?: AbortSignal;\n}\n\nexport type CompactionLoopOutcome =\n\t| { kind: \"success\"; result: CompactionResult; cycles: number }\n\t| { kind: \"skip\"; reason: string }\n\t| { kind: \"failed\"; reason: string; cycles: number };\n\nconst MAX_CYCLES = 4;\nconst MAX_LLM_CYCLES = 3;\nconst DEFAULT_KEEP_RECENT = 20_000;\n\nexport async function runCompactionLoop(deps: CompactionLoopDeps): Promise<CompactionLoopOutcome> {\n\tlet lastCause = \"start\";\n\tlet lastParams: CompactionCycleParams | undefined;\n\tlet lastObservedTokens: number | undefined;\n\tlet appliedResult: CompactionResult | undefined;\n\tlet baseKeepRecent = deps.getBaseKeepRecentTokens ? deps.getBaseKeepRecentTokens() : DEFAULT_KEEP_RECENT;\n\tif (!Number.isFinite(baseKeepRecent) || baseKeepRecent <= 0) {\n\t\tbaseKeepRecent = DEFAULT_KEEP_RECENT;\n\t}\n\n\tfor (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t}\n\n\t\tconst branch = deps.getBranch();\n\t\tif (branch.length > 0 && branch[branch.length - 1]?.type === \"compaction\") {\n\t\t\tif (appliedResult) {\n\t\t\t\treturn { kind: \"success\", result: appliedResult, cycles: cycle - 1 };\n\t\t\t}\n\t\t\treturn { kind: \"skip\", reason: \"already compacted\" };\n\t\t}\n\n\t\tconst observedTokens = deps.measureLiveTokens();\n\t\tif (observedTokens <= deps.getTriggerThreshold()) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\treason:\n\t\t\t\t\tbranch.length > 0 && branch[branch.length - 1]?.type === \"compaction\"\n\t\t\t\t\t\t? \"already compacted\"\n\t\t\t\t\t\t: \"within threshold\",\n\t\t\t};\n\t\t}\n\n\t\tconst selectedParams = selectCycleParams(cycle, lastCause, lastParams, baseKeepRecent);\n\t\tconst params = enforceMonotonicProgress(selectedParams, lastParams, observedTokens, lastObservedTokens);\n\t\tlastObservedTokens = observedTokens;\n\t\tlastParams = params;\n\n\t\tif (params.deterministicOnly || cycle > MAX_LLM_CYCLES) {\n\t\t\tif (deps.signal?.aborted) {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle - 1 };\n\t\t\t}\n\t\t\tconst { result } = await Promise.resolve(deps.buildDeterministicCheckpoint());\n\t\t\tawait deps.apply(result);\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlet modelInfo: ModelAndAuth;\n\t\ttry {\n\t\t\tmodelInfo = await deps.resolveModelAndAuth(params.modelTier);\n\t\t} catch {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\t\tif (modelInfo.failure) {\n\t\t\tlastCause = \"auth-failed\";\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step0\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet result: CompactionResult;\n\t\ttry {\n\t\t\t({ result } = await deps.summarizeAndVerify(\n\t\t\t\tparams,\n\t\t\t\tmodelInfo.model,\n\t\t\t\tmodelInfo.apiKey,\n\t\t\t\tmodelInfo.headers,\n\t\t\t\tbranch,\n\t\t\t));\n\t\t} catch (error) {\n\t\t\tlastCause = mapFailureCause(error);\n\t\t\tif (lastCause === \"aborted\") {\n\t\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t\t}\n\t\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step3\", cause: lastCause });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (deps.signal?.aborted) {\n\t\t\treturn { kind: \"failed\", reason: \"aborted\", cycles: cycle };\n\t\t}\n\t\tawait deps.apply(result);\n\t\tappliedResult = result;\n\n\t\tconst measuredAfter = deps.measureLiveTokens();\n\t\tif (measuredAfter <= deps.getTriggerThreshold() - deps.getMargin()) {\n\t\t\treturn { kind: \"success\", result, cycles: cycle };\n\t\t}\n\n\t\tlastCause = \"effect-not-restored\";\n\t\tdeps.onTransition({ cycle: cycle + 1, from: \"step5\", cause: lastCause });\n\t}\n\n\treturn { kind: \"failed\", reason: \"exhausted-compaction-cycles\", cycles: MAX_CYCLES };\n}\n\nfunction selectCycleParams(\n\tcycle: number,\n\tcause: string,\n\tlastParams: CompactionCycleParams | undefined,\n\tbaseKeepRecent: number,\n): CompactionCycleParams {\n\tif (cycle >= MAX_CYCLES) {\n\t\treturn {\n\t\t\tmodelTier: lastParams?.modelTier ?? \"session\",\n\t\t\tkeepRecentTokens: Math.max(1, Math.floor((lastParams?.keepRecentTokens ?? baseKeepRecent) / 2)),\n\t\t\tchunked: true,\n\t\t\tdeterministicOnly: true,\n\t\t};\n\t}\n\n\tif (!lastParams) {\n\t\treturn {\n\t\t\tmodelTier: \"cheap\",\n\t\t\tkeepRecentTokens: Math.max(1, baseKeepRecent),\n\t\t\tchunked: false,\n\t\t\tdeterministicOnly: false,\n\t\t};\n\t}\n\n\tconst params: CompactionCycleParams = {\n\t\t...lastParams,\n\t\tdeterministicOnly: false,\n\t};\n\n\tif (cause === \"gate-failed\" || cause === \"auth-failed\") {\n\t\tparams.modelTier = \"session\";\n\t} else if (cause === \"input-overflow\") {\n\t\tparams.chunked = true;\n\t} else if (cause === \"effect-not-restored\") {\n\t\tparams.chunked = true;\n\t\tparams.keepRecentTokens = Math.max(1, Math.floor(lastParams.keepRecentTokens / 2));\n\t}\n\n\treturn params;\n}\n\nfunction enforceMonotonicProgress(\n\tparams: CompactionCycleParams,\n\tlastParams: CompactionCycleParams | undefined,\n\tobservedTokens: number,\n\tlastObservedTokens: number | undefined,\n): CompactionCycleParams {\n\tif (\n\t\t!lastParams ||\n\t\tparams.deterministicOnly ||\n\t\t(lastObservedTokens !== undefined && observedTokens < lastObservedTokens)\n\t) {\n\t\treturn params;\n\t}\n\tif (!sameParams(params, lastParams)) {\n\t\treturn params;\n\t}\n\n\tconst keepRecentTokens = Math.max(1, Math.floor(params.keepRecentTokens / 2));\n\tif (keepRecentTokens !== params.keepRecentTokens) {\n\t\treturn { ...params, chunked: true, keepRecentTokens };\n\t}\n\tif (!params.chunked) {\n\t\treturn { ...params, chunked: true };\n\t}\n\treturn { ...params, modelTier: params.modelTier === \"cheap\" ? \"session\" : \"cheap\" };\n}\n\nfunction sameParams(a: CompactionCycleParams, b: CompactionCycleParams): boolean {\n\treturn (\n\t\ta.modelTier === b.modelTier &&\n\t\ta.keepRecentTokens === b.keepRecentTokens &&\n\t\ta.chunked === b.chunked &&\n\t\ta.deterministicOnly === b.deterministicOnly\n\t);\n}\n\nfunction mapFailureCause(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : typeof error === \"string\" ? error : \"\";\n\tif (message.includes(\"gate-failed\")) return \"gate-failed\";\n\tif (message.includes(\"input-overflow\")) return \"input-overflow\";\n\tif (message.includes(\"auto-compaction-cancelled\") || message.includes(\"aborted\")) return \"aborted\";\n\tif (message.includes(\"auth\") || message.includes(\"api key\") || message.includes(\"not compacted\"))\n\t\treturn \"auth-failed\";\n\treturn \"unknown-failure\";\n}\n"]}
@@ -0,0 +1,12 @@
1
+ export declare class TokenBudget {
2
+ private _ratio;
3
+ private _anchorTokens;
4
+ private _hasAnchor;
5
+ private _recentAbsError;
6
+ private _sampleCount;
7
+ get ratio(): number;
8
+ anchor(inputTokens: number, coveredChars: number): void;
9
+ estimateDelta(chars: number): number;
10
+ current(deltaChars: number, window: number): number;
11
+ }
12
+ //# sourceMappingURL=token-budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-budget.d.ts","sourceRoot":"","sources":["../../src/compaction/token-budget.ts"],"names":[],"mappings":"AAIA,qBAAa,WAAW;IACvB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,YAAY,CAAK;IAEzB,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAoBtD;IAED,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnC;IAED,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CASlD;CACD","sourcesContent":["const MIN_CHARS_PER_TOKEN = 2.2;\nconst MAX_CHARS_PER_TOKEN = 6;\nconst DEFAULT_CHARS_PER_TOKEN = 4;\n\nexport class TokenBudget {\n\tprivate _ratio = DEFAULT_CHARS_PER_TOKEN;\n\tprivate _anchorTokens = 0;\n\tprivate _hasAnchor = false;\n\tprivate _recentAbsError = 0;\n\tprivate _sampleCount = 0;\n\n\tget ratio(): number {\n\t\treturn this._ratio;\n\t}\n\n\tanchor(inputTokens: number, coveredChars: number): void {\n\t\tif (!Number.isFinite(inputTokens) || !Number.isFinite(coveredChars) || inputTokens <= 0 || coveredChars <= 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst observedRatio = clampRatio(coveredChars / inputTokens);\n\t\tif (this._hasAnchor) {\n\t\t\tconst previousEstimate = this.estimateDelta(coveredChars);\n\t\t\tconst absError = Math.abs(previousEstimate - inputTokens);\n\t\t\tthis._recentAbsError = this._sampleCount === 0 ? absError : this._recentAbsError * 0.5 + absError * 0.5;\n\t\t\tthis._ratio = clampRatio(this._ratio + (observedRatio - this._ratio) * 0.5);\n\t\t\tthis._sampleCount += 1;\n\t\t} else {\n\t\t\tthis._ratio = observedRatio;\n\t\t\tthis._recentAbsError = 0;\n\t\t\tthis._sampleCount = 1;\n\t\t\tthis._hasAnchor = true;\n\t\t}\n\n\t\tthis._anchorTokens = Math.ceil(inputTokens);\n\t}\n\n\testimateDelta(chars: number): number {\n\t\tif (!Number.isFinite(chars) || chars <= 0) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn Math.ceil(chars / this._ratio);\n\t}\n\n\tcurrent(deltaChars: number, window: number): number {\n\t\tconst delta = this.estimateDelta(deltaChars);\n\t\tif (!this._hasAnchor) {\n\t\t\treturn delta;\n\t\t}\n\n\t\tconst marginByWindow = Number.isFinite(window) && window > 0 ? Math.ceil(0.02 * window) : 0;\n\t\tconst marginByError = Math.ceil(2 * this._recentAbsError);\n\t\treturn this._anchorTokens + delta + Math.max(marginByWindow, marginByError);\n\t}\n}\n\nfunction clampRatio(ratio: number): number {\n\treturn Math.min(MAX_CHARS_PER_TOKEN, Math.max(MIN_CHARS_PER_TOKEN, ratio));\n}\n"]}
@@ -0,0 +1,52 @@
1
+ const MIN_CHARS_PER_TOKEN = 2.2;
2
+ const MAX_CHARS_PER_TOKEN = 6;
3
+ const DEFAULT_CHARS_PER_TOKEN = 4;
4
+ export class TokenBudget {
5
+ _ratio = DEFAULT_CHARS_PER_TOKEN;
6
+ _anchorTokens = 0;
7
+ _hasAnchor = false;
8
+ _recentAbsError = 0;
9
+ _sampleCount = 0;
10
+ get ratio() {
11
+ return this._ratio;
12
+ }
13
+ anchor(inputTokens, coveredChars) {
14
+ if (!Number.isFinite(inputTokens) || !Number.isFinite(coveredChars) || inputTokens <= 0 || coveredChars <= 0) {
15
+ return;
16
+ }
17
+ const observedRatio = clampRatio(coveredChars / inputTokens);
18
+ if (this._hasAnchor) {
19
+ const previousEstimate = this.estimateDelta(coveredChars);
20
+ const absError = Math.abs(previousEstimate - inputTokens);
21
+ this._recentAbsError = this._sampleCount === 0 ? absError : this._recentAbsError * 0.5 + absError * 0.5;
22
+ this._ratio = clampRatio(this._ratio + (observedRatio - this._ratio) * 0.5);
23
+ this._sampleCount += 1;
24
+ }
25
+ else {
26
+ this._ratio = observedRatio;
27
+ this._recentAbsError = 0;
28
+ this._sampleCount = 1;
29
+ this._hasAnchor = true;
30
+ }
31
+ this._anchorTokens = Math.ceil(inputTokens);
32
+ }
33
+ estimateDelta(chars) {
34
+ if (!Number.isFinite(chars) || chars <= 0) {
35
+ return 0;
36
+ }
37
+ return Math.ceil(chars / this._ratio);
38
+ }
39
+ current(deltaChars, window) {
40
+ const delta = this.estimateDelta(deltaChars);
41
+ if (!this._hasAnchor) {
42
+ return delta;
43
+ }
44
+ const marginByWindow = Number.isFinite(window) && window > 0 ? Math.ceil(0.02 * window) : 0;
45
+ const marginByError = Math.ceil(2 * this._recentAbsError);
46
+ return this._anchorTokens + delta + Math.max(marginByWindow, marginByError);
47
+ }
48
+ }
49
+ function clampRatio(ratio) {
50
+ return Math.min(MAX_CHARS_PER_TOKEN, Math.max(MIN_CHARS_PER_TOKEN, ratio));
51
+ }
52
+ //# sourceMappingURL=token-budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-budget.js","sourceRoot":"","sources":["../../src/compaction/token-budget.ts"],"names":[],"mappings":"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","sourcesContent":["const MIN_CHARS_PER_TOKEN = 2.2;\nconst MAX_CHARS_PER_TOKEN = 6;\nconst DEFAULT_CHARS_PER_TOKEN = 4;\n\nexport class TokenBudget {\n\tprivate _ratio = DEFAULT_CHARS_PER_TOKEN;\n\tprivate _anchorTokens = 0;\n\tprivate _hasAnchor = false;\n\tprivate _recentAbsError = 0;\n\tprivate _sampleCount = 0;\n\n\tget ratio(): number {\n\t\treturn this._ratio;\n\t}\n\n\tanchor(inputTokens: number, coveredChars: number): void {\n\t\tif (!Number.isFinite(inputTokens) || !Number.isFinite(coveredChars) || inputTokens <= 0 || coveredChars <= 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst observedRatio = clampRatio(coveredChars / inputTokens);\n\t\tif (this._hasAnchor) {\n\t\t\tconst previousEstimate = this.estimateDelta(coveredChars);\n\t\t\tconst absError = Math.abs(previousEstimate - inputTokens);\n\t\t\tthis._recentAbsError = this._sampleCount === 0 ? absError : this._recentAbsError * 0.5 + absError * 0.5;\n\t\t\tthis._ratio = clampRatio(this._ratio + (observedRatio - this._ratio) * 0.5);\n\t\t\tthis._sampleCount += 1;\n\t\t} else {\n\t\t\tthis._ratio = observedRatio;\n\t\t\tthis._recentAbsError = 0;\n\t\t\tthis._sampleCount = 1;\n\t\t\tthis._hasAnchor = true;\n\t\t}\n\n\t\tthis._anchorTokens = Math.ceil(inputTokens);\n\t}\n\n\testimateDelta(chars: number): number {\n\t\tif (!Number.isFinite(chars) || chars <= 0) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn Math.ceil(chars / this._ratio);\n\t}\n\n\tcurrent(deltaChars: number, window: number): number {\n\t\tconst delta = this.estimateDelta(deltaChars);\n\t\tif (!this._hasAnchor) {\n\t\t\treturn delta;\n\t\t}\n\n\t\tconst marginByWindow = Number.isFinite(window) && window > 0 ? Math.ceil(0.02 * window) : 0;\n\t\tconst marginByError = Math.ceil(2 * this._recentAbsError);\n\t\treturn this._anchorTokens + delta + Math.max(marginByWindow, marginByError);\n\t}\n}\n\nfunction clampRatio(ratio: number): number {\n\treturn Math.min(MAX_CHARS_PER_TOKEN, Math.max(MIN_CHARS_PER_TOKEN, ratio));\n}\n"]}
@@ -34,5 +34,5 @@ export declare function formatFileOperations(readFiles: string[], modifiedFiles:
34
34
  * reasonable token budgets. Full content is not needed for summarization.
35
35
  */
36
36
  export declare function serializeConversation(messages: Message[]): string;
37
- export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
37
+ export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context checkpointer. Input: a serialized agent conversation. Output: ONLY the checkpoint, exactly in the format below. No preamble. No commentary. Same language as the user. Never include secrets/keys/tokens \u2014 write [REDACTED].\n\nRULES:\n- Recent turns weigh heaviest. Old turns contribute only rules, decisions, and file knowledge.\n- ## Active Task: the user's most recent UNFULFILLED input, near-verbatim. A question awaiting an answer IS an active task. If the user's last signal cancels earlier work (stop/undo/never mind), record the cancellation and DROP the cancelled work everywhere.\n- ### Mandatory Rules: every user prohibition (\"do not X\", \"never Y\", \"stop doing Z\") as one bullet each, imperative, with source turn if known. PRESERVE existing rules verbatim. A user-corrected mistake appears ONLY here as \"DO NOT <mistake>\" \u2014 the mistaken work itself must not survive.\n- ## Files: one line per file that matters \u2014 path \u2014 why it matters (modified/created/read).\n- ## Done: numbered caveman log \u2014 \"N. VERB target \u2014 outcome\". Exact paths, commands, line numbers, error strings.\n- Sections with nothing: write \"(none)\".\n\nEXAMPLE INPUT (excerpt):\n[user]: add retry to the fetcher, and do not touch the legacy client\n[assistant]: (edits src/fetcher.ts, adds retry loop)\n[tool write src/fetcher.ts]: ok\n[tool bash npm test]: 2 failed: fetcher.test.ts\n[assistant]: (tries wrapping legacy client instead)\n[user]: no \u2014 stop changing the legacy client, I said don't touch it. Fix the two tests instead.\n\nEXAMPLE OUTPUT:\n## Active Task\nUser: \"Fix the two failing tests\" (fetcher.test.ts) \u2014 retry work continues, legacy-client changes cancelled.\n\n### Mandatory Rules\n- DO NOT touch the legacy client (user, twice)\n\n## Files\n- src/fetcher.ts \u2014 retry loop added (modified)\n- test/fetcher.test.ts \u2014 2 failing, current focus (read)\n\n## Done\n1. EDIT src/fetcher.ts \u2014 added retry loop\n2. TEST npm test \u2014 2 failed: fetcher.test.ts\n\n## Constraints & Preferences\n(none)\n\n## Key Decisions\n(none)\n\n## Blocked / Open\n- 2 fetcher tests failing\n\n## Critical Context\n(none)\n\nNote what the example TEACHES (not just shows): the legacy-client wrapping attempt (cancelled work) appears nowhere except as the DO-NOT rule; the Active Task is the tail, near-verbatim; Done lines are caveman-format.";
38
38
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/compaction/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,wBAAgB,aAAa,IAAI,cAAc,CAM9C;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CA2B9F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAK1G;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAUzF;AAmBD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAqDjE;AAMD,eAAO,MAAM,2BAA2B,oUAEmF,CAAC","sourcesContent":["/**\n * Shared utilities for compaction and branch summarization.\n */\n\nimport type { Message } from \"@caupulican/pi-ai\";\nimport type { AgentMessage } from \"../types.ts\";\n\n// ============================================================================\n// File Operation Tracking\n// ============================================================================\n\nexport interface FileOperations {\n\tread: Set<string>;\n\twritten: Set<string>;\n\tedited: Set<string>;\n}\n\nexport function createFileOps(): FileOperations {\n\treturn {\n\t\tread: new Set(),\n\t\twritten: new Set(),\n\t\tedited: new Set(),\n\t};\n}\n\n/**\n * Extract file operations from tool calls in an assistant message.\n */\nexport function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {\n\tif (message.role !== \"assistant\") return;\n\tif (!(\"content\" in message) || !Array.isArray(message.content)) return;\n\n\tfor (const block of message.content) {\n\t\tif (typeof block !== \"object\" || block === null) continue;\n\t\tif (!(\"type\" in block) || block.type !== \"toolCall\") continue;\n\t\tif (!(\"arguments\" in block) || !(\"name\" in block)) continue;\n\n\t\tconst args = block.arguments as Record<string, unknown> | undefined;\n\t\tif (!args) continue;\n\n\t\tconst path = typeof args.path === \"string\" ? args.path : undefined;\n\t\tif (!path) continue;\n\n\t\tswitch (block.name) {\n\t\t\tcase \"read\":\n\t\t\t\tfileOps.read.add(path);\n\t\t\t\tbreak;\n\t\t\tcase \"write\":\n\t\t\t\tfileOps.written.add(path);\n\t\t\t\tbreak;\n\t\t\tcase \"edit\":\n\t\t\t\tfileOps.edited.add(path);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Compute final file lists from file operations.\n * Returns readFiles (files only read, not modified) and modifiedFiles.\n */\nexport function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {\n\tconst modified = new Set([...fileOps.edited, ...fileOps.written]);\n\tconst readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();\n\tconst modifiedFiles = [...modified].sort();\n\treturn { readFiles: readOnly, modifiedFiles };\n}\n\n/**\n * Format file operations as XML tags for summary.\n */\nexport function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {\n\tconst sections: string[] = [];\n\tif (readFiles.length > 0) {\n\t\tsections.push(`<read-files>\\n${readFiles.join(\"\\n\")}\\n</read-files>`);\n\t}\n\tif (modifiedFiles.length > 0) {\n\t\tsections.push(`<modified-files>\\n${modifiedFiles.join(\"\\n\")}\\n</modified-files>`);\n\t}\n\tif (sections.length === 0) return \"\";\n\treturn `\\n\\n${sections.join(\"\\n\\n\")}`;\n}\n\n// ============================================================================\n// Message Serialization\n// ============================================================================\n\n/** Maximum characters for a tool result in serialized summaries. */\nconst TOOL_RESULT_MAX_CHARS = 2000;\n\n/**\n * Truncate text to a maximum character length for summarization.\n * Keeps the beginning and appends a truncation marker.\n */\nfunction truncateForSummary(text: string, maxChars: number): string {\n\tif (text.length <= maxChars) return text;\n\tconst truncatedChars = text.length - maxChars;\n\treturn `${text.slice(0, maxChars)}\\n\\n[... ${truncatedChars} more characters truncated]`;\n}\n\n/**\n * Serialize LLM messages to text for summarization.\n * This prevents the model from treating it as a conversation to continue.\n * Call convertToLlm() first to handle custom message types.\n *\n * Tool results are truncated to keep the summarization request within\n * reasonable token budgets. Full content is not needed for summarization.\n */\nexport function serializeConversation(messages: Message[]): string {\n\tconst parts: string[] = [];\n\n\tfor (const msg of messages) {\n\t\tif (msg.role === \"user\") {\n\t\t\tconst content =\n\t\t\t\ttypeof msg.content === \"string\"\n\t\t\t\t\t? msg.content\n\t\t\t\t\t: msg.content\n\t\t\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t.join(\"\");\n\t\t\tif (content) parts.push(`[User]: ${content}`);\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst textParts: string[] = [];\n\t\t\tconst thinkingParts: string[] = [];\n\t\t\tconst toolCalls: string[] = [];\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\ttextParts.push(block.text);\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tthinkingParts.push(block.thinking);\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tconst args = block.arguments as Record<string, unknown>;\n\t\t\t\t\tconst argsStr = Object.entries(args)\n\t\t\t\t\t\t.map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n\t\t\t\t\t\t.join(\", \");\n\t\t\t\t\ttoolCalls.push(`${block.name}(${argsStr})`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (thinkingParts.length > 0) {\n\t\t\t\tparts.push(`[Assistant thinking]: ${thinkingParts.join(\"\\n\")}`);\n\t\t\t}\n\t\t\tif (textParts.length > 0) {\n\t\t\t\tparts.push(`[Assistant]: ${textParts.join(\"\\n\")}`);\n\t\t\t}\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tparts.push(`[Assistant tool calls]: ${toolCalls.join(\"; \")}`);\n\t\t\t}\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\tconst content = msg.content\n\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\");\n\t\t\tif (content) {\n\t\t\t\tparts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\n// ============================================================================\n// Summarization System Prompt\n// ============================================================================\n\nexport const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;\n"]}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/compaction/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,wBAAgB,aAAa,IAAI,cAAc,CAM9C;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CA2B9F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAK1G;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAUzF;AAmBD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAqDjE;AAMD,eAAO,MAAM,2BAA2B,y2EA6CkL,CAAC","sourcesContent":["/**\n * Shared utilities for compaction and branch summarization.\n */\n\nimport type { Message } from \"@caupulican/pi-ai\";\nimport type { AgentMessage } from \"../types.ts\";\n\n// ============================================================================\n// File Operation Tracking\n// ============================================================================\n\nexport interface FileOperations {\n\tread: Set<string>;\n\twritten: Set<string>;\n\tedited: Set<string>;\n}\n\nexport function createFileOps(): FileOperations {\n\treturn {\n\t\tread: new Set(),\n\t\twritten: new Set(),\n\t\tedited: new Set(),\n\t};\n}\n\n/**\n * Extract file operations from tool calls in an assistant message.\n */\nexport function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {\n\tif (message.role !== \"assistant\") return;\n\tif (!(\"content\" in message) || !Array.isArray(message.content)) return;\n\n\tfor (const block of message.content) {\n\t\tif (typeof block !== \"object\" || block === null) continue;\n\t\tif (!(\"type\" in block) || block.type !== \"toolCall\") continue;\n\t\tif (!(\"arguments\" in block) || !(\"name\" in block)) continue;\n\n\t\tconst args = block.arguments as Record<string, unknown> | undefined;\n\t\tif (!args) continue;\n\n\t\tconst path = typeof args.path === \"string\" ? args.path : undefined;\n\t\tif (!path) continue;\n\n\t\tswitch (block.name) {\n\t\t\tcase \"read\":\n\t\t\t\tfileOps.read.add(path);\n\t\t\t\tbreak;\n\t\t\tcase \"write\":\n\t\t\t\tfileOps.written.add(path);\n\t\t\t\tbreak;\n\t\t\tcase \"edit\":\n\t\t\t\tfileOps.edited.add(path);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Compute final file lists from file operations.\n * Returns readFiles (files only read, not modified) and modifiedFiles.\n */\nexport function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {\n\tconst modified = new Set([...fileOps.edited, ...fileOps.written]);\n\tconst readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();\n\tconst modifiedFiles = [...modified].sort();\n\treturn { readFiles: readOnly, modifiedFiles };\n}\n\n/**\n * Format file operations as XML tags for summary.\n */\nexport function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {\n\tconst sections: string[] = [];\n\tif (readFiles.length > 0) {\n\t\tsections.push(`<read-files>\\n${readFiles.join(\"\\n\")}\\n</read-files>`);\n\t}\n\tif (modifiedFiles.length > 0) {\n\t\tsections.push(`<modified-files>\\n${modifiedFiles.join(\"\\n\")}\\n</modified-files>`);\n\t}\n\tif (sections.length === 0) return \"\";\n\treturn `\\n\\n${sections.join(\"\\n\\n\")}`;\n}\n\n// ============================================================================\n// Message Serialization\n// ============================================================================\n\n/** Maximum characters for a tool result in serialized summaries. */\nconst TOOL_RESULT_MAX_CHARS = 2000;\n\n/**\n * Truncate text to a maximum character length for summarization.\n * Keeps the beginning and appends a truncation marker.\n */\nfunction truncateForSummary(text: string, maxChars: number): string {\n\tif (text.length <= maxChars) return text;\n\tconst truncatedChars = text.length - maxChars;\n\treturn `${text.slice(0, maxChars)}\\n\\n[... ${truncatedChars} more characters truncated]`;\n}\n\n/**\n * Serialize LLM messages to text for summarization.\n * This prevents the model from treating it as a conversation to continue.\n * Call convertToLlm() first to handle custom message types.\n *\n * Tool results are truncated to keep the summarization request within\n * reasonable token budgets. Full content is not needed for summarization.\n */\nexport function serializeConversation(messages: Message[]): string {\n\tconst parts: string[] = [];\n\n\tfor (const msg of messages) {\n\t\tif (msg.role === \"user\") {\n\t\t\tconst content =\n\t\t\t\ttypeof msg.content === \"string\"\n\t\t\t\t\t? msg.content\n\t\t\t\t\t: msg.content\n\t\t\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t.join(\"\");\n\t\t\tif (content) parts.push(`[User]: ${content}`);\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst textParts: string[] = [];\n\t\t\tconst thinkingParts: string[] = [];\n\t\t\tconst toolCalls: string[] = [];\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\ttextParts.push(block.text);\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tthinkingParts.push(block.thinking);\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tconst args = block.arguments as Record<string, unknown>;\n\t\t\t\t\tconst argsStr = Object.entries(args)\n\t\t\t\t\t\t.map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n\t\t\t\t\t\t.join(\", \");\n\t\t\t\t\ttoolCalls.push(`${block.name}(${argsStr})`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (thinkingParts.length > 0) {\n\t\t\t\tparts.push(`[Assistant thinking]: ${thinkingParts.join(\"\\n\")}`);\n\t\t\t}\n\t\t\tif (textParts.length > 0) {\n\t\t\t\tparts.push(`[Assistant]: ${textParts.join(\"\\n\")}`);\n\t\t\t}\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tparts.push(`[Assistant tool calls]: ${toolCalls.join(\"; \")}`);\n\t\t\t}\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\tconst content = msg.content\n\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\");\n\t\t\tif (content) {\n\t\t\t\tparts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\n// ============================================================================\n// Summarization System Prompt\n// ============================================================================\n\nexport const SUMMARIZATION_SYSTEM_PROMPT = `You are a context checkpointer. Input: a serialized agent conversation. Output: ONLY the checkpoint, exactly in the format below. No preamble. No commentary. Same language as the user. Never include secrets/keys/tokens — write [REDACTED].\n\nRULES:\n- Recent turns weigh heaviest. Old turns contribute only rules, decisions, and file knowledge.\n- ## Active Task: the user's most recent UNFULFILLED input, near-verbatim. A question awaiting an answer IS an active task. If the user's last signal cancels earlier work (stop/undo/never mind), record the cancellation and DROP the cancelled work everywhere.\n- ### Mandatory Rules: every user prohibition (\"do not X\", \"never Y\", \"stop doing Z\") as one bullet each, imperative, with source turn if known. PRESERVE existing rules verbatim. A user-corrected mistake appears ONLY here as \"DO NOT <mistake>\" — the mistaken work itself must not survive.\n- ## Files: one line per file that matters — path — why it matters (modified/created/read).\n- ## Done: numbered caveman log — \"N. VERB target — outcome\". Exact paths, commands, line numbers, error strings.\n- Sections with nothing: write \"(none)\".\n\nEXAMPLE INPUT (excerpt):\n[user]: add retry to the fetcher, and do not touch the legacy client\n[assistant]: (edits src/fetcher.ts, adds retry loop)\n[tool write src/fetcher.ts]: ok\n[tool bash npm test]: 2 failed: fetcher.test.ts\n[assistant]: (tries wrapping legacy client instead)\n[user]: no — stop changing the legacy client, I said don't touch it. Fix the two tests instead.\n\nEXAMPLE OUTPUT:\n## Active Task\nUser: \"Fix the two failing tests\" (fetcher.test.ts) retry work continues, legacy-client changes cancelled.\n\n### Mandatory Rules\n- DO NOT touch the legacy client (user, twice)\n\n## Files\n- src/fetcher.ts retry loop added (modified)\n- test/fetcher.test.ts — 2 failing, current focus (read)\n\n## Done\n1. EDIT src/fetcher.ts — added retry loop\n2. TEST npm test — 2 failed: fetcher.test.ts\n\n## Constraints & Preferences\n(none)\n\n## Key Decisions\n(none)\n\n## Blocked / Open\n- 2 fetcher tests failing\n\n## Critical Context\n(none)\n\nNote what the example TEACHES (not just shows): the legacy-client wrapping attempt (cancelled work) appears nowhere except as the DO-NOT rule; the Active Task is the tail, near-verbatim; Done lines are caveman-format.`;\n"]}
@@ -147,7 +147,50 @@ export function serializeConversation(messages) {
147
147
  // ============================================================================
148
148
  // Summarization System Prompt
149
149
  // ============================================================================
150
- export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
150
+ export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context checkpointer. Input: a serialized agent conversation. Output: ONLY the checkpoint, exactly in the format below. No preamble. No commentary. Same language as the user. Never include secrets/keys/tokens — write [REDACTED].
151
151
 
152
- Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;
152
+ RULES:
153
+ - Recent turns weigh heaviest. Old turns contribute only rules, decisions, and file knowledge.
154
+ - ## Active Task: the user's most recent UNFULFILLED input, near-verbatim. A question awaiting an answer IS an active task. If the user's last signal cancels earlier work (stop/undo/never mind), record the cancellation and DROP the cancelled work everywhere.
155
+ - ### Mandatory Rules: every user prohibition ("do not X", "never Y", "stop doing Z") as one bullet each, imperative, with source turn if known. PRESERVE existing rules verbatim. A user-corrected mistake appears ONLY here as "DO NOT <mistake>" — the mistaken work itself must not survive.
156
+ - ## Files: one line per file that matters — path — why it matters (modified/created/read).
157
+ - ## Done: numbered caveman log — "N. VERB target — outcome". Exact paths, commands, line numbers, error strings.
158
+ - Sections with nothing: write "(none)".
159
+
160
+ EXAMPLE INPUT (excerpt):
161
+ [user]: add retry to the fetcher, and do not touch the legacy client
162
+ [assistant]: (edits src/fetcher.ts, adds retry loop)
163
+ [tool write src/fetcher.ts]: ok
164
+ [tool bash npm test]: 2 failed: fetcher.test.ts
165
+ [assistant]: (tries wrapping legacy client instead)
166
+ [user]: no — stop changing the legacy client, I said don't touch it. Fix the two tests instead.
167
+
168
+ EXAMPLE OUTPUT:
169
+ ## Active Task
170
+ User: "Fix the two failing tests" (fetcher.test.ts) — retry work continues, legacy-client changes cancelled.
171
+
172
+ ### Mandatory Rules
173
+ - DO NOT touch the legacy client (user, twice)
174
+
175
+ ## Files
176
+ - src/fetcher.ts — retry loop added (modified)
177
+ - test/fetcher.test.ts — 2 failing, current focus (read)
178
+
179
+ ## Done
180
+ 1. EDIT src/fetcher.ts — added retry loop
181
+ 2. TEST npm test — 2 failed: fetcher.test.ts
182
+
183
+ ## Constraints & Preferences
184
+ (none)
185
+
186
+ ## Key Decisions
187
+ (none)
188
+
189
+ ## Blocked / Open
190
+ - 2 fetcher tests failing
191
+
192
+ ## Critical Context
193
+ (none)
194
+
195
+ Note what the example TEACHES (not just shows): the legacy-client wrapping attempt (cancelled work) appears nowhere except as the DO-NOT rule; the Active Task is the tail, near-verbatim; Done lines are caveman-format.`;
153
196
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/compaction/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAeH,MAAM,UAAU,aAAa,GAAmB;IAC/C,OAAO;QACN,IAAI,EAAE,IAAI,GAAG,EAAE;QACf,OAAO,EAAE,IAAI,GAAG,EAAE;QAClB,MAAM,EAAE,IAAI,GAAG,EAAE;KACjB,CAAC;AAAA,CACF;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAqB,EAAE,OAAuB,EAAQ;IAC/F,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW;QAAE,OAAO;IACzC,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO;IAEvE,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QAC1D,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;YAAE,SAAS;QAC9D,IAAI,CAAC,CAAC,WAAW,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC;YAAE,SAAS;QAE5D,MAAM,IAAI,GAAG,KAAK,CAAC,SAAgD,CAAC;QACpE,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QACnE,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,MAAM;gBACV,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACvB,MAAM;YACP,KAAK,OAAO;gBACX,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBAC1B,MAAM;YACP,KAAK,MAAM;gBACV,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACzB,MAAM;QACR,CAAC;IACF,CAAC;AAAA,CACD;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAuB,EAAoD;IAC3G,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1E,MAAM,aAAa,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;AAAA,CAC9C;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAmB,EAAE,aAAuB,EAAU;IAC1F,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,iBAAiB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,QAAQ,CAAC,IAAI,CAAC,qBAAqB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IACnF,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;AAAA,CACtC;AAED,+EAA+E;AAC/E,wBAAwB;AACxB,+EAA+E;AAE/E,oEAAoE;AACpE,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC;;;GAGG;AACH,SAAS,kBAAkB,CAAC,IAAY,EAAE,QAAgB,EAAU;IACnE,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;IAC9C,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,YAAY,cAAc,6BAA6B,CAAC;AAAA,CACzF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAmB,EAAU;IAClE,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,MAAM,OAAO,GACZ,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;gBAC9B,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,GAAG,CAAC,OAAO;qBACV,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;qBACrE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;qBAClB,IAAI,CAAC,EAAE,CAAC,CAAC;YACd,IAAI,OAAO;gBAAE,KAAK,CAAC,IAAI,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC;QAC/C,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACrC,MAAM,SAAS,GAAa,EAAE,CAAC;YAC/B,MAAM,aAAa,GAAa,EAAE,CAAC;YACnC,MAAM,SAAS,GAAa,EAAE,CAAC;YAE/B,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;gBACjC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBAC3B,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC5B,CAAC;qBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;oBACtC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBACpC,CAAC;qBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;oBACtC,MAAM,IAAI,GAAG,KAAK,CAAC,SAAoC,CAAC;oBACxD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;yBAClC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;yBAC5C,IAAI,CAAC,IAAI,CAAC,CAAC;oBACb,SAAS,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,IAAI,OAAO,GAAG,CAAC,CAAC;gBAC7C,CAAC;YACF,CAAC;YAED,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC9B,KAAK,CAAC,IAAI,CAAC,yBAAyB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACjE,CAAC;YACD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1B,KAAK,CAAC,IAAI,CAAC,gBAAgB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACpD,CAAC;YACD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1B,KAAK,CAAC,IAAI,CAAC,2BAA2B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC/D,CAAC;QACF,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YACtC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO;iBACzB,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;iBACrE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;iBAClB,IAAI,CAAC,EAAE,CAAC,CAAC;YACX,IAAI,OAAO,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,kBAAkB,kBAAkB,CAAC,OAAO,EAAE,qBAAqB,CAAC,EAAE,CAAC,CAAC;YACpF,CAAC;QACF,CAAC;IACF,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAAA,CAC1B;AAED,+EAA+E;AAC/E,8BAA8B;AAC9B,+EAA+E;AAE/E,MAAM,CAAC,MAAM,2BAA2B,GAAG;;2HAEgF,CAAC","sourcesContent":["/**\n * Shared utilities for compaction and branch summarization.\n */\n\nimport type { Message } from \"@caupulican/pi-ai\";\nimport type { AgentMessage } from \"../types.ts\";\n\n// ============================================================================\n// File Operation Tracking\n// ============================================================================\n\nexport interface FileOperations {\n\tread: Set<string>;\n\twritten: Set<string>;\n\tedited: Set<string>;\n}\n\nexport function createFileOps(): FileOperations {\n\treturn {\n\t\tread: new Set(),\n\t\twritten: new Set(),\n\t\tedited: new Set(),\n\t};\n}\n\n/**\n * Extract file operations from tool calls in an assistant message.\n */\nexport function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {\n\tif (message.role !== \"assistant\") return;\n\tif (!(\"content\" in message) || !Array.isArray(message.content)) return;\n\n\tfor (const block of message.content) {\n\t\tif (typeof block !== \"object\" || block === null) continue;\n\t\tif (!(\"type\" in block) || block.type !== \"toolCall\") continue;\n\t\tif (!(\"arguments\" in block) || !(\"name\" in block)) continue;\n\n\t\tconst args = block.arguments as Record<string, unknown> | undefined;\n\t\tif (!args) continue;\n\n\t\tconst path = typeof args.path === \"string\" ? args.path : undefined;\n\t\tif (!path) continue;\n\n\t\tswitch (block.name) {\n\t\t\tcase \"read\":\n\t\t\t\tfileOps.read.add(path);\n\t\t\t\tbreak;\n\t\t\tcase \"write\":\n\t\t\t\tfileOps.written.add(path);\n\t\t\t\tbreak;\n\t\t\tcase \"edit\":\n\t\t\t\tfileOps.edited.add(path);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Compute final file lists from file operations.\n * Returns readFiles (files only read, not modified) and modifiedFiles.\n */\nexport function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {\n\tconst modified = new Set([...fileOps.edited, ...fileOps.written]);\n\tconst readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();\n\tconst modifiedFiles = [...modified].sort();\n\treturn { readFiles: readOnly, modifiedFiles };\n}\n\n/**\n * Format file operations as XML tags for summary.\n */\nexport function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {\n\tconst sections: string[] = [];\n\tif (readFiles.length > 0) {\n\t\tsections.push(`<read-files>\\n${readFiles.join(\"\\n\")}\\n</read-files>`);\n\t}\n\tif (modifiedFiles.length > 0) {\n\t\tsections.push(`<modified-files>\\n${modifiedFiles.join(\"\\n\")}\\n</modified-files>`);\n\t}\n\tif (sections.length === 0) return \"\";\n\treturn `\\n\\n${sections.join(\"\\n\\n\")}`;\n}\n\n// ============================================================================\n// Message Serialization\n// ============================================================================\n\n/** Maximum characters for a tool result in serialized summaries. */\nconst TOOL_RESULT_MAX_CHARS = 2000;\n\n/**\n * Truncate text to a maximum character length for summarization.\n * Keeps the beginning and appends a truncation marker.\n */\nfunction truncateForSummary(text: string, maxChars: number): string {\n\tif (text.length <= maxChars) return text;\n\tconst truncatedChars = text.length - maxChars;\n\treturn `${text.slice(0, maxChars)}\\n\\n[... ${truncatedChars} more characters truncated]`;\n}\n\n/**\n * Serialize LLM messages to text for summarization.\n * This prevents the model from treating it as a conversation to continue.\n * Call convertToLlm() first to handle custom message types.\n *\n * Tool results are truncated to keep the summarization request within\n * reasonable token budgets. Full content is not needed for summarization.\n */\nexport function serializeConversation(messages: Message[]): string {\n\tconst parts: string[] = [];\n\n\tfor (const msg of messages) {\n\t\tif (msg.role === \"user\") {\n\t\t\tconst content =\n\t\t\t\ttypeof msg.content === \"string\"\n\t\t\t\t\t? msg.content\n\t\t\t\t\t: msg.content\n\t\t\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t.join(\"\");\n\t\t\tif (content) parts.push(`[User]: ${content}`);\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst textParts: string[] = [];\n\t\t\tconst thinkingParts: string[] = [];\n\t\t\tconst toolCalls: string[] = [];\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\ttextParts.push(block.text);\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tthinkingParts.push(block.thinking);\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tconst args = block.arguments as Record<string, unknown>;\n\t\t\t\t\tconst argsStr = Object.entries(args)\n\t\t\t\t\t\t.map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n\t\t\t\t\t\t.join(\", \");\n\t\t\t\t\ttoolCalls.push(`${block.name}(${argsStr})`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (thinkingParts.length > 0) {\n\t\t\t\tparts.push(`[Assistant thinking]: ${thinkingParts.join(\"\\n\")}`);\n\t\t\t}\n\t\t\tif (textParts.length > 0) {\n\t\t\t\tparts.push(`[Assistant]: ${textParts.join(\"\\n\")}`);\n\t\t\t}\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tparts.push(`[Assistant tool calls]: ${toolCalls.join(\"; \")}`);\n\t\t\t}\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\tconst content = msg.content\n\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\");\n\t\t\tif (content) {\n\t\t\t\tparts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\n// ============================================================================\n// Summarization System Prompt\n// ============================================================================\n\nexport const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;\n"]}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/compaction/utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared utilities for compaction and branch summarization.\n */\n\nimport type { Message } from \"@caupulican/pi-ai\";\nimport type { AgentMessage } from \"../types.ts\";\n\n// ============================================================================\n// File Operation Tracking\n// ============================================================================\n\nexport interface FileOperations {\n\tread: Set<string>;\n\twritten: Set<string>;\n\tedited: Set<string>;\n}\n\nexport function createFileOps(): FileOperations {\n\treturn {\n\t\tread: new Set(),\n\t\twritten: new Set(),\n\t\tedited: new Set(),\n\t};\n}\n\n/**\n * Extract file operations from tool calls in an assistant message.\n */\nexport function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {\n\tif (message.role !== \"assistant\") return;\n\tif (!(\"content\" in message) || !Array.isArray(message.content)) return;\n\n\tfor (const block of message.content) {\n\t\tif (typeof block !== \"object\" || block === null) continue;\n\t\tif (!(\"type\" in block) || block.type !== \"toolCall\") continue;\n\t\tif (!(\"arguments\" in block) || !(\"name\" in block)) continue;\n\n\t\tconst args = block.arguments as Record<string, unknown> | undefined;\n\t\tif (!args) continue;\n\n\t\tconst path = typeof args.path === \"string\" ? args.path : undefined;\n\t\tif (!path) continue;\n\n\t\tswitch (block.name) {\n\t\t\tcase \"read\":\n\t\t\t\tfileOps.read.add(path);\n\t\t\t\tbreak;\n\t\t\tcase \"write\":\n\t\t\t\tfileOps.written.add(path);\n\t\t\t\tbreak;\n\t\t\tcase \"edit\":\n\t\t\t\tfileOps.edited.add(path);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/**\n * Compute final file lists from file operations.\n * Returns readFiles (files only read, not modified) and modifiedFiles.\n */\nexport function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {\n\tconst modified = new Set([...fileOps.edited, ...fileOps.written]);\n\tconst readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();\n\tconst modifiedFiles = [...modified].sort();\n\treturn { readFiles: readOnly, modifiedFiles };\n}\n\n/**\n * Format file operations as XML tags for summary.\n */\nexport function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {\n\tconst sections: string[] = [];\n\tif (readFiles.length > 0) {\n\t\tsections.push(`<read-files>\\n${readFiles.join(\"\\n\")}\\n</read-files>`);\n\t}\n\tif (modifiedFiles.length > 0) {\n\t\tsections.push(`<modified-files>\\n${modifiedFiles.join(\"\\n\")}\\n</modified-files>`);\n\t}\n\tif (sections.length === 0) return \"\";\n\treturn `\\n\\n${sections.join(\"\\n\\n\")}`;\n}\n\n// ============================================================================\n// Message Serialization\n// ============================================================================\n\n/** Maximum characters for a tool result in serialized summaries. */\nconst TOOL_RESULT_MAX_CHARS = 2000;\n\n/**\n * Truncate text to a maximum character length for summarization.\n * Keeps the beginning and appends a truncation marker.\n */\nfunction truncateForSummary(text: string, maxChars: number): string {\n\tif (text.length <= maxChars) return text;\n\tconst truncatedChars = text.length - maxChars;\n\treturn `${text.slice(0, maxChars)}\\n\\n[... ${truncatedChars} more characters truncated]`;\n}\n\n/**\n * Serialize LLM messages to text for summarization.\n * This prevents the model from treating it as a conversation to continue.\n * Call convertToLlm() first to handle custom message types.\n *\n * Tool results are truncated to keep the summarization request within\n * reasonable token budgets. Full content is not needed for summarization.\n */\nexport function serializeConversation(messages: Message[]): string {\n\tconst parts: string[] = [];\n\n\tfor (const msg of messages) {\n\t\tif (msg.role === \"user\") {\n\t\t\tconst content =\n\t\t\t\ttypeof msg.content === \"string\"\n\t\t\t\t\t? msg.content\n\t\t\t\t\t: msg.content\n\t\t\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t\t.join(\"\");\n\t\t\tif (content) parts.push(`[User]: ${content}`);\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst textParts: string[] = [];\n\t\t\tconst thinkingParts: string[] = [];\n\t\t\tconst toolCalls: string[] = [];\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\ttextParts.push(block.text);\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tthinkingParts.push(block.thinking);\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tconst args = block.arguments as Record<string, unknown>;\n\t\t\t\t\tconst argsStr = Object.entries(args)\n\t\t\t\t\t\t.map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n\t\t\t\t\t\t.join(\", \");\n\t\t\t\t\ttoolCalls.push(`${block.name}(${argsStr})`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (thinkingParts.length > 0) {\n\t\t\t\tparts.push(`[Assistant thinking]: ${thinkingParts.join(\"\\n\")}`);\n\t\t\t}\n\t\t\tif (textParts.length > 0) {\n\t\t\t\tparts.push(`[Assistant]: ${textParts.join(\"\\n\")}`);\n\t\t\t}\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tparts.push(`[Assistant tool calls]: ${toolCalls.join(\"; \")}`);\n\t\t\t}\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\tconst content = msg.content\n\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\");\n\t\t\tif (content) {\n\t\t\t\tparts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\n// ============================================================================\n// Summarization System Prompt\n// ============================================================================\n\nexport const SUMMARIZATION_SYSTEM_PROMPT = `You are a context checkpointer. Input: a serialized agent conversation. Output: ONLY the checkpoint, exactly in the format below. No preamble. No commentary. Same language as the user. Never include secrets/keys/tokens — write [REDACTED].\n\nRULES:\n- Recent turns weigh heaviest. Old turns contribute only rules, decisions, and file knowledge.\n- ## Active Task: the user's most recent UNFULFILLED input, near-verbatim. A question awaiting an answer IS an active task. If the user's last signal cancels earlier work (stop/undo/never mind), record the cancellation and DROP the cancelled work everywhere.\n- ### Mandatory Rules: every user prohibition (\"do not X\", \"never Y\", \"stop doing Z\") as one bullet each, imperative, with source turn if known. PRESERVE existing rules verbatim. A user-corrected mistake appears ONLY here as \"DO NOT <mistake>\" — the mistaken work itself must not survive.\n- ## Files: one line per file that matters — path — why it matters (modified/created/read).\n- ## Done: numbered caveman log — \"N. VERB target — outcome\". Exact paths, commands, line numbers, error strings.\n- Sections with nothing: write \"(none)\".\n\nEXAMPLE INPUT (excerpt):\n[user]: add retry to the fetcher, and do not touch the legacy client\n[assistant]: (edits src/fetcher.ts, adds retry loop)\n[tool write src/fetcher.ts]: ok\n[tool bash npm test]: 2 failed: fetcher.test.ts\n[assistant]: (tries wrapping legacy client instead)\n[user]: no — stop changing the legacy client, I said don't touch it. Fix the two tests instead.\n\nEXAMPLE OUTPUT:\n## Active Task\nUser: \"Fix the two failing tests\" (fetcher.test.ts) — retry work continues, legacy-client changes cancelled.\n\n### Mandatory Rules\n- DO NOT touch the legacy client (user, twice)\n\n## Files\n- src/fetcher.ts — retry loop added (modified)\n- test/fetcher.test.ts — 2 failing, current focus (read)\n\n## Done\n1. EDIT src/fetcher.ts — added retry loop\n2. TEST npm test — 2 failed: fetcher.test.ts\n\n## Constraints & Preferences\n(none)\n\n## Key Decisions\n(none)\n\n## Blocked / Open\n- 2 fetcher tests failing\n\n## Critical Context\n(none)\n\nNote what the example TEACHES (not just shows): the legacy-client wrapping attempt (cancelled work) appears nowhere except as the DO-NOT rule; the Active Task is the tail, near-verbatim; Done lines are caveman-format.`;\n"]}
@@ -0,0 +1,20 @@
1
+ import type { CompactionFacts } from "./extraction.ts";
2
+ export interface VerificationFailure {
3
+ check: string;
4
+ detail: string;
5
+ }
6
+ export interface VerificationReport {
7
+ ok: boolean;
8
+ failures: VerificationFailure[];
9
+ }
10
+ export declare const FILES_READ_RECALL_THRESHOLD = 0.8;
11
+ export declare const ACTIVE_TASK_CONTAINMENT_THRESHOLD = 0.9;
12
+ export declare const MANDATORY_RULES_RECALL_THRESHOLD = 0.7;
13
+ export declare const CANCELLED_WORK_DROPPED_THRESHOLD = 0.1;
14
+ export declare const ACTIONS_OVERLAP_THRESHOLD = 0.5;
15
+ export declare function verifySummary(summary: string, facts: CompactionFacts): VerificationReport;
16
+ export declare function buildRetryPrompt(report: VerificationReport, previousAttempt?: string): string;
17
+ export declare function tokenSet(text: string): Set<string>;
18
+ export declare function containment(needle: Set<string>, hay: Set<string>): number;
19
+ export declare function jaccard(a: Set<string>, b: Set<string>): number;
20
+ //# sourceMappingURL=verification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../src/compaction/verification.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,eAAO,MAAM,iCAAiC,MAAM,CAAC;AACrD,eAAO,MAAM,gCAAgC,MAAM,CAAC;AACpD,eAAO,MAAM,gCAAgC,MAAM,CAAC;AACpD,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAO7C,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,kBAAkB,CA0EzF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,CAI7F;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAQlD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAWzE;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAY9D","sourcesContent":["import type { CompactionFacts } from \"./extraction.ts\";\n\nexport interface VerificationFailure {\n\tcheck: string;\n\tdetail: string;\n}\n\nexport interface VerificationReport {\n\tok: boolean;\n\tfailures: VerificationFailure[];\n}\n\nexport const FILES_READ_RECALL_THRESHOLD = 0.8;\nexport const ACTIVE_TASK_CONTAINMENT_THRESHOLD = 0.9;\nexport const MANDATORY_RULES_RECALL_THRESHOLD = 0.7;\nexport const CANCELLED_WORK_DROPPED_THRESHOLD = 0.1;\nexport const ACTIONS_OVERLAP_THRESHOLD = 0.5;\n\nconst SECTION_FILES = \"files\";\nconst SECTION_DONE = \"done\";\nconst SECTION_ACTIVE_TASK = \"active task\";\nconst SECTION_MANDATORY_RULES = \"mandatory rules\";\n\nexport function verifySummary(summary: string, facts: CompactionFacts): VerificationReport {\n\tif (factsAreEmpty(facts)) {\n\t\treturn { ok: true, failures: [] };\n\t}\n\n\tconst sections = extractSections(summary);\n\tconst failures: VerificationFailure[] = [];\n\tconst filesSection = sections[SECTION_FILES] ?? \"\";\n\tconst doneSection = sections[SECTION_DONE] ?? \"\";\n\tconst activeTaskSection = sections[SECTION_ACTIVE_TASK] ?? \"\";\n\tconst mandatoryRulesSection = sections[SECTION_MANDATORY_RULES] ?? \"\";\n\n\tconst modifiedFiles = facts.files.filter((file) => file.kind !== \"read\");\n\tconst missingModifiedFiles = modifiedFiles.map((file) => file.path).filter((path) => !filesSection.includes(path));\n\tif (missingModifiedFiles.length > 0) {\n\t\tfailures.push({\n\t\t\tcheck: \"files-modified-recall\",\n\t\t\tdetail: `Missing modified/created files in ## Files: ${missingModifiedFiles.join(\", \")}`,\n\t\t});\n\t}\n\n\tconst readPaths = facts.files.filter((file) => file.kind === \"read\").map((file) => file.path);\n\tif (readPaths.length > 0) {\n\t\tconst score = containment(tokenSet(readPaths.join(\"\\n\")), tokenSet(filesSection));\n\t\tif (score < FILES_READ_RECALL_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"files-read-recall\",\n\t\t\t\tdetail: `Read file recall ${formatScore(score)} below ${FILES_READ_RECALL_THRESHOLD}`,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (facts.activeTaskSource) {\n\t\tconst score = containment(tokenSet(facts.activeTaskSource), tokenSet(activeTaskSection));\n\t\tif (score < ACTIVE_TASK_CONTAINMENT_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"active-task-containment\",\n\t\t\t\tdetail: `Active task containment ${formatScore(score)} below ${ACTIVE_TASK_CONTAINMENT_THRESHOLD}`,\n\t\t\t});\n\t\t}\n\t}\n\n\tfor (const prohibition of facts.prohibitions) {\n\t\tconst score = containment(tokenSet(prohibition), tokenSet(mandatoryRulesSection));\n\t\tif (score < MANDATORY_RULES_RECALL_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"mandatory-rules-recall\",\n\t\t\t\tdetail: `Missing mandatory rule: ${prohibition}`,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (facts.cancelledText) {\n\t\tconst summaryOutsideMandatoryRules = removeSection(summary, SECTION_MANDATORY_RULES);\n\t\tconst score = containment(tokenSet(facts.cancelledText), tokenSet(summaryOutsideMandatoryRules));\n\t\tif (score > CANCELLED_WORK_DROPPED_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"cancelled-work-dropped\",\n\t\t\t\tdetail: `Cancelled work leakage ${formatScore(score)} above ${CANCELLED_WORK_DROPPED_THRESHOLD}`,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (facts.actions.length > 0) {\n\t\tconst score = jaccard(tokenSet(facts.actions.join(\"\\n\")), tokenSet(doneSection));\n\t\tif (score < ACTIONS_OVERLAP_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"actions-overlap\",\n\t\t\t\tdetail: `Done/actions Jaccard ${formatScore(score)} below ${ACTIONS_OVERLAP_THRESHOLD}`,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn { ok: failures.length === 0, failures };\n}\n\nexport function buildRetryPrompt(report: VerificationReport, previousAttempt?: string): string {\n\tconst failures = report.failures.map((failure) => `${failure.check}: ${failure.detail}`).join(\"; \");\n\tconst previous = previousAttempt ? `\\n\\n<previous-attempt>\\n${previousAttempt}\\n</previous-attempt>` : \"\";\n\treturn `Your previous checkpoint failed verification: ${failures}. Fix ONLY these omissions.${previous}`;\n}\n\nexport function tokenSet(text: string): Set<string> {\n\treturn new Set(\n\t\ttext\n\t\t\t.toLowerCase()\n\t\t\t.split(/[^a-z0-9_./-]+/)\n\t\t\t.map((token) => token.trim())\n\t\t\t.filter((token) => token.length >= 3),\n\t);\n}\n\nexport function containment(needle: Set<string>, hay: Set<string>): number {\n\tif (needle.size === 0) {\n\t\treturn 1;\n\t}\n\tlet hits = 0;\n\tfor (const token of needle) {\n\t\tif (hay.has(token)) {\n\t\t\thits += 1;\n\t\t}\n\t}\n\treturn hits / needle.size;\n}\n\nexport function jaccard(a: Set<string>, b: Set<string>): number {\n\tif (a.size === 0 && b.size === 0) {\n\t\treturn 1;\n\t}\n\tlet intersection = 0;\n\tfor (const token of a) {\n\t\tif (b.has(token)) {\n\t\t\tintersection += 1;\n\t\t}\n\t}\n\tconst union = new Set([...a, ...b]).size;\n\treturn union === 0 ? 1 : intersection / union;\n}\n\nfunction factsAreEmpty(facts: CompactionFacts): boolean {\n\treturn (\n\t\tfacts.files.length === 0 &&\n\t\tfacts.actions.length === 0 &&\n\t\tfacts.prohibitions.length === 0 &&\n\t\tfacts.cancelledText === \"\" &&\n\t\tfacts.activeTaskSource === \"\"\n\t);\n}\n\nfunction extractSections(summary: string): Record<string, string> {\n\tconst sections: Record<string, string> = {};\n\tlet current: string | undefined;\n\tlet bucket: string[] = [];\n\n\tconst flush = (): void => {\n\t\tif (current) {\n\t\t\tsections[current] = bucket.join(\"\\n\").trim();\n\t\t}\n\t\tbucket = [];\n\t};\n\n\tfor (const line of summary.split(/\\r?\\n/)) {\n\t\tconst match = /^(?:##|###)\\s+(.+?)\\s*$/.exec(line);\n\t\tif (match) {\n\t\t\tflush();\n\t\t\tcurrent = normalizeHeading(match[1]);\n\t\t\tcontinue;\n\t\t}\n\t\tif (current) {\n\t\t\tbucket.push(line);\n\t\t}\n\t}\n\tflush();\n\treturn sections;\n}\n\nfunction removeSection(summary: string, heading: string): string {\n\tconst normalizedHeading = normalizeHeading(heading);\n\tconst kept: string[] = [];\n\tlet skipping = false;\n\tfor (const line of summary.split(/\\r?\\n/)) {\n\t\tconst match = /^(?:##|###)\\s+(.+?)\\s*$/.exec(line);\n\t\tif (match) {\n\t\t\tskipping = normalizeHeading(match[1]) === normalizedHeading;\n\t\t\tif (skipping) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!skipping) {\n\t\t\tkept.push(line);\n\t\t}\n\t}\n\treturn kept.join(\"\\n\");\n}\n\nfunction normalizeHeading(heading: string): string {\n\treturn heading.trim().toLowerCase();\n}\n\nfunction formatScore(score: number): string {\n\treturn score.toFixed(2);\n}\n"]}
@@ -0,0 +1,169 @@
1
+ export const FILES_READ_RECALL_THRESHOLD = 0.8;
2
+ export const ACTIVE_TASK_CONTAINMENT_THRESHOLD = 0.9;
3
+ export const MANDATORY_RULES_RECALL_THRESHOLD = 0.7;
4
+ export const CANCELLED_WORK_DROPPED_THRESHOLD = 0.1;
5
+ export const ACTIONS_OVERLAP_THRESHOLD = 0.5;
6
+ const SECTION_FILES = "files";
7
+ const SECTION_DONE = "done";
8
+ const SECTION_ACTIVE_TASK = "active task";
9
+ const SECTION_MANDATORY_RULES = "mandatory rules";
10
+ export function verifySummary(summary, facts) {
11
+ if (factsAreEmpty(facts)) {
12
+ return { ok: true, failures: [] };
13
+ }
14
+ const sections = extractSections(summary);
15
+ const failures = [];
16
+ const filesSection = sections[SECTION_FILES] ?? "";
17
+ const doneSection = sections[SECTION_DONE] ?? "";
18
+ const activeTaskSection = sections[SECTION_ACTIVE_TASK] ?? "";
19
+ const mandatoryRulesSection = sections[SECTION_MANDATORY_RULES] ?? "";
20
+ const modifiedFiles = facts.files.filter((file) => file.kind !== "read");
21
+ const missingModifiedFiles = modifiedFiles.map((file) => file.path).filter((path) => !filesSection.includes(path));
22
+ if (missingModifiedFiles.length > 0) {
23
+ failures.push({
24
+ check: "files-modified-recall",
25
+ detail: `Missing modified/created files in ## Files: ${missingModifiedFiles.join(", ")}`,
26
+ });
27
+ }
28
+ const readPaths = facts.files.filter((file) => file.kind === "read").map((file) => file.path);
29
+ if (readPaths.length > 0) {
30
+ const score = containment(tokenSet(readPaths.join("\n")), tokenSet(filesSection));
31
+ if (score < FILES_READ_RECALL_THRESHOLD) {
32
+ failures.push({
33
+ check: "files-read-recall",
34
+ detail: `Read file recall ${formatScore(score)} below ${FILES_READ_RECALL_THRESHOLD}`,
35
+ });
36
+ }
37
+ }
38
+ if (facts.activeTaskSource) {
39
+ const score = containment(tokenSet(facts.activeTaskSource), tokenSet(activeTaskSection));
40
+ if (score < ACTIVE_TASK_CONTAINMENT_THRESHOLD) {
41
+ failures.push({
42
+ check: "active-task-containment",
43
+ detail: `Active task containment ${formatScore(score)} below ${ACTIVE_TASK_CONTAINMENT_THRESHOLD}`,
44
+ });
45
+ }
46
+ }
47
+ for (const prohibition of facts.prohibitions) {
48
+ const score = containment(tokenSet(prohibition), tokenSet(mandatoryRulesSection));
49
+ if (score < MANDATORY_RULES_RECALL_THRESHOLD) {
50
+ failures.push({
51
+ check: "mandatory-rules-recall",
52
+ detail: `Missing mandatory rule: ${prohibition}`,
53
+ });
54
+ }
55
+ }
56
+ if (facts.cancelledText) {
57
+ const summaryOutsideMandatoryRules = removeSection(summary, SECTION_MANDATORY_RULES);
58
+ const score = containment(tokenSet(facts.cancelledText), tokenSet(summaryOutsideMandatoryRules));
59
+ if (score > CANCELLED_WORK_DROPPED_THRESHOLD) {
60
+ failures.push({
61
+ check: "cancelled-work-dropped",
62
+ detail: `Cancelled work leakage ${formatScore(score)} above ${CANCELLED_WORK_DROPPED_THRESHOLD}`,
63
+ });
64
+ }
65
+ }
66
+ if (facts.actions.length > 0) {
67
+ const score = jaccard(tokenSet(facts.actions.join("\n")), tokenSet(doneSection));
68
+ if (score < ACTIONS_OVERLAP_THRESHOLD) {
69
+ failures.push({
70
+ check: "actions-overlap",
71
+ detail: `Done/actions Jaccard ${formatScore(score)} below ${ACTIONS_OVERLAP_THRESHOLD}`,
72
+ });
73
+ }
74
+ }
75
+ return { ok: failures.length === 0, failures };
76
+ }
77
+ export function buildRetryPrompt(report, previousAttempt) {
78
+ const failures = report.failures.map((failure) => `${failure.check}: ${failure.detail}`).join("; ");
79
+ const previous = previousAttempt ? `\n\n<previous-attempt>\n${previousAttempt}\n</previous-attempt>` : "";
80
+ return `Your previous checkpoint failed verification: ${failures}. Fix ONLY these omissions.${previous}`;
81
+ }
82
+ export function tokenSet(text) {
83
+ return new Set(text
84
+ .toLowerCase()
85
+ .split(/[^a-z0-9_./-]+/)
86
+ .map((token) => token.trim())
87
+ .filter((token) => token.length >= 3));
88
+ }
89
+ export function containment(needle, hay) {
90
+ if (needle.size === 0) {
91
+ return 1;
92
+ }
93
+ let hits = 0;
94
+ for (const token of needle) {
95
+ if (hay.has(token)) {
96
+ hits += 1;
97
+ }
98
+ }
99
+ return hits / needle.size;
100
+ }
101
+ export function jaccard(a, b) {
102
+ if (a.size === 0 && b.size === 0) {
103
+ return 1;
104
+ }
105
+ let intersection = 0;
106
+ for (const token of a) {
107
+ if (b.has(token)) {
108
+ intersection += 1;
109
+ }
110
+ }
111
+ const union = new Set([...a, ...b]).size;
112
+ return union === 0 ? 1 : intersection / union;
113
+ }
114
+ function factsAreEmpty(facts) {
115
+ return (facts.files.length === 0 &&
116
+ facts.actions.length === 0 &&
117
+ facts.prohibitions.length === 0 &&
118
+ facts.cancelledText === "" &&
119
+ facts.activeTaskSource === "");
120
+ }
121
+ function extractSections(summary) {
122
+ const sections = {};
123
+ let current;
124
+ let bucket = [];
125
+ const flush = () => {
126
+ if (current) {
127
+ sections[current] = bucket.join("\n").trim();
128
+ }
129
+ bucket = [];
130
+ };
131
+ for (const line of summary.split(/\r?\n/)) {
132
+ const match = /^(?:##|###)\s+(.+?)\s*$/.exec(line);
133
+ if (match) {
134
+ flush();
135
+ current = normalizeHeading(match[1]);
136
+ continue;
137
+ }
138
+ if (current) {
139
+ bucket.push(line);
140
+ }
141
+ }
142
+ flush();
143
+ return sections;
144
+ }
145
+ function removeSection(summary, heading) {
146
+ const normalizedHeading = normalizeHeading(heading);
147
+ const kept = [];
148
+ let skipping = false;
149
+ for (const line of summary.split(/\r?\n/)) {
150
+ const match = /^(?:##|###)\s+(.+?)\s*$/.exec(line);
151
+ if (match) {
152
+ skipping = normalizeHeading(match[1]) === normalizedHeading;
153
+ if (skipping) {
154
+ continue;
155
+ }
156
+ }
157
+ if (!skipping) {
158
+ kept.push(line);
159
+ }
160
+ }
161
+ return kept.join("\n");
162
+ }
163
+ function normalizeHeading(heading) {
164
+ return heading.trim().toLowerCase();
165
+ }
166
+ function formatScore(score) {
167
+ return score.toFixed(2);
168
+ }
169
+ //# sourceMappingURL=verification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification.js","sourceRoot":"","sources":["../../src/compaction/verification.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CompactionFacts } from \"./extraction.ts\";\n\nexport interface VerificationFailure {\n\tcheck: string;\n\tdetail: string;\n}\n\nexport interface VerificationReport {\n\tok: boolean;\n\tfailures: VerificationFailure[];\n}\n\nexport const FILES_READ_RECALL_THRESHOLD = 0.8;\nexport const ACTIVE_TASK_CONTAINMENT_THRESHOLD = 0.9;\nexport const MANDATORY_RULES_RECALL_THRESHOLD = 0.7;\nexport const CANCELLED_WORK_DROPPED_THRESHOLD = 0.1;\nexport const ACTIONS_OVERLAP_THRESHOLD = 0.5;\n\nconst SECTION_FILES = \"files\";\nconst SECTION_DONE = \"done\";\nconst SECTION_ACTIVE_TASK = \"active task\";\nconst SECTION_MANDATORY_RULES = \"mandatory rules\";\n\nexport function verifySummary(summary: string, facts: CompactionFacts): VerificationReport {\n\tif (factsAreEmpty(facts)) {\n\t\treturn { ok: true, failures: [] };\n\t}\n\n\tconst sections = extractSections(summary);\n\tconst failures: VerificationFailure[] = [];\n\tconst filesSection = sections[SECTION_FILES] ?? \"\";\n\tconst doneSection = sections[SECTION_DONE] ?? \"\";\n\tconst activeTaskSection = sections[SECTION_ACTIVE_TASK] ?? \"\";\n\tconst mandatoryRulesSection = sections[SECTION_MANDATORY_RULES] ?? \"\";\n\n\tconst modifiedFiles = facts.files.filter((file) => file.kind !== \"read\");\n\tconst missingModifiedFiles = modifiedFiles.map((file) => file.path).filter((path) => !filesSection.includes(path));\n\tif (missingModifiedFiles.length > 0) {\n\t\tfailures.push({\n\t\t\tcheck: \"files-modified-recall\",\n\t\t\tdetail: `Missing modified/created files in ## Files: ${missingModifiedFiles.join(\", \")}`,\n\t\t});\n\t}\n\n\tconst readPaths = facts.files.filter((file) => file.kind === \"read\").map((file) => file.path);\n\tif (readPaths.length > 0) {\n\t\tconst score = containment(tokenSet(readPaths.join(\"\\n\")), tokenSet(filesSection));\n\t\tif (score < FILES_READ_RECALL_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"files-read-recall\",\n\t\t\t\tdetail: `Read file recall ${formatScore(score)} below ${FILES_READ_RECALL_THRESHOLD}`,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (facts.activeTaskSource) {\n\t\tconst score = containment(tokenSet(facts.activeTaskSource), tokenSet(activeTaskSection));\n\t\tif (score < ACTIVE_TASK_CONTAINMENT_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"active-task-containment\",\n\t\t\t\tdetail: `Active task containment ${formatScore(score)} below ${ACTIVE_TASK_CONTAINMENT_THRESHOLD}`,\n\t\t\t});\n\t\t}\n\t}\n\n\tfor (const prohibition of facts.prohibitions) {\n\t\tconst score = containment(tokenSet(prohibition), tokenSet(mandatoryRulesSection));\n\t\tif (score < MANDATORY_RULES_RECALL_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"mandatory-rules-recall\",\n\t\t\t\tdetail: `Missing mandatory rule: ${prohibition}`,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (facts.cancelledText) {\n\t\tconst summaryOutsideMandatoryRules = removeSection(summary, SECTION_MANDATORY_RULES);\n\t\tconst score = containment(tokenSet(facts.cancelledText), tokenSet(summaryOutsideMandatoryRules));\n\t\tif (score > CANCELLED_WORK_DROPPED_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"cancelled-work-dropped\",\n\t\t\t\tdetail: `Cancelled work leakage ${formatScore(score)} above ${CANCELLED_WORK_DROPPED_THRESHOLD}`,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (facts.actions.length > 0) {\n\t\tconst score = jaccard(tokenSet(facts.actions.join(\"\\n\")), tokenSet(doneSection));\n\t\tif (score < ACTIONS_OVERLAP_THRESHOLD) {\n\t\t\tfailures.push({\n\t\t\t\tcheck: \"actions-overlap\",\n\t\t\t\tdetail: `Done/actions Jaccard ${formatScore(score)} below ${ACTIONS_OVERLAP_THRESHOLD}`,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn { ok: failures.length === 0, failures };\n}\n\nexport function buildRetryPrompt(report: VerificationReport, previousAttempt?: string): string {\n\tconst failures = report.failures.map((failure) => `${failure.check}: ${failure.detail}`).join(\"; \");\n\tconst previous = previousAttempt ? `\\n\\n<previous-attempt>\\n${previousAttempt}\\n</previous-attempt>` : \"\";\n\treturn `Your previous checkpoint failed verification: ${failures}. Fix ONLY these omissions.${previous}`;\n}\n\nexport function tokenSet(text: string): Set<string> {\n\treturn new Set(\n\t\ttext\n\t\t\t.toLowerCase()\n\t\t\t.split(/[^a-z0-9_./-]+/)\n\t\t\t.map((token) => token.trim())\n\t\t\t.filter((token) => token.length >= 3),\n\t);\n}\n\nexport function containment(needle: Set<string>, hay: Set<string>): number {\n\tif (needle.size === 0) {\n\t\treturn 1;\n\t}\n\tlet hits = 0;\n\tfor (const token of needle) {\n\t\tif (hay.has(token)) {\n\t\t\thits += 1;\n\t\t}\n\t}\n\treturn hits / needle.size;\n}\n\nexport function jaccard(a: Set<string>, b: Set<string>): number {\n\tif (a.size === 0 && b.size === 0) {\n\t\treturn 1;\n\t}\n\tlet intersection = 0;\n\tfor (const token of a) {\n\t\tif (b.has(token)) {\n\t\t\tintersection += 1;\n\t\t}\n\t}\n\tconst union = new Set([...a, ...b]).size;\n\treturn union === 0 ? 1 : intersection / union;\n}\n\nfunction factsAreEmpty(facts: CompactionFacts): boolean {\n\treturn (\n\t\tfacts.files.length === 0 &&\n\t\tfacts.actions.length === 0 &&\n\t\tfacts.prohibitions.length === 0 &&\n\t\tfacts.cancelledText === \"\" &&\n\t\tfacts.activeTaskSource === \"\"\n\t);\n}\n\nfunction extractSections(summary: string): Record<string, string> {\n\tconst sections: Record<string, string> = {};\n\tlet current: string | undefined;\n\tlet bucket: string[] = [];\n\n\tconst flush = (): void => {\n\t\tif (current) {\n\t\t\tsections[current] = bucket.join(\"\\n\").trim();\n\t\t}\n\t\tbucket = [];\n\t};\n\n\tfor (const line of summary.split(/\\r?\\n/)) {\n\t\tconst match = /^(?:##|###)\\s+(.+?)\\s*$/.exec(line);\n\t\tif (match) {\n\t\t\tflush();\n\t\t\tcurrent = normalizeHeading(match[1]);\n\t\t\tcontinue;\n\t\t}\n\t\tif (current) {\n\t\t\tbucket.push(line);\n\t\t}\n\t}\n\tflush();\n\treturn sections;\n}\n\nfunction removeSection(summary: string, heading: string): string {\n\tconst normalizedHeading = normalizeHeading(heading);\n\tconst kept: string[] = [];\n\tlet skipping = false;\n\tfor (const line of summary.split(/\\r?\\n/)) {\n\t\tconst match = /^(?:##|###)\\s+(.+?)\\s*$/.exec(line);\n\t\tif (match) {\n\t\t\tskipping = normalizeHeading(match[1]) === normalizedHeading;\n\t\t\tif (skipping) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!skipping) {\n\t\t\tkept.push(line);\n\t\t}\n\t}\n\treturn kept.join(\"\\n\");\n}\n\nfunction normalizeHeading(heading: string): string {\n\treturn heading.trim().toLowerCase();\n}\n\nfunction formatScore(score: number): string {\n\treturn score.toFixed(2);\n}\n"]}
@@ -28,6 +28,8 @@ export interface ClassifyFailureInput {
28
28
  contextOverflow?: boolean;
29
29
  /** True when the failure came from an intentional abort (stopReason "aborted"). */
30
30
  aborted?: boolean;
31
+ /** Provider id; provider-specific signatures are checked before generic patterns. */
32
+ provider?: string;
31
33
  }
32
34
  export declare function classifyFailure(input: ClassifyFailureInput): ClassifiedError;
33
35
  //# sourceMappingURL=classifier.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../src/reliability/classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,aAAa,GACtB,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,SAAS,GACT,cAAc,GACd,kBAAkB,GAClB,MAAM,GACN,kBAAkB,GAClB,SAAS,GACT,SAAS,CAAC;AAEb,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,sBAAsB,EAAE,OAAO,CAAC;IAChC,cAAc,EAAE,OAAO,CAAC;IACxB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAwBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,eAAe,CAsC5E","sourcesContent":["/**\n * Pure provider-failure classifier.\n *\n * One classification produces four independent action booleans (hermes-derived design):\n * the retry loop, compaction, credential rotation, and provider failover each read their\n * own flag, so one pipeline can route a 429 to rotation, an overflow to compaction, and a\n * billing error to failover without re-parsing error text at each site.\n *\n * Pattern sources: AgentSession._isRetryableError / _isNonRetryableProviderLimitError\n * (the live, battle-tested regexes), split by reason so each maps to distinct actions.\n * Detection of context overflow stays in @caupulican/pi-ai (needs model state); hosts pass\n * `contextOverflow` in.\n */\n\nexport type FailureReason =\n\t| \"overloaded\"\n\t| \"rate_limit\"\n\t| \"server_error\"\n\t| \"network\"\n\t| \"stream_stall\"\n\t| \"context_overflow\"\n\t| \"auth\"\n\t| \"billing_or_quota\"\n\t| \"aborted\"\n\t| \"unknown\";\n\nexport interface ClassifiedError {\n\treason: FailureReason;\n\tretryable: boolean;\n\tshouldCompact: boolean;\n\tshouldRotateCredential: boolean;\n\tshouldFallback: boolean;\n\t/** Provider-suggested delay parsed from the message, capped by the retry policy at use site. */\n\tretryAfterMs?: number;\n\tmessage: string;\n}\n\nexport interface ClassifyFailureInput {\n\tmessage: string;\n\t/** Host-computed via pi-ai isContextOverflow(message, contextWindow). */\n\tcontextOverflow?: boolean;\n\t/** True when the failure came from an intentional abort (stopReason \"aborted\"). */\n\taborted?: boolean;\n}\n\nconst BILLING_OR_QUOTA =\n\t/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i;\nconst AUTH = /\\b401\\b|unauthorized|invalid.?api.?key|authentication.?error|forbidden|permission.?denied/i;\nconst RATE_LIMIT = /rate.?limit|too many requests|429/i;\nconst OVERLOADED = /overloaded/i;\nconst STREAM_STALL = /stream stalled|ended without|stream ended before message_stop|reset before headers/i;\nconst SERVER_ERROR =\n\t/500|502|503|504|service.?unavailable|server.?error|internal.?error|provider.?returned.?error|upstream.?connect|http2 request did not get a response|retry delay/i;\nconst NETWORK =\n\t/network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|socket hang up|timed? out|timeout|terminated/i;\n\nconst RETRY_AFTER_S = /retry.?(?:after|in)\\s+(\\d+(?:\\.\\d+)?)\\s*s\\b|\"retryDelay\"\\s*:\\s*\"(\\d+(?:\\.\\d+)?)s\"/i;\nconst RETRY_AFTER_MS = /retry.?(?:after|in)\\s+(\\d+)\\s*ms\\b/i;\n\nfunction parseRetryAfterMs(message: string): number | undefined {\n\tconst ms = RETRY_AFTER_MS.exec(message);\n\tif (ms) return Number(ms[1]);\n\tconst s = RETRY_AFTER_S.exec(message);\n\tif (s) return Math.round(Number(s[1] ?? s[2]) * 1000);\n\treturn undefined;\n}\n\nexport function classifyFailure(input: ClassifyFailureInput): ClassifiedError {\n\tconst message = input.message;\n\tconst retryAfterMs = parseRetryAfterMs(message);\n\n\tconst base = {\n\t\tretryable: false,\n\t\tshouldCompact: false,\n\t\tshouldRotateCredential: false,\n\t\tshouldFallback: false,\n\t\tmessage,\n\t};\n\n\tconst withRetry = <T extends { reason: FailureReason }>(obj: T): T | (T & { retryAfterMs: number }) =>\n\t\tretryAfterMs !== undefined ? { ...obj, retryAfterMs } : obj;\n\n\tif (input.aborted) return withRetry({ ...base, reason: \"aborted\" });\n\tif (input.contextOverflow) return withRetry({ ...base, reason: \"context_overflow\", shouldCompact: true });\n\tif (BILLING_OR_QUOTA.test(message)) return withRetry({ ...base, reason: \"billing_or_quota\", shouldFallback: true });\n\tif (AUTH.test(message))\n\t\treturn withRetry({ ...base, reason: \"auth\", shouldRotateCredential: true, shouldFallback: true });\n\n\tconst isRateLimit = RATE_LIMIT.test(message);\n\tif (isRateLimit || OVERLOADED.test(message)) {\n\t\treturn withRetry({\n\t\t\t...base,\n\t\t\treason: isRateLimit ? \"rate_limit\" : \"overloaded\",\n\t\t\tretryable: true,\n\t\t\tshouldRotateCredential: true,\n\t\t\tshouldFallback: true,\n\t\t});\n\t}\n\tif (STREAM_STALL.test(message))\n\t\treturn withRetry({ ...base, reason: \"stream_stall\", retryable: true, shouldFallback: true });\n\tif (SERVER_ERROR.test(message))\n\t\treturn withRetry({ ...base, reason: \"server_error\", retryable: true, shouldFallback: true });\n\tif (NETWORK.test(message)) return withRetry({ ...base, reason: \"network\", retryable: true, shouldFallback: true });\n\n\treturn withRetry({ ...base, reason: \"unknown\" });\n}\n"]}
1
+ {"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../src/reliability/classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,MAAM,MAAM,aAAa,GACtB,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,SAAS,GACT,cAAc,GACd,kBAAkB,GAClB,MAAM,GACN,kBAAkB,GAClB,SAAS,GACT,SAAS,CAAC;AAEb,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,sBAAsB,EAAE,OAAO,CAAC;IAChC,cAAc,EAAE,OAAO,CAAC;IACxB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,eAAe,CAuD5E","sourcesContent":["/**\n * Pure provider-failure classifier.\n *\n * One classification produces four independent action booleans (hermes-derived design):\n * the retry loop, compaction, credential rotation, and provider failover each read their\n * own flag, so one pipeline can route a 429 to rotation, an overflow to compaction, and a\n * billing error to failover without re-parsing error text at each site.\n *\n * Pattern sources: AgentSession._isRetryableError / _isNonRetryableProviderLimitError\n * (the live, battle-tested regexes), split by reason so each maps to distinct actions.\n * Detection of context overflow stays in @caupulican/pi-ai (needs model state); hosts pass\n * `contextOverflow` in.\n */\n\nimport { PROVIDER_FAILURE_SIGNATURES } from \"./provider-signatures.ts\";\n\nexport type FailureReason =\n\t| \"overloaded\"\n\t| \"rate_limit\"\n\t| \"server_error\"\n\t| \"network\"\n\t| \"stream_stall\"\n\t| \"context_overflow\"\n\t| \"auth\"\n\t| \"billing_or_quota\"\n\t| \"aborted\"\n\t| \"unknown\";\n\nexport interface ClassifiedError {\n\treason: FailureReason;\n\tretryable: boolean;\n\tshouldCompact: boolean;\n\tshouldRotateCredential: boolean;\n\tshouldFallback: boolean;\n\t/** Provider-suggested delay parsed from the message, capped by the retry policy at use site. */\n\tretryAfterMs?: number;\n\tmessage: string;\n}\n\nexport interface ClassifyFailureInput {\n\tmessage: string;\n\t/** Host-computed via pi-ai isContextOverflow(message, contextWindow). */\n\tcontextOverflow?: boolean;\n\t/** True when the failure came from an intentional abort (stopReason \"aborted\"). */\n\taborted?: boolean;\n\t/** Provider id; provider-specific signatures are checked before generic patterns. */\n\tprovider?: string;\n}\n\nconst BILLING_OR_QUOTA =\n\t/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing|usage.?limit(?:s)?\\s*(?:reached|exceeded|hit)|usage_limit_reached|hit your usage limit|hit your ChatGPT usage limit/i;\nconst AUTH = /\\b401\\b|unauthorized|invalid.?api.?key|authentication.?error|forbidden|permission.?denied/i;\nconst RATE_LIMIT = /rate.?limit|too many requests|429/i;\nconst OVERLOADED = /overloaded/i;\nconst STREAM_STALL = /stream stalled|ended without|stream ended before message_stop|reset before headers/i;\nconst SERVER_ERROR =\n\t/500|502|503|504|service.?unavailable|server.?error|internal.?error|provider.?returned.?error|upstream.?connect|http2 request did not get a response|retry delay/i;\nconst NETWORK =\n\t/network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|socket hang up|timed? out|timeout|terminated/i;\n\nconst RETRY_AFTER_S = /retry.?(?:after|in)\\s+(\\d+(?:\\.\\d+)?)\\s*s\\b|\"retryDelay\"\\s*:\\s*\"(\\d+(?:\\.\\d+)?)s\"/i;\nconst RETRY_AFTER_MS = /retry.?(?:after|in)\\s+(\\d+)\\s*ms\\b/i;\n\nfunction parseRetryAfterMs(message: string): number | undefined {\n\tconst ms = RETRY_AFTER_MS.exec(message);\n\tif (ms) return Number(ms[1]);\n\tconst s = RETRY_AFTER_S.exec(message);\n\tif (s) return Math.round(Number(s[1] ?? s[2]) * 1000);\n\treturn undefined;\n}\n\nexport function classifyFailure(input: ClassifyFailureInput): ClassifiedError {\n\tconst message = input.message;\n\tconst retryAfterMs = parseRetryAfterMs(message);\n\n\tconst base = {\n\t\tretryable: false,\n\t\tshouldCompact: false,\n\t\tshouldRotateCredential: false,\n\t\tshouldFallback: false,\n\t\tmessage,\n\t};\n\n\tconst withRetry = <T extends { reason: FailureReason }>(obj: T): T | (T & { retryAfterMs: number }) =>\n\t\tretryAfterMs !== undefined ? { ...obj, retryAfterMs } : obj;\n\n\tif (input.aborted) return withRetry({ ...base, reason: \"aborted\" });\n\tif (input.contextOverflow) return withRetry({ ...base, reason: \"context_overflow\", shouldCompact: true });\n\tconst providerSignatures = input.provider ? (PROVIDER_FAILURE_SIGNATURES[input.provider] ?? []) : [];\n\tfor (const signature of providerSignatures) {\n\t\tif (signature.pattern.test(message)) {\n\t\t\treturn withRetry({\n\t\t\t\t...base,\n\t\t\t\treason: signature.reason,\n\t\t\t\tshouldFallback: signature.reason === \"billing_or_quota\" || signature.reason === \"auth\",\n\t\t\t\tshouldRotateCredential: signature.reason === \"auth\",\n\t\t\t\tretryable:\n\t\t\t\t\tsignature.reason === \"rate_limit\" ||\n\t\t\t\t\tsignature.reason === \"overloaded\" ||\n\t\t\t\t\tsignature.reason === \"server_error\" ||\n\t\t\t\t\tsignature.reason === \"network\" ||\n\t\t\t\t\tsignature.reason === \"stream_stall\",\n\t\t\t});\n\t\t}\n\t}\n\tif (BILLING_OR_QUOTA.test(message)) return withRetry({ ...base, reason: \"billing_or_quota\", shouldFallback: true });\n\tif (AUTH.test(message))\n\t\treturn withRetry({ ...base, reason: \"auth\", shouldRotateCredential: true, shouldFallback: true });\n\n\tconst isRateLimit = RATE_LIMIT.test(message);\n\tif (isRateLimit || OVERLOADED.test(message)) {\n\t\treturn withRetry({\n\t\t\t...base,\n\t\t\treason: isRateLimit ? \"rate_limit\" : \"overloaded\",\n\t\t\tretryable: true,\n\t\t\tshouldRotateCredential: true,\n\t\t\tshouldFallback: true,\n\t\t});\n\t}\n\tif (STREAM_STALL.test(message))\n\t\treturn withRetry({ ...base, reason: \"stream_stall\", retryable: true, shouldFallback: true });\n\tif (SERVER_ERROR.test(message))\n\t\treturn withRetry({ ...base, reason: \"server_error\", retryable: true, shouldFallback: true });\n\tif (NETWORK.test(message)) return withRetry({ ...base, reason: \"network\", retryable: true, shouldFallback: true });\n\n\treturn withRetry({ ...base, reason: \"unknown\" });\n}\n"]}
@@ -11,7 +11,8 @@
11
11
  * Detection of context overflow stays in @caupulican/pi-ai (needs model state); hosts pass
12
12
  * `contextOverflow` in.
13
13
  */
14
- const BILLING_OR_QUOTA = /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i;
14
+ import { PROVIDER_FAILURE_SIGNATURES } from "./provider-signatures.js";
15
+ const BILLING_OR_QUOTA = /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing|usage.?limit(?:s)?\s*(?:reached|exceeded|hit)|usage_limit_reached|hit your usage limit|hit your ChatGPT usage limit/i;
15
16
  const AUTH = /\b401\b|unauthorized|invalid.?api.?key|authentication.?error|forbidden|permission.?denied/i;
16
17
  const RATE_LIMIT = /rate.?limit|too many requests|429/i;
17
18
  const OVERLOADED = /overloaded/i;
@@ -44,6 +45,22 @@ export function classifyFailure(input) {
44
45
  return withRetry({ ...base, reason: "aborted" });
45
46
  if (input.contextOverflow)
46
47
  return withRetry({ ...base, reason: "context_overflow", shouldCompact: true });
48
+ const providerSignatures = input.provider ? (PROVIDER_FAILURE_SIGNATURES[input.provider] ?? []) : [];
49
+ for (const signature of providerSignatures) {
50
+ if (signature.pattern.test(message)) {
51
+ return withRetry({
52
+ ...base,
53
+ reason: signature.reason,
54
+ shouldFallback: signature.reason === "billing_or_quota" || signature.reason === "auth",
55
+ shouldRotateCredential: signature.reason === "auth",
56
+ retryable: signature.reason === "rate_limit" ||
57
+ signature.reason === "overloaded" ||
58
+ signature.reason === "server_error" ||
59
+ signature.reason === "network" ||
60
+ signature.reason === "stream_stall",
61
+ });
62
+ }
63
+ }
47
64
  if (BILLING_OR_QUOTA.test(message))
48
65
  return withRetry({ ...base, reason: "billing_or_quota", shouldFallback: true });
49
66
  if (AUTH.test(message))