ultimate-pi 0.16.0 → 0.17.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.
- package/.pi/agents/harness/planning/hypothesis.md +1 -1
- package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
- package/.pi/extensions/harness-debate-tools.ts +12 -3
- package/.pi/extensions/harness-run-context.ts +12 -0
- package/.pi/extensions/harness-subagent-submit.ts +2 -25
- package/.pi/extensions/harness-telemetry.ts +29 -4
- package/.pi/extensions/lib/debate-bus-core.ts +15 -9
- package/.pi/extensions/lib/harness-subagent-auth.ts +104 -19
- package/.pi/extensions/lib/harness-subagent-policy.ts +14 -0
- package/.pi/extensions/lib/harness-subagents-bridge.ts +85 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +61 -8
- package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
- package/.pi/extensions/lib/plan-debate-gate.ts +80 -17
- package/.pi/extensions/lib/plan-debate-lanes.ts +27 -3
- package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
- package/.pi/extensions/lib/plan-messenger.ts +4 -0
- package/.pi/extensions/lib/plan-review-gate.ts +51 -0
- package/.pi/extensions/trace-recorder.ts +1 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +40 -17
- package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
- package/.pi/model-router.example.json +13 -4
- package/.pi/prompts/harness-plan.md +25 -7
- package/.pi/prompts/harness-setup.md +4 -4
- package/.pi/scripts/harness-generate-model-router.mjs +118 -36
- package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
- package/.pi/scripts/harness-sync-model-router.mjs +15 -2
- package/.pi/scripts/harness-verify.mjs +29 -0
- package/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
- package/vendor/pi-model-router/extensions/commands.ts +4 -4
- package/vendor/pi-model-router/extensions/index.ts +21 -0
- package/vendor/pi-model-router/extensions/provider.ts +130 -79
- package/vendor/pi-model-router/extensions/routing.ts +148 -0
- package/vendor/pi-model-router/extensions/state.ts +3 -0
- package/vendor/pi-model-router/extensions/types.ts +9 -0
- package/vendor/pi-model-router/extensions/ui.ts +16 -2
|
@@ -19,15 +19,21 @@ import type {
|
|
|
19
19
|
RouterTier,
|
|
20
20
|
RouterPinByProfile,
|
|
21
21
|
RouterThinkingByProfile,
|
|
22
|
+
SessionLock,
|
|
22
23
|
} from './types.js';
|
|
23
24
|
import { profileNames, parseCanonicalModelRef, ROUTER_TIERS } from './config.js';
|
|
24
25
|
import {
|
|
25
26
|
phaseForTier,
|
|
26
27
|
buildRoutingDecision,
|
|
27
28
|
decideRouting,
|
|
29
|
+
decideSessionLock,
|
|
28
30
|
runClassifier,
|
|
29
31
|
extractTextFromContent,
|
|
30
32
|
hasImageAttachment,
|
|
33
|
+
buildSessionLockContext,
|
|
34
|
+
sessionLockToRoutingDecision,
|
|
35
|
+
routingDecisionToSessionLock,
|
|
36
|
+
applyThinkingToDecision,
|
|
31
37
|
} from './routing.js';
|
|
32
38
|
|
|
33
39
|
export const createErrorMessage = (
|
|
@@ -138,6 +144,8 @@ export const registerRouterProvider = (
|
|
|
138
144
|
readonly thinkingByProfile: RouterThinkingByProfile;
|
|
139
145
|
readonly pinnedTierByProfile: RouterPinByProfile;
|
|
140
146
|
accumulatedCost: number;
|
|
147
|
+
get sessionLock(): SessionLock | undefined;
|
|
148
|
+
set sessionLock(v: SessionLock | undefined);
|
|
141
149
|
},
|
|
142
150
|
actions: {
|
|
143
151
|
persistState: () => void;
|
|
@@ -217,26 +225,111 @@ export const registerRouterProvider = (
|
|
|
217
225
|
state.routerEnabled = true;
|
|
218
226
|
|
|
219
227
|
const pinnedTier = state.pinnedTierByProfile[model.id];
|
|
228
|
+
const thinkingOverrides = state.thinkingByProfile[model.id];
|
|
220
229
|
const isBudgetExceeded =
|
|
221
230
|
state.currentConfig.maxSessionBudget !== undefined &&
|
|
222
231
|
state.accumulatedCost >= state.currentConfig.maxSessionBudget;
|
|
223
232
|
|
|
224
|
-
|
|
233
|
+
const checkModelSupportsImage = (modelRef: string) => {
|
|
234
|
+
try {
|
|
235
|
+
const { provider, modelId } = parseCanonicalModelRef(modelRef);
|
|
236
|
+
const m = state.currentModelRegistry?.find(provider, modelId);
|
|
237
|
+
return m?.input?.includes('image') ?? false;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const pickVisionCapableLock = (
|
|
244
|
+
lockDecision: RoutingDecision,
|
|
245
|
+
): RoutingDecision => {
|
|
246
|
+
if (!hasImageAttachment(context)) return lockDecision;
|
|
247
|
+
const candidates = [
|
|
248
|
+
lockDecision.targetLabel,
|
|
249
|
+
...(profile[lockDecision.tier].fallbacks ?? []),
|
|
250
|
+
];
|
|
251
|
+
if (candidates.some(checkModelSupportsImage)) return lockDecision;
|
|
252
|
+
for (const tier of ROUTER_TIERS) {
|
|
253
|
+
const refs = [profile[tier].model, ...(profile[tier].fallbacks ?? [])];
|
|
254
|
+
const visionRef = refs.find(checkModelSupportsImage);
|
|
255
|
+
if (visionRef) {
|
|
256
|
+
const { provider, modelId } = parseCanonicalModelRef(visionRef);
|
|
257
|
+
return {
|
|
258
|
+
...lockDecision,
|
|
259
|
+
tier,
|
|
260
|
+
targetLabel: visionRef,
|
|
261
|
+
targetProvider: provider,
|
|
262
|
+
targetModelId: modelId,
|
|
263
|
+
reasoning: `${lockDecision.reasoning} | Vision-capable model for image input.`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return lockDecision;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
let lock = state.sessionLock;
|
|
271
|
+
if (!lock || lock.profile !== model.id) {
|
|
272
|
+
const lockContext = buildSessionLockContext(context);
|
|
273
|
+
let lockDecision = decideSessionLock(
|
|
274
|
+
lockContext,
|
|
275
|
+
model.id,
|
|
276
|
+
profile,
|
|
277
|
+
pinnedTier,
|
|
278
|
+
thinkingOverrides,
|
|
279
|
+
state.currentConfig.phaseBias,
|
|
280
|
+
state.currentConfig.rules,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
state.currentConfig.classifierModel &&
|
|
285
|
+
!pinnedTier &&
|
|
286
|
+
!lockDecision.isRuleMatched
|
|
287
|
+
) {
|
|
288
|
+
const classifierResult = await runClassifier(
|
|
289
|
+
state.currentConfig.classifierModel,
|
|
290
|
+
state.currentModelRegistry,
|
|
291
|
+
lockContext,
|
|
292
|
+
undefined,
|
|
293
|
+
);
|
|
294
|
+
if (classifierResult) {
|
|
295
|
+
lockDecision = buildRoutingDecision(
|
|
296
|
+
model.id,
|
|
297
|
+
profile,
|
|
298
|
+
classifierResult.tier,
|
|
299
|
+
phaseForTier(classifierResult.tier),
|
|
300
|
+
`Session lock classifier: ${classifierResult.reasoning}`,
|
|
301
|
+
thinkingOverrides,
|
|
302
|
+
true,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
lockDecision = pickVisionCapableLock(lockDecision);
|
|
308
|
+
lock = routingDecisionToSessionLock(lockDecision);
|
|
309
|
+
state.sessionLock = lock;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const lockedBase = sessionLockToRoutingDecision(
|
|
313
|
+
lock,
|
|
314
|
+
profile,
|
|
315
|
+
thinkingOverrides,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
let thinkingDecision: RoutingDecision = decideRouting(
|
|
225
319
|
context,
|
|
226
320
|
model.id,
|
|
227
321
|
profile,
|
|
228
322
|
state.lastDecision,
|
|
229
323
|
pinnedTier,
|
|
230
|
-
|
|
324
|
+
thinkingOverrides,
|
|
231
325
|
state.currentConfig.phaseBias,
|
|
232
326
|
state.currentConfig.rules,
|
|
233
327
|
isBudgetExceeded,
|
|
234
328
|
);
|
|
235
329
|
|
|
236
|
-
// Optional Context Trigger Upgrade
|
|
237
330
|
if (
|
|
238
331
|
state.currentConfig.largeContextThreshold &&
|
|
239
|
-
|
|
332
|
+
thinkingDecision.tier !== 'high' &&
|
|
240
333
|
state.lastExtensionContext
|
|
241
334
|
) {
|
|
242
335
|
try {
|
|
@@ -245,28 +338,24 @@ export const registerRouterProvider = (
|
|
|
245
338
|
usage?.tokens &&
|
|
246
339
|
usage.tokens > state.currentConfig.largeContextThreshold
|
|
247
340
|
) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
'
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
false,
|
|
256
|
-
);
|
|
257
|
-
decision.isContextTriggered = true;
|
|
341
|
+
thinkingDecision = {
|
|
342
|
+
...thinkingDecision,
|
|
343
|
+
tier: 'high',
|
|
344
|
+
phase: 'planning',
|
|
345
|
+
reasoning: `Context usage (${usage.tokens}) exceeds threshold (${state.currentConfig.largeContextThreshold}). Forced high thinking.`,
|
|
346
|
+
isContextTriggered: true,
|
|
347
|
+
};
|
|
258
348
|
}
|
|
259
|
-
} catch (
|
|
349
|
+
} catch (_e) {
|
|
260
350
|
// ignore
|
|
261
351
|
}
|
|
262
352
|
}
|
|
263
353
|
|
|
264
|
-
// Classifier Override
|
|
265
354
|
if (
|
|
266
355
|
state.currentConfig.classifierModel &&
|
|
267
356
|
!pinnedTier &&
|
|
268
|
-
!
|
|
269
|
-
!
|
|
357
|
+
!thinkingDecision.isContextTriggered &&
|
|
358
|
+
!thinkingDecision.isRuleMatched
|
|
270
359
|
) {
|
|
271
360
|
const classifierResult = await runClassifier(
|
|
272
361
|
state.currentConfig.classifierModel,
|
|
@@ -275,24 +364,34 @@ export const registerRouterProvider = (
|
|
|
275
364
|
state.lastDecision?.phase,
|
|
276
365
|
);
|
|
277
366
|
if (classifierResult) {
|
|
278
|
-
|
|
367
|
+
thinkingDecision = buildRoutingDecision(
|
|
279
368
|
model.id,
|
|
280
369
|
profile,
|
|
281
370
|
classifierResult.tier,
|
|
282
371
|
phaseForTier(classifierResult.tier),
|
|
283
|
-
`
|
|
284
|
-
|
|
372
|
+
`Thinking classifier: ${classifierResult.reasoning}`,
|
|
373
|
+
thinkingOverrides,
|
|
285
374
|
true,
|
|
286
375
|
);
|
|
287
|
-
if (isBudgetExceeded &&
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
376
|
+
if (isBudgetExceeded && thinkingDecision.tier === 'high') {
|
|
377
|
+
thinkingDecision = {
|
|
378
|
+
...thinkingDecision,
|
|
379
|
+
tier: 'medium',
|
|
380
|
+
phase: 'implementation',
|
|
381
|
+
reasoning: `Budget exceeded. Downgraded thinking to medium. (Original: ${thinkingDecision.reasoning})`,
|
|
382
|
+
isBudgetForced: true,
|
|
383
|
+
};
|
|
292
384
|
}
|
|
293
385
|
}
|
|
294
386
|
}
|
|
295
387
|
|
|
388
|
+
let decision = applyThinkingToDecision(
|
|
389
|
+
lockedBase,
|
|
390
|
+
thinkingDecision,
|
|
391
|
+
profile,
|
|
392
|
+
thinkingOverrides,
|
|
393
|
+
);
|
|
394
|
+
|
|
296
395
|
const lastMessage = context.messages[context.messages.length - 1];
|
|
297
396
|
const previousDecision = state.lastDecision;
|
|
298
397
|
const isGoogleThinkingToolContinuation =
|
|
@@ -302,7 +401,7 @@ export const registerRouterProvider = (
|
|
|
302
401
|
previousDecision.thinking !== 'off' &&
|
|
303
402
|
decision.targetProvider === 'google' &&
|
|
304
403
|
decision.thinking !== 'off' &&
|
|
305
|
-
previousDecision.targetLabel
|
|
404
|
+
previousDecision.targetLabel === decision.targetLabel;
|
|
306
405
|
|
|
307
406
|
if (isGoogleThinkingToolContinuation) {
|
|
308
407
|
decision = {
|
|
@@ -319,56 +418,6 @@ export const registerRouterProvider = (
|
|
|
319
418
|
};
|
|
320
419
|
}
|
|
321
420
|
|
|
322
|
-
const imageAttached = hasImageAttachment(context);
|
|
323
|
-
if (imageAttached) {
|
|
324
|
-
const checkModelSupportsImage = (modelRef: string) => {
|
|
325
|
-
try {
|
|
326
|
-
const { provider, modelId } = parseCanonicalModelRef(modelRef);
|
|
327
|
-
const m = state.currentModelRegistry?.find(provider, modelId);
|
|
328
|
-
return m?.input?.includes('image') ?? false;
|
|
329
|
-
} catch {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
const tierModels = [
|
|
335
|
-
decision.targetLabel,
|
|
336
|
-
...(profile[decision.tier].fallbacks ?? []),
|
|
337
|
-
];
|
|
338
|
-
if (!tierModels.some(checkModelSupportsImage)) {
|
|
339
|
-
const tiersToTry: RouterTier[] =
|
|
340
|
-
decision.tier === 'low'
|
|
341
|
-
? ['medium', 'high']
|
|
342
|
-
: decision.tier === 'medium'
|
|
343
|
-
? ['high']
|
|
344
|
-
: [];
|
|
345
|
-
|
|
346
|
-
let foundTier: RouterTier | undefined;
|
|
347
|
-
for (const t of tiersToTry) {
|
|
348
|
-
const tModels = [
|
|
349
|
-
profile[t].model,
|
|
350
|
-
...(profile[t].fallbacks ?? []),
|
|
351
|
-
];
|
|
352
|
-
if (tModels.some(checkModelSupportsImage)) {
|
|
353
|
-
foundTier = t;
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (foundTier) {
|
|
359
|
-
decision = buildRoutingDecision(
|
|
360
|
-
model.id,
|
|
361
|
-
profile,
|
|
362
|
-
foundTier,
|
|
363
|
-
phaseForTier(foundTier),
|
|
364
|
-
`Forced ${foundTier} tier because the originally routed ${decision.tier} tier does not support image attachments.`,
|
|
365
|
-
state.thinkingByProfile[model.id],
|
|
366
|
-
false,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
421
|
state.lastDecision = decision;
|
|
373
422
|
actions.recordDebugDecision(decision);
|
|
374
423
|
|
|
@@ -376,10 +425,12 @@ export const registerRouterProvider = (
|
|
|
376
425
|
actions.updateStatus(state.lastExtensionContext);
|
|
377
426
|
}
|
|
378
427
|
|
|
428
|
+
const lockTier = lock.tier;
|
|
379
429
|
let modelsToTry = [
|
|
380
|
-
|
|
381
|
-
...(profile[
|
|
430
|
+
lock.modelRef,
|
|
431
|
+
...(profile[lockTier].fallbacks ?? []),
|
|
382
432
|
];
|
|
433
|
+
const imageAttached = hasImageAttachment(context);
|
|
383
434
|
if (imageAttached) {
|
|
384
435
|
modelsToTry = modelsToTry.filter((modelRef) => {
|
|
385
436
|
try {
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
RoutingDecision,
|
|
8
8
|
RoutingRule,
|
|
9
9
|
RouterThinkingByTier,
|
|
10
|
+
SessionLock,
|
|
10
11
|
} from './types.js';
|
|
11
12
|
import { parseCanonicalModelRef, isRouterTier } from './config.js';
|
|
12
13
|
|
|
@@ -38,6 +39,153 @@ export const getLastUserText = (context: Context): string => {
|
|
|
38
39
|
return '';
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
export const getFirstUserText = (context: Context): string => {
|
|
43
|
+
for (const message of context.messages) {
|
|
44
|
+
if (message.role === 'user') {
|
|
45
|
+
return extractTextFromContent(message.content).trim();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return '';
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Context for one-shot session model lock: system prompt + first user message only. */
|
|
52
|
+
export const buildSessionLockContext = (context: Context): Context => {
|
|
53
|
+
const firstUser = context.messages.find((message) => message.role === 'user');
|
|
54
|
+
const messages = firstUser ? [firstUser] : context.messages.slice(0, 1);
|
|
55
|
+
return { ...context, messages };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const sessionLockToRoutingDecision = (
|
|
59
|
+
lock: SessionLock,
|
|
60
|
+
profile: RouterProfile,
|
|
61
|
+
thinkingOverrides?: RouterThinkingByTier,
|
|
62
|
+
): RoutingDecision => {
|
|
63
|
+
const routed = profile[lock.tier];
|
|
64
|
+
const { provider, modelId } = parseCanonicalModelRef(lock.modelRef);
|
|
65
|
+
const baseThinking =
|
|
66
|
+
routed.thinking ??
|
|
67
|
+
(lock.tier === 'high' ? 'high' : lock.tier === 'low' ? 'low' : 'medium');
|
|
68
|
+
const effectiveThinking = thinkingOverrides?.[lock.tier] ?? baseThinking;
|
|
69
|
+
return {
|
|
70
|
+
profile: lock.profile,
|
|
71
|
+
tier: lock.tier,
|
|
72
|
+
phase: phaseForTier(lock.tier),
|
|
73
|
+
targetProvider: provider,
|
|
74
|
+
targetModelId: modelId,
|
|
75
|
+
targetLabel: lock.modelRef,
|
|
76
|
+
reasoning: lock.reasoning,
|
|
77
|
+
thinking: effectiveThinking,
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const routingDecisionToSessionLock = (
|
|
83
|
+
decision: RoutingDecision,
|
|
84
|
+
): SessionLock => ({
|
|
85
|
+
profile: decision.profile,
|
|
86
|
+
tier: decision.tier,
|
|
87
|
+
modelRef: decision.targetLabel,
|
|
88
|
+
reasoning: decision.reasoning,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Text used to score initial session complexity (system prompt + first user message). */
|
|
92
|
+
export const getSessionLockAnalysisText = (context: Context): string => {
|
|
93
|
+
const lockContext = buildSessionLockContext(context);
|
|
94
|
+
const system = (context.systemPrompt ?? '').trim();
|
|
95
|
+
const firstUser = getFirstUserText(lockContext);
|
|
96
|
+
if (system && firstUser) return `${system}\n\n${firstUser}`;
|
|
97
|
+
return system || firstUser || '';
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One-shot tier pick for session model lock (no prior-turn stickiness).
|
|
102
|
+
*/
|
|
103
|
+
export const decideSessionLock = (
|
|
104
|
+
context: Context,
|
|
105
|
+
profileName: string,
|
|
106
|
+
profile: RouterProfile,
|
|
107
|
+
pinnedTier?: RouterTier,
|
|
108
|
+
thinkingOverrides?: RouterThinkingByTier,
|
|
109
|
+
phaseBias = 0.5,
|
|
110
|
+
rules?: RoutingRule[],
|
|
111
|
+
): RoutingDecision => {
|
|
112
|
+
const analysisText = getSessionLockAnalysisText(context);
|
|
113
|
+
const synthetic: Context = {
|
|
114
|
+
systemPrompt: context.systemPrompt,
|
|
115
|
+
messages: analysisText
|
|
116
|
+
? [{ role: 'user', content: analysisText, timestamp: Date.now() }]
|
|
117
|
+
: [],
|
|
118
|
+
};
|
|
119
|
+
return decideRouting(
|
|
120
|
+
synthetic,
|
|
121
|
+
profileName,
|
|
122
|
+
profile,
|
|
123
|
+
undefined,
|
|
124
|
+
pinnedTier,
|
|
125
|
+
thinkingOverrides,
|
|
126
|
+
phaseBias,
|
|
127
|
+
rules,
|
|
128
|
+
false,
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** Per-turn thinking tier merged onto a session-locked model decision. */
|
|
133
|
+
export const applyThinkingToDecision = (
|
|
134
|
+
locked: RoutingDecision,
|
|
135
|
+
thinkingDecision: RoutingDecision,
|
|
136
|
+
profile: RouterProfile,
|
|
137
|
+
thinkingOverrides?: RouterThinkingByTier,
|
|
138
|
+
): RoutingDecision => {
|
|
139
|
+
const tier = thinkingDecision.tier;
|
|
140
|
+
const routed = profile[tier];
|
|
141
|
+
const baseThinking =
|
|
142
|
+
routed.thinking ??
|
|
143
|
+
(tier === 'high' ? 'high' : tier === 'low' ? 'low' : 'medium');
|
|
144
|
+
const effectiveThinking = thinkingOverrides?.[tier] ?? baseThinking;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
profile: locked.profile,
|
|
148
|
+
tier,
|
|
149
|
+
phase: thinkingDecision.phase,
|
|
150
|
+
targetProvider: locked.targetProvider,
|
|
151
|
+
targetModelId: locked.targetModelId,
|
|
152
|
+
targetLabel: locked.targetLabel,
|
|
153
|
+
reasoning: `${locked.reasoning} | Thinking: ${thinkingDecision.reasoning}`,
|
|
154
|
+
thinking: effectiveThinking,
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
isClassifier: thinkingDecision.isClassifier,
|
|
157
|
+
isRuleMatched: thinkingDecision.isRuleMatched,
|
|
158
|
+
isBudgetForced: thinkingDecision.isBudgetForced,
|
|
159
|
+
isContextTriggered: thinkingDecision.isContextTriggered,
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** Harness subagents: tier from system prompt + optional first user line. */
|
|
164
|
+
export const resolveTierFromPrompt = (
|
|
165
|
+
systemPrompt: string,
|
|
166
|
+
userText: string,
|
|
167
|
+
profileName: string,
|
|
168
|
+
profile: RouterProfile,
|
|
169
|
+
rules?: RoutingRule[],
|
|
170
|
+
phaseBias = 0.5,
|
|
171
|
+
): RouterTier => {
|
|
172
|
+
const context: Context = {
|
|
173
|
+
systemPrompt,
|
|
174
|
+
messages: userText
|
|
175
|
+
? [{ role: 'user', content: userText, timestamp: Date.now() }]
|
|
176
|
+
: [],
|
|
177
|
+
};
|
|
178
|
+
return decideSessionLock(
|
|
179
|
+
context,
|
|
180
|
+
profileName,
|
|
181
|
+
profile,
|
|
182
|
+
undefined,
|
|
183
|
+
undefined,
|
|
184
|
+
phaseBias,
|
|
185
|
+
rules,
|
|
186
|
+
).tier;
|
|
187
|
+
};
|
|
188
|
+
|
|
41
189
|
export const getRecentConversationText = (
|
|
42
190
|
context: Context,
|
|
43
191
|
limit = 6,
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
RouterThinkingByProfile,
|
|
4
4
|
RoutingDecision,
|
|
5
5
|
RouterPersistedState,
|
|
6
|
+
SessionLock,
|
|
6
7
|
} from './types.js';
|
|
7
8
|
|
|
8
9
|
export const isRouterPersistedState = (
|
|
@@ -30,6 +31,7 @@ export const buildPersistedState = (
|
|
|
30
31
|
lastDecision: RoutingDecision | undefined,
|
|
31
32
|
lastNonRouterModel: string | undefined,
|
|
32
33
|
accumulatedCost: number,
|
|
34
|
+
sessionLock: SessionLock | undefined,
|
|
33
35
|
): RouterPersistedState => {
|
|
34
36
|
return {
|
|
35
37
|
enabled: routerEnabled,
|
|
@@ -37,6 +39,7 @@ export const buildPersistedState = (
|
|
|
37
39
|
pinTier: pinnedTierByProfile[selectedProfile],
|
|
38
40
|
pinByProfile: { ...pinnedTierByProfile },
|
|
39
41
|
thinkingByProfile: { ...thinkingByProfile },
|
|
42
|
+
sessionLock,
|
|
40
43
|
debugEnabled,
|
|
41
44
|
widgetEnabled,
|
|
42
45
|
debugHistory,
|
|
@@ -53,12 +53,21 @@ export interface RoutingDecision {
|
|
|
53
53
|
isRuleMatched?: boolean;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** Fixed model for a router profile for the lifetime of a session (or until profile switch). */
|
|
57
|
+
export interface SessionLock {
|
|
58
|
+
profile: string;
|
|
59
|
+
tier: RouterTier;
|
|
60
|
+
modelRef: string;
|
|
61
|
+
reasoning: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
export interface RouterPersistedState {
|
|
57
65
|
enabled: boolean;
|
|
58
66
|
selectedProfile: string;
|
|
59
67
|
pinTier?: RouterTier;
|
|
60
68
|
pinByProfile?: RouterPinByProfile;
|
|
61
69
|
thinkingByProfile?: RouterThinkingByProfile;
|
|
70
|
+
sessionLock?: SessionLock;
|
|
62
71
|
debugEnabled?: boolean;
|
|
63
72
|
widgetEnabled?: boolean;
|
|
64
73
|
debugHistory?: RoutingDecision[];
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
RouterConfig,
|
|
5
5
|
RouterPinByProfile,
|
|
6
6
|
RouterThinkingByProfile,
|
|
7
|
+
SessionLock,
|
|
7
8
|
} from './types.js';
|
|
8
9
|
|
|
9
10
|
const getEffectiveThinking = (
|
|
@@ -63,6 +64,7 @@ export const updateStatus = (
|
|
|
63
64
|
accumulatedCost: number,
|
|
64
65
|
widgetEnabled: boolean,
|
|
65
66
|
currentConfig: RouterConfig,
|
|
67
|
+
sessionLock: SessionLock | undefined,
|
|
66
68
|
) => {
|
|
67
69
|
const activeRouterProfile = routerEnabled ? selectedProfile : undefined;
|
|
68
70
|
const statusProfile = selectedProfile;
|
|
@@ -81,7 +83,13 @@ export const updateStatus = (
|
|
|
81
83
|
activeRouterProfile,
|
|
82
84
|
lastDecision,
|
|
83
85
|
);
|
|
84
|
-
|
|
86
|
+
const locked =
|
|
87
|
+
sessionLock?.profile === activeRouterProfile
|
|
88
|
+
? sessionLock.modelRef
|
|
89
|
+
: lastDecision.targetLabel;
|
|
90
|
+
statusText = `router:${activeRouterProfile}${pinLabel} locked:${locked} thinking:${lastDecision.tier} (${effectiveThinking})`;
|
|
91
|
+
} else if (sessionLock?.profile === activeRouterProfile) {
|
|
92
|
+
statusText = `router:${activeRouterProfile}${pinLabel} locked:${sessionLock.modelRef} -> waiting`;
|
|
85
93
|
} else {
|
|
86
94
|
statusText = `router:${activeRouterProfile}${pinLabel} -> waiting`;
|
|
87
95
|
}
|
|
@@ -104,6 +112,11 @@ export const updateStatus = (
|
|
|
104
112
|
? ` / $${currentConfig.maxSessionBudget.toFixed(2)}`
|
|
105
113
|
: ''),
|
|
106
114
|
];
|
|
115
|
+
if (sessionLock && sessionLock.profile === statusProfile) {
|
|
116
|
+
widgetLines.push(
|
|
117
|
+
`Locked model: ${sessionLock.modelRef} (lock tier ${sessionLock.tier})`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
107
120
|
if (lastDecision && lastDecision.profile === statusProfile) {
|
|
108
121
|
const effectiveThinking = getEffectiveThinking(
|
|
109
122
|
thinkingByProfile,
|
|
@@ -114,8 +127,9 @@ export const updateStatus = (
|
|
|
114
127
|
const flagsStr = flags.length > 0 ? ` [${flags.join(',')}]` : '';
|
|
115
128
|
|
|
116
129
|
widgetLines.push(
|
|
117
|
-
`
|
|
130
|
+
`Thinking: ${lastDecision.tier}${flagsStr} (${effectiveThinking})`,
|
|
118
131
|
`Phase: ${lastDecision.phase}`,
|
|
132
|
+
`Delegate: ${lastDecision.targetProvider}/${lastDecision.targetModelId}`,
|
|
119
133
|
);
|
|
120
134
|
} else if (!routerEnabled && lastNonRouterModel) {
|
|
121
135
|
widgetLines.push(`Fallback: ${lastNonRouterModel}`);
|