@caupulican/pi-adaptative 0.81.3 → 0.81.4

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 (73) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/core/agent-session.d.ts +1 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +23 -17
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/billing-failover-controller.d.ts +7 -0
  7. package/dist/core/billing-failover-controller.d.ts.map +1 -1
  8. package/dist/core/billing-failover-controller.js +6 -0
  9. package/dist/core/billing-failover-controller.js.map +1 -1
  10. package/dist/core/compaction-support.d.ts +10 -1
  11. package/dist/core/compaction-support.d.ts.map +1 -1
  12. package/dist/core/compaction-support.js +68 -9
  13. package/dist/core/compaction-support.js.map +1 -1
  14. package/dist/core/export-html/index.d.ts.map +1 -1
  15. package/dist/core/export-html/index.js +9 -9
  16. package/dist/core/export-html/index.js.map +1 -1
  17. package/dist/core/failure-corpus.d.ts +49 -0
  18. package/dist/core/failure-corpus.d.ts.map +1 -0
  19. package/dist/core/failure-corpus.js +61 -0
  20. package/dist/core/failure-corpus.js.map +1 -0
  21. package/dist/core/model-router/fitness-gate.d.ts +5 -3
  22. package/dist/core/model-router/fitness-gate.d.ts.map +1 -1
  23. package/dist/core/model-router/fitness-gate.js +1 -0
  24. package/dist/core/model-router/fitness-gate.js.map +1 -1
  25. package/dist/core/model-router/status.d.ts +4 -0
  26. package/dist/core/model-router/status.d.ts.map +1 -1
  27. package/dist/core/model-router/status.js +4 -0
  28. package/dist/core/model-router/status.js.map +1 -1
  29. package/dist/core/model-router-controller.d.ts.map +1 -1
  30. package/dist/core/model-router-controller.js +2 -0
  31. package/dist/core/model-router-controller.js.map +1 -1
  32. package/dist/core/models/default-model-suggestions.d.ts.map +1 -1
  33. package/dist/core/models/default-model-suggestions.js +18 -0
  34. package/dist/core/models/default-model-suggestions.js.map +1 -1
  35. package/dist/core/settings-manager.d.ts +1 -0
  36. package/dist/core/settings-manager.d.ts.map +1 -1
  37. package/dist/core/settings-manager.js +12 -0
  38. package/dist/core/settings-manager.js.map +1 -1
  39. package/dist/core/slash-commands.d.ts.map +1 -1
  40. package/dist/core/slash-commands.js +4 -0
  41. package/dist/core/slash-commands.js.map +1 -1
  42. package/dist/core/tools/bash.d.ts.map +1 -1
  43. package/dist/core/tools/bash.js +6 -8
  44. package/dist/core/tools/bash.js.map +1 -1
  45. package/dist/modes/interactive/components/fitness-role-selector.d.ts +1 -1
  46. package/dist/modes/interactive/components/fitness-role-selector.d.ts.map +1 -1
  47. package/dist/modes/interactive/components/fitness-role-selector.js +5 -0
  48. package/dist/modes/interactive/components/fitness-role-selector.js.map +1 -1
  49. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  50. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.js +13 -0
  52. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
  54. package/dist/modes/interactive/local-model-commands.js +14 -0
  55. package/dist/modes/interactive/local-model-commands.js.map +1 -1
  56. package/dist/modes/interactive/session-flow-commands.d.ts +16 -0
  57. package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
  58. package/dist/modes/interactive/session-flow-commands.js +32 -0
  59. package/dist/modes/interactive/session-flow-commands.js.map +1 -1
  60. package/docs/bug-ledger.md +113 -0
  61. package/docs/models.md +9 -0
  62. package/docs/providers.md +12 -0
  63. package/docs/scout.md +4 -2
  64. package/docs/settings.md +2 -0
  65. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  66. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  67. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  68. package/examples/extensions/sandbox/package-lock.json +2 -2
  69. package/examples/extensions/sandbox/package.json +1 -1
  70. package/examples/extensions/with-deps/package-lock.json +2 -2
  71. package/examples/extensions/with-deps/package.json +1 -1
  72. package/npm-shrinkwrap.json +12 -12
  73. package/package.json +4 -4
@@ -1,4 +1,5 @@
1
1
  import type { Agent } from "@caupulican/pi-agent-core";
2
+ import { classifyFailure } from "@caupulican/pi-agent-core";
2
3
  import type { AssistantMessage } from "@caupulican/pi-ai";
3
4
  import type { ModelRegistry } from "./model-registry.ts";
4
5
  import type { ModelRouterFailoverStatus } from "./model-router/status.ts";
@@ -17,6 +18,12 @@ export interface BillingFailoverControllerDeps {
17
18
  }): void;
18
19
  exhausted: ExhaustedProviderRegistry;
19
20
  subscriptionHop?: boolean;
21
+ recordFailure?(args: {
22
+ provider?: string;
23
+ modelId?: string;
24
+ message: string;
25
+ classified: ReturnType<typeof classifyFailure>;
26
+ }): void;
20
27
  }
21
28
  export declare class BillingFailoverController {
22
29
  private readonly deps;
@@ -1 +1 @@
1
- {"version":3,"file":"billing-failover-controller.d.ts","sourceRoot":"","sources":["../../src/core/billing-failover-controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAEvD,OAAO,KAAK,EAAO,gBAAgB,EAAS,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAM1E,qBAAa,yBAAyB;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IAEnE,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAE/C;IAED,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMhC;IAED,QAAQ,IAAI,MAAM,EAAE,CAEnB;CACD;AAED,MAAM,WAAW,6BAA6B;IAC7C,KAAK,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxD,SAAS,EAAE,yBAAyB,CAAC;IACrC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,qBAAa,yBAAyB;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgC;IACrD,OAAO,CAAC,UAAU,CAAqB;IAEvC,YAAY,IAAI,EAAE,6BAA6B,EAE9C;IAED,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhC;IAED,iBAAiB,IAAI,MAAM,EAAE,CAE5B;IAED,aAAa,IAAI,MAAM,GAAG,SAAS,CAElC;IAED,SAAS,IAAI,yBAAyB,CAErC;IAEK,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBtE;CACD","sourcesContent":["import type { Agent } from \"@caupulican/pi-agent-core\";\nimport { classifyFailure } from \"@caupulican/pi-agent-core\";\nimport type { Api, AssistantMessage, Model } from \"@caupulican/pi-ai\";\nimport { decideBillingFailover } from \"./billing-failover.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport type { ModelRouterFailoverStatus } from \"./model-router/status.ts\";\n\nconst DEFAULT_MODEL_PER_PROVIDER: Record<string, string> = {\n\t\"openai-codex\": \"gpt-5.5\",\n};\n\nexport class ExhaustedProviderRegistry {\n\tprivate readonly exhausted = new Map<string, number | undefined>();\n\n\tmarkExhausted(ref: string, until?: number): void {\n\t\tthis.exhausted.set(ref, until);\n\t}\n\n\tisExhausted(ref: string): boolean {\n\t\tconst until = this.exhausted.get(ref);\n\t\tif (until === undefined) return this.exhausted.has(ref);\n\t\tif (Date.now() < until) return true;\n\t\tthis.exhausted.delete(ref);\n\t\treturn false;\n\t}\n\n\tsnapshot(): string[] {\n\t\treturn [...this.exhausted.keys()].filter((ref) => this.isExhausted(ref));\n\t}\n}\n\nexport interface BillingFailoverControllerDeps {\n\tagent: Agent;\n\tmodelRegistry: ModelRegistry;\n\temit(event: { type: \"warning\"; message: string }): void;\n\texhausted: ExhaustedProviderRegistry;\n\tsubscriptionHop?: boolean;\n}\n\nexport class BillingFailoverController {\n\tprivate readonly deps: BillingFailoverControllerDeps;\n\tprivate lastNotice: string | undefined;\n\n\tconstructor(deps: BillingFailoverControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tisExhausted(ref: string): boolean {\n\t\treturn this.deps.exhausted.isExhausted(ref);\n\t}\n\n\tsnapshotExhausted(): string[] {\n\t\treturn this.deps.exhausted.snapshot();\n\t}\n\n\tgetLastNotice(): string | undefined {\n\t\treturn this.lastNotice;\n\t}\n\n\tgetStatus(): ModelRouterFailoverStatus {\n\t\treturn { exhausted: this.snapshotExhausted(), lastNotice: this.lastNotice };\n\t}\n\n\tasync handleAssistantError(message: AssistantMessage): Promise<boolean> {\n\t\tif (message.stopReason !== \"error\") return false;\n\t\tconst classified = classifyFailure({ message: message.errorMessage ?? \"\", provider: message.provider });\n\t\tif (classified.reason !== \"billing_or_quota\") return false;\n\t\tconst failedModel = this.deps.modelRegistry.find(message.provider, message.model) ?? this.deps.agent.state.model;\n\t\tconst failedRef = `${failedModel.provider}/${failedModel.id}`;\n\t\tthis.deps.exhausted.markExhausted(failedRef, expiryFromRetryAfter(classified.retryAfterMs));\n\n\t\tconst defaultModelId = DEFAULT_MODEL_PER_PROVIDER[failedModel.provider];\n\t\tconst hop = defaultModelId ? this.deps.modelRegistry.find(failedModel.provider, defaultModelId) : undefined;\n\t\tconst action = decideBillingFailover({\n\t\t\tfailedModel: { provider: failedModel.provider, id: failedModel.id },\n\t\t\tbillingClass: this.deps.modelRegistry.isUsingOAuth(failedModel) ? \"subscription\" : \"metered\",\n\t\t\tproviderDefaultModelId: defaultModelId,\n\t\t\thopResolvesWithAuth: Boolean(hop && this.deps.modelRegistry.hasConfiguredAuth(hop)),\n\t\t\thopExhausted: hop ? this.deps.exhausted.isExhausted(`${hop.provider}/${hop.id}`) : false,\n\t\t\tsubscriptionHop: this.deps.subscriptionHop,\n\t\t});\n\t\tif (action.action === \"failover\") {\n\t\t\tthis.deps.agent.state.model = hop as Model<Api>;\n\t\t}\n\t\tthis.lastNotice = action.notice;\n\t\tthis.deps.emit({ type: \"warning\", message: action.notice });\n\t\treturn true;\n\t}\n}\n\nfunction expiryFromRetryAfter(retryAfterMs: number | undefined): number | undefined {\n\treturn retryAfterMs === undefined ? undefined : Date.now() + retryAfterMs;\n}\n"]}
1
+ {"version":3,"file":"billing-failover-controller.d.ts","sourceRoot":"","sources":["../../src/core/billing-failover-controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAO,gBAAgB,EAAS,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAM1E,qBAAa,yBAAyB;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IAEnE,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAE/C;IAED,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMhC;IAED,QAAQ,IAAI,MAAM,EAAE,CAEnB;CACD;AAED,MAAM,WAAW,6BAA6B;IAC7C,KAAK,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxD,SAAS,EAAE,yBAAyB,CAAC;IACrC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,CAAC,IAAI,EAAE;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;KAC/C,GAAG,IAAI,CAAC;CACT;AAED,qBAAa,yBAAyB;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgC;IACrD,OAAO,CAAC,UAAU,CAAqB;IAEvC,YAAY,IAAI,EAAE,6BAA6B,EAE9C;IAED,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhC;IAED,iBAAiB,IAAI,MAAM,EAAE,CAE5B;IAED,aAAa,IAAI,MAAM,GAAG,SAAS,CAElC;IAED,SAAS,IAAI,yBAAyB,CAErC;IAEK,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BtE;CACD","sourcesContent":["import type { Agent } from \"@caupulican/pi-agent-core\";\nimport { classifyFailure } from \"@caupulican/pi-agent-core\";\nimport type { Api, AssistantMessage, Model } from \"@caupulican/pi-ai\";\nimport { decideBillingFailover } from \"./billing-failover.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport type { ModelRouterFailoverStatus } from \"./model-router/status.ts\";\n\nconst DEFAULT_MODEL_PER_PROVIDER: Record<string, string> = {\n\t\"openai-codex\": \"gpt-5.5\",\n};\n\nexport class ExhaustedProviderRegistry {\n\tprivate readonly exhausted = new Map<string, number | undefined>();\n\n\tmarkExhausted(ref: string, until?: number): void {\n\t\tthis.exhausted.set(ref, until);\n\t}\n\n\tisExhausted(ref: string): boolean {\n\t\tconst until = this.exhausted.get(ref);\n\t\tif (until === undefined) return this.exhausted.has(ref);\n\t\tif (Date.now() < until) return true;\n\t\tthis.exhausted.delete(ref);\n\t\treturn false;\n\t}\n\n\tsnapshot(): string[] {\n\t\treturn [...this.exhausted.keys()].filter((ref) => this.isExhausted(ref));\n\t}\n}\n\nexport interface BillingFailoverControllerDeps {\n\tagent: Agent;\n\tmodelRegistry: ModelRegistry;\n\temit(event: { type: \"warning\"; message: string }): void;\n\texhausted: ExhaustedProviderRegistry;\n\tsubscriptionHop?: boolean;\n\trecordFailure?(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\tmessage: string;\n\t\tclassified: ReturnType<typeof classifyFailure>;\n\t}): void;\n}\n\nexport class BillingFailoverController {\n\tprivate readonly deps: BillingFailoverControllerDeps;\n\tprivate lastNotice: string | undefined;\n\n\tconstructor(deps: BillingFailoverControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tisExhausted(ref: string): boolean {\n\t\treturn this.deps.exhausted.isExhausted(ref);\n\t}\n\n\tsnapshotExhausted(): string[] {\n\t\treturn this.deps.exhausted.snapshot();\n\t}\n\n\tgetLastNotice(): string | undefined {\n\t\treturn this.lastNotice;\n\t}\n\n\tgetStatus(): ModelRouterFailoverStatus {\n\t\treturn { exhausted: this.snapshotExhausted(), lastNotice: this.lastNotice };\n\t}\n\n\tasync handleAssistantError(message: AssistantMessage): Promise<boolean> {\n\t\tif (message.stopReason !== \"error\") return false;\n\t\tconst classified = classifyFailure({ message: message.errorMessage ?? \"\", provider: message.provider });\n\t\tthis.deps.recordFailure?.({\n\t\t\tprovider: message.provider,\n\t\t\tmodelId: message.model,\n\t\t\tmessage: message.errorMessage ?? \"\",\n\t\t\tclassified,\n\t\t});\n\t\tif (classified.reason !== \"billing_or_quota\") return false;\n\t\tconst failedModel = this.deps.modelRegistry.find(message.provider, message.model) ?? this.deps.agent.state.model;\n\t\tconst failedRef = `${failedModel.provider}/${failedModel.id}`;\n\t\tthis.deps.exhausted.markExhausted(failedRef, expiryFromRetryAfter(classified.retryAfterMs));\n\n\t\tconst defaultModelId = DEFAULT_MODEL_PER_PROVIDER[failedModel.provider];\n\t\tconst hop = defaultModelId ? this.deps.modelRegistry.find(failedModel.provider, defaultModelId) : undefined;\n\t\tconst action = decideBillingFailover({\n\t\t\tfailedModel: { provider: failedModel.provider, id: failedModel.id },\n\t\t\tbillingClass: this.deps.modelRegistry.isUsingOAuth(failedModel) ? \"subscription\" : \"metered\",\n\t\t\tproviderDefaultModelId: defaultModelId,\n\t\t\thopResolvesWithAuth: Boolean(hop && this.deps.modelRegistry.hasConfiguredAuth(hop)),\n\t\t\thopExhausted: hop ? this.deps.exhausted.isExhausted(`${hop.provider}/${hop.id}`) : false,\n\t\t\tsubscriptionHop: this.deps.subscriptionHop,\n\t\t});\n\t\tif (action.action === \"failover\") {\n\t\t\tthis.deps.agent.state.model = hop as Model<Api>;\n\t\t}\n\t\tthis.lastNotice = action.notice;\n\t\tthis.deps.emit({ type: \"warning\", message: action.notice });\n\t\treturn true;\n\t}\n}\n\nfunction expiryFromRetryAfter(retryAfterMs: number | undefined): number | undefined {\n\treturn retryAfterMs === undefined ? undefined : Date.now() + retryAfterMs;\n}\n"]}
@@ -43,6 +43,12 @@ export class BillingFailoverController {
43
43
  if (message.stopReason !== "error")
44
44
  return false;
45
45
  const classified = classifyFailure({ message: message.errorMessage ?? "", provider: message.provider });
46
+ this.deps.recordFailure?.({
47
+ provider: message.provider,
48
+ modelId: message.model,
49
+ message: message.errorMessage ?? "",
50
+ classified,
51
+ });
46
52
  if (classified.reason !== "billing_or_quota")
47
53
  return false;
48
54
  const failedModel = this.deps.modelRegistry.find(message.provider, message.model) ?? this.deps.agent.state.model;
@@ -1 +1 @@
1
- {"version":3,"file":"billing-failover-controller.js","sourceRoot":"","sources":["../../src/core/billing-failover-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent } from \"@caupulican/pi-agent-core\";\nimport { classifyFailure } from \"@caupulican/pi-agent-core\";\nimport type { Api, AssistantMessage, Model } from \"@caupulican/pi-ai\";\nimport { decideBillingFailover } from \"./billing-failover.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport type { ModelRouterFailoverStatus } from \"./model-router/status.ts\";\n\nconst DEFAULT_MODEL_PER_PROVIDER: Record<string, string> = {\n\t\"openai-codex\": \"gpt-5.5\",\n};\n\nexport class ExhaustedProviderRegistry {\n\tprivate readonly exhausted = new Map<string, number | undefined>();\n\n\tmarkExhausted(ref: string, until?: number): void {\n\t\tthis.exhausted.set(ref, until);\n\t}\n\n\tisExhausted(ref: string): boolean {\n\t\tconst until = this.exhausted.get(ref);\n\t\tif (until === undefined) return this.exhausted.has(ref);\n\t\tif (Date.now() < until) return true;\n\t\tthis.exhausted.delete(ref);\n\t\treturn false;\n\t}\n\n\tsnapshot(): string[] {\n\t\treturn [...this.exhausted.keys()].filter((ref) => this.isExhausted(ref));\n\t}\n}\n\nexport interface BillingFailoverControllerDeps {\n\tagent: Agent;\n\tmodelRegistry: ModelRegistry;\n\temit(event: { type: \"warning\"; message: string }): void;\n\texhausted: ExhaustedProviderRegistry;\n\tsubscriptionHop?: boolean;\n}\n\nexport class BillingFailoverController {\n\tprivate readonly deps: BillingFailoverControllerDeps;\n\tprivate lastNotice: string | undefined;\n\n\tconstructor(deps: BillingFailoverControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tisExhausted(ref: string): boolean {\n\t\treturn this.deps.exhausted.isExhausted(ref);\n\t}\n\n\tsnapshotExhausted(): string[] {\n\t\treturn this.deps.exhausted.snapshot();\n\t}\n\n\tgetLastNotice(): string | undefined {\n\t\treturn this.lastNotice;\n\t}\n\n\tgetStatus(): ModelRouterFailoverStatus {\n\t\treturn { exhausted: this.snapshotExhausted(), lastNotice: this.lastNotice };\n\t}\n\n\tasync handleAssistantError(message: AssistantMessage): Promise<boolean> {\n\t\tif (message.stopReason !== \"error\") return false;\n\t\tconst classified = classifyFailure({ message: message.errorMessage ?? \"\", provider: message.provider });\n\t\tif (classified.reason !== \"billing_or_quota\") return false;\n\t\tconst failedModel = this.deps.modelRegistry.find(message.provider, message.model) ?? this.deps.agent.state.model;\n\t\tconst failedRef = `${failedModel.provider}/${failedModel.id}`;\n\t\tthis.deps.exhausted.markExhausted(failedRef, expiryFromRetryAfter(classified.retryAfterMs));\n\n\t\tconst defaultModelId = DEFAULT_MODEL_PER_PROVIDER[failedModel.provider];\n\t\tconst hop = defaultModelId ? this.deps.modelRegistry.find(failedModel.provider, defaultModelId) : undefined;\n\t\tconst action = decideBillingFailover({\n\t\t\tfailedModel: { provider: failedModel.provider, id: failedModel.id },\n\t\t\tbillingClass: this.deps.modelRegistry.isUsingOAuth(failedModel) ? \"subscription\" : \"metered\",\n\t\t\tproviderDefaultModelId: defaultModelId,\n\t\t\thopResolvesWithAuth: Boolean(hop && this.deps.modelRegistry.hasConfiguredAuth(hop)),\n\t\t\thopExhausted: hop ? this.deps.exhausted.isExhausted(`${hop.provider}/${hop.id}`) : false,\n\t\t\tsubscriptionHop: this.deps.subscriptionHop,\n\t\t});\n\t\tif (action.action === \"failover\") {\n\t\t\tthis.deps.agent.state.model = hop as Model<Api>;\n\t\t}\n\t\tthis.lastNotice = action.notice;\n\t\tthis.deps.emit({ type: \"warning\", message: action.notice });\n\t\treturn true;\n\t}\n}\n\nfunction expiryFromRetryAfter(retryAfterMs: number | undefined): number | undefined {\n\treturn retryAfterMs === undefined ? undefined : Date.now() + retryAfterMs;\n}\n"]}
1
+ {"version":3,"file":"billing-failover-controller.js","sourceRoot":"","sources":["../../src/core/billing-failover-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Agent } from \"@caupulican/pi-agent-core\";\nimport { classifyFailure } from \"@caupulican/pi-agent-core\";\nimport type { Api, AssistantMessage, Model } from \"@caupulican/pi-ai\";\nimport { decideBillingFailover } from \"./billing-failover.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport type { ModelRouterFailoverStatus } from \"./model-router/status.ts\";\n\nconst DEFAULT_MODEL_PER_PROVIDER: Record<string, string> = {\n\t\"openai-codex\": \"gpt-5.5\",\n};\n\nexport class ExhaustedProviderRegistry {\n\tprivate readonly exhausted = new Map<string, number | undefined>();\n\n\tmarkExhausted(ref: string, until?: number): void {\n\t\tthis.exhausted.set(ref, until);\n\t}\n\n\tisExhausted(ref: string): boolean {\n\t\tconst until = this.exhausted.get(ref);\n\t\tif (until === undefined) return this.exhausted.has(ref);\n\t\tif (Date.now() < until) return true;\n\t\tthis.exhausted.delete(ref);\n\t\treturn false;\n\t}\n\n\tsnapshot(): string[] {\n\t\treturn [...this.exhausted.keys()].filter((ref) => this.isExhausted(ref));\n\t}\n}\n\nexport interface BillingFailoverControllerDeps {\n\tagent: Agent;\n\tmodelRegistry: ModelRegistry;\n\temit(event: { type: \"warning\"; message: string }): void;\n\texhausted: ExhaustedProviderRegistry;\n\tsubscriptionHop?: boolean;\n\trecordFailure?(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\tmessage: string;\n\t\tclassified: ReturnType<typeof classifyFailure>;\n\t}): void;\n}\n\nexport class BillingFailoverController {\n\tprivate readonly deps: BillingFailoverControllerDeps;\n\tprivate lastNotice: string | undefined;\n\n\tconstructor(deps: BillingFailoverControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tisExhausted(ref: string): boolean {\n\t\treturn this.deps.exhausted.isExhausted(ref);\n\t}\n\n\tsnapshotExhausted(): string[] {\n\t\treturn this.deps.exhausted.snapshot();\n\t}\n\n\tgetLastNotice(): string | undefined {\n\t\treturn this.lastNotice;\n\t}\n\n\tgetStatus(): ModelRouterFailoverStatus {\n\t\treturn { exhausted: this.snapshotExhausted(), lastNotice: this.lastNotice };\n\t}\n\n\tasync handleAssistantError(message: AssistantMessage): Promise<boolean> {\n\t\tif (message.stopReason !== \"error\") return false;\n\t\tconst classified = classifyFailure({ message: message.errorMessage ?? \"\", provider: message.provider });\n\t\tthis.deps.recordFailure?.({\n\t\t\tprovider: message.provider,\n\t\t\tmodelId: message.model,\n\t\t\tmessage: message.errorMessage ?? \"\",\n\t\t\tclassified,\n\t\t});\n\t\tif (classified.reason !== \"billing_or_quota\") return false;\n\t\tconst failedModel = this.deps.modelRegistry.find(message.provider, message.model) ?? this.deps.agent.state.model;\n\t\tconst failedRef = `${failedModel.provider}/${failedModel.id}`;\n\t\tthis.deps.exhausted.markExhausted(failedRef, expiryFromRetryAfter(classified.retryAfterMs));\n\n\t\tconst defaultModelId = DEFAULT_MODEL_PER_PROVIDER[failedModel.provider];\n\t\tconst hop = defaultModelId ? this.deps.modelRegistry.find(failedModel.provider, defaultModelId) : undefined;\n\t\tconst action = decideBillingFailover({\n\t\t\tfailedModel: { provider: failedModel.provider, id: failedModel.id },\n\t\t\tbillingClass: this.deps.modelRegistry.isUsingOAuth(failedModel) ? \"subscription\" : \"metered\",\n\t\t\tproviderDefaultModelId: defaultModelId,\n\t\t\thopResolvesWithAuth: Boolean(hop && this.deps.modelRegistry.hasConfiguredAuth(hop)),\n\t\t\thopExhausted: hop ? this.deps.exhausted.isExhausted(`${hop.provider}/${hop.id}`) : false,\n\t\t\tsubscriptionHop: this.deps.subscriptionHop,\n\t\t});\n\t\tif (action.action === \"failover\") {\n\t\t\tthis.deps.agent.state.model = hop as Model<Api>;\n\t\t}\n\t\tthis.lastNotice = action.notice;\n\t\tthis.deps.emit({ type: \"warning\", message: action.notice });\n\t\treturn true;\n\t}\n}\n\nfunction expiryFromRetryAfter(retryAfterMs: number | undefined): number | undefined {\n\treturn retryAfterMs === undefined ? undefined : Date.now() + retryAfterMs;\n}\n"]}
@@ -6,9 +6,10 @@
6
6
  * session keeps same-signature private delegations at every call-in point.
7
7
  */
8
8
  import type { ThinkingLevel } from "@caupulican/pi-agent-core";
9
- import type { CompactionSettings } from "@caupulican/pi-agent-core/node";
9
+ import { type CompactionSettings } from "@caupulican/pi-agent-core/node";
10
10
  import type { Model } from "@caupulican/pi-ai";
11
11
  import type { ModelRegistry } from "./model-registry.ts";
12
+ import type { ModelFitnessReport } from "./research/model-fitness.ts";
12
13
  import type { SettingsManager } from "./settings-manager.ts";
13
14
  export interface CompactionSupportDeps {
14
15
  getModel(): Model<any> | undefined;
@@ -21,9 +22,15 @@ export interface CompactionSupportDeps {
21
22
  apiKey?: string;
22
23
  headers?: Record<string, string>;
23
24
  }>;
25
+ isModelExhausted(ref: string): boolean;
26
+ getStoredFitnessReport(ref: string): ModelFitnessReport | undefined;
27
+ /** Estimated tokens of the summarization input (live context; over-estimates, which is safe). */
28
+ estimateSummarizationInputTokens(): number;
29
+ emitWarning(message: string): void;
24
30
  }
25
31
  export declare class CompactionSupport {
26
32
  private readonly deps;
33
+ private lastSelectionReason;
27
34
  constructor(deps: CompactionSupportDeps);
28
35
  getAdaptedSettings(): CompactionSettings;
29
36
  getRequestAuth(model: Model<any>): Promise<{
@@ -44,7 +51,9 @@ export declare class CompactionSupport {
44
51
  failure?: string;
45
52
  }>;
46
53
  private getExplicitCompactionModelSetting;
54
+ private modelRef;
47
55
  private resolveConfiguredModel;
56
+ private selectConfiguredModel;
48
57
  private resolveDefaultModel;
49
58
  private modelsAreEqual;
50
59
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"compaction-support.d.ts","sourceRoot":"","sources":["../../src/core/compaction-support.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACrC,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,kBAAkB,IAAI,eAAe,CAAC;IACtC,gBAAgB,IAAI,aAAa,CAAC;IAClC,wGAAsG;IACtG,WAAW,IAAI,OAAO,CAAC;IACvB,0FAA0F;IAC1F,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;CAC1G;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAE7C,YAAY,IAAI,EAAE,qBAAqB,EAEtC;IAED,kBAAkB,IAAI,kBAAkB,CAoBvC;IAEK,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;QAChD,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACjC,CAAC,CAOD;IAED;;;;;;OAMG;IACG,mBAAmB,CACxB,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAC3B,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GACtB,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAwBrG;IAED,OAAO,CAAC,iCAAiC;IAKzC,OAAO,CAAC,sBAAsB;IAO9B,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,cAAc;IAItB;;;;;OAKG;IACH,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAIjD;IAED,0EAA0E;IAC1E,oBAAoB,CACnB,oBAAoB,EAAE,aAAa,GAAG,SAAS,EAC/C,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAC3B,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GACtB,aAAa,GAAG,SAAS,CAY3B;CACD","sourcesContent":["/**\n * Compaction support: summarizer model selection (#30 cost guard), request-auth resolution\n * with session-model fallback, and window-adapted compaction settings.\n *\n * Extracted verbatim from AgentSession (agent-session.ts) as a narrow-deps collaborator; the\n * session keeps same-signature private delegations at every call-in point.\n */\nimport type { ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport type { CompactionSettings } from \"@caupulican/pi-agent-core/node\";\nimport type { Model } from \"@caupulican/pi-ai\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { resolveCliModel } from \"./model-resolver.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface CompactionSupportDeps {\n\tgetModel(): Model<any> | undefined;\n\tgetSettingsManager(): SettingsManager;\n\tgetModelRegistry(): ModelRegistry;\n\t/** True when the agent's streamFn is (or wraps) the raw streamSimple — auth must be explicit then. */\n\tisRawStream(): boolean;\n\t/** Host auth resolution that THROWS with a user-actionable message when no key exists. */\n\tgetRequiredRequestAuth(model: Model<any>): Promise<{ apiKey?: string; headers?: Record<string, string> }>;\n}\n\nexport class CompactionSupport {\n\tprivate readonly deps: CompactionSupportDeps;\n\n\tconstructor(deps: CompactionSupportDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetAdaptedSettings(): CompactionSettings {\n\t\tconst settings = this.deps.getSettingsManager().getCompactionSettings();\n\t\tconst model = this.deps.getModel();\n\t\tif (!model) return settings;\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return settings;\n\n\t\t// Adapt reserveTokens: at most 25% of context window\n\t\tconst maxReserve = Math.floor(contextWindow * 0.25);\n\t\tconst reserveTokens = Math.min(settings.reserveTokens, maxReserve);\n\n\t\t// Adapt keepRecentTokens: at most 50% of context window\n\t\tconst maxKeepRecent = Math.floor(contextWindow * 0.5);\n\t\tconst keepRecentTokens = Math.min(settings.keepRecentTokens, maxKeepRecent);\n\n\t\treturn {\n\t\t\t...settings,\n\t\t\treserveTokens,\n\t\t\tkeepRecentTokens,\n\t\t};\n\t}\n\n\tasync getRequestAuth(model: Model<any>): Promise<{\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t}> {\n\t\tif (this.deps.isRawStream()) {\n\t\t\treturn this.deps.getRequiredRequestAuth(model);\n\t\t}\n\n\t\tconst result = await this.deps.getModelRegistry().getApiKeyAndHeaders(model);\n\t\treturn result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};\n\t}\n\n\t/**\n\t * Resolve the summarizer model AND its request auth for auto-compaction. The cheap auxiliary\n\t * model (#30) can be nominally available yet fail key resolution at request time (expired\n\t * OAuth, revoked key). Falling back to the session model keeps auto-compaction working in\n\t * exactly the situations where manual /compact works; only when neither resolves do we fail —\n\t * and then with a concrete message instead of a silent no-op.\n\t */\n\tasync resolveModelAndAuth(\n\t\tcompactionModel: Model<any>,\n\t\tsessionModel: Model<any>,\n\t): Promise<{ model: Model<any>; apiKey?: string; headers?: Record<string, string>; failure?: string }> {\n\t\tif (this.deps.isRawStream()) {\n\t\t\tconst registry = this.deps.getModelRegistry();\n\t\t\tlet auth = await registry.getApiKeyAndHeaders(compactionModel);\n\t\t\tif (auth.ok && auth.apiKey) {\n\t\t\t\treturn { model: compactionModel, apiKey: auth.apiKey, headers: auth.headers };\n\t\t\t}\n\t\t\tconst isSameModel =\n\t\t\t\tcompactionModel.provider === sessionModel.provider && compactionModel.id === sessionModel.id;\n\t\t\tif (!isSameModel) {\n\t\t\t\tauth = await registry.getApiKeyAndHeaders(sessionModel);\n\t\t\t\tif (auth.ok && auth.apiKey) {\n\t\t\t\t\treturn { model: sessionModel, apiKey: auth.apiKey, headers: auth.headers };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tmodel: sessionModel,\n\t\t\t\tfailure: `no usable API key for the summarizer (tried ${compactionModel.id}${isSameModel ? \"\" : ` and ${sessionModel.id}`})`,\n\t\t\t};\n\t\t}\n\n\t\t// Custom streamFn owns auth injection (CLI path) — resolve best-effort, never fail here.\n\t\tconst { apiKey, headers } = await this.getRequestAuth(compactionModel);\n\t\treturn { model: compactionModel, apiKey, headers };\n\t}\n\n\tprivate getExplicitCompactionModelSetting(): string | undefined {\n\t\tconst setting = this.deps.getSettingsManager().getCompactionModel().trim();\n\t\treturn setting && setting !== \"auto\" ? setting : undefined;\n\t}\n\n\tprivate resolveConfiguredModel(pattern: string): Model<any> | undefined {\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst resolved = resolveCliModel({ cliModel: pattern, modelRegistry: registry });\n\t\tif (!resolved.model || !registry.hasConfiguredAuth(resolved.model)) return undefined;\n\t\treturn resolved.model;\n\t}\n\n\tprivate resolveDefaultModel(sessionModel: Model<any>): Model<any> {\n\t\tconst router = this.deps.getSettingsManager().getModelRouterSettings();\n\t\tif (!router.enabled || !router.cheapModel) return sessionModel;\n\t\treturn this.resolveConfiguredModel(router.cheapModel) ?? sessionModel;\n\t}\n\n\tprivate modelsAreEqual(left: Model<any>, right: Model<any>): boolean {\n\t\treturn left.provider === right.provider && left.id === right.id;\n\t}\n\n\t/**\n\t * Resolve the model used to SUMMARIZE during compaction. Selection:\n\t * - an explicit `compaction.model` setting wins, but only if its provider is authed (else fall back);\n\t * - `\"auto\"`/unset follows the model router's configured cheap model when the router is enabled;\n\t * - otherwise the session model is used (safe default).\n\t */\n\tresolveModel(sessionModel: Model<any>): Model<any> {\n\t\tconst explicitSetting = this.getExplicitCompactionModelSetting();\n\t\tif (explicitSetting) return this.resolveConfiguredModel(explicitSetting) ?? sessionModel;\n\t\treturn this.resolveDefaultModel(sessionModel);\n\t}\n\n\t/** Default compaction should never inherit expensive session thinking. */\n\tresolveThinkingLevel(\n\t\tsessionThinkingLevel: ThinkingLevel | undefined,\n\t\tcompactionModel: Model<any>,\n\t\tsessionModel: Model<any>,\n\t): ThinkingLevel | undefined {\n\t\tif (this.getExplicitCompactionModelSetting()) return sessionThinkingLevel;\n\n\t\tconst router = this.deps.getSettingsManager().getModelRouterSettings();\n\t\tif (router.enabled && router.cheapModel) {\n\t\t\tconst routerModel = this.resolveConfiguredModel(router.cheapModel);\n\t\t\tif (routerModel && this.modelsAreEqual(routerModel, compactionModel)) {\n\t\t\t\treturn router.cheapThinking ?? \"low\";\n\t\t\t}\n\t\t}\n\n\t\treturn this.modelsAreEqual(compactionModel, sessionModel) ? \"low\" : sessionThinkingLevel;\n\t}\n}\n"]}
1
+ {"version":3,"file":"compaction-support.d.ts","sourceRoot":"","sources":["../../src/core/compaction-support.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,KAAK,kBAAkB,EAAuB,MAAM,gCAAgC,CAAC;AAC9F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACrC,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,kBAAkB,IAAI,eAAe,CAAC;IACtC,gBAAgB,IAAI,aAAa,CAAC;IAClC,wGAAsG;IACtG,WAAW,IAAI,OAAO,CAAC;IACvB,0FAA0F;IAC1F,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IAC1G,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAC;IACpE,iGAAiG;IACjG,gCAAgC,IAAI,MAAM,CAAC;IAC3C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAC7C,OAAO,CAAC,mBAAmB,CAAqB;IAEhD,YAAY,IAAI,EAAE,qBAAqB,EAEtC;IAED,kBAAkB,IAAI,kBAAkB,CAoBvC;IAEK,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;QAChD,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACjC,CAAC,CAOD;IAED;;;;;;OAMG;IACG,mBAAmB,CACxB,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAC3B,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GACtB,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAwBrG;IAED,OAAO,CAAC,iCAAiC;IAKzC,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,mBAAmB;IAiC3B,OAAO,CAAC,cAAc;IAItB;;;;;OAKG;IACH,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAuBjD;IAED,0EAA0E;IAC1E,oBAAoB,CACnB,oBAAoB,EAAE,aAAa,GAAG,SAAS,EAC/C,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAC3B,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GACtB,aAAa,GAAG,SAAS,CAY3B;CACD","sourcesContent":["/**\n * Compaction support: summarizer model selection (#30 cost guard), request-auth resolution\n * with session-model fallback, and window-adapted compaction settings.\n *\n * Extracted verbatim from AgentSession (agent-session.ts) as a narrow-deps collaborator; the\n * session keeps same-signature private delegations at every call-in point.\n */\nimport type { ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport { type CompactionSettings, summarizerCanIngest } from \"@caupulican/pi-agent-core/node\";\nimport type { Model } from \"@caupulican/pi-ai\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { resolveCliModel } from \"./model-resolver.ts\";\nimport { evaluateSurfaceFitness } from \"./model-router/fitness-gate.ts\";\nimport type { ModelFitnessReport } from \"./research/model-fitness.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface CompactionSupportDeps {\n\tgetModel(): Model<any> | undefined;\n\tgetSettingsManager(): SettingsManager;\n\tgetModelRegistry(): ModelRegistry;\n\t/** True when the agent's streamFn is (or wraps) the raw streamSimple — auth must be explicit then. */\n\tisRawStream(): boolean;\n\t/** Host auth resolution that THROWS with a user-actionable message when no key exists. */\n\tgetRequiredRequestAuth(model: Model<any>): Promise<{ apiKey?: string; headers?: Record<string, string> }>;\n\tisModelExhausted(ref: string): boolean;\n\tgetStoredFitnessReport(ref: string): ModelFitnessReport | undefined;\n\t/** Estimated tokens of the summarization input (live context; over-estimates, which is safe). */\n\testimateSummarizationInputTokens(): number;\n\temitWarning(message: string): void;\n}\n\nexport class CompactionSupport {\n\tprivate readonly deps: CompactionSupportDeps;\n\tprivate lastSelectionReason: string | undefined;\n\n\tconstructor(deps: CompactionSupportDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetAdaptedSettings(): CompactionSettings {\n\t\tconst settings = this.deps.getSettingsManager().getCompactionSettings();\n\t\tconst model = this.deps.getModel();\n\t\tif (!model) return settings;\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return settings;\n\n\t\t// Adapt reserveTokens: at most 25% of context window\n\t\tconst maxReserve = Math.floor(contextWindow * 0.25);\n\t\tconst reserveTokens = Math.min(settings.reserveTokens, maxReserve);\n\n\t\t// Adapt keepRecentTokens: at most 50% of context window\n\t\tconst maxKeepRecent = Math.floor(contextWindow * 0.5);\n\t\tconst keepRecentTokens = Math.min(settings.keepRecentTokens, maxKeepRecent);\n\n\t\treturn {\n\t\t\t...settings,\n\t\t\treserveTokens,\n\t\t\tkeepRecentTokens,\n\t\t};\n\t}\n\n\tasync getRequestAuth(model: Model<any>): Promise<{\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t}> {\n\t\tif (this.deps.isRawStream()) {\n\t\t\treturn this.deps.getRequiredRequestAuth(model);\n\t\t}\n\n\t\tconst result = await this.deps.getModelRegistry().getApiKeyAndHeaders(model);\n\t\treturn result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};\n\t}\n\n\t/**\n\t * Resolve the summarizer model AND its request auth for auto-compaction. The cheap auxiliary\n\t * model (#30) can be nominally available yet fail key resolution at request time (expired\n\t * OAuth, revoked key). Falling back to the session model keeps auto-compaction working in\n\t * exactly the situations where manual /compact works; only when neither resolves do we fail —\n\t * and then with a concrete message instead of a silent no-op.\n\t */\n\tasync resolveModelAndAuth(\n\t\tcompactionModel: Model<any>,\n\t\tsessionModel: Model<any>,\n\t): Promise<{ model: Model<any>; apiKey?: string; headers?: Record<string, string>; failure?: string }> {\n\t\tif (this.deps.isRawStream()) {\n\t\t\tconst registry = this.deps.getModelRegistry();\n\t\t\tlet auth = await registry.getApiKeyAndHeaders(compactionModel);\n\t\t\tif (auth.ok && auth.apiKey) {\n\t\t\t\treturn { model: compactionModel, apiKey: auth.apiKey, headers: auth.headers };\n\t\t\t}\n\t\t\tconst isSameModel =\n\t\t\t\tcompactionModel.provider === sessionModel.provider && compactionModel.id === sessionModel.id;\n\t\t\tif (!isSameModel) {\n\t\t\t\tauth = await registry.getApiKeyAndHeaders(sessionModel);\n\t\t\t\tif (auth.ok && auth.apiKey) {\n\t\t\t\t\treturn { model: sessionModel, apiKey: auth.apiKey, headers: auth.headers };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tmodel: sessionModel,\n\t\t\t\tfailure: `no usable API key for the summarizer (tried ${compactionModel.id}${isSameModel ? \"\" : ` and ${sessionModel.id}`})`,\n\t\t\t};\n\t\t}\n\n\t\t// Custom streamFn owns auth injection (CLI path) — resolve best-effort, never fail here.\n\t\tconst { apiKey, headers } = await this.getRequestAuth(compactionModel);\n\t\treturn { model: compactionModel, apiKey, headers };\n\t}\n\n\tprivate getExplicitCompactionModelSetting(): string | undefined {\n\t\tconst setting = this.deps.getSettingsManager().getCompactionModel().trim();\n\t\treturn setting && setting !== \"auto\" ? setting : undefined;\n\t}\n\n\tprivate modelRef(model: Model<any>): string {\n\t\treturn `${model.provider}/${model.id}`;\n\t}\n\n\tprivate resolveConfiguredModel(pattern: string): { model?: Model<any>; cause?: \"unresolved\" | \"unauthed\" } {\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst resolved = resolveCliModel({ cliModel: pattern, modelRegistry: registry });\n\t\tif (!resolved.model) return { cause: \"unresolved\" };\n\t\tif (!registry.hasConfiguredAuth(resolved.model)) return { cause: \"unauthed\" };\n\t\treturn { model: resolved.model };\n\t}\n\n\tprivate selectConfiguredModel(pattern: string): { model?: Model<any>; cause?: string } {\n\t\tconst resolved = this.resolveConfiguredModel(pattern);\n\t\tif (!resolved.model) return { cause: resolved.cause };\n\t\tif (this.deps.isModelExhausted(this.modelRef(resolved.model))) return { cause: \"exhausted\" };\n\t\treturn { model: resolved.model };\n\t}\n\n\tprivate resolveDefaultModel(sessionModel: Model<any>): Model<any> {\n\t\tconst router = this.deps.getSettingsManager().getModelRouterSettings();\n\t\tif (!router.enabled || !router.cheapModel) {\n\t\t\tthis.lastSelectionReason = \"session_default\";\n\t\t\treturn sessionModel;\n\t\t}\n\t\tconst selected = this.selectConfiguredModel(router.cheapModel);\n\t\tif (!selected.model) {\n\t\t\tthis.lastSelectionReason = `fallback:${selected.cause}`;\n\t\t\treturn sessionModel;\n\t\t}\n\t\t// Capacity is a hard constraint, independent of the fitness doctrine: a summarizer whose\n\t\t// window cannot hold the actual span produces recall-empty checkpoints (local servers\n\t\t// silently truncate over-window prompts instead of erroring), and the verification gate\n\t\t// then fails deterministically.\n\t\tconst estimatedInputTokens = this.deps.estimateSummarizationInputTokens();\n\t\tif (!summarizerCanIngest(selected.model, estimatedInputTokens)) {\n\t\t\tthis.lastSelectionReason = `fallback:window_too_small(~${Math.ceil(estimatedInputTokens / 1000)}k input vs ${selected.model.contextWindow} window)`;\n\t\t\treturn sessionModel;\n\t\t}\n\t\tconst fitness = this.deps.getStoredFitnessReport(this.modelRef(selected.model));\n\t\tconst verdict = evaluateSurfaceFitness(\"compaction\", fitness);\n\t\tif (!verdict.fit) {\n\t\t\tthis.lastSelectionReason =\n\t\t\t\tverdict.reason === \"lane_failed\"\n\t\t\t\t\t? `fallback:digest_unfit(${verdict.succeeded}/${verdict.total})`\n\t\t\t\t\t: \"fallback:unprobed\";\n\t\t\treturn sessionModel;\n\t\t}\n\t\tthis.lastSelectionReason = \"router_cheap\";\n\t\treturn selected.model;\n\t}\n\n\tprivate modelsAreEqual(left: Model<any>, right: Model<any>): boolean {\n\t\treturn left.provider === right.provider && left.id === right.id;\n\t}\n\n\t/**\n\t * Resolve the model used to SUMMARIZE during compaction. Selection:\n\t * - an explicit `compaction.model` setting wins, but only if its provider is authed (else fall back);\n\t * - `\"auto\"`/unset follows the model router's configured cheap model when the router is enabled;\n\t * - otherwise the session model is used (safe default).\n\t */\n\tresolveModel(sessionModel: Model<any>): Model<any> {\n\t\tconst explicitSetting = this.getExplicitCompactionModelSetting();\n\t\tif (explicitSetting) {\n\t\t\tconst selected = this.selectConfiguredModel(explicitSetting);\n\t\t\tthis.lastSelectionReason = selected.model ? \"explicit\" : `fallback:${selected.cause}`;\n\t\t\tif (!selected.model) this.deps.emitWarning(`Compaction summarizer ${this.lastSelectionReason}`);\n\t\t\t// An explicit user choice is honored (Class C doctrine), but silently sending an\n\t\t\t// over-window prompt yields a recall-empty summary — warn with the numbers.\n\t\t\tif (selected.model) {\n\t\t\t\tconst estimatedInputTokens = this.deps.estimateSummarizationInputTokens();\n\t\t\t\tif (!summarizerCanIngest(selected.model, estimatedInputTokens)) {\n\t\t\t\t\tthis.deps.emitWarning(\n\t\t\t\t\t\t`Compaction summarizer (explicit setting) likely cannot ingest the current context: ~${Math.ceil(estimatedInputTokens / 1000)}k input tokens vs a ${selected.model.contextWindow}-token window`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn selected.model ?? sessionModel;\n\t\t}\n\t\tconst model = this.resolveDefaultModel(sessionModel);\n\t\tif (this.lastSelectionReason?.startsWith(\"fallback:\")) {\n\t\t\tthis.deps.emitWarning(`Compaction summarizer ${this.lastSelectionReason}`);\n\t\t}\n\t\treturn model;\n\t}\n\n\t/** Default compaction should never inherit expensive session thinking. */\n\tresolveThinkingLevel(\n\t\tsessionThinkingLevel: ThinkingLevel | undefined,\n\t\tcompactionModel: Model<any>,\n\t\tsessionModel: Model<any>,\n\t): ThinkingLevel | undefined {\n\t\tif (this.getExplicitCompactionModelSetting()) return sessionThinkingLevel;\n\n\t\tconst router = this.deps.getSettingsManager().getModelRouterSettings();\n\t\tif (router.enabled && router.cheapModel) {\n\t\t\tconst routerModel = this.resolveConfiguredModel(router.cheapModel).model;\n\t\t\tif (routerModel && this.modelsAreEqual(routerModel, compactionModel)) {\n\t\t\t\treturn router.cheapThinking ?? \"low\";\n\t\t\t}\n\t\t}\n\n\t\treturn this.modelsAreEqual(compactionModel, sessionModel) ? \"low\" : sessionThinkingLevel;\n\t}\n}\n"]}
@@ -1,6 +1,9 @@
1
+ import { summarizerCanIngest } from "@caupulican/pi-agent-core/node";
1
2
  import { resolveCliModel } from "./model-resolver.js";
3
+ import { evaluateSurfaceFitness } from "./model-router/fitness-gate.js";
2
4
  export class CompactionSupport {
3
5
  deps;
6
+ lastSelectionReason;
4
7
  constructor(deps) {
5
8
  this.deps = deps;
6
9
  }
@@ -65,18 +68,57 @@ export class CompactionSupport {
65
68
  const setting = this.deps.getSettingsManager().getCompactionModel().trim();
66
69
  return setting && setting !== "auto" ? setting : undefined;
67
70
  }
71
+ modelRef(model) {
72
+ return `${model.provider}/${model.id}`;
73
+ }
68
74
  resolveConfiguredModel(pattern) {
69
75
  const registry = this.deps.getModelRegistry();
70
76
  const resolved = resolveCliModel({ cliModel: pattern, modelRegistry: registry });
71
- if (!resolved.model || !registry.hasConfiguredAuth(resolved.model))
72
- return undefined;
73
- return resolved.model;
77
+ if (!resolved.model)
78
+ return { cause: "unresolved" };
79
+ if (!registry.hasConfiguredAuth(resolved.model))
80
+ return { cause: "unauthed" };
81
+ return { model: resolved.model };
82
+ }
83
+ selectConfiguredModel(pattern) {
84
+ const resolved = this.resolveConfiguredModel(pattern);
85
+ if (!resolved.model)
86
+ return { cause: resolved.cause };
87
+ if (this.deps.isModelExhausted(this.modelRef(resolved.model)))
88
+ return { cause: "exhausted" };
89
+ return { model: resolved.model };
74
90
  }
75
91
  resolveDefaultModel(sessionModel) {
76
92
  const router = this.deps.getSettingsManager().getModelRouterSettings();
77
- if (!router.enabled || !router.cheapModel)
93
+ if (!router.enabled || !router.cheapModel) {
94
+ this.lastSelectionReason = "session_default";
95
+ return sessionModel;
96
+ }
97
+ const selected = this.selectConfiguredModel(router.cheapModel);
98
+ if (!selected.model) {
99
+ this.lastSelectionReason = `fallback:${selected.cause}`;
78
100
  return sessionModel;
79
- return this.resolveConfiguredModel(router.cheapModel) ?? sessionModel;
101
+ }
102
+ // Capacity is a hard constraint, independent of the fitness doctrine: a summarizer whose
103
+ // window cannot hold the actual span produces recall-empty checkpoints (local servers
104
+ // silently truncate over-window prompts instead of erroring), and the verification gate
105
+ // then fails deterministically.
106
+ const estimatedInputTokens = this.deps.estimateSummarizationInputTokens();
107
+ if (!summarizerCanIngest(selected.model, estimatedInputTokens)) {
108
+ this.lastSelectionReason = `fallback:window_too_small(~${Math.ceil(estimatedInputTokens / 1000)}k input vs ${selected.model.contextWindow} window)`;
109
+ return sessionModel;
110
+ }
111
+ const fitness = this.deps.getStoredFitnessReport(this.modelRef(selected.model));
112
+ const verdict = evaluateSurfaceFitness("compaction", fitness);
113
+ if (!verdict.fit) {
114
+ this.lastSelectionReason =
115
+ verdict.reason === "lane_failed"
116
+ ? `fallback:digest_unfit(${verdict.succeeded}/${verdict.total})`
117
+ : "fallback:unprobed";
118
+ return sessionModel;
119
+ }
120
+ this.lastSelectionReason = "router_cheap";
121
+ return selected.model;
80
122
  }
81
123
  modelsAreEqual(left, right) {
82
124
  return left.provider === right.provider && left.id === right.id;
@@ -89,9 +131,26 @@ export class CompactionSupport {
89
131
  */
90
132
  resolveModel(sessionModel) {
91
133
  const explicitSetting = this.getExplicitCompactionModelSetting();
92
- if (explicitSetting)
93
- return this.resolveConfiguredModel(explicitSetting) ?? sessionModel;
94
- return this.resolveDefaultModel(sessionModel);
134
+ if (explicitSetting) {
135
+ const selected = this.selectConfiguredModel(explicitSetting);
136
+ this.lastSelectionReason = selected.model ? "explicit" : `fallback:${selected.cause}`;
137
+ if (!selected.model)
138
+ this.deps.emitWarning(`Compaction summarizer ${this.lastSelectionReason}`);
139
+ // An explicit user choice is honored (Class C doctrine), but silently sending an
140
+ // over-window prompt yields a recall-empty summary — warn with the numbers.
141
+ if (selected.model) {
142
+ const estimatedInputTokens = this.deps.estimateSummarizationInputTokens();
143
+ if (!summarizerCanIngest(selected.model, estimatedInputTokens)) {
144
+ this.deps.emitWarning(`Compaction summarizer (explicit setting) likely cannot ingest the current context: ~${Math.ceil(estimatedInputTokens / 1000)}k input tokens vs a ${selected.model.contextWindow}-token window`);
145
+ }
146
+ }
147
+ return selected.model ?? sessionModel;
148
+ }
149
+ const model = this.resolveDefaultModel(sessionModel);
150
+ if (this.lastSelectionReason?.startsWith("fallback:")) {
151
+ this.deps.emitWarning(`Compaction summarizer ${this.lastSelectionReason}`);
152
+ }
153
+ return model;
95
154
  }
96
155
  /** Default compaction should never inherit expensive session thinking. */
97
156
  resolveThinkingLevel(sessionThinkingLevel, compactionModel, sessionModel) {
@@ -99,7 +158,7 @@ export class CompactionSupport {
99
158
  return sessionThinkingLevel;
100
159
  const router = this.deps.getSettingsManager().getModelRouterSettings();
101
160
  if (router.enabled && router.cheapModel) {
102
- const routerModel = this.resolveConfiguredModel(router.cheapModel);
161
+ const routerModel = this.resolveConfiguredModel(router.cheapModel).model;
103
162
  if (routerModel && this.modelsAreEqual(routerModel, compactionModel)) {
104
163
  return router.cheapThinking ?? "low";
105
164
  }
@@ -1 +1 @@
1
- {"version":3,"file":"compaction-support.js","sourceRoot":"","sources":["../../src/core/compaction-support.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Compaction support: summarizer model selection (#30 cost guard), request-auth resolution\n * with session-model fallback, and window-adapted compaction settings.\n *\n * Extracted verbatim from AgentSession (agent-session.ts) as a narrow-deps collaborator; the\n * session keeps same-signature private delegations at every call-in point.\n */\nimport type { ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport type { CompactionSettings } from \"@caupulican/pi-agent-core/node\";\nimport type { Model } from \"@caupulican/pi-ai\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { resolveCliModel } from \"./model-resolver.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface CompactionSupportDeps {\n\tgetModel(): Model<any> | undefined;\n\tgetSettingsManager(): SettingsManager;\n\tgetModelRegistry(): ModelRegistry;\n\t/** True when the agent's streamFn is (or wraps) the raw streamSimple — auth must be explicit then. */\n\tisRawStream(): boolean;\n\t/** Host auth resolution that THROWS with a user-actionable message when no key exists. */\n\tgetRequiredRequestAuth(model: Model<any>): Promise<{ apiKey?: string; headers?: Record<string, string> }>;\n}\n\nexport class CompactionSupport {\n\tprivate readonly deps: CompactionSupportDeps;\n\n\tconstructor(deps: CompactionSupportDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetAdaptedSettings(): CompactionSettings {\n\t\tconst settings = this.deps.getSettingsManager().getCompactionSettings();\n\t\tconst model = this.deps.getModel();\n\t\tif (!model) return settings;\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return settings;\n\n\t\t// Adapt reserveTokens: at most 25% of context window\n\t\tconst maxReserve = Math.floor(contextWindow * 0.25);\n\t\tconst reserveTokens = Math.min(settings.reserveTokens, maxReserve);\n\n\t\t// Adapt keepRecentTokens: at most 50% of context window\n\t\tconst maxKeepRecent = Math.floor(contextWindow * 0.5);\n\t\tconst keepRecentTokens = Math.min(settings.keepRecentTokens, maxKeepRecent);\n\n\t\treturn {\n\t\t\t...settings,\n\t\t\treserveTokens,\n\t\t\tkeepRecentTokens,\n\t\t};\n\t}\n\n\tasync getRequestAuth(model: Model<any>): Promise<{\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t}> {\n\t\tif (this.deps.isRawStream()) {\n\t\t\treturn this.deps.getRequiredRequestAuth(model);\n\t\t}\n\n\t\tconst result = await this.deps.getModelRegistry().getApiKeyAndHeaders(model);\n\t\treturn result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};\n\t}\n\n\t/**\n\t * Resolve the summarizer model AND its request auth for auto-compaction. The cheap auxiliary\n\t * model (#30) can be nominally available yet fail key resolution at request time (expired\n\t * OAuth, revoked key). Falling back to the session model keeps auto-compaction working in\n\t * exactly the situations where manual /compact works; only when neither resolves do we fail —\n\t * and then with a concrete message instead of a silent no-op.\n\t */\n\tasync resolveModelAndAuth(\n\t\tcompactionModel: Model<any>,\n\t\tsessionModel: Model<any>,\n\t): Promise<{ model: Model<any>; apiKey?: string; headers?: Record<string, string>; failure?: string }> {\n\t\tif (this.deps.isRawStream()) {\n\t\t\tconst registry = this.deps.getModelRegistry();\n\t\t\tlet auth = await registry.getApiKeyAndHeaders(compactionModel);\n\t\t\tif (auth.ok && auth.apiKey) {\n\t\t\t\treturn { model: compactionModel, apiKey: auth.apiKey, headers: auth.headers };\n\t\t\t}\n\t\t\tconst isSameModel =\n\t\t\t\tcompactionModel.provider === sessionModel.provider && compactionModel.id === sessionModel.id;\n\t\t\tif (!isSameModel) {\n\t\t\t\tauth = await registry.getApiKeyAndHeaders(sessionModel);\n\t\t\t\tif (auth.ok && auth.apiKey) {\n\t\t\t\t\treturn { model: sessionModel, apiKey: auth.apiKey, headers: auth.headers };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tmodel: sessionModel,\n\t\t\t\tfailure: `no usable API key for the summarizer (tried ${compactionModel.id}${isSameModel ? \"\" : ` and ${sessionModel.id}`})`,\n\t\t\t};\n\t\t}\n\n\t\t// Custom streamFn owns auth injection (CLI path) — resolve best-effort, never fail here.\n\t\tconst { apiKey, headers } = await this.getRequestAuth(compactionModel);\n\t\treturn { model: compactionModel, apiKey, headers };\n\t}\n\n\tprivate getExplicitCompactionModelSetting(): string | undefined {\n\t\tconst setting = this.deps.getSettingsManager().getCompactionModel().trim();\n\t\treturn setting && setting !== \"auto\" ? setting : undefined;\n\t}\n\n\tprivate resolveConfiguredModel(pattern: string): Model<any> | undefined {\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst resolved = resolveCliModel({ cliModel: pattern, modelRegistry: registry });\n\t\tif (!resolved.model || !registry.hasConfiguredAuth(resolved.model)) return undefined;\n\t\treturn resolved.model;\n\t}\n\n\tprivate resolveDefaultModel(sessionModel: Model<any>): Model<any> {\n\t\tconst router = this.deps.getSettingsManager().getModelRouterSettings();\n\t\tif (!router.enabled || !router.cheapModel) return sessionModel;\n\t\treturn this.resolveConfiguredModel(router.cheapModel) ?? sessionModel;\n\t}\n\n\tprivate modelsAreEqual(left: Model<any>, right: Model<any>): boolean {\n\t\treturn left.provider === right.provider && left.id === right.id;\n\t}\n\n\t/**\n\t * Resolve the model used to SUMMARIZE during compaction. Selection:\n\t * - an explicit `compaction.model` setting wins, but only if its provider is authed (else fall back);\n\t * - `\"auto\"`/unset follows the model router's configured cheap model when the router is enabled;\n\t * - otherwise the session model is used (safe default).\n\t */\n\tresolveModel(sessionModel: Model<any>): Model<any> {\n\t\tconst explicitSetting = this.getExplicitCompactionModelSetting();\n\t\tif (explicitSetting) return this.resolveConfiguredModel(explicitSetting) ?? sessionModel;\n\t\treturn this.resolveDefaultModel(sessionModel);\n\t}\n\n\t/** Default compaction should never inherit expensive session thinking. */\n\tresolveThinkingLevel(\n\t\tsessionThinkingLevel: ThinkingLevel | undefined,\n\t\tcompactionModel: Model<any>,\n\t\tsessionModel: Model<any>,\n\t): ThinkingLevel | undefined {\n\t\tif (this.getExplicitCompactionModelSetting()) return sessionThinkingLevel;\n\n\t\tconst router = this.deps.getSettingsManager().getModelRouterSettings();\n\t\tif (router.enabled && router.cheapModel) {\n\t\t\tconst routerModel = this.resolveConfiguredModel(router.cheapModel);\n\t\t\tif (routerModel && this.modelsAreEqual(routerModel, compactionModel)) {\n\t\t\t\treturn router.cheapThinking ?? \"low\";\n\t\t\t}\n\t\t}\n\n\t\treturn this.modelsAreEqual(compactionModel, sessionModel) ? \"low\" : sessionThinkingLevel;\n\t}\n}\n"]}
1
+ {"version":3,"file":"compaction-support.js","sourceRoot":"","sources":["../../src/core/compaction-support.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Compaction support: summarizer model selection (#30 cost guard), request-auth resolution\n * with session-model fallback, and window-adapted compaction settings.\n *\n * Extracted verbatim from AgentSession (agent-session.ts) as a narrow-deps collaborator; the\n * session keeps same-signature private delegations at every call-in point.\n */\nimport type { ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport { type CompactionSettings, summarizerCanIngest } from \"@caupulican/pi-agent-core/node\";\nimport type { Model } from \"@caupulican/pi-ai\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { resolveCliModel } from \"./model-resolver.ts\";\nimport { evaluateSurfaceFitness } from \"./model-router/fitness-gate.ts\";\nimport type { ModelFitnessReport } from \"./research/model-fitness.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface CompactionSupportDeps {\n\tgetModel(): Model<any> | undefined;\n\tgetSettingsManager(): SettingsManager;\n\tgetModelRegistry(): ModelRegistry;\n\t/** True when the agent's streamFn is (or wraps) the raw streamSimple — auth must be explicit then. */\n\tisRawStream(): boolean;\n\t/** Host auth resolution that THROWS with a user-actionable message when no key exists. */\n\tgetRequiredRequestAuth(model: Model<any>): Promise<{ apiKey?: string; headers?: Record<string, string> }>;\n\tisModelExhausted(ref: string): boolean;\n\tgetStoredFitnessReport(ref: string): ModelFitnessReport | undefined;\n\t/** Estimated tokens of the summarization input (live context; over-estimates, which is safe). */\n\testimateSummarizationInputTokens(): number;\n\temitWarning(message: string): void;\n}\n\nexport class CompactionSupport {\n\tprivate readonly deps: CompactionSupportDeps;\n\tprivate lastSelectionReason: string | undefined;\n\n\tconstructor(deps: CompactionSupportDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetAdaptedSettings(): CompactionSettings {\n\t\tconst settings = this.deps.getSettingsManager().getCompactionSettings();\n\t\tconst model = this.deps.getModel();\n\t\tif (!model) return settings;\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return settings;\n\n\t\t// Adapt reserveTokens: at most 25% of context window\n\t\tconst maxReserve = Math.floor(contextWindow * 0.25);\n\t\tconst reserveTokens = Math.min(settings.reserveTokens, maxReserve);\n\n\t\t// Adapt keepRecentTokens: at most 50% of context window\n\t\tconst maxKeepRecent = Math.floor(contextWindow * 0.5);\n\t\tconst keepRecentTokens = Math.min(settings.keepRecentTokens, maxKeepRecent);\n\n\t\treturn {\n\t\t\t...settings,\n\t\t\treserveTokens,\n\t\t\tkeepRecentTokens,\n\t\t};\n\t}\n\n\tasync getRequestAuth(model: Model<any>): Promise<{\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t}> {\n\t\tif (this.deps.isRawStream()) {\n\t\t\treturn this.deps.getRequiredRequestAuth(model);\n\t\t}\n\n\t\tconst result = await this.deps.getModelRegistry().getApiKeyAndHeaders(model);\n\t\treturn result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};\n\t}\n\n\t/**\n\t * Resolve the summarizer model AND its request auth for auto-compaction. The cheap auxiliary\n\t * model (#30) can be nominally available yet fail key resolution at request time (expired\n\t * OAuth, revoked key). Falling back to the session model keeps auto-compaction working in\n\t * exactly the situations where manual /compact works; only when neither resolves do we fail —\n\t * and then with a concrete message instead of a silent no-op.\n\t */\n\tasync resolveModelAndAuth(\n\t\tcompactionModel: Model<any>,\n\t\tsessionModel: Model<any>,\n\t): Promise<{ model: Model<any>; apiKey?: string; headers?: Record<string, string>; failure?: string }> {\n\t\tif (this.deps.isRawStream()) {\n\t\t\tconst registry = this.deps.getModelRegistry();\n\t\t\tlet auth = await registry.getApiKeyAndHeaders(compactionModel);\n\t\t\tif (auth.ok && auth.apiKey) {\n\t\t\t\treturn { model: compactionModel, apiKey: auth.apiKey, headers: auth.headers };\n\t\t\t}\n\t\t\tconst isSameModel =\n\t\t\t\tcompactionModel.provider === sessionModel.provider && compactionModel.id === sessionModel.id;\n\t\t\tif (!isSameModel) {\n\t\t\t\tauth = await registry.getApiKeyAndHeaders(sessionModel);\n\t\t\t\tif (auth.ok && auth.apiKey) {\n\t\t\t\t\treturn { model: sessionModel, apiKey: auth.apiKey, headers: auth.headers };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tmodel: sessionModel,\n\t\t\t\tfailure: `no usable API key for the summarizer (tried ${compactionModel.id}${isSameModel ? \"\" : ` and ${sessionModel.id}`})`,\n\t\t\t};\n\t\t}\n\n\t\t// Custom streamFn owns auth injection (CLI path) — resolve best-effort, never fail here.\n\t\tconst { apiKey, headers } = await this.getRequestAuth(compactionModel);\n\t\treturn { model: compactionModel, apiKey, headers };\n\t}\n\n\tprivate getExplicitCompactionModelSetting(): string | undefined {\n\t\tconst setting = this.deps.getSettingsManager().getCompactionModel().trim();\n\t\treturn setting && setting !== \"auto\" ? setting : undefined;\n\t}\n\n\tprivate modelRef(model: Model<any>): string {\n\t\treturn `${model.provider}/${model.id}`;\n\t}\n\n\tprivate resolveConfiguredModel(pattern: string): { model?: Model<any>; cause?: \"unresolved\" | \"unauthed\" } {\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst resolved = resolveCliModel({ cliModel: pattern, modelRegistry: registry });\n\t\tif (!resolved.model) return { cause: \"unresolved\" };\n\t\tif (!registry.hasConfiguredAuth(resolved.model)) return { cause: \"unauthed\" };\n\t\treturn { model: resolved.model };\n\t}\n\n\tprivate selectConfiguredModel(pattern: string): { model?: Model<any>; cause?: string } {\n\t\tconst resolved = this.resolveConfiguredModel(pattern);\n\t\tif (!resolved.model) return { cause: resolved.cause };\n\t\tif (this.deps.isModelExhausted(this.modelRef(resolved.model))) return { cause: \"exhausted\" };\n\t\treturn { model: resolved.model };\n\t}\n\n\tprivate resolveDefaultModel(sessionModel: Model<any>): Model<any> {\n\t\tconst router = this.deps.getSettingsManager().getModelRouterSettings();\n\t\tif (!router.enabled || !router.cheapModel) {\n\t\t\tthis.lastSelectionReason = \"session_default\";\n\t\t\treturn sessionModel;\n\t\t}\n\t\tconst selected = this.selectConfiguredModel(router.cheapModel);\n\t\tif (!selected.model) {\n\t\t\tthis.lastSelectionReason = `fallback:${selected.cause}`;\n\t\t\treturn sessionModel;\n\t\t}\n\t\t// Capacity is a hard constraint, independent of the fitness doctrine: a summarizer whose\n\t\t// window cannot hold the actual span produces recall-empty checkpoints (local servers\n\t\t// silently truncate over-window prompts instead of erroring), and the verification gate\n\t\t// then fails deterministically.\n\t\tconst estimatedInputTokens = this.deps.estimateSummarizationInputTokens();\n\t\tif (!summarizerCanIngest(selected.model, estimatedInputTokens)) {\n\t\t\tthis.lastSelectionReason = `fallback:window_too_small(~${Math.ceil(estimatedInputTokens / 1000)}k input vs ${selected.model.contextWindow} window)`;\n\t\t\treturn sessionModel;\n\t\t}\n\t\tconst fitness = this.deps.getStoredFitnessReport(this.modelRef(selected.model));\n\t\tconst verdict = evaluateSurfaceFitness(\"compaction\", fitness);\n\t\tif (!verdict.fit) {\n\t\t\tthis.lastSelectionReason =\n\t\t\t\tverdict.reason === \"lane_failed\"\n\t\t\t\t\t? `fallback:digest_unfit(${verdict.succeeded}/${verdict.total})`\n\t\t\t\t\t: \"fallback:unprobed\";\n\t\t\treturn sessionModel;\n\t\t}\n\t\tthis.lastSelectionReason = \"router_cheap\";\n\t\treturn selected.model;\n\t}\n\n\tprivate modelsAreEqual(left: Model<any>, right: Model<any>): boolean {\n\t\treturn left.provider === right.provider && left.id === right.id;\n\t}\n\n\t/**\n\t * Resolve the model used to SUMMARIZE during compaction. Selection:\n\t * - an explicit `compaction.model` setting wins, but only if its provider is authed (else fall back);\n\t * - `\"auto\"`/unset follows the model router's configured cheap model when the router is enabled;\n\t * - otherwise the session model is used (safe default).\n\t */\n\tresolveModel(sessionModel: Model<any>): Model<any> {\n\t\tconst explicitSetting = this.getExplicitCompactionModelSetting();\n\t\tif (explicitSetting) {\n\t\t\tconst selected = this.selectConfiguredModel(explicitSetting);\n\t\t\tthis.lastSelectionReason = selected.model ? \"explicit\" : `fallback:${selected.cause}`;\n\t\t\tif (!selected.model) this.deps.emitWarning(`Compaction summarizer ${this.lastSelectionReason}`);\n\t\t\t// An explicit user choice is honored (Class C doctrine), but silently sending an\n\t\t\t// over-window prompt yields a recall-empty summary — warn with the numbers.\n\t\t\tif (selected.model) {\n\t\t\t\tconst estimatedInputTokens = this.deps.estimateSummarizationInputTokens();\n\t\t\t\tif (!summarizerCanIngest(selected.model, estimatedInputTokens)) {\n\t\t\t\t\tthis.deps.emitWarning(\n\t\t\t\t\t\t`Compaction summarizer (explicit setting) likely cannot ingest the current context: ~${Math.ceil(estimatedInputTokens / 1000)}k input tokens vs a ${selected.model.contextWindow}-token window`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn selected.model ?? sessionModel;\n\t\t}\n\t\tconst model = this.resolveDefaultModel(sessionModel);\n\t\tif (this.lastSelectionReason?.startsWith(\"fallback:\")) {\n\t\t\tthis.deps.emitWarning(`Compaction summarizer ${this.lastSelectionReason}`);\n\t\t}\n\t\treturn model;\n\t}\n\n\t/** Default compaction should never inherit expensive session thinking. */\n\tresolveThinkingLevel(\n\t\tsessionThinkingLevel: ThinkingLevel | undefined,\n\t\tcompactionModel: Model<any>,\n\t\tsessionModel: Model<any>,\n\t): ThinkingLevel | undefined {\n\t\tif (this.getExplicitCompactionModelSetting()) return sessionThinkingLevel;\n\n\t\tconst router = this.deps.getSettingsManager().getModelRouterSettings();\n\t\tif (router.enabled && router.cheapModel) {\n\t\t\tconst routerModel = this.resolveConfiguredModel(router.cheapModel).model;\n\t\t\tif (routerModel && this.modelsAreEqual(routerModel, compactionModel)) {\n\t\t\t\treturn router.cheapThinking ?? \"low\";\n\t\t\t}\n\t\t}\n\n\t\treturn this.modelsAreEqual(compactionModel, sessionModel) ? \"low\" : sessionThinkingLevel;\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/export-html/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,gCAAgC,CAAC;AASnF;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAChC,oFAAoF;IACpF,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACpF,4GAA4G;IAC5G,YAAY,CACX,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EAChF,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,GACd;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACzD;AASD,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAgMD;;;GAGG;AACH,wBAAsB,mBAAmB,CACxC,EAAE,EAAE,cAAc,EAClB,KAAK,CAAC,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,GAC9B,OAAO,CAAC,MAAM,CAAC,CA0CjB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BzG","sourcesContent":["import type { AgentState } from \"@caupulican/pi-agent-core\";\nimport type { SessionEntry, SessionManager } from \"@caupulican/pi-agent-core/node\";\nimport { existsSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { APP_NAME, getExportTemplateDir } from \"../../config.ts\";\nimport { getResolvedThemeColors, getThemeExportColors } from \"../../modes/interactive/theme/theme.ts\";\nimport { normalizePath, resolvePath } from \"../../utils/paths.ts\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { openSession } from \"../session-manager-factory.ts\";\n\n/**\n * Interface for rendering custom tools to HTML.\n * Used by agent-session to pre-render extension tool output.\n */\nexport interface ToolHtmlRenderer {\n\t/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */\n\trenderCall(toolCallId: string, toolName: string, args: unknown): string | undefined;\n\t/** Render a tool result to HTML. Returns collapsed/expanded or undefined if tool has no custom renderer. */\n\trenderResult(\n\t\ttoolCallId: string,\n\t\ttoolName: string,\n\t\tresult: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,\n\t\tdetails: unknown,\n\t\tisError: boolean,\n\t): { collapsed?: string; expanded?: string } | undefined;\n}\n\n/** Pre-rendered HTML for a custom tool call and result */\ninterface RenderedToolHtml {\n\tcallHtml?: string;\n\tresultHtmlCollapsed?: string;\n\tresultHtmlExpanded?: string;\n}\n\nexport interface ExportOptions {\n\toutputPath?: string;\n\tthemeName?: string;\n\t/** Optional tool renderer for custom tools */\n\ttoolRenderer?: ToolHtmlRenderer;\n}\n\n/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */\nfunction parseColor(color: string): { r: number; g: number; b: number } | undefined {\n\tconst hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);\n\tif (hexMatch) {\n\t\treturn {\n\t\t\tr: Number.parseInt(hexMatch[1], 16),\n\t\t\tg: Number.parseInt(hexMatch[2], 16),\n\t\t\tb: Number.parseInt(hexMatch[3], 16),\n\t\t};\n\t}\n\tconst rgbMatch = color.match(/^rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)$/);\n\tif (rgbMatch) {\n\t\treturn {\n\t\t\tr: Number.parseInt(rgbMatch[1], 10),\n\t\t\tg: Number.parseInt(rgbMatch[2], 10),\n\t\t\tb: Number.parseInt(rgbMatch[3], 10),\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** Calculate relative luminance of a color (0-1, higher = lighter). */\nfunction getLuminance(r: number, g: number, b: number): number {\n\tconst toLinear = (c: number) => {\n\t\tconst s = c / 255;\n\t\treturn s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n\t};\n\treturn 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n}\n\n/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */\nfunction adjustBrightness(color: string, factor: number): string {\n\tconst parsed = parseColor(color);\n\tif (!parsed) return color;\n\tconst adjust = (c: number) => Math.min(255, Math.max(0, Math.round(c * factor)));\n\treturn `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`;\n}\n\n/** Derive export background colors from a base color (e.g., userMessageBg). */\nfunction deriveExportColors(baseColor: string): { pageBg: string; cardBg: string; infoBg: string } {\n\tconst parsed = parseColor(baseColor);\n\tif (!parsed) {\n\t\treturn {\n\t\t\tpageBg: \"rgb(24, 24, 30)\",\n\t\t\tcardBg: \"rgb(30, 30, 36)\",\n\t\t\tinfoBg: \"rgb(60, 55, 40)\",\n\t\t};\n\t}\n\n\tconst luminance = getLuminance(parsed.r, parsed.g, parsed.b);\n\tconst isLight = luminance > 0.5;\n\n\tif (isLight) {\n\t\treturn {\n\t\t\tpageBg: adjustBrightness(baseColor, 0.96),\n\t\t\tcardBg: baseColor,\n\t\t\tinfoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`,\n\t\t};\n\t}\n\treturn {\n\t\tpageBg: adjustBrightness(baseColor, 0.7),\n\t\tcardBg: adjustBrightness(baseColor, 0.85),\n\t\tinfoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`,\n\t};\n}\n\n/**\n * Generate CSS custom property declarations from theme colors.\n */\nfunction generateThemeVars(themeName?: string): string {\n\tconst colors = getResolvedThemeColors(themeName);\n\tconst lines: string[] = [];\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tlines.push(`--${key}: ${value};`);\n\t}\n\n\t// Use explicit theme export colors if available, otherwise derive from userMessageBg\n\tconst themeExport = getThemeExportColors(themeName);\n\tconst userMessageBg = colors.userMessageBg || \"#343541\";\n\tconst derivedColors = deriveExportColors(userMessageBg);\n\n\tlines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`);\n\tlines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`);\n\tlines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`);\n\n\treturn lines.join(\"\\n \");\n}\n\ninterface SessionData {\n\theader: ReturnType<SessionManager[\"getHeader\"]>;\n\tentries: ReturnType<SessionManager[\"getEntries\"]>;\n\tleafId: string | null;\n\tsystemPrompt?: string;\n\ttools?: Array<Pick<ToolDefinition, \"name\" | \"description\" | \"parameters\">>;\n\t/** Pre-rendered HTML for custom tool calls/results, keyed by tool call ID */\n\trenderedTools?: Record<string, RenderedToolHtml>;\n}\n\n/**\n * Core HTML generation logic shared by both export functions.\n */\nfunction generateHtml(sessionData: SessionData, themeName?: string): string {\n\tconst templateDir = getExportTemplateDir();\n\tconst template = readFileSync(join(templateDir, \"template.html\"), \"utf-8\");\n\tconst templateCss = readFileSync(join(templateDir, \"template.css\"), \"utf-8\");\n\tconst templateJs = readFileSync(join(templateDir, \"template.js\"), \"utf-8\");\n\tconst markedJs = readFileSync(join(templateDir, \"vendor\", \"marked.min.js\"), \"utf-8\");\n\tconst hljsJs = readFileSync(join(templateDir, \"vendor\", \"highlight.min.js\"), \"utf-8\");\n\n\tconst themeVars = generateThemeVars(themeName);\n\tconst colors = getResolvedThemeColors(themeName);\n\tconst themeExport = getThemeExportColors(themeName);\n\tconst derivedExportColors = deriveExportColors(colors.userMessageBg || \"#343541\");\n\tconst bodyBg = themeExport.pageBg ?? derivedExportColors.pageBg;\n\tconst containerBg = themeExport.cardBg ?? derivedExportColors.cardBg;\n\tconst infoBg = themeExport.infoBg ?? derivedExportColors.infoBg;\n\n\t// Base64 encode session data to avoid escaping issues\n\tconst sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString(\"base64\");\n\n\t// Build the CSS with theme variables injected\n\tconst css = templateCss\n\t\t.replace(\"{{THEME_VARS}}\", themeVars)\n\t\t.replace(\"{{BODY_BG}}\", bodyBg)\n\t\t.replace(\"{{CONTAINER_BG}}\", containerBg)\n\t\t.replace(\"{{INFO_BG}}\", infoBg);\n\n\treturn template\n\t\t.replace(\"{{CSS}}\", css)\n\t\t.replace(\"{{JS}}\", templateJs)\n\t\t.replace(\"{{SESSION_DATA}}\", sessionDataBase64)\n\t\t.replace(\"{{MARKED_JS}}\", markedJs)\n\t\t.replace(\"{{HIGHLIGHT_JS}}\", hljsJs);\n}\n\n/** Tools rendered directly by the HTML template (not pre-rendered via TUI→ANSI→HTML pipeline) */\nconst TEMPLATE_RENDERED_TOOLS = new Set([\"bash\", \"read\", \"write\", \"edit\", \"ls\"]);\n\n/**\n * Pre-render custom tools to HTML using their TUI renderers.\n */\nfunction preRenderCustomTools(\n\tentries: SessionEntry[],\n\ttoolRenderer: ToolHtmlRenderer,\n): Record<string, RenderedToolHtml> {\n\tconst renderedTools: Record<string, RenderedToolHtml> = {};\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst msg = entry.message;\n\n\t\t// Find tool calls in assistant messages\n\t\tif (msg.role === \"assistant\" && Array.isArray(msg.content)) {\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"toolCall\" && !TEMPLATE_RENDERED_TOOLS.has(block.name)) {\n\t\t\t\t\tconst callHtml = toolRenderer.renderCall(block.id, block.name, block.arguments);\n\t\t\t\t\tif (callHtml) {\n\t\t\t\t\t\trenderedTools[block.id] = { callHtml };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Find tool results\n\t\tif (msg.role === \"toolResult\" && msg.toolCallId) {\n\t\t\tconst toolName = msg.toolName || \"\";\n\t\t\t// Only render if we have a pre-rendered call OR it's not template-rendered\n\t\t\tconst existing = renderedTools[msg.toolCallId];\n\t\t\tif (existing || !TEMPLATE_RENDERED_TOOLS.has(toolName)) {\n\t\t\t\tconst rendered = toolRenderer.renderResult(\n\t\t\t\t\tmsg.toolCallId,\n\t\t\t\t\ttoolName,\n\t\t\t\t\tmsg.content,\n\t\t\t\t\tmsg.details,\n\t\t\t\t\tmsg.isError || false,\n\t\t\t\t);\n\t\t\t\tif (rendered) {\n\t\t\t\t\trenderedTools[msg.toolCallId] = {\n\t\t\t\t\t\t...existing,\n\t\t\t\t\t\tresultHtmlCollapsed: rendered.collapsed,\n\t\t\t\t\t\tresultHtmlExpanded: rendered.expanded,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn renderedTools;\n}\n\n/**\n * Export session to HTML using SessionManager and AgentState.\n * Used by TUI's /export command.\n */\nexport async function exportSessionToHtml(\n\tsm: SessionManager,\n\tstate?: AgentState,\n\toptions?: ExportOptions | string,\n): Promise<string> {\n\tconst opts: ExportOptions = typeof options === \"string\" ? { outputPath: options } : options || {};\n\n\tconst sessionFile = sm.getSessionFile();\n\tif (!sessionFile) {\n\t\tthrow new Error(\"Cannot export in-memory session to HTML\");\n\t}\n\tif (!existsSync(sessionFile)) {\n\t\tthrow new Error(\"Nothing to export yet - start a conversation first\");\n\t}\n\n\tconst entries = sm.getEntries();\n\n\t// Pre-render custom tools if a tool renderer is provided\n\tlet renderedTools: Record<string, RenderedToolHtml> | undefined;\n\tif (opts.toolRenderer) {\n\t\trenderedTools = preRenderCustomTools(entries, opts.toolRenderer);\n\t\t// Only include if we actually rendered something\n\t\tif (Object.keys(renderedTools).length === 0) {\n\t\t\trenderedTools = undefined;\n\t\t}\n\t}\n\n\tconst sessionData: SessionData = {\n\t\theader: sm.getHeader(),\n\t\tentries,\n\t\tleafId: sm.getLeafId(),\n\t\tsystemPrompt: state?.systemPrompt,\n\t\ttools: state?.tools?.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })),\n\t\trenderedTools,\n\t};\n\n\tconst html = generateHtml(sessionData, opts.themeName);\n\n\tlet outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;\n\tif (!outputPath) {\n\t\tconst sessionBasename = basename(sessionFile, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${sessionBasename}.html`;\n\t}\n\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n\n/**\n * Export session file to HTML (standalone, without AgentState).\n * Used by CLI for exporting arbitrary session files.\n */\nexport async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {\n\tconst opts: ExportOptions = typeof options === \"string\" ? { outputPath: options } : options || {};\n\tconst resolvedInputPath = resolvePath(inputPath);\n\n\tif (!existsSync(resolvedInputPath)) {\n\t\tthrow new Error(`File not found: ${resolvedInputPath}`);\n\t}\n\n\tconst sm = openSession(resolvedInputPath);\n\n\tconst sessionData: SessionData = {\n\t\theader: sm.getHeader(),\n\t\tentries: sm.getEntries(),\n\t\tleafId: sm.getLeafId(),\n\t\tsystemPrompt: undefined,\n\t\ttools: undefined,\n\t};\n\n\tconst html = generateHtml(sessionData, opts.themeName);\n\n\tlet outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;\n\tif (!outputPath) {\n\t\tconst inputBasename = basename(resolvedInputPath, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${inputBasename}.html`;\n\t}\n\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/export-html/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,gCAAgC,CAAC;AASnF;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAChC,oFAAoF;IACpF,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACpF,4GAA4G;IAC5G,YAAY,CACX,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EAChF,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,GACd;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACzD;AASD,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAgMD;;;GAGG;AACH,wBAAsB,mBAAmB,CACxC,EAAE,EAAE,cAAc,EAClB,KAAK,CAAC,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,GAC9B,OAAO,CAAC,MAAM,CAAC,CA0CjB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BzG","sourcesContent":["import type { AgentState } from \"@caupulican/pi-agent-core\";\nimport type { SessionEntry, SessionManager } from \"@caupulican/pi-agent-core/node\";\nimport { existsSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { APP_NAME, getExportTemplateDir } from \"../../config.ts\";\nimport { getResolvedThemeColors, getThemeExportColors } from \"../../modes/interactive/theme/theme.ts\";\nimport { normalizePath, resolvePath } from \"../../utils/paths.ts\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { openSession } from \"../session-manager-factory.ts\";\n\n/**\n * Interface for rendering custom tools to HTML.\n * Used by agent-session to pre-render extension tool output.\n */\nexport interface ToolHtmlRenderer {\n\t/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */\n\trenderCall(toolCallId: string, toolName: string, args: unknown): string | undefined;\n\t/** Render a tool result to HTML. Returns collapsed/expanded or undefined if tool has no custom renderer. */\n\trenderResult(\n\t\ttoolCallId: string,\n\t\ttoolName: string,\n\t\tresult: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,\n\t\tdetails: unknown,\n\t\tisError: boolean,\n\t): { collapsed?: string; expanded?: string } | undefined;\n}\n\n/** Pre-rendered HTML for a custom tool call and result */\ninterface RenderedToolHtml {\n\tcallHtml?: string;\n\tresultHtmlCollapsed?: string;\n\tresultHtmlExpanded?: string;\n}\n\nexport interface ExportOptions {\n\toutputPath?: string;\n\tthemeName?: string;\n\t/** Optional tool renderer for custom tools */\n\ttoolRenderer?: ToolHtmlRenderer;\n}\n\n/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */\nfunction parseColor(color: string): { r: number; g: number; b: number } | undefined {\n\tconst hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);\n\tif (hexMatch) {\n\t\treturn {\n\t\t\tr: Number.parseInt(hexMatch[1], 16),\n\t\t\tg: Number.parseInt(hexMatch[2], 16),\n\t\t\tb: Number.parseInt(hexMatch[3], 16),\n\t\t};\n\t}\n\tconst rgbMatch = color.match(/^rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)$/);\n\tif (rgbMatch) {\n\t\treturn {\n\t\t\tr: Number.parseInt(rgbMatch[1], 10),\n\t\t\tg: Number.parseInt(rgbMatch[2], 10),\n\t\t\tb: Number.parseInt(rgbMatch[3], 10),\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** Calculate relative luminance of a color (0-1, higher = lighter). */\nfunction getLuminance(r: number, g: number, b: number): number {\n\tconst toLinear = (c: number) => {\n\t\tconst s = c / 255;\n\t\treturn s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n\t};\n\treturn 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n}\n\n/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */\nfunction adjustBrightness(color: string, factor: number): string {\n\tconst parsed = parseColor(color);\n\tif (!parsed) return color;\n\tconst adjust = (c: number) => Math.min(255, Math.max(0, Math.round(c * factor)));\n\treturn `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`;\n}\n\n/** Derive export background colors from a base color (e.g., userMessageBg). */\nfunction deriveExportColors(baseColor: string): { pageBg: string; cardBg: string; infoBg: string } {\n\tconst parsed = parseColor(baseColor);\n\tif (!parsed) {\n\t\treturn {\n\t\t\tpageBg: \"rgb(24, 24, 30)\",\n\t\t\tcardBg: \"rgb(30, 30, 36)\",\n\t\t\tinfoBg: \"rgb(60, 55, 40)\",\n\t\t};\n\t}\n\n\tconst luminance = getLuminance(parsed.r, parsed.g, parsed.b);\n\tconst isLight = luminance > 0.5;\n\n\tif (isLight) {\n\t\treturn {\n\t\t\tpageBg: adjustBrightness(baseColor, 0.96),\n\t\t\tcardBg: baseColor,\n\t\t\tinfoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`,\n\t\t};\n\t}\n\treturn {\n\t\tpageBg: adjustBrightness(baseColor, 0.7),\n\t\tcardBg: adjustBrightness(baseColor, 0.85),\n\t\tinfoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`,\n\t};\n}\n\n/**\n * Generate CSS custom property declarations from theme colors.\n */\nfunction generateThemeVars(themeName?: string): string {\n\tconst colors = getResolvedThemeColors(themeName);\n\tconst lines: string[] = [];\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tlines.push(`--${key}: ${value};`);\n\t}\n\n\t// Use explicit theme export colors if available, otherwise derive from userMessageBg\n\tconst themeExport = getThemeExportColors(themeName);\n\tconst userMessageBg = colors.userMessageBg || \"#343541\";\n\tconst derivedColors = deriveExportColors(userMessageBg);\n\n\tlines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`);\n\tlines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`);\n\tlines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`);\n\n\treturn lines.join(\"\\n \");\n}\n\ninterface SessionData {\n\theader: ReturnType<SessionManager[\"getHeader\"]>;\n\tentries: ReturnType<SessionManager[\"getEntries\"]>;\n\tleafId: string | null;\n\tsystemPrompt?: string;\n\ttools?: Array<Pick<ToolDefinition, \"name\" | \"description\" | \"parameters\">>;\n\t/** Pre-rendered HTML for custom tool calls/results, keyed by tool call ID */\n\trenderedTools?: Record<string, RenderedToolHtml>;\n}\n\n/**\n * Core HTML generation logic shared by both export functions.\n */\nfunction generateHtml(sessionData: SessionData, themeName?: string): string {\n\tconst templateDir = getExportTemplateDir();\n\tconst template = readFileSync(join(templateDir, \"template.html\"), \"utf-8\");\n\tconst templateCss = readFileSync(join(templateDir, \"template.css\"), \"utf-8\");\n\tconst templateJs = readFileSync(join(templateDir, \"template.js\"), \"utf-8\");\n\tconst markedJs = readFileSync(join(templateDir, \"vendor\", \"marked.min.js\"), \"utf-8\");\n\tconst hljsJs = readFileSync(join(templateDir, \"vendor\", \"highlight.min.js\"), \"utf-8\");\n\n\tconst themeVars = generateThemeVars(themeName);\n\tconst colors = getResolvedThemeColors(themeName);\n\tconst themeExport = getThemeExportColors(themeName);\n\tconst derivedExportColors = deriveExportColors(colors.userMessageBg || \"#343541\");\n\tconst bodyBg = themeExport.pageBg ?? derivedExportColors.pageBg;\n\tconst containerBg = themeExport.cardBg ?? derivedExportColors.cardBg;\n\tconst infoBg = themeExport.infoBg ?? derivedExportColors.infoBg;\n\n\t// Base64 encode session data to avoid escaping issues\n\tconst sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString(\"base64\");\n\n\t// Build the CSS with theme variables injected\n\tconst css = templateCss\n\t\t.replace(\"{{THEME_VARS}}\", () => themeVars)\n\t\t.replace(\"{{BODY_BG}}\", () => bodyBg)\n\t\t.replace(\"{{CONTAINER_BG}}\", () => containerBg)\n\t\t.replace(\"{{INFO_BG}}\", () => infoBg);\n\n\treturn template\n\t\t.replace(\"{{CSS}}\", () => css)\n\t\t.replace(\"{{JS}}\", () => templateJs)\n\t\t.replace(\"{{SESSION_DATA}}\", () => sessionDataBase64)\n\t\t.replace(\"{{MARKED_JS}}\", () => markedJs)\n\t\t.replace(\"{{HIGHLIGHT_JS}}\", () => hljsJs);\n}\n\n/** Tools rendered directly by the HTML template (not pre-rendered via TUI→ANSI→HTML pipeline) */\nconst TEMPLATE_RENDERED_TOOLS = new Set([\"bash\", \"read\", \"write\", \"edit\", \"ls\"]);\n\n/**\n * Pre-render custom tools to HTML using their TUI renderers.\n */\nfunction preRenderCustomTools(\n\tentries: SessionEntry[],\n\ttoolRenderer: ToolHtmlRenderer,\n): Record<string, RenderedToolHtml> {\n\tconst renderedTools: Record<string, RenderedToolHtml> = {};\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst msg = entry.message;\n\n\t\t// Find tool calls in assistant messages\n\t\tif (msg.role === \"assistant\" && Array.isArray(msg.content)) {\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"toolCall\" && !TEMPLATE_RENDERED_TOOLS.has(block.name)) {\n\t\t\t\t\tconst callHtml = toolRenderer.renderCall(block.id, block.name, block.arguments);\n\t\t\t\t\tif (callHtml) {\n\t\t\t\t\t\trenderedTools[block.id] = { callHtml };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Find tool results\n\t\tif (msg.role === \"toolResult\" && msg.toolCallId) {\n\t\t\tconst toolName = msg.toolName || \"\";\n\t\t\t// Only render if we have a pre-rendered call OR it's not template-rendered\n\t\t\tconst existing = renderedTools[msg.toolCallId];\n\t\t\tif (existing || !TEMPLATE_RENDERED_TOOLS.has(toolName)) {\n\t\t\t\tconst rendered = toolRenderer.renderResult(\n\t\t\t\t\tmsg.toolCallId,\n\t\t\t\t\ttoolName,\n\t\t\t\t\tmsg.content,\n\t\t\t\t\tmsg.details,\n\t\t\t\t\tmsg.isError || false,\n\t\t\t\t);\n\t\t\t\tif (rendered) {\n\t\t\t\t\trenderedTools[msg.toolCallId] = {\n\t\t\t\t\t\t...existing,\n\t\t\t\t\t\tresultHtmlCollapsed: rendered.collapsed,\n\t\t\t\t\t\tresultHtmlExpanded: rendered.expanded,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn renderedTools;\n}\n\n/**\n * Export session to HTML using SessionManager and AgentState.\n * Used by TUI's /export command.\n */\nexport async function exportSessionToHtml(\n\tsm: SessionManager,\n\tstate?: AgentState,\n\toptions?: ExportOptions | string,\n): Promise<string> {\n\tconst opts: ExportOptions = typeof options === \"string\" ? { outputPath: options } : options || {};\n\n\tconst sessionFile = sm.getSessionFile();\n\tif (!sessionFile) {\n\t\tthrow new Error(\"Cannot export in-memory session to HTML\");\n\t}\n\tif (!existsSync(sessionFile)) {\n\t\tthrow new Error(\"Nothing to export yet - start a conversation first\");\n\t}\n\n\tconst entries = sm.getEntries();\n\n\t// Pre-render custom tools if a tool renderer is provided\n\tlet renderedTools: Record<string, RenderedToolHtml> | undefined;\n\tif (opts.toolRenderer) {\n\t\trenderedTools = preRenderCustomTools(entries, opts.toolRenderer);\n\t\t// Only include if we actually rendered something\n\t\tif (Object.keys(renderedTools).length === 0) {\n\t\t\trenderedTools = undefined;\n\t\t}\n\t}\n\n\tconst sessionData: SessionData = {\n\t\theader: sm.getHeader(),\n\t\tentries,\n\t\tleafId: sm.getLeafId(),\n\t\tsystemPrompt: state?.systemPrompt,\n\t\ttools: state?.tools?.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })),\n\t\trenderedTools,\n\t};\n\n\tconst html = generateHtml(sessionData, opts.themeName);\n\n\tlet outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;\n\tif (!outputPath) {\n\t\tconst sessionBasename = basename(sessionFile, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${sessionBasename}.html`;\n\t}\n\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n\n/**\n * Export session file to HTML (standalone, without AgentState).\n * Used by CLI for exporting arbitrary session files.\n */\nexport async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {\n\tconst opts: ExportOptions = typeof options === \"string\" ? { outputPath: options } : options || {};\n\tconst resolvedInputPath = resolvePath(inputPath);\n\n\tif (!existsSync(resolvedInputPath)) {\n\t\tthrow new Error(`File not found: ${resolvedInputPath}`);\n\t}\n\n\tconst sm = openSession(resolvedInputPath);\n\n\tconst sessionData: SessionData = {\n\t\theader: sm.getHeader(),\n\t\tentries: sm.getEntries(),\n\t\tleafId: sm.getLeafId(),\n\t\tsystemPrompt: undefined,\n\t\ttools: undefined,\n\t};\n\n\tconst html = generateHtml(sessionData, opts.themeName);\n\n\tlet outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;\n\tif (!outputPath) {\n\t\tconst inputBasename = basename(resolvedInputPath, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${inputBasename}.html`;\n\t}\n\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n"]}
@@ -104,16 +104,16 @@ function generateHtml(sessionData, themeName) {
104
104
  const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
105
105
  // Build the CSS with theme variables injected
106
106
  const css = templateCss
107
- .replace("{{THEME_VARS}}", themeVars)
108
- .replace("{{BODY_BG}}", bodyBg)
109
- .replace("{{CONTAINER_BG}}", containerBg)
110
- .replace("{{INFO_BG}}", infoBg);
107
+ .replace("{{THEME_VARS}}", () => themeVars)
108
+ .replace("{{BODY_BG}}", () => bodyBg)
109
+ .replace("{{CONTAINER_BG}}", () => containerBg)
110
+ .replace("{{INFO_BG}}", () => infoBg);
111
111
  return template
112
- .replace("{{CSS}}", css)
113
- .replace("{{JS}}", templateJs)
114
- .replace("{{SESSION_DATA}}", sessionDataBase64)
115
- .replace("{{MARKED_JS}}", markedJs)
116
- .replace("{{HIGHLIGHT_JS}}", hljsJs);
112
+ .replace("{{CSS}}", () => css)
113
+ .replace("{{JS}}", () => templateJs)
114
+ .replace("{{SESSION_DATA}}", () => sessionDataBase64)
115
+ .replace("{{MARKED_JS}}", () => markedJs)
116
+ .replace("{{HIGHLIGHT_JS}}", () => hljsJs);
117
117
  }
118
118
  /** Tools rendered directly by the HTML template (not pre-rendered via TUI→ANSI→HTML pipeline) */
119
119
  const TEMPLATE_RENDERED_TOOLS = new Set(["bash", "read", "write", "edit", "ls"]);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/export-html/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState } from \"@caupulican/pi-agent-core\";\nimport type { SessionEntry, SessionManager } from \"@caupulican/pi-agent-core/node\";\nimport { existsSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { APP_NAME, getExportTemplateDir } from \"../../config.ts\";\nimport { getResolvedThemeColors, getThemeExportColors } from \"../../modes/interactive/theme/theme.ts\";\nimport { normalizePath, resolvePath } from \"../../utils/paths.ts\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { openSession } from \"../session-manager-factory.ts\";\n\n/**\n * Interface for rendering custom tools to HTML.\n * Used by agent-session to pre-render extension tool output.\n */\nexport interface ToolHtmlRenderer {\n\t/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */\n\trenderCall(toolCallId: string, toolName: string, args: unknown): string | undefined;\n\t/** Render a tool result to HTML. Returns collapsed/expanded or undefined if tool has no custom renderer. */\n\trenderResult(\n\t\ttoolCallId: string,\n\t\ttoolName: string,\n\t\tresult: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,\n\t\tdetails: unknown,\n\t\tisError: boolean,\n\t): { collapsed?: string; expanded?: string } | undefined;\n}\n\n/** Pre-rendered HTML for a custom tool call and result */\ninterface RenderedToolHtml {\n\tcallHtml?: string;\n\tresultHtmlCollapsed?: string;\n\tresultHtmlExpanded?: string;\n}\n\nexport interface ExportOptions {\n\toutputPath?: string;\n\tthemeName?: string;\n\t/** Optional tool renderer for custom tools */\n\ttoolRenderer?: ToolHtmlRenderer;\n}\n\n/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */\nfunction parseColor(color: string): { r: number; g: number; b: number } | undefined {\n\tconst hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);\n\tif (hexMatch) {\n\t\treturn {\n\t\t\tr: Number.parseInt(hexMatch[1], 16),\n\t\t\tg: Number.parseInt(hexMatch[2], 16),\n\t\t\tb: Number.parseInt(hexMatch[3], 16),\n\t\t};\n\t}\n\tconst rgbMatch = color.match(/^rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)$/);\n\tif (rgbMatch) {\n\t\treturn {\n\t\t\tr: Number.parseInt(rgbMatch[1], 10),\n\t\t\tg: Number.parseInt(rgbMatch[2], 10),\n\t\t\tb: Number.parseInt(rgbMatch[3], 10),\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** Calculate relative luminance of a color (0-1, higher = lighter). */\nfunction getLuminance(r: number, g: number, b: number): number {\n\tconst toLinear = (c: number) => {\n\t\tconst s = c / 255;\n\t\treturn s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n\t};\n\treturn 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n}\n\n/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */\nfunction adjustBrightness(color: string, factor: number): string {\n\tconst parsed = parseColor(color);\n\tif (!parsed) return color;\n\tconst adjust = (c: number) => Math.min(255, Math.max(0, Math.round(c * factor)));\n\treturn `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`;\n}\n\n/** Derive export background colors from a base color (e.g., userMessageBg). */\nfunction deriveExportColors(baseColor: string): { pageBg: string; cardBg: string; infoBg: string } {\n\tconst parsed = parseColor(baseColor);\n\tif (!parsed) {\n\t\treturn {\n\t\t\tpageBg: \"rgb(24, 24, 30)\",\n\t\t\tcardBg: \"rgb(30, 30, 36)\",\n\t\t\tinfoBg: \"rgb(60, 55, 40)\",\n\t\t};\n\t}\n\n\tconst luminance = getLuminance(parsed.r, parsed.g, parsed.b);\n\tconst isLight = luminance > 0.5;\n\n\tif (isLight) {\n\t\treturn {\n\t\t\tpageBg: adjustBrightness(baseColor, 0.96),\n\t\t\tcardBg: baseColor,\n\t\t\tinfoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`,\n\t\t};\n\t}\n\treturn {\n\t\tpageBg: adjustBrightness(baseColor, 0.7),\n\t\tcardBg: adjustBrightness(baseColor, 0.85),\n\t\tinfoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`,\n\t};\n}\n\n/**\n * Generate CSS custom property declarations from theme colors.\n */\nfunction generateThemeVars(themeName?: string): string {\n\tconst colors = getResolvedThemeColors(themeName);\n\tconst lines: string[] = [];\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tlines.push(`--${key}: ${value};`);\n\t}\n\n\t// Use explicit theme export colors if available, otherwise derive from userMessageBg\n\tconst themeExport = getThemeExportColors(themeName);\n\tconst userMessageBg = colors.userMessageBg || \"#343541\";\n\tconst derivedColors = deriveExportColors(userMessageBg);\n\n\tlines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`);\n\tlines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`);\n\tlines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`);\n\n\treturn lines.join(\"\\n \");\n}\n\ninterface SessionData {\n\theader: ReturnType<SessionManager[\"getHeader\"]>;\n\tentries: ReturnType<SessionManager[\"getEntries\"]>;\n\tleafId: string | null;\n\tsystemPrompt?: string;\n\ttools?: Array<Pick<ToolDefinition, \"name\" | \"description\" | \"parameters\">>;\n\t/** Pre-rendered HTML for custom tool calls/results, keyed by tool call ID */\n\trenderedTools?: Record<string, RenderedToolHtml>;\n}\n\n/**\n * Core HTML generation logic shared by both export functions.\n */\nfunction generateHtml(sessionData: SessionData, themeName?: string): string {\n\tconst templateDir = getExportTemplateDir();\n\tconst template = readFileSync(join(templateDir, \"template.html\"), \"utf-8\");\n\tconst templateCss = readFileSync(join(templateDir, \"template.css\"), \"utf-8\");\n\tconst templateJs = readFileSync(join(templateDir, \"template.js\"), \"utf-8\");\n\tconst markedJs = readFileSync(join(templateDir, \"vendor\", \"marked.min.js\"), \"utf-8\");\n\tconst hljsJs = readFileSync(join(templateDir, \"vendor\", \"highlight.min.js\"), \"utf-8\");\n\n\tconst themeVars = generateThemeVars(themeName);\n\tconst colors = getResolvedThemeColors(themeName);\n\tconst themeExport = getThemeExportColors(themeName);\n\tconst derivedExportColors = deriveExportColors(colors.userMessageBg || \"#343541\");\n\tconst bodyBg = themeExport.pageBg ?? derivedExportColors.pageBg;\n\tconst containerBg = themeExport.cardBg ?? derivedExportColors.cardBg;\n\tconst infoBg = themeExport.infoBg ?? derivedExportColors.infoBg;\n\n\t// Base64 encode session data to avoid escaping issues\n\tconst sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString(\"base64\");\n\n\t// Build the CSS with theme variables injected\n\tconst css = templateCss\n\t\t.replace(\"{{THEME_VARS}}\", themeVars)\n\t\t.replace(\"{{BODY_BG}}\", bodyBg)\n\t\t.replace(\"{{CONTAINER_BG}}\", containerBg)\n\t\t.replace(\"{{INFO_BG}}\", infoBg);\n\n\treturn template\n\t\t.replace(\"{{CSS}}\", css)\n\t\t.replace(\"{{JS}}\", templateJs)\n\t\t.replace(\"{{SESSION_DATA}}\", sessionDataBase64)\n\t\t.replace(\"{{MARKED_JS}}\", markedJs)\n\t\t.replace(\"{{HIGHLIGHT_JS}}\", hljsJs);\n}\n\n/** Tools rendered directly by the HTML template (not pre-rendered via TUI→ANSI→HTML pipeline) */\nconst TEMPLATE_RENDERED_TOOLS = new Set([\"bash\", \"read\", \"write\", \"edit\", \"ls\"]);\n\n/**\n * Pre-render custom tools to HTML using their TUI renderers.\n */\nfunction preRenderCustomTools(\n\tentries: SessionEntry[],\n\ttoolRenderer: ToolHtmlRenderer,\n): Record<string, RenderedToolHtml> {\n\tconst renderedTools: Record<string, RenderedToolHtml> = {};\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst msg = entry.message;\n\n\t\t// Find tool calls in assistant messages\n\t\tif (msg.role === \"assistant\" && Array.isArray(msg.content)) {\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"toolCall\" && !TEMPLATE_RENDERED_TOOLS.has(block.name)) {\n\t\t\t\t\tconst callHtml = toolRenderer.renderCall(block.id, block.name, block.arguments);\n\t\t\t\t\tif (callHtml) {\n\t\t\t\t\t\trenderedTools[block.id] = { callHtml };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Find tool results\n\t\tif (msg.role === \"toolResult\" && msg.toolCallId) {\n\t\t\tconst toolName = msg.toolName || \"\";\n\t\t\t// Only render if we have a pre-rendered call OR it's not template-rendered\n\t\t\tconst existing = renderedTools[msg.toolCallId];\n\t\t\tif (existing || !TEMPLATE_RENDERED_TOOLS.has(toolName)) {\n\t\t\t\tconst rendered = toolRenderer.renderResult(\n\t\t\t\t\tmsg.toolCallId,\n\t\t\t\t\ttoolName,\n\t\t\t\t\tmsg.content,\n\t\t\t\t\tmsg.details,\n\t\t\t\t\tmsg.isError || false,\n\t\t\t\t);\n\t\t\t\tif (rendered) {\n\t\t\t\t\trenderedTools[msg.toolCallId] = {\n\t\t\t\t\t\t...existing,\n\t\t\t\t\t\tresultHtmlCollapsed: rendered.collapsed,\n\t\t\t\t\t\tresultHtmlExpanded: rendered.expanded,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn renderedTools;\n}\n\n/**\n * Export session to HTML using SessionManager and AgentState.\n * Used by TUI's /export command.\n */\nexport async function exportSessionToHtml(\n\tsm: SessionManager,\n\tstate?: AgentState,\n\toptions?: ExportOptions | string,\n): Promise<string> {\n\tconst opts: ExportOptions = typeof options === \"string\" ? { outputPath: options } : options || {};\n\n\tconst sessionFile = sm.getSessionFile();\n\tif (!sessionFile) {\n\t\tthrow new Error(\"Cannot export in-memory session to HTML\");\n\t}\n\tif (!existsSync(sessionFile)) {\n\t\tthrow new Error(\"Nothing to export yet - start a conversation first\");\n\t}\n\n\tconst entries = sm.getEntries();\n\n\t// Pre-render custom tools if a tool renderer is provided\n\tlet renderedTools: Record<string, RenderedToolHtml> | undefined;\n\tif (opts.toolRenderer) {\n\t\trenderedTools = preRenderCustomTools(entries, opts.toolRenderer);\n\t\t// Only include if we actually rendered something\n\t\tif (Object.keys(renderedTools).length === 0) {\n\t\t\trenderedTools = undefined;\n\t\t}\n\t}\n\n\tconst sessionData: SessionData = {\n\t\theader: sm.getHeader(),\n\t\tentries,\n\t\tleafId: sm.getLeafId(),\n\t\tsystemPrompt: state?.systemPrompt,\n\t\ttools: state?.tools?.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })),\n\t\trenderedTools,\n\t};\n\n\tconst html = generateHtml(sessionData, opts.themeName);\n\n\tlet outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;\n\tif (!outputPath) {\n\t\tconst sessionBasename = basename(sessionFile, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${sessionBasename}.html`;\n\t}\n\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n\n/**\n * Export session file to HTML (standalone, without AgentState).\n * Used by CLI for exporting arbitrary session files.\n */\nexport async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {\n\tconst opts: ExportOptions = typeof options === \"string\" ? { outputPath: options } : options || {};\n\tconst resolvedInputPath = resolvePath(inputPath);\n\n\tif (!existsSync(resolvedInputPath)) {\n\t\tthrow new Error(`File not found: ${resolvedInputPath}`);\n\t}\n\n\tconst sm = openSession(resolvedInputPath);\n\n\tconst sessionData: SessionData = {\n\t\theader: sm.getHeader(),\n\t\tentries: sm.getEntries(),\n\t\tleafId: sm.getLeafId(),\n\t\tsystemPrompt: undefined,\n\t\ttools: undefined,\n\t};\n\n\tconst html = generateHtml(sessionData, opts.themeName);\n\n\tlet outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;\n\tif (!outputPath) {\n\t\tconst inputBasename = basename(resolvedInputPath, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${inputBasename}.html`;\n\t}\n\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/export-html/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState } from \"@caupulican/pi-agent-core\";\nimport type { SessionEntry, SessionManager } from \"@caupulican/pi-agent-core/node\";\nimport { existsSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { APP_NAME, getExportTemplateDir } from \"../../config.ts\";\nimport { getResolvedThemeColors, getThemeExportColors } from \"../../modes/interactive/theme/theme.ts\";\nimport { normalizePath, resolvePath } from \"../../utils/paths.ts\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { openSession } from \"../session-manager-factory.ts\";\n\n/**\n * Interface for rendering custom tools to HTML.\n * Used by agent-session to pre-render extension tool output.\n */\nexport interface ToolHtmlRenderer {\n\t/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */\n\trenderCall(toolCallId: string, toolName: string, args: unknown): string | undefined;\n\t/** Render a tool result to HTML. Returns collapsed/expanded or undefined if tool has no custom renderer. */\n\trenderResult(\n\t\ttoolCallId: string,\n\t\ttoolName: string,\n\t\tresult: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,\n\t\tdetails: unknown,\n\t\tisError: boolean,\n\t): { collapsed?: string; expanded?: string } | undefined;\n}\n\n/** Pre-rendered HTML for a custom tool call and result */\ninterface RenderedToolHtml {\n\tcallHtml?: string;\n\tresultHtmlCollapsed?: string;\n\tresultHtmlExpanded?: string;\n}\n\nexport interface ExportOptions {\n\toutputPath?: string;\n\tthemeName?: string;\n\t/** Optional tool renderer for custom tools */\n\ttoolRenderer?: ToolHtmlRenderer;\n}\n\n/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */\nfunction parseColor(color: string): { r: number; g: number; b: number } | undefined {\n\tconst hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);\n\tif (hexMatch) {\n\t\treturn {\n\t\t\tr: Number.parseInt(hexMatch[1], 16),\n\t\t\tg: Number.parseInt(hexMatch[2], 16),\n\t\t\tb: Number.parseInt(hexMatch[3], 16),\n\t\t};\n\t}\n\tconst rgbMatch = color.match(/^rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)$/);\n\tif (rgbMatch) {\n\t\treturn {\n\t\t\tr: Number.parseInt(rgbMatch[1], 10),\n\t\t\tg: Number.parseInt(rgbMatch[2], 10),\n\t\t\tb: Number.parseInt(rgbMatch[3], 10),\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** Calculate relative luminance of a color (0-1, higher = lighter). */\nfunction getLuminance(r: number, g: number, b: number): number {\n\tconst toLinear = (c: number) => {\n\t\tconst s = c / 255;\n\t\treturn s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n\t};\n\treturn 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n}\n\n/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */\nfunction adjustBrightness(color: string, factor: number): string {\n\tconst parsed = parseColor(color);\n\tif (!parsed) return color;\n\tconst adjust = (c: number) => Math.min(255, Math.max(0, Math.round(c * factor)));\n\treturn `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`;\n}\n\n/** Derive export background colors from a base color (e.g., userMessageBg). */\nfunction deriveExportColors(baseColor: string): { pageBg: string; cardBg: string; infoBg: string } {\n\tconst parsed = parseColor(baseColor);\n\tif (!parsed) {\n\t\treturn {\n\t\t\tpageBg: \"rgb(24, 24, 30)\",\n\t\t\tcardBg: \"rgb(30, 30, 36)\",\n\t\t\tinfoBg: \"rgb(60, 55, 40)\",\n\t\t};\n\t}\n\n\tconst luminance = getLuminance(parsed.r, parsed.g, parsed.b);\n\tconst isLight = luminance > 0.5;\n\n\tif (isLight) {\n\t\treturn {\n\t\t\tpageBg: adjustBrightness(baseColor, 0.96),\n\t\t\tcardBg: baseColor,\n\t\t\tinfoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`,\n\t\t};\n\t}\n\treturn {\n\t\tpageBg: adjustBrightness(baseColor, 0.7),\n\t\tcardBg: adjustBrightness(baseColor, 0.85),\n\t\tinfoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`,\n\t};\n}\n\n/**\n * Generate CSS custom property declarations from theme colors.\n */\nfunction generateThemeVars(themeName?: string): string {\n\tconst colors = getResolvedThemeColors(themeName);\n\tconst lines: string[] = [];\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tlines.push(`--${key}: ${value};`);\n\t}\n\n\t// Use explicit theme export colors if available, otherwise derive from userMessageBg\n\tconst themeExport = getThemeExportColors(themeName);\n\tconst userMessageBg = colors.userMessageBg || \"#343541\";\n\tconst derivedColors = deriveExportColors(userMessageBg);\n\n\tlines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`);\n\tlines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`);\n\tlines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`);\n\n\treturn lines.join(\"\\n \");\n}\n\ninterface SessionData {\n\theader: ReturnType<SessionManager[\"getHeader\"]>;\n\tentries: ReturnType<SessionManager[\"getEntries\"]>;\n\tleafId: string | null;\n\tsystemPrompt?: string;\n\ttools?: Array<Pick<ToolDefinition, \"name\" | \"description\" | \"parameters\">>;\n\t/** Pre-rendered HTML for custom tool calls/results, keyed by tool call ID */\n\trenderedTools?: Record<string, RenderedToolHtml>;\n}\n\n/**\n * Core HTML generation logic shared by both export functions.\n */\nfunction generateHtml(sessionData: SessionData, themeName?: string): string {\n\tconst templateDir = getExportTemplateDir();\n\tconst template = readFileSync(join(templateDir, \"template.html\"), \"utf-8\");\n\tconst templateCss = readFileSync(join(templateDir, \"template.css\"), \"utf-8\");\n\tconst templateJs = readFileSync(join(templateDir, \"template.js\"), \"utf-8\");\n\tconst markedJs = readFileSync(join(templateDir, \"vendor\", \"marked.min.js\"), \"utf-8\");\n\tconst hljsJs = readFileSync(join(templateDir, \"vendor\", \"highlight.min.js\"), \"utf-8\");\n\n\tconst themeVars = generateThemeVars(themeName);\n\tconst colors = getResolvedThemeColors(themeName);\n\tconst themeExport = getThemeExportColors(themeName);\n\tconst derivedExportColors = deriveExportColors(colors.userMessageBg || \"#343541\");\n\tconst bodyBg = themeExport.pageBg ?? derivedExportColors.pageBg;\n\tconst containerBg = themeExport.cardBg ?? derivedExportColors.cardBg;\n\tconst infoBg = themeExport.infoBg ?? derivedExportColors.infoBg;\n\n\t// Base64 encode session data to avoid escaping issues\n\tconst sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString(\"base64\");\n\n\t// Build the CSS with theme variables injected\n\tconst css = templateCss\n\t\t.replace(\"{{THEME_VARS}}\", () => themeVars)\n\t\t.replace(\"{{BODY_BG}}\", () => bodyBg)\n\t\t.replace(\"{{CONTAINER_BG}}\", () => containerBg)\n\t\t.replace(\"{{INFO_BG}}\", () => infoBg);\n\n\treturn template\n\t\t.replace(\"{{CSS}}\", () => css)\n\t\t.replace(\"{{JS}}\", () => templateJs)\n\t\t.replace(\"{{SESSION_DATA}}\", () => sessionDataBase64)\n\t\t.replace(\"{{MARKED_JS}}\", () => markedJs)\n\t\t.replace(\"{{HIGHLIGHT_JS}}\", () => hljsJs);\n}\n\n/** Tools rendered directly by the HTML template (not pre-rendered via TUI→ANSI→HTML pipeline) */\nconst TEMPLATE_RENDERED_TOOLS = new Set([\"bash\", \"read\", \"write\", \"edit\", \"ls\"]);\n\n/**\n * Pre-render custom tools to HTML using their TUI renderers.\n */\nfunction preRenderCustomTools(\n\tentries: SessionEntry[],\n\ttoolRenderer: ToolHtmlRenderer,\n): Record<string, RenderedToolHtml> {\n\tconst renderedTools: Record<string, RenderedToolHtml> = {};\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst msg = entry.message;\n\n\t\t// Find tool calls in assistant messages\n\t\tif (msg.role === \"assistant\" && Array.isArray(msg.content)) {\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"toolCall\" && !TEMPLATE_RENDERED_TOOLS.has(block.name)) {\n\t\t\t\t\tconst callHtml = toolRenderer.renderCall(block.id, block.name, block.arguments);\n\t\t\t\t\tif (callHtml) {\n\t\t\t\t\t\trenderedTools[block.id] = { callHtml };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Find tool results\n\t\tif (msg.role === \"toolResult\" && msg.toolCallId) {\n\t\t\tconst toolName = msg.toolName || \"\";\n\t\t\t// Only render if we have a pre-rendered call OR it's not template-rendered\n\t\t\tconst existing = renderedTools[msg.toolCallId];\n\t\t\tif (existing || !TEMPLATE_RENDERED_TOOLS.has(toolName)) {\n\t\t\t\tconst rendered = toolRenderer.renderResult(\n\t\t\t\t\tmsg.toolCallId,\n\t\t\t\t\ttoolName,\n\t\t\t\t\tmsg.content,\n\t\t\t\t\tmsg.details,\n\t\t\t\t\tmsg.isError || false,\n\t\t\t\t);\n\t\t\t\tif (rendered) {\n\t\t\t\t\trenderedTools[msg.toolCallId] = {\n\t\t\t\t\t\t...existing,\n\t\t\t\t\t\tresultHtmlCollapsed: rendered.collapsed,\n\t\t\t\t\t\tresultHtmlExpanded: rendered.expanded,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn renderedTools;\n}\n\n/**\n * Export session to HTML using SessionManager and AgentState.\n * Used by TUI's /export command.\n */\nexport async function exportSessionToHtml(\n\tsm: SessionManager,\n\tstate?: AgentState,\n\toptions?: ExportOptions | string,\n): Promise<string> {\n\tconst opts: ExportOptions = typeof options === \"string\" ? { outputPath: options } : options || {};\n\n\tconst sessionFile = sm.getSessionFile();\n\tif (!sessionFile) {\n\t\tthrow new Error(\"Cannot export in-memory session to HTML\");\n\t}\n\tif (!existsSync(sessionFile)) {\n\t\tthrow new Error(\"Nothing to export yet - start a conversation first\");\n\t}\n\n\tconst entries = sm.getEntries();\n\n\t// Pre-render custom tools if a tool renderer is provided\n\tlet renderedTools: Record<string, RenderedToolHtml> | undefined;\n\tif (opts.toolRenderer) {\n\t\trenderedTools = preRenderCustomTools(entries, opts.toolRenderer);\n\t\t// Only include if we actually rendered something\n\t\tif (Object.keys(renderedTools).length === 0) {\n\t\t\trenderedTools = undefined;\n\t\t}\n\t}\n\n\tconst sessionData: SessionData = {\n\t\theader: sm.getHeader(),\n\t\tentries,\n\t\tleafId: sm.getLeafId(),\n\t\tsystemPrompt: state?.systemPrompt,\n\t\ttools: state?.tools?.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })),\n\t\trenderedTools,\n\t};\n\n\tconst html = generateHtml(sessionData, opts.themeName);\n\n\tlet outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;\n\tif (!outputPath) {\n\t\tconst sessionBasename = basename(sessionFile, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${sessionBasename}.html`;\n\t}\n\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n\n/**\n * Export session file to HTML (standalone, without AgentState).\n * Used by CLI for exporting arbitrary session files.\n */\nexport async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {\n\tconst opts: ExportOptions = typeof options === \"string\" ? { outputPath: options } : options || {};\n\tconst resolvedInputPath = resolvePath(inputPath);\n\n\tif (!existsSync(resolvedInputPath)) {\n\t\tthrow new Error(`File not found: ${resolvedInputPath}`);\n\t}\n\n\tconst sm = openSession(resolvedInputPath);\n\n\tconst sessionData: SessionData = {\n\t\theader: sm.getHeader(),\n\t\tentries: sm.getEntries(),\n\t\tleafId: sm.getLeafId(),\n\t\tsystemPrompt: undefined,\n\t\ttools: undefined,\n\t};\n\n\tconst html = generateHtml(sessionData, opts.themeName);\n\n\tlet outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;\n\tif (!outputPath) {\n\t\tconst inputBasename = basename(resolvedInputPath, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${inputBasename}.html`;\n\t}\n\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n"]}