vigthoria-cli 1.11.31 → 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 +78 -11
- package/dist/utils/agentRunOutcome.d.ts +4 -0
- package/dist/utils/agentRunOutcome.js +55 -3
- package/dist/utils/api.d.ts +7 -1
- package/dist/utils/api.js +189 -58
- 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-workspace-path.js +10 -1
- package/package.json +6 -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,6 +17,7 @@ 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';
|
|
@@ -77,6 +78,7 @@ export class ChatCommand {
|
|
|
77
78
|
v3SeenToolResults = new Set();
|
|
78
79
|
v3StartSeen = false;
|
|
79
80
|
lastAgentRoute = null;
|
|
81
|
+
pendingGoaContext = null;
|
|
80
82
|
// Last completed Agent run — used by /retry, /continue, and the final summary block.
|
|
81
83
|
lastAgentRunOutcome = null;
|
|
82
84
|
isJwtExpirationError(error) {
|
|
@@ -782,6 +784,8 @@ export class ChatCommand {
|
|
|
782
784
|
v3IterationCount = 0;
|
|
783
785
|
v3ToolCallCount = 0;
|
|
784
786
|
v3LastActivity = Date.now();
|
|
787
|
+
v3IdleWatchInterval = null;
|
|
788
|
+
v3IdleNoticeShown = false;
|
|
785
789
|
v3StreamingStarted = false;
|
|
786
790
|
/**
|
|
787
791
|
* Strip server-internal path prefixes from tool output strings.
|
|
@@ -797,10 +801,41 @@ export class ChatCommand {
|
|
|
797
801
|
return text;
|
|
798
802
|
return text
|
|
799
803
|
.replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
|
|
800
|
-
.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, '')
|
|
801
808
|
.replace(/<\|(?:mask_start|mask_end)\|>/g, '')
|
|
802
809
|
.trim();
|
|
803
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
|
+
}
|
|
804
839
|
describeV3AgentTool(toolName) {
|
|
805
840
|
const normalized = String(toolName || '').toLowerCase();
|
|
806
841
|
if (/read|grep|search|list|find|glob/.test(normalized)) {
|
|
@@ -935,6 +970,7 @@ export class ChatCommand {
|
|
|
935
970
|
return '';
|
|
936
971
|
output = output.replace(/<\/think>/gi, '</thinking>');
|
|
937
972
|
output = output.replace(/<think>/gi, '<thinking>');
|
|
973
|
+
output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
|
|
938
974
|
if (this.v3SuppressThinkingStream) {
|
|
939
975
|
const closeIdx = output.search(/<\/thinking>/i);
|
|
940
976
|
if (closeIdx < 0) {
|
|
@@ -1095,6 +1131,13 @@ export class ChatCommand {
|
|
|
1095
1131
|
: (event.content || '');
|
|
1096
1132
|
if (text) {
|
|
1097
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
|
+
}
|
|
1098
1141
|
this.writeV3StreamText(spinner, text);
|
|
1099
1142
|
}
|
|
1100
1143
|
else {
|
|
@@ -1720,6 +1763,10 @@ export class ChatCommand {
|
|
|
1720
1763
|
return false;
|
|
1721
1764
|
}
|
|
1722
1765
|
async handleDirectPrompt(prompt) {
|
|
1766
|
+
const { goaContext, userPrompt } = splitGoaContextFromInput(prompt);
|
|
1767
|
+
if (goaContext)
|
|
1768
|
+
this.pendingGoaContext = goaContext;
|
|
1769
|
+
const effectivePrompt = userPrompt || prompt;
|
|
1723
1770
|
// Suppress all setup banners in direct-prompt mode so only the final
|
|
1724
1771
|
// answer reaches stdout. Interactive (REPL) mode still shows them.
|
|
1725
1772
|
if (!this.jsonOutput) {
|
|
@@ -1732,18 +1779,18 @@ export class ChatCommand {
|
|
|
1732
1779
|
console.log();
|
|
1733
1780
|
}
|
|
1734
1781
|
if (this.workflowTarget) {
|
|
1735
|
-
await this.runWorkflowTargetPrompt(
|
|
1782
|
+
await this.runWorkflowTargetPrompt(effectivePrompt);
|
|
1736
1783
|
return;
|
|
1737
1784
|
}
|
|
1738
1785
|
// Smart routing: for agent mode, determine if prompt needs tool access
|
|
1739
1786
|
if (this.agentMode) {
|
|
1740
|
-
if (this.isSimpleDirectPrompt(
|
|
1787
|
+
if (this.isSimpleDirectPrompt(effectivePrompt)) {
|
|
1741
1788
|
// Simple prompt: downgrade to plain chat model, no agent system prompt
|
|
1742
1789
|
this.currentModel = this.getDefaultChatModel();
|
|
1743
|
-
await this.runSimplePrompt(
|
|
1790
|
+
await this.runSimplePrompt(effectivePrompt);
|
|
1744
1791
|
}
|
|
1745
1792
|
else {
|
|
1746
|
-
await this.runAgentTurn(
|
|
1793
|
+
await this.runAgentTurn(effectivePrompt);
|
|
1747
1794
|
}
|
|
1748
1795
|
return;
|
|
1749
1796
|
}
|
|
@@ -1752,10 +1799,10 @@ export class ChatCommand {
|
|
|
1752
1799
|
this.logger.error(this.operatorAccessMessage());
|
|
1753
1800
|
return;
|
|
1754
1801
|
}
|
|
1755
|
-
await this.runOperatorTurn(
|
|
1802
|
+
await this.runOperatorTurn(effectivePrompt);
|
|
1756
1803
|
return;
|
|
1757
1804
|
}
|
|
1758
|
-
await this.runSimplePrompt(
|
|
1805
|
+
await this.runSimplePrompt(effectivePrompt);
|
|
1759
1806
|
}
|
|
1760
1807
|
formatWorkflowTargetResult(result) {
|
|
1761
1808
|
if (typeof result === 'string') {
|
|
@@ -2186,7 +2233,19 @@ export class ChatCommand {
|
|
|
2186
2233
|
if (!this.tools) {
|
|
2187
2234
|
throw new Error('Agent tools are not initialized.');
|
|
2188
2235
|
}
|
|
2189
|
-
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);
|
|
2190
2249
|
this.lastAgentRoute = await resolveAgentRoute(this.api, resolvedPrompt);
|
|
2191
2250
|
if (!this.jsonOutput) {
|
|
2192
2251
|
const r = this.lastAgentRoute;
|
|
@@ -2941,6 +3000,7 @@ export class ChatCommand {
|
|
|
2941
3000
|
spinner: 'clock',
|
|
2942
3001
|
isSilent: true,
|
|
2943
3002
|
}).start();
|
|
3003
|
+
this.startV3IdleWatch(spinner);
|
|
2944
3004
|
// Live run telemetry, used both for the final summary block and for /retry, /continue.
|
|
2945
3005
|
const liveOutcome = {
|
|
2946
3006
|
...createLiveOutcome(),
|
|
@@ -3207,8 +3267,8 @@ export class ChatCommand {
|
|
|
3207
3267
|
console.log(chalk.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
|
|
3208
3268
|
}
|
|
3209
3269
|
}
|
|
3210
|
-
// Show change summary
|
|
3211
|
-
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) {
|
|
3212
3272
|
const fileCount = changedFileCount;
|
|
3213
3273
|
if (fileCount > 0) {
|
|
3214
3274
|
console.log(chalk.gray(`\nFiles changed: ${fileCount}`));
|
|
@@ -3402,10 +3462,12 @@ export class ChatCommand {
|
|
|
3402
3462
|
}, null, 2));
|
|
3403
3463
|
}
|
|
3404
3464
|
this.messages.push({ role: 'assistant', content: finalAnswer || runEvaluation.statusHeadline });
|
|
3465
|
+
this.stopV3IdleWatch();
|
|
3405
3466
|
watcher?.stop();
|
|
3406
3467
|
return true;
|
|
3407
3468
|
}
|
|
3408
3469
|
catch (error) {
|
|
3470
|
+
this.stopV3IdleWatch();
|
|
3409
3471
|
watcher?.stop();
|
|
3410
3472
|
if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
|
|
3411
3473
|
const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
|
|
@@ -3619,7 +3681,12 @@ export class ChatCommand {
|
|
|
3619
3681
|
return lines.join('\n');
|
|
3620
3682
|
};
|
|
3621
3683
|
while (true) {
|
|
3622
|
-
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;
|
|
3623
3690
|
const trimmed = input.trim();
|
|
3624
3691
|
if (!trimmed) {
|
|
3625
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
|
}
|
|
@@ -252,6 +290,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
|
|
|
252
290
|
|| (liveOutcome.analysisToolsUsed >= 4
|
|
253
291
|
&& liveOutcome.analysisReadToolsUsed >= 3
|
|
254
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
|
+
}
|
|
255
300
|
if (hasFatalError && !hasSubstantiveAnswer) {
|
|
256
301
|
return {
|
|
257
302
|
executorSucceeded: false,
|
|
@@ -293,6 +338,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
|
|
|
293
338
|
uiTheme: 'success',
|
|
294
339
|
};
|
|
295
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
|
+
}
|
|
296
348
|
const legacySuccess = liveOutcome.workspaceHasOutput && liveOutcome.changedFileCount > 0;
|
|
297
349
|
return {
|
|
298
350
|
executorSucceeded: legacySuccess && !hasFatalError,
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -323,8 +323,11 @@ export declare class APIClient {
|
|
|
323
323
|
executionOptions?: Record<string, unknown>;
|
|
324
324
|
}): Promise<VigFlowExecutionResult>;
|
|
325
325
|
getVigFlowExecutionStatus(executionId: string): Promise<VigFlowExecutionStatus>;
|
|
326
|
-
/**
|
|
326
|
+
/** Legacy cap — prefer resolveV3ContextCharLimit() for token-aligned budgets. */
|
|
327
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;
|
|
328
331
|
buildV3AgentContext(context?: Record<string, any>): string;
|
|
329
332
|
/**
|
|
330
333
|
* Compact a V3 context payload so the serialized JSON stays under
|
|
@@ -336,6 +339,9 @@ export declare class APIClient {
|
|
|
336
339
|
* 5. Drop readmeExcerpt
|
|
337
340
|
*/
|
|
338
341
|
private compactV3Context;
|
|
342
|
+
/** Normalize common Unix-only commands before Windows cmd/powershell execution. */
|
|
343
|
+
private normalizeWindowsRunCommand;
|
|
344
|
+
private resolveRunCommandCwd;
|
|
339
345
|
/** Remove duplicate functionIndex entries that poison model context after compaction. */
|
|
340
346
|
private dedupeBrainPayload;
|
|
341
347
|
buildMinimalV3AgentContext(context?: Record<string, any>): string;
|
package/dist/utils/api.js
CHANGED
|
@@ -15,6 +15,7 @@ import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOu
|
|
|
15
15
|
import { joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
|
|
16
16
|
import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
|
|
17
17
|
import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
|
|
18
|
+
import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
|
|
18
19
|
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
|
|
19
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.';
|
|
20
21
|
export class CLIError extends Error {
|
|
@@ -1449,8 +1450,41 @@ export class APIClient {
|
|
|
1449
1450
|
return payload.execution;
|
|
1450
1451
|
});
|
|
1451
1452
|
}
|
|
1452
|
-
/**
|
|
1453
|
-
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
|
+
}
|
|
1454
1488
|
buildV3AgentContext(context = {}) {
|
|
1455
1489
|
const resolvedContext = this.ensureExecutionContext(context);
|
|
1456
1490
|
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
@@ -1558,7 +1592,16 @@ export class APIClient {
|
|
|
1558
1592
|
* 5. Drop readmeExcerpt
|
|
1559
1593
|
*/
|
|
1560
1594
|
compactV3Context(payload) {
|
|
1561
|
-
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
|
+
};
|
|
1562
1605
|
if (payload.vigthoriaBrain) {
|
|
1563
1606
|
payload.vigthoriaBrain = this.dedupeBrainPayload(payload.vigthoriaBrain);
|
|
1564
1607
|
}
|
|
@@ -1566,80 +1609,155 @@ export class APIClient {
|
|
|
1566
1609
|
payload.vigthoria_brain = payload.vigthoriaBrain || this.dedupeBrainPayload(payload.vigthoria_brain);
|
|
1567
1610
|
}
|
|
1568
1611
|
let json = JSON.stringify(payload);
|
|
1569
|
-
|
|
1612
|
+
compactionMeta.beforeChars = json.length;
|
|
1613
|
+
if (json.length <= LIMIT) {
|
|
1614
|
+
compactionMeta.afterChars = json.length;
|
|
1615
|
+
payload.contextBudgetMeta = compactionMeta;
|
|
1570
1616
|
return json;
|
|
1617
|
+
}
|
|
1618
|
+
compactionMeta.applied = true;
|
|
1619
|
+
phases.push('over_limit');
|
|
1571
1620
|
if (!/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_QUIET_CONTEXT_COMPACTION || ''))) {
|
|
1572
|
-
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`);
|
|
1573
1622
|
}
|
|
1574
1623
|
if (process.env.DEBUG || process.env.VIGTHORIA_DEBUG) {
|
|
1575
1624
|
process.stderr.write(`[context] Payload ${json.length} chars exceeds ${LIMIT} limit, compacting...\n`);
|
|
1576
1625
|
}
|
|
1577
|
-
// Phase 1 — shrink workspaceFiles to fit
|
|
1578
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
|
|
1579
1642
|
if (summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
|
|
1580
|
-
const fileEntries = Object.entries(summary.workspaceFiles);
|
|
1581
1643
|
const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
|
|
1582
|
-
const budget = LIMIT - overhead -
|
|
1644
|
+
const budget = LIMIT - overhead - 1024;
|
|
1583
1645
|
if (budget > 0) {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
trimmed[k] = v;
|
|
1591
|
-
used += entryLen;
|
|
1592
|
-
}
|
|
1593
|
-
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';
|
|
1594
1652
|
}
|
|
1595
1653
|
else {
|
|
1596
1654
|
delete summary.workspaceFiles;
|
|
1597
1655
|
}
|
|
1598
1656
|
json = JSON.stringify(payload);
|
|
1599
1657
|
if (json.length <= LIMIT)
|
|
1600
|
-
return
|
|
1658
|
+
return finish();
|
|
1659
|
+
}
|
|
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();
|
|
1601
1666
|
}
|
|
1602
|
-
// Phase
|
|
1603
|
-
if (summary?.workspaceFiles) {
|
|
1667
|
+
// Phase 4 — never fully drop workspaceFiles when remote hydration is required
|
|
1668
|
+
if (summary?.workspaceFiles && !hydrationRequired) {
|
|
1604
1669
|
delete summary.workspaceFiles;
|
|
1605
1670
|
json = JSON.stringify(payload);
|
|
1606
1671
|
if (json.length <= LIMIT)
|
|
1607
|
-
return
|
|
1672
|
+
return finish();
|
|
1608
1673
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1674
|
+
if (hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
|
|
1675
|
+
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 4_000);
|
|
1676
|
+
summary.workspaceFilesCompaction = 'priority-minimal';
|
|
1612
1677
|
json = JSON.stringify(payload);
|
|
1613
1678
|
if (json.length <= LIMIT)
|
|
1614
|
-
return
|
|
1679
|
+
return finish();
|
|
1615
1680
|
}
|
|
1616
|
-
// Phase
|
|
1617
|
-
if (
|
|
1618
|
-
|
|
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);
|
|
1619
1684
|
json = JSON.stringify(payload);
|
|
1620
1685
|
if (json.length <= LIMIT)
|
|
1621
|
-
return
|
|
1686
|
+
return finish();
|
|
1622
1687
|
}
|
|
1623
|
-
// Phase
|
|
1624
|
-
if (summary?.
|
|
1625
|
-
|
|
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);
|
|
1626
1691
|
json = JSON.stringify(payload);
|
|
1627
1692
|
if (json.length <= LIMIT)
|
|
1628
|
-
return
|
|
1693
|
+
return finish();
|
|
1629
1694
|
}
|
|
1630
|
-
// Phase
|
|
1695
|
+
// Phase 7 — drop history entirely
|
|
1631
1696
|
if (Array.isArray(payload.history) && payload.history.length > 0) {
|
|
1632
1697
|
payload.history = [];
|
|
1633
1698
|
json = JSON.stringify(payload);
|
|
1634
1699
|
if (json.length <= LIMIT)
|
|
1635
|
-
return
|
|
1700
|
+
return finish();
|
|
1636
1701
|
}
|
|
1637
|
-
// Phase
|
|
1702
|
+
// Phase 8 — drop localWorkspaceSummary metadata but keep hydration files for remote clients
|
|
1638
1703
|
if (payload.localWorkspaceSummary) {
|
|
1639
|
-
|
|
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
|
+
};
|
|
1640
1714
|
json = JSON.stringify(payload);
|
|
1641
1715
|
}
|
|
1642
|
-
|
|
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;
|
|
1643
1761
|
}
|
|
1644
1762
|
/** Remove duplicate functionIndex entries that poison model context after compaction. */
|
|
1645
1763
|
dedupeBrainPayload(brain) {
|
|
@@ -2674,18 +2792,8 @@ menu {
|
|
|
2674
2792
|
}
|
|
2675
2793
|
return;
|
|
2676
2794
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
if (typeof content !== 'string') {
|
|
2680
|
-
continue;
|
|
2681
|
-
}
|
|
2682
|
-
const filePath = this.normalizeAgentWorkspaceRelativePath(rawPath, serverRoot || undefined);
|
|
2683
|
-
if (filePath) {
|
|
2684
|
-
streamedFiles[filePath] = content;
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
return;
|
|
2688
|
-
}
|
|
2795
|
+
// workspace_snapshot is handled by applyV3AgentStreamEventToWorkspace only —
|
|
2796
|
+
// never count hydrated baselines as "files changed".
|
|
2689
2797
|
// Only count confirmed mutations — not workspace snapshots (hydrated baseline)
|
|
2690
2798
|
// or tool_call previews that may still fail client-side / read-only guards.
|
|
2691
2799
|
if (event.type === 'tool_result' && event.success) {
|
|
@@ -2796,7 +2904,12 @@ menu {
|
|
|
2796
2904
|
const name = String(event.name || event.tool || '').trim();
|
|
2797
2905
|
const args = event.arguments || {};
|
|
2798
2906
|
const serverRoot = String(context.__v3ServerWorkspaceRoot || '').trim();
|
|
2799
|
-
const
|
|
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
|
+
}
|
|
2800
2913
|
if (!target) {
|
|
2801
2914
|
return { success: false, output: '', error: 'Tool path is outside the workspace.' };
|
|
2802
2915
|
}
|
|
@@ -2911,16 +3024,34 @@ menu {
|
|
|
2911
3024
|
let command = String(args.command || '').trim();
|
|
2912
3025
|
if (!command)
|
|
2913
3026
|
return { success: false, output: '', error: 'run_command requires command.' };
|
|
2914
|
-
|
|
2915
|
-
|
|
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 };
|
|
2916
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);
|
|
2917
3038
|
const { exec } = await import('child_process');
|
|
2918
3039
|
return await new Promise((resolve) => {
|
|
2919
|
-
|
|
2920
|
-
cwd
|
|
3040
|
+
const execOptions = {
|
|
3041
|
+
cwd,
|
|
2921
3042
|
timeout: Number(args.timeout || 30000),
|
|
2922
|
-
|
|
2923
|
-
|
|
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) => {
|
|
2924
3055
|
resolve({
|
|
2925
3056
|
success: !error,
|
|
2926
3057
|
output: String(stdout || stderr || '').slice(0, 12000),
|
|
@@ -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
|
+
}
|
|
@@ -43,7 +43,16 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
43
43
|
return null;
|
|
44
44
|
};
|
|
45
45
|
const stripScrubbedPlaceholder = (value) => {
|
|
46
|
-
|
|
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
|
+
}
|
|
47
56
|
const placeholderPatterns = [
|
|
48
57
|
[/^(?:var\/)?www\/\[internal\](?:\/(.*))?$/i, 1],
|
|
49
58
|
[/^\[internal\](?:\/(.*))?$/i, 1],
|
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",
|
|
@@ -69,7 +69,10 @@
|
|
|
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
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"
|
|
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"
|
|
73
76
|
},
|
|
74
77
|
"keywords": [
|
|
75
78
|
"ai",
|