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.
- package/dist/commands/chat.d.ts +10 -0
- package/dist/commands/chat.js +242 -20
- package/dist/utils/agentRoute.d.ts +17 -0
- package/dist/utils/agentRoute.js +137 -0
- package/dist/utils/api.d.ts +2 -1
- package/dist/utils/api.js +71 -73
- package/dist/utils/fastAgentRouter.d.ts +34 -0
- package/dist/utils/fastAgentRouter.js +171 -0
- package/dist/utils/requestIntent.d.ts +35 -0
- package/dist/utils/requestIntent.js +188 -0
- package/dist/utils/templateInstantPath.d.ts +18 -0
- package/dist/utils/templateInstantPath.js +125 -0
- package/dist/utils/v3-workspace-path.d.ts +7 -0
- package/dist/utils/v3-workspace-path.js +85 -0
- package/dist/utils/workspace-stream.js +9 -2
- package/package.json +2 -1
package/dist/utils/api.js
CHANGED
|
@@ -12,6 +12,8 @@ import WebSocket from 'ws';
|
|
|
12
12
|
import { buildSemanticContext } from './context-ranker.js';
|
|
13
13
|
import { getChangedFiles } from './workspace-cache.js';
|
|
14
14
|
import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
|
|
15
|
+
import { joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
|
|
16
|
+
import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
|
|
15
17
|
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
|
|
16
18
|
export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
|
|
17
19
|
export class CLIError extends Error {
|
|
@@ -396,6 +398,7 @@ export class APIClient {
|
|
|
396
398
|
this.ws = null;
|
|
397
399
|
}
|
|
398
400
|
}
|
|
401
|
+
/** Exposed for Balanced 4B agent routing (local inference :8016). */
|
|
399
402
|
getSelfHostedModelsApiUrl() {
|
|
400
403
|
const configuredUrl = process.env.VIGTHORIA_SELF_HOSTED_MODELS_API_URL
|
|
401
404
|
|| this.config.get('selfHostedModelsApiUrl');
|
|
@@ -1446,6 +1449,25 @@ export class APIClient {
|
|
|
1446
1449
|
// files are provided inline in localWorkspaceSummary.workspaceFiles.
|
|
1447
1450
|
const effectiveWorkspacePath = serverWorkspacePath || null;
|
|
1448
1451
|
const needsHydration = !serverWorkspacePath && !!localWorkspacePath;
|
|
1452
|
+
const agentTaskType = resolvedContext.agentTaskType || 'general';
|
|
1453
|
+
const rawPrompt = resolvedContext.rawPrompt || resolvedContext.prompt || '';
|
|
1454
|
+
const executionHints = (resolvedContext.executionHints && typeof resolvedContext.executionHints === 'object')
|
|
1455
|
+
? { ...resolvedContext.executionHints }
|
|
1456
|
+
: buildExecutionHints(agentTaskType, rawPrompt, {
|
|
1457
|
+
fastPath: resolvedContext.fastPath,
|
|
1458
|
+
});
|
|
1459
|
+
if (!executionHints.task_kind) {
|
|
1460
|
+
executionHints.task_kind = agentTaskType;
|
|
1461
|
+
}
|
|
1462
|
+
if (!executionHints.classify_from) {
|
|
1463
|
+
executionHints.classify_from = rawPrompt;
|
|
1464
|
+
}
|
|
1465
|
+
if (resolvedContext.workflowType === 'analysis_only' || resolvedContext.workflowType === 'full') {
|
|
1466
|
+
executionHints.workflow_type = resolvedContext.workflowType;
|
|
1467
|
+
if (resolvedContext.workflowType === 'analysis_only') {
|
|
1468
|
+
executionHints.requires_file_changes = false;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1449
1471
|
const payload = {
|
|
1450
1472
|
workspace: this.buildPublicWorkspaceDescriptor(resolvedContext.workspace, {
|
|
1451
1473
|
localWorkspacePath,
|
|
@@ -1453,16 +1475,11 @@ export class APIClient {
|
|
|
1453
1475
|
}),
|
|
1454
1476
|
activeFile: resolvedContext.activeFile || null,
|
|
1455
1477
|
history: resolvedContext.history || [],
|
|
1456
|
-
agentTaskType
|
|
1457
|
-
|
|
1478
|
+
agentTaskType,
|
|
1479
|
+
workflowType: executionHints.workflow_type,
|
|
1480
|
+
rawPrompt,
|
|
1458
1481
|
contextualPrompt: resolvedContext.contextualPrompt || '',
|
|
1459
|
-
executionHints
|
|
1460
|
-
task_kind: resolvedContext.agentTaskType || 'general',
|
|
1461
|
-
requires_file_changes: resolvedContext.agentTaskType === 'analysis' || resolvedContext.agentTaskType === 'verification'
|
|
1462
|
-
? false
|
|
1463
|
-
: undefined,
|
|
1464
|
-
classify_from: resolvedContext.rawPrompt || resolvedContext.prompt || '',
|
|
1465
|
-
},
|
|
1482
|
+
executionHints,
|
|
1466
1483
|
model: resolvedModel,
|
|
1467
1484
|
requestedModel,
|
|
1468
1485
|
requestedModelResolved: resolvedModel,
|
|
@@ -1523,6 +1540,9 @@ export class APIClient {
|
|
|
1523
1540
|
let json = JSON.stringify(payload);
|
|
1524
1541
|
if (json.length <= LIMIT)
|
|
1525
1542
|
return json;
|
|
1543
|
+
if (!/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_QUIET_CONTEXT_COMPACTION || ''))) {
|
|
1544
|
+
process.stderr.write(`Workspace context is large - sent a focused summary (${Math.round(LIMIT / 1000)}k limit).\n`);
|
|
1545
|
+
}
|
|
1526
1546
|
if (process.env.DEBUG || process.env.VIGTHORIA_DEBUG) {
|
|
1527
1547
|
process.stderr.write(`[context] Payload ${json.length} chars exceeds ${LIMIT} limit, compacting...\n`);
|
|
1528
1548
|
}
|
|
@@ -1651,6 +1671,25 @@ export class APIClient {
|
|
|
1651
1671
|
const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
|
|
1652
1672
|
const rawBrain = resolvedContext.vigthoriaBrain || resolvedContext.vigthoria_brain || null;
|
|
1653
1673
|
const dedupedBrain = rawBrain ? this.dedupeBrainPayload(rawBrain) : null;
|
|
1674
|
+
const agentTaskType = resolvedContext.agentTaskType || 'general';
|
|
1675
|
+
const rawPrompt = resolvedContext.rawPrompt || resolvedContext.prompt || '';
|
|
1676
|
+
const executionHints = (resolvedContext.executionHints && typeof resolvedContext.executionHints === 'object')
|
|
1677
|
+
? { ...resolvedContext.executionHints }
|
|
1678
|
+
: buildExecutionHints(agentTaskType, rawPrompt, {
|
|
1679
|
+
fastPath: resolvedContext.fastPath,
|
|
1680
|
+
});
|
|
1681
|
+
if (!executionHints.task_kind) {
|
|
1682
|
+
executionHints.task_kind = agentTaskType;
|
|
1683
|
+
}
|
|
1684
|
+
if (!executionHints.classify_from) {
|
|
1685
|
+
executionHints.classify_from = rawPrompt;
|
|
1686
|
+
}
|
|
1687
|
+
if (resolvedContext.workflowType === 'analysis_only' || resolvedContext.workflowType === 'full') {
|
|
1688
|
+
executionHints.workflow_type = resolvedContext.workflowType;
|
|
1689
|
+
if (resolvedContext.workflowType === 'analysis_only') {
|
|
1690
|
+
executionHints.requires_file_changes = false;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1654
1693
|
return JSON.stringify({
|
|
1655
1694
|
workspace: this.buildPublicWorkspaceDescriptor(resolvedContext.workspace, {
|
|
1656
1695
|
localWorkspacePath: targetPath,
|
|
@@ -1658,16 +1697,11 @@ export class APIClient {
|
|
|
1658
1697
|
}),
|
|
1659
1698
|
activeFile: resolvedContext.activeFile || null,
|
|
1660
1699
|
history: resolvedContext.history || [],
|
|
1661
|
-
agentTaskType
|
|
1662
|
-
|
|
1700
|
+
agentTaskType,
|
|
1701
|
+
workflowType: executionHints.workflow_type,
|
|
1702
|
+
rawPrompt,
|
|
1663
1703
|
contextualPrompt: resolvedContext.contextualPrompt || '',
|
|
1664
|
-
executionHints
|
|
1665
|
-
task_kind: resolvedContext.agentTaskType || 'general',
|
|
1666
|
-
requires_file_changes: resolvedContext.agentTaskType === 'analysis' || resolvedContext.agentTaskType === 'verification'
|
|
1667
|
-
? false
|
|
1668
|
-
: undefined,
|
|
1669
|
-
classify_from: resolvedContext.rawPrompt || resolvedContext.prompt || '',
|
|
1670
|
-
},
|
|
1704
|
+
executionHints,
|
|
1671
1705
|
executionSurface: resolvedContext.executionSurface || 'cli',
|
|
1672
1706
|
clientSurface: resolvedContext.clientSurface || 'cli',
|
|
1673
1707
|
localMachineCapable: resolvedContext.localMachineCapable !== false,
|
|
@@ -2660,7 +2694,7 @@ menu {
|
|
|
2660
2694
|
if (!relativePath && String(rawPath || '.').trim() !== '.') {
|
|
2661
2695
|
return null;
|
|
2662
2696
|
}
|
|
2663
|
-
const absolutePath =
|
|
2697
|
+
const absolutePath = relativePath ? joinV3WorkspacePath(rootPath, relativePath) : path.resolve(rootPath);
|
|
2664
2698
|
const resolvedRoot = path.resolve(rootPath);
|
|
2665
2699
|
if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
|
|
2666
2700
|
return null;
|
|
@@ -2864,7 +2898,7 @@ menu {
|
|
|
2864
2898
|
if (!relativePath) {
|
|
2865
2899
|
return false;
|
|
2866
2900
|
}
|
|
2867
|
-
const absolutePath =
|
|
2901
|
+
const absolutePath = joinV3WorkspacePath(rootPath, relativePath);
|
|
2868
2902
|
const resolvedRoot = path.resolve(rootPath);
|
|
2869
2903
|
if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
|
|
2870
2904
|
this.logger.warn(`Refusing to write V3 file outside workspace: ${rawPath}`);
|
|
@@ -2891,7 +2925,7 @@ menu {
|
|
|
2891
2925
|
if (!relativePath) {
|
|
2892
2926
|
return false;
|
|
2893
2927
|
}
|
|
2894
|
-
const absolutePath =
|
|
2928
|
+
const absolutePath = joinV3WorkspacePath(rootPath, relativePath);
|
|
2895
2929
|
const resolvedRoot = path.resolve(rootPath);
|
|
2896
2930
|
if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
|
|
2897
2931
|
this.logger.warn(`Refusing to delete V3 file outside workspace: ${rawPath}`);
|
|
@@ -2935,56 +2969,7 @@ menu {
|
|
|
2935
2969
|
}
|
|
2936
2970
|
}
|
|
2937
2971
|
normalizeAgentWorkspaceRelativePath(rawPath, rootPath) {
|
|
2938
|
-
|
|
2939
|
-
if (!input) {
|
|
2940
|
-
return '';
|
|
2941
|
-
}
|
|
2942
|
-
const safeRelative = (candidate) => {
|
|
2943
|
-
const stripped = String(candidate || '').replace(/^\/+/, '');
|
|
2944
|
-
if (!stripped || /^[a-zA-Z]:\//.test(stripped)) {
|
|
2945
|
-
return '';
|
|
2946
|
-
}
|
|
2947
|
-
const normalized = path.posix.normalize(stripped);
|
|
2948
|
-
if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
|
|
2949
|
-
return '';
|
|
2950
|
-
}
|
|
2951
|
-
return normalized;
|
|
2952
|
-
};
|
|
2953
|
-
const internalWorkspacePatterns = [
|
|
2954
|
-
/^\/?var\/www\/\.vigthoria\/v3-temp\/vig-remote-[^/]+\/(.+)$/i,
|
|
2955
|
-
/^\.vigthoria\/v3-temp\/vig-remote-[^/]+\/(.+)$/i,
|
|
2956
|
-
/^\/?tmp\/vig-remote(?:-server)?-[^/]+\/(.+)$/i,
|
|
2957
|
-
/^\/?tmp\/vig-fork-[^/]+\/(.+)$/i,
|
|
2958
|
-
/^\/?home\/user\/(.+)$/i,
|
|
2959
|
-
/^\/?root\/(.+)$/i,
|
|
2960
|
-
];
|
|
2961
|
-
for (const pattern of internalWorkspacePatterns) {
|
|
2962
|
-
const match = input.match(pattern);
|
|
2963
|
-
if (match && match[1]) {
|
|
2964
|
-
return safeRelative(match[1]);
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
const normalizedRoot = String(rootPath || '').trim().replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
2968
|
-
if (normalizedRoot) {
|
|
2969
|
-
const rootNoLeadingSlash = normalizedRoot.replace(/^\//, '');
|
|
2970
|
-
const rootBase = path.posix.basename(normalizedRoot);
|
|
2971
|
-
const prefixes = [
|
|
2972
|
-
`${normalizedRoot}/`,
|
|
2973
|
-
`${rootNoLeadingSlash}/`,
|
|
2974
|
-
`${rootBase}/`,
|
|
2975
|
-
];
|
|
2976
|
-
for (const prefix of prefixes) {
|
|
2977
|
-
if (input.startsWith(prefix)) {
|
|
2978
|
-
return safeRelative(input.slice(prefix.length));
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
const embeddedRoot = `/${rootBase}/`;
|
|
2982
|
-
const embeddedIndex = input.indexOf(embeddedRoot);
|
|
2983
|
-
if (embeddedIndex >= 0) {
|
|
2984
|
-
return safeRelative(input.slice(embeddedIndex + embeddedRoot.length));
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
return safeRelative(input);
|
|
2972
|
+
return normalizeV3WorkspaceRelativePath(rawPath, rootPath);
|
|
2988
2973
|
}
|
|
2989
2974
|
async ensureAgentFrontendPolish(message = '', context = {}) {
|
|
2990
2975
|
const rootPath = this.resolveAgentTargetPath(context);
|
|
@@ -3542,7 +3527,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3542
3527
|
this.activeV3StreamedFiles = streamedFiles;
|
|
3543
3528
|
this.activeV3StreamContext = context;
|
|
3544
3529
|
context.__v3StreamedFiles = streamedFiles;
|
|
3545
|
-
const idleTimeoutMs =
|
|
3530
|
+
const idleTimeoutMs = (() => {
|
|
3531
|
+
const base = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
|
|
3532
|
+
if (isPlannerBuildTask(String(context.agentTaskType || ''), String(context.rawPrompt || ''))) {
|
|
3533
|
+
const raw = process.env.VIGTHORIA_AGENT_PLANNER_IDLE_TIMEOUT_MS || '300000';
|
|
3534
|
+
const extended = Number.parseInt(raw, 10);
|
|
3535
|
+
return Math.max(base, Number.isFinite(extended) && extended > 0 ? extended : 300000);
|
|
3536
|
+
}
|
|
3537
|
+
return base;
|
|
3538
|
+
})();
|
|
3546
3539
|
try {
|
|
3547
3540
|
while (true) {
|
|
3548
3541
|
let chunk;
|
|
@@ -3562,6 +3555,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3562
3555
|
chunk = result;
|
|
3563
3556
|
break;
|
|
3564
3557
|
}
|
|
3558
|
+
if (this.pendingV3ClientToolTasks.size > 0) {
|
|
3559
|
+
continue;
|
|
3560
|
+
}
|
|
3565
3561
|
if (this.hasAgentWorkspaceOutput(context)) {
|
|
3566
3562
|
const stalledError = new Error('V3 agent stream stalled after writing workspace output');
|
|
3567
3563
|
stalledError.name = 'AbortError';
|
|
@@ -3748,7 +3744,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3748
3744
|
&& context.localMachineCapable !== false;
|
|
3749
3745
|
const rescueEligibleSaaS = preferLocalV3
|
|
3750
3746
|
&& /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(message);
|
|
3751
|
-
const timeoutMs =
|
|
3747
|
+
const timeoutMs = rescueEligibleSaaS
|
|
3748
|
+
? Math.min(resolvePlannerAgentTimeoutMs(baseTimeoutMs, String(executionContext.agentTaskType || ''), String(executionContext.rawPrompt || message || '')), 210000)
|
|
3749
|
+
: resolvePlannerAgentTimeoutMs(baseTimeoutMs, String(executionContext.agentTaskType || ''), String(executionContext.rawPrompt || message || ''));
|
|
3752
3750
|
const maxAttempts = preferLocalV3 ? 2 : 1;
|
|
3753
3751
|
let lastErrors = [];
|
|
3754
3752
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vigthoria Balanced 4B — fast agent path classifier (~0.7–2s on local inference :8016).
|
|
3
|
+
* Regex/trivial detection always wins; LLM router refines ambiguous build prompts only.
|
|
4
|
+
*/
|
|
5
|
+
export type AgentRoutePath = 'template-instant' | 'v3-agent' | 'analysis-only';
|
|
6
|
+
export interface BalancedRouterJson {
|
|
7
|
+
path?: string;
|
|
8
|
+
task_kind?: string;
|
|
9
|
+
taskKind?: string;
|
|
10
|
+
fast_path?: string | null;
|
|
11
|
+
fastPath?: string | null;
|
|
12
|
+
confidence?: number;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface BalancedRouterResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
raw?: string;
|
|
18
|
+
parsed?: BalancedRouterJson;
|
|
19
|
+
latencyMs?: number;
|
|
20
|
+
model?: string;
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function parseBalancedRouterJson(raw: string): BalancedRouterJson | null;
|
|
25
|
+
/**
|
|
26
|
+
* Inference bases for Balanced 4B routing only.
|
|
27
|
+
* Local developer machines must opt in (env) — avoids ~1.5s localhost probe on every Windows turn.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getInferenceRouterBaseUrls(selfHostedUrl: string | null): string[];
|
|
30
|
+
export declare function isFastInferenceRouterAvailable(selfHostedUrl: string | null, timeoutMs?: number): Promise<boolean>;
|
|
31
|
+
export declare function routeWithBalanced4b(prompt: string, selfHostedUrl: string | null, options?: {
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
}): Promise<BalancedRouterResult>;
|
|
34
|
+
export declare function shouldUseBalancedRouter(prompt: string, regexTaskKind?: string): boolean;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vigthoria Balanced 4B — fast agent path classifier (~0.7–2s on local inference :8016).
|
|
3
|
+
* Regex/trivial detection always wins; LLM router refines ambiguous build prompts only.
|
|
4
|
+
*/
|
|
5
|
+
const ROUTER_SYSTEM = `You are Vigthoria Agent Router. Reply with ONE JSON object only. No markdown, no HTML, no code fences.
|
|
6
|
+
|
|
7
|
+
Paths:
|
|
8
|
+
- template-instant: single trivial HTML page (hello world, one popup, simple static site) — use Template Service
|
|
9
|
+
- v3-agent: multi-file builds, games, refactors, complex implementation
|
|
10
|
+
- analysis-only: read-only review/audit/explain, no workspace writes
|
|
11
|
+
|
|
12
|
+
task_kind: web-build | game-build | implementation | analysis | debugging
|
|
13
|
+
fast_path: "template-instant" or null
|
|
14
|
+
|
|
15
|
+
Example: {"path":"template-instant","task_kind":"web-build","fast_path":"template-instant","confidence":0.95,"reason":"trivial html popup page"}`;
|
|
16
|
+
const INFERENCE_MODEL_IDS = [
|
|
17
|
+
'vigthoria-balanced-4b',
|
|
18
|
+
'vigthoria-v3-balanced-4b',
|
|
19
|
+
];
|
|
20
|
+
function normalizePath(value) {
|
|
21
|
+
const p = String(value || '').trim().toLowerCase();
|
|
22
|
+
if (p === 'template-instant' || p === 'template' || p === 'html-template') {
|
|
23
|
+
return 'template-instant';
|
|
24
|
+
}
|
|
25
|
+
if (p === 'analysis-only' || p === 'analysis') {
|
|
26
|
+
return 'analysis-only';
|
|
27
|
+
}
|
|
28
|
+
if (p === 'v3-agent' || p === 'v3' || p === 'agent') {
|
|
29
|
+
return 'v3-agent';
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
export function parseBalancedRouterJson(raw) {
|
|
34
|
+
const text = String(raw || '').trim();
|
|
35
|
+
if (!text)
|
|
36
|
+
return null;
|
|
37
|
+
const start = text.indexOf('{');
|
|
38
|
+
if (start < 0)
|
|
39
|
+
return null;
|
|
40
|
+
let depth = 0;
|
|
41
|
+
for (let i = start; i < text.length; i += 1) {
|
|
42
|
+
if (text[i] === '{')
|
|
43
|
+
depth += 1;
|
|
44
|
+
if (text[i] === '}') {
|
|
45
|
+
depth -= 1;
|
|
46
|
+
if (depth === 0) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(text.slice(start, i + 1));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Inference bases for Balanced 4B routing only.
|
|
60
|
+
* Local developer machines must opt in (env) — avoids ~1.5s localhost probe on every Windows turn.
|
|
61
|
+
*/
|
|
62
|
+
export function getInferenceRouterBaseUrls(selfHostedUrl) {
|
|
63
|
+
const urls = [
|
|
64
|
+
process.env.VIGTHORIA_INFERENCE_URL,
|
|
65
|
+
process.env.VIGTHORIA_SELF_HOSTED_MODELS_API_URL,
|
|
66
|
+
selfHostedUrl,
|
|
67
|
+
].filter(Boolean).map((u) => String(u).replace(/\/$/, ''));
|
|
68
|
+
const allowLocalhost = process.env.VIGTHORIA_ENABLE_LOCAL_INFERENCE_ROUTER === '1'
|
|
69
|
+
|| process.env.VIGTHORIA_AGENT_LLM_ROUTER === '1'
|
|
70
|
+
|| process.platform === 'linux';
|
|
71
|
+
if (allowLocalhost) {
|
|
72
|
+
urls.push('http://127.0.0.1:8016');
|
|
73
|
+
}
|
|
74
|
+
return [...new Set(urls)];
|
|
75
|
+
}
|
|
76
|
+
let inferenceHealthCache = null;
|
|
77
|
+
export async function isFastInferenceRouterAvailable(selfHostedUrl, timeoutMs = 400) {
|
|
78
|
+
if (process.env.VIGTHORIA_AGENT_LLM_ROUTER === '0') {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (inferenceHealthCache && now - inferenceHealthCache.checkedAt < 60_000) {
|
|
83
|
+
return inferenceHealthCache.ok;
|
|
84
|
+
}
|
|
85
|
+
for (const base of getInferenceRouterBaseUrls(selfHostedUrl)) {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(`${base}/health`, { signal: controller.signal });
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (response.ok) {
|
|
92
|
+
inferenceHealthCache = { ok: true, checkedAt: now };
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
inferenceHealthCache = { ok: false, checkedAt: now };
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
export async function routeWithBalanced4b(prompt, selfHostedUrl, options = {}) {
|
|
104
|
+
const timeoutMs = options.timeoutMs ?? Number.parseInt(process.env.VIGTHORIA_AGENT_ROUTER_TIMEOUT_MS || '8000', 10);
|
|
105
|
+
const bases = getInferenceRouterBaseUrls(selfHostedUrl);
|
|
106
|
+
let lastError = 'Inference router unreachable';
|
|
107
|
+
for (const base of bases) {
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
110
|
+
const started = Date.now();
|
|
111
|
+
for (const model of INFERENCE_MODEL_IDS) {
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(`${base}/v1/chat/completions`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
model,
|
|
118
|
+
messages: [
|
|
119
|
+
{ role: 'system', content: ROUTER_SYSTEM },
|
|
120
|
+
{ role: 'user', content: String(prompt || '').slice(0, 1200) },
|
|
121
|
+
],
|
|
122
|
+
max_tokens: 96,
|
|
123
|
+
temperature: 0,
|
|
124
|
+
stream: false,
|
|
125
|
+
}),
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
const payload = await response.json().catch(() => null);
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
lastError = `HTTP ${response.status} from ${base}`;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const raw = payload?.choices?.[0]?.message?.content || '';
|
|
135
|
+
const parsed = parseBalancedRouterJson(raw);
|
|
136
|
+
if (!parsed) {
|
|
137
|
+
lastError = 'Router returned non-JSON';
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
raw,
|
|
143
|
+
parsed,
|
|
144
|
+
latencyMs: Date.now() - started,
|
|
145
|
+
model,
|
|
146
|
+
endpoint: base,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
return { ok: false, error: lastError };
|
|
156
|
+
}
|
|
157
|
+
export function shouldUseBalancedRouter(prompt, regexTaskKind = '') {
|
|
158
|
+
if (process.env.VIGTHORIA_AGENT_LLM_ROUTER === '0') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const kind = String(regexTaskKind || '').toLowerCase();
|
|
162
|
+
// Regex already resolved clear build families — LLM must not override (e.g. landing → implementation).
|
|
163
|
+
if (kind === 'web-build' || kind === 'game-build') {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const text = String(prompt || '').trim();
|
|
167
|
+
if (text.length > 700 || text.length < 12) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt intent detection for CLI routing, V3 hints, and rescue gating.
|
|
3
|
+
* Keep aligned with V3-Code-Agent/agent.py _classify_request heuristics.
|
|
4
|
+
*/
|
|
5
|
+
/** Strip CLI-appended platform / mode shaping so it cannot flip quality profiles. */
|
|
6
|
+
export declare function stripExecutionShaping(prompt: string): string;
|
|
7
|
+
export declare function hasBuildVerb(prompt: string): boolean;
|
|
8
|
+
export declare function hasArtifactNoun(prompt: string): boolean;
|
|
9
|
+
export declare function hasGameIntent(prompt: string): boolean;
|
|
10
|
+
export declare function isContinueOrRetryPrompt(prompt: string): boolean;
|
|
11
|
+
export declare function hasWebPageIntent(prompt: string): boolean;
|
|
12
|
+
export declare function isTrivialHtmlPageRequest(prompt: string): boolean;
|
|
13
|
+
export declare function hasCloneBuildIntent(prompt: string): boolean;
|
|
14
|
+
export declare function hasExplicitWriteIntent(prompt: string): boolean;
|
|
15
|
+
export declare function taskRequiresWorkspaceChanges(prompt: string): boolean;
|
|
16
|
+
export declare function hasAnalyzeAndBuildIntent(prompt: string): boolean;
|
|
17
|
+
export declare function inferAgentTaskType(prompt: string): string;
|
|
18
|
+
export declare function shouldSkipAnalysisRescue(liveOutcome: {
|
|
19
|
+
tasksTotal?: number;
|
|
20
|
+
changedFileCount?: number;
|
|
21
|
+
requiresWorkspaceChanges?: boolean;
|
|
22
|
+
}): boolean;
|
|
23
|
+
export declare function isPlannerBuildTask(agentTaskType: string, prompt: string): boolean;
|
|
24
|
+
export declare function resolvePlannerAgentTimeoutMs(baseTimeoutMs: number, agentTaskType: string, prompt: string): number;
|
|
25
|
+
/** V3 workflow mode: suppress write path when the user did not ask for file changes. */
|
|
26
|
+
export declare function resolveWorkflowType(agentTaskType: string, prompt: string): 'analysis_only' | 'full';
|
|
27
|
+
export declare function buildExecutionHints(agentTaskType: string, prompt: string, options?: {
|
|
28
|
+
fastPath?: string;
|
|
29
|
+
}): {
|
|
30
|
+
task_kind: string;
|
|
31
|
+
requires_file_changes: boolean | undefined;
|
|
32
|
+
classify_from: string;
|
|
33
|
+
workflow_type: 'analysis_only' | 'full';
|
|
34
|
+
fast_path?: string;
|
|
35
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt intent detection for CLI routing, V3 hints, and rescue gating.
|
|
3
|
+
* Keep aligned with V3-Code-Agent/agent.py _classify_request heuristics.
|
|
4
|
+
*/
|
|
5
|
+
const CLI_SHAPING_BLOCK = /(?:^|\n\n)Platform:\s*(?:Windows|macOS|Linux)\.[\s\S]*?(?=\n\n[A-Z][^\n]{0,40}:|\s*$)/i;
|
|
6
|
+
const READONLY_SHAPING_BLOCK = /(?:^|\n\n)(?:Read-only analysis mode is active\.|Diagnostic mode is active\.)[\s\S]*?(?=\n\n[A-Z][^\n]{0,40}:|\s*$)/i;
|
|
7
|
+
const BUILD_VERBS = /\b(build|create|make|implement|complete|fix|repair|edit|modify|write|generate|add|finish|scaffold|develop|update|change|refactor|let's|let us|we need|we should|erstelle|erstellen|schreib|schreibe|bearbeite)\b/i;
|
|
8
|
+
const ARTIFACT_NOUNS = /\b(file|files|project|game|spiel|app|website|html5|frontend|component|feature|code|workspace|repo|clone|replica|remake|page|site|canvas|sprite|level|levels|enemy|enemies|character|npc|world|scene|script|module|api|database|config|landing|dashboard|platformer|arcade|playable|collectible|scoreboard|leaderboard|pitfall|pacman|rogue|html|css|javascript|typescript)\b/i;
|
|
9
|
+
/** Canvas/playable games only — bare "html5" or "website" must NOT match. */
|
|
10
|
+
const GAME_INTENT = /\b(game|spiel|playable|html5\s+(?:game|canvas)|html5\s+game|canvas\s+game|game\s+canvas|arcade|platformer|side[- ]?scroller|pitfall|pac[- ]?man|tetris|snake|breakout|pong|roguelike|metroidvania|tower\s+defense|clone\s+of\s+(?:a\s+)?(?:game|pitfall|pac|mario|zelda|arcade)|gameplay|collectible|boss\s+fight|level\s+design|sprite\s+sheet|wild\s+(?:boar|pig|forest)|forest\s+pig)\b/i;
|
|
11
|
+
const WEB_PAGE_INTENT = /\b(website|web\s*site|webpage|web\s*page|landing\s+page|home\s*page|index\.html|html\s+page|static\s+page|single\s+page|popup|alert|modal|hello\s+world)\b/i;
|
|
12
|
+
const CLONE_BUILD = /\bclone\s+of\b|\b(build|create|make)\s+(?:a|an|the|us|me)?\s*(?:clone|replica|remake|port)\b/i;
|
|
13
|
+
const READ_ONLY_INTENT = /\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;
|
|
14
|
+
const EXPLICIT_WRITE_VERBS = /\b(implement|write|edit|modify|update|fix|repair|create|build|generate|scaffold|deploy|refactor|develop|integrate)\b/i;
|
|
15
|
+
const TRIVIAL_HTML_PAGE = /\b(hello\s*world|simple\s+(?:html|web|page|site)|html5\s+website|html\s+5\s+website|popup|alert\s*\(|show\s+a\s+popup|one\s+page|single\s+html)\b/i;
|
|
16
|
+
/** Strip CLI-appended platform / mode shaping so it cannot flip quality profiles. */
|
|
17
|
+
export function stripExecutionShaping(prompt) {
|
|
18
|
+
return String(prompt || '')
|
|
19
|
+
.replace(CLI_SHAPING_BLOCK, '')
|
|
20
|
+
.replace(READONLY_SHAPING_BLOCK, '')
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
23
|
+
export function hasBuildVerb(prompt) {
|
|
24
|
+
return BUILD_VERBS.test(stripExecutionShaping(prompt));
|
|
25
|
+
}
|
|
26
|
+
export function hasArtifactNoun(prompt) {
|
|
27
|
+
return ARTIFACT_NOUNS.test(stripExecutionShaping(prompt));
|
|
28
|
+
}
|
|
29
|
+
/** Game clones require a game target — generic "clone of X" must not imply game-build. */
|
|
30
|
+
const GAME_CLONE_TARGET = /\bclone\s+of\s+(?:a\s+)?(?:the\s+)?(?:game|pitfall|pac[- ]?man|mario|zelda|tetris|snake|arcade|spiel)\b/i;
|
|
31
|
+
export function hasGameIntent(prompt) {
|
|
32
|
+
const text = stripExecutionShaping(prompt);
|
|
33
|
+
if (GAME_INTENT.test(text)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (GAME_CLONE_TARGET.test(text)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (CLONE_BUILD.test(text) && /\b(game|spiel|pitfall|pacman|arcade|platformer|playable|canvas)\b/i.test(text)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
export function isContinueOrRetryPrompt(prompt) {
|
|
45
|
+
const text = stripExecutionShaping(prompt).toLowerCase();
|
|
46
|
+
return /\b(continue\s+(?:the\s+)?(?:previous|last|current)\s+(?:agent\s+)?run|\/retry|\/continue|resume\s+(?:the\s+)?(?:failed|unfinished)|planner\s+produced\s+\d+\s+execution\s+tasks)\b/i.test(text);
|
|
47
|
+
}
|
|
48
|
+
export function hasWebPageIntent(prompt) {
|
|
49
|
+
const text = stripExecutionShaping(prompt);
|
|
50
|
+
return WEB_PAGE_INTENT.test(text) || /\bhtml5\s+website\b/i.test(text);
|
|
51
|
+
}
|
|
52
|
+
export function isTrivialHtmlPageRequest(prompt) {
|
|
53
|
+
const text = stripExecutionShaping(prompt);
|
|
54
|
+
if (isContinueOrRetryPrompt(prompt)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!hasBuildVerb(prompt) && !/\b(write|create|make|show)\b/i.test(text)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (hasGameIntent(prompt)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (text.length > 420) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (/\b(multi[- ]?page|full\s+saas|dashboard|e[\s-]?commerce|auth|database|backend|api|microservice)\b/i.test(text)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return TRIVIAL_HTML_PAGE.test(text)
|
|
70
|
+
|| (hasWebPageIntent(prompt) && /\b(html5|html5\s+website|website|popup|hello\s*world)\b/i.test(text));
|
|
71
|
+
}
|
|
72
|
+
export function hasCloneBuildIntent(prompt) {
|
|
73
|
+
return CLONE_BUILD.test(stripExecutionShaping(prompt)) && hasBuildVerb(prompt);
|
|
74
|
+
}
|
|
75
|
+
export function hasExplicitWriteIntent(prompt) {
|
|
76
|
+
const text = stripExecutionShaping(prompt);
|
|
77
|
+
return (BUILD_VERBS.test(text) && ARTIFACT_NOUNS.test(text))
|
|
78
|
+
|| hasCloneBuildIntent(prompt)
|
|
79
|
+
|| (BUILD_VERBS.test(text) && hasGameIntent(prompt));
|
|
80
|
+
}
|
|
81
|
+
export function taskRequiresWorkspaceChanges(prompt) {
|
|
82
|
+
const text = String(prompt || '').trim();
|
|
83
|
+
const shaped = stripExecutionShaping(text);
|
|
84
|
+
const readOnlyIntent = READ_ONLY_INTENT.test(shaped);
|
|
85
|
+
const explicitWriteIntent = hasExplicitWriteIntent(text);
|
|
86
|
+
if (readOnlyIntent && !explicitWriteIntent) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return explicitWriteIntent;
|
|
90
|
+
}
|
|
91
|
+
export function hasAnalyzeAndBuildIntent(prompt) {
|
|
92
|
+
const text = stripExecutionShaping(prompt);
|
|
93
|
+
return /\b(analy[sz]e|analyse)\b/i.test(text)
|
|
94
|
+
&& /\b(complete|finish|fix|implement|build|repair|missing\s+elements?)\b/i.test(text)
|
|
95
|
+
&& hasExplicitWriteIntent(prompt);
|
|
96
|
+
}
|
|
97
|
+
export function inferAgentTaskType(prompt) {
|
|
98
|
+
const text = stripExecutionShaping(prompt);
|
|
99
|
+
if (hasAnalyzeAndBuildIntent(prompt)) {
|
|
100
|
+
if (hasGameIntent(prompt)) {
|
|
101
|
+
return 'game-build';
|
|
102
|
+
}
|
|
103
|
+
if (hasWebPageIntent(prompt)) {
|
|
104
|
+
return 'web-build';
|
|
105
|
+
}
|
|
106
|
+
return 'implementation';
|
|
107
|
+
}
|
|
108
|
+
if (/\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(text)
|
|
109
|
+
&& !hasExplicitWriteIntent(prompt)) {
|
|
110
|
+
return 'debugging';
|
|
111
|
+
}
|
|
112
|
+
if (/\b(analy[sz]e|analyse|audit|review|inspect|identify\s+gaps?|gap\s+analysis|production\s+blockers?|read[\s-]?only)\b/i.test(text)
|
|
113
|
+
&& !hasExplicitWriteIntent(prompt)) {
|
|
114
|
+
return 'analysis';
|
|
115
|
+
}
|
|
116
|
+
if (hasBuildVerb(prompt) && !/\b(analy[sz]e|analyse|audit|review|inspect)\b/i.test(text)) {
|
|
117
|
+
if (hasGameIntent(prompt)) {
|
|
118
|
+
return 'game-build';
|
|
119
|
+
}
|
|
120
|
+
if (hasWebPageIntent(prompt)) {
|
|
121
|
+
return 'web-build';
|
|
122
|
+
}
|
|
123
|
+
return 'implementation';
|
|
124
|
+
}
|
|
125
|
+
return 'analysis';
|
|
126
|
+
}
|
|
127
|
+
export function shouldSkipAnalysisRescue(liveOutcome) {
|
|
128
|
+
if (liveOutcome.requiresWorkspaceChanges) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if ((liveOutcome.tasksTotal || 0) > 0) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if ((liveOutcome.changedFileCount || 0) > 0) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
export function isPlannerBuildTask(agentTaskType, prompt) {
|
|
140
|
+
const kind = String(agentTaskType || '').toLowerCase();
|
|
141
|
+
if (isTrivialHtmlPageRequest(prompt)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (/^(game-build|web-build|implementation|build|repair|refactor-build)$/.test(kind)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return hasExplicitWriteIntent(prompt) && !READ_ONLY_INTENT.test(stripExecutionShaping(prompt));
|
|
148
|
+
}
|
|
149
|
+
export function resolvePlannerAgentTimeoutMs(baseTimeoutMs, agentTaskType, prompt) {
|
|
150
|
+
if (!isPlannerBuildTask(agentTaskType, prompt)) {
|
|
151
|
+
return baseTimeoutMs;
|
|
152
|
+
}
|
|
153
|
+
const raw = process.env.VIGTHORIA_AGENT_PLANNER_TIMEOUT_MS || process.env.V3_AGENT_PLANNER_TIMEOUT_MS || '900000';
|
|
154
|
+
const extended = Number.parseInt(raw, 10);
|
|
155
|
+
const floor = Number.isFinite(extended) && extended > 0 ? extended : 900000;
|
|
156
|
+
return Math.max(baseTimeoutMs, floor);
|
|
157
|
+
}
|
|
158
|
+
/** V3 workflow mode: suppress write path when the user did not ask for file changes. */
|
|
159
|
+
export function resolveWorkflowType(agentTaskType, prompt) {
|
|
160
|
+
if (hasAnalyzeAndBuildIntent(prompt)) {
|
|
161
|
+
return 'full';
|
|
162
|
+
}
|
|
163
|
+
if (!taskRequiresWorkspaceChanges(prompt)) {
|
|
164
|
+
return 'analysis_only';
|
|
165
|
+
}
|
|
166
|
+
const kind = String(agentTaskType || '').toLowerCase();
|
|
167
|
+
if (kind === 'analysis' || kind === 'debugging' || kind === 'verification') {
|
|
168
|
+
return 'analysis_only';
|
|
169
|
+
}
|
|
170
|
+
return 'full';
|
|
171
|
+
}
|
|
172
|
+
export function buildExecutionHints(agentTaskType, prompt, options = {}) {
|
|
173
|
+
const workflowType = resolveWorkflowType(agentTaskType, prompt);
|
|
174
|
+
const kind = String(agentTaskType || 'general').toLowerCase();
|
|
175
|
+
const hints = {
|
|
176
|
+
task_kind: kind,
|
|
177
|
+
requires_file_changes: workflowType === 'analysis_only' ? false : undefined,
|
|
178
|
+
classify_from: String(prompt || ''),
|
|
179
|
+
workflow_type: workflowType,
|
|
180
|
+
};
|
|
181
|
+
if (options.fastPath) {
|
|
182
|
+
hints.fast_path = options.fastPath;
|
|
183
|
+
}
|
|
184
|
+
else if (isTrivialHtmlPageRequest(prompt)) {
|
|
185
|
+
hints.fast_path = 'template-instant';
|
|
186
|
+
}
|
|
187
|
+
return hints;
|
|
188
|
+
}
|