vigthoria-cli 1.10.51 → 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 +51 -2
- package/dist/index.js +55 -2
- package/dist/utils/api.d.ts +30 -0
- package/dist/utils/api.js +606 -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,
|
|
@@ -1448,7 +1469,9 @@ export class APIClient {
|
|
|
1448
1469
|
legacyFallbackAllowed: resolvedContext.legacyFallbackAllowed === true,
|
|
1449
1470
|
executionSurface: resolvedContext.executionSurface || 'cli',
|
|
1450
1471
|
clientSurface: resolvedContext.clientSurface || 'cli',
|
|
1451
|
-
localMachineCapable
|
|
1472
|
+
localMachineCapable,
|
|
1473
|
+
clientToolExecution,
|
|
1474
|
+
client_tool_execution: clientToolExecution,
|
|
1452
1475
|
runtimeEnvironment: publicRuntimeEnvironment,
|
|
1453
1476
|
workspacePath: effectiveWorkspacePath,
|
|
1454
1477
|
projectPath: effectiveWorkspacePath,
|
|
@@ -1471,6 +1494,11 @@ export class APIClient {
|
|
|
1471
1494
|
requestStartedAt: resolvedContext.requestStartedAt,
|
|
1472
1495
|
subscriptionPlan: this.config.getNormalizedPlan() || null,
|
|
1473
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 || '',
|
|
1474
1502
|
};
|
|
1475
1503
|
return this.compactV3Context(payload);
|
|
1476
1504
|
}
|
|
@@ -1602,6 +1630,8 @@ export class APIClient {
|
|
|
1602
1630
|
contextId: resolvedContext.contextId,
|
|
1603
1631
|
traceId: resolvedContext.traceId,
|
|
1604
1632
|
requestStartedAt: resolvedContext.requestStartedAt,
|
|
1633
|
+
vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
|
|
1634
|
+
vigthoria_brain: resolvedContext.vigthoriaBrain || null,
|
|
1605
1635
|
});
|
|
1606
1636
|
}
|
|
1607
1637
|
extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
|
|
@@ -2075,7 +2105,7 @@ menu {
|
|
|
2075
2105
|
const headers = await this.getMcpHeaders();
|
|
2076
2106
|
const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
|
|
2077
2107
|
const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
|
|
2078
|
-
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
2108
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, String(executionContext.rawPrompt || executionContext.contextualPrompt || executionContext.prompt || ''));
|
|
2079
2109
|
const metadata = {
|
|
2080
2110
|
source: 'vigthoria-cli',
|
|
2081
2111
|
sharedContextId: executionContext.contextId,
|
|
@@ -2236,7 +2266,7 @@ menu {
|
|
|
2236
2266
|
cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
|
|
2237
2267
|
};
|
|
2238
2268
|
}
|
|
2239
|
-
buildLocalWorkspaceSummary(rootPath) {
|
|
2269
|
+
buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
|
|
2240
2270
|
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
2241
2271
|
return null;
|
|
2242
2272
|
}
|
|
@@ -2247,8 +2277,31 @@ menu {
|
|
|
2247
2277
|
files: [],
|
|
2248
2278
|
};
|
|
2249
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
|
+
}
|
|
2250
2303
|
summary.fileCount = snapshot.fileCount;
|
|
2251
|
-
summary.files =
|
|
2304
|
+
summary.files = orderedPaths.slice(0, 40);
|
|
2252
2305
|
const packageJsonPath = path.join(rootPath, 'package.json');
|
|
2253
2306
|
if (fs.existsSync(packageJsonPath)) {
|
|
2254
2307
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
@@ -2266,7 +2319,7 @@ menu {
|
|
|
2266
2319
|
}
|
|
2267
2320
|
// Hydrate workspace: include actual file contents so the V3 server
|
|
2268
2321
|
// can populate the remote workspace before the agent starts.
|
|
2269
|
-
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath,
|
|
2322
|
+
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
|
|
2270
2323
|
return summary;
|
|
2271
2324
|
}
|
|
2272
2325
|
catch (error) {
|
|
@@ -2302,6 +2355,8 @@ menu {
|
|
|
2302
2355
|
continue;
|
|
2303
2356
|
if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
|
|
2304
2357
|
continue;
|
|
2358
|
+
if (/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
|
|
2359
|
+
continue;
|
|
2305
2360
|
const absolutePath = path.join(rootPath, relativePath);
|
|
2306
2361
|
try {
|
|
2307
2362
|
const stat = fs.statSync(absolutePath);
|
|
@@ -2481,6 +2536,19 @@ menu {
|
|
|
2481
2536
|
return;
|
|
2482
2537
|
}
|
|
2483
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
|
+
}
|
|
2484
2552
|
return;
|
|
2485
2553
|
}
|
|
2486
2554
|
const args = event.arguments || {};
|
|
@@ -2533,6 +2601,178 @@ menu {
|
|
|
2533
2601
|
}
|
|
2534
2602
|
return false;
|
|
2535
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
|
+
}
|
|
2536
2776
|
writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
|
|
2537
2777
|
const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, sourceRoot || rootPath);
|
|
2538
2778
|
if (!relativePath) {
|
|
@@ -3054,6 +3294,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3054
3294
|
.replace(/<\/think>/gi, '</thinking>')
|
|
3055
3295
|
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
|
|
3056
3296
|
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
|
3297
|
+
.replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
|
|
3057
3298
|
.replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
|
|
3058
3299
|
.replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
|
|
3059
3300
|
.replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
|
|
@@ -3146,10 +3387,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3146
3387
|
};
|
|
3147
3388
|
return sanitizeValue(event);
|
|
3148
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
|
+
}
|
|
3149
3406
|
async collectV3AgentStream(response, context = {}) {
|
|
3150
3407
|
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
3151
3408
|
return response.json();
|
|
3152
3409
|
}
|
|
3410
|
+
this.clearAgentRunDiagnostics();
|
|
3153
3411
|
const reader = response.body.getReader();
|
|
3154
3412
|
const decoder = new TextDecoder();
|
|
3155
3413
|
let buffer = '';
|
|
@@ -3158,183 +3416,202 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3158
3416
|
let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
|
|
3159
3417
|
let serverWorkspaceRoot = null;
|
|
3160
3418
|
const streamedFiles = {};
|
|
3419
|
+
this.activeV3StreamedFiles = streamedFiles;
|
|
3420
|
+
this.activeV3StreamContext = context;
|
|
3421
|
+
context.__v3StreamedFiles = streamedFiles;
|
|
3161
3422
|
const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
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
|
+
}
|
|
3179
3454
|
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3455
|
+
}
|
|
3456
|
+
catch (error) {
|
|
3457
|
+
if (error && error.name === 'AbortError') {
|
|
3458
|
+
error.partialData = {
|
|
3184
3459
|
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3185
3460
|
context_id: contextId,
|
|
3186
3461
|
result: final,
|
|
3187
3462
|
events,
|
|
3188
3463
|
files: streamedFiles,
|
|
3189
3464
|
};
|
|
3190
|
-
throw stalledError;
|
|
3191
3465
|
}
|
|
3466
|
+
throw error;
|
|
3192
3467
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
error.partialData = {
|
|
3197
|
-
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3198
|
-
context_id: contextId,
|
|
3199
|
-
result: final,
|
|
3200
|
-
events,
|
|
3201
|
-
files: streamedFiles,
|
|
3202
|
-
};
|
|
3468
|
+
const { done, value } = chunk;
|
|
3469
|
+
if (done) {
|
|
3470
|
+
break;
|
|
3203
3471
|
}
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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
|
+
}
|
|
3221
3491
|
}
|
|
3222
|
-
|
|
3223
|
-
|
|
3492
|
+
const payload = dataLines.join('\n').trim();
|
|
3493
|
+
if (!payload || payload === '[DONE]') {
|
|
3224
3494
|
continue;
|
|
3225
3495
|
}
|
|
3226
|
-
|
|
3227
|
-
|
|
3496
|
+
let event;
|
|
3497
|
+
try {
|
|
3498
|
+
event = JSON.parse(payload);
|
|
3228
3499
|
}
|
|
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
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
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
|
+
}
|
|
3275
3547
|
}
|
|
3276
3548
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
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
|
+
}
|
|
3281
3556
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
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');
|
|
3284
3582
|
}
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
if (event.checkpointed && event.task_id) {
|
|
3288
|
-
// Agent checkpointed — return data so caller can auto-continue
|
|
3289
|
-
return {
|
|
3290
|
-
task_id: event.task_id,
|
|
3291
|
-
context_id: contextId,
|
|
3292
|
-
result: final || event,
|
|
3293
|
-
events,
|
|
3294
|
-
files: streamedFiles,
|
|
3295
|
-
partial: true,
|
|
3296
|
-
checkpointed: true,
|
|
3297
|
-
checkpointed_task_id: event.task_id,
|
|
3298
|
-
};
|
|
3583
|
+
if (event.type === 'complete' || event.type === 'message') {
|
|
3584
|
+
final = event;
|
|
3299
3585
|
}
|
|
3300
|
-
|
|
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(() => { });
|
|
3301
3590
|
return {
|
|
3302
3591
|
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
3303
3592
|
context_id: contextId,
|
|
3304
|
-
result: final
|
|
3593
|
+
result: final,
|
|
3305
3594
|
events,
|
|
3306
3595
|
files: streamedFiles,
|
|
3307
|
-
|
|
3596
|
+
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3308
3597
|
};
|
|
3309
3598
|
}
|
|
3310
|
-
throw new Error(event.message || 'V3 agent returned an error');
|
|
3311
|
-
}
|
|
3312
|
-
if (event.type === 'complete' || event.type === 'message') {
|
|
3313
|
-
final = event;
|
|
3314
|
-
}
|
|
3315
|
-
// Exit stream early on complete — agent is done; server-side teardown
|
|
3316
|
-
// can hold the connection open for many seconds otherwise.
|
|
3317
|
-
if (event.type === 'complete') {
|
|
3318
|
-
reader.cancel().catch(() => { });
|
|
3319
|
-
return {
|
|
3320
|
-
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
3321
|
-
context_id: contextId,
|
|
3322
|
-
result: final,
|
|
3323
|
-
events,
|
|
3324
|
-
files: streamedFiles,
|
|
3325
|
-
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3326
|
-
};
|
|
3327
3599
|
}
|
|
3328
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]);
|
|
3329
3614
|
}
|
|
3330
|
-
return {
|
|
3331
|
-
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3332
|
-
context_id: contextId,
|
|
3333
|
-
result: final,
|
|
3334
|
-
events,
|
|
3335
|
-
files: streamedFiles,
|
|
3336
|
-
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3337
|
-
};
|
|
3338
3615
|
}
|
|
3339
3616
|
async runV3AgentWorkflow(message, context = {}) {
|
|
3340
3617
|
const executionContext = await this.bindExecutionContext(context);
|
|
@@ -3457,14 +3734,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3457
3734
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3458
3735
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3459
3736
|
const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
3460
|
-
return {
|
|
3737
|
+
return this.finalizeV3AgentWorkflowResponse(continuationData, {
|
|
3461
3738
|
content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
|
|
3462
3739
|
taskId: continuationData.task_id || data.task_id || null,
|
|
3463
3740
|
contextId: finalContextId,
|
|
3464
3741
|
backendUrl: baseUrl,
|
|
3465
3742
|
partial: continuationData.checkpointed === true,
|
|
3466
3743
|
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
3467
|
-
};
|
|
3744
|
+
});
|
|
3468
3745
|
}
|
|
3469
3746
|
const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
3470
3747
|
const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
|
|
@@ -3476,13 +3753,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3476
3753
|
errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
|
|
3477
3754
|
continue;
|
|
3478
3755
|
}
|
|
3479
|
-
return {
|
|
3756
|
+
return this.finalizeV3AgentWorkflowResponse(data, {
|
|
3480
3757
|
content: this.formatV3AgentResponse(data),
|
|
3481
3758
|
taskId: data.task_id || null,
|
|
3482
3759
|
contextId,
|
|
3483
3760
|
backendUrl: baseUrl,
|
|
3484
3761
|
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
3485
|
-
};
|
|
3762
|
+
});
|
|
3486
3763
|
}
|
|
3487
3764
|
catch (error) {
|
|
3488
3765
|
if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
|
|
@@ -3490,14 +3767,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3490
3767
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
3491
3768
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3492
3769
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3493
|
-
return {
|
|
3770
|
+
return this.finalizeV3AgentWorkflowResponse(error.partialData, {
|
|
3494
3771
|
content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
|
|
3495
3772
|
taskId: error.partialData.task_id || null,
|
|
3496
3773
|
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
3497
3774
|
backendUrl: baseUrl,
|
|
3498
3775
|
partial: true,
|
|
3499
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 },
|
|
3500
|
-
};
|
|
3777
|
+
});
|
|
3501
3778
|
}
|
|
3502
3779
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3503
3780
|
}
|
|
@@ -3559,6 +3836,158 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3559
3836
|
}
|
|
3560
3837
|
throw new Error(errors.join(' | '));
|
|
3561
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
|
+
}
|
|
3562
3991
|
formatOperatorResponse(data = {}) {
|
|
3563
3992
|
// If the server returned a direct answer field, prefer it (for lookup tasks)
|
|
3564
3993
|
if (typeof data.answer === 'string' && data.answer.trim()) {
|
|
@@ -5094,6 +5523,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5094
5523
|
getLastChatTransportErrors() {
|
|
5095
5524
|
return this.lastChatTransportErrors;
|
|
5096
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
|
+
}
|
|
5097
5539
|
async submitClientToolResult(contextId, callId, result, backendUrl) {
|
|
5098
5540
|
const trimmedContextId = String(contextId || '').trim();
|
|
5099
5541
|
const trimmedCallId = String(callId || '').trim();
|
|
@@ -5130,6 +5572,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5130
5572
|
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
5131
5573
|
continue;
|
|
5132
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
|
+
}
|
|
5133
5579
|
throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
5134
5580
|
}
|
|
5135
5581
|
}
|