vigthoria-cli 1.11.12 → 1.11.18

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,99 @@ 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
+ forceMinimalFallback: route?.path === 'template-instant',
2555
+ });
2556
+ if (!result.ok && mustComplete) {
2557
+ result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2558
+ allowFallback: true,
2559
+ forceMinimalFallback: true,
2560
+ });
2561
+ }
2562
+ if (!result.ok) {
2563
+ if (!this.jsonOutput) {
2564
+ console.log(chalk.yellow(`Template instant path skipped: ${result.error || 'no match'}`));
2565
+ }
2566
+ return false;
2567
+ }
2568
+ const summary = [
2569
+ `Created ${result.entryPath || 'index.html'} via template-instant`,
2570
+ result.templateName ? `(${result.templateName})` : '',
2571
+ result.processingMs ? `in ${Math.round(result.processingMs)}ms` : '',
2572
+ result.usedFallback ? '(minimal fallback)' : '',
2573
+ ].filter(Boolean).join(' ');
2574
+ this.messages.push({ role: 'assistant', content: summary });
2575
+ const liveOutcome = createLiveOutcome();
2576
+ liveOutcome.requiresWorkspaceChanges = true;
2577
+ liveOutcome.workspaceHasOutput = true;
2578
+ liveOutcome.changedFileCount = 1;
2579
+ liveOutcome.tasksTotal = 1;
2580
+ liveOutcome.tasksSucceeded = 1;
2581
+ liveOutcome.answerContent = summary;
2582
+ const evaluation = evaluateExecutorSuccess(liveOutcome);
2583
+ this.lastAgentRunOutcome = {
2584
+ prompt,
2585
+ taskId: null,
2586
+ contextId: null,
2587
+ tasksSucceeded: 1,
2588
+ tasksTotal: 1,
2589
+ failedTaskIds: [],
2590
+ unfinishedTaskIds: [],
2591
+ qualityScore: null,
2592
+ qualityMissing: [],
2593
+ qualityBlockers: [],
2594
+ hasOutput: true,
2595
+ answerContent: summary,
2596
+ selfHealStatus: 'skipped',
2597
+ selfHealTool: null,
2598
+ plannerError: null,
2599
+ executorError: null,
2600
+ clientToolErrors: [],
2601
+ transportErrors: [],
2602
+ workspacePath: workspacePath || null,
2603
+ workspaceSyncIssue: null,
2604
+ finishedAt: Date.now(),
2605
+ };
2606
+ if (!this.jsonOutput) {
2607
+ console.log(chalk.green(`✓ ${summary}`));
2608
+ console.log(chalk.gray(' Run: vigthoria preview'));
2609
+ this.printAgentRunSummary(this.lastAgentRunOutcome, evaluation, 1);
2610
+ }
2611
+ return true;
2612
+ }
2400
2613
  async tryDirectSingleFileFlow(prompt) {
2401
2614
  if (!this.directPromptMode || !this.tools) {
2402
2615
  return false;
@@ -2516,13 +2729,7 @@ export class ChatCommand {
2516
2729
  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
2730
  }
2518
2731
  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);
2732
+ return promptRequiresWorkspaceChanges(prompt);
2526
2733
  }
2527
2734
  getPreviousActionablePrompt() {
2528
2735
  if (this.lastActionableUserInput && !this.isConfirmationFollowUp(this.lastActionableUserInput)) {
@@ -2663,7 +2870,11 @@ export class ChatCommand {
2663
2870
  }
2664
2871
  };
2665
2872
  const executionPrompt = this.buildExecutionPrompt(contextualPrompt);
2666
- const agentTaskType = this.inferAgentTaskType(prompt);
2873
+ const agentTaskType = this.lastAgentRoute?.taskKind || this.inferAgentTaskType(prompt);
2874
+ const workflowType = resolveWorkflowType(agentTaskType, prompt);
2875
+ const executionHints = buildExecutionHints(agentTaskType, prompt, {
2876
+ fastPath: this.lastAgentRoute?.fastPath,
2877
+ });
2667
2878
  const workspaceContext = {
2668
2879
  workspacePath: workspacePath,
2669
2880
  projectPath: workspacePath,
@@ -2699,10 +2910,14 @@ export class ChatCommand {
2699
2910
  workspace: { path: workspacePath },
2700
2911
  ...workspaceContext,
2701
2912
  agentTaskType,
2913
+ workflowType,
2914
+ executionHints,
2915
+ fastPath: this.lastAgentRoute?.fastPath,
2916
+ agentRoute: this.lastAgentRoute,
2702
2917
  executionSurface: 'cli',
2703
2918
  clientSurface: 'cli',
2704
2919
  localMachineCapable: true,
2705
- agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
2920
+ agentTimeoutMs: resolvePlannerAgentTimeoutMs(DEFAULT_V3_AGENT_TIMEOUT_MS, agentTaskType, prompt),
2706
2921
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
2707
2922
  agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
2708
2923
  model: routingPolicy.selectedModel,
@@ -2795,7 +3010,7 @@ export class ChatCommand {
2795
3010
  const previewGate = (response.metadata?.previewGate || null);
2796
3011
  const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
2797
3012
  const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
2798
- const requiresWorkspaceChanges = this.taskRequiresWorkspaceChanges(prompt);
3013
+ const requiresWorkspaceChanges = promptRequiresWorkspaceChanges(prompt);
2799
3014
  const answerContent = normalizeAgentAnswerContent(response.content, this.v3StreamedAnswerBuffer);
2800
3015
  liveOutcome.changedFileCount = changedFileCount;
2801
3016
  liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
@@ -2874,7 +3089,11 @@ export class ChatCommand {
2874
3089
  if (isToolEvidenceStubAnswer(finalAnswer)) {
2875
3090
  finalAnswer = '';
2876
3091
  }
2877
- const needsAnalysisRescue = !requiresWorkspaceChanges && !this.jsonOutput
3092
+ const needsAnalysisRescue = !shouldSkipAnalysisRescue({
3093
+ tasksTotal: liveOutcome.tasksTotal,
3094
+ changedFileCount,
3095
+ requiresWorkspaceChanges,
3096
+ }) && !this.jsonOutput
2878
3097
  && (!executorSucceeded || !isSubstantiveAgentAnswer(finalAnswer));
2879
3098
  if (needsAnalysisRescue) {
2880
3099
  const rescued = await this.tryAnalysisLocalRescue(prompt, workspacePath);
@@ -3146,13 +3365,16 @@ export class ChatCommand {
3146
3365
  this.v3LastActivity = Date.now();
3147
3366
  this.v3StreamingStarted = false;
3148
3367
  try {
3368
+ const retryTaskType = this.inferAgentTaskType(rawPrompt);
3149
3369
  const retryResponse = await this.api.runV3AgentWorkflow(executionPrompt, {
3150
3370
  workspace: { path: this.currentProjectPath },
3151
3371
  ...workspaceContext,
3152
3372
  executionSurface: 'cli',
3153
3373
  clientSurface: 'cli',
3154
3374
  localMachineCapable: true,
3155
- agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
3375
+ agentTaskType: retryTaskType,
3376
+ workflowType: resolveWorkflowType(retryTaskType, rawPrompt),
3377
+ agentTimeoutMs: resolvePlannerAgentTimeoutMs(DEFAULT_V3_AGENT_TIMEOUT_MS, retryTaskType, rawPrompt),
3156
3378
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
3157
3379
  agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
3158
3380
  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;