pi-smart-router 0.19.4 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.pi/extensions/smart-router/command-formatters.ts +19 -3
  2. package/.pi/extensions/smart-router/delegate-stream.ts +78 -1
  3. package/.pi/extensions/smart-router/delegation-runtime.ts +20 -4
  4. package/.pi/extensions/smart-router/extension-setup.ts +29 -0
  5. package/.pi/extensions/smart-router/planning-delegate.ts +23 -0
  6. package/.pi/extensions/smart-router/route-and-delegate.ts +25 -0
  7. package/.pi/extensions/smart-router/session-lifecycle.ts +59 -0
  8. package/.pi/extensions/smart-router/stream-delegation.ts +15 -0
  9. package/.pi/extensions/smart-router/types.ts +17 -0
  10. package/README.md +100 -1
  11. package/config/benchmark-profiles.json +2 -3
  12. package/config/operator-config.json.example +10 -0
  13. package/dist/api/middleware/pi-router-middleware.d.ts +8 -0
  14. package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -1
  15. package/dist/api/middleware/pi-router-middleware.js +12 -0
  16. package/dist/api/middleware/pi-router-middleware.js.map +1 -1
  17. package/dist/api/session-eviction.d.ts +27 -0
  18. package/dist/api/session-eviction.d.ts.map +1 -0
  19. package/dist/api/session-eviction.js +21 -0
  20. package/dist/api/session-eviction.js.map +1 -0
  21. package/dist/cli/smart-router-cli.d.ts +9 -5
  22. package/dist/cli/smart-router-cli.d.ts.map +1 -1
  23. package/dist/cli/smart-router-cli.js +23 -16
  24. package/dist/cli/smart-router-cli.js.map +1 -1
  25. package/dist/config/defaults.d.ts +2 -2
  26. package/dist/config/defaults.d.ts.map +1 -1
  27. package/dist/config/defaults.js +6 -3
  28. package/dist/config/defaults.js.map +1 -1
  29. package/dist/config/pi-model-mapper.d.ts.map +1 -1
  30. package/dist/config/pi-model-mapper.js +5 -0
  31. package/dist/config/pi-model-mapper.js.map +1 -1
  32. package/dist/domain/delegation/adaptive-reasoning.d.ts +114 -0
  33. package/dist/domain/delegation/adaptive-reasoning.d.ts.map +1 -0
  34. package/dist/domain/delegation/adaptive-reasoning.js +273 -0
  35. package/dist/domain/delegation/adaptive-reasoning.js.map +1 -0
  36. package/dist/domain/matching/hydra-matcher.d.ts +47 -0
  37. package/dist/domain/matching/hydra-matcher.d.ts.map +1 -1
  38. package/dist/domain/matching/hydra-matcher.js +53 -0
  39. package/dist/domain/matching/hydra-matcher.js.map +1 -1
  40. package/dist/domain/matching/missing-weights-reason-codes.d.ts +18 -0
  41. package/dist/domain/matching/missing-weights-reason-codes.d.ts.map +1 -0
  42. package/dist/domain/matching/missing-weights-reason-codes.js +17 -0
  43. package/dist/domain/matching/missing-weights-reason-codes.js.map +1 -0
  44. package/dist/domain/matching/modernbert-heads.d.ts +6 -0
  45. package/dist/domain/matching/modernbert-heads.d.ts.map +1 -1
  46. package/dist/domain/matching/modernbert-heads.js +4 -0
  47. package/dist/domain/matching/modernbert-heads.js.map +1 -1
  48. package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -1
  49. package/dist/domain/pipeline/router-pipeline.js +37 -2
  50. package/dist/domain/pipeline/router-pipeline.js.map +1 -1
  51. package/dist/domain/pricing/peak-pricing.d.ts +94 -0
  52. package/dist/domain/pricing/peak-pricing.d.ts.map +1 -0
  53. package/dist/domain/pricing/peak-pricing.js +144 -0
  54. package/dist/domain/pricing/peak-pricing.js.map +1 -0
  55. package/dist/domain/routing/expected-cost.d.ts +89 -0
  56. package/dist/domain/routing/expected-cost.d.ts.map +1 -1
  57. package/dist/domain/routing/expected-cost.js +138 -2
  58. package/dist/domain/routing/expected-cost.js.map +1 -1
  59. package/dist/domain/types/entities.d.ts +39 -0
  60. package/dist/domain/types/entities.d.ts.map +1 -1
  61. package/dist/domain/types/index.d.ts +2 -1
  62. package/dist/domain/types/index.d.ts.map +1 -1
  63. package/dist/domain/types/schemas.d.ts +66 -0
  64. package/dist/domain/types/schemas.d.ts.map +1 -1
  65. package/dist/domain/types/schemas.js +95 -0
  66. package/dist/domain/types/schemas.js.map +1 -1
  67. package/dist/domain/types/store-port.d.ts +16 -1
  68. package/dist/domain/types/store-port.d.ts.map +1 -1
  69. package/dist/index.d.ts +2 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +1 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/infra/telemetry.d.ts +15 -7
  74. package/dist/infra/telemetry.d.ts.map +1 -1
  75. package/dist/infra/telemetry.js +33 -13
  76. package/dist/infra/telemetry.js.map +1 -1
  77. package/dist/infrastructure/persistence/sqlite-store.d.ts +16 -1
  78. package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -1
  79. package/dist/infrastructure/persistence/sqlite-store.js +119 -4
  80. package/dist/infrastructure/persistence/sqlite-store.js.map +1 -1
  81. package/dist/infrastructure/pricing/price-broker.d.ts +7 -1
  82. package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -1
  83. package/dist/infrastructure/pricing/price-broker.js +11 -5
  84. package/dist/infrastructure/pricing/price-broker.js.map +1 -1
  85. package/dist/infrastructure/telemetry/routing-telemetry.d.ts +69 -4
  86. package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -1
  87. package/dist/infrastructure/telemetry/routing-telemetry.js +132 -3
  88. package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -1
  89. package/dist/infrastructure/telemetry/session-stats.d.ts +49 -1
  90. package/dist/infrastructure/telemetry/session-stats.d.ts.map +1 -1
  91. package/dist/infrastructure/telemetry/session-stats.js +78 -6
  92. package/dist/infrastructure/telemetry/session-stats.js.map +1 -1
  93. package/package.json +2 -1
  94. package/skills/router-release-operator/SKILL.md +67 -12
  95. package/skills/router-release-operator/references/issue-intake-checklist.md +21 -8
  96. package/skills/router-release-operator/references/release-manifest-template.md +18 -7
  97. package/skills/router-release-operator/references/release-profiles.md +13 -0
  98. package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +3 -3
  99. package/src/api/middleware/pi-router-middleware.ts +14 -0
  100. package/src/api/session-eviction.ts +36 -0
  101. package/src/cli/smart-router-cli.ts +23 -13
  102. package/src/config/defaults.ts +8 -1
  103. package/src/config/pi-model-mapper.ts +5 -0
  104. package/src/domain/delegation/adaptive-reasoning.ts +407 -0
  105. package/src/domain/matching/hydra-matcher.ts +85 -0
  106. package/src/domain/matching/missing-weights-reason-codes.ts +22 -0
  107. package/src/domain/matching/modernbert-heads.ts +12 -0
  108. package/src/domain/pipeline/router-pipeline.ts +50 -1
  109. package/src/domain/pricing/peak-pricing.ts +220 -0
  110. package/src/domain/routing/expected-cost.ts +240 -2
  111. package/src/domain/types/entities.ts +43 -0
  112. package/src/domain/types/index.ts +7 -0
  113. package/src/domain/types/schemas.ts +112 -0
  114. package/src/domain/types/store-port.ts +18 -1
  115. package/src/index.ts +2 -0
  116. package/src/infra/telemetry.ts +38 -9
  117. package/src/infrastructure/persistence/sqlite-store.ts +138 -5
  118. package/src/infrastructure/pricing/price-broker.ts +15 -4
  119. package/src/infrastructure/telemetry/routing-telemetry.ts +186 -6
  120. package/src/infrastructure/telemetry/session-stats.ts +126 -7
@@ -312,9 +312,16 @@ export function formatStatsMessage(
312
312
  return 'No routing stats yet (empty telemetry window).';
313
313
  }
314
314
 
315
+ const costBasisLabel =
316
+ snapshot.cost_basis === 'actual'
317
+ ? 'actual'
318
+ : snapshot.cost_basis === 'mixed'
319
+ ? `mixed (${snapshot.actual_usage_count}/${snapshot.entry_count} actual)`
320
+ : 'estimated';
321
+
315
322
  const lines = [
316
323
  `Entries: ${snapshot.entry_count}`,
317
- `Cost: total ${formatUsd(snapshot.total_cost_usd)} | mean ${formatMean(snapshot.mean_cost_usd, '')}`,
324
+ `Cost (${costBasisLabel}): total ${formatUsd(snapshot.total_cost_usd)} | mean ${formatMean(snapshot.mean_cost_usd, '')}`,
318
325
  `Latency: total ${snapshot.total_latency_ms.toFixed(0)}ms | mean ${formatMean(snapshot.mean_latency_ms, 'ms')}`,
319
326
  `Planning delegate share: ${formatShare(snapshot.planning_delegate_share)} (direct ${formatShare(snapshot.direct_share)})`,
320
327
  `Local vs cloud (when known): local ${formatShare(snapshot.local_share)} | cloud ${formatShare(snapshot.cloud_share)}`,
@@ -324,10 +331,19 @@ export function formatStatsMessage(
324
331
  ` other: ${snapshot.role_cost.other.count} | ${formatUsd(snapshot.role_cost.other.total_cost_usd)}`,
325
332
  ];
326
333
 
334
+ if (snapshot.actual_usage_count > 0) {
335
+ lines.push(
336
+ `Actual usage: ${snapshot.actual_usage_count}/${snapshot.entry_count} entries with host-reported tokens` +
337
+ (snapshot.actual_total_tokens !== null
338
+ ? ` | ${snapshot.actual_total_tokens} actual tokens`
339
+ : ''),
340
+ );
341
+ }
342
+
327
343
  if (snapshot.frontier_savings_usd !== undefined) {
328
344
  lines.push(
329
- `Vs always-frontier savings (est.): ${formatUsd(snapshot.frontier_savings_usd)}`,
330
- ' formula: sum max(0, tokens/1e6 * frontier_cost_per_1m - estimated_cost_usd); omitted when prices missing',
345
+ `Vs always-frontier savings: ${formatUsd(snapshot.frontier_savings_usd)} (${snapshot.cost_basis === 'estimated' ? 'est.' : 'actuals preferred, est. when missing'})`,
346
+ ' formula: sum max(0, tokens/1e6 * frontier_cost_per_1m - cost); tokens/cost prefer actuals; omitted when prices missing',
331
347
  );
332
348
  } else {
333
349
  lines.push('Vs always-frontier savings: (omitted — frontier prices unavailable)');
@@ -10,6 +10,12 @@ import {
10
10
  } from '@earendil-works/pi-ai/compat';
11
11
 
12
12
  import { parseAssistantMessageError } from '../../../src/infrastructure/delegation/provider-error.js';
13
+ import { extractUsageActuals } from '../../../src/infrastructure/telemetry/routing-telemetry.js';
14
+ import {
15
+ resolveAdaptiveReasoning,
16
+ type AdaptiveReasoningResult,
17
+ type AdaptiveReasoningSignal,
18
+ } from '../../../src/domain/delegation/adaptive-reasoning.js';
13
19
  import {
14
20
  buildDelegationContext,
15
21
  forwardDelegatedEvent,
@@ -89,6 +95,7 @@ export async function collectDelegatedStream(
89
95
  deps: StreamDelegationDeps,
90
96
  options: SimpleStreamOptions | undefined,
91
97
  headroomContext?: DelegationHeadroomContext,
98
+ reasoning?: AdaptiveReasoningResult,
92
99
  ): Promise<DelegatedStreamResult> {
93
100
  throwIfAborted(options);
94
101
 
@@ -97,6 +104,7 @@ export async function collectDelegatedStream(
97
104
  targetModel,
98
105
  options,
99
106
  headroomContext,
107
+ reasoning,
100
108
  );
101
109
  const delegateStream = resolveDelegateStream(targetModel, deps);
102
110
  const inner = delegateStream(targetModel, context, delegationOptions);
@@ -156,6 +164,7 @@ export async function pipeDelegatedStream(
156
164
  options: SimpleStreamOptions | undefined,
157
165
  headroomContext: DelegationHeadroomContext | undefined,
158
166
  pipe: PipeDelegatedStreamOptions,
167
+ reasoning?: AdaptiveReasoningResult,
159
168
  ): Promise<PipedDelegatedStreamResult> {
160
169
  throwIfAborted(options);
161
170
 
@@ -164,6 +173,7 @@ export async function pipeDelegatedStream(
164
173
  targetModel,
165
174
  options,
166
175
  headroomContext,
176
+ reasoning,
167
177
  );
168
178
  const delegateStream = resolveDelegateStream(targetModel, deps);
169
179
  const inner = delegateStream(targetModel, context, delegationOptions);
@@ -257,11 +267,29 @@ function recordDelegateOutcome(
257
267
  deps: StreamDelegationDeps,
258
268
  sessionId: string | undefined,
259
269
  result: DelegatedStreamResult,
270
+ requestId?: string,
260
271
  ): void {
261
272
  if (!result.finalMessage) {
262
273
  return;
263
274
  }
264
275
 
276
+ // SP-241 / #164: capture post-turn usage actuals (success and
277
+ // failed-with-usage terminals). Fail open — extraction/hook errors and
278
+ // missing usage must never fail the route.
279
+ if (requestId !== undefined) {
280
+ try {
281
+ const actuals = extractUsageActuals(result.finalMessage.usage);
282
+ if (actuals) {
283
+ deps.onDelegationUsage?.(requestId, actuals);
284
+ }
285
+ } catch (error) {
286
+ console.warn(
287
+ '[smart-router] usage actuals capture failed (fail open)',
288
+ error instanceof Error ? error.message : String(error),
289
+ );
290
+ }
291
+ }
292
+
265
293
  if (result.failed) {
266
294
  const parsed = parseAssistantMessageError(result.finalMessage);
267
295
  deps.router.dispatch.recordOutcome(targetModel.id, parsed);
@@ -282,6 +310,11 @@ function recordDelegateOutcome(
282
310
  /**
283
311
  * Delegate with outcome recording. When `pipe` is provided, live-forwards to outer
284
312
  * (holding the terminal event). Otherwise collects into a buffer (planning / probes).
313
+ *
314
+ * SP-245 (#166): when `reasoningSignal` is provided, the adaptive reasoning
315
+ * policy resolves the effective thinking level from turn signals and merges it
316
+ * into the delegated stream options (never lowering an explicit operator
317
+ * /thinking; fail open on non-reasoning models).
285
318
  */
286
319
  export async function delegateWithOutcome(
287
320
  targetModel: Model<Api>,
@@ -291,12 +324,54 @@ export async function delegateWithOutcome(
291
324
  sessionId: string | undefined,
292
325
  headroomContext?: DelegationHeadroomContext,
293
326
  pipe?: PipeDelegatedStreamOptions,
327
+ /** Routing request id for post-turn usage actuals capture (SP-241, #164). */
328
+ requestId?: string,
329
+ /** Turn/routing signals for adaptive reasoning (SP-245, #166). */
330
+ reasoningSignal?: AdaptiveReasoningSignal,
294
331
  ): Promise<PipedDelegatedStreamResult | DelegatedStreamResult> {
332
+ const reasoning = reasoningSignal
333
+ ? resolveAdaptiveReasoning(
334
+ targetModel,
335
+ reasoningSignal,
336
+ options?.reasoning,
337
+ // SP-246 (#166): operator enable/disable + floor/ceiling knobs.
338
+ deps.adaptiveReasoningConfig
339
+ ? {
340
+ enabled: deps.adaptiveReasoningConfig.enabled,
341
+ ...(deps.adaptiveReasoningConfig.min_level !== undefined
342
+ ? { floor: deps.adaptiveReasoningConfig.min_level }
343
+ : {}),
344
+ ...(deps.adaptiveReasoningConfig.max_level !== undefined
345
+ ? { ceiling: deps.adaptiveReasoningConfig.max_level }
346
+ : {}),
347
+ }
348
+ : undefined,
349
+ )
350
+ : undefined;
351
+
352
+ // SP-246 (#166): reasoning telemetry (requested / applied / reason code).
353
+ // Fail open — telemetry must never fail the route.
354
+ if (reasoningSignal && reasoning && requestId !== undefined) {
355
+ try {
356
+ deps.onDelegationReasoning?.(requestId, {
357
+ reasoning_level_requested: options?.reasoning ?? null,
358
+ reasoning_level_applied: reasoning.reasoning ?? null,
359
+ reasoning_reason_code: reasoning.reasonCode,
360
+ });
361
+ } catch (error) {
362
+ console.warn(
363
+ '[smart-router] reasoning telemetry callback failed (fail open)',
364
+ error instanceof Error ? error.message : String(error),
365
+ );
366
+ }
367
+ }
368
+
295
369
  const delegationContext = buildDelegationContext(
296
370
  context,
297
371
  targetModel,
298
372
  deps,
299
373
  sessionId,
374
+ reasoning,
300
375
  );
301
376
 
302
377
  const result = pipe
@@ -307,6 +382,7 @@ export async function delegateWithOutcome(
307
382
  options,
308
383
  headroomContext,
309
384
  pipe,
385
+ reasoning,
310
386
  )
311
387
  : await collectDelegatedStream(
312
388
  targetModel,
@@ -314,9 +390,10 @@ export async function delegateWithOutcome(
314
390
  deps,
315
391
  options,
316
392
  headroomContext,
393
+ reasoning,
317
394
  );
318
395
 
319
- recordDelegateOutcome(targetModel, deps, sessionId, result);
396
+ recordDelegateOutcome(targetModel, deps, sessionId, result, requestId);
320
397
 
321
398
  return result;
322
399
  }
@@ -13,6 +13,10 @@ import {
13
13
  } from '@earendil-works/pi-ai/compat';
14
14
  import type { ModelRegistry } from '@earendil-works/pi-coding-agent';
15
15
 
16
+ import {
17
+ applyConcisenessHint,
18
+ type AdaptiveReasoningResult,
19
+ } from '../../../src/domain/delegation/adaptive-reasoning.js';
16
20
  import {
17
21
  isGoogleDelegationTarget,
18
22
  normalizeDelegationContext,
@@ -87,6 +91,7 @@ export async function resolveDelegationOptions(
87
91
  targetModel: Model<Api>,
88
92
  callerOptions?: SimpleStreamOptions,
89
93
  headroomContext?: DelegationHeadroomContext,
94
+ reasoning?: AdaptiveReasoningResult,
90
95
  ): Promise<SimpleStreamOptions> {
91
96
  const auth = await modelRegistry.getApiKeyAndHeaders(targetModel);
92
97
  if (!auth.ok) {
@@ -124,6 +129,14 @@ export async function resolveDelegationOptions(
124
129
  ...(auth.headers !== undefined ? { headers: auth.headers } : {}),
125
130
  ...(mergedEnv !== undefined ? { env: mergedEnv } : {}),
126
131
  ...(maxTokens !== undefined ? { maxTokens } : {}),
132
+ // SP-245 (#166): adaptive reasoning policy merge — applied after the
133
+ // caller pick so the policy-effective level wins, while every other
134
+ // DELEGATION_CALLER_OPTION_KEYS entry (incl. thinkingBudgets) passes
135
+ // through untouched. Explicit operator /thinking floors are already
136
+ // resolved inside the policy result.
137
+ ...(reasoning?.reasoning !== undefined
138
+ ? { reasoning: reasoning.reasoning }
139
+ : {}),
127
140
  };
128
141
  }
129
142
 
@@ -146,6 +159,7 @@ export function buildDelegationContext(
146
159
  targetModel: Model<Api>,
147
160
  deps: StreamDelegationDeps,
148
161
  sessionId: string | undefined,
162
+ reasoning?: AdaptiveReasoningResult,
149
163
  ): Context {
150
164
  const sessionExecution = sessionId
151
165
  ? deps.executionLedger.getLastExecution(sessionId)
@@ -155,11 +169,13 @@ export function buildDelegationContext(
155
169
  sessionExecution,
156
170
  });
157
171
 
158
- if (isGoogleDelegationTarget(targetModel)) {
159
- return repairGeminiReplayContext(normalized, targetModel, sessionExecution);
160
- }
172
+ const repaired = isGoogleDelegationTarget(targetModel)
173
+ ? repairGeminiReplayContext(normalized, targetModel, sessionExecution)
174
+ : normalized;
161
175
 
162
- return normalized;
176
+ // SP-245 (#166): light conciseness nudge only when the policy resolved a
177
+ // low/minimal effective level on a high-verbosity (GLM-class) profile.
178
+ return applyConcisenessHint(repaired, reasoning);
163
179
  }
164
180
 
165
181
  export function createErrorMessage(
@@ -120,6 +120,9 @@ export async function createSmartRouterRuntime(cwd: string): Promise<{
120
120
  executionLedger,
121
121
  lifecycleHookState,
122
122
  planningDelegateConfig: operatorConfig.planning_delegate,
123
+ ...(operatorConfig.adaptive_reasoning !== undefined
124
+ ? { adaptiveReasoningConfig: operatorConfig.adaptive_reasoning }
125
+ : {}),
123
126
  datasetRecorder,
124
127
  outcomeRecorder,
125
128
  sessionPinner,
@@ -138,6 +141,32 @@ export async function createSmartRouterRuntime(cwd: string): Promise<{
138
141
  }
139
142
  runtime.syncRegisteredLimits?.(limits);
140
143
  },
144
+ onDelegationUsage(requestId, actuals) {
145
+ // SP-241 / #164: persist post-turn usage actuals onto the routing
146
+ // telemetry row. Fail open — a telemetry write must never fail the
147
+ // route, and stores without update support simply skip actuals.
148
+ try {
149
+ runtime.store.updateTelemetryUsageActuals?.(requestId, actuals);
150
+ } catch (error) {
151
+ console.warn(
152
+ '[smart-router] failed to persist usage actuals (fail open)',
153
+ error instanceof Error ? error.message : String(error),
154
+ );
155
+ }
156
+ },
157
+ onDelegationReasoning(requestId, fields) {
158
+ // SP-246 / #166: persist the adaptive reasoning decision (requested /
159
+ // applied level + reason code) onto the routing telemetry row. Fail
160
+ // open — telemetry must never fail the route.
161
+ try {
162
+ runtime.store.updateTelemetryReasoning?.(requestId, fields);
163
+ } catch (error) {
164
+ console.warn(
165
+ '[smart-router] failed to persist reasoning telemetry (fail open)',
166
+ error instanceof Error ? error.message : String(error),
167
+ );
168
+ }
169
+ },
141
170
  },
142
171
  };
143
172
 
@@ -27,6 +27,7 @@ import type {
27
27
  PlanningDelegateObservability,
28
28
  RoutingDecision,
29
29
  } from '../../../src/domain/types/index.js';
30
+ import { resolveAdaptiveReasoning } from '../../../src/domain/delegation/adaptive-reasoning.js';
30
31
  import { DEFAULT_PLANNING_DELEGATE_CONFIG } from '../../../src/domain/types/schemas.js';
31
32
  import {
32
33
  createPlanningDelegateObservability,
@@ -190,11 +191,33 @@ export async function defaultSpawnPlanningDelegate(
190
191
  deps: StreamDelegationDeps,
191
192
  ): Promise<PlanningDelegateSpawnResult> {
192
193
  try {
194
+ // SP-245 (#166): planning sub-calls keep higher reasoning — the adaptive
195
+ // policy resolves at least `medium` for planning turns (explicit operator
196
+ // /thinking floors still win inside the policy). SP-246: honor the
197
+ // operator enable/disable + floor/ceiling knobs.
198
+ const reasoning = resolveAdaptiveReasoning(
199
+ frontierModel,
200
+ { turnType: 'planning' },
201
+ options?.reasoning,
202
+ deps.adaptiveReasoningConfig
203
+ ? {
204
+ enabled: deps.adaptiveReasoningConfig.enabled,
205
+ ...(deps.adaptiveReasoningConfig.min_level !== undefined
206
+ ? { floor: deps.adaptiveReasoningConfig.min_level }
207
+ : {}),
208
+ ...(deps.adaptiveReasoningConfig.max_level !== undefined
209
+ ? { ceiling: deps.adaptiveReasoningConfig.max_level }
210
+ : {}),
211
+ }
212
+ : undefined,
213
+ );
193
214
  const result = await collectDelegatedStream(
194
215
  frontierModel,
195
216
  compressedContext,
196
217
  deps,
197
218
  options,
219
+ undefined,
220
+ reasoning,
198
221
  );
199
222
  if (result.failed || !result.finalMessage) {
200
223
  return {
@@ -9,6 +9,7 @@ import type {
9
9
 
10
10
  import { safeCloudDefault } from '../../../src/domain/pipeline/safe-default.js';
11
11
  import { computeOutputHeadroom } from '../../../src/domain/delegation/output-headroom.js';
12
+ import { resolvePeakPricingAdjustment } from '../../../src/domain/pricing/peak-pricing.js';
12
13
  import {
13
14
  CONTEXT_OVERFLOW_NO_FIT,
14
15
  resolveContextOverflowFallback,
@@ -84,6 +85,14 @@ function logRoutingDecision(
84
85
  return;
85
86
  }
86
87
 
88
+ // SP-244 / #165: surface peak vs off-peak pricing rationale for the selected
89
+ // model (Z.ai GLM Coding Plan / DeepSeek API windows; multiplier 1 + window
90
+ // 'none' for non-target providers — fail open).
91
+ const peakPricing = resolvePeakPricingAdjustment({
92
+ id: delegate?.modelId ?? decision.selected_model_id,
93
+ ...(delegate?.provider !== undefined ? { provider: delegate.provider } : {}),
94
+ });
95
+
87
96
  console.warn(
88
97
  '[smart-router] routing decision',
89
98
  JSON.stringify({
@@ -93,6 +102,12 @@ function logRoutingDecision(
93
102
  stage: decision.stage,
94
103
  reason_code: decision.reason_code,
95
104
  routing_latency_ms: decision.routing_latency_ms,
105
+ pricing_window: peakPricing.window,
106
+ peak_pricing: {
107
+ window: peakPricing.window,
108
+ cost_multiplier: peakPricing.cost_multiplier,
109
+ adapter_id: peakPricing.adapter_id,
110
+ },
96
111
  features: decision.features ?? null,
97
112
  delegate,
98
113
  }),
@@ -521,6 +536,15 @@ export async function routeAndDelegate(
521
536
  ...(failoverNotice !== undefined ? { failoverNotice } : {}),
522
537
  contextWindow: targetModel.contextWindow,
523
538
  },
539
+ decision.request_id,
540
+ // SP-245 (#166): adaptive reasoning policy — effective thinking level
541
+ // from this turn's envelope + routing decision (recomputed per
542
+ // failover iteration so escalation targets re-evaluate).
543
+ {
544
+ turnType: request.turn_type,
545
+ decision,
546
+ ...(targetProfile !== undefined ? { profile: targetProfile } : {}),
547
+ },
524
548
  );
525
549
 
526
550
  if (!isPipedResult(result)) {
@@ -761,6 +785,7 @@ export async function routeAndDelegate(
761
785
  ...(failoverNotice !== undefined ? { failoverNotice } : {}),
762
786
  contextWindow: fallbackModel.contextWindow,
763
787
  },
788
+ decision.request_id,
764
789
  );
765
790
  if (isPipedResult(fallbackResult)) {
766
791
  commitPipedTerminal(fallbackResult);
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
2
2
 
3
+ import { evictInMemorySessionState } from '../../../src/api/session-eviction.js';
3
4
  import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
4
5
  import {
5
6
  bindSharedModelRegistry,
@@ -15,6 +16,15 @@ export const FLEET_MODE_ENTRY_TYPE = 'smart-router-fleet-mode' as const;
15
16
  const SMART_ROUTER_PROVIDER = 'smart-router' as const;
16
17
  const SMART_ROUTER_AUTO_ID = 'auto' as const;
17
18
 
19
+ /**
20
+ * Orphan-session TTL fallback (SP-248, #145). `session_shutdown` is the primary
21
+ * teardown signal; if pi ever shuts a session down without delivering it
22
+ * (extension reload races, missed events), the session's in-memory routing
23
+ * state is evicted once it has been idle for ORPHAN_SESSION_TTL_MS. The sweep
24
+ * runs on `session_start` and fails open — eviction must never break startup.
25
+ */
26
+ export const ORPHAN_SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
27
+
18
28
  export function isSmartRouterActive(model: { provider: string; id: string }): boolean {
19
29
  return model.provider === SMART_ROUTER_PROVIDER && model.id === SMART_ROUTER_AUTO_ID;
20
30
  }
@@ -97,6 +107,39 @@ export function setupSessionHooks(
97
107
  ): void {
98
108
  let activeModel: { provider: string; id: string } | undefined;
99
109
 
110
+ // Last-seen stamp per session for the orphan-TTL fallback (SP-248).
111
+ const lastSeenBySession = new Map<string, number>();
112
+
113
+ /** Evict all in-memory routing state for one session via the SP-247 helper. */
114
+ function evictSessionState(sessionId: string | undefined): void {
115
+ // Fail open: a missing/empty session id must never break teardown.
116
+ if (sessionId === undefined || sessionId === '') {
117
+ return;
118
+ }
119
+ evictInMemorySessionState(sessionId, {
120
+ executionLedger: runtime.executionLedger,
121
+ lifecycleHookState: runtime.lifecycleHookState,
122
+ sessionRouting: runtime.sessionRouting,
123
+ });
124
+ lastSeenBySession.delete(sessionId);
125
+ }
126
+
127
+ /** Drop sessions idle longer than ORPHAN_SESSION_TTL_MS (never throws). */
128
+ function sweepOrphanedSessions(now: number): void {
129
+ try {
130
+ for (const [sessionId, lastSeen] of lastSeenBySession) {
131
+ if (now - lastSeen > ORPHAN_SESSION_TTL_MS) {
132
+ evictSessionState(sessionId);
133
+ }
134
+ }
135
+ } catch (error) {
136
+ console.warn(
137
+ '[smart-router] orphan session sweep failed (fail open)',
138
+ error instanceof Error ? error.message : String(error),
139
+ );
140
+ }
141
+ }
142
+
100
143
  pi.on('session_start', async (_event, ctx) => {
101
144
  activeModel = ctx.model;
102
145
  bindSharedModelRegistry(runtime, ctx.modelRegistry);
@@ -128,6 +171,10 @@ export function setupSessionHooks(
128
171
  datasetNotify.fn = runtime.notifyDatasetEnabled;
129
172
 
130
173
  const sessionId = ctx.sessionManager.getSessionId();
174
+ const now = Date.now();
175
+ lastSeenBySession.set(sessionId, now);
176
+ sweepOrphanedSessions(now);
177
+
131
178
  await sessionPinner.restoreSessionPin(sessionId);
132
179
 
133
180
  if (ctx.model !== undefined && isSmartRouterActive(ctx.model)) {
@@ -158,5 +205,17 @@ export function setupSessionHooks(
158
205
  delete runtime.streamDeps.ensureFleetFresh;
159
206
  datasetNotify.fn = undefined;
160
207
  ctx.ui.setStatus('smart-router-lmu', undefined);
208
+
209
+ // SP-248 (#145): drop per-session routing state so long-running pi
210
+ // processes do not retain ledger/lifecycle/routing maps forever.
211
+ // Fail open — teardown cleanup must never crash the host agent.
212
+ try {
213
+ evictSessionState(ctx.sessionManager.getSessionId());
214
+ } catch (error) {
215
+ console.warn(
216
+ '[smart-router] session teardown eviction failed (fail open)',
217
+ error instanceof Error ? error.message : String(error),
218
+ );
219
+ }
161
220
  });
162
221
  }
@@ -8,6 +8,7 @@ import {
8
8
  } from '@earendil-works/pi-ai/compat';
9
9
 
10
10
  import type { RoutingDecision, RoutingFeatureSidecar } from '../../../src/domain/types/index.js';
11
+ import { resolvePeakPricingAdjustment } from '../../../src/domain/pricing/peak-pricing.js';
11
12
  import { createErrorMessage } from './delegation-runtime.js';
12
13
  import { routeAndDelegate } from './route-and-delegate.js';
13
14
  import type { StreamDelegationDeps } from './types.js';
@@ -24,6 +25,14 @@ export function logRoutingDecision(
24
25
  return;
25
26
  }
26
27
 
28
+ // SP-244 / #165: surface peak vs off-peak pricing rationale for the selected
29
+ // model (Z.ai GLM Coding Plan / DeepSeek API windows; multiplier 1 + window
30
+ // 'none' for non-target providers — fail open).
31
+ const peakPricing = resolvePeakPricingAdjustment({
32
+ id: delegate?.modelId ?? decision.selected_model_id,
33
+ ...(delegate?.provider !== undefined ? { provider: delegate.provider } : {}),
34
+ });
35
+
27
36
  console.warn(
28
37
  '[smart-router] routing decision',
29
38
  JSON.stringify({
@@ -33,6 +42,12 @@ export function logRoutingDecision(
33
42
  stage: decision.stage,
34
43
  reason_code: decision.reason_code,
35
44
  routing_latency_ms: decision.routing_latency_ms,
45
+ pricing_window: peakPricing.window,
46
+ peak_pricing: {
47
+ window: peakPricing.window,
48
+ cost_multiplier: peakPricing.cost_multiplier,
49
+ adapter_id: peakPricing.adapter_id,
50
+ },
36
51
  features: decision.features ?? null,
37
52
  delegate,
38
53
  }),
@@ -11,10 +11,13 @@ import type { HydraMatcher } from '../../../src/domain/matching/hydra-matcher.js
11
11
  import { ExecutionLedger } from '../../../src/domain/delegation/execution-ledger.js';
12
12
  import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
13
13
  import type {
14
+ AdaptiveReasoningConfig,
14
15
  ModelProfile,
15
16
  PlanningDelegateConfig,
16
17
  PriceCatalog,
17
18
  RoutingDecision,
19
+ RoutingReasoningTelemetry,
20
+ RoutingUsageActuals,
18
21
  } from '../../../src/domain/types/index.js';
19
22
  import type { PlanningDelegateSpawnFn } from './planning-delegate.js';
20
23
  import type { StorePort } from '../../../src/domain/types/store-port.js';
@@ -68,6 +71,8 @@ export interface StreamDelegationDeps {
68
71
  spawnPlanningDelegate?: PlanningDelegateSpawnFn;
69
72
  /** Planning delegate knobs incl. global + per-call timeout bounds (SP-213, #120). */
70
73
  readonly planningDelegateConfig?: PlanningDelegateConfig;
74
+ /** Adaptive reasoning knobs: enable/disable + floor/ceiling (SP-246, #166). */
75
+ readonly adaptiveReasoningConfig?: AdaptiveReasoningConfig;
71
76
  readonly lifecycleHookState?: LifecycleHookState;
72
77
  readonly datasetRecorder?: DatasetRecorder;
73
78
  readonly outcomeRecorder?: OutcomeRecorder;
@@ -82,6 +87,18 @@ export interface StreamDelegationDeps {
82
87
  readonly contextWindow?: number;
83
88
  readonly maxTokens?: number;
84
89
  }) => void;
90
+ /**
91
+ * Fired after a delegated stream ends with host-reported usage actuals
92
+ * (SP-241, #164). Also fires on failed-with-usage terminals. Implementations
93
+ * must not throw — actuals capture never fails the route.
94
+ */
95
+ onDelegationUsage?: (requestId: string, actuals: RoutingUsageActuals) => void;
96
+ /**
97
+ * Fired after the adaptive reasoning policy resolves the effective thinking
98
+ * level for a delegated stream (SP-246, #166). Implementations must not
99
+ * throw — reasoning telemetry must never fail the route.
100
+ */
101
+ onDelegationReasoning?: (requestId: string, fields: RoutingReasoningTelemetry) => void;
85
102
  }
86
103
 
87
104
  export interface SmartRouterRuntime {