vigthoria-cli 1.11.12 → 1.11.17

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.
@@ -28,6 +28,7 @@ export declare class ChatCommand {
28
28
  private tools;
29
29
  private sessionManager;
30
30
  private projectMemory;
31
+ private workspaceBrain;
31
32
  private currentSession;
32
33
  private agentMode;
33
34
  private currentProjectPath;
@@ -51,6 +52,7 @@ export declare class ChatCommand {
51
52
  private v3StreamedAnswerDisplayed;
52
53
  private v3SeenToolCalls;
53
54
  private v3SeenToolResults;
55
+ private lastAgentRoute;
54
56
  private lastAgentRunOutcome;
55
57
  private isJwtExpirationError;
56
58
  private isNetworkError;
@@ -103,7 +105,11 @@ export declare class ChatCommand {
103
105
  private buildTaskShapingInstructions;
104
106
  private buildExecutionPrompt;
105
107
  private isProjectBrainRuntimeDisabled;
108
+ private getWorkspaceBrainService;
109
+ private canPromptForWorkspaceIndex;
110
+ private promptWorkspaceIndex;
106
111
  private buildProjectBrainRuntimeContext;
112
+ private buildWorkspaceBrainRuntimeContext;
107
113
  private rememberBrainEvent;
108
114
  private getPromptRuntimeContext;
109
115
  private v3IterationCount;
@@ -168,6 +174,10 @@ export declare class ChatCommand {
168
174
  private persistLocalLoopOutcome;
169
175
  private runLocalAgentLoop;
170
176
  private primeBypassedTargetFileContext;
177
+ /**
178
+ * Sub-second path for trivial HTML/hello-world: Template Service → index.html (no V3 planner).
179
+ */
180
+ private tryTemplateInstantPath;
171
181
  private tryDirectSingleFileFlow;
172
182
  private isConfirmationFollowUp;
173
183
  private taskRequiresWorkspaceChanges;
@@ -11,11 +11,15 @@ import { BridgeClient, getBridgeClient } from '../utils/bridge-client.js';
11
11
  import { WorkspaceWatcher } from '../utils/workspace-stream.js';
12
12
  import { TaskDisplay } from '../utils/task-display.js';
13
13
  import { ProjectMemoryService } from '../utils/project-memory.js';
14
+ import { WorkspaceBrainService } from '../utils/workspace-brain-service.js';
14
15
  import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
15
16
  import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session-menu.js';
16
17
  import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
17
18
  import { looksLikeMarkdownReport, renderMarkdownToTerminal, summarizeMarkdownReport, } from '../utils/terminalMarkdown.js';
18
19
  import { emitDeckEvent, isDeckModeEnabled } from '../utils/deckEvents.js';
20
+ import { inferAgentTaskType as sharedInferAgentTaskType, isTrivialHtmlPageRequest, resolvePlannerAgentTimeoutMs, resolveWorkflowType, shouldSkipAnalysisRescue, taskRequiresWorkspaceChanges as promptRequiresWorkspaceChanges, buildExecutionHints, } from '../utils/requestIntent.js';
21
+ import { resolveAgentRoute } from '../utils/agentRoute.js';
22
+ import { runTemplateInstantPath } from '../utils/templateInstantPath.js';
19
23
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
20
24
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
21
25
  if (!rawValue) {
@@ -48,6 +52,7 @@ export class ChatCommand {
48
52
  tools = null;
49
53
  sessionManager;
50
54
  projectMemory = null;
55
+ workspaceBrain = null;
51
56
  currentSession = null;
52
57
  agentMode = false;
53
58
  currentProjectPath = process.cwd();
@@ -72,6 +77,7 @@ export class ChatCommand {
72
77
  v3StreamedAnswerDisplayed = false;
73
78
  v3SeenToolCalls = new Set();
74
79
  v3SeenToolResults = new Set();
80
+ lastAgentRoute = null;
75
81
  // Last completed Agent run — used by /retry, /continue, and the final summary block.
76
82
  lastAgentRunOutcome = null;
77
83
  isJwtExpirationError(error) {
@@ -305,6 +311,16 @@ export class ChatCommand {
305
311
  routeReason: 'explicit-model-selection',
306
312
  };
307
313
  }
314
+ if (this.lastAgentRoute?.path === 'template-instant') {
315
+ return {
316
+ selectedModel: 'agent',
317
+ explicitModel: false,
318
+ heavyTask,
319
+ cloudEligible,
320
+ cloudSelected: false,
321
+ routeReason: `template-instant [${this.lastAgentRoute.source}]`,
322
+ };
323
+ }
308
324
  if (requiresV3Workflow) {
309
325
  return {
310
326
  selectedModel: 'agent',
@@ -315,6 +331,16 @@ export class ChatCommand {
315
331
  routeReason: 'v3-required-frontend-task',
316
332
  };
317
333
  }
334
+ if (this.lastAgentRoute?.source === 'balanced-4b' || this.lastAgentRoute?.source === 'merged') {
335
+ return {
336
+ selectedModel: 'agent',
337
+ explicitModel: false,
338
+ heavyTask,
339
+ cloudEligible,
340
+ cloudSelected: false,
341
+ routeReason: `balanced-4b-router → ${this.lastAgentRoute.taskKind}`,
342
+ };
343
+ }
318
344
  if (this.config.shouldUseCloudForHeavyTask(prompt)) {
319
345
  return {
320
346
  selectedModel: 'cloud',
@@ -551,14 +577,7 @@ export class ChatCommand {
551
577
  return 'debugging';
552
578
  if (this.isAnalysisLookupPrompt(prompt))
553
579
  return 'analysis';
554
- if (/\b(analy[sz]e|analyse|audit|review|inspect|identify\s+gaps?|gap\s+analysis|production\s+blockers?|read[\s-]?only)\b/i.test(prompt)) {
555
- return 'analysis';
556
- }
557
- if (/\b(implement|build|create|write|fix|repair|scaffold|deploy|add feature)\b/i.test(prompt)
558
- && !/\b(analy[sz]e|analyse|audit|review|inspect)\b/i.test(prompt)) {
559
- return /\b(game|html5|pacman|rogue|playable)\b/i.test(prompt) ? 'game-build' : 'implementation';
560
- }
561
- return 'analysis';
580
+ return sharedInferAgentTaskType(prompt);
562
581
  }
563
582
  buildTaskShapingInstructions(prompt) {
564
583
  const instructions = [];
@@ -594,6 +613,45 @@ export class ChatCommand {
594
613
  isProjectBrainRuntimeDisabled() {
595
614
  return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_BRAIN || process.env.VIGTHORIA_BRAIN_DISABLED || ''));
596
615
  }
616
+ getWorkspaceBrainService() {
617
+ if (this.isProjectBrainRuntimeDisabled()) {
618
+ return null;
619
+ }
620
+ try {
621
+ if (!this.workspaceBrain) {
622
+ this.workspaceBrain = new WorkspaceBrainService({
623
+ workspacePath: this.currentProjectPath,
624
+ apiBase: this.config.get('apiUrl'),
625
+ getAuthToken: () => this.config.get('authToken'),
626
+ autoIndex: false,
627
+ });
628
+ }
629
+ return this.workspaceBrain;
630
+ }
631
+ catch {
632
+ return null;
633
+ }
634
+ }
635
+ canPromptForWorkspaceIndex() {
636
+ if (this.jsonOutput || !process.stdin.isTTY || !process.stdout.isTTY) {
637
+ return false;
638
+ }
639
+ return !/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_INDEX_PROMPT || ''));
640
+ }
641
+ async promptWorkspaceIndex(fileCount, workspaceName) {
642
+ if (!this.canPromptForWorkspaceIndex()) {
643
+ return 'later';
644
+ }
645
+ const answer = await new Promise((resolve) => {
646
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
647
+ const prompt = `Index "${workspaceName}" for Vigthoria Brain (${fileCount} files) to improve large-context tasks? [y/N] `;
648
+ rl.question(chalk.cyan(prompt), (value) => {
649
+ rl.close();
650
+ resolve(value);
651
+ });
652
+ });
653
+ return /^(y|yes)$/i.test(answer.trim()) ? 'index_now' : 'later';
654
+ }
597
655
  buildProjectBrainRuntimeContext(prompt, source) {
598
656
  if (this.isProjectBrainRuntimeDisabled()) {
599
657
  return undefined;
@@ -620,6 +678,42 @@ export class ChatCommand {
620
678
  return undefined;
621
679
  }
622
680
  }
681
+ async buildWorkspaceBrainRuntimeContext(prompt) {
682
+ const service = this.getWorkspaceBrainService();
683
+ if (!service) {
684
+ return {};
685
+ }
686
+ try {
687
+ const result = await service.ensureIndexed({
688
+ promptIfMissing: this.canPromptForWorkspaceIndex(),
689
+ askToIndex: (fileCount, workspaceName) => this.promptWorkspaceIndex(fileCount, workspaceName),
690
+ });
691
+ const status = service.getStatus();
692
+ const context = {
693
+ workspaceBrain: {
694
+ schema: 'vigthoria.workspace-brain.v1',
695
+ indexed: result.indexed,
696
+ prompted: result.prompted || null,
697
+ indexedFileCount: status.indexedFileCount || result.fileCount,
698
+ totalChunks: status.totalChunks || result.chunkCount,
699
+ indexedAt: status.meta?.indexedAt || null,
700
+ indexHash: status.meta?.indexHash || null,
701
+ },
702
+ };
703
+ const codebaseContext = service.buildCodebaseContext(prompt);
704
+ if (codebaseContext) {
705
+ context.codebaseContext = codebaseContext;
706
+ }
707
+ const accountBrainContext = await service.fetchAccountBrainContext().catch(() => '');
708
+ if (accountBrainContext) {
709
+ context.accountBrainContext = accountBrainContext;
710
+ }
711
+ return context;
712
+ }
713
+ catch {
714
+ return {};
715
+ }
716
+ }
623
717
  rememberBrainEvent(type, text, mode) {
624
718
  if (this.isProjectBrainRuntimeDisabled()) {
625
719
  return;
@@ -641,6 +735,19 @@ export class ChatCommand {
641
735
  const brainContext = this.buildProjectBrainRuntimeContext(prompt, this.operatorMode ? 'vigthoria-cli.operator' : this.agentMode ? 'vigthoria-cli.agent' : 'vigthoria-cli.chat');
642
736
  if (brainContext) {
643
737
  runtimeContext.vigthoriaBrain = brainContext;
738
+ if (typeof brainContext.context === 'string' && brainContext.context.trim()) {
739
+ runtimeContext.projectMemory = brainContext.context;
740
+ }
741
+ }
742
+ const workspaceBrainContext = await this.buildWorkspaceBrainRuntimeContext(prompt);
743
+ if (workspaceBrainContext.workspaceBrain) {
744
+ runtimeContext.workspaceBrain = workspaceBrainContext.workspaceBrain;
745
+ }
746
+ if (typeof workspaceBrainContext.codebaseContext === 'string' && workspaceBrainContext.codebaseContext.trim()) {
747
+ runtimeContext.codebaseContext = workspaceBrainContext.codebaseContext;
748
+ }
749
+ if (typeof workspaceBrainContext.accountBrainContext === 'string' && workspaceBrainContext.accountBrainContext.trim()) {
750
+ runtimeContext.accountBrainContext = workspaceBrainContext.accountBrainContext;
644
751
  }
645
752
  if (!this.isBrowserTaskPrompt(prompt)) {
646
753
  return runtimeContext;
@@ -2057,7 +2164,20 @@ export class ChatCommand {
2057
2164
  if (!this.tools) {
2058
2165
  throw new Error('Agent tools are not initialized.');
2059
2166
  }
2167
+ this.lastAgentRoute = await resolveAgentRoute(this.api, prompt);
2168
+ if (!this.jsonOutput) {
2169
+ const r = this.lastAgentRoute;
2170
+ const routerNote = r.routerLatencyMs
2171
+ ? chalk.gray(` (${r.routerModel || 'balanced-4b'} ${r.routerLatencyMs}ms)`)
2172
+ : '';
2173
+ this.logger.debug(`Agent route: ${r.path} / ${r.taskKind} [${r.source}]${routerNote} — ${r.reason}`);
2174
+ }
2060
2175
  const requiresV3Workflow = this.shouldRequireV3AgentWorkflow(prompt);
2176
+ const handledByTemplateInstant = await this.tryTemplateInstantPath(prompt);
2177
+ if (handledByTemplateInstant) {
2178
+ this.saveSession();
2179
+ return;
2180
+ }
2061
2181
  const handledByDirectFileFlow = await this.tryDirectSingleFileFlow(prompt);
2062
2182
  if (handledByDirectFileFlow) {
2063
2183
  this.saveSession();
@@ -2397,6 +2517,98 @@ export class ChatCommand {
2397
2517
  this.messages.push({ role: 'system', content: readSummary });
2398
2518
  }
2399
2519
  }
2520
+ /**
2521
+ * Sub-second path for trivial HTML/hello-world: Template Service → index.html (no V3 planner).
2522
+ */
2523
+ async tryTemplateInstantPath(prompt) {
2524
+ const route = this.lastAgentRoute;
2525
+ if (route?.path !== 'template-instant' && !isTrivialHtmlPageRequest(prompt)) {
2526
+ return false;
2527
+ }
2528
+ let workspacePath = this.currentProjectPath;
2529
+ try {
2530
+ const fromPrompt = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
2531
+ if (fromPrompt)
2532
+ workspacePath = fromPrompt;
2533
+ }
2534
+ catch {
2535
+ // use default workspace
2536
+ }
2537
+ if (!this.jsonOutput) {
2538
+ console.log();
2539
+ console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
2540
+ const r = this.lastAgentRoute;
2541
+ console.log(chalk.gray(`Reason: ${r?.reason || 'template-instant (trivial HTML page)'}`));
2542
+ console.log(chalk.gray(`Path: template-instant [${r?.source || 'regex'}]`));
2543
+ console.log(chalk.gray('Model: template-service'));
2544
+ console.log(chalk.gray('Cloud Eligible: false'));
2545
+ console.log(chalk.gray('Cloud Selected: false'));
2546
+ console.log(chalk.gray('━'.repeat(30)));
2547
+ console.log();
2548
+ console.log(chalk.cyan('⚡ Template instant path — matching template and writing index.html...'));
2549
+ }
2550
+ this.messages.push({ role: 'user', content: prompt });
2551
+ const mustComplete = route?.path === 'template-instant';
2552
+ let result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2553
+ allowFallback: true,
2554
+ });
2555
+ if (!result.ok && mustComplete) {
2556
+ result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2557
+ allowFallback: true,
2558
+ forceMinimalFallback: true,
2559
+ });
2560
+ }
2561
+ if (!result.ok) {
2562
+ if (!this.jsonOutput) {
2563
+ console.log(chalk.yellow(`Template instant path skipped: ${result.error || 'no match'}`));
2564
+ }
2565
+ return false;
2566
+ }
2567
+ const summary = [
2568
+ `Created ${result.entryPath || 'index.html'} via template-instant`,
2569
+ result.templateName ? `(${result.templateName})` : '',
2570
+ result.processingMs ? `in ${Math.round(result.processingMs)}ms` : '',
2571
+ result.usedFallback ? '(minimal fallback)' : '',
2572
+ ].filter(Boolean).join(' ');
2573
+ this.messages.push({ role: 'assistant', content: summary });
2574
+ const liveOutcome = createLiveOutcome();
2575
+ liveOutcome.requiresWorkspaceChanges = true;
2576
+ liveOutcome.workspaceHasOutput = true;
2577
+ liveOutcome.changedFileCount = 1;
2578
+ liveOutcome.tasksTotal = 1;
2579
+ liveOutcome.tasksSucceeded = 1;
2580
+ liveOutcome.answerContent = summary;
2581
+ const evaluation = evaluateExecutorSuccess(liveOutcome);
2582
+ this.lastAgentRunOutcome = {
2583
+ prompt,
2584
+ taskId: null,
2585
+ contextId: null,
2586
+ tasksSucceeded: 1,
2587
+ tasksTotal: 1,
2588
+ failedTaskIds: [],
2589
+ unfinishedTaskIds: [],
2590
+ qualityScore: null,
2591
+ qualityMissing: [],
2592
+ qualityBlockers: [],
2593
+ hasOutput: true,
2594
+ answerContent: summary,
2595
+ selfHealStatus: 'skipped',
2596
+ selfHealTool: null,
2597
+ plannerError: null,
2598
+ executorError: null,
2599
+ clientToolErrors: [],
2600
+ transportErrors: [],
2601
+ workspacePath: workspacePath || null,
2602
+ workspaceSyncIssue: null,
2603
+ finishedAt: Date.now(),
2604
+ };
2605
+ if (!this.jsonOutput) {
2606
+ console.log(chalk.green(`✓ ${summary}`));
2607
+ console.log(chalk.gray(' Run: vigthoria preview'));
2608
+ this.printAgentRunSummary(this.lastAgentRunOutcome, evaluation, 1);
2609
+ }
2610
+ return true;
2611
+ }
2400
2612
  async tryDirectSingleFileFlow(prompt) {
2401
2613
  if (!this.directPromptMode || !this.tools) {
2402
2614
  return false;
@@ -2516,13 +2728,7 @@ export class ChatCommand {
2516
2728
  return /^(ja|ja bitte|ja bitte mach das|mach das|bitte mach das|genau|ok|okay|yes|yes please|please do|do it|go ahead|continue|proceed|make it so)$/.test(normalized);
2517
2729
  }
2518
2730
  taskRequiresWorkspaceChanges(prompt) {
2519
- const text = String(prompt || '').trim();
2520
- const readOnlyIntent = /\b(analy[sz]e|analyse|analysis|audit|review|inspect|proof|understand|summari[sz]e|scan|read[\s-]?only|where we left|what is|how does|show me|tell me|identify\s+gaps?|gap\s+analysis|production\s+blockers?)\b/i.test(text);
2521
- const explicitWriteIntent = /\b(build|create|make|implement|complete|fix|repair|edit|modify|write|generate|add|finish|scaffold|develop|update|change|refactor)\b/i.test(text);
2522
- if (readOnlyIntent && !explicitWriteIntent) {
2523
- return false;
2524
- }
2525
- return explicitWriteIntent && /\b(file|project|game|app|website|html5|frontend|component|feature|code|workspace|repo)\b/i.test(text);
2731
+ return promptRequiresWorkspaceChanges(prompt);
2526
2732
  }
2527
2733
  getPreviousActionablePrompt() {
2528
2734
  if (this.lastActionableUserInput && !this.isConfirmationFollowUp(this.lastActionableUserInput)) {
@@ -2663,7 +2869,11 @@ export class ChatCommand {
2663
2869
  }
2664
2870
  };
2665
2871
  const executionPrompt = this.buildExecutionPrompt(contextualPrompt);
2666
- const agentTaskType = this.inferAgentTaskType(prompt);
2872
+ const agentTaskType = this.lastAgentRoute?.taskKind || this.inferAgentTaskType(prompt);
2873
+ const workflowType = resolveWorkflowType(agentTaskType, prompt);
2874
+ const executionHints = buildExecutionHints(agentTaskType, prompt, {
2875
+ fastPath: this.lastAgentRoute?.fastPath,
2876
+ });
2667
2877
  const workspaceContext = {
2668
2878
  workspacePath: workspacePath,
2669
2879
  projectPath: workspacePath,
@@ -2699,10 +2909,14 @@ export class ChatCommand {
2699
2909
  workspace: { path: workspacePath },
2700
2910
  ...workspaceContext,
2701
2911
  agentTaskType,
2912
+ workflowType,
2913
+ executionHints,
2914
+ fastPath: this.lastAgentRoute?.fastPath,
2915
+ agentRoute: this.lastAgentRoute,
2702
2916
  executionSurface: 'cli',
2703
2917
  clientSurface: 'cli',
2704
2918
  localMachineCapable: true,
2705
- agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
2919
+ agentTimeoutMs: resolvePlannerAgentTimeoutMs(DEFAULT_V3_AGENT_TIMEOUT_MS, agentTaskType, prompt),
2706
2920
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
2707
2921
  agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
2708
2922
  model: routingPolicy.selectedModel,
@@ -2795,7 +3009,7 @@ export class ChatCommand {
2795
3009
  const previewGate = (response.metadata?.previewGate || null);
2796
3010
  const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
2797
3011
  const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
2798
- const requiresWorkspaceChanges = this.taskRequiresWorkspaceChanges(prompt);
3012
+ const requiresWorkspaceChanges = promptRequiresWorkspaceChanges(prompt);
2799
3013
  const answerContent = normalizeAgentAnswerContent(response.content, this.v3StreamedAnswerBuffer);
2800
3014
  liveOutcome.changedFileCount = changedFileCount;
2801
3015
  liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
@@ -2874,7 +3088,11 @@ export class ChatCommand {
2874
3088
  if (isToolEvidenceStubAnswer(finalAnswer)) {
2875
3089
  finalAnswer = '';
2876
3090
  }
2877
- const needsAnalysisRescue = !requiresWorkspaceChanges && !this.jsonOutput
3091
+ const needsAnalysisRescue = !shouldSkipAnalysisRescue({
3092
+ tasksTotal: liveOutcome.tasksTotal,
3093
+ changedFileCount,
3094
+ requiresWorkspaceChanges,
3095
+ }) && !this.jsonOutput
2878
3096
  && (!executorSucceeded || !isSubstantiveAgentAnswer(finalAnswer));
2879
3097
  if (needsAnalysisRescue) {
2880
3098
  const rescued = await this.tryAnalysisLocalRescue(prompt, workspacePath);
@@ -3146,13 +3364,16 @@ export class ChatCommand {
3146
3364
  this.v3LastActivity = Date.now();
3147
3365
  this.v3StreamingStarted = false;
3148
3366
  try {
3367
+ const retryTaskType = this.inferAgentTaskType(rawPrompt);
3149
3368
  const retryResponse = await this.api.runV3AgentWorkflow(executionPrompt, {
3150
3369
  workspace: { path: this.currentProjectPath },
3151
3370
  ...workspaceContext,
3152
3371
  executionSurface: 'cli',
3153
3372
  clientSurface: 'cli',
3154
3373
  localMachineCapable: true,
3155
- agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
3374
+ agentTaskType: retryTaskType,
3375
+ workflowType: resolveWorkflowType(retryTaskType, rawPrompt),
3376
+ agentTimeoutMs: resolvePlannerAgentTimeoutMs(DEFAULT_V3_AGENT_TIMEOUT_MS, retryTaskType, rawPrompt),
3156
3377
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
3157
3378
  agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
3158
3379
  model: routingPolicy.selectedModel,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Unified agent routing: regex (instant) → optional Balanced 4B → V3 execution hints.
3
+ */
4
+ import type { APIClient } from './api.js';
5
+ import { type AgentRoutePath } from './fastAgentRouter.js';
6
+ export type { AgentRoutePath };
7
+ export interface AgentRouteDecision {
8
+ path: AgentRoutePath;
9
+ taskKind: string;
10
+ fastPath?: string;
11
+ confidence: number;
12
+ reason: string;
13
+ source: 'regex-trivial' | 'regex' | 'balanced-4b' | 'merged';
14
+ routerLatencyMs?: number;
15
+ routerModel?: string;
16
+ }
17
+ export declare function resolveAgentRoute(api: APIClient, prompt: string): Promise<AgentRouteDecision>;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Unified agent routing: regex (instant) → optional Balanced 4B → V3 execution hints.
3
+ */
4
+ import { inferAgentTaskType, isTrivialHtmlPageRequest, isContinueOrRetryPrompt, hasGameIntent, hasWebPageIntent, taskRequiresWorkspaceChanges, stripExecutionShaping, } from './requestIntent.js';
5
+ import { isFastInferenceRouterAvailable, routeWithBalanced4b, shouldUseBalancedRouter, } from './fastAgentRouter.js';
6
+ function normalizeTaskKind(value, fallback) {
7
+ const kind = String(value || fallback).trim().toLowerCase();
8
+ const allowed = new Set([
9
+ 'web-build', 'game-build', 'implementation', 'analysis', 'debugging', 'verification', 'build', 'repair',
10
+ ]);
11
+ if (allowed.has(kind)) {
12
+ return kind === 'build' ? 'implementation' : kind;
13
+ }
14
+ return fallback;
15
+ }
16
+ function mergeLlmRoute(regexTask, prompt, parsed) {
17
+ const llmPath = String(parsed.path || '').toLowerCase();
18
+ const llmTask = normalizeTaskKind(parsed.task_kind || parsed.taskKind, regexTask);
19
+ const llmFast = parsed.fast_path ?? parsed.fastPath ?? undefined;
20
+ const confidence = Number(parsed.confidence) || 0.75;
21
+ const reason = String(parsed.reason || 'balanced-4b router');
22
+ // Regex game/web signals override wrong LLM labels (hello-world → v3-agent bug).
23
+ if (isTrivialHtmlPageRequest(prompt) || (hasWebPageIntent(prompt) && !hasGameIntent(prompt) && llmTask === 'game-build')) {
24
+ return {
25
+ path: 'template-instant',
26
+ taskKind: 'web-build',
27
+ fastPath: 'template-instant',
28
+ confidence: Math.max(confidence, 0.92),
29
+ reason: 'regex web-page override',
30
+ source: 'merged',
31
+ };
32
+ }
33
+ if (llmPath === 'template-instant' || llmPath === 'html-template' || llmFast === 'template-instant') {
34
+ if (hasWebPageIntent(prompt) || isTrivialHtmlPageRequest(prompt)) {
35
+ return {
36
+ path: 'template-instant',
37
+ taskKind: 'web-build',
38
+ fastPath: 'template-instant',
39
+ confidence,
40
+ reason,
41
+ source: 'merged',
42
+ };
43
+ }
44
+ }
45
+ if (llmPath === 'analysis-only' || llmTask === 'analysis' || llmTask === 'debugging') {
46
+ if (!taskRequiresWorkspaceChanges(prompt)) {
47
+ return {
48
+ path: 'analysis-only',
49
+ taskKind: llmTask === 'debugging' ? 'debugging' : 'analysis',
50
+ confidence,
51
+ reason,
52
+ source: 'balanced-4b',
53
+ };
54
+ }
55
+ }
56
+ if (hasGameIntent(prompt) || llmTask === 'game-build') {
57
+ return {
58
+ path: 'v3-agent',
59
+ taskKind: 'game-build',
60
+ confidence,
61
+ reason,
62
+ source: 'merged',
63
+ };
64
+ }
65
+ const taskKind = (regexTask === 'web-build' || regexTask === 'game-build') ? regexTask : (llmTask || regexTask);
66
+ return {
67
+ path: 'v3-agent',
68
+ taskKind,
69
+ fastPath: typeof llmFast === 'string' ? llmFast : undefined,
70
+ confidence,
71
+ reason,
72
+ source: 'balanced-4b',
73
+ };
74
+ }
75
+ export async function resolveAgentRoute(api, prompt) {
76
+ const stripped = stripExecutionShaping(prompt);
77
+ if (isContinueOrRetryPrompt(stripped)) {
78
+ const regexTask = inferAgentTaskType(stripped);
79
+ return {
80
+ path: 'v3-agent',
81
+ taskKind: regexTask === 'analysis' ? 'implementation' : regexTask,
82
+ confidence: 0.9,
83
+ reason: 'continue/retry follow-up (regex)',
84
+ source: 'regex',
85
+ };
86
+ }
87
+ if (isTrivialHtmlPageRequest(stripped)) {
88
+ return {
89
+ path: 'template-instant',
90
+ taskKind: 'web-build',
91
+ fastPath: 'template-instant',
92
+ confidence: 1,
93
+ reason: 'trivial html page (regex)',
94
+ source: 'regex-trivial',
95
+ };
96
+ }
97
+ const regexTask = inferAgentTaskType(stripped);
98
+ if (!taskRequiresWorkspaceChanges(stripped) && (regexTask === 'analysis' || regexTask === 'debugging')) {
99
+ return {
100
+ path: 'analysis-only',
101
+ taskKind: regexTask,
102
+ confidence: 0.95,
103
+ reason: 'read-only analysis (regex)',
104
+ source: 'regex',
105
+ };
106
+ }
107
+ if (hasGameIntent(stripped) && regexTask === 'game-build') {
108
+ return {
109
+ path: 'v3-agent',
110
+ taskKind: 'game-build',
111
+ confidence: 0.95,
112
+ reason: 'game build (regex)',
113
+ source: 'regex',
114
+ };
115
+ }
116
+ const selfHosted = api.getSelfHostedModelsApiUrl();
117
+ const tryLlm = shouldUseBalancedRouter(stripped, regexTask)
118
+ && await isFastInferenceRouterAvailable(selfHosted);
119
+ if (tryLlm) {
120
+ const llm = await routeWithBalanced4b(stripped, selfHosted);
121
+ if (llm.ok && llm.parsed) {
122
+ const merged = mergeLlmRoute(regexTask, stripped, llm.parsed);
123
+ return {
124
+ ...merged,
125
+ routerLatencyMs: llm.latencyMs,
126
+ routerModel: llm.model,
127
+ };
128
+ }
129
+ }
130
+ return {
131
+ path: 'v3-agent',
132
+ taskKind: regexTask,
133
+ confidence: 0.8,
134
+ reason: 'default v3 agent (regex)',
135
+ source: 'regex',
136
+ };
137
+ }
@@ -237,7 +237,8 @@ export declare class APIClient {
237
237
  * on Windows / Node 25+.
238
238
  */
239
239
  destroy(): void;
240
- private getSelfHostedModelsApiUrl;
240
+ /** Exposed for Balanced 4B agent routing (local inference :8016). */
241
+ getSelfHostedModelsApiUrl(): string | null;
241
242
  login(email: string, password: string): Promise<boolean>;
242
243
  loginWithToken(token: string): Promise<boolean>;
243
244
  private extractUserProfile;