@caupulican/pi-adaptative 0.81.0 → 0.81.2

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.
@@ -5,6 +5,7 @@
5
5
  * Extracted verbatim from AgentSession (agent-session.ts) as a narrow-deps collaborator; the
6
6
  * session keeps same-signature private delegations at every call-in point.
7
7
  */
8
+ import type { ThinkingLevel } from "@caupulican/pi-agent-core";
8
9
  import type { CompactionSettings } from "@caupulican/pi-agent-core/node";
9
10
  import type { Model } from "@caupulican/pi-ai";
10
11
  import type { ModelRegistry } from "./model-registry.ts";
@@ -42,15 +43,18 @@ export declare class CompactionSupport {
42
43
  headers?: Record<string, string>;
43
44
  failure?: string;
44
45
  }>;
46
+ private getExplicitCompactionModelSetting;
47
+ private resolveConfiguredModel;
48
+ private resolveDefaultModel;
49
+ private modelsAreEqual;
45
50
  /**
46
- * Resolve the model used to SUMMARIZE during compaction (cost guard, #30). A compaction summary is an
47
- * extraction task — it does not need the main (expensive) model. Selection:
51
+ * Resolve the model used to SUMMARIZE during compaction. Selection:
48
52
  * - an explicit `compaction.model` setting wins, but only if its provider is authed (else fall back);
49
- * - `"auto"` (default) picks the CHEAPEST authed model whose context window can hold a compaction
50
- * (capability floor), and ONLY if it is strictly cheaper than the session model — so we never
51
- * downgrade to an equally-priced but weaker summarizer (agy's floor: don't degrade the checkpoint);
53
+ * - `"auto"`/unset follows the model router's configured cheap model when the router is enabled;
52
54
  * - otherwise the session model is used (safe default).
53
55
  */
54
56
  resolveModel(sessionModel: Model<any>): Model<any>;
57
+ /** Default compaction should never inherit expensive session thinking. */
58
+ resolveThinkingLevel(sessionThinkingLevel: ThinkingLevel | undefined, compactionModel: Model<any>, sessionModel: Model<any>): ThinkingLevel | undefined;
55
59
  }
56
60
  //# sourceMappingURL=compaction-support.d.ts.map
@@ -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,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;;;;;;;;OAQG;IACH,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAoBjD;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 { 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\t/**\n\t * Resolve the model used to SUMMARIZE during compaction (cost guard, #30). A compaction summary is an\n\t * extraction task it does not need the main (expensive) model. Selection:\n\t * - an explicit `compaction.model` setting wins, but only if its provider is authed (else fall back);\n\t * - `\"auto\"` (default) picks the CHEAPEST authed model whose context window can hold a compaction\n\t * (capability floor), and ONLY if it is strictly cheaper than the session model — so we never\n\t * downgrade to an equally-priced but weaker summarizer (agy's floor: don't degrade the checkpoint);\n\t * - otherwise the session model is used (safe default).\n\t */\n\tresolveModel(sessionModel: Model<any>): Model<any> {\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst setting = this.deps.getSettingsManager().getCompactionModel();\n\t\tif (setting && setting !== \"auto\") {\n\t\t\tconst resolved = resolveCliModel({ cliModel: setting, modelRegistry: registry });\n\t\t\tif (resolved.model && registry.hasConfiguredAuth(resolved.model)) return resolved.model;\n\t\t\treturn sessionModel; // configured but unusable don't break compaction\n\t\t}\n\t\t// \"auto\": cheapest authed model that can summarize a large context AND is cheaper than the session\n\t\t// model. The context-window floor keeps a tiny local model from being picked for a big summary.\n\t\tconst FLOOR_CONTEXT = 64_000;\n\t\tconst sessionInputCost = sessionModel.cost?.input ?? Number.POSITIVE_INFINITY;\n\t\tlet best: Model<any> | undefined;\n\t\tfor (const m of registry.getAvailable()) {\n\t\t\tif ((m.contextWindow ?? 0) < FLOOR_CONTEXT) continue;\n\t\t\tconst cost = m.cost?.input ?? Number.POSITIVE_INFINITY;\n\t\t\tif (cost >= sessionInputCost) continue; // only ever pick something cheaper than the session model\n\t\t\tif (!best || cost < (best.cost?.input ?? Number.POSITIVE_INFINITY)) best = m;\n\t\t}\n\t\treturn best ?? sessionModel;\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,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"]}
@@ -61,39 +61,50 @@ export class CompactionSupport {
61
61
  const { apiKey, headers } = await this.getRequestAuth(compactionModel);
62
62
  return { model: compactionModel, apiKey, headers };
63
63
  }
64
+ getExplicitCompactionModelSetting() {
65
+ const setting = this.deps.getSettingsManager().getCompactionModel().trim();
66
+ return setting && setting !== "auto" ? setting : undefined;
67
+ }
68
+ resolveConfiguredModel(pattern) {
69
+ const registry = this.deps.getModelRegistry();
70
+ const resolved = resolveCliModel({ cliModel: pattern, modelRegistry: registry });
71
+ if (!resolved.model || !registry.hasConfiguredAuth(resolved.model))
72
+ return undefined;
73
+ return resolved.model;
74
+ }
75
+ resolveDefaultModel(sessionModel) {
76
+ const router = this.deps.getSettingsManager().getModelRouterSettings();
77
+ if (!router.enabled || !router.cheapModel)
78
+ return sessionModel;
79
+ return this.resolveConfiguredModel(router.cheapModel) ?? sessionModel;
80
+ }
81
+ modelsAreEqual(left, right) {
82
+ return left.provider === right.provider && left.id === right.id;
83
+ }
64
84
  /**
65
- * Resolve the model used to SUMMARIZE during compaction (cost guard, #30). A compaction summary is an
66
- * extraction task — it does not need the main (expensive) model. Selection:
85
+ * Resolve the model used to SUMMARIZE during compaction. Selection:
67
86
  * - an explicit `compaction.model` setting wins, but only if its provider is authed (else fall back);
68
- * - `"auto"` (default) picks the CHEAPEST authed model whose context window can hold a compaction
69
- * (capability floor), and ONLY if it is strictly cheaper than the session model — so we never
70
- * downgrade to an equally-priced but weaker summarizer (agy's floor: don't degrade the checkpoint);
87
+ * - `"auto"`/unset follows the model router's configured cheap model when the router is enabled;
71
88
  * - otherwise the session model is used (safe default).
72
89
  */
73
90
  resolveModel(sessionModel) {
74
- const registry = this.deps.getModelRegistry();
75
- const setting = this.deps.getSettingsManager().getCompactionModel();
76
- if (setting && setting !== "auto") {
77
- const resolved = resolveCliModel({ cliModel: setting, modelRegistry: registry });
78
- if (resolved.model && registry.hasConfiguredAuth(resolved.model))
79
- return resolved.model;
80
- return sessionModel; // configured but unusable → don't break compaction
81
- }
82
- // "auto": cheapest authed model that can summarize a large context AND is cheaper than the session
83
- // model. The context-window floor keeps a tiny local model from being picked for a big summary.
84
- const FLOOR_CONTEXT = 64_000;
85
- const sessionInputCost = sessionModel.cost?.input ?? Number.POSITIVE_INFINITY;
86
- let best;
87
- for (const m of registry.getAvailable()) {
88
- if ((m.contextWindow ?? 0) < FLOOR_CONTEXT)
89
- continue;
90
- const cost = m.cost?.input ?? Number.POSITIVE_INFINITY;
91
- if (cost >= sessionInputCost)
92
- continue; // only ever pick something cheaper than the session model
93
- if (!best || cost < (best.cost?.input ?? Number.POSITIVE_INFINITY))
94
- best = m;
91
+ const explicitSetting = this.getExplicitCompactionModelSetting();
92
+ if (explicitSetting)
93
+ return this.resolveConfiguredModel(explicitSetting) ?? sessionModel;
94
+ return this.resolveDefaultModel(sessionModel);
95
+ }
96
+ /** Default compaction should never inherit expensive session thinking. */
97
+ resolveThinkingLevel(sessionThinkingLevel, compactionModel, sessionModel) {
98
+ if (this.getExplicitCompactionModelSetting())
99
+ return sessionThinkingLevel;
100
+ const router = this.deps.getSettingsManager().getModelRouterSettings();
101
+ if (router.enabled && router.cheapModel) {
102
+ const routerModel = this.resolveConfiguredModel(router.cheapModel);
103
+ if (routerModel && this.modelsAreEqual(routerModel, compactionModel)) {
104
+ return router.cheapThinking ?? "low";
105
+ }
95
106
  }
96
- return best ?? sessionModel;
107
+ return this.modelsAreEqual(compactionModel, sessionModel) ? "low" : sessionThinkingLevel;
97
108
  }
98
109
  }
99
110
  //# sourceMappingURL=compaction-support.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"compaction-support.js","sourceRoot":"","sources":["../../src/core/compaction-support.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAatD,MAAM,OAAO,iBAAiB;IACZ,IAAI,CAAwB;IAE7C,YAAY,IAA2B,EAAE;QACxC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAAA,CACjB;IAED,kBAAkB,GAAuB;QACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,qBAAqB,EAAE,CAAC;QACxE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACnC,IAAI,CAAC,KAAK;YAAE,OAAO,QAAQ,CAAC;QAC5B,MAAM,aAAa,GAAG,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC;QAC/C,IAAI,aAAa,IAAI,CAAC;YAAE,OAAO,QAAQ,CAAC;QAExC,qDAAqD;QACrD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;QACpD,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QAEnE,wDAAwD;QACxD,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,GAAG,CAAC,CAAC;QACtD,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;QAE5E,OAAO;YACN,GAAG,QAAQ;YACX,aAAa;YACb,gBAAgB;SAChB,CAAC;IAAA,CACF;IAED,KAAK,CAAC,cAAc,CAAC,KAAiB,EAGnC;QACF,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC;QAChD,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC;QAC7E,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAAA,CAC3E;IAED;;;;;;OAMG;IACH,KAAK,CAAC,mBAAmB,CACxB,eAA2B,EAC3B,YAAwB,EAC8E;QACtG,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC9C,IAAI,IAAI,GAAG,MAAM,QAAQ,CAAC,mBAAmB,CAAC,eAAe,CAAC,CAAC;YAC/D,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC5B,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;YAC/E,CAAC;YACD,MAAM,WAAW,GAChB,eAAe,CAAC,QAAQ,KAAK,YAAY,CAAC,QAAQ,IAAI,eAAe,CAAC,EAAE,KAAK,YAAY,CAAC,EAAE,CAAC;YAC9F,IAAI,CAAC,WAAW,EAAE,CAAC;gBAClB,IAAI,GAAG,MAAM,QAAQ,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;gBACxD,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBAC5B,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC5E,CAAC;YACF,CAAC;YACD,OAAO;gBACN,KAAK,EAAE,YAAY;gBACnB,OAAO,EAAE,+CAA+C,eAAe,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,YAAY,CAAC,EAAE,EAAE,GAAG;aAC5H,CAAC;QACH,CAAC;QAED,2FAAyF;QACzF,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACvE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAAA,CACnD;IAED;;;;;;;;OAQG;IACH,YAAY,CAAC,YAAwB,EAAc;QAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,kBAAkB,EAAE,CAAC;QACpE,IAAI,OAAO,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACnC,MAAM,QAAQ,GAAG,eAAe,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjF,IAAI,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,iBAAiB,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,OAAO,QAAQ,CAAC,KAAK,CAAC;YACxF,OAAO,YAAY,CAAC,CAAC,qDAAmD;QACzE,CAAC;QACD,mGAAmG;QACnG,gGAAgG;QAChG,MAAM,aAAa,GAAG,MAAM,CAAC;QAC7B,MAAM,gBAAgB,GAAG,YAAY,CAAC,IAAI,EAAE,KAAK,IAAI,MAAM,CAAC,iBAAiB,CAAC;QAC9E,IAAI,IAA4B,CAAC;QACjC,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC;YACzC,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,CAAC,GAAG,aAAa;gBAAE,SAAS;YACrD,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI,MAAM,CAAC,iBAAiB,CAAC;YACvD,IAAI,IAAI,IAAI,gBAAgB;gBAAE,SAAS,CAAC,0DAA0D;YAClG,IAAI,CAAC,IAAI,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,MAAM,CAAC,iBAAiB,CAAC;gBAAE,IAAI,GAAG,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,IAAI,IAAI,YAAY,CAAC;IAAA,CAC5B;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 { 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\t/**\n\t * Resolve the model used to SUMMARIZE during compaction (cost guard, #30). A compaction summary is an\n\t * extraction task — it does not need the main (expensive) model. Selection:\n\t * - an explicit `compaction.model` setting wins, but only if its provider is authed (else fall back);\n\t * - `\"auto\"` (default) picks the CHEAPEST authed model whose context window can hold a compaction\n\t * (capability floor), and ONLY if it is strictly cheaper than the session model — so we never\n\t * downgrade to an equally-priced but weaker summarizer (agy's floor: don't degrade the checkpoint);\n\t * - otherwise the session model is used (safe default).\n\t */\n\tresolveModel(sessionModel: Model<any>): Model<any> {\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst setting = this.deps.getSettingsManager().getCompactionModel();\n\t\tif (setting && setting !== \"auto\") {\n\t\t\tconst resolved = resolveCliModel({ cliModel: setting, modelRegistry: registry });\n\t\t\tif (resolved.model && registry.hasConfiguredAuth(resolved.model)) return resolved.model;\n\t\t\treturn sessionModel; // configured but unusable → don't break compaction\n\t\t}\n\t\t// \"auto\": cheapest authed model that can summarize a large context AND is cheaper than the session\n\t\t// model. The context-window floor keeps a tiny local model from being picked for a big summary.\n\t\tconst FLOOR_CONTEXT = 64_000;\n\t\tconst sessionInputCost = sessionModel.cost?.input ?? Number.POSITIVE_INFINITY;\n\t\tlet best: Model<any> | undefined;\n\t\tfor (const m of registry.getAvailable()) {\n\t\t\tif ((m.contextWindow ?? 0) < FLOOR_CONTEXT) continue;\n\t\t\tconst cost = m.cost?.input ?? Number.POSITIVE_INFINITY;\n\t\t\tif (cost >= sessionInputCost) continue; // only ever pick something cheaper than the session model\n\t\t\tif (!best || cost < (best.cost?.input ?? Number.POSITIVE_INFINITY)) best = m;\n\t\t}\n\t\treturn best ?? sessionModel;\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 } 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,4 +1,4 @@
1
- export declare const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300000;
1
+ export declare const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 660000;
2
2
  export declare const HTTP_IDLE_TIMEOUT_CHOICES: readonly [{
3
3
  readonly label: "30 sec";
4
4
  readonly timeoutMs: 30000;
@@ -11,6 +11,9 @@ export declare const HTTP_IDLE_TIMEOUT_CHOICES: readonly [{
11
11
  }, {
12
12
  readonly label: "5 min";
13
13
  readonly timeoutMs: 300000;
14
+ }, {
15
+ readonly label: "11 min";
16
+ readonly timeoutMs: 660000;
14
17
  }, {
15
18
  readonly label: "disabled";
16
19
  readonly timeoutMs: 0;
@@ -1 +1 @@
1
- {"version":3,"file":"http-dispatcher.d.ts","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAEpD,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;EAM5B,CAAC;AAEX,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAgBzE;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED,wBAAgB,uBAAuB,CAAC,SAAS,GAAE,MAAqC,GAAG,IAAI,CAgB9F","sourcesContent":["import * as undici from \"undici\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"2 min\", timeoutMs: 120_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t}),\n\t);\n\t// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's\n\t// bundled fetch can otherwise consume compressed responses through npm undici's\n\t// dispatcher without decompressing them, causing response.json() failures.\n\tundici.install?.();\n}\n"]}
1
+ {"version":3,"file":"http-dispatcher.d.ts","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAEpD,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;EAO5B,CAAC;AAEX,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAgBzE;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED,wBAAgB,uBAAuB,CAAC,SAAS,GAAE,MAAqC,GAAG,IAAI,CAgB9F","sourcesContent":["import * as undici from \"undici\";\n\n// Must stay strictly greater than the stall watchdog's quiet bound (600s — see\n// pi-agent-core DEFAULT_STREAM_IDLE): undici's body/headers timeouts are idle timeouts\n// between chunks, so a lower value here would kill quiet-but-healthy streams before the\n// phase-aware watchdog ever sees the gap. 660s = quiet bound + 60s margin.\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 660_000;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"2 min\", timeoutMs: 120_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"11 min\", timeoutMs: 660_000 },\n\t{ label: \"disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t}),\n\t);\n\t// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's\n\t// bundled fetch can otherwise consume compressed responses through npm undici's\n\t// dispatcher without decompressing them, causing response.json() failures.\n\tundici.install?.();\n}\n"]}
@@ -1,10 +1,15 @@
1
1
  import * as undici from "undici";
2
- export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
2
+ // Must stay strictly greater than the stall watchdog's quiet bound (600s — see
3
+ // pi-agent-core DEFAULT_STREAM_IDLE): undici's body/headers timeouts are idle timeouts
4
+ // between chunks, so a lower value here would kill quiet-but-healthy streams before the
5
+ // phase-aware watchdog ever sees the gap. 660s = quiet bound + 60s margin.
6
+ export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 660_000;
3
7
  export const HTTP_IDLE_TIMEOUT_CHOICES = [
4
8
  { label: "30 sec", timeoutMs: 30_000 },
5
9
  { label: "1 min", timeoutMs: 60_000 },
6
10
  { label: "2 min", timeoutMs: 120_000 },
7
11
  { label: "5 min", timeoutMs: 300_000 },
12
+ { label: "11 min", timeoutMs: 660_000 },
8
13
  { label: "disabled", timeoutMs: 0 },
9
14
  ];
10
15
  export function parseHttpIdleTimeoutMs(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"http-dispatcher.js","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACxC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE;IACtC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE;IACrC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE;IACtC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE;IACtC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE;CAC1B,CAAC;AAEX,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAsB;IAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC;YAC1C,OAAO,CAAC,CAAC;QACV,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,OAAO,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAAA,CACzB;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAiB,EAAU;IAClE,MAAM,MAAM,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;IACtF,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO,MAAM,CAAC,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,SAAS,GAAG,IAAI,MAAM,CAAC;AAAA,CACjC;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAS,GAAW,4BAA4B,EAAQ;IAC/F,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,CAAC,mBAAmB,CACzB,IAAI,MAAM,CAAC,iBAAiB,CAAC;QAC5B,OAAO,EAAE,KAAK;QACd,WAAW,EAAE,mBAAmB;QAChC,cAAc,EAAE,mBAAmB;KACnC,CAAC,CACF,CAAC;IACF,+EAA+E;IAC/E,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;AAAA,CACnB","sourcesContent":["import * as undici from \"undici\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"2 min\", timeoutMs: 120_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t}),\n\t);\n\t// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's\n\t// bundled fetch can otherwise consume compressed responses through npm undici's\n\t// dispatcher without decompressing them, causing response.json() failures.\n\tundici.install?.();\n}\n"]}
1
+ {"version":3,"file":"http-dispatcher.js","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,iFAA+E;AAC/E,uFAAuF;AACvF,wFAAwF;AACxF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACxC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE;IACtC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE;IACrC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE;IACtC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE;IACtC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE;IACvC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE;CAC1B,CAAC;AAEX,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAsB;IAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC;YAC1C,OAAO,CAAC,CAAC;QACV,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,OAAO,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAAA,CACzB;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAiB,EAAU;IAClE,MAAM,MAAM,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;IACtF,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO,MAAM,CAAC,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,SAAS,GAAG,IAAI,MAAM,CAAC;AAAA,CACjC;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAS,GAAW,4BAA4B,EAAQ;IAC/F,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,CAAC,mBAAmB,CACzB,IAAI,MAAM,CAAC,iBAAiB,CAAC;QAC5B,OAAO,EAAE,KAAK;QACd,WAAW,EAAE,mBAAmB;QAChC,cAAc,EAAE,mBAAmB;KACnC,CAAC,CACF,CAAC;IACF,+EAA+E;IAC/E,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;AAAA,CACnB","sourcesContent":["import * as undici from \"undici\";\n\n// Must stay strictly greater than the stall watchdog's quiet bound (600s — see\n// pi-agent-core DEFAULT_STREAM_IDLE): undici's body/headers timeouts are idle timeouts\n// between chunks, so a lower value here would kill quiet-but-healthy streams before the\n// phase-aware watchdog ever sees the gap. 660s = quiet bound + 60s margin.\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 660_000;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"2 min\", timeoutMs: 120_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"11 min\", timeoutMs: 660_000 },\n\t{ label: \"disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t}),\n\t);\n\t// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's\n\t// bundled fetch can otherwise consume compressed responses through npm undici's\n\t// dispatcher without decompressing them, causing response.json() failures.\n\tundici.install?.();\n}\n"]}
@@ -70,11 +70,17 @@ export interface ProviderRetrySettings {
70
70
  maxRetries?: number;
71
71
  maxRetryDelayMs?: number;
72
72
  }
73
+ export interface StreamStallSettings {
74
+ connectMs?: number;
75
+ activeIdleMs?: number;
76
+ quietIdleMs?: number;
77
+ }
73
78
  export interface RetrySettings {
74
79
  enabled?: boolean;
75
80
  maxRetries?: number;
76
81
  baseDelayMs?: number;
77
82
  provider?: ProviderRetrySettings;
83
+ stall?: StreamStallSettings;
78
84
  }
79
85
  export interface TerminalSettings {
80
86
  showImages?: boolean;
@@ -611,6 +617,16 @@ export declare class SettingsManager {
611
617
  maxRetries: number;
612
618
  baseDelayMs: number;
613
619
  };
620
+ /**
621
+ * Stream-stall watchdog bounds (pi-agent-core reliability/watchdogs.ts). Returns only the
622
+ * fields the user set, validated; unset fields fall back to DEFAULT_STREAM_IDLE at the
623
+ * wiring site (agent-session constructor). Resolved per request, so edits apply live.
624
+ */
625
+ getStreamStallSettings(): {
626
+ connectMs?: number;
627
+ activeIdleMs?: number;
628
+ quietIdleMs?: number;
629
+ };
614
630
  getHttpIdleTimeoutMs(): number;
615
631
  setHttpIdleTimeoutMs(timeoutMs: number): void;
616
632
  getProviderRetrySettings(): {