vigthoria-cli 1.11.29 → 1.11.36
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 +5 -0
- package/dist/commands/chat.js +117 -32
- package/dist/utils/agentRunOutcome.d.ts +4 -0
- package/dist/utils/agentRunOutcome.js +72 -3
- package/dist/utils/api.d.ts +8 -1
- package/dist/utils/api.js +244 -48
- package/dist/utils/contextBudget.d.ts +29 -0
- package/dist/utils/contextBudget.js +55 -0
- package/dist/utils/goaEvents.d.ts +19 -0
- package/dist/utils/goaEvents.js +63 -0
- package/dist/utils/v3-stream-events.d.ts +5 -0
- package/dist/utils/v3-stream-events.js +41 -0
- package/dist/utils/v3-workspace-path.js +56 -1
- package/package.json +7 -3
package/dist/commands/chat.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export declare class ChatCommand {
|
|
|
54
54
|
private v3SeenToolResults;
|
|
55
55
|
private v3StartSeen;
|
|
56
56
|
private lastAgentRoute;
|
|
57
|
+
private pendingGoaContext;
|
|
57
58
|
private lastAgentRunOutcome;
|
|
58
59
|
private isJwtExpirationError;
|
|
59
60
|
private isNetworkError;
|
|
@@ -116,6 +117,8 @@ export declare class ChatCommand {
|
|
|
116
117
|
private v3IterationCount;
|
|
117
118
|
private v3ToolCallCount;
|
|
118
119
|
private v3LastActivity;
|
|
120
|
+
private v3IdleWatchInterval;
|
|
121
|
+
private v3IdleNoticeShown;
|
|
119
122
|
private v3StreamingStarted;
|
|
120
123
|
/**
|
|
121
124
|
* Strip server-internal path prefixes from tool output strings.
|
|
@@ -123,6 +126,8 @@ export declare class ChatCommand {
|
|
|
123
126
|
*/
|
|
124
127
|
private sanitizeServerPath;
|
|
125
128
|
private stripHiddenThoughtBlocks;
|
|
129
|
+
private startV3IdleWatch;
|
|
130
|
+
private stopV3IdleWatch;
|
|
126
131
|
private describeV3AgentTool;
|
|
127
132
|
private terminalColumns;
|
|
128
133
|
private fitTerminalText;
|
package/dist/commands/chat.js
CHANGED
|
@@ -17,9 +17,11 @@ import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session
|
|
|
17
17
|
import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
|
|
18
18
|
import { looksLikeMarkdownReport, renderMarkdownToTerminal, summarizeMarkdownReport, } from '../utils/terminalMarkdown.js';
|
|
19
19
|
import { emitDeckEvent, isDeckModeEnabled } from '../utils/deckEvents.js';
|
|
20
|
+
import { formatGoaSystemGrounding, splitGoaContextFromInput, } from '../utils/goaEvents.js';
|
|
20
21
|
import { inferAgentTaskType as sharedInferAgentTaskType, inferAgentTaskTypeWithContext, isTrivialHtmlPageRequest, resolvePlannerAgentTimeoutMs, resolveWorkflowType, shouldSkipAnalysisRescue, taskRequiresWorkspaceChanges as promptRequiresWorkspaceChanges, taskRequiresWorkspaceChangesWithContext, buildExecutionHints, isAgentContinuePrompt, isAgentRetryPrompt, isBuiltContinuePrompt, isBuiltRetryPrompt, isBuiltWriteConfirmationPrompt, isConfirmationFollowUp, isWritePermissionGrant, } from '../utils/requestIntent.js';
|
|
21
22
|
import { resolveAgentRoute } from '../utils/agentRoute.js';
|
|
22
23
|
import { runTemplateInstantPath } from '../utils/templateInstantPath.js';
|
|
24
|
+
import { isV3StreamKeepaliveEvent } from '../utils/v3-stream-events.js';
|
|
23
25
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
24
26
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
25
27
|
if (!rawValue) {
|
|
@@ -76,6 +78,7 @@ export class ChatCommand {
|
|
|
76
78
|
v3SeenToolResults = new Set();
|
|
77
79
|
v3StartSeen = false;
|
|
78
80
|
lastAgentRoute = null;
|
|
81
|
+
pendingGoaContext = null;
|
|
79
82
|
// Last completed Agent run — used by /retry, /continue, and the final summary block.
|
|
80
83
|
lastAgentRunOutcome = null;
|
|
81
84
|
isJwtExpirationError(error) {
|
|
@@ -781,6 +784,8 @@ export class ChatCommand {
|
|
|
781
784
|
v3IterationCount = 0;
|
|
782
785
|
v3ToolCallCount = 0;
|
|
783
786
|
v3LastActivity = Date.now();
|
|
787
|
+
v3IdleWatchInterval = null;
|
|
788
|
+
v3IdleNoticeShown = false;
|
|
784
789
|
v3StreamingStarted = false;
|
|
785
790
|
/**
|
|
786
791
|
* Strip server-internal path prefixes from tool output strings.
|
|
@@ -796,10 +801,41 @@ export class ChatCommand {
|
|
|
796
801
|
return text;
|
|
797
802
|
return text
|
|
798
803
|
.replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
|
|
799
|
-
.replace(/<think>[\s\S]*?<\/
|
|
804
|
+
.replace(/<think>[\s\S]*?<\/redacted_thinking>/gi, '')
|
|
805
|
+
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
|
|
806
|
+
.replace(/<\/redacted_thinking>/gi, '')
|
|
807
|
+
.replace(/<\/thinking>/gi, '')
|
|
800
808
|
.replace(/<\|(?:mask_start|mask_end)\|>/g, '')
|
|
801
809
|
.trim();
|
|
802
810
|
}
|
|
811
|
+
startV3IdleWatch(spinner) {
|
|
812
|
+
this.stopV3IdleWatch();
|
|
813
|
+
this.v3IdleNoticeShown = false;
|
|
814
|
+
if (this.jsonOutput || !spinner) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
this.v3IdleWatchInterval = setInterval(() => {
|
|
818
|
+
const idleMs = Date.now() - this.v3LastActivity;
|
|
819
|
+
if (idleMs < 15_000 || this.v3IdleNoticeShown) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
this.v3IdleNoticeShown = true;
|
|
823
|
+
if (spinner.isSpinning) {
|
|
824
|
+
spinner.stop();
|
|
825
|
+
}
|
|
826
|
+
const seconds = Math.round(idleMs / 1000);
|
|
827
|
+
process.stderr.write(chalk.yellow(` [Wait] Model still working (${seconds}s) — large context or server retry may take up to 90s...\n`));
|
|
828
|
+
spinner.start();
|
|
829
|
+
spinner.text = 'Waiting for model response...';
|
|
830
|
+
}, 5_000);
|
|
831
|
+
}
|
|
832
|
+
stopV3IdleWatch() {
|
|
833
|
+
if (this.v3IdleWatchInterval) {
|
|
834
|
+
clearInterval(this.v3IdleWatchInterval);
|
|
835
|
+
this.v3IdleWatchInterval = null;
|
|
836
|
+
}
|
|
837
|
+
this.v3IdleNoticeShown = false;
|
|
838
|
+
}
|
|
803
839
|
describeV3AgentTool(toolName) {
|
|
804
840
|
const normalized = String(toolName || '').toLowerCase();
|
|
805
841
|
if (/read|grep|search|list|find|glob/.test(normalized)) {
|
|
@@ -934,6 +970,7 @@ export class ChatCommand {
|
|
|
934
970
|
return '';
|
|
935
971
|
output = output.replace(/<\/think>/gi, '</thinking>');
|
|
936
972
|
output = output.replace(/<think>/gi, '<thinking>');
|
|
973
|
+
output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
|
|
937
974
|
if (this.v3SuppressThinkingStream) {
|
|
938
975
|
const closeIdx = output.search(/<\/thinking>/i);
|
|
939
976
|
if (closeIdx < 0) {
|
|
@@ -979,6 +1016,9 @@ export class ChatCommand {
|
|
|
979
1016
|
return;
|
|
980
1017
|
}
|
|
981
1018
|
this.v3LastActivity = Date.now();
|
|
1019
|
+
if (isV3StreamKeepaliveEvent(event)) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
982
1022
|
if (event.type === 'tool_call') {
|
|
983
1023
|
const eventKey = this.toolEventKey(event);
|
|
984
1024
|
if (this.v3SeenToolCalls.has(eventKey)) {
|
|
@@ -1091,6 +1131,13 @@ export class ChatCommand {
|
|
|
1091
1131
|
: (event.content || '');
|
|
1092
1132
|
if (text) {
|
|
1093
1133
|
this.v3LastActivity = Date.now();
|
|
1134
|
+
if (event.type === 'message' && /^\[Context\]/i.test(String(text).trim())) {
|
|
1135
|
+
if (spinner.isSpinning)
|
|
1136
|
+
spinner.stop();
|
|
1137
|
+
process.stderr.write(chalk.cyan(` ${this.fitTerminalText(String(text).trim(), 8)}\n`));
|
|
1138
|
+
spinner.text = 'Analyzing...';
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1094
1141
|
this.writeV3StreamText(spinner, text);
|
|
1095
1142
|
}
|
|
1096
1143
|
else {
|
|
@@ -1138,11 +1185,6 @@ export class ChatCommand {
|
|
|
1138
1185
|
const quality = this.sanitizeServerPath(plan.quality_profile || '');
|
|
1139
1186
|
const status = typeof plan.status === 'string' ? plan.status : '';
|
|
1140
1187
|
const summary = typeof plan.summary === 'string' ? this.sanitizeServerPath(plan.summary) : '';
|
|
1141
|
-
if (status === 'planning') {
|
|
1142
|
-
const elapsed = Number.isFinite(Number(plan.elapsed_seconds)) ? Number(plan.elapsed_seconds) : 0;
|
|
1143
|
-
spinner.text = elapsed > 0 ? `Planning... (${elapsed}s elapsed)` : 'Planning...';
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
1188
|
if (spinner.isSpinning)
|
|
1147
1189
|
spinner.stop();
|
|
1148
1190
|
process.stdout.write(chalk.cyan(` [Plan] `) + this.fitTerminalText(`Task: ${planKind || 'analyzing'}${quality ? ` (${quality})` : ''}`, 9) + '\n');
|
|
@@ -1721,6 +1763,10 @@ export class ChatCommand {
|
|
|
1721
1763
|
return false;
|
|
1722
1764
|
}
|
|
1723
1765
|
async handleDirectPrompt(prompt) {
|
|
1766
|
+
const { goaContext, userPrompt } = splitGoaContextFromInput(prompt);
|
|
1767
|
+
if (goaContext)
|
|
1768
|
+
this.pendingGoaContext = goaContext;
|
|
1769
|
+
const effectivePrompt = userPrompt || prompt;
|
|
1724
1770
|
// Suppress all setup banners in direct-prompt mode so only the final
|
|
1725
1771
|
// answer reaches stdout. Interactive (REPL) mode still shows them.
|
|
1726
1772
|
if (!this.jsonOutput) {
|
|
@@ -1733,18 +1779,18 @@ export class ChatCommand {
|
|
|
1733
1779
|
console.log();
|
|
1734
1780
|
}
|
|
1735
1781
|
if (this.workflowTarget) {
|
|
1736
|
-
await this.runWorkflowTargetPrompt(
|
|
1782
|
+
await this.runWorkflowTargetPrompt(effectivePrompt);
|
|
1737
1783
|
return;
|
|
1738
1784
|
}
|
|
1739
1785
|
// Smart routing: for agent mode, determine if prompt needs tool access
|
|
1740
1786
|
if (this.agentMode) {
|
|
1741
|
-
if (this.isSimpleDirectPrompt(
|
|
1787
|
+
if (this.isSimpleDirectPrompt(effectivePrompt)) {
|
|
1742
1788
|
// Simple prompt: downgrade to plain chat model, no agent system prompt
|
|
1743
1789
|
this.currentModel = this.getDefaultChatModel();
|
|
1744
|
-
await this.runSimplePrompt(
|
|
1790
|
+
await this.runSimplePrompt(effectivePrompt);
|
|
1745
1791
|
}
|
|
1746
1792
|
else {
|
|
1747
|
-
await this.runAgentTurn(
|
|
1793
|
+
await this.runAgentTurn(effectivePrompt);
|
|
1748
1794
|
}
|
|
1749
1795
|
return;
|
|
1750
1796
|
}
|
|
@@ -1753,10 +1799,10 @@ export class ChatCommand {
|
|
|
1753
1799
|
this.logger.error(this.operatorAccessMessage());
|
|
1754
1800
|
return;
|
|
1755
1801
|
}
|
|
1756
|
-
await this.runOperatorTurn(
|
|
1802
|
+
await this.runOperatorTurn(effectivePrompt);
|
|
1757
1803
|
return;
|
|
1758
1804
|
}
|
|
1759
|
-
await this.runSimplePrompt(
|
|
1805
|
+
await this.runSimplePrompt(effectivePrompt);
|
|
1760
1806
|
}
|
|
1761
1807
|
formatWorkflowTargetResult(result) {
|
|
1762
1808
|
if (typeof result === 'string') {
|
|
@@ -2187,7 +2233,19 @@ export class ChatCommand {
|
|
|
2187
2233
|
if (!this.tools) {
|
|
2188
2234
|
throw new Error('Agent tools are not initialized.');
|
|
2189
2235
|
}
|
|
2190
|
-
const
|
|
2236
|
+
const { goaContext, userPrompt } = splitGoaContextFromInput(prompt);
|
|
2237
|
+
if (goaContext) {
|
|
2238
|
+
this.pendingGoaContext = goaContext;
|
|
2239
|
+
}
|
|
2240
|
+
const effectivePrompt = userPrompt || prompt;
|
|
2241
|
+
if (this.pendingGoaContext) {
|
|
2242
|
+
this.messages.push({
|
|
2243
|
+
role: 'system',
|
|
2244
|
+
content: formatGoaSystemGrounding(this.pendingGoaContext),
|
|
2245
|
+
});
|
|
2246
|
+
this.pendingGoaContext = null;
|
|
2247
|
+
}
|
|
2248
|
+
const resolvedPrompt = this.resolveAgentTurnPrompt(effectivePrompt);
|
|
2191
2249
|
this.lastAgentRoute = await resolveAgentRoute(this.api, resolvedPrompt);
|
|
2192
2250
|
if (!this.jsonOutput) {
|
|
2193
2251
|
const r = this.lastAgentRoute;
|
|
@@ -2942,6 +3000,7 @@ export class ChatCommand {
|
|
|
2942
3000
|
spinner: 'clock',
|
|
2943
3001
|
isSilent: true,
|
|
2944
3002
|
}).start();
|
|
3003
|
+
this.startV3IdleWatch(spinner);
|
|
2945
3004
|
// Live run telemetry, used both for the final summary block and for /retry, /continue.
|
|
2946
3005
|
const liveOutcome = {
|
|
2947
3006
|
...createLiveOutcome(),
|
|
@@ -3003,6 +3062,21 @@ export class ChatCommand {
|
|
|
3003
3062
|
fastPath: this.lastAgentRoute?.fastPath,
|
|
3004
3063
|
priorPrompt: priorPrompt || null,
|
|
3005
3064
|
});
|
|
3065
|
+
const priorOutcome = this.lastAgentRunOutcome;
|
|
3066
|
+
const resumeTaskIds = priorOutcome
|
|
3067
|
+
? [...new Set([...(priorOutcome.failedTaskIds || []), ...(priorOutcome.unfinishedTaskIds || [])])]
|
|
3068
|
+
: [];
|
|
3069
|
+
const shouldResumePlan = isAgentContinuePrompt(contextualPrompt)
|
|
3070
|
+
|| isAgentRetryPrompt(contextualPrompt)
|
|
3071
|
+
|| isBuiltContinuePrompt(contextualPrompt)
|
|
3072
|
+
|| isBuiltRetryPrompt(contextualPrompt);
|
|
3073
|
+
const agentExecutionHints = shouldResumePlan
|
|
3074
|
+
? {
|
|
3075
|
+
...executionHints,
|
|
3076
|
+
resume_existing_plan: true,
|
|
3077
|
+
...(resumeTaskIds.length > 0 ? { remaining_task_ids: resumeTaskIds } : {}),
|
|
3078
|
+
}
|
|
3079
|
+
: executionHints;
|
|
3006
3080
|
const workspaceContext = {
|
|
3007
3081
|
workspacePath: workspacePath,
|
|
3008
3082
|
projectPath: workspacePath,
|
|
@@ -3039,7 +3113,7 @@ export class ChatCommand {
|
|
|
3039
3113
|
...workspaceContext,
|
|
3040
3114
|
agentTaskType,
|
|
3041
3115
|
workflowType,
|
|
3042
|
-
executionHints,
|
|
3116
|
+
executionHints: agentExecutionHints,
|
|
3043
3117
|
fastPath: this.lastAgentRoute?.fastPath,
|
|
3044
3118
|
agentRoute: this.lastAgentRoute,
|
|
3045
3119
|
executionSurface: 'cli',
|
|
@@ -3057,20 +3131,15 @@ export class ChatCommand {
|
|
|
3057
3131
|
history: this.getMessagesForModel(),
|
|
3058
3132
|
...runtimeContext,
|
|
3059
3133
|
onStreamEvent: (event) => {
|
|
3134
|
+
if (isV3StreamKeepaliveEvent(event)) {
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3060
3137
|
if (event.type === 'plan') {
|
|
3061
|
-
|
|
3062
|
-
const
|
|
3063
|
-
if (
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
}
|
|
3067
|
-
else {
|
|
3068
|
-
taskDisplay.complete(0);
|
|
3069
|
-
const tasks = plan?.tasks;
|
|
3070
|
-
if (Array.isArray(tasks) && tasks.length > 0) {
|
|
3071
|
-
taskDisplay.start(1);
|
|
3072
|
-
liveOutcome.tasksTotal = tasks.length;
|
|
3073
|
-
}
|
|
3138
|
+
taskDisplay.complete(0);
|
|
3139
|
+
const tasks = event?.plan?.tasks;
|
|
3140
|
+
if (Array.isArray(tasks) && tasks.length > 0) {
|
|
3141
|
+
taskDisplay.start(1);
|
|
3142
|
+
liveOutcome.tasksTotal = tasks.length;
|
|
3074
3143
|
}
|
|
3075
3144
|
}
|
|
3076
3145
|
else if (event.type === 'executor_start') {
|
|
@@ -3113,10 +3182,19 @@ export class ChatCommand {
|
|
|
3113
3182
|
}
|
|
3114
3183
|
else if (event.type === 'error') {
|
|
3115
3184
|
const msg = typeof event.message === 'string' ? event.message : '';
|
|
3116
|
-
|
|
3185
|
+
parsePlannerSummary(msg);
|
|
3186
|
+
const plannerSummary = /Planner-Executor completed/i.test(msg);
|
|
3187
|
+
const allTasksSucceeded = liveOutcome.tasksTotal > 0
|
|
3188
|
+
&& liveOutcome.tasksSucceeded >= liveOutcome.tasksTotal
|
|
3189
|
+
&& liveOutcome.failedTaskIds.size === 0;
|
|
3190
|
+
if (plannerSummary) {
|
|
3191
|
+
if (!allTasksSucceeded && !liveOutcome.plannerError) {
|
|
3192
|
+
liveOutcome.plannerError = msg;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
else if (/plan|planner|dependency graph/i.test(msg)) {
|
|
3117
3196
|
if (!liveOutcome.plannerError)
|
|
3118
3197
|
liveOutcome.plannerError = msg;
|
|
3119
|
-
parsePlannerSummary(msg);
|
|
3120
3198
|
}
|
|
3121
3199
|
else if (/executor|task failed|iteration/i.test(msg)) {
|
|
3122
3200
|
if (!liveOutcome.executorError)
|
|
@@ -3189,8 +3267,8 @@ export class ChatCommand {
|
|
|
3189
3267
|
console.log(chalk.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
|
|
3190
3268
|
}
|
|
3191
3269
|
}
|
|
3192
|
-
// Show change summary
|
|
3193
|
-
if (!this.jsonOutput && !this.directPromptMode && response.changedFiles) {
|
|
3270
|
+
// Show change summary only when the agent actually wrote files this run
|
|
3271
|
+
if (!this.jsonOutput && !this.directPromptMode && response.changedFiles && requiresWorkspaceChanges) {
|
|
3194
3272
|
const fileCount = changedFileCount;
|
|
3195
3273
|
if (fileCount > 0) {
|
|
3196
3274
|
console.log(chalk.gray(`\nFiles changed: ${fileCount}`));
|
|
@@ -3384,10 +3462,12 @@ export class ChatCommand {
|
|
|
3384
3462
|
}, null, 2));
|
|
3385
3463
|
}
|
|
3386
3464
|
this.messages.push({ role: 'assistant', content: finalAnswer || runEvaluation.statusHeadline });
|
|
3465
|
+
this.stopV3IdleWatch();
|
|
3387
3466
|
watcher?.stop();
|
|
3388
3467
|
return true;
|
|
3389
3468
|
}
|
|
3390
3469
|
catch (error) {
|
|
3470
|
+
this.stopV3IdleWatch();
|
|
3391
3471
|
watcher?.stop();
|
|
3392
3472
|
if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
|
|
3393
3473
|
const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
|
|
@@ -3601,7 +3681,12 @@ export class ChatCommand {
|
|
|
3601
3681
|
return lines.join('\n');
|
|
3602
3682
|
};
|
|
3603
3683
|
while (true) {
|
|
3604
|
-
const
|
|
3684
|
+
const rawInput = await readMultiLineInput();
|
|
3685
|
+
const { goaContext, userPrompt: strippedInput } = splitGoaContextFromInput(rawInput);
|
|
3686
|
+
if (goaContext) {
|
|
3687
|
+
this.pendingGoaContext = goaContext;
|
|
3688
|
+
}
|
|
3689
|
+
const input = strippedInput || rawInput;
|
|
3605
3690
|
const trimmed = input.trim();
|
|
3606
3691
|
if (!trimmed) {
|
|
3607
3692
|
continue;
|
|
@@ -34,6 +34,10 @@ export interface RunEvaluation {
|
|
|
34
34
|
}
|
|
35
35
|
export declare function createLiveOutcome(): LiveOutcome;
|
|
36
36
|
export declare function isExecutorTimeoutFailure(liveOutcome: LiveOutcome): boolean;
|
|
37
|
+
export declare function isInferenceDegradedFailure(liveOutcome: LiveOutcome): boolean;
|
|
38
|
+
/** Strip model thinking blocks and stray closing tags from user-visible answer text. */
|
|
39
|
+
export declare function stripAgentThinkingTags(text: string): string;
|
|
40
|
+
export declare function isGenericFallbackAnswer(text: string): boolean;
|
|
37
41
|
/** Tool-trace stubs like `[read_file] "index.html"` or `<read_file>…</read_file>` — not a user-facing answer. */
|
|
38
42
|
export declare function isToolEvidenceStubAnswer(text: string): boolean;
|
|
39
43
|
/** True when text looks like a real answer, not an executor/system placeholder. */
|
|
@@ -30,10 +30,21 @@ const LIST_ONLY_DISCOVERY_TOOLS = new Set([
|
|
|
30
30
|
'dir',
|
|
31
31
|
]);
|
|
32
32
|
const GENERIC_SUMMARY_RE = /^(completed the requested analysis|reviewed the workspace without writing changes|task completed|done|finished|analysis completed)([.\s]|$)/i;
|
|
33
|
+
/** Planner fallback / degraded inference — generic greeting unrelated to the user question. */
|
|
34
|
+
const GENERIC_FALLBACK_GREETING_RE = /^(?:hello!?|hi!?|hey!?)\s*(?:how can i assist|how can i help|what would you like|whether you'?re looking to build)/i;
|
|
35
|
+
/** System-prompt fragments leaked when the model falls back without real context. */
|
|
36
|
+
const SYSTEM_PROMPT_LEAK_RE = /avoid global namespace pollution unless explicitly needed|minify assets only when explicitly requested|include proper accessibility attributes \(aria-label/i;
|
|
33
37
|
export function isExecutorTimeoutFailure(liveOutcome) {
|
|
34
38
|
const errorText = String(liveOutcome.executorError || '').trim();
|
|
35
39
|
return /executor llm call timed out|fast fallback also failed|executor model timed out/i.test(errorText);
|
|
36
40
|
}
|
|
41
|
+
export function isInferenceDegradedFailure(liveOutcome) {
|
|
42
|
+
const answer = stripAgentThinkingTags(liveOutcome.answerContent || '');
|
|
43
|
+
if (!answer)
|
|
44
|
+
return false;
|
|
45
|
+
return isGenericFallbackAnswer(answer)
|
|
46
|
+
&& (liveOutcome.analysisToolsUsed > 0 || liveOutcome.requiresWorkspaceChanges);
|
|
47
|
+
}
|
|
37
48
|
function looksLikeRawV3EventPayload(text) {
|
|
38
49
|
const trimmed = String(text || '').trim();
|
|
39
50
|
if (!/^[{[]/.test(trimmed))
|
|
@@ -56,9 +67,34 @@ function looksLikeRawV3EventPayload(text) {
|
|
|
56
67
|
return false;
|
|
57
68
|
}
|
|
58
69
|
}
|
|
70
|
+
/** Strip model thinking blocks and stray closing tags from user-visible answer text. */
|
|
71
|
+
export function stripAgentThinkingTags(text) {
|
|
72
|
+
let output = String(text || '');
|
|
73
|
+
if (!output)
|
|
74
|
+
return '';
|
|
75
|
+
output = output.replace(/<\/think>/gi, '</thinking>');
|
|
76
|
+
output = output.replace(/<think>/gi, '<thinking>');
|
|
77
|
+
output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
|
|
78
|
+
output = output.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
|
79
|
+
output = output.replace(/<thinking>[\s\S]*$/i, '');
|
|
80
|
+
output = output.replace(/<\/thinking>/gi, '');
|
|
81
|
+
return output.trim();
|
|
82
|
+
}
|
|
83
|
+
export function isGenericFallbackAnswer(text) {
|
|
84
|
+
const trimmed = stripAgentThinkingTags(text);
|
|
85
|
+
if (!trimmed)
|
|
86
|
+
return true;
|
|
87
|
+
if (GENERIC_FALLBACK_GREETING_RE.test(trimmed))
|
|
88
|
+
return true;
|
|
89
|
+
if (SYSTEM_PROMPT_LEAK_RE.test(trimmed))
|
|
90
|
+
return true;
|
|
91
|
+
if (/^now,?\s+i'?m ready to assist/i.test(trimmed) && trimmed.length < 400)
|
|
92
|
+
return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
59
95
|
/** Tool-trace stubs like `[read_file] "index.html"` or `<read_file>…</read_file>` — not a user-facing answer. */
|
|
60
96
|
export function isToolEvidenceStubAnswer(text) {
|
|
61
|
-
const trimmed =
|
|
97
|
+
const trimmed = stripAgentThinkingTags(text);
|
|
62
98
|
if (!trimmed)
|
|
63
99
|
return true;
|
|
64
100
|
if (looksLikeRawV3EventPayload(trimmed))
|
|
@@ -89,13 +125,15 @@ export function isToolEvidenceStubAnswer(text) {
|
|
|
89
125
|
}
|
|
90
126
|
/** True when text looks like a real answer, not an executor/system placeholder. */
|
|
91
127
|
export function isSubstantiveAgentAnswer(text) {
|
|
92
|
-
const trimmed =
|
|
128
|
+
const trimmed = stripAgentThinkingTags(text);
|
|
93
129
|
if (trimmed.length < 80)
|
|
94
130
|
return false;
|
|
95
131
|
if (EXECUTOR_PLACEHOLDER_RE.test(trimmed))
|
|
96
132
|
return false;
|
|
97
133
|
if (/^v3 agent workflow completed\.?$/i.test(trimmed))
|
|
98
134
|
return false;
|
|
135
|
+
if (isGenericFallbackAnswer(trimmed))
|
|
136
|
+
return false;
|
|
99
137
|
if (isToolEvidenceStubAnswer(trimmed))
|
|
100
138
|
return false;
|
|
101
139
|
return true;
|
|
@@ -103,7 +141,7 @@ export function isSubstantiveAgentAnswer(text) {
|
|
|
103
141
|
/** Pick the first substantive answer from response body and/or streamed text. */
|
|
104
142
|
export function normalizeAgentAnswerContent(...sources) {
|
|
105
143
|
for (const source of sources) {
|
|
106
|
-
const text = String(source || '')
|
|
144
|
+
const text = stripAgentThinkingTags(String(source || ''));
|
|
107
145
|
if (isSubstantiveAgentAnswer(text))
|
|
108
146
|
return text;
|
|
109
147
|
}
|
|
@@ -206,6 +244,23 @@ export function evaluateExecutorSuccess(liveOutcome) {
|
|
|
206
244
|
const allTasksPassed = liveOutcome.tasksSucceeded === liveOutcome.tasksTotal;
|
|
207
245
|
const partialTasksPassed = liveOutcome.tasksSucceeded > 0;
|
|
208
246
|
const failedCount = liveOutcome.failedTaskIds.size;
|
|
247
|
+
if (allTasksPassed && failedCount === 0 && !liveOutcome.streamAborted) {
|
|
248
|
+
const qualityOnlyPlannerNote = Boolean(liveOutcome.plannerError)
|
|
249
|
+
&& /Planner-Executor completed:\s*\d+\s*\/\s*\d+\s+tasks succeeded/i.test(String(liveOutcome.plannerError));
|
|
250
|
+
const recoveredAfterTransientErrors = liveOutcome.executorFailed
|
|
251
|
+
|| Boolean(liveOutcome.executorError)
|
|
252
|
+
|| qualityOnlyPlannerNote;
|
|
253
|
+
if (!liveOutcome.plannerError || qualityOnlyPlannerNote || recoveredAfterTransientErrors) {
|
|
254
|
+
const qualityNote = qualityOnlyPlannerNote || /Quality (?:audit|blockers)/i.test(String(liveOutcome.plannerError || ''));
|
|
255
|
+
return {
|
|
256
|
+
executorSucceeded: true,
|
|
257
|
+
statusHeadline: qualityNote
|
|
258
|
+
? `✓ Tasks complete (${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal}) — quality audit below target`
|
|
259
|
+
: `✓ Agent run finished (${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal} tasks completed)`,
|
|
260
|
+
uiTheme: qualityNote ? 'warning' : 'success',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
209
264
|
if (allTasksPassed && !hasFatalError) {
|
|
210
265
|
return {
|
|
211
266
|
executorSucceeded: true,
|
|
@@ -235,6 +290,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
|
|
|
235
290
|
|| (liveOutcome.analysisToolsUsed >= 4
|
|
236
291
|
&& liveOutcome.analysisReadToolsUsed >= 3
|
|
237
292
|
&& answerText.length >= 700);
|
|
293
|
+
if (isInferenceDegradedFailure(liveOutcome)) {
|
|
294
|
+
return {
|
|
295
|
+
executorSucceeded: false,
|
|
296
|
+
statusHeadline: '✕ Model inference degraded — generic fallback answer, not your question — try /retry',
|
|
297
|
+
uiTheme: 'error',
|
|
298
|
+
};
|
|
299
|
+
}
|
|
238
300
|
if (hasFatalError && !hasSubstantiveAnswer) {
|
|
239
301
|
return {
|
|
240
302
|
executorSucceeded: false,
|
|
@@ -276,6 +338,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
|
|
|
276
338
|
uiTheme: 'success',
|
|
277
339
|
};
|
|
278
340
|
}
|
|
341
|
+
if (isInferenceDegradedFailure(liveOutcome)) {
|
|
342
|
+
return {
|
|
343
|
+
executorSucceeded: false,
|
|
344
|
+
statusHeadline: '✕ Model inference degraded — server could not run the code model — try /retry in a minute',
|
|
345
|
+
uiTheme: 'error',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
279
348
|
const legacySuccess = liveOutcome.workspaceHasOutput && liveOutcome.changedFileCount > 0;
|
|
280
349
|
return {
|
|
281
350
|
executorSucceeded: legacySuccess && !hasFatalError,
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -272,6 +272,7 @@ export declare class APIClient {
|
|
|
272
272
|
*/
|
|
273
273
|
isAnalysisOnlyTask(message?: string, context?: Record<string, any>): boolean;
|
|
274
274
|
private normalizeWorkspaceRelativePath;
|
|
275
|
+
private collectAgentStateSyncPaths;
|
|
275
276
|
private listFrontendWorkspaceFiles;
|
|
276
277
|
private chooseFrontendPreviewEntry;
|
|
277
278
|
private extractLinkedFrontendAssets;
|
|
@@ -322,8 +323,11 @@ export declare class APIClient {
|
|
|
322
323
|
executionOptions?: Record<string, unknown>;
|
|
323
324
|
}): Promise<VigFlowExecutionResult>;
|
|
324
325
|
getVigFlowExecutionStatus(executionId: string): Promise<VigFlowExecutionStatus>;
|
|
325
|
-
/**
|
|
326
|
+
/** Legacy cap — prefer resolveV3ContextCharLimit() for token-aligned budgets. */
|
|
326
327
|
private static readonly V3_CONTEXT_CHAR_LIMIT;
|
|
328
|
+
private static readonly HYDRATION_PRIORITY_BASENAMES;
|
|
329
|
+
/** Keep critical workspace files for server hydration when context must shrink. */
|
|
330
|
+
private compactWorkspaceFilesToBudget;
|
|
327
331
|
buildV3AgentContext(context?: Record<string, any>): string;
|
|
328
332
|
/**
|
|
329
333
|
* Compact a V3 context payload so the serialized JSON stays under
|
|
@@ -335,6 +339,9 @@ export declare class APIClient {
|
|
|
335
339
|
* 5. Drop readmeExcerpt
|
|
336
340
|
*/
|
|
337
341
|
private compactV3Context;
|
|
342
|
+
/** Normalize common Unix-only commands before Windows cmd/powershell execution. */
|
|
343
|
+
private normalizeWindowsRunCommand;
|
|
344
|
+
private resolveRunCommandCwd;
|
|
338
345
|
/** Remove duplicate functionIndex entries that poison model context after compaction. */
|
|
339
346
|
private dedupeBrainPayload;
|
|
340
347
|
buildMinimalV3AgentContext(context?: Record<string, any>): string;
|
package/dist/utils/api.js
CHANGED
|
@@ -14,6 +14,8 @@ import { getChangedFiles } from './workspace-cache.js';
|
|
|
14
14
|
import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
|
|
15
15
|
import { joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
|
|
16
16
|
import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
|
|
17
|
+
import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
|
|
18
|
+
import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
|
|
17
19
|
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
|
|
18
20
|
export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
|
|
19
21
|
export class CLIError extends Error {
|
|
@@ -732,6 +734,30 @@ export class APIClient {
|
|
|
732
734
|
normalizeWorkspaceRelativePath(filePath) {
|
|
733
735
|
return String(filePath || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
734
736
|
}
|
|
737
|
+
collectAgentStateSyncPaths(rootPath) {
|
|
738
|
+
const paths = [];
|
|
739
|
+
const progressPath = path.join(rootPath, '.vigthoria', 'agent-state', 'progress.json');
|
|
740
|
+
if (!fs.existsSync(progressPath)) {
|
|
741
|
+
return paths;
|
|
742
|
+
}
|
|
743
|
+
paths.push('.vigthoria/agent-state/progress.json');
|
|
744
|
+
const tasksDir = path.join(rootPath, '.vigthoria', 'agent-state', 'tasks');
|
|
745
|
+
if (!fs.existsSync(tasksDir)) {
|
|
746
|
+
return paths;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
|
|
750
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
paths.push(`.vigthoria/agent-state/tasks/${entry.name}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
// Ignore unreadable task snapshots.
|
|
758
|
+
}
|
|
759
|
+
return paths;
|
|
760
|
+
}
|
|
735
761
|
listFrontendWorkspaceFiles(rootPath) {
|
|
736
762
|
const results = [];
|
|
737
763
|
const stack = [rootPath];
|
|
@@ -1424,8 +1450,41 @@ export class APIClient {
|
|
|
1424
1450
|
return payload.execution;
|
|
1425
1451
|
});
|
|
1426
1452
|
}
|
|
1427
|
-
/**
|
|
1428
|
-
static V3_CONTEXT_CHAR_LIMIT =
|
|
1453
|
+
/** Legacy cap — prefer resolveV3ContextCharLimit() for token-aligned budgets. */
|
|
1454
|
+
static V3_CONTEXT_CHAR_LIMIT = 100_000;
|
|
1455
|
+
static HYDRATION_PRIORITY_BASENAMES = new Set([
|
|
1456
|
+
'package.json', 'index.html', 'game.js', 'main.js', 'app.js', 'script.js',
|
|
1457
|
+
'styles.css', 'style.css', 'README.md', 'manifest.json',
|
|
1458
|
+
]);
|
|
1459
|
+
/** Keep critical workspace files for server hydration when context must shrink. */
|
|
1460
|
+
compactWorkspaceFilesToBudget(files, budgetChars) {
|
|
1461
|
+
const entries = Object.entries(files);
|
|
1462
|
+
const rank = (filePath) => {
|
|
1463
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
1464
|
+
const base = normalized.split('/').pop() || '';
|
|
1465
|
+
if (APIClient.HYDRATION_PRIORITY_BASENAMES.has(base))
|
|
1466
|
+
return 0;
|
|
1467
|
+
if (/^\.vigthoria\/agent-state\//i.test(normalized))
|
|
1468
|
+
return 1;
|
|
1469
|
+
if (/\.(html?|js|mjs|cjs|css|json|ts|tsx|jsx)$/i.test(base))
|
|
1470
|
+
return 2;
|
|
1471
|
+
return 3;
|
|
1472
|
+
};
|
|
1473
|
+
entries.sort((a, b) => rank(a[0]) - rank(b[0]) || a[0].localeCompare(b[0]));
|
|
1474
|
+
const trimmed = {};
|
|
1475
|
+
let used = 2;
|
|
1476
|
+
for (const [filePath, content] of entries) {
|
|
1477
|
+
const maxContentLen = rank(filePath) <= 1 ? content.length : Math.min(content.length, WORKSPACE_FILE_CHAR_CAP);
|
|
1478
|
+
const clipped = content.length > maxContentLen ? `${content.slice(0, maxContentLen)}\n/* … truncated for remote context … */` : content;
|
|
1479
|
+
const entryLen = JSON.stringify(filePath).length + 1 + JSON.stringify(clipped).length + 1;
|
|
1480
|
+
if (used + entryLen > budgetChars) {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
trimmed[filePath] = clipped;
|
|
1484
|
+
used += entryLen;
|
|
1485
|
+
}
|
|
1486
|
+
return trimmed;
|
|
1487
|
+
}
|
|
1429
1488
|
buildV3AgentContext(context = {}) {
|
|
1430
1489
|
const resolvedContext = this.ensureExecutionContext(context);
|
|
1431
1490
|
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
@@ -1533,7 +1592,16 @@ export class APIClient {
|
|
|
1533
1592
|
* 5. Drop readmeExcerpt
|
|
1534
1593
|
*/
|
|
1535
1594
|
compactV3Context(payload) {
|
|
1536
|
-
const LIMIT =
|
|
1595
|
+
const LIMIT = resolveV3ContextCharLimit();
|
|
1596
|
+
const phases = [];
|
|
1597
|
+
const compactionMeta = {
|
|
1598
|
+
applied: false,
|
|
1599
|
+
beforeChars: 0,
|
|
1600
|
+
afterChars: 0,
|
|
1601
|
+
charLimit: LIMIT,
|
|
1602
|
+
estimatedInputBudget: Math.floor(LIMIT / 4),
|
|
1603
|
+
phases,
|
|
1604
|
+
};
|
|
1537
1605
|
if (payload.vigthoriaBrain) {
|
|
1538
1606
|
payload.vigthoriaBrain = this.dedupeBrainPayload(payload.vigthoriaBrain);
|
|
1539
1607
|
}
|
|
@@ -1541,80 +1609,155 @@ export class APIClient {
|
|
|
1541
1609
|
payload.vigthoria_brain = payload.vigthoriaBrain || this.dedupeBrainPayload(payload.vigthoria_brain);
|
|
1542
1610
|
}
|
|
1543
1611
|
let json = JSON.stringify(payload);
|
|
1544
|
-
|
|
1612
|
+
compactionMeta.beforeChars = json.length;
|
|
1613
|
+
if (json.length <= LIMIT) {
|
|
1614
|
+
compactionMeta.afterChars = json.length;
|
|
1615
|
+
payload.contextBudgetMeta = compactionMeta;
|
|
1545
1616
|
return json;
|
|
1617
|
+
}
|
|
1618
|
+
compactionMeta.applied = true;
|
|
1619
|
+
phases.push('over_limit');
|
|
1546
1620
|
if (!/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_QUIET_CONTEXT_COMPACTION || ''))) {
|
|
1547
|
-
process.stderr.write(`Workspace context is large
|
|
1621
|
+
process.stderr.write(`Workspace context is large — sending a focused summary (~${Math.round(LIMIT / 1000)}k char budget, token-aligned).\n`);
|
|
1548
1622
|
}
|
|
1549
1623
|
if (process.env.DEBUG || process.env.VIGTHORIA_DEBUG) {
|
|
1550
1624
|
process.stderr.write(`[context] Payload ${json.length} chars exceeds ${LIMIT} limit, compacting...\n`);
|
|
1551
1625
|
}
|
|
1552
|
-
// Phase 1 — shrink workspaceFiles to fit
|
|
1553
1626
|
const summary = payload.localWorkspaceSummary;
|
|
1627
|
+
const hydrationRequired = payload.workspaceHydrationRequired === true;
|
|
1628
|
+
const finish = () => {
|
|
1629
|
+
compactionMeta.afterChars = json.length;
|
|
1630
|
+
payload.contextBudgetMeta = compactionMeta;
|
|
1631
|
+
return json;
|
|
1632
|
+
};
|
|
1633
|
+
// Phase 1 — truncate history before touching workspace file hydration
|
|
1634
|
+
if (Array.isArray(payload.history) && payload.history.length > 6) {
|
|
1635
|
+
payload.history = payload.history.slice(-6);
|
|
1636
|
+
phases.push('trim_history_6');
|
|
1637
|
+
json = JSON.stringify(payload);
|
|
1638
|
+
if (json.length <= LIMIT)
|
|
1639
|
+
return finish();
|
|
1640
|
+
}
|
|
1641
|
+
// Phase 2 — shrink workspaceFiles (priority order) to fit
|
|
1554
1642
|
if (summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
|
|
1555
|
-
const fileEntries = Object.entries(summary.workspaceFiles);
|
|
1556
1643
|
const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
|
|
1557
|
-
const budget = LIMIT - overhead -
|
|
1644
|
+
const budget = LIMIT - overhead - 1024;
|
|
1558
1645
|
if (budget > 0) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
trimmed[k] = v;
|
|
1566
|
-
used += entryLen;
|
|
1567
|
-
}
|
|
1568
|
-
summary.workspaceFiles = trimmed;
|
|
1646
|
+
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, budget);
|
|
1647
|
+
summary.workspaceFilesCompaction = Object.keys(summary.workspaceFiles).length > 0 ? 'priority-trimmed' : 'empty';
|
|
1648
|
+
}
|
|
1649
|
+
else if (hydrationRequired) {
|
|
1650
|
+
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 8_000);
|
|
1651
|
+
summary.workspaceFilesCompaction = 'priority-minimal';
|
|
1569
1652
|
}
|
|
1570
1653
|
else {
|
|
1571
1654
|
delete summary.workspaceFiles;
|
|
1572
1655
|
}
|
|
1573
1656
|
json = JSON.stringify(payload);
|
|
1574
1657
|
if (json.length <= LIMIT)
|
|
1575
|
-
return
|
|
1658
|
+
return finish();
|
|
1576
1659
|
}
|
|
1577
|
-
// Phase
|
|
1578
|
-
if (summary?.
|
|
1660
|
+
// Phase 3 — drop readme before dropping hydration payload
|
|
1661
|
+
if (summary?.readmeExcerpt) {
|
|
1662
|
+
delete summary.readmeExcerpt;
|
|
1663
|
+
json = JSON.stringify(payload);
|
|
1664
|
+
if (json.length <= LIMIT)
|
|
1665
|
+
return finish();
|
|
1666
|
+
}
|
|
1667
|
+
// Phase 4 — never fully drop workspaceFiles when remote hydration is required
|
|
1668
|
+
if (summary?.workspaceFiles && !hydrationRequired) {
|
|
1579
1669
|
delete summary.workspaceFiles;
|
|
1580
1670
|
json = JSON.stringify(payload);
|
|
1581
1671
|
if (json.length <= LIMIT)
|
|
1582
|
-
return
|
|
1672
|
+
return finish();
|
|
1583
1673
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1674
|
+
if (hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
|
|
1675
|
+
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 4_000);
|
|
1676
|
+
summary.workspaceFilesCompaction = 'priority-minimal';
|
|
1587
1677
|
json = JSON.stringify(payload);
|
|
1588
1678
|
if (json.length <= LIMIT)
|
|
1589
|
-
return
|
|
1679
|
+
return finish();
|
|
1590
1680
|
}
|
|
1591
|
-
// Phase
|
|
1592
|
-
if (
|
|
1593
|
-
|
|
1681
|
+
// Phase 5 — truncate history to last 3 messages
|
|
1682
|
+
if (Array.isArray(payload.history) && payload.history.length > 3) {
|
|
1683
|
+
payload.history = payload.history.slice(-3);
|
|
1594
1684
|
json = JSON.stringify(payload);
|
|
1595
1685
|
if (json.length <= LIMIT)
|
|
1596
|
-
return
|
|
1686
|
+
return finish();
|
|
1597
1687
|
}
|
|
1598
|
-
// Phase
|
|
1599
|
-
if (summary?.
|
|
1600
|
-
|
|
1688
|
+
// Phase 6 — truncate file list
|
|
1689
|
+
if (summary?.files && Array.isArray(summary.files) && summary.files.length > 15) {
|
|
1690
|
+
summary.files = summary.files.slice(0, 15);
|
|
1601
1691
|
json = JSON.stringify(payload);
|
|
1602
1692
|
if (json.length <= LIMIT)
|
|
1603
|
-
return
|
|
1693
|
+
return finish();
|
|
1604
1694
|
}
|
|
1605
|
-
// Phase
|
|
1695
|
+
// Phase 7 — drop history entirely
|
|
1606
1696
|
if (Array.isArray(payload.history) && payload.history.length > 0) {
|
|
1607
1697
|
payload.history = [];
|
|
1608
1698
|
json = JSON.stringify(payload);
|
|
1609
1699
|
if (json.length <= LIMIT)
|
|
1610
|
-
return
|
|
1700
|
+
return finish();
|
|
1611
1701
|
}
|
|
1612
|
-
// Phase
|
|
1702
|
+
// Phase 8 — drop localWorkspaceSummary metadata but keep hydration files for remote clients
|
|
1613
1703
|
if (payload.localWorkspaceSummary) {
|
|
1614
|
-
|
|
1704
|
+
const keepFiles = hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object'
|
|
1705
|
+
? summary.workspaceFiles
|
|
1706
|
+
: undefined;
|
|
1707
|
+
payload.localWorkspaceSummary = {
|
|
1708
|
+
path: summary?.path,
|
|
1709
|
+
name: summary?.name,
|
|
1710
|
+
fileCount: summary?.fileCount,
|
|
1711
|
+
files: Array.isArray(summary?.files) ? summary.files.slice(0, 20) : [],
|
|
1712
|
+
...(keepFiles ? { workspaceFiles: keepFiles, workspaceFilesCompaction: summary?.workspaceFilesCompaction || 'priority-minimal' } : {}),
|
|
1713
|
+
};
|
|
1615
1714
|
json = JSON.stringify(payload);
|
|
1616
1715
|
}
|
|
1617
|
-
|
|
1716
|
+
if (hydrationRequired && json.length > LIMIT) {
|
|
1717
|
+
process.stderr.write('Warning: workspace context is still large after compaction; remote agent may have partial project visibility. Consider closing other context or using /compact.\n');
|
|
1718
|
+
}
|
|
1719
|
+
return finish();
|
|
1720
|
+
}
|
|
1721
|
+
/** Normalize common Unix-only commands before Windows cmd/powershell execution. */
|
|
1722
|
+
normalizeWindowsRunCommand(command) {
|
|
1723
|
+
let cmd = String(command || '').trim();
|
|
1724
|
+
if (!cmd)
|
|
1725
|
+
return cmd;
|
|
1726
|
+
if (/^\/[^\s/]/.test(cmd) && !/^\/[a-z]:/i.test(cmd)) {
|
|
1727
|
+
throw new Error('Command starts with "/" which Windows interprets as an invalid switch. Use workspace-relative paths without a leading slash.');
|
|
1728
|
+
}
|
|
1729
|
+
const replacements = [
|
|
1730
|
+
[/^ls(\s|$)/i, 'dir$1'],
|
|
1731
|
+
[/^cat\s+/i, 'type '],
|
|
1732
|
+
[/^pwd\s*$/i, 'cd'],
|
|
1733
|
+
[/^rm\s+-rf?\s+/i, 'rmdir /s /q '],
|
|
1734
|
+
[/^rm\s+/i, 'del '],
|
|
1735
|
+
[/^cp\s+/i, 'copy '],
|
|
1736
|
+
[/^mv\s+/i, 'move '],
|
|
1737
|
+
];
|
|
1738
|
+
for (const [pattern, replacement] of replacements) {
|
|
1739
|
+
cmd = cmd.replace(pattern, replacement);
|
|
1740
|
+
}
|
|
1741
|
+
return cmd;
|
|
1742
|
+
}
|
|
1743
|
+
resolveRunCommandCwd(rootPath, args, serverRoot) {
|
|
1744
|
+
const cwdRaw = args.cwd ?? args.path ?? '.';
|
|
1745
|
+
let target = this.resolveV3ClientToolPath(rootPath, cwdRaw, serverRoot ? [serverRoot] : []);
|
|
1746
|
+
if (!target) {
|
|
1747
|
+
target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
|
|
1748
|
+
}
|
|
1749
|
+
if (!target) {
|
|
1750
|
+
return path.resolve(rootPath);
|
|
1751
|
+
}
|
|
1752
|
+
try {
|
|
1753
|
+
if (fs.existsSync(target.absolutePath) && fs.statSync(target.absolutePath).isFile()) {
|
|
1754
|
+
return path.dirname(target.absolutePath);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
catch {
|
|
1758
|
+
// fall through to directory cwd
|
|
1759
|
+
}
|
|
1760
|
+
return target.absolutePath;
|
|
1618
1761
|
}
|
|
1619
1762
|
/** Remove duplicate functionIndex entries that poison model context after compaction. */
|
|
1620
1763
|
dedupeBrainPayload(brain) {
|
|
@@ -2401,7 +2544,8 @@ menu {
|
|
|
2401
2544
|
};
|
|
2402
2545
|
const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
|
|
2403
2546
|
const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
|
|
2404
|
-
|
|
2547
|
+
const agentStatePaths = this.collectAgentStateSyncPaths(rootPath);
|
|
2548
|
+
let orderedPaths = [...agentStatePaths, ...candidatePaths];
|
|
2405
2549
|
try {
|
|
2406
2550
|
const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
|
|
2407
2551
|
const changes = getChangedFiles(rootPath, candidatePaths);
|
|
@@ -2478,7 +2622,9 @@ menu {
|
|
|
2478
2622
|
continue;
|
|
2479
2623
|
if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
|
|
2480
2624
|
continue;
|
|
2481
|
-
|
|
2625
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
2626
|
+
const isAgentStateSync = /^\.vigthoria\/agent-state\//i.test(normalizedPath);
|
|
2627
|
+
if (!isAgentStateSync && /(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
|
|
2482
2628
|
continue;
|
|
2483
2629
|
const absolutePath = path.join(rootPath, relativePath);
|
|
2484
2630
|
try {
|
|
@@ -2646,6 +2792,8 @@ menu {
|
|
|
2646
2792
|
}
|
|
2647
2793
|
return;
|
|
2648
2794
|
}
|
|
2795
|
+
// workspace_snapshot is handled by applyV3AgentStreamEventToWorkspace only —
|
|
2796
|
+
// never count hydrated baselines as "files changed".
|
|
2649
2797
|
// Only count confirmed mutations — not workspace snapshots (hydrated baseline)
|
|
2650
2798
|
// or tool_call previews that may still fail client-side / read-only guards.
|
|
2651
2799
|
if (event.type === 'tool_result' && event.success) {
|
|
@@ -2694,9 +2842,17 @@ menu {
|
|
|
2694
2842
|
}
|
|
2695
2843
|
return false;
|
|
2696
2844
|
}
|
|
2697
|
-
resolveV3ClientToolPath(rootPath, rawPath) {
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2845
|
+
resolveV3ClientToolPath(rootPath, rawPath, sourceRoots = []) {
|
|
2846
|
+
const raw = String(rawPath || '.').trim();
|
|
2847
|
+
const roots = [rootPath, ...sourceRoots.filter(Boolean)];
|
|
2848
|
+
let relativePath = '';
|
|
2849
|
+
for (const sourceRoot of roots) {
|
|
2850
|
+
relativePath = this.normalizeAgentWorkspaceRelativePath(raw, sourceRoot);
|
|
2851
|
+
if (relativePath || raw === '.' || raw === '') {
|
|
2852
|
+
break;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
if (!relativePath && raw !== '.' && raw !== '') {
|
|
2700
2856
|
return null;
|
|
2701
2857
|
}
|
|
2702
2858
|
const absolutePath = relativePath ? joinV3WorkspacePath(rootPath, relativePath) : path.resolve(rootPath);
|
|
@@ -2747,7 +2903,13 @@ menu {
|
|
|
2747
2903
|
}
|
|
2748
2904
|
const name = String(event.name || event.tool || '').trim();
|
|
2749
2905
|
const args = event.arguments || {};
|
|
2750
|
-
const
|
|
2906
|
+
const serverRoot = String(context.__v3ServerWorkspaceRoot || '').trim();
|
|
2907
|
+
const isRunCommand = name === 'run_command';
|
|
2908
|
+
const pathArg = isRunCommand ? (args.cwd ?? args.path ?? '.') : (args.path || args.cwd || '.');
|
|
2909
|
+
let target = this.resolveV3ClientToolPath(rootPath, pathArg, serverRoot ? [serverRoot] : []);
|
|
2910
|
+
if (!target && isRunCommand) {
|
|
2911
|
+
target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
|
|
2912
|
+
}
|
|
2751
2913
|
if (!target) {
|
|
2752
2914
|
return { success: false, output: '', error: 'Tool path is outside the workspace.' };
|
|
2753
2915
|
}
|
|
@@ -2831,6 +2993,14 @@ menu {
|
|
|
2831
2993
|
error: `File not found: ${target.relativePath}. Write the file before running syntax_check.`,
|
|
2832
2994
|
};
|
|
2833
2995
|
}
|
|
2996
|
+
const stat = fs.statSync(target.absolutePath);
|
|
2997
|
+
if (stat.isDirectory()) {
|
|
2998
|
+
return {
|
|
2999
|
+
success: false,
|
|
3000
|
+
output: '',
|
|
3001
|
+
error: `Expected a file, but path is a directory: ${target.relativePath}`,
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
2834
3004
|
const ext = path.extname(target.absolutePath).toLowerCase();
|
|
2835
3005
|
if (ext === '.json') {
|
|
2836
3006
|
JSON.parse(fs.readFileSync(target.absolutePath, 'utf8'));
|
|
@@ -2851,12 +3021,37 @@ menu {
|
|
|
2851
3021
|
return { success: true, output: `Syntax check passed: ${target.relativePath}` };
|
|
2852
3022
|
}
|
|
2853
3023
|
if (name === 'run_command') {
|
|
2854
|
-
|
|
3024
|
+
let command = String(args.command || '').trim();
|
|
2855
3025
|
if (!command)
|
|
2856
3026
|
return { success: false, output: '', error: 'run_command requires command.' };
|
|
3027
|
+
try {
|
|
3028
|
+
if (process.platform === 'win32') {
|
|
3029
|
+
command = this.normalizeWindowsRunCommand(command);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
catch (error) {
|
|
3033
|
+
return { success: false, output: '', error: error.message };
|
|
3034
|
+
}
|
|
3035
|
+
const cwd = this.resolveRunCommandCwd(rootPath, args, serverRoot);
|
|
3036
|
+
const usePowerShell = process.platform === 'win32'
|
|
3037
|
+
&& /^(npm|npx|node|yarn|pnpm|git|python|pip|powershell)\b/i.test(command);
|
|
2857
3038
|
const { exec } = await import('child_process');
|
|
2858
3039
|
return await new Promise((resolve) => {
|
|
2859
|
-
|
|
3040
|
+
const execOptions = {
|
|
3041
|
+
cwd,
|
|
3042
|
+
timeout: Number(args.timeout || 30000),
|
|
3043
|
+
maxBuffer: 1024 * 1024,
|
|
3044
|
+
env: process.env,
|
|
3045
|
+
};
|
|
3046
|
+
if (usePowerShell) {
|
|
3047
|
+
execOptions.shell = 'powershell.exe';
|
|
3048
|
+
execOptions.windowsHide = true;
|
|
3049
|
+
command = `-NoProfile -ExecutionPolicy Bypass -Command ${JSON.stringify(command)}`;
|
|
3050
|
+
}
|
|
3051
|
+
else if (process.platform === 'win32') {
|
|
3052
|
+
execOptions.shell = 'cmd.exe';
|
|
3053
|
+
}
|
|
3054
|
+
exec(command, execOptions, (error, stdout, stderr) => {
|
|
2860
3055
|
resolve({
|
|
2861
3056
|
success: !error,
|
|
2862
3057
|
output: String(stdout || stderr || '').slice(0, 12000),
|
|
@@ -3688,6 +3883,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3688
3883
|
}
|
|
3689
3884
|
if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
|
|
3690
3885
|
serverWorkspaceRoot = event.workspace_root.trim();
|
|
3886
|
+
context.__v3ServerWorkspaceRoot = serverWorkspaceRoot;
|
|
3691
3887
|
}
|
|
3692
3888
|
if (event.type === 'client_tool_request') {
|
|
3693
3889
|
const toolTask = this.handleV3ClientToolRequest(event, context, response.url).catch((error) => {
|
|
@@ -3721,7 +3917,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3721
3917
|
}
|
|
3722
3918
|
}
|
|
3723
3919
|
}
|
|
3724
|
-
if (typeof context.onStreamEvent === 'function') {
|
|
3920
|
+
if (typeof context.onStreamEvent === 'function' && !isV3StreamKeepaliveEvent(userEvent)) {
|
|
3725
3921
|
try {
|
|
3726
3922
|
context.onStreamEvent(userEvent);
|
|
3727
3923
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side context budget helpers — aligned with V3 server context_budget.py.
|
|
3
|
+
* Initial agent context JSON should leave headroom for system prompt + tool loops.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_CODE_RUNTIME_CTX: number;
|
|
6
|
+
export declare const CONTEXT_SAFETY_MARGIN: number;
|
|
7
|
+
export declare const DEFAULT_MAX_OUTPUT_TOKENS: number;
|
|
8
|
+
export declare const TOKEN_BUDGET_SAFETY_FACTOR: number;
|
|
9
|
+
export declare const CHARS_PER_TOKEN_EST = 4;
|
|
10
|
+
/** Max chars per file in workspaceFiles hydration (matches server TOOL_HISTORY cap order-of-magnitude). */
|
|
11
|
+
export declare const WORKSPACE_FILE_CHAR_CAP: number;
|
|
12
|
+
export declare function estimateTokens(text: string): number;
|
|
13
|
+
export declare function estimateTokensForBudget(text: string): number;
|
|
14
|
+
export declare function estimateJsonTokens(value: unknown): number;
|
|
15
|
+
export declare function resolveInputBudget(runtimeNctx?: number, maxOutput?: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Char limit for the initial V3 context JSON payload.
|
|
18
|
+
* ~45% of input token budget × 4 chars/token, clamped 32k–100k.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveV3ContextCharLimit(runtimeNctx?: number): number;
|
|
21
|
+
export declare function compactToolListingOutput(name: string, output: string, charCap: number): string;
|
|
22
|
+
export interface ContextCompactionMeta {
|
|
23
|
+
applied: boolean;
|
|
24
|
+
beforeChars: number;
|
|
25
|
+
afterChars: number;
|
|
26
|
+
charLimit: number;
|
|
27
|
+
estimatedInputBudget: number;
|
|
28
|
+
phases: string[];
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side context budget helpers — aligned with V3 server context_budget.py.
|
|
3
|
+
* Initial agent context JSON should leave headroom for system prompt + tool loops.
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_CODE_RUNTIME_CTX = Number(process.env.V3_CODE_35B_RUNTIME_CTX || '49152');
|
|
6
|
+
export const CONTEXT_SAFETY_MARGIN = Number(process.env.V3_CONTEXT_SAFETY_MARGIN || '2048');
|
|
7
|
+
export const DEFAULT_MAX_OUTPUT_TOKENS = Number(process.env.V3_MAX_TOKENS || '16384');
|
|
8
|
+
export const TOKEN_BUDGET_SAFETY_FACTOR = Number(process.env.V3_TOKEN_BUDGET_SAFETY_FACTOR || '1.2');
|
|
9
|
+
export const CHARS_PER_TOKEN_EST = 4;
|
|
10
|
+
/** Max chars per file in workspaceFiles hydration (matches server TOOL_HISTORY cap order-of-magnitude). */
|
|
11
|
+
export const WORKSPACE_FILE_CHAR_CAP = Number(process.env.V3_TOOL_HISTORY_CHAR_CAP || '12000');
|
|
12
|
+
export function estimateTokens(text) {
|
|
13
|
+
return Math.max(1, Math.ceil(String(text || '').length / CHARS_PER_TOKEN_EST));
|
|
14
|
+
}
|
|
15
|
+
export function estimateTokensForBudget(text) {
|
|
16
|
+
return Math.ceil(estimateTokens(text) * TOKEN_BUDGET_SAFETY_FACTOR);
|
|
17
|
+
}
|
|
18
|
+
export function estimateJsonTokens(value) {
|
|
19
|
+
try {
|
|
20
|
+
return estimateTokens(JSON.stringify(value ?? ''));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function resolveInputBudget(runtimeNctx = DEFAULT_CODE_RUNTIME_CTX, maxOutput = DEFAULT_MAX_OUTPUT_TOKENS) {
|
|
27
|
+
const cappedOutput = Math.max(4096, Math.min(maxOutput, Math.floor(runtimeNctx / 4)));
|
|
28
|
+
return Math.max(4096, Math.floor((runtimeNctx - cappedOutput - CONTEXT_SAFETY_MARGIN) / TOKEN_BUDGET_SAFETY_FACTOR));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Char limit for the initial V3 context JSON payload.
|
|
32
|
+
* ~45% of input token budget × 4 chars/token, clamped 32k–100k.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveV3ContextCharLimit(runtimeNctx = DEFAULT_CODE_RUNTIME_CTX) {
|
|
35
|
+
const inputBudget = resolveInputBudget(runtimeNctx);
|
|
36
|
+
const initialTurnTokens = Math.floor(inputBudget * 0.45);
|
|
37
|
+
const charLimit = initialTurnTokens * CHARS_PER_TOKEN_EST;
|
|
38
|
+
return Math.min(100_000, Math.max(32_000, charLimit));
|
|
39
|
+
}
|
|
40
|
+
export function compactToolListingOutput(name, output, charCap) {
|
|
41
|
+
const text = String(output || '');
|
|
42
|
+
if (text.length <= charCap)
|
|
43
|
+
return text;
|
|
44
|
+
if (['list_directory', 'glob', 'grep', 'search_project'].includes(name)) {
|
|
45
|
+
const lines = text.split('\n');
|
|
46
|
+
if (lines.length > 80) {
|
|
47
|
+
const head = lines.slice(0, 40).join('\n');
|
|
48
|
+
const tail = lines.slice(-20).join('\n');
|
|
49
|
+
return `${head}\n\n... [${lines.length} lines total, middle omitted for context budget] ...\n\n${tail}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const head = Math.max(1200, Math.floor(charCap / 3));
|
|
53
|
+
const tail = Math.max(1200, Math.floor(charCap / 3));
|
|
54
|
+
return `${text.slice(0, head)}\n\n...[truncated for context budget]...\n\n${text.slice(-tail)}`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workbench GOA orchestrator context (VIGTHORIA_DECK_MODE / comfort prompt auto-route).
|
|
3
|
+
* Emitted as: @vigthoria-goa:{json}
|
|
4
|
+
*/
|
|
5
|
+
export interface GoaOrchestratorContext {
|
|
6
|
+
workspace_type?: 'GREENFIELD' | 'BROWNFIELD';
|
|
7
|
+
analysis?: Record<string, unknown>;
|
|
8
|
+
instruction_count?: number;
|
|
9
|
+
executed?: boolean;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseGoaLine(line: string): GoaOrchestratorContext | null;
|
|
13
|
+
export declare function stripGoaLines(text: string): string;
|
|
14
|
+
/** Split Workbench-injected GOA prefix from the user prompt. */
|
|
15
|
+
export declare function splitGoaContextFromInput(input: string): {
|
|
16
|
+
goaContext: GoaOrchestratorContext | null;
|
|
17
|
+
userPrompt: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function formatGoaSystemGrounding(context: GoaOrchestratorContext): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workbench GOA orchestrator context (VIGTHORIA_DECK_MODE / comfort prompt auto-route).
|
|
3
|
+
* Emitted as: @vigthoria-goa:{json}
|
|
4
|
+
*/
|
|
5
|
+
const GOA_PREFIX = '@vigthoria-goa:';
|
|
6
|
+
export function parseGoaLine(line) {
|
|
7
|
+
const trimmed = line.trim();
|
|
8
|
+
if (!trimmed.startsWith(GOA_PREFIX))
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(trimmed.slice(GOA_PREFIX.length));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function stripGoaLines(text) {
|
|
18
|
+
return text
|
|
19
|
+
.split('\n')
|
|
20
|
+
.filter((line) => !line.trim().startsWith(GOA_PREFIX))
|
|
21
|
+
.join('\n');
|
|
22
|
+
}
|
|
23
|
+
/** Split Workbench-injected GOA prefix from the user prompt. */
|
|
24
|
+
export function splitGoaContextFromInput(input) {
|
|
25
|
+
const lines = input.split('\n');
|
|
26
|
+
let goaContext = null;
|
|
27
|
+
const rest = [];
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const parsed = parseGoaLine(line);
|
|
30
|
+
if (parsed && !goaContext) {
|
|
31
|
+
goaContext = parsed;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
rest.push(line);
|
|
35
|
+
}
|
|
36
|
+
return { goaContext, userPrompt: rest.join('\n').trim() };
|
|
37
|
+
}
|
|
38
|
+
export function formatGoaSystemGrounding(context) {
|
|
39
|
+
const parts = [
|
|
40
|
+
'VIGTHORIA GOA ORCHESTRATOR CONTEXT (pre-executed by Workbench — do not re-scaffold greenfield templates):',
|
|
41
|
+
`- workspace_type: ${context.workspace_type || 'unknown'}`,
|
|
42
|
+
];
|
|
43
|
+
if (context.executed === true) {
|
|
44
|
+
parts.push('- Workbench already executed the GOA payload (template write / AST pre-scan / stabilize).');
|
|
45
|
+
parts.push('- Continue from the current workspace state; prefer surgical edits over full rewrites.');
|
|
46
|
+
}
|
|
47
|
+
if (context.analysis && typeof context.analysis === 'object') {
|
|
48
|
+
const a = context.analysis;
|
|
49
|
+
if (typeof a.detected_framework === 'string')
|
|
50
|
+
parts.push(`- framework: ${a.detected_framework}`);
|
|
51
|
+
if (Array.isArray(a.target_files) && a.target_files.length) {
|
|
52
|
+
parts.push(`- target_files: ${a.target_files.slice(0, 6).join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
if (typeof a.anchor_hint === 'string')
|
|
55
|
+
parts.push(`- anchor: ${a.anchor_hint}`);
|
|
56
|
+
if (typeof a.template_match === 'string')
|
|
57
|
+
parts.push(`- template_match: ${a.template_match}`);
|
|
58
|
+
}
|
|
59
|
+
if (typeof context.instruction_count === 'number') {
|
|
60
|
+
parts.push(`- instructions_planned: ${context.instruction_count}`);
|
|
61
|
+
}
|
|
62
|
+
return parts.join('\n');
|
|
63
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect V3 agent SSE keepalive/heartbeat events that exist only to keep
|
|
3
|
+
* connections alive — not substantive work the CLI should surface as progress.
|
|
4
|
+
*/
|
|
5
|
+
const PLANNING_KEEPALIVE = /^Planning in progress\.\.\.\s*\(\d+s(?:\s+elapsed)?\)$/i;
|
|
6
|
+
const PARALLEL_KEEPALIVE = /^Parallel tasks in progress\.\.\.\s*\(\d+s(?:\s+elapsed)?\)$/i;
|
|
7
|
+
const GENERIC_ELAPSED_KEEPALIVE = /\bin progress\b.*\(\d+s(?:\s+elapsed)?\)$/i;
|
|
8
|
+
export function isV3StreamKeepaliveEvent(event) {
|
|
9
|
+
if (!event || typeof event !== 'object') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const record = event;
|
|
13
|
+
const type = typeof record.type === 'string' ? record.type : '';
|
|
14
|
+
if (type === 'heartbeat') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (type === 'progress' || type === 'status') {
|
|
18
|
+
const content = String(record.content || record.message || record.status || '').trim();
|
|
19
|
+
if (!content) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (PLANNING_KEEPALIVE.test(content) || PARALLEL_KEEPALIVE.test(content)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (GENERIC_ELAPSED_KEEPALIVE.test(content)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (type === 'plan') {
|
|
30
|
+
const plan = record.plan;
|
|
31
|
+
if (plan && typeof plan === 'object') {
|
|
32
|
+
const planRecord = plan;
|
|
33
|
+
const status = typeof planRecord.status === 'string' ? planRecord.status : '';
|
|
34
|
+
const tasks = planRecord.tasks;
|
|
35
|
+
if (status === 'planning' && !Array.isArray(tasks)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
@@ -42,6 +42,45 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
42
42
|
}
|
|
43
43
|
return null;
|
|
44
44
|
};
|
|
45
|
+
const stripScrubbedPlaceholder = (value) => {
|
|
46
|
+
let candidate = String(value || '').replace(/^\/+/, '');
|
|
47
|
+
// Hybrid paths models echo back after SSE scrubbing, e.g. C:/var/www/[internal]/game.js
|
|
48
|
+
const hybridWin = candidate.match(/^[a-zA-Z]:\/(?:var\/)?www\/\[internal\](?:\/(.*))?$/i);
|
|
49
|
+
if (hybridWin) {
|
|
50
|
+
return safeRelative(hybridWin[1] || '');
|
|
51
|
+
}
|
|
52
|
+
const hybridWinBare = candidate.match(/^[a-zA-Z]:\/\[internal\](?:\/(.*))?$/i);
|
|
53
|
+
if (hybridWinBare) {
|
|
54
|
+
return safeRelative(hybridWinBare[1] || '');
|
|
55
|
+
}
|
|
56
|
+
const placeholderPatterns = [
|
|
57
|
+
[/^(?:var\/)?www\/\[internal\](?:\/(.*))?$/i, 1],
|
|
58
|
+
[/^\[internal\](?:\/(.*))?$/i, 1],
|
|
59
|
+
[/^\[Vigthoria service\](?:\/(.*))?$/i, 1],
|
|
60
|
+
[/^\[Vigthoria Agent\](?:\/(.*))?$/i, 1],
|
|
61
|
+
[/^\[Vigthoria storage\](?:\/(.*))?$/i, 1],
|
|
62
|
+
[/^\[temp\](?:\/(.*))?$/i, 1],
|
|
63
|
+
[/^\[home\](?:\/(.*))?$/i, 1],
|
|
64
|
+
[/^\[workspace\](?:\/(.*))?$/i, 1],
|
|
65
|
+
[/^(?:var\/)?www\/\[Vigthoria service\](?:\/(.*))?$/i, 1],
|
|
66
|
+
];
|
|
67
|
+
for (const [pattern, groupIndex] of placeholderPatterns) {
|
|
68
|
+
const match = candidate.match(pattern);
|
|
69
|
+
if (match) {
|
|
70
|
+
return safeRelative(match[groupIndex] || '');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const bracketSplit = candidate.split(/\[(?:internal|Vigthoria service|Vigthoria Agent|temp|home|workspace)\]/i);
|
|
74
|
+
if (bracketSplit.length > 1) {
|
|
75
|
+
const tail = bracketSplit[bracketSplit.length - 1].replace(/^\/+/, '');
|
|
76
|
+
return safeRelative(tail);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
};
|
|
80
|
+
const scrubbed = stripScrubbedPlaceholder(input);
|
|
81
|
+
if (scrubbed !== null) {
|
|
82
|
+
return scrubbed;
|
|
83
|
+
}
|
|
45
84
|
const decoded = decodeBoundaryUri(input);
|
|
46
85
|
if (decoded !== null) {
|
|
47
86
|
return decoded;
|
|
@@ -72,6 +111,18 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
72
111
|
}
|
|
73
112
|
const normalizedRoot = String(rootPath || '').trim().replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
74
113
|
if (normalizedRoot) {
|
|
114
|
+
if (/^[a-zA-Z]:\//.test(input)) {
|
|
115
|
+
const winRoot = normalizedRoot;
|
|
116
|
+
const inputLower = input.toLowerCase();
|
|
117
|
+
const winRootLower = winRoot.toLowerCase();
|
|
118
|
+
if (inputLower === winRootLower || inputLower === `${winRootLower}/`) {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
if (inputLower.startsWith(`${winRootLower}/`)) {
|
|
122
|
+
return safeRelative(input.slice(winRoot.length + 1));
|
|
123
|
+
}
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
75
126
|
const rootNoLeadingSlash = normalizedRoot.replace(/^\//, '');
|
|
76
127
|
const rootBase = path.posix.basename(normalizedRoot);
|
|
77
128
|
const prefixes = [
|
|
@@ -90,7 +141,11 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
90
141
|
return safeRelative(input.slice(embeddedIndex + embeddedRoot.length));
|
|
91
142
|
}
|
|
92
143
|
}
|
|
93
|
-
|
|
144
|
+
const relative = safeRelative(input);
|
|
145
|
+
if (relative === 'workspace') {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
return relative;
|
|
94
149
|
}
|
|
95
150
|
/** Join workspace root + relative path without path.resolve URI corruption on Windows. */
|
|
96
151
|
export function joinV3WorkspacePath(rootPath, relativePath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigthoria-cli",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.36",
|
|
4
4
|
"description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dev": "ts-node src/index.ts",
|
|
38
38
|
"test": "npm run test:cli",
|
|
39
39
|
"test:agent:outcome": "npm run build && node scripts/test-agent-run-outcome.js",
|
|
40
|
-
"test:cli": "npm run build && npm run precheck:services && node scripts/test-cli-suite.js && npm run test:agent:outcome",
|
|
40
|
+
"test:cli": "npm run build && npm run precheck:services && node scripts/test-cli-suite.js && npm run test:agent:outcome && npm run test:v3-stream-mutation",
|
|
41
41
|
"test:regression": "npm run build && node scripts/test-regression-1.6.22.js",
|
|
42
42
|
"test:agent:smoke": "npm run build && node scripts/test-agent-smoke.js",
|
|
43
43
|
"test:agent:routing": "npm run build && node scripts/test-agent-routing-policy.js",
|
|
@@ -68,7 +68,11 @@
|
|
|
68
68
|
"test:model:governance": "npm run build && node scripts/test-model-governance-no-agent.js",
|
|
69
69
|
"validate:no-go": "bash scripts/release/validate-no-go-gates.sh",
|
|
70
70
|
"test:legion:billing:e2e": "npm run build && node scripts/test-legion-godmode-billing-e2e.js",
|
|
71
|
-
"test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js"
|
|
71
|
+
"test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js",
|
|
72
|
+
"test:v3-workspace-path": "npm run build && node scripts/test-v3-workspace-path.js",
|
|
73
|
+
"test:v3-stream-mutation": "npm run build && node scripts/test-v3-stream-mutation.js",
|
|
74
|
+
"test:context:budget": "npm run build && node scripts/test-context-budget.js",
|
|
75
|
+
"test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.js"
|
|
72
76
|
},
|
|
73
77
|
"keywords": [
|
|
74
78
|
"ai",
|