@caupulican/pi-adaptative 0.80.80 → 0.80.81

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 (66) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/core/agent-session.d.ts +15 -14
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +202 -9
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/cost/daily-usage.d.ts +23 -0
  7. package/dist/core/cost/daily-usage.d.ts.map +1 -0
  8. package/dist/core/cost/daily-usage.js +135 -0
  9. package/dist/core/cost/daily-usage.js.map +1 -0
  10. package/dist/core/model-router/config-diagnostics.d.ts +4 -0
  11. package/dist/core/model-router/config-diagnostics.d.ts.map +1 -0
  12. package/dist/core/model-router/config-diagnostics.js +26 -0
  13. package/dist/core/model-router/config-diagnostics.js.map +1 -0
  14. package/dist/core/model-router/intent-classifier.d.ts +3 -0
  15. package/dist/core/model-router/intent-classifier.d.ts.map +1 -0
  16. package/dist/core/model-router/intent-classifier.js +12 -0
  17. package/dist/core/model-router/intent-classifier.js.map +1 -0
  18. package/dist/core/model-router/session-buffer.d.ts +21 -0
  19. package/dist/core/model-router/session-buffer.d.ts.map +1 -0
  20. package/dist/core/model-router/session-buffer.js +20 -0
  21. package/dist/core/model-router/session-buffer.js.map +1 -0
  22. package/dist/core/model-router/status.d.ts +17 -0
  23. package/dist/core/model-router/status.d.ts.map +1 -0
  24. package/dist/core/model-router/status.js +66 -0
  25. package/dist/core/model-router/status.js.map +1 -0
  26. package/dist/core/model-router/tool-escalation.d.ts +9 -0
  27. package/dist/core/model-router/tool-escalation.d.ts.map +1 -0
  28. package/dist/core/model-router/tool-escalation.js +97 -0
  29. package/dist/core/model-router/tool-escalation.js.map +1 -0
  30. package/dist/core/resource-profile-equality.d.ts +5 -0
  31. package/dist/core/resource-profile-equality.d.ts.map +1 -0
  32. package/dist/core/resource-profile-equality.js +18 -0
  33. package/dist/core/resource-profile-equality.js.map +1 -0
  34. package/dist/core/settings-manager.d.ts +11 -0
  35. package/dist/core/settings-manager.d.ts.map +1 -1
  36. package/dist/core/settings-manager.js +7 -0
  37. package/dist/core/settings-manager.js.map +1 -1
  38. package/dist/core/slash-commands.d.ts.map +1 -1
  39. package/dist/core/slash-commands.js +2 -0
  40. package/dist/core/slash-commands.js.map +1 -1
  41. package/dist/main.d.ts.map +1 -1
  42. package/dist/main.js +5 -0
  43. package/dist/main.js.map +1 -1
  44. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  45. package/dist/modes/interactive/components/config-selector.js +3 -3
  46. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  47. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/footer.js +6 -1
  49. package/dist/modes/interactive/components/footer.js.map +1 -1
  50. package/dist/modes/interactive/components/settings-selector.d.ts +2 -1
  51. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  52. package/dist/modes/interactive/components/settings-selector.js +15 -0
  53. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  54. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  55. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  56. package/dist/modes/interactive/interactive-mode.js +84 -15
  57. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  58. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  59. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  60. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  61. package/examples/extensions/sandbox/package-lock.json +2 -2
  62. package/examples/extensions/sandbox/package.json +1 -1
  63. package/examples/extensions/with-deps/package-lock.json +2 -2
  64. package/examples/extensions/with-deps/package.json +1 -1
  65. package/npm-shrinkwrap.json +12 -12
  66. package/package.json +4 -4
@@ -15,7 +15,7 @@
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname, join } from "node:path";
17
17
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "@caupulican/pi-ai";
18
- import { getAgentDir } from "../config.js";
18
+ import { getAgentDir, getSessionsDir } from "../config.js";
19
19
  import { theme } from "../modes/interactive/theme/theme.js";
20
20
  import { stripFrontmatter } from "../utils/frontmatter.js";
21
21
  import { resolvePath } from "../utils/paths.js";
@@ -24,6 +24,7 @@ import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth
24
24
  import { executeBashWithOperations } from "./bash-executor.js";
25
25
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
26
26
  import { applyContextGc } from "./context-gc.js";
27
+ import { aggregateDailyUsageFromSessionFiles, aggregateDailyUsageFromSessionRoot, formatDailyUsageBreakdown, getLocalDayWindow, } from "./cost/daily-usage.js";
27
28
  import { downgradeReasoning, estimateTurnCostUsd, evaluateCostGuard } from "./cost-guard.js";
28
29
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
29
30
  import { exportSessionToHtml } from "./export-html/index.js";
@@ -42,6 +43,11 @@ import { TranscriptRecallProvider } from "./memory/providers/transcript-recall.j
42
43
  import { compactToolResultDetailsForRetention } from "./message-retention.js";
43
44
  import { createCustomMessage } from "./messages.js";
44
45
  import { resolveCliModel, resolveProfileModelSettings } from "./model-resolver.js";
46
+ import { collectModelRouterConfigDiagnostics } from "./model-router/config-diagnostics.js";
47
+ import { classifyModelRouterIntent } from "./model-router/intent-classifier.js";
48
+ import { bufferModelRouterSessionCustomMessage, bufferModelRouterSessionMessage, createModelRouterSessionBuffer, flushModelRouterSessionBuffer, } from "./model-router/session-buffer.js";
49
+ import { formatModelRouterStatus, getRecentModelRouterDecisions, MODEL_ROUTER_DECISION_CUSTOM_TYPE, } from "./model-router/status.js";
50
+ import { shouldEscalateModelRouterTool } from "./model-router/tool-escalation.js";
45
51
  import { expandPromptTemplate } from "./prompt-templates.js";
46
52
  import { stripResourceProfileBlocks } from "./resource-profile-blocks.js";
47
53
  import { classifyToolTrust, UNTRUSTED_BOUNDARY_SYSTEM_RULE, wrapUntrustedText } from "./security/untrusted-boundary.js";
@@ -77,6 +83,12 @@ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
77
83
  // ============================================================================
78
84
  // AgentSession Class
79
85
  // ============================================================================
86
+ function formatModelRouterModel(model) {
87
+ return `${model.provider}/${model.id}`;
88
+ }
89
+ function persistModelRouterDecision(sessionManager, decision) {
90
+ sessionManager.appendCustomEntry(MODEL_ROUTER_DECISION_CUSTOM_TYPE, decision);
91
+ }
80
92
  export class AgentSession {
81
93
  agent;
82
94
  sessionManager;
@@ -132,10 +144,18 @@ export class AgentSession {
132
144
  _gatewayRegistry = new GatewayRegistry();
133
145
  /** Cache for getSpawnedUsage(), keyed by session entry count (Bug #22 — avoid O(N) per render frame). */
134
146
  _spawnedUsageCache;
147
+ _dailyUsageCache;
135
148
  /** Latest proactive cost-guard decision (#34), for the host UI to surface. Undefined when disabled. */
136
149
  _lastCostGuardDecision;
137
150
  /** One-shot latch so the cost guard downgrades reasoning once per over-threshold episode, not every call. */
138
151
  _costGuardDowngraded = false;
152
+ /** Active model-router intent for the current transient routed turn, if any. */
153
+ _activeModelRouterIntent;
154
+ _modelRouterSessionBuffer;
155
+ _modelRouterEscalationRequested = false;
156
+ _lastModelRouterDecision;
157
+ _lastModelRouterSkipReason;
158
+ _lastModelRouterIntent;
139
159
  /** Lazily-built skill curator (#32) over `<agentDir>/skills`. */
140
160
  _skillCuratorInstance;
141
161
  /** Set on dispose so in-flight background reflection bails instead of writing to a dead session (Bug #21). */
@@ -456,6 +476,15 @@ export class AgentSession {
456
476
  }
457
477
  _installAgentToolHooks() {
458
478
  this.agent.beforeToolCall = async ({ toolCall, args }) => {
479
+ if (this._activeModelRouterIntent &&
480
+ shouldEscalateModelRouterTool({ intent: this._activeModelRouterIntent, toolName: toolCall.name, args })) {
481
+ this._modelRouterEscalationRequested = true;
482
+ this.agent.abort();
483
+ return {
484
+ block: true,
485
+ reason: "Model router escalation required: a cheap research turn attempted a mutating tool. Retry the turn on the configured expensive model.",
486
+ };
487
+ }
459
488
  const runner = this._extensionRunner;
460
489
  if (!runner.hasHandlers("tool_call")) {
461
490
  return undefined;
@@ -562,8 +591,16 @@ export class AgentSession {
562
591
  // accumulate until the interactive Node process hits the V8 heap limit.
563
592
  if (event.type === "message_end") {
564
593
  compactToolResultDetailsForRetention(event.message);
594
+ const modelRouterBuffer = this._modelRouterSessionBuffer;
595
+ if (modelRouterBuffer && event.message.role === "custom") {
596
+ bufferModelRouterSessionCustomMessage(modelRouterBuffer, event.message);
597
+ }
598
+ else if (modelRouterBuffer &&
599
+ (event.message.role === "user" || event.message.role === "assistant" || event.message.role === "toolResult")) {
600
+ bufferModelRouterSessionMessage(modelRouterBuffer, event.message);
601
+ }
565
602
  // Check if this is a custom message from extensions
566
- if (event.message.role === "custom") {
603
+ else if (event.message.role === "custom") {
567
604
  // Persist as CustomMessageEntry
568
605
  this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
569
606
  }
@@ -1086,6 +1123,140 @@ export class AgentSession {
1086
1123
  await this._drainQueuedExtensionCommands();
1087
1124
  }
1088
1125
  }
1126
+ _resolveModelRouterModelForIntent(intent) {
1127
+ const settings = this.settingsManager.getModelRouterSettings();
1128
+ const modelLabel = intent === "research" ? "cheap model" : "expensive model";
1129
+ if (!settings.enabled) {
1130
+ this._lastModelRouterSkipReason = "disabled";
1131
+ return undefined;
1132
+ }
1133
+ const modelPattern = intent === "research" ? settings.cheapModel : settings.expensiveModel;
1134
+ if (!modelPattern) {
1135
+ this._lastModelRouterSkipReason = `${modelLabel} unset`;
1136
+ return undefined;
1137
+ }
1138
+ const resolved = resolveCliModel({ cliModel: modelPattern, modelRegistry: this._modelRegistry });
1139
+ if (!resolved.model) {
1140
+ this._lastModelRouterSkipReason = `${modelLabel} unresolved: ${modelPattern}`;
1141
+ return undefined;
1142
+ }
1143
+ const resolvedName = formatModelRouterModel(resolved.model);
1144
+ if (!this._modelRegistry.hasConfiguredAuth(resolved.model)) {
1145
+ this._lastModelRouterSkipReason = `${modelLabel} missing auth: ${resolvedName}`;
1146
+ return undefined;
1147
+ }
1148
+ this._lastModelRouterSkipReason = undefined;
1149
+ return resolved.model;
1150
+ }
1151
+ _resolveModelRouterTurnModel(prompt) {
1152
+ const intent = classifyModelRouterIntent(prompt);
1153
+ this._lastModelRouterIntent = intent;
1154
+ return this._resolveModelRouterModelForIntent(intent);
1155
+ }
1156
+ getModelRouterStatus(formatLabel) {
1157
+ const recentDecisions = getRecentModelRouterDecisions(this.sessionManager.getEntries());
1158
+ const lastDecision = this._lastModelRouterDecision ?? recentDecisions.at(-1);
1159
+ const historicalDecisions = this._lastModelRouterDecision ? recentDecisions : recentDecisions.slice(0, -1);
1160
+ const settings = this.settingsManager.getModelRouterSettings();
1161
+ const lines = [
1162
+ formatModelRouterStatus(settings, lastDecision, formatLabel, historicalDecisions, this._lastModelRouterSkipReason, this._lastModelRouterIntent ?? lastDecision?.intent),
1163
+ ];
1164
+ const diagnostics = collectModelRouterConfigDiagnostics(settings, this._modelRegistry);
1165
+ if (diagnostics.length > 0) {
1166
+ lines.push(formatLabel ? formatLabel("Config diagnostics:") : "Config diagnostics:");
1167
+ for (const diagnostic of diagnostics) {
1168
+ lines.push(`- ${diagnostic}`);
1169
+ }
1170
+ }
1171
+ return lines.join("\n");
1172
+ }
1173
+ async _runAgentPromptWithModelRouter(messages, routedModel, routedIntent, persistDecision = true) {
1174
+ if (!routedModel) {
1175
+ await this._runAgentPrompt(messages);
1176
+ return;
1177
+ }
1178
+ const previousModel = this.agent.state.model;
1179
+ const previousThinkingLevel = this.agent.state.thinkingLevel;
1180
+ const previousActiveModelRouterIntent = this._activeModelRouterIntent;
1181
+ const previousModelRouterSessionBuffer = this._modelRouterSessionBuffer;
1182
+ const previousModelRouterEscalationRequested = this._modelRouterEscalationRequested;
1183
+ const bufferRoutedTurn = routedIntent === "research";
1184
+ const originalHistoryLength = this.agent.state.messages.length;
1185
+ let retryModel;
1186
+ let completedDecision = routedIntent
1187
+ ? {
1188
+ intent: routedIntent,
1189
+ routedModel: formatModelRouterModel(routedModel),
1190
+ outcome: "routed",
1191
+ }
1192
+ : undefined;
1193
+ let thrownError;
1194
+ if (routedIntent) {
1195
+ this._lastModelRouterDecision = completedDecision;
1196
+ }
1197
+ this._activeModelRouterIntent = routedIntent;
1198
+ if (bufferRoutedTurn) {
1199
+ this._modelRouterSessionBuffer = createModelRouterSessionBuffer();
1200
+ this._modelRouterEscalationRequested = false;
1201
+ }
1202
+ if (!modelsAreEqual(this.model, routedModel)) {
1203
+ this.agent.state.model = routedModel;
1204
+ this.agent.state.thinkingLevel = clampThinkingLevel(routedModel, previousThinkingLevel);
1205
+ }
1206
+ try {
1207
+ await this._runAgentPrompt(messages);
1208
+ if (bufferRoutedTurn && this._modelRouterEscalationRequested) {
1209
+ this.agent.state.messages.splice(originalHistoryLength);
1210
+ retryModel = this._resolveModelRouterModelForIntent("modify") ?? previousModel;
1211
+ completedDecision = {
1212
+ intent: routedIntent,
1213
+ routedModel: formatModelRouterModel(routedModel),
1214
+ outcome: "escalated",
1215
+ retryModel: formatModelRouterModel(retryModel),
1216
+ };
1217
+ this._lastModelRouterDecision = completedDecision;
1218
+ }
1219
+ else if (bufferRoutedTurn && this._modelRouterSessionBuffer) {
1220
+ flushModelRouterSessionBuffer(this._modelRouterSessionBuffer, (message) => {
1221
+ this.sessionManager.appendMessage(message);
1222
+ }, (customType, content, display, details) => {
1223
+ this.sessionManager.appendCustomMessageEntry(customType, content, display, details);
1224
+ });
1225
+ }
1226
+ }
1227
+ catch (error) {
1228
+ thrownError = error;
1229
+ if (completedDecision) {
1230
+ completedDecision = { ...completedDecision, outcome: "failed" };
1231
+ this._lastModelRouterDecision = completedDecision;
1232
+ }
1233
+ }
1234
+ finally {
1235
+ this.agent.state.model = previousModel;
1236
+ this.agent.state.thinkingLevel = previousThinkingLevel;
1237
+ this._activeModelRouterIntent = previousActiveModelRouterIntent;
1238
+ this._modelRouterSessionBuffer = previousModelRouterSessionBuffer;
1239
+ this._modelRouterEscalationRequested = previousModelRouterEscalationRequested;
1240
+ }
1241
+ if (retryModel && !thrownError) {
1242
+ try {
1243
+ await this._runAgentPromptWithModelRouter(messages, retryModel, "modify", false);
1244
+ }
1245
+ catch (error) {
1246
+ thrownError = error;
1247
+ if (completedDecision) {
1248
+ completedDecision = { ...completedDecision, outcome: "failed" };
1249
+ this._lastModelRouterDecision = completedDecision;
1250
+ }
1251
+ }
1252
+ }
1253
+ if (persistDecision && completedDecision) {
1254
+ persistModelRouterDecision(this.sessionManager, completedDecision);
1255
+ }
1256
+ if (thrownError) {
1257
+ throw thrownError;
1258
+ }
1259
+ }
1089
1260
  async _handlePostAgentRun() {
1090
1261
  const msg = this._lastAssistantMessage;
1091
1262
  this._lastAssistantMessage = undefined;
@@ -1149,6 +1320,8 @@ export class AgentSession {
1149
1320
  const processSlashCommands = options?.processSlashCommands ?? expandPromptTemplates;
1150
1321
  const preflightResult = options?.preflightResult;
1151
1322
  let messages;
1323
+ let routedTurnModel;
1324
+ let routedTurnIntent;
1152
1325
  // R4 effectiveness feedback: remember the recall page + the query so we can score, after the
1153
1326
  // response, whether the agent actually used the recalled context.
1154
1327
  let injectedRecall = "";
@@ -1211,18 +1384,21 @@ export class AgentSession {
1211
1384
  }
1212
1385
  // Flush any pending bash messages before the new prompt
1213
1386
  this._flushPendingBashMessages();
1387
+ routedTurnModel = this._resolveModelRouterTurnModel(expandedText);
1388
+ routedTurnIntent = routedTurnModel ? classifyModelRouterIntent(expandedText) : undefined;
1389
+ const requestModel = routedTurnModel ?? this.model;
1214
1390
  // Validate model
1215
- if (!this.model) {
1391
+ if (!requestModel) {
1216
1392
  throw new Error(formatNoModelSelectedMessage());
1217
1393
  }
1218
- if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
1219
- const isOAuth = this._modelRegistry.isUsingOAuth(this.model);
1394
+ if (!this._modelRegistry.hasConfiguredAuth(requestModel)) {
1395
+ const isOAuth = this._modelRegistry.isUsingOAuth(requestModel);
1220
1396
  if (isOAuth) {
1221
- throw new Error(`Authentication failed for "${this.model.provider}". ` +
1397
+ throw new Error(`Authentication failed for "${requestModel.provider}". ` +
1222
1398
  `Credentials may have expired or network is unavailable. ` +
1223
- `Run '/login ${this.model.provider}' to re-authenticate.`);
1399
+ `Run '/login ${requestModel.provider}' to re-authenticate.`);
1224
1400
  }
1225
- throw new Error(formatNoApiKeyFoundMessage(this.model.provider));
1401
+ throw new Error(formatNoApiKeyFoundMessage(requestModel.provider));
1226
1402
  }
1227
1403
  // Check if we need to compact before sending (catches aborted responses).
1228
1404
  // Do not call agent.continue() here: the next model turn must include the
@@ -1301,7 +1477,7 @@ export class AgentSession {
1301
1477
  return;
1302
1478
  }
1303
1479
  preflightResult?.(true);
1304
- await this._runAgentPrompt(messages);
1480
+ await this._runAgentPromptWithModelRouter(messages, routedTurnModel, routedTurnIntent);
1305
1481
  // R4: score whether the agent actually used the recalled context, so the recall gate can adapt.
1306
1482
  if (injectedRecall) {
1307
1483
  const response = this._findLastAssistantMessage();
@@ -3464,6 +3640,23 @@ export class AgentSession {
3464
3640
  this._spawnedUsageCache = { entryCount, totals };
3465
3641
  return totals;
3466
3642
  }
3643
+ getDailyUsageTotals(now = new Date()) {
3644
+ const sessionDir = this.sessionManager.getSessionDir();
3645
+ const scope = this.sessionManager.usesDefaultSessionDir() ? getSessionsDir() : sessionDir;
3646
+ const nowMs = now.getTime();
3647
+ if (this._dailyUsageCache?.sessionDir === scope && this._dailyUsageCache.expiresAt > nowMs) {
3648
+ return this._dailyUsageCache.totals;
3649
+ }
3650
+ const window = getLocalDayWindow(now);
3651
+ const totals = this.sessionManager.usesDefaultSessionDir()
3652
+ ? aggregateDailyUsageFromSessionRoot(scope, window)
3653
+ : aggregateDailyUsageFromSessionFiles(sessionDir, window);
3654
+ this._dailyUsageCache = { sessionDir: scope, expiresAt: nowMs + 10_000, totals };
3655
+ return totals;
3656
+ }
3657
+ getDailyUsageBreakdown(formatLabel, now = new Date()) {
3658
+ return formatDailyUsageBreakdown(this.getDailyUsageTotals(now), formatLabel);
3659
+ }
3467
3660
  /**
3468
3661
  * Run a one-shot LLM completion fully ISOLATED from the main session — the load-bearing
3469
3662
  * primitive for the native reflection engine (adaptive-agent design §6c/§7).