vigthoria-cli 1.10.50 → 1.10.55
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/background.d.ts +19 -0
- package/dist/commands/background.js +160 -0
- package/dist/commands/chat.js +89 -6
- package/dist/index.js +55 -2
- package/dist/utils/api.d.ts +30 -0
- package/dist/utils/api.js +624 -160
- package/package.json +1 -1
package/dist/utils/api.js
CHANGED
|
@@ -9,6 +9,8 @@ import https from 'https';
|
|
|
9
9
|
import net from 'net';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import WebSocket from 'ws';
|
|
12
|
+
import { buildSemanticContext } from './context-ranker.js';
|
|
13
|
+
import { getChangedFiles } from './workspace-cache.js';
|
|
12
14
|
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
|
|
13
15
|
export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
|
|
14
16
|
export class CLIError extends Error {
|
|
@@ -282,6 +284,11 @@ export class APIClient {
|
|
|
282
284
|
vigFlowTokens = new Map();
|
|
283
285
|
_httpsAgent = null;
|
|
284
286
|
lastChatTransportErrors = [];
|
|
287
|
+
clientToolErrors = [];
|
|
288
|
+
pendingV3ClientToolTasks = new Set();
|
|
289
|
+
/** Tracks files mutated via client_tool_request during an active V3 SSE stream. */
|
|
290
|
+
activeV3StreamedFiles = null;
|
|
291
|
+
activeV3StreamContext = null;
|
|
285
292
|
constructor(config, logger) {
|
|
286
293
|
this.config = config;
|
|
287
294
|
this.logger = logger;
|
|
@@ -631,6 +638,13 @@ export class APIClient {
|
|
|
631
638
|
}
|
|
632
639
|
return `${baseUrl}/api/v3-agent/continue`;
|
|
633
640
|
}
|
|
641
|
+
getV3AgentBackgroundUrl(baseUrl, suffix = '') {
|
|
642
|
+
const cleanSuffix = suffix ? `/${suffix.replace(/^\/+/, '')}` : '';
|
|
643
|
+
if (/127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)) {
|
|
644
|
+
return `${baseUrl}/api/agent/background${cleanSuffix}`;
|
|
645
|
+
}
|
|
646
|
+
return `${baseUrl}/api/v3-agent/background${cleanSuffix}`;
|
|
647
|
+
}
|
|
634
648
|
getOperatorBaseUrls() {
|
|
635
649
|
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
636
650
|
const urls = [
|
|
@@ -720,7 +734,7 @@ export class APIClient {
|
|
|
720
734
|
continue;
|
|
721
735
|
}
|
|
722
736
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
723
|
-
if (entry.name === '.git' || entry.name === 'node_modules') {
|
|
737
|
+
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.vigthoria') {
|
|
724
738
|
continue;
|
|
725
739
|
}
|
|
726
740
|
const fullPath = path.join(current, entry.name);
|
|
@@ -1410,11 +1424,18 @@ export class APIClient {
|
|
|
1410
1424
|
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
1411
1425
|
const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
|
|
1412
1426
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1413
|
-
const
|
|
1427
|
+
const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
|
|
1428
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
|
|
1414
1429
|
const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
|
|
1415
1430
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
1416
1431
|
const localWorkspaceName = this.getDisplayWorkspaceName(localWorkspacePath);
|
|
1417
1432
|
const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
|
|
1433
|
+
const executionSurface = String(resolvedContext.executionSurface || resolvedContext.clientSurface || 'cli');
|
|
1434
|
+
const localMachineCapable = resolvedContext.localMachineCapable !== false;
|
|
1435
|
+
const clientToolExecution = resolvedContext.clientToolExecution === false
|
|
1436
|
+
? false
|
|
1437
|
+
: (resolvedContext.clientToolExecution === true
|
|
1438
|
+
|| (localMachineCapable && ['cli', 'fork', 'local-ide', 'desktop', 'local'].includes(executionSurface)));
|
|
1418
1439
|
const publicRuntimeEnvironment = this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
|
|
1419
1440
|
localWorkspacePath,
|
|
1420
1441
|
serverWorkspacePath,
|
|
@@ -1432,6 +1453,15 @@ export class APIClient {
|
|
|
1432
1453
|
activeFile: resolvedContext.activeFile || null,
|
|
1433
1454
|
history: resolvedContext.history || [],
|
|
1434
1455
|
agentTaskType: resolvedContext.agentTaskType || 'general',
|
|
1456
|
+
rawPrompt: resolvedContext.rawPrompt || resolvedContext.prompt || '',
|
|
1457
|
+
contextualPrompt: resolvedContext.contextualPrompt || '',
|
|
1458
|
+
executionHints: {
|
|
1459
|
+
task_kind: resolvedContext.agentTaskType || 'general',
|
|
1460
|
+
requires_file_changes: resolvedContext.agentTaskType === 'analysis' || resolvedContext.agentTaskType === 'verification'
|
|
1461
|
+
? false
|
|
1462
|
+
: undefined,
|
|
1463
|
+
classify_from: resolvedContext.rawPrompt || resolvedContext.prompt || '',
|
|
1464
|
+
},
|
|
1435
1465
|
model: resolvedModel,
|
|
1436
1466
|
requestedModel,
|
|
1437
1467
|
requestedModelResolved: resolvedModel,
|
|
@@ -1439,7 +1469,9 @@ export class APIClient {
|
|
|
1439
1469
|
legacyFallbackAllowed: resolvedContext.legacyFallbackAllowed === true,
|
|
1440
1470
|
executionSurface: resolvedContext.executionSurface || 'cli',
|
|
1441
1471
|
clientSurface: resolvedContext.clientSurface || 'cli',
|
|
1442
|
-
localMachineCapable
|
|
1472
|
+
localMachineCapable,
|
|
1473
|
+
clientToolExecution,
|
|
1474
|
+
client_tool_execution: clientToolExecution,
|
|
1443
1475
|
runtimeEnvironment: publicRuntimeEnvironment,
|
|
1444
1476
|
workspacePath: effectiveWorkspacePath,
|
|
1445
1477
|
projectPath: effectiveWorkspacePath,
|
|
@@ -1462,6 +1494,11 @@ export class APIClient {
|
|
|
1462
1494
|
requestStartedAt: resolvedContext.requestStartedAt,
|
|
1463
1495
|
subscriptionPlan: this.config.getNormalizedPlan() || null,
|
|
1464
1496
|
email: this.config.get('email') || null,
|
|
1497
|
+
vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
|
|
1498
|
+
vigthoria_brain: resolvedContext.vigthoriaBrain || null,
|
|
1499
|
+
codebaseContext: resolvedContext.codebaseContext || '',
|
|
1500
|
+
accountBrainContext: resolvedContext.accountBrainContext || '',
|
|
1501
|
+
projectMemory: resolvedContext.projectMemory || '',
|
|
1465
1502
|
};
|
|
1466
1503
|
return this.compactV3Context(payload);
|
|
1467
1504
|
}
|
|
@@ -1568,6 +1605,15 @@ export class APIClient {
|
|
|
1568
1605
|
activeFile: resolvedContext.activeFile || null,
|
|
1569
1606
|
history: resolvedContext.history || [],
|
|
1570
1607
|
agentTaskType: resolvedContext.agentTaskType || 'general',
|
|
1608
|
+
rawPrompt: resolvedContext.rawPrompt || resolvedContext.prompt || '',
|
|
1609
|
+
contextualPrompt: resolvedContext.contextualPrompt || '',
|
|
1610
|
+
executionHints: {
|
|
1611
|
+
task_kind: resolvedContext.agentTaskType || 'general',
|
|
1612
|
+
requires_file_changes: resolvedContext.agentTaskType === 'analysis' || resolvedContext.agentTaskType === 'verification'
|
|
1613
|
+
? false
|
|
1614
|
+
: undefined,
|
|
1615
|
+
classify_from: resolvedContext.rawPrompt || resolvedContext.prompt || '',
|
|
1616
|
+
},
|
|
1571
1617
|
executionSurface: resolvedContext.executionSurface || 'cli',
|
|
1572
1618
|
clientSurface: resolvedContext.clientSurface || 'cli',
|
|
1573
1619
|
localMachineCapable: resolvedContext.localMachineCapable !== false,
|
|
@@ -1584,6 +1630,8 @@ export class APIClient {
|
|
|
1584
1630
|
contextId: resolvedContext.contextId,
|
|
1585
1631
|
traceId: resolvedContext.traceId,
|
|
1586
1632
|
requestStartedAt: resolvedContext.requestStartedAt,
|
|
1633
|
+
vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
|
|
1634
|
+
vigthoria_brain: resolvedContext.vigthoriaBrain || null,
|
|
1587
1635
|
});
|
|
1588
1636
|
}
|
|
1589
1637
|
extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
|
|
@@ -2057,7 +2105,7 @@ menu {
|
|
|
2057
2105
|
const headers = await this.getMcpHeaders();
|
|
2058
2106
|
const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
|
|
2059
2107
|
const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
|
|
2060
|
-
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
2108
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, String(executionContext.rawPrompt || executionContext.contextualPrompt || executionContext.prompt || ''));
|
|
2061
2109
|
const metadata = {
|
|
2062
2110
|
source: 'vigthoria-cli',
|
|
2063
2111
|
sharedContextId: executionContext.contextId,
|
|
@@ -2218,7 +2266,7 @@ menu {
|
|
|
2218
2266
|
cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
|
|
2219
2267
|
};
|
|
2220
2268
|
}
|
|
2221
|
-
buildLocalWorkspaceSummary(rootPath) {
|
|
2269
|
+
buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
|
|
2222
2270
|
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
2223
2271
|
return null;
|
|
2224
2272
|
}
|
|
@@ -2229,8 +2277,31 @@ menu {
|
|
|
2229
2277
|
files: [],
|
|
2230
2278
|
};
|
|
2231
2279
|
const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
|
|
2280
|
+
const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
|
|
2281
|
+
let orderedPaths = candidatePaths;
|
|
2282
|
+
try {
|
|
2283
|
+
const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
|
|
2284
|
+
const changes = getChangedFiles(rootPath, candidatePaths);
|
|
2285
|
+
const ordered = new Set();
|
|
2286
|
+
for (const filePath of changes.changed)
|
|
2287
|
+
ordered.add(filePath);
|
|
2288
|
+
for (const filePath of ranked)
|
|
2289
|
+
ordered.add(filePath);
|
|
2290
|
+
for (const filePath of changes.unchanged)
|
|
2291
|
+
ordered.add(filePath);
|
|
2292
|
+
orderedPaths = Array.from(ordered);
|
|
2293
|
+
summary.semanticRank = ranked.slice(0, 20);
|
|
2294
|
+
summary.changeSummary = {
|
|
2295
|
+
changed: changes.changed.length,
|
|
2296
|
+
unchanged: changes.unchanged.length,
|
|
2297
|
+
total: changes.total,
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
catch {
|
|
2301
|
+
orderedPaths = candidatePaths;
|
|
2302
|
+
}
|
|
2232
2303
|
summary.fileCount = snapshot.fileCount;
|
|
2233
|
-
summary.files =
|
|
2304
|
+
summary.files = orderedPaths.slice(0, 40);
|
|
2234
2305
|
const packageJsonPath = path.join(rootPath, 'package.json');
|
|
2235
2306
|
if (fs.existsSync(packageJsonPath)) {
|
|
2236
2307
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
@@ -2248,7 +2319,7 @@ menu {
|
|
|
2248
2319
|
}
|
|
2249
2320
|
// Hydrate workspace: include actual file contents so the V3 server
|
|
2250
2321
|
// can populate the remote workspace before the agent starts.
|
|
2251
|
-
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath,
|
|
2322
|
+
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
|
|
2252
2323
|
return summary;
|
|
2253
2324
|
}
|
|
2254
2325
|
catch (error) {
|
|
@@ -2284,6 +2355,8 @@ menu {
|
|
|
2284
2355
|
continue;
|
|
2285
2356
|
if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
|
|
2286
2357
|
continue;
|
|
2358
|
+
if (/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
|
|
2359
|
+
continue;
|
|
2287
2360
|
const absolutePath = path.join(rootPath, relativePath);
|
|
2288
2361
|
try {
|
|
2289
2362
|
const stat = fs.statSync(absolutePath);
|
|
@@ -2463,6 +2536,19 @@ menu {
|
|
|
2463
2536
|
return;
|
|
2464
2537
|
}
|
|
2465
2538
|
if (event.type !== 'tool_call') {
|
|
2539
|
+
if (event.type === 'tool_result' && event.success) {
|
|
2540
|
+
const name = String(event.name || event.tool || '').trim();
|
|
2541
|
+
const args = event.arguments || {};
|
|
2542
|
+
const targetPath = typeof event.target === 'string'
|
|
2543
|
+
? event.target
|
|
2544
|
+
: (typeof args.path === 'string' ? args.path : '');
|
|
2545
|
+
if ((name === 'write_file' || name === 'edit_file') && targetPath) {
|
|
2546
|
+
const filePath = this.normalizeAgentWorkspaceRelativePath(targetPath, serverRoot || undefined);
|
|
2547
|
+
if (filePath && typeof args.content === 'string') {
|
|
2548
|
+
streamedFiles[filePath] = args.content;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2466
2552
|
return;
|
|
2467
2553
|
}
|
|
2468
2554
|
const args = event.arguments || {};
|
|
@@ -2515,6 +2601,178 @@ menu {
|
|
|
2515
2601
|
}
|
|
2516
2602
|
return false;
|
|
2517
2603
|
}
|
|
2604
|
+
resolveV3ClientToolPath(rootPath, rawPath) {
|
|
2605
|
+
const relativePath = this.normalizeAgentWorkspaceRelativePath(String(rawPath || '.'), rootPath);
|
|
2606
|
+
if (!relativePath && String(rawPath || '.').trim() !== '.') {
|
|
2607
|
+
return null;
|
|
2608
|
+
}
|
|
2609
|
+
const absolutePath = path.resolve(rootPath, relativePath || '.');
|
|
2610
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
2611
|
+
if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
return { relativePath: relativePath || '.', absolutePath };
|
|
2615
|
+
}
|
|
2616
|
+
recordV3ClientToolMutation(event, context, result) {
|
|
2617
|
+
if (!result.success) {
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
const name = String(event.name || event.tool || '').trim();
|
|
2621
|
+
if (name !== 'write_file' && name !== 'edit_file') {
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
const args = event.arguments || {};
|
|
2625
|
+
const rootPath = this.resolveAgentTargetPath(context);
|
|
2626
|
+
if (!rootPath) {
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
const target = this.resolveV3ClientToolPath(rootPath, args.path || '.');
|
|
2630
|
+
if (!target) {
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
const bucket = this.activeV3StreamedFiles || context.__v3StreamedFiles;
|
|
2634
|
+
if (!bucket) {
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
if (name === 'write_file' && typeof args.content === 'string') {
|
|
2638
|
+
bucket[target.relativePath] = args.content;
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
if (name === 'edit_file') {
|
|
2642
|
+
try {
|
|
2643
|
+
bucket[target.relativePath] = fs.readFileSync(target.absolutePath, 'utf8');
|
|
2644
|
+
}
|
|
2645
|
+
catch {
|
|
2646
|
+
// ignore read failures; success was already reported by the client tool
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
async executeV3ClientToolRequest(event, context = {}) {
|
|
2651
|
+
const rootPath = this.resolveAgentTargetPath(context);
|
|
2652
|
+
if (!rootPath) {
|
|
2653
|
+
return { success: false, output: '', error: 'No local workspace path is available for client tool execution.' };
|
|
2654
|
+
}
|
|
2655
|
+
const name = String(event.name || event.tool || '').trim();
|
|
2656
|
+
const args = event.arguments || {};
|
|
2657
|
+
const target = this.resolveV3ClientToolPath(rootPath, args.path || args.cwd || '.');
|
|
2658
|
+
if (!target) {
|
|
2659
|
+
return { success: false, output: '', error: 'Tool path is outside the workspace.' };
|
|
2660
|
+
}
|
|
2661
|
+
try {
|
|
2662
|
+
if (name === 'write_file') {
|
|
2663
|
+
if (typeof args.content !== 'string')
|
|
2664
|
+
return { success: false, output: '', error: 'write_file requires string content.' };
|
|
2665
|
+
fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
|
|
2666
|
+
fs.writeFileSync(target.absolutePath, args.content, 'utf8');
|
|
2667
|
+
const result = { success: true, output: `Updated ${target.relativePath}` };
|
|
2668
|
+
this.recordV3ClientToolMutation(event, context, result);
|
|
2669
|
+
return result;
|
|
2670
|
+
}
|
|
2671
|
+
if (name === 'edit_file') {
|
|
2672
|
+
const oldString = typeof args.old_string === 'string' ? args.old_string : String(args.old_text || '');
|
|
2673
|
+
const newString = typeof args.new_string === 'string' ? args.new_string : String(args.new_text || '');
|
|
2674
|
+
if (!oldString)
|
|
2675
|
+
return { success: false, output: '', error: 'edit_file requires old_string.' };
|
|
2676
|
+
const existing = fs.readFileSync(target.absolutePath, 'utf8');
|
|
2677
|
+
const count = existing.split(oldString).length - 1;
|
|
2678
|
+
if (count !== 1)
|
|
2679
|
+
return { success: false, output: '', error: `old_string found ${count} times; expected exactly once.` };
|
|
2680
|
+
fs.writeFileSync(target.absolutePath, existing.replace(oldString, newString), 'utf8');
|
|
2681
|
+
const result = { success: true, output: `Edited ${target.relativePath}` };
|
|
2682
|
+
this.recordV3ClientToolMutation(event, context, result);
|
|
2683
|
+
return result;
|
|
2684
|
+
}
|
|
2685
|
+
if (name === 'read_file') {
|
|
2686
|
+
return { success: true, output: fs.readFileSync(target.absolutePath, 'utf8') };
|
|
2687
|
+
}
|
|
2688
|
+
if (name === 'list_directory') {
|
|
2689
|
+
const entries = fs.readdirSync(target.absolutePath, { withFileTypes: true })
|
|
2690
|
+
.filter((entry) => !['node_modules', '.git'].includes(entry.name))
|
|
2691
|
+
.map((entry) => `${entry.isDirectory() ? '[DIR] ' : ''}${entry.name}`);
|
|
2692
|
+
return { success: true, output: entries.join('\n') || '(empty)' };
|
|
2693
|
+
}
|
|
2694
|
+
if (name === 'glob' || name === 'search_files') {
|
|
2695
|
+
const pattern = String(args.pattern || args.query || '').toLowerCase();
|
|
2696
|
+
const files = [];
|
|
2697
|
+
const walk = (dir) => {
|
|
2698
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2699
|
+
if (['node_modules', '.git'].includes(entry.name))
|
|
2700
|
+
continue;
|
|
2701
|
+
const absolute = path.join(dir, entry.name);
|
|
2702
|
+
if (entry.isDirectory()) {
|
|
2703
|
+
walk(absolute);
|
|
2704
|
+
continue;
|
|
2705
|
+
}
|
|
2706
|
+
const relative = path.relative(rootPath, absolute).replace(/\\/g, '/');
|
|
2707
|
+
if (!pattern || relative.toLowerCase().includes(pattern.replace(/\*/g, ''))) {
|
|
2708
|
+
files.push(relative);
|
|
2709
|
+
}
|
|
2710
|
+
else if (name === 'search_files') {
|
|
2711
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
2712
|
+
if (content.toLowerCase().includes(pattern))
|
|
2713
|
+
files.push(relative);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
};
|
|
2717
|
+
walk(fs.statSync(target.absolutePath).isDirectory() ? target.absolutePath : rootPath);
|
|
2718
|
+
return { success: true, output: files.slice(0, 200).join('\n') || '(no matches)' };
|
|
2719
|
+
}
|
|
2720
|
+
if (name === 'syntax_check') {
|
|
2721
|
+
const content = fs.readFileSync(target.absolutePath, 'utf8');
|
|
2722
|
+
if (/\.json$/i.test(target.absolutePath))
|
|
2723
|
+
JSON.parse(content);
|
|
2724
|
+
return { success: true, output: `Syntax check passed: ${target.relativePath}` };
|
|
2725
|
+
}
|
|
2726
|
+
if (name === 'run_command') {
|
|
2727
|
+
const command = String(args.command || '').trim();
|
|
2728
|
+
if (!command)
|
|
2729
|
+
return { success: false, output: '', error: 'run_command requires command.' };
|
|
2730
|
+
const { exec } = await import('child_process');
|
|
2731
|
+
return await new Promise((resolve) => {
|
|
2732
|
+
exec(command, { cwd: target.absolutePath, timeout: Number(args.timeout || 30000) }, (error, stdout, stderr) => {
|
|
2733
|
+
resolve({
|
|
2734
|
+
success: !error,
|
|
2735
|
+
output: String(stdout || stderr || '').slice(0, 12000),
|
|
2736
|
+
error: error ? String(stderr || error.message) : '',
|
|
2737
|
+
});
|
|
2738
|
+
});
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
return { success: false, output: '', error: `Unsupported client tool: ${name}` };
|
|
2742
|
+
}
|
|
2743
|
+
catch (error) {
|
|
2744
|
+
return { success: false, output: '', error: error.message };
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
async handleV3ClientToolRequest(event, context = {}, responseUrl) {
|
|
2748
|
+
const contextId = String(event.context_id || context.contextId || '').trim();
|
|
2749
|
+
const callId = String(event.call_id || '').trim();
|
|
2750
|
+
const toolName = String(event.tool || event.name || 'client_tool').trim();
|
|
2751
|
+
if (!contextId || !callId)
|
|
2752
|
+
return;
|
|
2753
|
+
const backendUrl = responseUrl ? new URL(responseUrl).origin : (context.mcpContextBackendUrl || null);
|
|
2754
|
+
try {
|
|
2755
|
+
const result = await this.executeV3ClientToolRequest(event, context);
|
|
2756
|
+
try {
|
|
2757
|
+
await this.submitClientToolResult(contextId, callId, result, backendUrl);
|
|
2758
|
+
}
|
|
2759
|
+
catch (submitError) {
|
|
2760
|
+
const submitMessage = submitError.message || String(submitError);
|
|
2761
|
+
if (result.success) {
|
|
2762
|
+
this.recordClientToolError(`Local ${toolName} succeeded on your machine, but the Vigthoria agent could not accept the result (${submitMessage}). Your workspace may not have synced to the server.`);
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
throw submitError;
|
|
2766
|
+
}
|
|
2767
|
+
if (!result.success && result.error) {
|
|
2768
|
+
this.recordClientToolError(`Local ${toolName} failed: ${result.error}`);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
catch (error) {
|
|
2772
|
+
this.recordClientToolError(`Local tool bridge failed for ${toolName}: ${error.message}`);
|
|
2773
|
+
throw error;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2518
2776
|
writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
|
|
2519
2777
|
const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, sourceRoot || rootPath);
|
|
2520
2778
|
if (!relativePath) {
|
|
@@ -3036,6 +3294,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3036
3294
|
.replace(/<\/think>/gi, '</thinking>')
|
|
3037
3295
|
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
|
|
3038
3296
|
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
|
3297
|
+
.replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
|
|
3039
3298
|
.replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
|
|
3040
3299
|
.replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
|
|
3041
3300
|
.replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
|
|
@@ -3128,10 +3387,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3128
3387
|
};
|
|
3129
3388
|
return sanitizeValue(event);
|
|
3130
3389
|
}
|
|
3390
|
+
resolveAgentChangedFiles(data) {
|
|
3391
|
+
const streamed = data?.files && typeof data.files === 'object' ? data.files : {};
|
|
3392
|
+
const clientMutations = this.activeV3StreamedFiles && typeof this.activeV3StreamedFiles === 'object'
|
|
3393
|
+
? this.activeV3StreamedFiles
|
|
3394
|
+
: {};
|
|
3395
|
+
return { ...streamed, ...clientMutations };
|
|
3396
|
+
}
|
|
3397
|
+
finalizeV3AgentWorkflowResponse(data, response) {
|
|
3398
|
+
const changedFiles = this.resolveAgentChangedFiles(data);
|
|
3399
|
+
this.activeV3StreamedFiles = null;
|
|
3400
|
+
this.activeV3StreamContext = null;
|
|
3401
|
+
return {
|
|
3402
|
+
...response,
|
|
3403
|
+
changedFiles,
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3131
3406
|
async collectV3AgentStream(response, context = {}) {
|
|
3132
3407
|
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
3133
3408
|
return response.json();
|
|
3134
3409
|
}
|
|
3410
|
+
this.clearAgentRunDiagnostics();
|
|
3135
3411
|
const reader = response.body.getReader();
|
|
3136
3412
|
const decoder = new TextDecoder();
|
|
3137
3413
|
let buffer = '';
|
|
@@ -3140,183 +3416,202 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3140
3416
|
let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
|
|
3141
3417
|
let serverWorkspaceRoot = null;
|
|
3142
3418
|
const streamedFiles = {};
|
|
3419
|
+
this.activeV3StreamedFiles = streamedFiles;
|
|
3420
|
+
this.activeV3StreamContext = context;
|
|
3421
|
+
context.__v3StreamedFiles = streamedFiles;
|
|
3143
3422
|
const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3423
|
+
try {
|
|
3424
|
+
while (true) {
|
|
3425
|
+
let chunk;
|
|
3426
|
+
try {
|
|
3427
|
+
const readPromise = reader.read();
|
|
3428
|
+
while (true) {
|
|
3429
|
+
const timeoutSentinel = Symbol('v3-agent-idle-timeout');
|
|
3430
|
+
const result = idleTimeoutMs > 0
|
|
3431
|
+
? await Promise.race([
|
|
3432
|
+
readPromise,
|
|
3433
|
+
new Promise((resolve) => {
|
|
3434
|
+
setTimeout(() => resolve(timeoutSentinel), idleTimeoutMs);
|
|
3435
|
+
}),
|
|
3436
|
+
])
|
|
3437
|
+
: await readPromise;
|
|
3438
|
+
if (result !== timeoutSentinel) {
|
|
3439
|
+
chunk = result;
|
|
3440
|
+
break;
|
|
3441
|
+
}
|
|
3442
|
+
if (this.hasAgentWorkspaceOutput(context)) {
|
|
3443
|
+
const stalledError = new Error('V3 agent stream stalled after writing workspace output');
|
|
3444
|
+
stalledError.name = 'AbortError';
|
|
3445
|
+
stalledError.partialData = {
|
|
3446
|
+
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3447
|
+
context_id: contextId,
|
|
3448
|
+
result: final,
|
|
3449
|
+
events,
|
|
3450
|
+
files: streamedFiles,
|
|
3451
|
+
};
|
|
3452
|
+
throw stalledError;
|
|
3453
|
+
}
|
|
3161
3454
|
}
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3455
|
+
}
|
|
3456
|
+
catch (error) {
|
|
3457
|
+
if (error && error.name === 'AbortError') {
|
|
3458
|
+
error.partialData = {
|
|
3166
3459
|
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3167
3460
|
context_id: contextId,
|
|
3168
3461
|
result: final,
|
|
3169
3462
|
events,
|
|
3170
3463
|
files: streamedFiles,
|
|
3171
3464
|
};
|
|
3172
|
-
throw stalledError;
|
|
3173
3465
|
}
|
|
3466
|
+
throw error;
|
|
3174
3467
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
error.partialData = {
|
|
3179
|
-
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3180
|
-
context_id: contextId,
|
|
3181
|
-
result: final,
|
|
3182
|
-
events,
|
|
3183
|
-
files: streamedFiles,
|
|
3184
|
-
};
|
|
3468
|
+
const { done, value } = chunk;
|
|
3469
|
+
if (done) {
|
|
3470
|
+
break;
|
|
3185
3471
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3472
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3473
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
3474
|
+
buffer = frames.pop() || '';
|
|
3475
|
+
for (const frame of frames) {
|
|
3476
|
+
const lines = frame.split(/\r?\n/);
|
|
3477
|
+
const dataLines = [];
|
|
3478
|
+
let explicitEventType = null;
|
|
3479
|
+
for (const rawLine of lines) {
|
|
3480
|
+
const line = rawLine.trimEnd();
|
|
3481
|
+
if (!line || line.startsWith(':')) {
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
if (line.startsWith('event:')) {
|
|
3485
|
+
explicitEventType = line.slice(6).trim() || null;
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3488
|
+
if (line.startsWith('data:')) {
|
|
3489
|
+
dataLines.push(line.slice(5).trimStart());
|
|
3490
|
+
}
|
|
3203
3491
|
}
|
|
3204
|
-
|
|
3205
|
-
|
|
3492
|
+
const payload = dataLines.join('\n').trim();
|
|
3493
|
+
if (!payload || payload === '[DONE]') {
|
|
3206
3494
|
continue;
|
|
3207
3495
|
}
|
|
3208
|
-
|
|
3209
|
-
|
|
3496
|
+
let event;
|
|
3497
|
+
try {
|
|
3498
|
+
event = JSON.parse(payload);
|
|
3210
3499
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
}
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3500
|
+
catch {
|
|
3501
|
+
event = {
|
|
3502
|
+
type: explicitEventType || 'message',
|
|
3503
|
+
content: payload,
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
if (explicitEventType && (!event.type || event.type === 'message')) {
|
|
3507
|
+
event.type = explicitEventType;
|
|
3508
|
+
}
|
|
3509
|
+
const userEvent = this.sanitizeV3AgentEventForUser(event);
|
|
3510
|
+
events.push(userEvent);
|
|
3511
|
+
if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
|
|
3512
|
+
contextId = event.context_id.trim();
|
|
3513
|
+
}
|
|
3514
|
+
if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
|
|
3515
|
+
serverWorkspaceRoot = event.workspace_root.trim();
|
|
3516
|
+
}
|
|
3517
|
+
if (event.type === 'client_tool_request') {
|
|
3518
|
+
const toolTask = this.handleV3ClientToolRequest(event, context, response.url).catch((error) => {
|
|
3519
|
+
this.logger.warn(`Failed to handle V3 client tool request: ${error.message}`);
|
|
3520
|
+
});
|
|
3521
|
+
this.pendingV3ClientToolTasks.add(toolTask);
|
|
3522
|
+
void toolTask.finally(() => {
|
|
3523
|
+
this.pendingV3ClientToolTasks.delete(toolTask);
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
|
|
3527
|
+
this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
|
|
3528
|
+
// Empty workspace soft guard: the first list_directory result can be
|
|
3529
|
+
// transiently empty before remote workspace hydration catches up.
|
|
3530
|
+
// Avoid hard-failing here; let the stream continue.
|
|
3531
|
+
if (event.type === 'tool_result'
|
|
3532
|
+
&& event.name === 'list_directory'
|
|
3533
|
+
&& event.success === true
|
|
3534
|
+
&& serverWorkspaceRoot
|
|
3535
|
+
&& typeof event.output === 'string') {
|
|
3536
|
+
const listOutput = event.output.trim();
|
|
3537
|
+
const looksEmpty = listOutput === `[${serverWorkspaceRoot}]`
|
|
3538
|
+
|| listOutput === `[${serverWorkspaceRoot}/]`
|
|
3539
|
+
|| listOutput === `[${serverWorkspaceRoot}]\\n`
|
|
3540
|
+
|| /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
|
|
3541
|
+
if (looksEmpty) {
|
|
3542
|
+
const localPath = this.resolveAgentTargetPath(context);
|
|
3543
|
+
const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
|
|
3544
|
+
if (localHasFiles) {
|
|
3545
|
+
this.logger?.debug?.('V3 remote workspace initially empty; waiting for hydration instead of aborting.');
|
|
3546
|
+
}
|
|
3257
3547
|
}
|
|
3258
3548
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3549
|
+
if (typeof context.onStreamEvent === 'function') {
|
|
3550
|
+
try {
|
|
3551
|
+
context.onStreamEvent(userEvent);
|
|
3552
|
+
}
|
|
3553
|
+
catch {
|
|
3554
|
+
// Ignore UI callback failures; never break the agent stream for them.
|
|
3555
|
+
}
|
|
3263
3556
|
}
|
|
3264
|
-
|
|
3265
|
-
|
|
3557
|
+
if (event.type === 'error') {
|
|
3558
|
+
if (event.checkpointed && event.task_id) {
|
|
3559
|
+
// Agent checkpointed — return data so caller can auto-continue
|
|
3560
|
+
return {
|
|
3561
|
+
task_id: event.task_id,
|
|
3562
|
+
context_id: contextId,
|
|
3563
|
+
result: final || event,
|
|
3564
|
+
events,
|
|
3565
|
+
files: streamedFiles,
|
|
3566
|
+
partial: true,
|
|
3567
|
+
checkpointed: true,
|
|
3568
|
+
checkpointed_task_id: event.task_id,
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
if (this.hasAgentWorkspaceOutput(context)) {
|
|
3572
|
+
return {
|
|
3573
|
+
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
3574
|
+
context_id: contextId,
|
|
3575
|
+
result: final || event,
|
|
3576
|
+
events,
|
|
3577
|
+
files: streamedFiles,
|
|
3578
|
+
partial: true,
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
throw new Error(event.message || 'V3 agent returned an error');
|
|
3266
3582
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
if (event.checkpointed && event.task_id) {
|
|
3270
|
-
// Agent checkpointed — return data so caller can auto-continue
|
|
3271
|
-
return {
|
|
3272
|
-
task_id: event.task_id,
|
|
3273
|
-
context_id: contextId,
|
|
3274
|
-
result: final || event,
|
|
3275
|
-
events,
|
|
3276
|
-
files: streamedFiles,
|
|
3277
|
-
partial: true,
|
|
3278
|
-
checkpointed: true,
|
|
3279
|
-
checkpointed_task_id: event.task_id,
|
|
3280
|
-
};
|
|
3583
|
+
if (event.type === 'complete' || event.type === 'message') {
|
|
3584
|
+
final = event;
|
|
3281
3585
|
}
|
|
3282
|
-
|
|
3586
|
+
// Exit stream early on complete — agent is done; server-side teardown
|
|
3587
|
+
// can hold the connection open for many seconds otherwise.
|
|
3588
|
+
if (event.type === 'complete') {
|
|
3589
|
+
reader.cancel().catch(() => { });
|
|
3283
3590
|
return {
|
|
3284
3591
|
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
3285
3592
|
context_id: contextId,
|
|
3286
|
-
result: final
|
|
3593
|
+
result: final,
|
|
3287
3594
|
events,
|
|
3288
3595
|
files: streamedFiles,
|
|
3289
|
-
|
|
3596
|
+
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3290
3597
|
};
|
|
3291
3598
|
}
|
|
3292
|
-
throw new Error(event.message || 'V3 agent returned an error');
|
|
3293
|
-
}
|
|
3294
|
-
if (event.type === 'complete' || event.type === 'message') {
|
|
3295
|
-
final = event;
|
|
3296
|
-
}
|
|
3297
|
-
// Exit stream early on complete — agent is done; server-side teardown
|
|
3298
|
-
// can hold the connection open for many seconds otherwise.
|
|
3299
|
-
if (event.type === 'complete') {
|
|
3300
|
-
reader.cancel().catch(() => { });
|
|
3301
|
-
return {
|
|
3302
|
-
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
3303
|
-
context_id: contextId,
|
|
3304
|
-
result: final,
|
|
3305
|
-
events,
|
|
3306
|
-
files: streamedFiles,
|
|
3307
|
-
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3308
|
-
};
|
|
3309
3599
|
}
|
|
3310
3600
|
}
|
|
3601
|
+
await Promise.allSettled([...this.pendingV3ClientToolTasks]);
|
|
3602
|
+
return {
|
|
3603
|
+
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3604
|
+
context_id: contextId,
|
|
3605
|
+
result: final,
|
|
3606
|
+
events,
|
|
3607
|
+
files: streamedFiles,
|
|
3608
|
+
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3609
|
+
clientToolErrors: this.getClientToolErrors(),
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
finally {
|
|
3613
|
+
await Promise.allSettled([...this.pendingV3ClientToolTasks]);
|
|
3311
3614
|
}
|
|
3312
|
-
return {
|
|
3313
|
-
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3314
|
-
context_id: contextId,
|
|
3315
|
-
result: final,
|
|
3316
|
-
events,
|
|
3317
|
-
files: streamedFiles,
|
|
3318
|
-
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3319
|
-
};
|
|
3320
3615
|
}
|
|
3321
3616
|
async runV3AgentWorkflow(message, context = {}) {
|
|
3322
3617
|
const executionContext = await this.bindExecutionContext(context);
|
|
@@ -3439,14 +3734,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3439
3734
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3440
3735
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3441
3736
|
const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
3442
|
-
return {
|
|
3737
|
+
return this.finalizeV3AgentWorkflowResponse(continuationData, {
|
|
3443
3738
|
content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
|
|
3444
3739
|
taskId: continuationData.task_id || data.task_id || null,
|
|
3445
3740
|
contextId: finalContextId,
|
|
3446
3741
|
backendUrl: baseUrl,
|
|
3447
3742
|
partial: continuationData.checkpointed === true,
|
|
3448
3743
|
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
3449
|
-
};
|
|
3744
|
+
});
|
|
3450
3745
|
}
|
|
3451
3746
|
const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
3452
3747
|
const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
|
|
@@ -3458,13 +3753,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3458
3753
|
errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
|
|
3459
3754
|
continue;
|
|
3460
3755
|
}
|
|
3461
|
-
return {
|
|
3756
|
+
return this.finalizeV3AgentWorkflowResponse(data, {
|
|
3462
3757
|
content: this.formatV3AgentResponse(data),
|
|
3463
3758
|
taskId: data.task_id || null,
|
|
3464
3759
|
contextId,
|
|
3465
3760
|
backendUrl: baseUrl,
|
|
3466
3761
|
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
3467
|
-
};
|
|
3762
|
+
});
|
|
3468
3763
|
}
|
|
3469
3764
|
catch (error) {
|
|
3470
3765
|
if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
|
|
@@ -3472,14 +3767,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3472
3767
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
3473
3768
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3474
3769
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3475
|
-
return {
|
|
3770
|
+
return this.finalizeV3AgentWorkflowResponse(error.partialData, {
|
|
3476
3771
|
content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
|
|
3477
3772
|
taskId: error.partialData.task_id || null,
|
|
3478
3773
|
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
3479
3774
|
backendUrl: baseUrl,
|
|
3480
3775
|
partial: true,
|
|
3481
3776
|
metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null, mcpContextId: requestExecutionContext.mcpContextId || executionContext.mcpContextId || null, previewGate },
|
|
3482
|
-
};
|
|
3777
|
+
});
|
|
3483
3778
|
}
|
|
3484
3779
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3485
3780
|
}
|
|
@@ -3541,6 +3836,158 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3541
3836
|
}
|
|
3542
3837
|
throw new Error(errors.join(' | '));
|
|
3543
3838
|
}
|
|
3839
|
+
async startV3BackgroundJob(message, context = {}) {
|
|
3840
|
+
await this.ensureV3ServiceKey();
|
|
3841
|
+
const executionContext = await this.bindExecutionContext({
|
|
3842
|
+
...context,
|
|
3843
|
+
backgroundJob: true,
|
|
3844
|
+
clientToolExecution: false,
|
|
3845
|
+
});
|
|
3846
|
+
const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
|
|
3847
|
+
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
3848
|
+
const body = {
|
|
3849
|
+
request: message,
|
|
3850
|
+
context: this.buildV3AgentContext({
|
|
3851
|
+
...executionContext,
|
|
3852
|
+
backgroundJob: true,
|
|
3853
|
+
clientToolExecution: false,
|
|
3854
|
+
}),
|
|
3855
|
+
stream: false,
|
|
3856
|
+
model: resolvedModel,
|
|
3857
|
+
mcp_context_id: executionContext.mcpContextId || undefined,
|
|
3858
|
+
};
|
|
3859
|
+
const errors = [];
|
|
3860
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3861
|
+
try {
|
|
3862
|
+
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl), {
|
|
3863
|
+
method: 'POST',
|
|
3864
|
+
headers: await this.getV3AgentHeaders(),
|
|
3865
|
+
body: JSON.stringify(body),
|
|
3866
|
+
});
|
|
3867
|
+
if (!response.ok) {
|
|
3868
|
+
errors.push(`${baseUrl}: ${response.status} ${await response.text().catch(() => '')}`);
|
|
3869
|
+
continue;
|
|
3870
|
+
}
|
|
3871
|
+
return await response.json();
|
|
3872
|
+
}
|
|
3873
|
+
catch (error) {
|
|
3874
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
throw new Error(`Unable to start background job: ${errors.join('; ')}`);
|
|
3878
|
+
}
|
|
3879
|
+
async listV3BackgroundJobs(limit = 20) {
|
|
3880
|
+
await this.ensureV3ServiceKey();
|
|
3881
|
+
const errors = [];
|
|
3882
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3883
|
+
try {
|
|
3884
|
+
const response = await fetch(`${this.getV3AgentBackgroundUrl(baseUrl)}?limit=${encodeURIComponent(String(limit))}`, {
|
|
3885
|
+
method: 'GET',
|
|
3886
|
+
headers: await this.getV3AgentHeaders(),
|
|
3887
|
+
});
|
|
3888
|
+
if (!response.ok) {
|
|
3889
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
3890
|
+
continue;
|
|
3891
|
+
}
|
|
3892
|
+
const data = await response.json();
|
|
3893
|
+
return Array.isArray(data.jobs) ? data.jobs : [];
|
|
3894
|
+
}
|
|
3895
|
+
catch (error) {
|
|
3896
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
throw new Error(`Unable to list background jobs: ${errors.join('; ')}`);
|
|
3900
|
+
}
|
|
3901
|
+
async getV3BackgroundJob(jobId) {
|
|
3902
|
+
await this.ensureV3ServiceKey();
|
|
3903
|
+
const errors = [];
|
|
3904
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3905
|
+
try {
|
|
3906
|
+
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, encodeURIComponent(jobId)), {
|
|
3907
|
+
method: 'GET',
|
|
3908
|
+
headers: await this.getV3AgentHeaders(),
|
|
3909
|
+
});
|
|
3910
|
+
if (!response.ok) {
|
|
3911
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
3912
|
+
continue;
|
|
3913
|
+
}
|
|
3914
|
+
return await response.json();
|
|
3915
|
+
}
|
|
3916
|
+
catch (error) {
|
|
3917
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
throw new Error(`Unable to get background job: ${errors.join('; ')}`);
|
|
3921
|
+
}
|
|
3922
|
+
async cancelV3BackgroundJob(jobId) {
|
|
3923
|
+
await this.ensureV3ServiceKey();
|
|
3924
|
+
const errors = [];
|
|
3925
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3926
|
+
try {
|
|
3927
|
+
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/cancel`), {
|
|
3928
|
+
method: 'POST',
|
|
3929
|
+
headers: await this.getV3AgentHeaders(),
|
|
3930
|
+
});
|
|
3931
|
+
if (!response.ok) {
|
|
3932
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
3933
|
+
continue;
|
|
3934
|
+
}
|
|
3935
|
+
return await response.json();
|
|
3936
|
+
}
|
|
3937
|
+
catch (error) {
|
|
3938
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
throw new Error(`Unable to cancel background job: ${errors.join('; ')}`);
|
|
3942
|
+
}
|
|
3943
|
+
async getV3BackgroundJobFiles(jobId) {
|
|
3944
|
+
await this.ensureV3ServiceKey();
|
|
3945
|
+
const errors = [];
|
|
3946
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3947
|
+
try {
|
|
3948
|
+
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/files`), {
|
|
3949
|
+
method: 'GET',
|
|
3950
|
+
headers: await this.getV3AgentHeaders(),
|
|
3951
|
+
});
|
|
3952
|
+
if (!response.ok) {
|
|
3953
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
3954
|
+
continue;
|
|
3955
|
+
}
|
|
3956
|
+
return await response.json();
|
|
3957
|
+
}
|
|
3958
|
+
catch (error) {
|
|
3959
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
throw new Error(`Unable to fetch background job files: ${errors.join('; ')}`);
|
|
3963
|
+
}
|
|
3964
|
+
async applyV3BackgroundJobFiles(jobId, context = {}) {
|
|
3965
|
+
const data = await this.getV3BackgroundJobFiles(jobId);
|
|
3966
|
+
const rootPath = this.resolveAgentTargetPath({
|
|
3967
|
+
...context,
|
|
3968
|
+
workspacePath: context.workspacePath || data.local_workspace_path,
|
|
3969
|
+
projectPath: context.projectPath || data.local_workspace_path,
|
|
3970
|
+
targetPath: context.targetPath || data.local_workspace_path,
|
|
3971
|
+
});
|
|
3972
|
+
if (!rootPath) {
|
|
3973
|
+
throw new Error('No local workspace path available for applying background job files.');
|
|
3974
|
+
}
|
|
3975
|
+
const applied = [];
|
|
3976
|
+
const skipped = [];
|
|
3977
|
+
for (const [relativePath, content] of Object.entries(data.files || {})) {
|
|
3978
|
+
if (typeof content !== 'string') {
|
|
3979
|
+
skipped.push(relativePath);
|
|
3980
|
+
continue;
|
|
3981
|
+
}
|
|
3982
|
+
if (this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath)) {
|
|
3983
|
+
applied.push(relativePath);
|
|
3984
|
+
}
|
|
3985
|
+
else {
|
|
3986
|
+
skipped.push(relativePath);
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
return { applied, skipped, status: data.status };
|
|
3990
|
+
}
|
|
3544
3991
|
formatOperatorResponse(data = {}) {
|
|
3545
3992
|
// If the server returned a direct answer field, prefer it (for lookup tasks)
|
|
3546
3993
|
if (typeof data.answer === 'string' && data.answer.trim()) {
|
|
@@ -5076,6 +5523,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5076
5523
|
getLastChatTransportErrors() {
|
|
5077
5524
|
return this.lastChatTransportErrors;
|
|
5078
5525
|
}
|
|
5526
|
+
getClientToolErrors() {
|
|
5527
|
+
return [...this.clientToolErrors];
|
|
5528
|
+
}
|
|
5529
|
+
clearAgentRunDiagnostics() {
|
|
5530
|
+
this.clientToolErrors = [];
|
|
5531
|
+
this.pendingV3ClientToolTasks.clear();
|
|
5532
|
+
}
|
|
5533
|
+
recordClientToolError(message) {
|
|
5534
|
+
const trimmed = sanitizeUserFacingErrorText(String(message || '')).slice(0, 280);
|
|
5535
|
+
if (trimmed && !this.clientToolErrors.includes(trimmed)) {
|
|
5536
|
+
this.clientToolErrors.push(trimmed);
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5079
5539
|
async submitClientToolResult(contextId, callId, result, backendUrl) {
|
|
5080
5540
|
const trimmedContextId = String(contextId || '').trim();
|
|
5081
5541
|
const trimmedCallId = String(callId || '').trim();
|
|
@@ -5112,6 +5572,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5112
5572
|
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
5113
5573
|
continue;
|
|
5114
5574
|
}
|
|
5575
|
+
if (response.status === 404 && /no pending client tool call/i.test(errorText)) {
|
|
5576
|
+
// Server already consumed or timed out this call — treat as idempotent when local work succeeded.
|
|
5577
|
+
return;
|
|
5578
|
+
}
|
|
5115
5579
|
throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
5116
5580
|
}
|
|
5117
5581
|
}
|