vigthoria-cli 1.10.51 → 1.11.0
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 +110 -36
- package/dist/index.js +55 -2
- package/dist/utils/agentRunOutcome.d.ts +36 -0
- package/dist/utils/agentRunOutcome.js +123 -0
- package/dist/utils/api.d.ts +32 -0
- package/dist/utils/api.js +657 -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
|
}
|
|
@@ -1485,6 +1513,12 @@ export class APIClient {
|
|
|
1485
1513
|
*/
|
|
1486
1514
|
compactV3Context(payload) {
|
|
1487
1515
|
const LIMIT = APIClient.V3_CONTEXT_CHAR_LIMIT;
|
|
1516
|
+
if (payload.vigthoriaBrain) {
|
|
1517
|
+
payload.vigthoriaBrain = this.dedupeBrainPayload(payload.vigthoriaBrain);
|
|
1518
|
+
}
|
|
1519
|
+
if (payload.vigthoria_brain) {
|
|
1520
|
+
payload.vigthoria_brain = payload.vigthoriaBrain || this.dedupeBrainPayload(payload.vigthoria_brain);
|
|
1521
|
+
}
|
|
1488
1522
|
let json = JSON.stringify(payload);
|
|
1489
1523
|
if (json.length <= LIMIT)
|
|
1490
1524
|
return json;
|
|
@@ -1558,6 +1592,51 @@ export class APIClient {
|
|
|
1558
1592
|
}
|
|
1559
1593
|
return json;
|
|
1560
1594
|
}
|
|
1595
|
+
/** Remove duplicate functionIndex entries that poison model context after compaction. */
|
|
1596
|
+
dedupeBrainPayload(brain) {
|
|
1597
|
+
if (!brain || typeof brain !== 'object') {
|
|
1598
|
+
return brain;
|
|
1599
|
+
}
|
|
1600
|
+
const copy = { ...brain };
|
|
1601
|
+
const dedupeIndexEntries = (entries) => {
|
|
1602
|
+
const seen = new Set();
|
|
1603
|
+
const unique = [];
|
|
1604
|
+
for (const entry of entries) {
|
|
1605
|
+
if (!entry || typeof entry !== 'object')
|
|
1606
|
+
continue;
|
|
1607
|
+
const file = String(entry.file || '')
|
|
1608
|
+
.replace(/\\/g, '/')
|
|
1609
|
+
.replace(/^home\/user\//i, '')
|
|
1610
|
+
.toLowerCase();
|
|
1611
|
+
const name = String(entry.name || '');
|
|
1612
|
+
const line = String(entry.line ?? '');
|
|
1613
|
+
const key = `${name}::${file}::${line}`;
|
|
1614
|
+
if (seen.has(key))
|
|
1615
|
+
continue;
|
|
1616
|
+
seen.add(key);
|
|
1617
|
+
unique.push(entry);
|
|
1618
|
+
}
|
|
1619
|
+
return unique;
|
|
1620
|
+
};
|
|
1621
|
+
if (copy.functionIndex && typeof copy.functionIndex === 'object') {
|
|
1622
|
+
const deduped = {};
|
|
1623
|
+
for (const [symbol, entries] of Object.entries(copy.functionIndex)) {
|
|
1624
|
+
deduped[symbol] = Array.isArray(entries) ? dedupeIndexEntries(entries) : [];
|
|
1625
|
+
}
|
|
1626
|
+
copy.functionIndex = deduped;
|
|
1627
|
+
}
|
|
1628
|
+
if (copy.files && typeof copy.files === 'object') {
|
|
1629
|
+
const normalized = {};
|
|
1630
|
+
for (const [filePath, meta] of Object.entries(copy.files)) {
|
|
1631
|
+
const norm = String(filePath).replace(/\\/g, '/').replace(/^home\/user\//i, '');
|
|
1632
|
+
if (!(norm in normalized)) {
|
|
1633
|
+
normalized[norm] = meta;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
copy.files = normalized;
|
|
1637
|
+
}
|
|
1638
|
+
return copy;
|
|
1639
|
+
}
|
|
1561
1640
|
buildMinimalV3AgentContext(context = {}) {
|
|
1562
1641
|
const resolvedContext = this.ensureExecutionContext(context);
|
|
1563
1642
|
const targetPath = this.resolveAgentTargetPath(resolvedContext)
|
|
@@ -1602,6 +1681,8 @@ export class APIClient {
|
|
|
1602
1681
|
contextId: resolvedContext.contextId,
|
|
1603
1682
|
traceId: resolvedContext.traceId,
|
|
1604
1683
|
requestStartedAt: resolvedContext.requestStartedAt,
|
|
1684
|
+
vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
|
|
1685
|
+
vigthoria_brain: resolvedContext.vigthoriaBrain || null,
|
|
1605
1686
|
});
|
|
1606
1687
|
}
|
|
1607
1688
|
extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
|
|
@@ -2075,7 +2156,7 @@ menu {
|
|
|
2075
2156
|
const headers = await this.getMcpHeaders();
|
|
2076
2157
|
const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
|
|
2077
2158
|
const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
|
|
2078
|
-
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
2159
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, String(executionContext.rawPrompt || executionContext.contextualPrompt || executionContext.prompt || ''));
|
|
2079
2160
|
const metadata = {
|
|
2080
2161
|
source: 'vigthoria-cli',
|
|
2081
2162
|
sharedContextId: executionContext.contextId,
|
|
@@ -2236,7 +2317,7 @@ menu {
|
|
|
2236
2317
|
cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
|
|
2237
2318
|
};
|
|
2238
2319
|
}
|
|
2239
|
-
buildLocalWorkspaceSummary(rootPath) {
|
|
2320
|
+
buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
|
|
2240
2321
|
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
2241
2322
|
return null;
|
|
2242
2323
|
}
|
|
@@ -2247,8 +2328,31 @@ menu {
|
|
|
2247
2328
|
files: [],
|
|
2248
2329
|
};
|
|
2249
2330
|
const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
|
|
2331
|
+
const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
|
|
2332
|
+
let orderedPaths = candidatePaths;
|
|
2333
|
+
try {
|
|
2334
|
+
const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
|
|
2335
|
+
const changes = getChangedFiles(rootPath, candidatePaths);
|
|
2336
|
+
const ordered = new Set();
|
|
2337
|
+
for (const filePath of changes.changed)
|
|
2338
|
+
ordered.add(filePath);
|
|
2339
|
+
for (const filePath of ranked)
|
|
2340
|
+
ordered.add(filePath);
|
|
2341
|
+
for (const filePath of changes.unchanged)
|
|
2342
|
+
ordered.add(filePath);
|
|
2343
|
+
orderedPaths = Array.from(ordered);
|
|
2344
|
+
summary.semanticRank = ranked.slice(0, 20);
|
|
2345
|
+
summary.changeSummary = {
|
|
2346
|
+
changed: changes.changed.length,
|
|
2347
|
+
unchanged: changes.unchanged.length,
|
|
2348
|
+
total: changes.total,
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
catch {
|
|
2352
|
+
orderedPaths = candidatePaths;
|
|
2353
|
+
}
|
|
2250
2354
|
summary.fileCount = snapshot.fileCount;
|
|
2251
|
-
summary.files =
|
|
2355
|
+
summary.files = orderedPaths.slice(0, 40);
|
|
2252
2356
|
const packageJsonPath = path.join(rootPath, 'package.json');
|
|
2253
2357
|
if (fs.existsSync(packageJsonPath)) {
|
|
2254
2358
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
@@ -2266,7 +2370,7 @@ menu {
|
|
|
2266
2370
|
}
|
|
2267
2371
|
// Hydrate workspace: include actual file contents so the V3 server
|
|
2268
2372
|
// can populate the remote workspace before the agent starts.
|
|
2269
|
-
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath,
|
|
2373
|
+
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
|
|
2270
2374
|
return summary;
|
|
2271
2375
|
}
|
|
2272
2376
|
catch (error) {
|
|
@@ -2302,6 +2406,8 @@ menu {
|
|
|
2302
2406
|
continue;
|
|
2303
2407
|
if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
|
|
2304
2408
|
continue;
|
|
2409
|
+
if (/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
|
|
2410
|
+
continue;
|
|
2305
2411
|
const absolutePath = path.join(rootPath, relativePath);
|
|
2306
2412
|
try {
|
|
2307
2413
|
const stat = fs.statSync(absolutePath);
|
|
@@ -2481,6 +2587,19 @@ menu {
|
|
|
2481
2587
|
return;
|
|
2482
2588
|
}
|
|
2483
2589
|
if (event.type !== 'tool_call') {
|
|
2590
|
+
if (event.type === 'tool_result' && event.success) {
|
|
2591
|
+
const name = String(event.name || event.tool || '').trim();
|
|
2592
|
+
const args = event.arguments || {};
|
|
2593
|
+
const targetPath = typeof event.target === 'string'
|
|
2594
|
+
? event.target
|
|
2595
|
+
: (typeof args.path === 'string' ? args.path : '');
|
|
2596
|
+
if ((name === 'write_file' || name === 'edit_file') && targetPath) {
|
|
2597
|
+
const filePath = this.normalizeAgentWorkspaceRelativePath(targetPath, serverRoot || undefined);
|
|
2598
|
+
if (filePath && typeof args.content === 'string') {
|
|
2599
|
+
streamedFiles[filePath] = args.content;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2484
2603
|
return;
|
|
2485
2604
|
}
|
|
2486
2605
|
const args = event.arguments || {};
|
|
@@ -2533,6 +2652,178 @@ menu {
|
|
|
2533
2652
|
}
|
|
2534
2653
|
return false;
|
|
2535
2654
|
}
|
|
2655
|
+
resolveV3ClientToolPath(rootPath, rawPath) {
|
|
2656
|
+
const relativePath = this.normalizeAgentWorkspaceRelativePath(String(rawPath || '.'), rootPath);
|
|
2657
|
+
if (!relativePath && String(rawPath || '.').trim() !== '.') {
|
|
2658
|
+
return null;
|
|
2659
|
+
}
|
|
2660
|
+
const absolutePath = path.resolve(rootPath, relativePath || '.');
|
|
2661
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
2662
|
+
if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
return { relativePath: relativePath || '.', absolutePath };
|
|
2666
|
+
}
|
|
2667
|
+
recordV3ClientToolMutation(event, context, result) {
|
|
2668
|
+
if (!result.success) {
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
const name = String(event.name || event.tool || '').trim();
|
|
2672
|
+
if (name !== 'write_file' && name !== 'edit_file') {
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
const args = event.arguments || {};
|
|
2676
|
+
const rootPath = this.resolveAgentTargetPath(context);
|
|
2677
|
+
if (!rootPath) {
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
const target = this.resolveV3ClientToolPath(rootPath, args.path || '.');
|
|
2681
|
+
if (!target) {
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const bucket = this.activeV3StreamedFiles || context.__v3StreamedFiles;
|
|
2685
|
+
if (!bucket) {
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
if (name === 'write_file' && typeof args.content === 'string') {
|
|
2689
|
+
bucket[target.relativePath] = args.content;
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
if (name === 'edit_file') {
|
|
2693
|
+
try {
|
|
2694
|
+
bucket[target.relativePath] = fs.readFileSync(target.absolutePath, 'utf8');
|
|
2695
|
+
}
|
|
2696
|
+
catch {
|
|
2697
|
+
// ignore read failures; success was already reported by the client tool
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
async executeV3ClientToolRequest(event, context = {}) {
|
|
2702
|
+
const rootPath = this.resolveAgentTargetPath(context);
|
|
2703
|
+
if (!rootPath) {
|
|
2704
|
+
return { success: false, output: '', error: 'No local workspace path is available for client tool execution.' };
|
|
2705
|
+
}
|
|
2706
|
+
const name = String(event.name || event.tool || '').trim();
|
|
2707
|
+
const args = event.arguments || {};
|
|
2708
|
+
const target = this.resolveV3ClientToolPath(rootPath, args.path || args.cwd || '.');
|
|
2709
|
+
if (!target) {
|
|
2710
|
+
return { success: false, output: '', error: 'Tool path is outside the workspace.' };
|
|
2711
|
+
}
|
|
2712
|
+
try {
|
|
2713
|
+
if (name === 'write_file') {
|
|
2714
|
+
if (typeof args.content !== 'string')
|
|
2715
|
+
return { success: false, output: '', error: 'write_file requires string content.' };
|
|
2716
|
+
fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
|
|
2717
|
+
fs.writeFileSync(target.absolutePath, args.content, 'utf8');
|
|
2718
|
+
const result = { success: true, output: `Updated ${target.relativePath}` };
|
|
2719
|
+
this.recordV3ClientToolMutation(event, context, result);
|
|
2720
|
+
return result;
|
|
2721
|
+
}
|
|
2722
|
+
if (name === 'edit_file') {
|
|
2723
|
+
const oldString = typeof args.old_string === 'string' ? args.old_string : String(args.old_text || '');
|
|
2724
|
+
const newString = typeof args.new_string === 'string' ? args.new_string : String(args.new_text || '');
|
|
2725
|
+
if (!oldString)
|
|
2726
|
+
return { success: false, output: '', error: 'edit_file requires old_string.' };
|
|
2727
|
+
const existing = fs.readFileSync(target.absolutePath, 'utf8');
|
|
2728
|
+
const count = existing.split(oldString).length - 1;
|
|
2729
|
+
if (count !== 1)
|
|
2730
|
+
return { success: false, output: '', error: `old_string found ${count} times; expected exactly once.` };
|
|
2731
|
+
fs.writeFileSync(target.absolutePath, existing.replace(oldString, newString), 'utf8');
|
|
2732
|
+
const result = { success: true, output: `Edited ${target.relativePath}` };
|
|
2733
|
+
this.recordV3ClientToolMutation(event, context, result);
|
|
2734
|
+
return result;
|
|
2735
|
+
}
|
|
2736
|
+
if (name === 'read_file') {
|
|
2737
|
+
return { success: true, output: fs.readFileSync(target.absolutePath, 'utf8') };
|
|
2738
|
+
}
|
|
2739
|
+
if (name === 'list_directory') {
|
|
2740
|
+
const entries = fs.readdirSync(target.absolutePath, { withFileTypes: true })
|
|
2741
|
+
.filter((entry) => !['node_modules', '.git'].includes(entry.name))
|
|
2742
|
+
.map((entry) => `${entry.isDirectory() ? '[DIR] ' : ''}${entry.name}`);
|
|
2743
|
+
return { success: true, output: entries.join('\n') || '(empty)' };
|
|
2744
|
+
}
|
|
2745
|
+
if (name === 'glob' || name === 'search_files') {
|
|
2746
|
+
const pattern = String(args.pattern || args.query || '').toLowerCase();
|
|
2747
|
+
const files = [];
|
|
2748
|
+
const walk = (dir) => {
|
|
2749
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2750
|
+
if (['node_modules', '.git'].includes(entry.name))
|
|
2751
|
+
continue;
|
|
2752
|
+
const absolute = path.join(dir, entry.name);
|
|
2753
|
+
if (entry.isDirectory()) {
|
|
2754
|
+
walk(absolute);
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2757
|
+
const relative = path.relative(rootPath, absolute).replace(/\\/g, '/');
|
|
2758
|
+
if (!pattern || relative.toLowerCase().includes(pattern.replace(/\*/g, ''))) {
|
|
2759
|
+
files.push(relative);
|
|
2760
|
+
}
|
|
2761
|
+
else if (name === 'search_files') {
|
|
2762
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
2763
|
+
if (content.toLowerCase().includes(pattern))
|
|
2764
|
+
files.push(relative);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
walk(fs.statSync(target.absolutePath).isDirectory() ? target.absolutePath : rootPath);
|
|
2769
|
+
return { success: true, output: files.slice(0, 200).join('\n') || '(no matches)' };
|
|
2770
|
+
}
|
|
2771
|
+
if (name === 'syntax_check') {
|
|
2772
|
+
const content = fs.readFileSync(target.absolutePath, 'utf8');
|
|
2773
|
+
if (/\.json$/i.test(target.absolutePath))
|
|
2774
|
+
JSON.parse(content);
|
|
2775
|
+
return { success: true, output: `Syntax check passed: ${target.relativePath}` };
|
|
2776
|
+
}
|
|
2777
|
+
if (name === 'run_command') {
|
|
2778
|
+
const command = String(args.command || '').trim();
|
|
2779
|
+
if (!command)
|
|
2780
|
+
return { success: false, output: '', error: 'run_command requires command.' };
|
|
2781
|
+
const { exec } = await import('child_process');
|
|
2782
|
+
return await new Promise((resolve) => {
|
|
2783
|
+
exec(command, { cwd: target.absolutePath, timeout: Number(args.timeout || 30000) }, (error, stdout, stderr) => {
|
|
2784
|
+
resolve({
|
|
2785
|
+
success: !error,
|
|
2786
|
+
output: String(stdout || stderr || '').slice(0, 12000),
|
|
2787
|
+
error: error ? String(stderr || error.message) : '',
|
|
2788
|
+
});
|
|
2789
|
+
});
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
return { success: false, output: '', error: `Unsupported client tool: ${name}` };
|
|
2793
|
+
}
|
|
2794
|
+
catch (error) {
|
|
2795
|
+
return { success: false, output: '', error: error.message };
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
async handleV3ClientToolRequest(event, context = {}, responseUrl) {
|
|
2799
|
+
const contextId = String(event.context_id || context.contextId || '').trim();
|
|
2800
|
+
const callId = String(event.call_id || '').trim();
|
|
2801
|
+
const toolName = String(event.tool || event.name || 'client_tool').trim();
|
|
2802
|
+
if (!contextId || !callId)
|
|
2803
|
+
return;
|
|
2804
|
+
const backendUrl = responseUrl ? new URL(responseUrl).origin : (context.mcpContextBackendUrl || null);
|
|
2805
|
+
try {
|
|
2806
|
+
const result = await this.executeV3ClientToolRequest(event, context);
|
|
2807
|
+
try {
|
|
2808
|
+
await this.submitClientToolResult(contextId, callId, result, backendUrl);
|
|
2809
|
+
}
|
|
2810
|
+
catch (submitError) {
|
|
2811
|
+
const submitMessage = submitError.message || String(submitError);
|
|
2812
|
+
if (result.success) {
|
|
2813
|
+
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.`);
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
throw submitError;
|
|
2817
|
+
}
|
|
2818
|
+
if (!result.success && result.error) {
|
|
2819
|
+
this.recordClientToolError(`Local ${toolName} failed: ${result.error}`);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
catch (error) {
|
|
2823
|
+
this.recordClientToolError(`Local tool bridge failed for ${toolName}: ${error.message}`);
|
|
2824
|
+
throw error;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2536
2827
|
writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
|
|
2537
2828
|
const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, sourceRoot || rootPath);
|
|
2538
2829
|
if (!relativePath) {
|
|
@@ -3054,6 +3345,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3054
3345
|
.replace(/<\/think>/gi, '</thinking>')
|
|
3055
3346
|
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
|
|
3056
3347
|
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
|
3348
|
+
.replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
|
|
3057
3349
|
.replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
|
|
3058
3350
|
.replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
|
|
3059
3351
|
.replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
|
|
@@ -3146,10 +3438,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3146
3438
|
};
|
|
3147
3439
|
return sanitizeValue(event);
|
|
3148
3440
|
}
|
|
3441
|
+
resolveAgentChangedFiles(data) {
|
|
3442
|
+
const streamed = data?.files && typeof data.files === 'object' ? data.files : {};
|
|
3443
|
+
const clientMutations = this.activeV3StreamedFiles && typeof this.activeV3StreamedFiles === 'object'
|
|
3444
|
+
? this.activeV3StreamedFiles
|
|
3445
|
+
: {};
|
|
3446
|
+
return { ...streamed, ...clientMutations };
|
|
3447
|
+
}
|
|
3448
|
+
finalizeV3AgentWorkflowResponse(data, response) {
|
|
3449
|
+
const changedFiles = this.resolveAgentChangedFiles(data);
|
|
3450
|
+
this.activeV3StreamedFiles = null;
|
|
3451
|
+
this.activeV3StreamContext = null;
|
|
3452
|
+
return {
|
|
3453
|
+
...response,
|
|
3454
|
+
changedFiles,
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3149
3457
|
async collectV3AgentStream(response, context = {}) {
|
|
3150
3458
|
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
3151
3459
|
return response.json();
|
|
3152
3460
|
}
|
|
3461
|
+
this.clearAgentRunDiagnostics();
|
|
3153
3462
|
const reader = response.body.getReader();
|
|
3154
3463
|
const decoder = new TextDecoder();
|
|
3155
3464
|
let buffer = '';
|
|
@@ -3158,183 +3467,202 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3158
3467
|
let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
|
|
3159
3468
|
let serverWorkspaceRoot = null;
|
|
3160
3469
|
const streamedFiles = {};
|
|
3470
|
+
this.activeV3StreamedFiles = streamedFiles;
|
|
3471
|
+
this.activeV3StreamContext = context;
|
|
3472
|
+
context.__v3StreamedFiles = streamedFiles;
|
|
3161
3473
|
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
|
-
|
|
3474
|
+
try {
|
|
3475
|
+
while (true) {
|
|
3476
|
+
let chunk;
|
|
3477
|
+
try {
|
|
3478
|
+
const readPromise = reader.read();
|
|
3479
|
+
while (true) {
|
|
3480
|
+
const timeoutSentinel = Symbol('v3-agent-idle-timeout');
|
|
3481
|
+
const result = idleTimeoutMs > 0
|
|
3482
|
+
? await Promise.race([
|
|
3483
|
+
readPromise,
|
|
3484
|
+
new Promise((resolve) => {
|
|
3485
|
+
setTimeout(() => resolve(timeoutSentinel), idleTimeoutMs);
|
|
3486
|
+
}),
|
|
3487
|
+
])
|
|
3488
|
+
: await readPromise;
|
|
3489
|
+
if (result !== timeoutSentinel) {
|
|
3490
|
+
chunk = result;
|
|
3491
|
+
break;
|
|
3492
|
+
}
|
|
3493
|
+
if (this.hasAgentWorkspaceOutput(context)) {
|
|
3494
|
+
const stalledError = new Error('V3 agent stream stalled after writing workspace output');
|
|
3495
|
+
stalledError.name = 'AbortError';
|
|
3496
|
+
stalledError.partialData = {
|
|
3497
|
+
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3498
|
+
context_id: contextId,
|
|
3499
|
+
result: final,
|
|
3500
|
+
events,
|
|
3501
|
+
files: streamedFiles,
|
|
3502
|
+
};
|
|
3503
|
+
throw stalledError;
|
|
3504
|
+
}
|
|
3179
3505
|
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3506
|
+
}
|
|
3507
|
+
catch (error) {
|
|
3508
|
+
if (error && error.name === 'AbortError') {
|
|
3509
|
+
error.partialData = {
|
|
3184
3510
|
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3185
3511
|
context_id: contextId,
|
|
3186
3512
|
result: final,
|
|
3187
3513
|
events,
|
|
3188
3514
|
files: streamedFiles,
|
|
3189
3515
|
};
|
|
3190
|
-
throw stalledError;
|
|
3191
3516
|
}
|
|
3517
|
+
throw error;
|
|
3192
3518
|
}
|
|
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
|
-
};
|
|
3519
|
+
const { done, value } = chunk;
|
|
3520
|
+
if (done) {
|
|
3521
|
+
break;
|
|
3203
3522
|
}
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3523
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3524
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
3525
|
+
buffer = frames.pop() || '';
|
|
3526
|
+
for (const frame of frames) {
|
|
3527
|
+
const lines = frame.split(/\r?\n/);
|
|
3528
|
+
const dataLines = [];
|
|
3529
|
+
let explicitEventType = null;
|
|
3530
|
+
for (const rawLine of lines) {
|
|
3531
|
+
const line = rawLine.trimEnd();
|
|
3532
|
+
if (!line || line.startsWith(':')) {
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
if (line.startsWith('event:')) {
|
|
3536
|
+
explicitEventType = line.slice(6).trim() || null;
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
if (line.startsWith('data:')) {
|
|
3540
|
+
dataLines.push(line.slice(5).trimStart());
|
|
3541
|
+
}
|
|
3221
3542
|
}
|
|
3222
|
-
|
|
3223
|
-
|
|
3543
|
+
const payload = dataLines.join('\n').trim();
|
|
3544
|
+
if (!payload || payload === '[DONE]') {
|
|
3224
3545
|
continue;
|
|
3225
3546
|
}
|
|
3226
|
-
|
|
3227
|
-
|
|
3547
|
+
let event;
|
|
3548
|
+
try {
|
|
3549
|
+
event = JSON.parse(payload);
|
|
3228
3550
|
}
|
|
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
|
-
|
|
3551
|
+
catch {
|
|
3552
|
+
event = {
|
|
3553
|
+
type: explicitEventType || 'message',
|
|
3554
|
+
content: payload,
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
if (explicitEventType && (!event.type || event.type === 'message')) {
|
|
3558
|
+
event.type = explicitEventType;
|
|
3559
|
+
}
|
|
3560
|
+
const userEvent = this.sanitizeV3AgentEventForUser(event);
|
|
3561
|
+
events.push(userEvent);
|
|
3562
|
+
if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
|
|
3563
|
+
contextId = event.context_id.trim();
|
|
3564
|
+
}
|
|
3565
|
+
if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
|
|
3566
|
+
serverWorkspaceRoot = event.workspace_root.trim();
|
|
3567
|
+
}
|
|
3568
|
+
if (event.type === 'client_tool_request') {
|
|
3569
|
+
const toolTask = this.handleV3ClientToolRequest(event, context, response.url).catch((error) => {
|
|
3570
|
+
this.logger.warn(`Failed to handle V3 client tool request: ${error.message}`);
|
|
3571
|
+
});
|
|
3572
|
+
this.pendingV3ClientToolTasks.add(toolTask);
|
|
3573
|
+
void toolTask.finally(() => {
|
|
3574
|
+
this.pendingV3ClientToolTasks.delete(toolTask);
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
|
|
3578
|
+
this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
|
|
3579
|
+
// Empty workspace soft guard: the first list_directory result can be
|
|
3580
|
+
// transiently empty before remote workspace hydration catches up.
|
|
3581
|
+
// Avoid hard-failing here; let the stream continue.
|
|
3582
|
+
if (event.type === 'tool_result'
|
|
3583
|
+
&& event.name === 'list_directory'
|
|
3584
|
+
&& event.success === true
|
|
3585
|
+
&& serverWorkspaceRoot
|
|
3586
|
+
&& typeof event.output === 'string') {
|
|
3587
|
+
const listOutput = event.output.trim();
|
|
3588
|
+
const looksEmpty = listOutput === `[${serverWorkspaceRoot}]`
|
|
3589
|
+
|| listOutput === `[${serverWorkspaceRoot}/]`
|
|
3590
|
+
|| listOutput === `[${serverWorkspaceRoot}]\\n`
|
|
3591
|
+
|| /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
|
|
3592
|
+
if (looksEmpty) {
|
|
3593
|
+
const localPath = this.resolveAgentTargetPath(context);
|
|
3594
|
+
const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
|
|
3595
|
+
if (localHasFiles) {
|
|
3596
|
+
this.logger?.debug?.('V3 remote workspace initially empty; waiting for hydration instead of aborting.');
|
|
3597
|
+
}
|
|
3275
3598
|
}
|
|
3276
3599
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3600
|
+
if (typeof context.onStreamEvent === 'function') {
|
|
3601
|
+
try {
|
|
3602
|
+
context.onStreamEvent(userEvent);
|
|
3603
|
+
}
|
|
3604
|
+
catch {
|
|
3605
|
+
// Ignore UI callback failures; never break the agent stream for them.
|
|
3606
|
+
}
|
|
3281
3607
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3608
|
+
if (event.type === 'error') {
|
|
3609
|
+
if (event.checkpointed && event.task_id) {
|
|
3610
|
+
// Agent checkpointed — return data so caller can auto-continue
|
|
3611
|
+
return {
|
|
3612
|
+
task_id: event.task_id,
|
|
3613
|
+
context_id: contextId,
|
|
3614
|
+
result: final || event,
|
|
3615
|
+
events,
|
|
3616
|
+
files: streamedFiles,
|
|
3617
|
+
partial: true,
|
|
3618
|
+
checkpointed: true,
|
|
3619
|
+
checkpointed_task_id: event.task_id,
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
if (this.hasAgentWorkspaceOutput(context)) {
|
|
3623
|
+
return {
|
|
3624
|
+
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
3625
|
+
context_id: contextId,
|
|
3626
|
+
result: final || event,
|
|
3627
|
+
events,
|
|
3628
|
+
files: streamedFiles,
|
|
3629
|
+
partial: true,
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
throw new Error(event.message || 'V3 agent returned an error');
|
|
3284
3633
|
}
|
|
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
|
-
};
|
|
3634
|
+
if (event.type === 'complete' || event.type === 'message') {
|
|
3635
|
+
final = event;
|
|
3299
3636
|
}
|
|
3300
|
-
|
|
3637
|
+
// Exit stream early on complete — agent is done; server-side teardown
|
|
3638
|
+
// can hold the connection open for many seconds otherwise.
|
|
3639
|
+
if (event.type === 'complete') {
|
|
3640
|
+
reader.cancel().catch(() => { });
|
|
3301
3641
|
return {
|
|
3302
3642
|
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
3303
3643
|
context_id: contextId,
|
|
3304
|
-
result: final
|
|
3644
|
+
result: final,
|
|
3305
3645
|
events,
|
|
3306
3646
|
files: streamedFiles,
|
|
3307
|
-
|
|
3647
|
+
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3308
3648
|
};
|
|
3309
3649
|
}
|
|
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
3650
|
}
|
|
3328
3651
|
}
|
|
3652
|
+
await Promise.allSettled([...this.pendingV3ClientToolTasks]);
|
|
3653
|
+
return {
|
|
3654
|
+
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3655
|
+
context_id: contextId,
|
|
3656
|
+
result: final,
|
|
3657
|
+
events,
|
|
3658
|
+
files: streamedFiles,
|
|
3659
|
+
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3660
|
+
clientToolErrors: this.getClientToolErrors(),
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
finally {
|
|
3664
|
+
await Promise.allSettled([...this.pendingV3ClientToolTasks]);
|
|
3329
3665
|
}
|
|
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
3666
|
}
|
|
3339
3667
|
async runV3AgentWorkflow(message, context = {}) {
|
|
3340
3668
|
const executionContext = await this.bindExecutionContext(context);
|
|
@@ -3457,14 +3785,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3457
3785
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3458
3786
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3459
3787
|
const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
3460
|
-
return {
|
|
3788
|
+
return this.finalizeV3AgentWorkflowResponse(continuationData, {
|
|
3461
3789
|
content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
|
|
3462
3790
|
taskId: continuationData.task_id || data.task_id || null,
|
|
3463
3791
|
contextId: finalContextId,
|
|
3464
3792
|
backendUrl: baseUrl,
|
|
3465
3793
|
partial: continuationData.checkpointed === true,
|
|
3466
3794
|
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
3467
|
-
};
|
|
3795
|
+
});
|
|
3468
3796
|
}
|
|
3469
3797
|
const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
3470
3798
|
const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
|
|
@@ -3476,13 +3804,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3476
3804
|
errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
|
|
3477
3805
|
continue;
|
|
3478
3806
|
}
|
|
3479
|
-
return {
|
|
3807
|
+
return this.finalizeV3AgentWorkflowResponse(data, {
|
|
3480
3808
|
content: this.formatV3AgentResponse(data),
|
|
3481
3809
|
taskId: data.task_id || null,
|
|
3482
3810
|
contextId,
|
|
3483
3811
|
backendUrl: baseUrl,
|
|
3484
3812
|
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
3485
|
-
};
|
|
3813
|
+
});
|
|
3486
3814
|
}
|
|
3487
3815
|
catch (error) {
|
|
3488
3816
|
if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
|
|
@@ -3490,14 +3818,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3490
3818
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
3491
3819
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3492
3820
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3493
|
-
return {
|
|
3821
|
+
return this.finalizeV3AgentWorkflowResponse(error.partialData, {
|
|
3494
3822
|
content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
|
|
3495
3823
|
taskId: error.partialData.task_id || null,
|
|
3496
3824
|
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
3497
3825
|
backendUrl: baseUrl,
|
|
3498
3826
|
partial: true,
|
|
3499
3827
|
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
|
-
};
|
|
3828
|
+
});
|
|
3501
3829
|
}
|
|
3502
3830
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3503
3831
|
}
|
|
@@ -3559,6 +3887,158 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3559
3887
|
}
|
|
3560
3888
|
throw new Error(errors.join(' | '));
|
|
3561
3889
|
}
|
|
3890
|
+
async startV3BackgroundJob(message, context = {}) {
|
|
3891
|
+
await this.ensureV3ServiceKey();
|
|
3892
|
+
const executionContext = await this.bindExecutionContext({
|
|
3893
|
+
...context,
|
|
3894
|
+
backgroundJob: true,
|
|
3895
|
+
clientToolExecution: false,
|
|
3896
|
+
});
|
|
3897
|
+
const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
|
|
3898
|
+
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
3899
|
+
const body = {
|
|
3900
|
+
request: message,
|
|
3901
|
+
context: this.buildV3AgentContext({
|
|
3902
|
+
...executionContext,
|
|
3903
|
+
backgroundJob: true,
|
|
3904
|
+
clientToolExecution: false,
|
|
3905
|
+
}),
|
|
3906
|
+
stream: false,
|
|
3907
|
+
model: resolvedModel,
|
|
3908
|
+
mcp_context_id: executionContext.mcpContextId || undefined,
|
|
3909
|
+
};
|
|
3910
|
+
const errors = [];
|
|
3911
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3912
|
+
try {
|
|
3913
|
+
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl), {
|
|
3914
|
+
method: 'POST',
|
|
3915
|
+
headers: await this.getV3AgentHeaders(),
|
|
3916
|
+
body: JSON.stringify(body),
|
|
3917
|
+
});
|
|
3918
|
+
if (!response.ok) {
|
|
3919
|
+
errors.push(`${baseUrl}: ${response.status} ${await response.text().catch(() => '')}`);
|
|
3920
|
+
continue;
|
|
3921
|
+
}
|
|
3922
|
+
return await response.json();
|
|
3923
|
+
}
|
|
3924
|
+
catch (error) {
|
|
3925
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
throw new Error(`Unable to start background job: ${errors.join('; ')}`);
|
|
3929
|
+
}
|
|
3930
|
+
async listV3BackgroundJobs(limit = 20) {
|
|
3931
|
+
await this.ensureV3ServiceKey();
|
|
3932
|
+
const errors = [];
|
|
3933
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3934
|
+
try {
|
|
3935
|
+
const response = await fetch(`${this.getV3AgentBackgroundUrl(baseUrl)}?limit=${encodeURIComponent(String(limit))}`, {
|
|
3936
|
+
method: 'GET',
|
|
3937
|
+
headers: await this.getV3AgentHeaders(),
|
|
3938
|
+
});
|
|
3939
|
+
if (!response.ok) {
|
|
3940
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
3941
|
+
continue;
|
|
3942
|
+
}
|
|
3943
|
+
const data = await response.json();
|
|
3944
|
+
return Array.isArray(data.jobs) ? data.jobs : [];
|
|
3945
|
+
}
|
|
3946
|
+
catch (error) {
|
|
3947
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
throw new Error(`Unable to list background jobs: ${errors.join('; ')}`);
|
|
3951
|
+
}
|
|
3952
|
+
async getV3BackgroundJob(jobId) {
|
|
3953
|
+
await this.ensureV3ServiceKey();
|
|
3954
|
+
const errors = [];
|
|
3955
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3956
|
+
try {
|
|
3957
|
+
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, encodeURIComponent(jobId)), {
|
|
3958
|
+
method: 'GET',
|
|
3959
|
+
headers: await this.getV3AgentHeaders(),
|
|
3960
|
+
});
|
|
3961
|
+
if (!response.ok) {
|
|
3962
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
3963
|
+
continue;
|
|
3964
|
+
}
|
|
3965
|
+
return await response.json();
|
|
3966
|
+
}
|
|
3967
|
+
catch (error) {
|
|
3968
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
throw new Error(`Unable to get background job: ${errors.join('; ')}`);
|
|
3972
|
+
}
|
|
3973
|
+
async cancelV3BackgroundJob(jobId) {
|
|
3974
|
+
await this.ensureV3ServiceKey();
|
|
3975
|
+
const errors = [];
|
|
3976
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3977
|
+
try {
|
|
3978
|
+
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/cancel`), {
|
|
3979
|
+
method: 'POST',
|
|
3980
|
+
headers: await this.getV3AgentHeaders(),
|
|
3981
|
+
});
|
|
3982
|
+
if (!response.ok) {
|
|
3983
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
3984
|
+
continue;
|
|
3985
|
+
}
|
|
3986
|
+
return await response.json();
|
|
3987
|
+
}
|
|
3988
|
+
catch (error) {
|
|
3989
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
throw new Error(`Unable to cancel background job: ${errors.join('; ')}`);
|
|
3993
|
+
}
|
|
3994
|
+
async getV3BackgroundJobFiles(jobId) {
|
|
3995
|
+
await this.ensureV3ServiceKey();
|
|
3996
|
+
const errors = [];
|
|
3997
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
3998
|
+
try {
|
|
3999
|
+
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/files`), {
|
|
4000
|
+
method: 'GET',
|
|
4001
|
+
headers: await this.getV3AgentHeaders(),
|
|
4002
|
+
});
|
|
4003
|
+
if (!response.ok) {
|
|
4004
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
4005
|
+
continue;
|
|
4006
|
+
}
|
|
4007
|
+
return await response.json();
|
|
4008
|
+
}
|
|
4009
|
+
catch (error) {
|
|
4010
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
throw new Error(`Unable to fetch background job files: ${errors.join('; ')}`);
|
|
4014
|
+
}
|
|
4015
|
+
async applyV3BackgroundJobFiles(jobId, context = {}) {
|
|
4016
|
+
const data = await this.getV3BackgroundJobFiles(jobId);
|
|
4017
|
+
const rootPath = this.resolveAgentTargetPath({
|
|
4018
|
+
...context,
|
|
4019
|
+
workspacePath: context.workspacePath || data.local_workspace_path,
|
|
4020
|
+
projectPath: context.projectPath || data.local_workspace_path,
|
|
4021
|
+
targetPath: context.targetPath || data.local_workspace_path,
|
|
4022
|
+
});
|
|
4023
|
+
if (!rootPath) {
|
|
4024
|
+
throw new Error('No local workspace path available for applying background job files.');
|
|
4025
|
+
}
|
|
4026
|
+
const applied = [];
|
|
4027
|
+
const skipped = [];
|
|
4028
|
+
for (const [relativePath, content] of Object.entries(data.files || {})) {
|
|
4029
|
+
if (typeof content !== 'string') {
|
|
4030
|
+
skipped.push(relativePath);
|
|
4031
|
+
continue;
|
|
4032
|
+
}
|
|
4033
|
+
if (this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath)) {
|
|
4034
|
+
applied.push(relativePath);
|
|
4035
|
+
}
|
|
4036
|
+
else {
|
|
4037
|
+
skipped.push(relativePath);
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
return { applied, skipped, status: data.status };
|
|
4041
|
+
}
|
|
3562
4042
|
formatOperatorResponse(data = {}) {
|
|
3563
4043
|
// If the server returned a direct answer field, prefer it (for lookup tasks)
|
|
3564
4044
|
if (typeof data.answer === 'string' && data.answer.trim()) {
|
|
@@ -5094,6 +5574,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5094
5574
|
getLastChatTransportErrors() {
|
|
5095
5575
|
return this.lastChatTransportErrors;
|
|
5096
5576
|
}
|
|
5577
|
+
getClientToolErrors() {
|
|
5578
|
+
return [...this.clientToolErrors];
|
|
5579
|
+
}
|
|
5580
|
+
clearAgentRunDiagnostics() {
|
|
5581
|
+
this.clientToolErrors = [];
|
|
5582
|
+
this.pendingV3ClientToolTasks.clear();
|
|
5583
|
+
}
|
|
5584
|
+
recordClientToolError(message) {
|
|
5585
|
+
const trimmed = sanitizeUserFacingErrorText(String(message || '')).slice(0, 280);
|
|
5586
|
+
if (trimmed && !this.clientToolErrors.includes(trimmed)) {
|
|
5587
|
+
this.clientToolErrors.push(trimmed);
|
|
5588
|
+
}
|
|
5589
|
+
}
|
|
5097
5590
|
async submitClientToolResult(contextId, callId, result, backendUrl) {
|
|
5098
5591
|
const trimmedContextId = String(contextId || '').trim();
|
|
5099
5592
|
const trimmedCallId = String(callId || '').trim();
|
|
@@ -5130,6 +5623,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5130
5623
|
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
5131
5624
|
continue;
|
|
5132
5625
|
}
|
|
5626
|
+
if (response.status === 404 && /no pending client tool call/i.test(errorText)) {
|
|
5627
|
+
// Server already consumed or timed out this call — treat as idempotent when local work succeeded.
|
|
5628
|
+
return;
|
|
5629
|
+
}
|
|
5133
5630
|
throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
5134
5631
|
}
|
|
5135
5632
|
}
|