vigthoria-cli 1.10.0 → 1.10.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +99 -17
- package/dist/commands/chat.d.ts +34 -0
- package/dist/commands/chat.js +1162 -61
- package/dist/commands/config.js +11 -2
- package/dist/commands/legion.js +8 -2
- package/dist/commands/wallet.d.ts +25 -0
- package/dist/commands/wallet.js +191 -0
- package/dist/index.js +158 -2
- package/dist/utils/api.d.ts +90 -2
- package/dist/utils/api.js +1730 -266
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.js +53 -12
- package/dist/utils/persona.d.ts +4 -0
- package/dist/utils/persona.js +34 -0
- package/dist/utils/tools.d.ts +5 -0
- package/dist/utils/tools.js +53 -1
- package/dist/utils/workspace-stream.js +56 -15
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +4 -2
- package/scripts/release/validate-no-go-gates.sh +3 -1
package/dist/utils/api.js
CHANGED
|
@@ -9,16 +9,24 @@ import https from 'https';
|
|
|
9
9
|
import net from 'net';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import WebSocket from 'ws';
|
|
12
|
+
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
|
|
13
|
+
export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
|
|
12
14
|
export class CLIError extends Error {
|
|
13
15
|
category;
|
|
14
16
|
statusCode;
|
|
15
17
|
endpoint;
|
|
18
|
+
walletUrl;
|
|
19
|
+
topupUrl;
|
|
20
|
+
balance;
|
|
16
21
|
constructor(message, category, opts) {
|
|
17
22
|
super(message);
|
|
18
23
|
this.name = 'CLIError';
|
|
19
24
|
this.category = category;
|
|
20
25
|
this.statusCode = opts?.statusCode;
|
|
21
26
|
this.endpoint = opts?.endpoint;
|
|
27
|
+
this.walletUrl = opts?.walletUrl;
|
|
28
|
+
this.topupUrl = opts?.topupUrl;
|
|
29
|
+
this.balance = opts?.balance;
|
|
22
30
|
if (opts?.cause)
|
|
23
31
|
this.cause = opts.cause;
|
|
24
32
|
}
|
|
@@ -30,13 +38,25 @@ export function classifyError(error, fallbackCategory = 'network') {
|
|
|
30
38
|
const axErr = error;
|
|
31
39
|
const status = axErr?.response?.status;
|
|
32
40
|
const endpoint = axErr?.config?.url || axErr?.config?.baseURL || '';
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
const responseData = axErr?.response?.data;
|
|
42
|
+
const nestedError = responseData?.error;
|
|
43
|
+
const message = responseData
|
|
44
|
+
? typeof nestedError === 'string'
|
|
45
|
+
? nestedError
|
|
46
|
+
: typeof responseData.message === 'string'
|
|
47
|
+
? responseData.message
|
|
48
|
+
: typeof nestedError === 'object' && nestedError && typeof nestedError.message === 'string'
|
|
49
|
+
? nestedError.message
|
|
50
|
+
: error.message
|
|
39
51
|
: error.message || String(error);
|
|
52
|
+
if (status === 402 || responseData?.errorCode === 'INSUFFICIENT_CREDITS' || responseData?.errorCode === 'WALLET_REQUIRED' || responseData?.errorCode === 'CLOUD_CREDIT_REQUIRED') {
|
|
53
|
+
const walletUrl = String(responseData?.walletUrl || responseData?.topupUrl || VIGTHORIA_HUB_CREDITS_URL);
|
|
54
|
+
const balance = typeof responseData?.balance === 'number' ? responseData.balance : undefined;
|
|
55
|
+
const creditMessage = typeof responseData?.message === 'string'
|
|
56
|
+
? responseData.message
|
|
57
|
+
: `Your cloud credit balance is too low. Add credits: ${walletUrl}`;
|
|
58
|
+
return new CLIError(creditMessage, 'credits', { statusCode: status || 402, endpoint, walletUrl, topupUrl: walletUrl, balance });
|
|
59
|
+
}
|
|
40
60
|
if (status === 401 || status === 403) {
|
|
41
61
|
// Distinguish repo/community auth from model auth
|
|
42
62
|
if (/community|repo/i.test(endpoint)) {
|
|
@@ -64,7 +84,7 @@ export function formatCLIError(err) {
|
|
|
64
84
|
case 'repo_session':
|
|
65
85
|
return `${tag} Repository session expired or missing${err.statusCode ? ` (${err.statusCode})` : ''}. This does not affect AI commands. Re-authenticate repo with: vigthoria repo list`;
|
|
66
86
|
case 'model_backend':
|
|
67
|
-
return
|
|
87
|
+
return VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE;
|
|
68
88
|
case 'bridge':
|
|
69
89
|
return `${tag} Bridge connection error: ${err.message}`;
|
|
70
90
|
case 'network':
|
|
@@ -75,6 +95,8 @@ export function formatCLIError(err) {
|
|
|
75
95
|
return `${tag} Response parsing error: ${err.message}`;
|
|
76
96
|
case 'tool_execution':
|
|
77
97
|
return `${tag} Tool execution error: ${err.message}`;
|
|
98
|
+
case 'credits':
|
|
99
|
+
return `${tag} ${err.message}${err.walletUrl ? `\nTop up credits: ${err.walletUrl}` : ''}`;
|
|
78
100
|
default:
|
|
79
101
|
return `${tag} ${err.message}`;
|
|
80
102
|
}
|
|
@@ -242,6 +264,21 @@ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
|
242
264
|
const parsed = Number.parseInt(rawValue, 10);
|
|
243
265
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
244
266
|
})();
|
|
267
|
+
const DEFAULT_MCP_BIND_TIMEOUT_MS = (() => {
|
|
268
|
+
const rawValue = process.env.VIGTHORIA_MCP_BIND_TIMEOUT_MS || process.env.MCP_BIND_TIMEOUT_MS || '5000';
|
|
269
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
270
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 5000;
|
|
271
|
+
})();
|
|
272
|
+
const DEFAULT_WORKSPACE_SCAN_TIMEOUT_MS = (() => {
|
|
273
|
+
const rawValue = process.env.VIGTHORIA_WORKSPACE_SCAN_TIMEOUT_MS || '2500';
|
|
274
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
275
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 2500;
|
|
276
|
+
})();
|
|
277
|
+
const DEFAULT_WORKSPACE_SCAN_MAX_FILES = (() => {
|
|
278
|
+
const rawValue = process.env.VIGTHORIA_WORKSPACE_SCAN_MAX_FILES || '1800';
|
|
279
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
280
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1800;
|
|
281
|
+
})();
|
|
245
282
|
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
246
283
|
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS;
|
|
247
284
|
if (!rawValue) {
|
|
@@ -250,6 +287,16 @@ const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
|
250
287
|
const parsed = Number.parseInt(rawValue, 10);
|
|
251
288
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
252
289
|
})();
|
|
290
|
+
const DEFAULT_CHAT_CONNECT_TIMEOUT_MS = (() => {
|
|
291
|
+
const rawValue = process.env.VIGTHORIA_CHAT_CONNECT_TIMEOUT_MS || '20000';
|
|
292
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
293
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 20000;
|
|
294
|
+
})();
|
|
295
|
+
const DEFAULT_CHAT_IDLE_TIMEOUT_MS = (() => {
|
|
296
|
+
const rawValue = process.env.VIGTHORIA_CHAT_IDLE_TIMEOUT_MS || '120000';
|
|
297
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
298
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 120000;
|
|
299
|
+
})();
|
|
253
300
|
export class APIClient {
|
|
254
301
|
client;
|
|
255
302
|
modelRouterClient;
|
|
@@ -291,7 +338,7 @@ export class APIClient {
|
|
|
291
338
|
'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
|
|
292
339
|
},
|
|
293
340
|
});
|
|
294
|
-
//
|
|
341
|
+
// Optional dedicated Vigthoria model endpoint for internal/server deployments only.
|
|
295
342
|
const selfHostedModelsApiUrl = this.getSelfHostedModelsApiUrl();
|
|
296
343
|
this.selfHostedModelRouterClient = selfHostedModelsApiUrl ? axios.create({
|
|
297
344
|
baseURL: selfHostedModelsApiUrl,
|
|
@@ -581,16 +628,20 @@ export class APIClient {
|
|
|
581
628
|
}
|
|
582
629
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
583
630
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
584
|
-
const
|
|
585
|
-
||
|
|
586
|
-
|
|
587
|
-
const urls = [
|
|
631
|
+
const includeLoopbackV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1'
|
|
632
|
+
|| (preferLocal && isServerRuntime());
|
|
633
|
+
const localCandidates = [
|
|
588
634
|
process.env.VIGTHORIA_V3_AGENT_URL,
|
|
589
635
|
process.env.V3_AGENT_URL,
|
|
590
|
-
...(
|
|
636
|
+
...(includeLoopbackV3Agent ? ['http://127.0.0.1:8030'] : []),
|
|
637
|
+
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
638
|
+
const remoteCandidates = [
|
|
591
639
|
configuredApiUrl,
|
|
592
640
|
'https://coder.vigthoria.io',
|
|
593
641
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
642
|
+
const urls = preferLocal && localCandidates.length > 0
|
|
643
|
+
? [...localCandidates, ...remoteCandidates]
|
|
644
|
+
: [...remoteCandidates, ...localCandidates];
|
|
594
645
|
return [...new Set(urls)];
|
|
595
646
|
}
|
|
596
647
|
getV3AgentRunUrl(baseUrl) {
|
|
@@ -1043,6 +1094,7 @@ export class APIClient {
|
|
|
1043
1094
|
async getV3AgentHeaders() {
|
|
1044
1095
|
const headers = {
|
|
1045
1096
|
'Content-Type': 'application/json',
|
|
1097
|
+
Accept: 'text/event-stream',
|
|
1046
1098
|
};
|
|
1047
1099
|
const authToken = this.getAccessToken();
|
|
1048
1100
|
if (authToken) {
|
|
@@ -1051,12 +1103,365 @@ export class APIClient {
|
|
|
1051
1103
|
}
|
|
1052
1104
|
const serviceKey = process.env.VIGTHORIA_V3_SERVICE_KEY
|
|
1053
1105
|
|| process.env.V3_SERVICE_KEY
|
|
1054
|
-
|| process.env.HYPERLOOP_SERVICE_KEY
|
|
1106
|
+
|| process.env.HYPERLOOP_SERVICE_KEY
|
|
1107
|
+
|| this.config.get('v3ServiceKey');
|
|
1055
1108
|
if (serviceKey) {
|
|
1056
1109
|
headers['X-Service-Key'] = serviceKey;
|
|
1057
1110
|
}
|
|
1058
1111
|
return headers;
|
|
1059
1112
|
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Ensure the V3 service key is available in config.
|
|
1115
|
+
* If not already set via env or stored config, fetches it from the Coder API
|
|
1116
|
+
* using the current user's auth token and caches it for this session and beyond.
|
|
1117
|
+
*/
|
|
1118
|
+
async ensureV3ServiceKey() {
|
|
1119
|
+
const existing = process.env.VIGTHORIA_V3_SERVICE_KEY
|
|
1120
|
+
|| process.env.V3_SERVICE_KEY
|
|
1121
|
+
|| process.env.HYPERLOOP_SERVICE_KEY
|
|
1122
|
+
|| this.config.get('v3ServiceKey');
|
|
1123
|
+
if (existing)
|
|
1124
|
+
return;
|
|
1125
|
+
try {
|
|
1126
|
+
const token = this.getAccessToken();
|
|
1127
|
+
if (!token)
|
|
1128
|
+
return;
|
|
1129
|
+
const response = await this.client.get('/api/cli/v3-service-key', {
|
|
1130
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1131
|
+
timeout: 6000,
|
|
1132
|
+
});
|
|
1133
|
+
const key = response.data?.v3ServiceKey;
|
|
1134
|
+
if (key) {
|
|
1135
|
+
this.config.set('v3ServiceKey', key);
|
|
1136
|
+
this.logger.debug('V3 service key retrieved and cached from Coder API.');
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
// Non-fatal: fall through; the health check will surface the auth error.
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Fast preflight for local agent loop — probes model backends in parallel
|
|
1145
|
+
* so users see a clear pass/fail before "Planning..." hangs on slow routes.
|
|
1146
|
+
*/
|
|
1147
|
+
async runChatModelPreflight(requestedModel = 'agent') {
|
|
1148
|
+
const routes = [];
|
|
1149
|
+
const probes = [
|
|
1150
|
+
{
|
|
1151
|
+
name: 'Vigthoria Coder API',
|
|
1152
|
+
run: async () => {
|
|
1153
|
+
const health = await this.getCoderHealth();
|
|
1154
|
+
return { ok: health.ok, error: health.error };
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
name: 'Vigthoria Models API',
|
|
1159
|
+
run: async () => {
|
|
1160
|
+
const health = await this.getModelsHealth();
|
|
1161
|
+
return { ok: health.ok, error: health.error };
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
];
|
|
1165
|
+
const selfHostedHealth = await this.getSelfHostedHealth();
|
|
1166
|
+
if (selfHostedHealth) {
|
|
1167
|
+
probes.unshift({
|
|
1168
|
+
name: selfHostedHealth.name || 'Dedicated Models API',
|
|
1169
|
+
run: async () => ({ ok: selfHostedHealth.ok, error: selfHostedHealth.error }),
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
if (this.isSelfHostedPreferredModel(this.resolveModelId(requestedModel), requestedModel)) {
|
|
1173
|
+
// Agent/code models should also accept V3 health as a usable inference path.
|
|
1174
|
+
probes.push({
|
|
1175
|
+
name: 'V3 Agent API',
|
|
1176
|
+
run: async () => {
|
|
1177
|
+
const v3 = await this.runV3HealthCheck();
|
|
1178
|
+
return { ok: v3.healthy, error: v3.error };
|
|
1179
|
+
},
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
const results = await Promise.all(probes.map(async (probe) => {
|
|
1183
|
+
try {
|
|
1184
|
+
const result = await probe.run();
|
|
1185
|
+
routes.push({ name: probe.name, ok: result.ok, error: result.error });
|
|
1186
|
+
return result.ok;
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
const message = error.message || String(error);
|
|
1190
|
+
routes.push({ name: probe.name, ok: false, error: message });
|
|
1191
|
+
return false;
|
|
1192
|
+
}
|
|
1193
|
+
}));
|
|
1194
|
+
const healthy = results.some(Boolean);
|
|
1195
|
+
const inferenceRoutes = routes.filter((route) => route.name !== 'V3 Agent API');
|
|
1196
|
+
const firstHealthy = inferenceRoutes.find((route) => route.ok) || routes.find((route) => route.ok);
|
|
1197
|
+
return {
|
|
1198
|
+
healthy,
|
|
1199
|
+
endpoint: firstHealthy?.name || 'api.vigthoria.io',
|
|
1200
|
+
error: healthy
|
|
1201
|
+
? undefined
|
|
1202
|
+
: 'No Vigthoria model backend responded during preflight. Run `vigthoria login` and check your internet connection.',
|
|
1203
|
+
routes,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
async runV3HealthCheck(options = {}) {
|
|
1207
|
+
await this.ensureV3ServiceKey();
|
|
1208
|
+
const endpoints = this.getV3AgentBaseUrls(false);
|
|
1209
|
+
const headers = await this.getV3AgentHeaders();
|
|
1210
|
+
const perEndpointTimeoutMs = 8000;
|
|
1211
|
+
const probe = async (baseUrl) => {
|
|
1212
|
+
const runUrl = this.getV3AgentRunUrl(baseUrl);
|
|
1213
|
+
const healthUrl = /127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)
|
|
1214
|
+
? `${baseUrl}/health`
|
|
1215
|
+
: `${baseUrl}/api/v3-agent/health`;
|
|
1216
|
+
try {
|
|
1217
|
+
const response = await fetch(healthUrl, {
|
|
1218
|
+
method: 'GET',
|
|
1219
|
+
headers,
|
|
1220
|
+
signal: AbortSignal.timeout(perEndpointTimeoutMs),
|
|
1221
|
+
});
|
|
1222
|
+
if (response.status === 401 || response.status === 403) {
|
|
1223
|
+
return {
|
|
1224
|
+
healthy: false,
|
|
1225
|
+
endpoint: runUrl,
|
|
1226
|
+
error: 'V3 service rejected authentication. Run: vigthoria login',
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
if (response.ok || response.status === 405) {
|
|
1230
|
+
return { healthy: true, endpoint: runUrl };
|
|
1231
|
+
}
|
|
1232
|
+
if (response.status === 502 || response.status === 503 || response.status === 504) {
|
|
1233
|
+
return {
|
|
1234
|
+
healthy: false,
|
|
1235
|
+
endpoint: runUrl,
|
|
1236
|
+
error: `V3 agent proxy returned ${response.status}. The V3 Code Agent service may be restarting or overloaded.`,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
healthy: false,
|
|
1241
|
+
endpoint: runUrl,
|
|
1242
|
+
error: `V3 agent health returned ${response.status}`,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
catch (error) {
|
|
1246
|
+
const reason = error?.message || String(error);
|
|
1247
|
+
if (/timeout|aborted|timed out/i.test(reason)) {
|
|
1248
|
+
return {
|
|
1249
|
+
healthy: false,
|
|
1250
|
+
endpoint: runUrl,
|
|
1251
|
+
error: options.soft
|
|
1252
|
+
? 'V3 health probe timed out; continuing with agent stream.'
|
|
1253
|
+
: 'V3 agent health check timed out. The service may be busy with another run or temporarily overloaded.',
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
healthy: false,
|
|
1258
|
+
endpoint: runUrl,
|
|
1259
|
+
error: reason,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
const results = await Promise.all(endpoints.map((baseUrl) => probe(baseUrl)));
|
|
1264
|
+
const healthy = results.find((entry) => entry.healthy);
|
|
1265
|
+
if (healthy) {
|
|
1266
|
+
return healthy;
|
|
1267
|
+
}
|
|
1268
|
+
const timedOut = results.find((entry) => /timed out/i.test(entry.error || ''));
|
|
1269
|
+
if (timedOut) {
|
|
1270
|
+
return timedOut;
|
|
1271
|
+
}
|
|
1272
|
+
return results[0] || {
|
|
1273
|
+
healthy: false,
|
|
1274
|
+
endpoint: this.getV3AgentRunUrl(endpoints[0] || 'https://coder.vigthoria.io'),
|
|
1275
|
+
error: 'V3 agent service is not responding.',
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
resolveChatConnectTimeoutMs(options = {}) {
|
|
1279
|
+
if (options.connectTimeoutMs && options.connectTimeoutMs > 0) {
|
|
1280
|
+
return options.connectTimeoutMs;
|
|
1281
|
+
}
|
|
1282
|
+
if (options.timeoutMs && options.timeoutMs > 0 && options.timeoutMs <= 60000) {
|
|
1283
|
+
return options.timeoutMs;
|
|
1284
|
+
}
|
|
1285
|
+
return DEFAULT_CHAT_CONNECT_TIMEOUT_MS;
|
|
1286
|
+
}
|
|
1287
|
+
resolveChatIdleTimeoutMs(options = {}) {
|
|
1288
|
+
if (options.idleTimeoutMs && options.idleTimeoutMs > 0) {
|
|
1289
|
+
return options.idleTimeoutMs;
|
|
1290
|
+
}
|
|
1291
|
+
if (options.timeoutMs && options.timeoutMs > 60000) {
|
|
1292
|
+
return options.timeoutMs;
|
|
1293
|
+
}
|
|
1294
|
+
return DEFAULT_CHAT_IDLE_TIMEOUT_MS;
|
|
1295
|
+
}
|
|
1296
|
+
mapPreflightEndpointToRoute(endpoint) {
|
|
1297
|
+
const normalized = String(endpoint || '').toLowerCase();
|
|
1298
|
+
if (normalized.includes('coder'))
|
|
1299
|
+
return 'coder';
|
|
1300
|
+
if (normalized.includes('models'))
|
|
1301
|
+
return 'models';
|
|
1302
|
+
if (normalized.includes('dedicated') || normalized.includes('self-hosted') || normalized.includes('selfhosted')) {
|
|
1303
|
+
return 'selfhosted';
|
|
1304
|
+
}
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
isCanonicalCoderDuplicate() {
|
|
1308
|
+
const apiUrl = String(this.config.get('apiUrl') || '').replace(/\/$/, '').toLowerCase();
|
|
1309
|
+
return apiUrl === 'https://coder.vigthoria.io';
|
|
1310
|
+
}
|
|
1311
|
+
async consumeOpenAIStreamResponse(response, idleTimeoutMs, onDelta) {
|
|
1312
|
+
if (!response.body) {
|
|
1313
|
+
throw new Error('Streaming response had no body');
|
|
1314
|
+
}
|
|
1315
|
+
const reader = response.body.getReader();
|
|
1316
|
+
const decoder = new TextDecoder();
|
|
1317
|
+
let buffer = '';
|
|
1318
|
+
let content = '';
|
|
1319
|
+
let lastActivity = Date.now();
|
|
1320
|
+
const waitForChunk = async () => {
|
|
1321
|
+
const remainingIdle = idleTimeoutMs - (Date.now() - lastActivity);
|
|
1322
|
+
if (idleTimeoutMs > 0 && remainingIdle <= 0) {
|
|
1323
|
+
throw new Error('Model stream stalled (no output)');
|
|
1324
|
+
}
|
|
1325
|
+
const readPromise = reader.read();
|
|
1326
|
+
if (idleTimeoutMs <= 0) {
|
|
1327
|
+
return readPromise;
|
|
1328
|
+
}
|
|
1329
|
+
return Promise.race([
|
|
1330
|
+
readPromise,
|
|
1331
|
+
new Promise((_, reject) => {
|
|
1332
|
+
setTimeout(() => reject(new Error('Model stream stalled (no output)')), Math.max(1000, remainingIdle));
|
|
1333
|
+
}),
|
|
1334
|
+
]);
|
|
1335
|
+
};
|
|
1336
|
+
while (true) {
|
|
1337
|
+
const { done, value } = await waitForChunk();
|
|
1338
|
+
if (done) {
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
lastActivity = Date.now();
|
|
1342
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1343
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
1344
|
+
buffer = frames.pop() || '';
|
|
1345
|
+
for (const frame of frames) {
|
|
1346
|
+
const dataLines = frame
|
|
1347
|
+
.split(/\r?\n/)
|
|
1348
|
+
.filter((line) => line.startsWith('data:'))
|
|
1349
|
+
.map((line) => line.slice(5).trimStart());
|
|
1350
|
+
const payload = dataLines.join('\n').trim();
|
|
1351
|
+
if (!payload || payload === '[DONE]') {
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
const parsed = JSON.parse(payload);
|
|
1356
|
+
const delta = parsed?.choices?.[0]?.delta?.content
|
|
1357
|
+
|| parsed?.choices?.[0]?.message?.content
|
|
1358
|
+
|| parsed?.choices?.[0]?.text
|
|
1359
|
+
|| '';
|
|
1360
|
+
if (typeof delta === 'string' && delta) {
|
|
1361
|
+
content += delta;
|
|
1362
|
+
onDelta?.(delta);
|
|
1363
|
+
lastActivity = Date.now();
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
// Ignore malformed stream frames.
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return content.trim();
|
|
1372
|
+
}
|
|
1373
|
+
async tryStreamingModelRouterChat(baseURL, messages, resolvedModel, requestedModel, headers, options) {
|
|
1374
|
+
const connectTimeoutMs = this.resolveChatConnectTimeoutMs(options);
|
|
1375
|
+
const idleTimeoutMs = this.resolveChatIdleTimeoutMs(options);
|
|
1376
|
+
const controller = new AbortController();
|
|
1377
|
+
const connectTimer = setTimeout(() => controller.abort(), connectTimeoutMs);
|
|
1378
|
+
try {
|
|
1379
|
+
const response = await fetch(`${baseURL.replace(/\/$/, '')}/v1/chat/completions`, {
|
|
1380
|
+
method: 'POST',
|
|
1381
|
+
headers: {
|
|
1382
|
+
'Content-Type': 'application/json',
|
|
1383
|
+
...headers,
|
|
1384
|
+
},
|
|
1385
|
+
body: JSON.stringify({
|
|
1386
|
+
model: resolvedModel,
|
|
1387
|
+
messages,
|
|
1388
|
+
max_tokens: this.config.get('preferences').maxTokens,
|
|
1389
|
+
temperature: 0.7,
|
|
1390
|
+
stream: true,
|
|
1391
|
+
}),
|
|
1392
|
+
signal: controller.signal,
|
|
1393
|
+
});
|
|
1394
|
+
if (!response.ok) {
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
clearTimeout(connectTimer);
|
|
1398
|
+
const content = await this.consumeOpenAIStreamResponse(response, idleTimeoutMs, options.onStreamDelta);
|
|
1399
|
+
if (!content) {
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
return {
|
|
1403
|
+
id: `vigthoria-stream-${Date.now()}`,
|
|
1404
|
+
message: content,
|
|
1405
|
+
model: resolvedModel || requestedModel,
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
catch (error) {
|
|
1409
|
+
clearTimeout(connectTimer);
|
|
1410
|
+
throw error;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
async runV3AgentAuthPreflight(baseUrl, body, executionContext) {
|
|
1414
|
+
const endpoint = this.getV3AgentRunUrl(baseUrl);
|
|
1415
|
+
const healthEndpoint = /127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)
|
|
1416
|
+
? `${baseUrl}/health`
|
|
1417
|
+
: `${baseUrl}/api/v3-agent/health`;
|
|
1418
|
+
const headers = await this.getV3AgentHeaders();
|
|
1419
|
+
headers['X-Vigthoria-Preflight'] = '1';
|
|
1420
|
+
if (executionContext.mcpContextId) {
|
|
1421
|
+
headers['X-MCP-Context-Id'] = String(executionContext.mcpContextId);
|
|
1422
|
+
}
|
|
1423
|
+
const controller = new AbortController();
|
|
1424
|
+
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
1425
|
+
try {
|
|
1426
|
+
const response = await fetch(healthEndpoint, {
|
|
1427
|
+
method: 'GET',
|
|
1428
|
+
headers,
|
|
1429
|
+
signal: controller.signal,
|
|
1430
|
+
});
|
|
1431
|
+
if (response.status === 401 || response.status === 403) {
|
|
1432
|
+
const errorText = await response.text().catch(() => '');
|
|
1433
|
+
return {
|
|
1434
|
+
ok: false,
|
|
1435
|
+
status: response.status,
|
|
1436
|
+
endpoint: healthEndpoint,
|
|
1437
|
+
reason: sanitizeUserFacingErrorText(errorText).slice(0, 200) || 'Unauthorized',
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
// Any non-auth response means token acceptance passed at the V3 service layer.
|
|
1441
|
+
return { ok: true, status: response.status, endpoint: healthEndpoint };
|
|
1442
|
+
}
|
|
1443
|
+
catch (error) {
|
|
1444
|
+
const reason = sanitizeUserFacingErrorText(error?.message || String(error)).slice(0, 200);
|
|
1445
|
+
// Preflight should not block agent execution on transient timeout/abort network conditions.
|
|
1446
|
+
if (/aborted|timeout|timed out|network/i.test(reason)) {
|
|
1447
|
+
return {
|
|
1448
|
+
ok: true,
|
|
1449
|
+
status: 0,
|
|
1450
|
+
endpoint: healthEndpoint,
|
|
1451
|
+
reason,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
ok: false,
|
|
1456
|
+
status: 0,
|
|
1457
|
+
endpoint: healthEndpoint,
|
|
1458
|
+
reason,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
finally {
|
|
1462
|
+
clearTimeout(timeoutId);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1060
1465
|
async executeV3AgentRunRequest(baseUrl, body, executionContext, signal) {
|
|
1061
1466
|
const makeRequest = async () => {
|
|
1062
1467
|
const headers = await this.getV3AgentHeaders();
|
|
@@ -1262,7 +1667,8 @@ export class APIClient {
|
|
|
1262
1667
|
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
1263
1668
|
const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
|
|
1264
1669
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1265
|
-
const
|
|
1670
|
+
const prompt = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || '').trim();
|
|
1671
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, { prompt });
|
|
1266
1672
|
const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
|
|
1267
1673
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
1268
1674
|
const localWorkspaceName = localWorkspacePath ? path.basename(localWorkspacePath) : null;
|
|
@@ -1314,6 +1720,46 @@ export class APIClient {
|
|
|
1314
1720
|
requestStartedAt: resolvedContext.requestStartedAt,
|
|
1315
1721
|
subscriptionPlan: this.config.getNormalizedPlan() || null,
|
|
1316
1722
|
email: this.config.get('email') || null,
|
|
1723
|
+
rawPrompt: prompt || null,
|
|
1724
|
+
promptFocusTerms: prompt ? this.extractPromptFocusTerms(prompt) : [],
|
|
1725
|
+
vigthoriaBrain: resolvedContext.vigthoriaBrain
|
|
1726
|
+
? this.trimVigthoriaBrainForTransport(resolvedContext.vigthoriaBrain)
|
|
1727
|
+
: null,
|
|
1728
|
+
clientToolExecution: resolvedContext.clientToolExecution === true
|
|
1729
|
+
|| (resolvedContext.localMachineCapable !== false
|
|
1730
|
+
&& (resolvedContext.executionSurface === 'cli' || resolvedContext.clientSurface === 'cli')
|
|
1731
|
+
&& !!localWorkspacePath
|
|
1732
|
+
&& !serverWorkspacePath),
|
|
1733
|
+
clientToolPathRules: (resolvedContext.clientToolExecution === true || (resolvedContext.localMachineCapable !== false
|
|
1734
|
+
&& (resolvedContext.executionSurface === 'cli' || resolvedContext.clientSurface === 'cli')
|
|
1735
|
+
&& !!localWorkspacePath
|
|
1736
|
+
&& !serverWorkspacePath))
|
|
1737
|
+
? [
|
|
1738
|
+
'Client-side read tools execute on the user real machine via the CLI.',
|
|
1739
|
+
'Use paths relative to the workspace root only.',
|
|
1740
|
+
'Never prefix tool paths with workspace/ or vigthoria://workspace/.',
|
|
1741
|
+
`The workspace root folder name is "${localWorkspaceName || 'project'}". Do not repeat it in paths.`,
|
|
1742
|
+
'Examples: ".", "Vigthoria-dominion/README.md", "vigthoria-dominion-win/".',
|
|
1743
|
+
].join(' ')
|
|
1744
|
+
: null,
|
|
1745
|
+
executionHints: {
|
|
1746
|
+
...(resolvedContext.agentTaskType === 'analysis'
|
|
1747
|
+
? {
|
|
1748
|
+
max_iterations: Number.parseInt(process.env.VIGTHORIA_ANALYSIS_MAX_ITERATIONS || '15', 10) || 15,
|
|
1749
|
+
analysis_guidance: this.buildAnalysisGuidance(Array.isArray(localWorkspaceSummary?.promptFocusDirectories)
|
|
1750
|
+
? localWorkspaceSummary.promptFocusDirectories
|
|
1751
|
+
: [], Array.isArray(localWorkspaceSummary?.mandatoryReadPaths)
|
|
1752
|
+
? localWorkspaceSummary.mandatoryReadPaths
|
|
1753
|
+
: []),
|
|
1754
|
+
}
|
|
1755
|
+
: {}),
|
|
1756
|
+
...(['implementation', 'game-build', 'web-build', 'build', 'fix'].includes(String(resolvedContext.agentTaskType || '').toLowerCase())
|
|
1757
|
+
? {
|
|
1758
|
+
requires_file_changes: true,
|
|
1759
|
+
task_kind: String(resolvedContext.agentTaskType || 'implementation').toLowerCase(),
|
|
1760
|
+
}
|
|
1761
|
+
: {}),
|
|
1762
|
+
},
|
|
1317
1763
|
};
|
|
1318
1764
|
return this.compactV3Context(payload);
|
|
1319
1765
|
}
|
|
@@ -1341,9 +1787,15 @@ export class APIClient {
|
|
|
1341
1787
|
const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
|
|
1342
1788
|
const budget = LIMIT - overhead - 512; // reserve a little headroom
|
|
1343
1789
|
if (budget > 0) {
|
|
1790
|
+
const focusTerms = Array.isArray(payload.promptFocusTerms) ? payload.promptFocusTerms : [];
|
|
1791
|
+
const focusDirectories = Array.isArray(payload.localWorkspaceSummary?.promptFocusDirectories)
|
|
1792
|
+
? payload.localWorkspaceSummary.promptFocusDirectories
|
|
1793
|
+
: [];
|
|
1794
|
+
const sortedEntries = [...fileEntries].sort((a, b) => this.scoreWorkspacePathForHydration(b[0], focusTerms, focusDirectories)
|
|
1795
|
+
- this.scoreWorkspacePathForHydration(a[0], focusTerms, focusDirectories));
|
|
1344
1796
|
const trimmed = {};
|
|
1345
1797
|
let used = 2; // {}
|
|
1346
|
-
for (const [k, v] of
|
|
1798
|
+
for (const [k, v] of sortedEntries) {
|
|
1347
1799
|
const entryLen = JSON.stringify(k).length + 1 + JSON.stringify(v).length + 1;
|
|
1348
1800
|
if (used + entryLen > budget)
|
|
1349
1801
|
break;
|
|
@@ -1909,7 +2361,8 @@ menu {
|
|
|
1909
2361
|
const headers = await this.getMcpHeaders();
|
|
1910
2362
|
const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
|
|
1911
2363
|
const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
|
|
1912
|
-
const
|
|
2364
|
+
const prompt = String(executionContext.rawPrompt || executionContext.contextualPrompt || '').trim();
|
|
2365
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, { prompt });
|
|
1913
2366
|
const metadata = {
|
|
1914
2367
|
source: 'vigthoria-cli',
|
|
1915
2368
|
sharedContextId: executionContext.contextId,
|
|
@@ -1945,6 +2398,7 @@ menu {
|
|
|
1945
2398
|
method: 'PUT',
|
|
1946
2399
|
headers,
|
|
1947
2400
|
body: JSON.stringify({ data }),
|
|
2401
|
+
signal: AbortSignal.timeout(DEFAULT_MCP_BIND_TIMEOUT_MS),
|
|
1948
2402
|
});
|
|
1949
2403
|
if (!response.ok) {
|
|
1950
2404
|
const errorText = await response.text().catch(() => '');
|
|
@@ -1976,6 +2430,7 @@ menu {
|
|
|
1976
2430
|
userId: this.config.get('userId') || this.config.get('email') || 'vigthoria-cli',
|
|
1977
2431
|
metadata,
|
|
1978
2432
|
}),
|
|
2433
|
+
signal: AbortSignal.timeout(DEFAULT_MCP_BIND_TIMEOUT_MS),
|
|
1979
2434
|
});
|
|
1980
2435
|
if (!createResponse.ok) {
|
|
1981
2436
|
const errorText = await createResponse.text().catch(() => '');
|
|
@@ -2050,6 +2505,7 @@ menu {
|
|
|
2050
2505
|
}
|
|
2051
2506
|
const localName = paths.localWorkspacePath ? path.basename(paths.localWorkspacePath) : null;
|
|
2052
2507
|
const serverBindableWorkspace = !!paths.serverWorkspacePath || runtime.serverBindableWorkspace === true;
|
|
2508
|
+
const isLocalMachine = !serverBindableWorkspace;
|
|
2053
2509
|
return {
|
|
2054
2510
|
osPlatform: runtime.osPlatform || null,
|
|
2055
2511
|
platform: runtime.platform || null,
|
|
@@ -2060,17 +2516,273 @@ menu {
|
|
|
2060
2516
|
workspaceName: localName,
|
|
2061
2517
|
workspacePath: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
|
|
2062
2518
|
cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
|
|
2519
|
+
localExecutionRequired: isLocalMachine,
|
|
2520
|
+
toolExecutionSurface: isLocalMachine ? 'cli-local' : 'server-workspace',
|
|
2521
|
+
executionGuidance: isLocalMachine
|
|
2522
|
+
? 'The user runs Vigthoria CLI on their local machine. Real files and folders live on the client filesystem. Server workspace tools only see a partial hydrated copy — read `.vigthoria-workspace-index.md` and `promptFocusDirectories` before guessing root-level README.md. Prefer localWorkspaceSummary for structure and tell the user when live local tool execution is required.'
|
|
2523
|
+
: null,
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
extractPromptFocusTerms(prompt) {
|
|
2527
|
+
const terms = new Set();
|
|
2528
|
+
const normalized = String(prompt || '').trim();
|
|
2529
|
+
if (!normalized) {
|
|
2530
|
+
return [];
|
|
2531
|
+
}
|
|
2532
|
+
for (const match of normalized.matchAll(/["'`]([^"'`]{2,120})["'`]/g)) {
|
|
2533
|
+
terms.add(match[1].trim());
|
|
2534
|
+
}
|
|
2535
|
+
for (const match of normalized.matchAll(/(?:project|game|folder|directory|path|repo(?:sitory)?)\s+([A-Za-z0-9][\w ._-]{2,80})/gi)) {
|
|
2536
|
+
terms.add(match[1].trim());
|
|
2537
|
+
}
|
|
2538
|
+
for (const match of normalized.matchAll(/(?:investigate|find|explore|look at|check|inspect|analyse|analyze)\s+(?:the\s+)?([A-Za-z0-9][\w ._-]{2,80}?)(?:\s+(?:folder|directory|foulder|dir|project|game))?\b/gi)) {
|
|
2539
|
+
terms.add(match[1].trim());
|
|
2540
|
+
}
|
|
2541
|
+
for (const match of normalized.matchAll(/\b([A-Z][a-z0-9]+(?:\s+[A-Z][a-z0-9]+)+)\b/g)) {
|
|
2542
|
+
const phrase = match[1].trim();
|
|
2543
|
+
if (phrase.length <= 48) {
|
|
2544
|
+
terms.add(phrase);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
for (const match of normalized.matchAll(/\/([\w .-]{2,80})/g)) {
|
|
2548
|
+
const segment = match[1].trim();
|
|
2549
|
+
if (!/^(users|home|desktop|documents|var|www|c)$/i.test(segment)) {
|
|
2550
|
+
terms.add(segment);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return [...terms].filter((term) => term.length >= 3 && term.length <= 48).slice(0, 8);
|
|
2554
|
+
}
|
|
2555
|
+
buildTopLevelWorkspaceLayout(rootPath) {
|
|
2556
|
+
const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cache', '__pycache__', '.venv', 'venv']);
|
|
2557
|
+
const layout = { directories: [], files: [] };
|
|
2558
|
+
try {
|
|
2559
|
+
for (const entry of fs.readdirSync(rootPath, { withFileTypes: true })) {
|
|
2560
|
+
if (entry.name.startsWith('.')) {
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
if (entry.isDirectory()) {
|
|
2564
|
+
if (!skipDirs.has(entry.name)) {
|
|
2565
|
+
layout.directories.push(entry.name);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
else if (entry.isFile()) {
|
|
2569
|
+
layout.files.push(entry.name);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
catch {
|
|
2574
|
+
return layout;
|
|
2575
|
+
}
|
|
2576
|
+
layout.directories.sort((a, b) => a.localeCompare(b));
|
|
2577
|
+
layout.files.sort((a, b) => a.localeCompare(b));
|
|
2578
|
+
return layout;
|
|
2579
|
+
}
|
|
2580
|
+
normalizeFocusPathKey(value) {
|
|
2581
|
+
return String(value || '').toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
2582
|
+
}
|
|
2583
|
+
resolvePromptFocusDirectories(rootPath, focusTerms) {
|
|
2584
|
+
const layout = this.buildTopLevelWorkspaceLayout(rootPath);
|
|
2585
|
+
const matches = new Set();
|
|
2586
|
+
const normalizedTerms = focusTerms.map((term) => term.toLowerCase());
|
|
2587
|
+
const normalizedTermKeys = focusTerms.map((term) => this.normalizeFocusPathKey(term));
|
|
2588
|
+
for (const dir of layout.directories) {
|
|
2589
|
+
const dirLower = dir.toLowerCase();
|
|
2590
|
+
const dirKey = this.normalizeFocusPathKey(dir);
|
|
2591
|
+
for (let index = 0; index < normalizedTerms.length; index += 1) {
|
|
2592
|
+
const term = normalizedTerms[index];
|
|
2593
|
+
const termKey = normalizedTermKeys[index];
|
|
2594
|
+
if (dirLower === term
|
|
2595
|
+
|| dirLower.includes(term)
|
|
2596
|
+
|| term.includes(dirLower)
|
|
2597
|
+
|| (termKey && (dirKey === termKey || dirKey.includes(termKey) || termKey.includes(dirKey)))) {
|
|
2598
|
+
matches.add(`${dir.replace(/\\/g, '/')}/`);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
for (const term of focusTerms) {
|
|
2603
|
+
const candidate = path.join(rootPath, term);
|
|
2604
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
2605
|
+
matches.add(`${term.replace(/\\/g, '/')}/`);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
return [...matches];
|
|
2609
|
+
}
|
|
2610
|
+
buildMandatoryFocusReadPaths(rootPath, focusDirectories) {
|
|
2611
|
+
const candidates = [];
|
|
2612
|
+
const pushIfExists = (relativePath) => {
|
|
2613
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
2614
|
+
const absolutePath = path.join(rootPath, normalized);
|
|
2615
|
+
if (fs.existsSync(absolutePath)) {
|
|
2616
|
+
candidates.push(normalized);
|
|
2617
|
+
}
|
|
2063
2618
|
};
|
|
2619
|
+
for (const focusDir of focusDirectories) {
|
|
2620
|
+
const base = focusDir.replace(/\\/g, '/').replace(/\/$/, '');
|
|
2621
|
+
pushIfExists(`${base}/package.json`);
|
|
2622
|
+
pushIfExists(`${base}/game.js`);
|
|
2623
|
+
pushIfExists(`${base}/src/Game.js`);
|
|
2624
|
+
pushIfExists(`${base}/src/factions/FactionModels.js`);
|
|
2625
|
+
pushIfExists(`${base}/GAME_DESIGN_DOCUMENT.md`);
|
|
2626
|
+
}
|
|
2627
|
+
const layout = this.buildTopLevelWorkspaceLayout(rootPath);
|
|
2628
|
+
for (const dir of layout.directories) {
|
|
2629
|
+
if (/dominion/i.test(dir) && /-win|desktop|electron/i.test(dir)) {
|
|
2630
|
+
pushIfExists(`${dir.replace(/\\/g, '/')}/`);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return [...new Set(candidates)];
|
|
2634
|
+
}
|
|
2635
|
+
buildAnalysisGuidance(focusDirectories, mandatoryReadPaths) {
|
|
2636
|
+
const focusLabel = focusDirectories.length > 0
|
|
2637
|
+
? focusDirectories.map((entry) => entry.replace(/\/$/, '')).join(', ')
|
|
2638
|
+
: 'the requested project folder';
|
|
2639
|
+
const reads = mandatoryReadPaths.length > 0
|
|
2640
|
+
? mandatoryReadPaths.slice(0, 12).map((entry) => `- \`${entry}\``).join('\n')
|
|
2641
|
+
: '- package.json\n- game.js\n- src/Game.js\n- src/factions/\n- GAME_DESIGN_DOCUMENT.md';
|
|
2642
|
+
return [
|
|
2643
|
+
`The user asked for a grounded overview of ${focusLabel}.`,
|
|
2644
|
+
'Do NOT summarize from root README.md, PRODUCTION_ROADMAP.md, or PROJECT_STATUS_REPORT.md alone — they may be stale or AI-generated.',
|
|
2645
|
+
'Read these code-first paths before writing your final answer:',
|
|
2646
|
+
reads,
|
|
2647
|
+
'MANDATORY: Compare README claims against package.json, game.js, and src/. If they disagree, your final answer MUST:',
|
|
2648
|
+
'1) State what the game actually is based on code,',
|
|
2649
|
+
'2) Quote what README incorrectly claims,',
|
|
2650
|
+
'3) Explicitly say "README is inconsistent with the codebase" and recommend fixing it.',
|
|
2651
|
+
'List `vigthoria-dominion-win/` (or similar) if present to confirm desktop/Electron distribution.',
|
|
2652
|
+
].join('\n');
|
|
2653
|
+
}
|
|
2654
|
+
collectFocusDirectoryFilePaths(rootPath, focusDirRelative, maxFiles = 250) {
|
|
2655
|
+
const focusRoot = path.join(rootPath, focusDirRelative);
|
|
2656
|
+
if (!fs.existsSync(focusRoot)) {
|
|
2657
|
+
return [];
|
|
2658
|
+
}
|
|
2659
|
+
const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cache', '__pycache__', '.venv', 'venv']);
|
|
2660
|
+
const results = [];
|
|
2661
|
+
const stack = [{ abs: focusRoot, rel: focusDirRelative.replace(/\\/g, '/') }];
|
|
2662
|
+
while (stack.length > 0 && results.length < maxFiles) {
|
|
2663
|
+
const current = stack.pop();
|
|
2664
|
+
if (!current) {
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2667
|
+
let entries;
|
|
2668
|
+
try {
|
|
2669
|
+
entries = fs.readdirSync(current.abs, { withFileTypes: true });
|
|
2670
|
+
}
|
|
2671
|
+
catch {
|
|
2672
|
+
continue;
|
|
2673
|
+
}
|
|
2674
|
+
for (const entry of entries) {
|
|
2675
|
+
if (results.length >= maxFiles) {
|
|
2676
|
+
break;
|
|
2677
|
+
}
|
|
2678
|
+
const rel = `${current.rel}${current.rel.endsWith('/') ? '' : '/'}${entry.name}`.replace(/\\/g, '/');
|
|
2679
|
+
const abs = path.join(current.abs, entry.name);
|
|
2680
|
+
if (entry.isDirectory()) {
|
|
2681
|
+
if (!skipDirs.has(entry.name) && !entry.name.startsWith('.')) {
|
|
2682
|
+
stack.push({ abs, rel: `${rel}/` });
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
else if (entry.isFile()) {
|
|
2686
|
+
results.push(rel);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
return results;
|
|
2691
|
+
}
|
|
2692
|
+
mergeWorkspacePathCandidates(primary, extras) {
|
|
2693
|
+
const seen = new Set();
|
|
2694
|
+
const merged = [];
|
|
2695
|
+
for (const candidate of [...extras, ...primary]) {
|
|
2696
|
+
const normalized = candidate.replace(/\\/g, '/');
|
|
2697
|
+
if (!normalized || seen.has(normalized)) {
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
seen.add(normalized);
|
|
2701
|
+
merged.push(normalized);
|
|
2702
|
+
}
|
|
2703
|
+
return merged;
|
|
2704
|
+
}
|
|
2705
|
+
scoreWorkspacePathForHydration(filePath, focusTerms, focusDirectories = []) {
|
|
2706
|
+
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
2707
|
+
let score = 0;
|
|
2708
|
+
if (normalized === '.vigthoria-workspace-index.md') {
|
|
2709
|
+
score += 1000;
|
|
2710
|
+
}
|
|
2711
|
+
if (normalized.endsWith('package.json')) {
|
|
2712
|
+
score += 360;
|
|
2713
|
+
}
|
|
2714
|
+
if (/(^|\/)game\.js$/i.test(normalized) || /\/src\/game\.js$/i.test(normalized)) {
|
|
2715
|
+
score += 350;
|
|
2716
|
+
}
|
|
2717
|
+
if (/\/src\/factions\//i.test(normalized) || /factionmodels\.js/i.test(normalized)) {
|
|
2718
|
+
score += 340;
|
|
2719
|
+
}
|
|
2720
|
+
if (/game_design_document\.md/i.test(normalized)) {
|
|
2721
|
+
score += 320;
|
|
2722
|
+
}
|
|
2723
|
+
if (normalized.endsWith('readme.md')) {
|
|
2724
|
+
score += focusDirectories.some((dir) => normalized.startsWith(dir.toLowerCase())) ? 120 : 40;
|
|
2725
|
+
}
|
|
2726
|
+
if (/production_roadmap|project_status_report|status_report|roadmap\.md/i.test(normalized)) {
|
|
2727
|
+
score += 20;
|
|
2728
|
+
}
|
|
2729
|
+
if (/\.(js|ts|tsx|jsx|mjs|cjs)$/i.test(normalized)) {
|
|
2730
|
+
score += 100;
|
|
2731
|
+
}
|
|
2732
|
+
if (/\.(md|txt|json|yaml|yml|toml)$/i.test(normalized)) {
|
|
2733
|
+
score += 40;
|
|
2734
|
+
}
|
|
2735
|
+
for (const dir of focusDirectories.map((entry) => entry.toLowerCase())) {
|
|
2736
|
+
if (normalized.startsWith(dir)) {
|
|
2737
|
+
score += 250;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
for (const term of focusTerms) {
|
|
2741
|
+
const termLower = term.toLowerCase();
|
|
2742
|
+
const termKey = this.normalizeFocusPathKey(term);
|
|
2743
|
+
if (normalized.includes(termLower) || (termKey && normalized.replace(/[\s_./\\-]+/g, '').includes(termKey))) {
|
|
2744
|
+
score += 80;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return score;
|
|
2748
|
+
}
|
|
2749
|
+
sortWorkspacePathsForHydration(filePaths, focusTerms, focusDirectories) {
|
|
2750
|
+
return [...filePaths].sort((a, b) => this.scoreWorkspacePathForHydration(b, focusTerms, focusDirectories)
|
|
2751
|
+
- this.scoreWorkspacePathForHydration(a, focusTerms, focusDirectories));
|
|
2752
|
+
}
|
|
2753
|
+
trimVigthoriaBrainForTransport(brain, maxChars = 12000) {
|
|
2754
|
+
try {
|
|
2755
|
+
const serialized = typeof brain === 'string' ? brain : JSON.stringify(brain);
|
|
2756
|
+
if (serialized.length <= maxChars) {
|
|
2757
|
+
return typeof brain === 'string' ? brain : JSON.parse(serialized);
|
|
2758
|
+
}
|
|
2759
|
+
return {
|
|
2760
|
+
truncated: true,
|
|
2761
|
+
excerpt: serialized.slice(0, maxChars),
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
catch {
|
|
2765
|
+
return null;
|
|
2766
|
+
}
|
|
2064
2767
|
}
|
|
2065
|
-
buildLocalWorkspaceSummary(rootPath) {
|
|
2768
|
+
buildLocalWorkspaceSummary(rootPath, options = {}) {
|
|
2066
2769
|
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
2067
2770
|
return null;
|
|
2068
2771
|
}
|
|
2069
2772
|
try {
|
|
2773
|
+
const prompt = String(options.prompt || '').trim();
|
|
2774
|
+
const focusTerms = prompt ? this.extractPromptFocusTerms(prompt) : [];
|
|
2775
|
+
const topLevelLayout = this.buildTopLevelWorkspaceLayout(rootPath);
|
|
2776
|
+
const focusDirectories = this.resolvePromptFocusDirectories(rootPath, focusTerms);
|
|
2777
|
+
const mandatoryReadPaths = this.buildMandatoryFocusReadPaths(rootPath, focusDirectories);
|
|
2070
2778
|
const summary = {
|
|
2071
2779
|
path: 'vigthoria://workspace/',
|
|
2072
2780
|
name: path.basename(rootPath),
|
|
2073
2781
|
files: [],
|
|
2782
|
+
topLevelLayout,
|
|
2783
|
+
promptFocusTerms: focusTerms,
|
|
2784
|
+
promptFocusDirectories: focusDirectories,
|
|
2785
|
+
mandatoryReadPaths,
|
|
2074
2786
|
};
|
|
2075
2787
|
const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
|
|
2076
2788
|
summary.fileCount = snapshot.fileCount;
|
|
@@ -2087,12 +2799,34 @@ menu {
|
|
|
2087
2799
|
};
|
|
2088
2800
|
}
|
|
2089
2801
|
const readmePath = path.join(rootPath, 'README.md');
|
|
2090
|
-
if (fs.existsSync(readmePath)) {
|
|
2802
|
+
if (focusDirectories.length === 0 && fs.existsSync(readmePath)) {
|
|
2091
2803
|
summary.readmeExcerpt = fs.readFileSync(readmePath, 'utf8').slice(0, 2500);
|
|
2092
2804
|
}
|
|
2805
|
+
else if (focusDirectories.length > 0) {
|
|
2806
|
+
summary.readmeWarning = 'Root README and planning docs are often stale or agent-generated. Prefer package.json, game.js, src/, and GAME_DESIGN_DOCUMENT.md inside the focus folder.';
|
|
2807
|
+
}
|
|
2808
|
+
for (const focusDir of focusDirectories) {
|
|
2809
|
+
const focusReadme = path.join(rootPath, focusDir, 'README.md');
|
|
2810
|
+
if (fs.existsSync(focusReadme)) {
|
|
2811
|
+
summary.focusReadmeWarning = summary.focusReadmeWarning || {};
|
|
2812
|
+
summary.focusReadmeWarning[`${focusDir}README.md`.replace(/\\/g, '/')] =
|
|
2813
|
+
'Subfolder README may be stale or wrong. Prefer package.json, game.js, src/, and GAME_DESIGN_DOCUMENT.md.';
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
const focusPaths = [
|
|
2817
|
+
...mandatoryReadPaths,
|
|
2818
|
+
...focusDirectories.flatMap((focusDir) => this.collectFocusDirectoryFilePaths(rootPath, focusDir)),
|
|
2819
|
+
];
|
|
2820
|
+
const hydrationPaths = this.sortWorkspacePathsForHydration(this.mergeWorkspacePathCandidates(snapshot.paths, focusPaths), focusTerms, focusDirectories);
|
|
2093
2821
|
// Hydrate workspace: include actual file contents so the V3 server
|
|
2094
2822
|
// can populate the remote workspace before the agent starts.
|
|
2095
|
-
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath,
|
|
2823
|
+
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, hydrationPaths);
|
|
2824
|
+
summary.workspaceFiles['.vigthoria-workspace-index.md'] = this.buildWorkspaceIndexFile(rootPath, snapshot, {
|
|
2825
|
+
focusTerms,
|
|
2826
|
+
focusDirectories,
|
|
2827
|
+
topLevelLayout,
|
|
2828
|
+
mandatoryReadPaths,
|
|
2829
|
+
});
|
|
2096
2830
|
return summary;
|
|
2097
2831
|
}
|
|
2098
2832
|
catch (error) {
|
|
@@ -2100,6 +2834,61 @@ menu {
|
|
|
2100
2834
|
return null;
|
|
2101
2835
|
}
|
|
2102
2836
|
}
|
|
2837
|
+
buildWorkspaceIndexFile(rootPath, snapshot, options = {}) {
|
|
2838
|
+
const workspaceName = path.basename(rootPath);
|
|
2839
|
+
const listedPaths = snapshot.paths.slice(0, 1800);
|
|
2840
|
+
const focusTerms = options.focusTerms || [];
|
|
2841
|
+
const focusDirectories = options.focusDirectories || [];
|
|
2842
|
+
const mandatoryReadPaths = options.mandatoryReadPaths || [];
|
|
2843
|
+
const topLevelLayout = options.topLevelLayout || this.buildTopLevelWorkspaceLayout(rootPath);
|
|
2844
|
+
const lines = [
|
|
2845
|
+
'# Vigthoria Local Workspace Index',
|
|
2846
|
+
'',
|
|
2847
|
+
'This file is generated by the Vigthoria CLI so the remote V3 agent can search and understand the local workspace layout without seeing private machine paths.',
|
|
2848
|
+
'Read this file before guessing paths like README.md at the workspace root.',
|
|
2849
|
+
'README.md, PRODUCTION_ROADMAP.md, and PROJECT_STATUS_REPORT.md may be stale or AI-generated. Trust package.json, game.js, src/, and GAME_DESIGN_DOCUMENT.md instead.',
|
|
2850
|
+
'',
|
|
2851
|
+
'## Path rules for client-side tools',
|
|
2852
|
+
'- The workspace root is already bound to the user folder below.',
|
|
2853
|
+
'- Use relative paths from that root: `.`, `Vigthoria-dominion/`, `README.md`.',
|
|
2854
|
+
'- NEVER prefix paths with `workspace/` — that alias does not exist on the user machine.',
|
|
2855
|
+
'- Do NOT repeat the workspace folder name in paths.',
|
|
2856
|
+
'',
|
|
2857
|
+
`Workspace name: ${workspaceName}`,
|
|
2858
|
+
`Indexed files: ${snapshot.fileCount}`,
|
|
2859
|
+
'',
|
|
2860
|
+
'## Top-level layout',
|
|
2861
|
+
...(topLevelLayout.directories.length > 0
|
|
2862
|
+
? topLevelLayout.directories.map((dir) => `- ${dir}/`)
|
|
2863
|
+
: ['- (no subdirectories listed)']),
|
|
2864
|
+
...(topLevelLayout.files.length > 0
|
|
2865
|
+
? ['', '## Top-level files', ...topLevelLayout.files.map((file) => `- ${file}`)]
|
|
2866
|
+
: []),
|
|
2867
|
+
];
|
|
2868
|
+
if (focusTerms.length > 0 || focusDirectories.length > 0) {
|
|
2869
|
+
lines.push('', '## Prompt focus');
|
|
2870
|
+
if (focusTerms.length > 0) {
|
|
2871
|
+
lines.push(`User focus terms: ${focusTerms.join(', ')}`);
|
|
2872
|
+
}
|
|
2873
|
+
if (focusDirectories.length > 0) {
|
|
2874
|
+
lines.push('Likely project folders:');
|
|
2875
|
+
for (const focusDir of focusDirectories) {
|
|
2876
|
+
lines.push(`- ${focusDir}`);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
if (mandatoryReadPaths.length > 0) {
|
|
2880
|
+
lines.push('', '## Mandatory code-first reads (before summarizing)');
|
|
2881
|
+
for (const readPath of mandatoryReadPaths.slice(0, 12)) {
|
|
2882
|
+
lines.push(`- ${readPath}`);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
lines.push('', '## Files', ...listedPaths.map((filePath) => `- ${filePath.replace(/\\/g, '/')}`));
|
|
2887
|
+
if (snapshot.fileCount > listedPaths.length) {
|
|
2888
|
+
lines.push('', `Index truncated: ${snapshot.fileCount - listedPaths.length} additional files were not listed.`);
|
|
2889
|
+
}
|
|
2890
|
+
return `${lines.join('\n')}\n`;
|
|
2891
|
+
}
|
|
2103
2892
|
/**
|
|
2104
2893
|
* Collect text file contents from the workspace for V3 agent hydration.
|
|
2105
2894
|
* Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
|
|
@@ -2152,14 +2941,24 @@ menu {
|
|
|
2152
2941
|
if (!root || !fs.existsSync(root)) {
|
|
2153
2942
|
return false;
|
|
2154
2943
|
}
|
|
2944
|
+
const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cache', '__pycache__', '.venv', 'venv']);
|
|
2945
|
+
const startedAt = Date.now();
|
|
2155
2946
|
const stack = [root];
|
|
2947
|
+
let scannedDirs = 0;
|
|
2156
2948
|
while (stack.length > 0) {
|
|
2949
|
+
if (Date.now() - startedAt > DEFAULT_WORKSPACE_SCAN_TIMEOUT_MS) {
|
|
2950
|
+
return false;
|
|
2951
|
+
}
|
|
2157
2952
|
const current = stack.pop();
|
|
2158
2953
|
if (!current)
|
|
2159
2954
|
continue;
|
|
2955
|
+
scannedDirs += 1;
|
|
2956
|
+
if (scannedDirs > DEFAULT_WORKSPACE_SCAN_MAX_FILES) {
|
|
2957
|
+
return false;
|
|
2958
|
+
}
|
|
2160
2959
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
2161
2960
|
for (const entry of entries) {
|
|
2162
|
-
if (
|
|
2961
|
+
if (skipDirs.has(entry.name)) {
|
|
2163
2962
|
continue;
|
|
2164
2963
|
}
|
|
2165
2964
|
const fullPath = path.join(current, entry.name);
|
|
@@ -2178,16 +2977,24 @@ menu {
|
|
|
2178
2977
|
return false;
|
|
2179
2978
|
}
|
|
2180
2979
|
getAgentWorkspaceSnapshot(rootPath) {
|
|
2980
|
+
const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cache', '__pycache__', '.venv', 'venv']);
|
|
2181
2981
|
const stack = [rootPath];
|
|
2982
|
+
const startedAt = Date.now();
|
|
2182
2983
|
let fileCount = 0;
|
|
2183
2984
|
const entries = [];
|
|
2184
2985
|
while (stack.length > 0) {
|
|
2986
|
+
if (Date.now() - startedAt > DEFAULT_WORKSPACE_SCAN_TIMEOUT_MS) {
|
|
2987
|
+
break;
|
|
2988
|
+
}
|
|
2989
|
+
if (entries.length >= DEFAULT_WORKSPACE_SCAN_MAX_FILES) {
|
|
2990
|
+
break;
|
|
2991
|
+
}
|
|
2185
2992
|
const current = stack.pop();
|
|
2186
2993
|
if (!current) {
|
|
2187
2994
|
continue;
|
|
2188
2995
|
}
|
|
2189
2996
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
2190
|
-
if (
|
|
2997
|
+
if (skipDirs.has(entry.name)) {
|
|
2191
2998
|
continue;
|
|
2192
2999
|
}
|
|
2193
3000
|
const fullPath = path.join(current, entry.name);
|
|
@@ -2201,6 +3008,9 @@ menu {
|
|
|
2201
3008
|
fileCount += 1;
|
|
2202
3009
|
const stat = fs.statSync(fullPath);
|
|
2203
3010
|
entries.push(`${path.relative(rootPath, fullPath)}:${stat.size}:${stat.mtimeMs}`);
|
|
3011
|
+
if (entries.length >= DEFAULT_WORKSPACE_SCAN_MAX_FILES) {
|
|
3012
|
+
break;
|
|
3013
|
+
}
|
|
2204
3014
|
}
|
|
2205
3015
|
}
|
|
2206
3016
|
entries.sort();
|
|
@@ -2843,14 +3653,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2843
3653
|
if (typeof result === 'string') {
|
|
2844
3654
|
return sanitizeUserFacingPathText(result);
|
|
2845
3655
|
}
|
|
2846
|
-
if (typeof result?.summary === 'string' && result.summary.trim()) {
|
|
3656
|
+
if (typeof result?.summary === 'string' && result.summary.trim() && !this.isGenericV3AgentSummary(result.summary)) {
|
|
2847
3657
|
return sanitizeUserFacingPathText(result.summary);
|
|
2848
3658
|
}
|
|
2849
|
-
if (typeof result?.message === 'string' && result.message.trim()) {
|
|
3659
|
+
if (typeof result?.message === 'string' && result.message.trim() && !this.isGenericV3AgentSummary(result.message)) {
|
|
2850
3660
|
return sanitizeUserFacingPathText(result.message);
|
|
2851
3661
|
}
|
|
2852
3662
|
if (Array.isArray(data?.events)) {
|
|
2853
|
-
const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim());
|
|
3663
|
+
const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim() && !this.isGenericV3AgentSummary(event.summary));
|
|
2854
3664
|
if (completionEvent) {
|
|
2855
3665
|
return sanitizeUserFacingPathText(completionEvent.summary);
|
|
2856
3666
|
}
|
|
@@ -2860,77 +3670,463 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2860
3670
|
}
|
|
2861
3671
|
// Synthesize a grounded answer from the tool-call evidence the
|
|
2862
3672
|
// agent produced, rather than dumping the raw event trace.
|
|
2863
|
-
const answer = this.synthesizeAnswerFromV3Events(data.events);
|
|
3673
|
+
const answer = this.synthesizeAnswerFromV3Events(data.events, data.liveToolEvidence);
|
|
2864
3674
|
if (answer) {
|
|
2865
3675
|
return answer;
|
|
2866
3676
|
}
|
|
2867
3677
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
3678
|
+
if (Array.isArray(data?.liveToolEvidence) && data.liveToolEvidence.length > 0) {
|
|
3679
|
+
const answer = this.synthesizeAnswerFromV3Events([], data.liveToolEvidence);
|
|
3680
|
+
if (answer) {
|
|
3681
|
+
return answer;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
// Last resort: if data has files written, report them.
|
|
3685
|
+
if (data?.files && typeof data.files === 'object' && Object.keys(data.files).length > 0) {
|
|
2870
3686
|
const fileList = Object.keys(data.files).join(', ');
|
|
2871
3687
|
return `Agent wrote workspace files: ${sanitizeUserFacingPathText(fileList)}`;
|
|
2872
3688
|
}
|
|
2873
3689
|
const text = sanitizeUserFacingPathText(JSON.stringify(data, null, 2));
|
|
2874
3690
|
return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
|
|
2875
3691
|
}
|
|
3692
|
+
isGenericV3AgentSummary(value) {
|
|
3693
|
+
const text = String(value || '').trim();
|
|
3694
|
+
if (!text)
|
|
3695
|
+
return true;
|
|
3696
|
+
return /^(task completed|v3 agent workflow completed\.?|agent run finished|workflow completed\.?|max iterations \(\d+\) reached after\b)/i.test(text);
|
|
3697
|
+
}
|
|
3698
|
+
isV3StreamStatusMessage(value) {
|
|
3699
|
+
const text = String(value || '').trim();
|
|
3700
|
+
if (!text)
|
|
3701
|
+
return true;
|
|
3702
|
+
return /^(the agent produced an in-progress note|continuing to finish cleanly|a previous tool call failed|max iterations \(\d+\) reached)/i.test(text)
|
|
3703
|
+
|| /pending blocker|not been resolved|instead of a final report/i.test(text);
|
|
3704
|
+
}
|
|
3705
|
+
scoreEvidenceFilePath(filePath) {
|
|
3706
|
+
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3707
|
+
let score = 0;
|
|
3708
|
+
if (normalized.endsWith('package.json'))
|
|
3709
|
+
score += 100;
|
|
3710
|
+
if (/(^|\/)game\.js$/i.test(normalized) || /\/src\/game\.js$/i.test(normalized))
|
|
3711
|
+
score += 95;
|
|
3712
|
+
if (/\/src\/factions\//i.test(normalized) || /factionmodels\.js/i.test(normalized))
|
|
3713
|
+
score += 90;
|
|
3714
|
+
if (/game_design_document\.md/i.test(normalized))
|
|
3715
|
+
score += 85;
|
|
3716
|
+
if (/\.(js|ts|tsx|jsx|mjs|cjs)$/i.test(normalized))
|
|
3717
|
+
score += 70;
|
|
3718
|
+
if (normalized.endsWith('index.html'))
|
|
3719
|
+
score += 50;
|
|
3720
|
+
if (/production_roadmap|project_status_report|status_report/i.test(normalized))
|
|
3721
|
+
score += 5;
|
|
3722
|
+
if (normalized.endsWith('readme.md'))
|
|
3723
|
+
score += normalized.includes('vigthoria-dominion') ? 25 : 10;
|
|
3724
|
+
return score;
|
|
3725
|
+
}
|
|
3726
|
+
isLowTrustDocPath(filePath) {
|
|
3727
|
+
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3728
|
+
return normalized.endsWith('readme.md')
|
|
3729
|
+
|| /production_roadmap|project_status_report|status_report|roadmap\.md/i.test(normalized);
|
|
3730
|
+
}
|
|
3731
|
+
extractCodeSignalsFromEvidence(filesRead, liveToolEvidence = []) {
|
|
3732
|
+
const signals = new Set();
|
|
3733
|
+
const ingest = (filePath, excerpt) => {
|
|
3734
|
+
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3735
|
+
const text = String(excerpt || '');
|
|
3736
|
+
const lower = text.toLowerCase();
|
|
3737
|
+
if (normalized.endsWith('package.json')) {
|
|
3738
|
+
const match = text.match(/"description"\s*:\s*"([^"]+)"/i);
|
|
3739
|
+
if (match?.[1])
|
|
3740
|
+
signals.add(`package.json: ${match[1]}`);
|
|
3741
|
+
if (/rts|real-time strategy|command & conquer/i.test(text)) {
|
|
3742
|
+
signals.add('package.json identifies a real-time strategy game project.');
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
if (/(^|\/)game\.js$/i.test(normalized) || /\/src\/game\.js$/i.test(normalized)) {
|
|
3746
|
+
if (/rts|real-time strategy|command & conquer|dune 2000|3d rts/i.test(lower)) {
|
|
3747
|
+
signals.add('game.js entry identifies a 3D RTS engine (Command & Conquer / Dune 2000 style).');
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
if (/\/src\/factions\//i.test(normalized) || /factionmodels\.js/i.test(normalized)) {
|
|
3751
|
+
const factions = ['Iron Dominion', 'Nova Collective', 'Aqua Tide', 'Swarm Hive']
|
|
3752
|
+
.filter((name) => lower.includes(name.toLowerCase()));
|
|
3753
|
+
if (factions.length >= 2) {
|
|
3754
|
+
signals.add(`Faction code references: ${factions.join(', ')} (4-faction RTS).`);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
if (/vigthoria-dominion-win/i.test(normalized) || /dist-desktop|electron/i.test(lower)) {
|
|
3758
|
+
signals.add('Desktop/Electron build artifacts are present (full desktop RTS distribution).');
|
|
3759
|
+
}
|
|
3760
|
+
};
|
|
3761
|
+
for (const entry of filesRead)
|
|
3762
|
+
ingest(entry.path, entry.excerpt);
|
|
3763
|
+
for (const entry of liveToolEvidence) {
|
|
3764
|
+
if (entry?.success === false)
|
|
3765
|
+
continue;
|
|
3766
|
+
ingest(String(entry.target || entry.arguments?.path || ''), String(entry.output || ''));
|
|
3767
|
+
}
|
|
3768
|
+
return [...signals];
|
|
3769
|
+
}
|
|
3770
|
+
classifyGameGenreFromText(text) {
|
|
3771
|
+
const lower = String(text || '').toLowerCase();
|
|
3772
|
+
if (/card game|deck-building|dominion is a deck|treasure cards?|victory points?|buy phase|action phase/i.test(lower)) {
|
|
3773
|
+
return 'card/deck-building game';
|
|
3774
|
+
}
|
|
3775
|
+
if (/rts|real-time strategy|command & conquer|dune 2000|3d rts|build bases|gather resources|train units/i.test(lower)) {
|
|
3776
|
+
return '3D real-time strategy (RTS) game';
|
|
3777
|
+
}
|
|
3778
|
+
if (/turn-based strategy|4x strategy/i.test(lower)) {
|
|
3779
|
+
return 'turn-based strategy game';
|
|
3780
|
+
}
|
|
3781
|
+
if (/platformer|roguelike|puzzle game/i.test(lower)) {
|
|
3782
|
+
const match = lower.match(/(platformer|roguelike|puzzle game)/i);
|
|
3783
|
+
return match?.[1] || null;
|
|
3784
|
+
}
|
|
3785
|
+
return null;
|
|
3786
|
+
}
|
|
3787
|
+
extractReadmeGenreClaims(filesRead, liveToolEvidence = []) {
|
|
3788
|
+
const claims = [];
|
|
3789
|
+
const ingest = (filePath, excerpt) => {
|
|
3790
|
+
const normalized = String(filePath || '').replace(/\\/g, '/');
|
|
3791
|
+
if (!/readme\.md$/i.test(normalized))
|
|
3792
|
+
return;
|
|
3793
|
+
const genre = this.classifyGameGenreFromText(excerpt);
|
|
3794
|
+
if (!genre)
|
|
3795
|
+
return;
|
|
3796
|
+
const lines = String(excerpt || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
3797
|
+
const quoteLine = lines.find((line) => /card game|deck|rts|real-time strategy|strategy game|browser game|3d/i.test(line))
|
|
3798
|
+
|| lines.slice(0, 6).join(' ');
|
|
3799
|
+
claims.push({
|
|
3800
|
+
path: normalized,
|
|
3801
|
+
genre,
|
|
3802
|
+
quote: quoteLine.slice(0, 220),
|
|
3803
|
+
});
|
|
3804
|
+
};
|
|
3805
|
+
for (const entry of filesRead)
|
|
3806
|
+
ingest(entry.path, entry.excerpt);
|
|
3807
|
+
for (const entry of liveToolEvidence) {
|
|
3808
|
+
if (entry?.success === false)
|
|
3809
|
+
continue;
|
|
3810
|
+
ingest(String(entry.target || entry.arguments?.path || ''), String(entry.output || ''));
|
|
3811
|
+
}
|
|
3812
|
+
return claims;
|
|
3813
|
+
}
|
|
3814
|
+
extractCodeGenreClaims(filesRead, liveToolEvidence = []) {
|
|
3815
|
+
const sources = [];
|
|
3816
|
+
const factions = new Set();
|
|
3817
|
+
let genre = null;
|
|
3818
|
+
const ingest = (filePath, excerpt) => {
|
|
3819
|
+
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3820
|
+
const text = String(excerpt || '');
|
|
3821
|
+
const detected = this.classifyGameGenreFromText(text);
|
|
3822
|
+
const isCodeFile = normalized.endsWith('package.json')
|
|
3823
|
+
|| /(^|\/)game\.js$/i.test(normalized)
|
|
3824
|
+
|| /\/src\//i.test(normalized);
|
|
3825
|
+
if (detected && isCodeFile) {
|
|
3826
|
+
genre = genre || detected;
|
|
3827
|
+
sources.push(normalized);
|
|
3828
|
+
}
|
|
3829
|
+
if (/\/src\/factions\//i.test(normalized) || /factionmodels\.js/i.test(normalized)) {
|
|
3830
|
+
for (const name of ['Iron Dominion', 'Nova Collective', 'Aqua Tide', 'Swarm Hive']) {
|
|
3831
|
+
if (text.toLowerCase().includes(name.toLowerCase()))
|
|
3832
|
+
factions.add(name);
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
for (const entry of filesRead)
|
|
3837
|
+
ingest(entry.path, entry.excerpt);
|
|
3838
|
+
for (const entry of liveToolEvidence) {
|
|
3839
|
+
if (entry?.success === false)
|
|
3840
|
+
continue;
|
|
3841
|
+
ingest(String(entry.target || entry.arguments?.path || ''), String(entry.output || ''));
|
|
3842
|
+
}
|
|
3843
|
+
return { genre, sources: [...new Set(sources)], factions: [...factions] };
|
|
3844
|
+
}
|
|
3845
|
+
buildReadmeCodeConsistencySection(filesRead, liveToolEvidence = []) {
|
|
3846
|
+
const readmeClaims = this.extractReadmeGenreClaims(filesRead, liveToolEvidence);
|
|
3847
|
+
const codeClaims = this.extractCodeGenreClaims(filesRead, liveToolEvidence);
|
|
3848
|
+
if (readmeClaims.length === 0 && !codeClaims.genre) {
|
|
3849
|
+
return null;
|
|
3850
|
+
}
|
|
3851
|
+
const lines = ['## README vs code — consistency check'];
|
|
3852
|
+
if (codeClaims.genre) {
|
|
3853
|
+
lines.push('', '**What the game actually is (from code):**');
|
|
3854
|
+
lines.push(`- Genre: **${codeClaims.genre}**`);
|
|
3855
|
+
if (codeClaims.factions.length >= 2) {
|
|
3856
|
+
lines.push(`- Factions in source: ${codeClaims.factions.join(', ')}`);
|
|
3857
|
+
}
|
|
3858
|
+
if (codeClaims.sources.length > 0) {
|
|
3859
|
+
lines.push(`- Evidence: ${codeClaims.sources.slice(0, 4).map((entry) => `\`${entry}\``).join(', ')}`);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
else {
|
|
3863
|
+
lines.push('', '**What the game actually is (from code):** _Not determined — core code files were not read yet._');
|
|
3864
|
+
}
|
|
3865
|
+
if (readmeClaims.length > 0) {
|
|
3866
|
+
lines.push('', '**What README says:**');
|
|
3867
|
+
for (const claim of readmeClaims.slice(0, 4)) {
|
|
3868
|
+
lines.push(`- \`${claim.path}\` → **${claim.genre}**`);
|
|
3869
|
+
if (claim.quote)
|
|
3870
|
+
lines.push(` > "${claim.quote}"`);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
const readmeGenres = new Set(readmeClaims.map((entry) => entry.genre));
|
|
3874
|
+
const hasConflict = codeClaims.genre
|
|
3875
|
+
&& readmeClaims.some((entry) => entry.genre !== codeClaims.genre);
|
|
3876
|
+
if (hasConflict && codeClaims.genre) {
|
|
3877
|
+
lines.push('', '**Status: INCONSISTENT** — README documentation contradicts the codebase.', '', `The project code identifies this as a **${codeClaims.genre}**, but README describes it differently (${[...readmeGenres].join('; ')}).`, 'When reporting to the user, state the **code truth first**, then explicitly call out that README is wrong or stale and should be corrected.');
|
|
3878
|
+
}
|
|
3879
|
+
else if (codeClaims.genre && readmeClaims.length > 0) {
|
|
3880
|
+
lines.push('', '**Status: CONSISTENT** — README and code agree on the project type.');
|
|
3881
|
+
}
|
|
3882
|
+
else if (readmeClaims.length > 0 && !codeClaims.genre) {
|
|
3883
|
+
lines.push('', '**Status: UNVERIFIED** — README was read but core code was not inspected.', 'Do not present README claims as fact until package.json, game.js, and src/ are read.');
|
|
3884
|
+
}
|
|
3885
|
+
return lines.join('\n');
|
|
3886
|
+
}
|
|
3887
|
+
detectDocReliabilityWarnings(filesRead, liveToolEvidence = []) {
|
|
3888
|
+
const warnings = [];
|
|
3889
|
+
let subfolderCardGameReadme = false;
|
|
3890
|
+
let codeSaysRts = false;
|
|
3891
|
+
let readRootPlanningDocs = false;
|
|
3892
|
+
let readCoreCode = false;
|
|
3893
|
+
const inspect = (filePath, excerpt) => {
|
|
3894
|
+
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3895
|
+
const lower = String(excerpt || '').toLowerCase();
|
|
3896
|
+
if (/production_roadmap|project_status_report|status_report/i.test(normalized)) {
|
|
3897
|
+
readRootPlanningDocs = true;
|
|
3898
|
+
}
|
|
3899
|
+
if (/vigthoria-dominion\/readme\.md$/i.test(normalized) && /card game|deck-building|dominion is a deck/i.test(lower)) {
|
|
3900
|
+
subfolderCardGameReadme = true;
|
|
3901
|
+
}
|
|
3902
|
+
if (/readme\.md$/i.test(normalized) && /card game|deck-building|dominion is a deck/i.test(lower)) {
|
|
3903
|
+
subfolderCardGameReadme = true;
|
|
3904
|
+
}
|
|
3905
|
+
if (/(^|\/)game\.js$/i.test(normalized) || /\/src\/game\.js$/i.test(normalized) || normalized.endsWith('package.json')) {
|
|
3906
|
+
if (/rts|real-time strategy|command & conquer|3d rts/i.test(lower)) {
|
|
3907
|
+
codeSaysRts = true;
|
|
3908
|
+
readCoreCode = true;
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
if (/\/src\//i.test(normalized) && /\.(js|ts)$/i.test(normalized)) {
|
|
3912
|
+
readCoreCode = true;
|
|
3913
|
+
}
|
|
3914
|
+
};
|
|
3915
|
+
for (const entry of filesRead)
|
|
3916
|
+
inspect(entry.path, entry.excerpt);
|
|
3917
|
+
for (const entry of liveToolEvidence) {
|
|
3918
|
+
if (entry?.success === false)
|
|
3919
|
+
continue;
|
|
3920
|
+
inspect(String(entry.target || entry.arguments?.path || ''), String(entry.output || ''));
|
|
3921
|
+
}
|
|
3922
|
+
if (subfolderCardGameReadme && codeSaysRts) {
|
|
3923
|
+
warnings.push('README describes a card/deck-building game, but code (`game.js` / `package.json`) describes a 3D RTS. Report the RTS truth first, then tell the user README is inconsistent and should be fixed.');
|
|
3924
|
+
}
|
|
3925
|
+
if (readRootPlanningDocs && !readCoreCode) {
|
|
3926
|
+
warnings.push('The agent read planning/status markdown at the workspace root but did not read core game code under `Vigthoria-dominion/src/` or `package.json`. Any genre/feature summary from those docs may be wrong.');
|
|
3927
|
+
}
|
|
3928
|
+
if (!readCoreCode) {
|
|
3929
|
+
warnings.push('Core source files (`package.json`, entry `game.js`, and key files under `src/`) were not read. The overview may be incomplete until those files are inspected.');
|
|
3930
|
+
}
|
|
3931
|
+
return warnings;
|
|
3932
|
+
}
|
|
2876
3933
|
/**
|
|
2877
3934
|
* Build a human-readable answer from the tool results in a V3 event
|
|
2878
3935
|
* stream when the server didn't emit a proper complete/message event.
|
|
2879
3936
|
*/
|
|
2880
|
-
synthesizeAnswerFromV3Events(events) {
|
|
3937
|
+
synthesizeAnswerFromV3Events(events, liveToolEvidence = []) {
|
|
2881
3938
|
const toolResults = [];
|
|
3939
|
+
const failedPaths = [];
|
|
2882
3940
|
const filesRead = [];
|
|
2883
3941
|
const filesWritten = [];
|
|
3942
|
+
const directoriesListed = [];
|
|
3943
|
+
const searches = [];
|
|
2884
3944
|
const assistantFragments = [];
|
|
3945
|
+
const pendingToolCalls = [];
|
|
3946
|
+
const ingestToolEvidence = (name, args, output, success) => {
|
|
3947
|
+
const target = String(args.path || args.file_path || args.file || args.target || '').trim();
|
|
3948
|
+
const cleanOutput = sanitizeUserFacingPathText(String(output || '').trim());
|
|
3949
|
+
if (!success || !cleanOutput) {
|
|
3950
|
+
if (!success && cleanOutput) {
|
|
3951
|
+
const failPath = target || name;
|
|
3952
|
+
failedPaths.push({
|
|
3953
|
+
path: sanitizeUserFacingPathText(failPath),
|
|
3954
|
+
detail: cleanOutput.split('\n').find(Boolean)?.slice(0, 160) || cleanOutput.slice(0, 160),
|
|
3955
|
+
});
|
|
3956
|
+
toolResults.push(`[${name}] ${cleanOutput.slice(0, 400)}`);
|
|
3957
|
+
}
|
|
3958
|
+
return;
|
|
3959
|
+
}
|
|
3960
|
+
if (name === 'read_file') {
|
|
3961
|
+
const filePath = target || 'unknown file';
|
|
3962
|
+
const excerpt = cleanOutput.length > 4000 ? `${cleanOutput.slice(0, 4000)}\n\n[...truncated...]` : cleanOutput;
|
|
3963
|
+
filesRead.push({ path: sanitizeUserFacingPathText(filePath), excerpt });
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
if (name === 'list_directory') {
|
|
3967
|
+
const excerpt = cleanOutput.length > 1200 ? `${cleanOutput.slice(0, 1200)}\n[...truncated...]` : cleanOutput;
|
|
3968
|
+
directoriesListed.push({ path: sanitizeUserFacingPathText(target || '.'), excerpt });
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
if (name === 'search_files') {
|
|
3972
|
+
const pattern = String(args.pattern || args.query || '').trim();
|
|
3973
|
+
const searchPath = String(args.path || '.').trim();
|
|
3974
|
+
searches.push(`${pattern || 'search'} in ${sanitizeUserFacingPathText(searchPath)}`);
|
|
3975
|
+
if (cleanOutput.length > 0) {
|
|
3976
|
+
toolResults.push(`[search_files] ${cleanOutput.slice(0, 600)}`);
|
|
3977
|
+
}
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
if (name === 'write_file' || name === 'create_file' || name === 'edit_file') {
|
|
3981
|
+
if (target)
|
|
3982
|
+
filesWritten.push(sanitizeUserFacingPathText(target));
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
toolResults.push(`[${name}] ${cleanOutput.slice(0, 600)}`);
|
|
3986
|
+
};
|
|
2885
3987
|
for (const event of events) {
|
|
2886
3988
|
if (!event)
|
|
2887
3989
|
continue;
|
|
2888
|
-
if (event.type === '
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3990
|
+
if (event.type === 'tool_call') {
|
|
3991
|
+
pendingToolCalls.push({
|
|
3992
|
+
name: String(event.name || event.tool || 'unknown_tool'),
|
|
3993
|
+
args: event.arguments && typeof event.arguments === 'object' ? event.arguments : {},
|
|
3994
|
+
});
|
|
3995
|
+
continue;
|
|
3996
|
+
}
|
|
3997
|
+
if (event.type === 'tool_result') {
|
|
3998
|
+
const success = event.success !== false;
|
|
3999
|
+
const output = typeof event.output === 'string'
|
|
4000
|
+
? event.output
|
|
4001
|
+
: typeof event.result === 'string'
|
|
4002
|
+
? event.result
|
|
4003
|
+
: '';
|
|
4004
|
+
const name = String(event.name || event.tool || 'unknown_tool');
|
|
4005
|
+
const callIndex = pendingToolCalls.findIndex((call) => call.name === name);
|
|
4006
|
+
const call = callIndex >= 0 ? pendingToolCalls.splice(callIndex, 1)[0] : pendingToolCalls.shift();
|
|
4007
|
+
ingestToolEvidence(name, call?.args || {}, output, success);
|
|
2901
4008
|
}
|
|
2902
4009
|
if (event.type === 'assistant' && typeof event.content === 'string' && event.content.trim()) {
|
|
2903
|
-
|
|
4010
|
+
const fragment = sanitizeUserFacingPathText(event.content.trim());
|
|
4011
|
+
if (!this.isV3StreamStatusMessage(fragment)) {
|
|
4012
|
+
assistantFragments.push(fragment);
|
|
4013
|
+
}
|
|
2904
4014
|
}
|
|
2905
4015
|
// Some servers emit 'text' events for incremental assistant text
|
|
2906
4016
|
if (event.type === 'text' && typeof event.content === 'string' && event.content.trim()) {
|
|
2907
|
-
|
|
4017
|
+
const fragment = sanitizeUserFacingPathText(event.content.trim());
|
|
4018
|
+
if (!this.isV3StreamStatusMessage(fragment)) {
|
|
4019
|
+
assistantFragments.push(fragment);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
if (event.type === 'message' && typeof event.content === 'string' && event.content.trim()) {
|
|
4023
|
+
const fragment = sanitizeUserFacingPathText(event.content.trim());
|
|
4024
|
+
if (!this.isV3StreamStatusMessage(fragment)) {
|
|
4025
|
+
assistantFragments.push(fragment);
|
|
4026
|
+
}
|
|
2908
4027
|
}
|
|
2909
4028
|
// Some servers emit content_block_delta for streamed text
|
|
2910
4029
|
if (event.type === 'content_block_delta' && typeof event.delta?.text === 'string' && event.delta.text.trim()) {
|
|
2911
4030
|
assistantFragments.push(sanitizeUserFacingPathText(event.delta.text.trim()));
|
|
2912
4031
|
}
|
|
2913
4032
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
4033
|
+
for (const entry of liveToolEvidence) {
|
|
4034
|
+
if (!entry || typeof entry !== 'object')
|
|
4035
|
+
continue;
|
|
4036
|
+
ingestToolEvidence(String(entry.name || entry.tool || 'unknown_tool'), entry.arguments && typeof entry.arguments === 'object' ? entry.arguments : { path: entry.target || entry.path || '' }, String(entry.output || ''), entry.success !== false);
|
|
4037
|
+
}
|
|
4038
|
+
// Prefer grounded tool evidence over streamed status chatter.
|
|
4039
|
+
const hasEvidence = directoriesListed.length > 0
|
|
4040
|
+
|| filesRead.length > 0
|
|
4041
|
+
|| searches.length > 0
|
|
4042
|
+
|| filesWritten.length > 0
|
|
4043
|
+
|| toolResults.length > 0
|
|
4044
|
+
|| failedPaths.length > 0;
|
|
4045
|
+
if (hasEvidence) {
|
|
4046
|
+
const sections = [];
|
|
4047
|
+
sections.push('## Workspace analysis (from local file inspection)');
|
|
4048
|
+
sections.push('This report is rebuilt from files the agent actually read. **Code and package manifests outrank README/planning docs**, which may be stale or AI-generated.');
|
|
4049
|
+
const codeSignals = this.extractCodeSignalsFromEvidence(filesRead, liveToolEvidence);
|
|
4050
|
+
const consistencySection = this.buildReadmeCodeConsistencySection(filesRead, liveToolEvidence);
|
|
4051
|
+
if (consistencySection) {
|
|
4052
|
+
sections.unshift(consistencySection);
|
|
4053
|
+
}
|
|
4054
|
+
if (codeSignals.length > 0) {
|
|
4055
|
+
sections.push(['## What the code says (trusted)', ...codeSignals.map((line) => `- ${line}`)].join('\n'));
|
|
4056
|
+
if (!consistencySection && codeSignals.some((line) => /rts|real-time strategy|3d rts|4-faction|desktop\/electron/i.test(line))) {
|
|
4057
|
+
sections.unshift([
|
|
4058
|
+
'## Executive summary',
|
|
4059
|
+
'Vigthoria Dominion is a **3D real-time strategy game** (Command & Conquer / Dune 2000 style) with **four factions**, based on code evidence from your local workspace — not from README/planning docs, which may be wrong.',
|
|
4060
|
+
'The project includes browser/Three.js source (`Vigthoria-dominion/`) and may include a desktop Electron build (`vigthoria-dominion-win/`).',
|
|
4061
|
+
].join('\n'));
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
else if (consistencySection && /INCONSISTENT/i.test(consistencySection)) {
|
|
4065
|
+
sections.unshift([
|
|
4066
|
+
'## Executive summary',
|
|
4067
|
+
'The agent found that **README documentation does not match the codebase**. See the consistency check below — the code defines what this game actually is.',
|
|
4068
|
+
].join('\n'));
|
|
4069
|
+
}
|
|
4070
|
+
const docWarnings = this.detectDocReliabilityWarnings(filesRead, liveToolEvidence);
|
|
4071
|
+
if (docWarnings.length > 0) {
|
|
4072
|
+
sections.push(['## Documentation reliability warnings', ...docWarnings.map((line) => `- ${line}`)].join('\n'));
|
|
4073
|
+
}
|
|
4074
|
+
if (directoriesListed.length > 0) {
|
|
4075
|
+
const uniqueDirs = new Map();
|
|
4076
|
+
for (const entry of directoriesListed) {
|
|
4077
|
+
if (!uniqueDirs.has(entry.path))
|
|
4078
|
+
uniqueDirs.set(entry.path, entry.excerpt);
|
|
4079
|
+
}
|
|
4080
|
+
const dirLines = [...uniqueDirs.entries()].slice(0, 8).map(([dirPath, excerpt]) => {
|
|
4081
|
+
const preview = excerpt.split('\n').filter(Boolean).slice(0, 12).join('\n');
|
|
4082
|
+
return `### ${dirPath}\n${preview}`;
|
|
4083
|
+
});
|
|
4084
|
+
sections.push(['## Directories inspected', ...dirLines].join('\n\n'));
|
|
4085
|
+
}
|
|
4086
|
+
if (filesRead.length > 0) {
|
|
4087
|
+
const uniqueFiles = new Map();
|
|
4088
|
+
for (const entry of filesRead) {
|
|
4089
|
+
if (!uniqueFiles.has(entry.path))
|
|
4090
|
+
uniqueFiles.set(entry.path, entry.excerpt);
|
|
4091
|
+
}
|
|
4092
|
+
const sortedFiles = [...uniqueFiles.entries()].sort((a, b) => this.scoreEvidenceFilePath(b[0]) - this.scoreEvidenceFilePath(a[0]));
|
|
4093
|
+
const fileLines = sortedFiles.slice(0, 8).map(([filePath, excerpt]) => {
|
|
4094
|
+
const tag = this.isLowTrustDocPath(filePath) ? ' _(low-trust doc)_' : '';
|
|
4095
|
+
return `### ${filePath}${tag}\n${excerpt}`;
|
|
4096
|
+
});
|
|
4097
|
+
sections.push(['## Files read (code-first order)', ...fileLines].join('\n\n'));
|
|
4098
|
+
}
|
|
4099
|
+
if (searches.length > 0) {
|
|
4100
|
+
sections.push(`## Searches run\n${Array.from(new Set(searches)).slice(0, 8).map((entry) => `- ${entry}`).join('\n')}`);
|
|
4101
|
+
}
|
|
4102
|
+
if (filesWritten.length > 0) {
|
|
4103
|
+
sections.push(`## Files written\n${Array.from(new Set(filesWritten)).slice(0, 12).map((entry) => `- ${entry}`).join('\n')}`);
|
|
4104
|
+
}
|
|
4105
|
+
if (failedPaths.length > 0) {
|
|
4106
|
+
const uniqueFails = new Map();
|
|
4107
|
+
for (const entry of failedPaths) {
|
|
4108
|
+
if (!uniqueFails.has(entry.path))
|
|
4109
|
+
uniqueFails.set(entry.path, entry.detail);
|
|
4110
|
+
}
|
|
4111
|
+
const failLines = [...uniqueFails.entries()].slice(0, 12).map(([failPath, detail]) => `- \`${failPath}\`${detail ? ` — ${detail}` : ''}`);
|
|
4112
|
+
sections.push([
|
|
4113
|
+
'## Paths not found (exploration misses)',
|
|
4114
|
+
...failLines,
|
|
4115
|
+
'',
|
|
4116
|
+
'_These paths were guessed by the agent and do not exist at that location. Check the directories inspected above for the correct layout (often under `src/`)._',
|
|
4117
|
+
].join('\n'));
|
|
4118
|
+
}
|
|
4119
|
+
if (toolResults.length > 0) {
|
|
4120
|
+
sections.push(`## Additional tool evidence\n${toolResults.slice(-5).join('\n\n')}`);
|
|
4121
|
+
}
|
|
4122
|
+
return sections.join('\n\n');
|
|
4123
|
+
}
|
|
4124
|
+
// Concatenate substantive assistant text fragments in order.
|
|
2916
4125
|
const fullAssistantText = assistantFragments.join('\n\n').trim();
|
|
2917
|
-
if (fullAssistantText.length >
|
|
4126
|
+
if (fullAssistantText.length > 80 && !this.isGenericV3AgentSummary(fullAssistantText)) {
|
|
2918
4127
|
return fullAssistantText;
|
|
2919
4128
|
}
|
|
2920
|
-
|
|
2921
|
-
const sections = [];
|
|
2922
|
-
if (filesRead.length > 0) {
|
|
2923
|
-
sections.push(`Files inspected: ${filesRead.join(', ')}`);
|
|
2924
|
-
}
|
|
2925
|
-
if (filesWritten.length > 0) {
|
|
2926
|
-
sections.push(`Files written: ${filesWritten.join(', ')}`);
|
|
2927
|
-
}
|
|
2928
|
-
if (toolResults.length > 0) {
|
|
2929
|
-
sections.push(toolResults.slice(-5).join('\n'));
|
|
2930
|
-
}
|
|
2931
|
-
return sections.length > 0
|
|
2932
|
-
? sections.join('\n\n')
|
|
2933
|
-
: '';
|
|
4129
|
+
return '';
|
|
2934
4130
|
}
|
|
2935
4131
|
sanitizeV3AgentEventForUser(event) {
|
|
2936
4132
|
const sanitizeValue = (value) => {
|
|
@@ -3013,17 +4209,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3013
4209
|
break;
|
|
3014
4210
|
}
|
|
3015
4211
|
buffer += decoder.decode(value, { stream: true });
|
|
3016
|
-
const
|
|
3017
|
-
buffer =
|
|
3018
|
-
for (const
|
|
3019
|
-
|
|
3020
|
-
|
|
4212
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
4213
|
+
buffer = frames.pop() || '';
|
|
4214
|
+
for (const frame of frames) {
|
|
4215
|
+
const lines = frame.split(/\r?\n/);
|
|
4216
|
+
const dataLines = [];
|
|
4217
|
+
let explicitEventType = null;
|
|
4218
|
+
for (const rawLine of lines) {
|
|
4219
|
+
const line = rawLine.trimEnd();
|
|
4220
|
+
if (!line || line.startsWith(':')) {
|
|
4221
|
+
continue;
|
|
4222
|
+
}
|
|
4223
|
+
if (line.startsWith('event:')) {
|
|
4224
|
+
explicitEventType = line.slice(6).trim() || null;
|
|
4225
|
+
continue;
|
|
4226
|
+
}
|
|
4227
|
+
if (line.startsWith('data:')) {
|
|
4228
|
+
dataLines.push(line.slice(5).trimStart());
|
|
4229
|
+
}
|
|
3021
4230
|
}
|
|
3022
|
-
const payload =
|
|
4231
|
+
const payload = dataLines.join('\n').trim();
|
|
3023
4232
|
if (!payload || payload === '[DONE]') {
|
|
3024
4233
|
continue;
|
|
3025
4234
|
}
|
|
3026
|
-
|
|
4235
|
+
let event;
|
|
4236
|
+
try {
|
|
4237
|
+
event = JSON.parse(payload);
|
|
4238
|
+
}
|
|
4239
|
+
catch {
|
|
4240
|
+
event = {
|
|
4241
|
+
type: explicitEventType || 'message',
|
|
4242
|
+
content: payload,
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4245
|
+
if (explicitEventType && (!event.type || event.type === 'message')) {
|
|
4246
|
+
event.type = explicitEventType;
|
|
4247
|
+
}
|
|
3027
4248
|
const userEvent = this.sanitizeV3AgentEventForUser(event);
|
|
3028
4249
|
events.push(userEvent);
|
|
3029
4250
|
if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
|
|
@@ -3034,10 +4255,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3034
4255
|
}
|
|
3035
4256
|
this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
|
|
3036
4257
|
this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
|
|
3037
|
-
// Empty workspace guard:
|
|
3038
|
-
//
|
|
3039
|
-
//
|
|
3040
|
-
// instead of letting the agent spin on an empty directory.
|
|
4258
|
+
// Empty workspace soft guard: the first list_directory result can be
|
|
4259
|
+
// transiently empty before remote workspace hydration catches up.
|
|
4260
|
+
// Avoid hard-failing here; let the stream continue.
|
|
3041
4261
|
if (event.type === 'tool_result'
|
|
3042
4262
|
&& event.name === 'list_directory'
|
|
3043
4263
|
&& event.success === true
|
|
@@ -3052,9 +4272,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3052
4272
|
const localPath = this.resolveAgentTargetPath(context);
|
|
3053
4273
|
const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
|
|
3054
4274
|
if (localHasFiles) {
|
|
3055
|
-
|
|
3056
|
-
+ 'Your local workspace has files but the remote agent sees an empty directory. '
|
|
3057
|
-
+ 'This is a workspace sync failure. Falling back to local agent loop.');
|
|
4275
|
+
this.logger?.debug?.('V3 remote workspace initially empty; waiting for hydration instead of aborting.');
|
|
3058
4276
|
}
|
|
3059
4277
|
}
|
|
3060
4278
|
}
|
|
@@ -3066,6 +4284,43 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3066
4284
|
// Ignore UI callback failures; never break the agent stream for them.
|
|
3067
4285
|
}
|
|
3068
4286
|
}
|
|
4287
|
+
if (event.type === 'client_tool_request') {
|
|
4288
|
+
const activeContextId = String(event.context_id || contextId || context.contextId || '').trim();
|
|
4289
|
+
const callId = String(event.call_id || '').trim();
|
|
4290
|
+
const handler = context.onClientToolExecute;
|
|
4291
|
+
let toolResult = {
|
|
4292
|
+
success: false,
|
|
4293
|
+
output: '',
|
|
4294
|
+
error: 'No local tool handler configured',
|
|
4295
|
+
};
|
|
4296
|
+
if (typeof handler === 'function' && activeContextId && callId) {
|
|
4297
|
+
try {
|
|
4298
|
+
toolResult = await handler(event);
|
|
4299
|
+
}
|
|
4300
|
+
catch (error) {
|
|
4301
|
+
toolResult = {
|
|
4302
|
+
success: false,
|
|
4303
|
+
output: '',
|
|
4304
|
+
error: sanitizeUserFacingErrorText(error?.message || 'Local tool execution failed'),
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
if (activeContextId && callId) {
|
|
4309
|
+
await this.submitClientToolResult(activeContextId, callId, toolResult, context.v3BackendUrl);
|
|
4310
|
+
}
|
|
4311
|
+
continue;
|
|
4312
|
+
}
|
|
4313
|
+
if (event.type === 'context' && typeof context.onWorkspaceContext === 'function') {
|
|
4314
|
+
try {
|
|
4315
|
+
await context.onWorkspaceContext({
|
|
4316
|
+
contextId: String(event.context_id || contextId || '').trim(),
|
|
4317
|
+
serverWorkspaceRoot: String(event.workspace_root || serverWorkspaceRoot || '').trim(),
|
|
4318
|
+
});
|
|
4319
|
+
}
|
|
4320
|
+
catch {
|
|
4321
|
+
// Ignore workspace bind callback failures.
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
3069
4324
|
if (event.type === 'error') {
|
|
3070
4325
|
if (event.checkpointed && event.task_id) {
|
|
3071
4326
|
// Agent checkpointed — return data so caller can auto-continue
|
|
@@ -3095,6 +4350,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3095
4350
|
if (event.type === 'complete' || event.type === 'message') {
|
|
3096
4351
|
final = event;
|
|
3097
4352
|
}
|
|
4353
|
+
// Exit stream early on complete — agent is done; server-side teardown
|
|
4354
|
+
// can hold the connection open for many seconds otherwise.
|
|
4355
|
+
if (event.type === 'complete') {
|
|
4356
|
+
reader.cancel().catch(() => { });
|
|
4357
|
+
return {
|
|
4358
|
+
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|
|
4359
|
+
context_id: contextId,
|
|
4360
|
+
result: final,
|
|
4361
|
+
events,
|
|
4362
|
+
files: streamedFiles,
|
|
4363
|
+
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
3098
4366
|
}
|
|
3099
4367
|
}
|
|
3100
4368
|
return {
|
|
@@ -3106,6 +4374,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3106
4374
|
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
3107
4375
|
};
|
|
3108
4376
|
}
|
|
4377
|
+
async submitClientToolResult(contextId, callId, result, backendUrl) {
|
|
4378
|
+
const trimmedContextId = String(contextId || '').trim();
|
|
4379
|
+
const trimmedCallId = String(callId || '').trim();
|
|
4380
|
+
if (!trimmedContextId || !trimmedCallId) {
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
const baseUrl = String(backendUrl || this.getV3AgentBaseUrls()[0] || '').replace(/\/$/, '');
|
|
4384
|
+
if (!baseUrl) {
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
const headers = await this.getV3AgentHeaders();
|
|
4388
|
+
const endpoint = /127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)
|
|
4389
|
+
? `${baseUrl}/api/agent/client-tool-result`
|
|
4390
|
+
: `${baseUrl}/api/v3-agent/client-tool-result`;
|
|
4391
|
+
const response = await fetch(endpoint, {
|
|
4392
|
+
method: 'POST',
|
|
4393
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
4394
|
+
body: JSON.stringify({
|
|
4395
|
+
context_id: trimmedContextId,
|
|
4396
|
+
call_id: trimmedCallId,
|
|
4397
|
+
success: result.success === true,
|
|
4398
|
+
output: String(result.output || ''),
|
|
4399
|
+
error: String(result.error || ''),
|
|
4400
|
+
}),
|
|
4401
|
+
});
|
|
4402
|
+
if (!response.ok) {
|
|
4403
|
+
const errorText = await response.text().catch(() => '');
|
|
4404
|
+
throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
3109
4407
|
async runV3AgentWorkflow(message, context = {}) {
|
|
3110
4408
|
const executionContext = await this.bindExecutionContext(context);
|
|
3111
4409
|
const requestedTimeoutMs = Number(executionContext.agentTimeoutMs ?? DEFAULT_V3_AGENT_TIMEOUT_MS);
|
|
@@ -3135,7 +4433,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3135
4433
|
legacyFallbackAllowed: true,
|
|
3136
4434
|
})
|
|
3137
4435
|
: executionContext;
|
|
3138
|
-
const
|
|
4436
|
+
const buildRequestBody = (contextIdOverride) => ({
|
|
3139
4437
|
request: message,
|
|
3140
4438
|
model: resolvedModel,
|
|
3141
4439
|
preferred_model: resolvedModel,
|
|
@@ -3145,119 +4443,154 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3145
4443
|
context: useRelaxedAttempt
|
|
3146
4444
|
? this.buildMinimalV3AgentContext(requestExecutionContext)
|
|
3147
4445
|
: this.buildV3AgentContext(requestExecutionContext),
|
|
3148
|
-
context_id: requestExecutionContext.contextId,
|
|
4446
|
+
context_id: contextIdOverride ?? requestExecutionContext.contextId,
|
|
3149
4447
|
mcp_context_id: useRelaxedAttempt ? null : requestExecutionContext.mcpContextId || null,
|
|
3150
4448
|
stream: true,
|
|
3151
|
-
};
|
|
4449
|
+
});
|
|
3152
4450
|
for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
const
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
4451
|
+
let contextIdOverride;
|
|
4452
|
+
const authPreflight = await this.runV3AgentAuthPreflight(baseUrl, buildRequestBody(), requestExecutionContext);
|
|
4453
|
+
if (!authPreflight.ok) {
|
|
4454
|
+
errors.push(`V3 auth preflight ${authPreflight.status} [${authPreflight.endpoint}]: ${authPreflight.reason || 'Unauthorized'}`);
|
|
4455
|
+
continue;
|
|
4456
|
+
}
|
|
4457
|
+
for (let contextRetry = 0; contextRetry < 2; contextRetry += 1) {
|
|
4458
|
+
const requestBody = buildRequestBody(contextIdOverride);
|
|
4459
|
+
const controller = new AbortController();
|
|
4460
|
+
const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
4461
|
+
try {
|
|
4462
|
+
const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
|
|
4463
|
+
if (!response.ok) {
|
|
4464
|
+
const errorText = await response.text().catch(() => '');
|
|
4465
|
+
const sanitized = sanitizeUserFacingErrorText(errorText).slice(0, 200);
|
|
4466
|
+
const isContextCollision = response.status === 409 && /already in progress/i.test(errorText);
|
|
4467
|
+
const isTransientWorkspaceHydration = /remote workspace is empty|workspace sync failure/i.test(errorText);
|
|
4468
|
+
if ((isContextCollision || isTransientWorkspaceHydration) && contextRetry === 0) {
|
|
4469
|
+
contextIdOverride = `${requestExecutionContext.contextId || 'vig'}-retry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4470
|
+
this.logger?.debug?.(`V3 transient agent error on ${baseUrl}; retrying with fresh context id.`);
|
|
4471
|
+
continue;
|
|
4472
|
+
}
|
|
4473
|
+
throw new Error(`V3 agent ${response.status}: ${sanitized}`);
|
|
4474
|
+
}
|
|
4475
|
+
const data = await this.collectV3AgentStream(response, {
|
|
4476
|
+
...requestExecutionContext,
|
|
4477
|
+
v3BackendUrl: baseUrl,
|
|
4478
|
+
});
|
|
4479
|
+
// Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
|
|
4480
|
+
if (data.checkpointed && data.checkpointed_task_id) {
|
|
4481
|
+
const maxContinuations = 10;
|
|
4482
|
+
let continuationData = data;
|
|
4483
|
+
let continuations = 0;
|
|
4484
|
+
while (continuationData.checkpointed && continuationData.checkpointed_task_id && continuations < maxContinuations) {
|
|
4485
|
+
continuations++;
|
|
4486
|
+
if (typeof requestExecutionContext.onStreamEvent === 'function') {
|
|
4487
|
+
try {
|
|
4488
|
+
requestExecutionContext.onStreamEvent({
|
|
4489
|
+
type: 'message',
|
|
4490
|
+
content: `Auto-continuing task (phase ${continuations + 1})...`,
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
catch { /* ignore */ }
|
|
4494
|
+
}
|
|
4495
|
+
const continueBody = {
|
|
4496
|
+
task_id: continuationData.checkpointed_task_id,
|
|
4497
|
+
context: requestBody.context,
|
|
4498
|
+
context_id: requestBody.context_id,
|
|
4499
|
+
mcp_context_id: requestBody.mcp_context_id,
|
|
4500
|
+
stream: true,
|
|
4501
|
+
};
|
|
4502
|
+
const continueController = new AbortController();
|
|
4503
|
+
const continueTimeoutId = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
|
|
3170
4504
|
try {
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
4505
|
+
const continueHeaders = await this.getV3AgentHeaders();
|
|
4506
|
+
const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
4507
|
+
method: 'POST',
|
|
4508
|
+
headers: { ...continueHeaders, 'Content-Type': 'application/json' },
|
|
4509
|
+
body: JSON.stringify(continueBody),
|
|
4510
|
+
signal: continueController.signal,
|
|
4511
|
+
});
|
|
4512
|
+
if (!continueResponse.ok) {
|
|
4513
|
+
break; // Fall through to normal completion with partial data
|
|
4514
|
+
}
|
|
4515
|
+
continuationData = await this.collectV3AgentStream(continueResponse, {
|
|
4516
|
+
...requestExecutionContext,
|
|
4517
|
+
v3BackendUrl: baseUrl,
|
|
3174
4518
|
});
|
|
3175
4519
|
}
|
|
3176
|
-
catch {
|
|
3177
|
-
}
|
|
3178
|
-
const continueBody = {
|
|
3179
|
-
task_id: continuationData.checkpointed_task_id,
|
|
3180
|
-
context: requestBody.context,
|
|
3181
|
-
context_id: requestBody.context_id,
|
|
3182
|
-
mcp_context_id: requestBody.mcp_context_id,
|
|
3183
|
-
stream: true,
|
|
3184
|
-
};
|
|
3185
|
-
const continueController = new AbortController();
|
|
3186
|
-
const continueTimeoutId = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
|
|
3187
|
-
try {
|
|
3188
|
-
const continueHeaders = await this.getV3AgentHeaders();
|
|
3189
|
-
const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
3190
|
-
method: 'POST',
|
|
3191
|
-
headers: { ...continueHeaders, 'Content-Type': 'application/json' },
|
|
3192
|
-
body: JSON.stringify(continueBody),
|
|
3193
|
-
signal: continueController.signal,
|
|
3194
|
-
});
|
|
3195
|
-
if (!continueResponse.ok) {
|
|
4520
|
+
catch {
|
|
3196
4521
|
break; // Fall through to normal completion with partial data
|
|
3197
4522
|
}
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
}
|
|
3203
|
-
finally {
|
|
3204
|
-
if (continueTimeoutId)
|
|
3205
|
-
clearTimeout(continueTimeoutId);
|
|
4523
|
+
finally {
|
|
4524
|
+
if (continueTimeoutId)
|
|
4525
|
+
clearTimeout(continueTimeoutId);
|
|
4526
|
+
}
|
|
3206
4527
|
}
|
|
4528
|
+
// Use the final continuation data for workspace recovery
|
|
4529
|
+
this.recoverAgentWorkspaceFiles(executionContext, continuationData.files || {}, expectedFiles);
|
|
4530
|
+
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
4531
|
+
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
4532
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
4533
|
+
const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
4534
|
+
return {
|
|
4535
|
+
content: this.formatV3AgentResponse({
|
|
4536
|
+
...continuationData,
|
|
4537
|
+
liveToolEvidence: requestExecutionContext.liveToolEvidence,
|
|
4538
|
+
}) || this.formatV3AgentResponse({
|
|
4539
|
+
...data,
|
|
4540
|
+
liveToolEvidence: requestExecutionContext.liveToolEvidence,
|
|
4541
|
+
}),
|
|
4542
|
+
taskId: continuationData.task_id || data.task_id || null,
|
|
4543
|
+
contextId: finalContextId,
|
|
4544
|
+
backendUrl: baseUrl,
|
|
4545
|
+
partial: continuationData.checkpointed === true,
|
|
4546
|
+
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
4547
|
+
};
|
|
3207
4548
|
}
|
|
3208
|
-
|
|
3209
|
-
|
|
4549
|
+
const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
4550
|
+
const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
|
|
4551
|
+
this.recoverAgentWorkspaceFiles(executionContext, data.files || {}, expectedFiles);
|
|
3210
4552
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
3211
4553
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3212
4554
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3213
|
-
|
|
4555
|
+
if (previewGate.required && previewGate.passed !== true && !this.hasAgentWorkspaceOutput(executionContext)) {
|
|
4556
|
+
errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
|
|
4557
|
+
continue;
|
|
4558
|
+
}
|
|
3214
4559
|
return {
|
|
3215
|
-
content: this.formatV3AgentResponse(
|
|
3216
|
-
|
|
3217
|
-
|
|
4560
|
+
content: this.formatV3AgentResponse({
|
|
4561
|
+
...data,
|
|
4562
|
+
liveToolEvidence: requestExecutionContext.liveToolEvidence,
|
|
4563
|
+
}),
|
|
4564
|
+
taskId: data.task_id || null,
|
|
4565
|
+
contextId,
|
|
3218
4566
|
backendUrl: baseUrl,
|
|
3219
|
-
|
|
3220
|
-
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
4567
|
+
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
3221
4568
|
};
|
|
3222
4569
|
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
4570
|
+
catch (error) {
|
|
4571
|
+
if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
|
|
4572
|
+
this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
|
|
4573
|
+
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
4574
|
+
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
4575
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
4576
|
+
return {
|
|
4577
|
+
content: this.formatV3AgentResponse({
|
|
4578
|
+
...error.partialData,
|
|
4579
|
+
liveToolEvidence: requestExecutionContext.liveToolEvidence,
|
|
4580
|
+
}) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
|
|
4581
|
+
taskId: error.partialData.task_id || null,
|
|
4582
|
+
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
4583
|
+
backendUrl: baseUrl,
|
|
4584
|
+
partial: true,
|
|
4585
|
+
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 },
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3232
4589
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
contextId,
|
|
3237
|
-
backendUrl: baseUrl,
|
|
3238
|
-
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
3239
|
-
};
|
|
3240
|
-
}
|
|
3241
|
-
catch (error) {
|
|
3242
|
-
if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
|
|
3243
|
-
this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
|
|
3244
|
-
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
3245
|
-
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3246
|
-
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3247
|
-
return {
|
|
3248
|
-
content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
|
|
3249
|
-
taskId: error.partialData.task_id || null,
|
|
3250
|
-
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
3251
|
-
backendUrl: baseUrl,
|
|
3252
|
-
partial: true,
|
|
3253
|
-
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 },
|
|
3254
|
-
};
|
|
4590
|
+
finally {
|
|
4591
|
+
if (timeoutId)
|
|
4592
|
+
clearTimeout(timeoutId);
|
|
3255
4593
|
}
|
|
3256
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3257
|
-
}
|
|
3258
|
-
finally {
|
|
3259
|
-
if (timeoutId)
|
|
3260
|
-
clearTimeout(timeoutId);
|
|
3261
4594
|
}
|
|
3262
4595
|
}
|
|
3263
4596
|
lastErrors = errors;
|
|
@@ -3271,7 +4604,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3271
4604
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
3272
4605
|
}
|
|
3273
4606
|
const errors = lastErrors;
|
|
3274
|
-
const onlyUnauthorizedErrors = errors.length > 0 && errors.every((entry) => /V3 agent 401
|
|
4607
|
+
const onlyUnauthorizedErrors = errors.length > 0 && errors.every((entry) => /V3 agent 401:|V3 agent 401 \[|V3 auth preflight 401|V3 auth preflight 403/i.test(entry));
|
|
3275
4608
|
const usingStoredConfigToken = !process.env.VIGTHORIA_TOKEN
|
|
3276
4609
|
&& !process.env.VIGTHORIA_AUTH_TOKEN
|
|
3277
4610
|
&& Boolean(this.config.get('authToken'));
|
|
@@ -3281,7 +4614,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3281
4614
|
this.config.clearAuth();
|
|
3282
4615
|
throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
|
|
3283
4616
|
}
|
|
3284
|
-
|
|
4617
|
+
const rejectedEndpoints = Array.from(new Set(errors
|
|
4618
|
+
.map((entry) => {
|
|
4619
|
+
const match = entry.match(/\[(https?:\/\/[^\]]+)\]/i);
|
|
4620
|
+
return match ? match[1] : null;
|
|
4621
|
+
})
|
|
4622
|
+
.filter((entry) => Boolean(entry))));
|
|
4623
|
+
const endpointSummary = rejectedEndpoints.length > 0
|
|
4624
|
+
? ` Rejected V3 endpoints: ${rejectedEndpoints.join(', ')}.`
|
|
4625
|
+
: '';
|
|
4626
|
+
throw new Error(`V3 agent authentication failed at the V3 service layer while your gateway login token is still valid.${endpointSummary} Please retry shortly.`);
|
|
3285
4627
|
}
|
|
3286
4628
|
if (preferLocalV3
|
|
3287
4629
|
&& !this.hasAgentWorkspaceOutput(executionContext)
|
|
@@ -3506,7 +4848,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3506
4848
|
*
|
|
3507
4849
|
* NO localhost fallbacks - CLI is for external users, not server-side!
|
|
3508
4850
|
*/
|
|
3509
|
-
async chat(messages, model, useLocal = false) {
|
|
4851
|
+
async chat(messages, model, useLocal = false, options = {}) {
|
|
3510
4852
|
this.lastChatTransportErrors = [];
|
|
3511
4853
|
const resolvedModel = this.resolveModelId(model);
|
|
3512
4854
|
const candidateModels = this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()
|
|
@@ -3517,7 +4859,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3517
4859
|
candidateModels.push(fallbackModel);
|
|
3518
4860
|
}
|
|
3519
4861
|
for (const candidateModel of candidateModels) {
|
|
3520
|
-
const response = await this.tryChatWithModel(messages, candidateModel, model);
|
|
4862
|
+
const response = await this.tryChatWithModel(messages, candidateModel, model, options);
|
|
3521
4863
|
if (response) {
|
|
3522
4864
|
if (candidateModel !== resolvedModel) {
|
|
3523
4865
|
this.logger.debug(`Recovered chat request using fallback model: ${candidateModel}`);
|
|
@@ -3525,79 +4867,100 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3525
4867
|
return response;
|
|
3526
4868
|
}
|
|
3527
4869
|
}
|
|
3528
|
-
//
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
throw new CLIError(
|
|
4870
|
+
// Keep route diagnostics internal. Users only need to know the Vigthoria service is temporarily unavailable.
|
|
4871
|
+
if (this.lastChatTransportErrors.length > 0) {
|
|
4872
|
+
this.logger.debug(`Chat transport failures: ${this.lastChatTransportErrors.slice(0, 4).join(' | ')}`);
|
|
4873
|
+
}
|
|
4874
|
+
throw new CLIError(VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE, 'model_backend');
|
|
4875
|
+
}
|
|
4876
|
+
getLastChatTransportErrors() {
|
|
4877
|
+
return [...this.lastChatTransportErrors];
|
|
3533
4878
|
}
|
|
3534
4879
|
shouldSkipCloudRoutes(resolvedModel) {
|
|
3535
4880
|
return this.shouldSimulateCloudFailure() && this.isCloudModelId(resolvedModel);
|
|
3536
4881
|
}
|
|
3537
|
-
async tryChatWithModel(messages, resolvedModel, requestedModel) {
|
|
4882
|
+
async tryChatWithModel(messages, resolvedModel, requestedModel, options = {}) {
|
|
3538
4883
|
const routeFailures = [];
|
|
3539
|
-
const
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
4884
|
+
const connectTimeoutMs = this.resolveChatConnectTimeoutMs(options);
|
|
4885
|
+
const idleTimeoutMs = this.resolveChatIdleTimeoutMs(options);
|
|
4886
|
+
const useStream = options.stream === true;
|
|
4887
|
+
const notifyRoute = (label) => {
|
|
4888
|
+
try {
|
|
4889
|
+
options.onRouteAttempt?.(label);
|
|
4890
|
+
}
|
|
4891
|
+
catch {
|
|
4892
|
+
// Spinner callbacks must not break chat routing.
|
|
4893
|
+
}
|
|
4894
|
+
};
|
|
4895
|
+
const tryModelsRoute = async () => {
|
|
4896
|
+
if (this.shouldSkipCloudRoutes(resolvedModel) || this.isCloudModelId(resolvedModel)) {
|
|
4897
|
+
return null;
|
|
4898
|
+
}
|
|
4899
|
+
const token = this.getAccessToken();
|
|
4900
|
+
const authHeaders = token ? { Authorization: `Bearer ${token}` } : {};
|
|
4901
|
+
if (useStream) {
|
|
4902
|
+
try {
|
|
4903
|
+
notifyRoute('Streaming from Vigthoria Models API...');
|
|
4904
|
+
return await this.tryStreamingModelRouterChat(this.modelRouterClient.defaults.baseURL || 'https://api.vigthoria.io', messages, resolvedModel, requestedModel, authHeaders, options);
|
|
4905
|
+
}
|
|
4906
|
+
catch (error) {
|
|
4907
|
+
const errMsg = error?.message || 'Unknown error';
|
|
4908
|
+
routeFailures.push(`models-stream:${String(errMsg).slice(0, 120)}`);
|
|
4909
|
+
}
|
|
3544
4910
|
}
|
|
3545
|
-
}
|
|
3546
|
-
// STRATEGY 1: Direct Vigthoria Models API (api.vigthoria.io)
|
|
3547
|
-
if (!this.shouldSkipCloudRoutes(resolvedModel)) {
|
|
3548
4911
|
try {
|
|
3549
|
-
|
|
3550
|
-
const token = this.getAccessToken();
|
|
4912
|
+
notifyRoute('Connecting to Vigthoria Models API...');
|
|
3551
4913
|
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
3552
4914
|
model: resolvedModel,
|
|
3553
4915
|
messages,
|
|
3554
4916
|
max_tokens: this.config.get('preferences').maxTokens,
|
|
3555
4917
|
temperature: 0.7,
|
|
3556
4918
|
stream: false,
|
|
4919
|
+
cloudAccessApproved: this.isCloudModelId(resolvedModel),
|
|
4920
|
+
routeClass: 'direct-chat',
|
|
3557
4921
|
}, {
|
|
3558
|
-
|
|
4922
|
+
timeout: connectTimeoutMs,
|
|
4923
|
+
headers: {
|
|
4924
|
+
...authHeaders,
|
|
4925
|
+
...this.buildDirectChatHeaders(this.isCloudModelId(resolvedModel)),
|
|
4926
|
+
},
|
|
3559
4927
|
});
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
message: content,
|
|
3569
|
-
model: response.data.model || resolvedModel || requestedModel,
|
|
3570
|
-
usage: response.data.usage,
|
|
3571
|
-
};
|
|
3572
|
-
}
|
|
4928
|
+
const content = response.data.choices?.[0]?.message?.content || response.data.choices?.[0]?.text;
|
|
4929
|
+
if (typeof content === 'string' && content.trim()) {
|
|
4930
|
+
return {
|
|
4931
|
+
id: response.data.id || `vigthoria-${Date.now()}`,
|
|
4932
|
+
message: content,
|
|
4933
|
+
model: response.data.model || resolvedModel || requestedModel,
|
|
4934
|
+
usage: response.data.usage,
|
|
4935
|
+
};
|
|
3573
4936
|
}
|
|
3574
|
-
this.logger.debug(`Direct API returned no choices for ${resolvedModel}`);
|
|
3575
4937
|
}
|
|
3576
4938
|
catch (error) {
|
|
3577
4939
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3578
|
-
this.logger.debug(`Direct Vigthoria Models API failed for ${resolvedModel}: ${errMsg}`);
|
|
3579
4940
|
routeFailures.push(`models:${String(errMsg).slice(0, 120)}`);
|
|
3580
4941
|
}
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
4942
|
+
return null;
|
|
4943
|
+
};
|
|
4944
|
+
const tryCoderRoute = async () => {
|
|
4945
|
+
if (!this.config.isAuthenticated() || this.shouldSkipCloudRoutes(resolvedModel)) {
|
|
4946
|
+
return null;
|
|
4947
|
+
}
|
|
3587
4948
|
try {
|
|
3588
|
-
|
|
4949
|
+
notifyRoute('Connecting to Vigthoria Coder API...');
|
|
3589
4950
|
const response = await this.client.post('/api/ai/chat', {
|
|
3590
4951
|
messages,
|
|
3591
4952
|
model: resolvedModel,
|
|
3592
4953
|
maxTokens: this.config.get('preferences').maxTokens,
|
|
3593
4954
|
temperature: 0.7,
|
|
4955
|
+
routeClass: 'direct-chat',
|
|
4956
|
+
cloudAccessApproved: this.isCloudModelId(resolvedModel),
|
|
4957
|
+
}, {
|
|
4958
|
+
timeout: idleTimeoutMs,
|
|
4959
|
+
headers: this.buildDirectChatHeaders(this.isCloudModelId(resolvedModel)),
|
|
3594
4960
|
});
|
|
3595
4961
|
if (response.data.success !== false) {
|
|
3596
4962
|
const content = response.data.response || response.data.message || response.data.content;
|
|
3597
|
-
if (typeof content
|
|
3598
|
-
this.logger.debug(`Cloud API returned empty message content for ${resolvedModel}`);
|
|
3599
|
-
}
|
|
3600
|
-
else {
|
|
4963
|
+
if (typeof content === 'string' && content.trim()) {
|
|
3601
4964
|
return {
|
|
3602
4965
|
id: response.data.id || `vigthoria-coder-${Date.now()}`,
|
|
3603
4966
|
message: content,
|
|
@@ -3606,48 +4969,81 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3606
4969
|
};
|
|
3607
4970
|
}
|
|
3608
4971
|
}
|
|
3609
|
-
this.logger.debug(`Cloud API returned success=false for ${resolvedModel}`);
|
|
3610
4972
|
}
|
|
3611
4973
|
catch (error) {
|
|
3612
4974
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3613
|
-
this.logger.debug(`Vigthoria Cloud API failed for ${resolvedModel}: ${errMsg}`);
|
|
3614
4975
|
routeFailures.push(`coder:${String(errMsg).slice(0, 120)}`);
|
|
3615
4976
|
}
|
|
4977
|
+
return null;
|
|
4978
|
+
};
|
|
4979
|
+
const tryCanonicalRoute = async () => {
|
|
4980
|
+
if (options.fastFail || this.isCanonicalCoderDuplicate() || !this.config.isAuthenticated()) {
|
|
4981
|
+
return null;
|
|
4982
|
+
}
|
|
3616
4983
|
try {
|
|
3617
|
-
|
|
4984
|
+
notifyRoute('Connecting to Vigthoria Coder (canonical)...');
|
|
3618
4985
|
const token = this.getAccessToken();
|
|
3619
4986
|
const response = await axios.post('https://coder.vigthoria.io/api/ai/chat', {
|
|
3620
4987
|
messages,
|
|
3621
4988
|
model: resolvedModel,
|
|
3622
4989
|
maxTokens: this.config.get('preferences').maxTokens,
|
|
3623
4990
|
temperature: 0.7,
|
|
4991
|
+
routeClass: 'direct-chat',
|
|
4992
|
+
cloudAccessApproved: this.isCloudModelId(resolvedModel),
|
|
3624
4993
|
}, {
|
|
3625
|
-
timeout:
|
|
4994
|
+
timeout: idleTimeoutMs,
|
|
3626
4995
|
httpsAgent: this._httpsAgent ?? undefined,
|
|
3627
|
-
headers:
|
|
4996
|
+
headers: {
|
|
4997
|
+
...(token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {}),
|
|
4998
|
+
...this.buildDirectChatHeaders(this.isCloudModelId(resolvedModel)),
|
|
4999
|
+
},
|
|
3628
5000
|
});
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
};
|
|
3638
|
-
}
|
|
5001
|
+
const content = response.data?.response || response.data?.message || response.data?.content;
|
|
5002
|
+
if (response.data?.success !== false && typeof content === 'string' && content.trim()) {
|
|
5003
|
+
return {
|
|
5004
|
+
id: response.data.id || `vigthoria-coder-canonical-${Date.now()}`,
|
|
5005
|
+
message: content,
|
|
5006
|
+
model: response.data.model || resolvedModel || requestedModel,
|
|
5007
|
+
usage: response.data.usage,
|
|
5008
|
+
};
|
|
3639
5009
|
}
|
|
3640
5010
|
}
|
|
3641
5011
|
catch (error) {
|
|
3642
5012
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3643
|
-
this.logger.debug(`Canonical Vigthoria Cloud fallback failed for ${resolvedModel}: ${errMsg}`);
|
|
3644
5013
|
routeFailures.push(`coder-canonical:${String(errMsg).slice(0, 120)}`);
|
|
3645
5014
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
5015
|
+
return null;
|
|
5016
|
+
};
|
|
5017
|
+
const trySelfHostedRoute = async () => {
|
|
5018
|
+
notifyRoute('Connecting to dedicated model endpoint...');
|
|
5019
|
+
return this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel, idleTimeoutMs, options);
|
|
5020
|
+
};
|
|
5021
|
+
const preferred = options.preferredRoute
|
|
5022
|
+
|| (this.config.isAuthenticated() ? 'coder' : 'models');
|
|
5023
|
+
const singleRouteMode = options.fastFail === true && (options.singleRoute === true || !!options.preferredRoute);
|
|
5024
|
+
const routeOrder = singleRouteMode && options.preferredRoute
|
|
5025
|
+
? [options.preferredRoute]
|
|
5026
|
+
: preferred === 'coder'
|
|
5027
|
+
? ['coder', 'models', 'selfhosted']
|
|
5028
|
+
: preferred === 'selfhosted'
|
|
5029
|
+
? ['selfhosted', 'coder', 'models']
|
|
5030
|
+
: ['models', 'coder', 'selfhosted'];
|
|
5031
|
+
for (const route of routeOrder) {
|
|
5032
|
+
let result = null;
|
|
5033
|
+
if (route === 'coder') {
|
|
5034
|
+
result = await tryCoderRoute();
|
|
5035
|
+
if (!result) {
|
|
5036
|
+
result = await tryCanonicalRoute();
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
else if (route === 'models') {
|
|
5040
|
+
result = await tryModelsRoute();
|
|
5041
|
+
}
|
|
5042
|
+
else {
|
|
5043
|
+
result = await trySelfHostedRoute();
|
|
5044
|
+
}
|
|
5045
|
+
if (result) {
|
|
5046
|
+
return result;
|
|
3651
5047
|
}
|
|
3652
5048
|
}
|
|
3653
5049
|
if (routeFailures.length > 0) {
|
|
@@ -3655,13 +5051,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3655
5051
|
}
|
|
3656
5052
|
return null;
|
|
3657
5053
|
}
|
|
3658
|
-
async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel) {
|
|
5054
|
+
async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel, requestTimeoutMs, options = {}) {
|
|
3659
5055
|
if (!this.selfHostedModelRouterClient || !this.shouldTrySelfHostedFallback(resolvedModel, requestedModel)) {
|
|
3660
5056
|
return null;
|
|
3661
5057
|
}
|
|
3662
5058
|
const selfHostedModel = this.getSelfHostedFallbackModelId(resolvedModel, requestedModel);
|
|
3663
5059
|
try {
|
|
3664
|
-
this.logger.debug(`
|
|
5060
|
+
this.logger.debug(`Dedicated Vigthoria model endpoint fallback: ${selfHostedModel}`);
|
|
3665
5061
|
const response = await this.selfHostedModelRouterClient.post('/v1/chat/completions', {
|
|
3666
5062
|
model: selfHostedModel,
|
|
3667
5063
|
messages,
|
|
@@ -3669,6 +5065,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3669
5065
|
temperature: 0.7,
|
|
3670
5066
|
stream: false,
|
|
3671
5067
|
}, {
|
|
5068
|
+
timeout: requestTimeoutMs,
|
|
3672
5069
|
headers: {
|
|
3673
5070
|
'x-agent-mode': 'true',
|
|
3674
5071
|
},
|
|
@@ -3676,21 +5073,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3676
5073
|
if (response.data.choices && response.data.choices.length > 0) {
|
|
3677
5074
|
const content = response.data.choices[0].message?.content || response.data.choices[0].text;
|
|
3678
5075
|
if (typeof content !== 'string' || !content.trim()) {
|
|
3679
|
-
this.logger.debug(`
|
|
5076
|
+
this.logger.debug(`Dedicated Vigthoria model endpoint returned empty message content for ${selfHostedModel}`);
|
|
3680
5077
|
return null;
|
|
3681
5078
|
}
|
|
3682
5079
|
return {
|
|
3683
|
-
id: response.data.id || `vigthoria-
|
|
5080
|
+
id: response.data.id || `vigthoria-dedicated-${Date.now()}`,
|
|
3684
5081
|
message: content,
|
|
3685
5082
|
model: response.data.model || selfHostedModel,
|
|
3686
5083
|
usage: response.data.usage,
|
|
3687
5084
|
};
|
|
3688
5085
|
}
|
|
3689
|
-
this.logger.debug(`
|
|
5086
|
+
this.logger.debug(`Dedicated Vigthoria model endpoint returned no choices for ${selfHostedModel}`);
|
|
3690
5087
|
}
|
|
3691
5088
|
catch (error) {
|
|
3692
5089
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3693
|
-
this.logger.debug(`
|
|
5090
|
+
this.logger.debug(`Dedicated Vigthoria model endpoint fallback failed for ${selfHostedModel}: ${errMsg}`);
|
|
3694
5091
|
}
|
|
3695
5092
|
return null;
|
|
3696
5093
|
}
|
|
@@ -3708,11 +5105,37 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3708
5105
|
return null;
|
|
3709
5106
|
}
|
|
3710
5107
|
isCloudModelId(resolvedModel) {
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
5108
|
+
const normalized = String(resolvedModel || '').trim().toLowerCase();
|
|
5109
|
+
const cloudIds = new Set([
|
|
5110
|
+
'deepseek-v3.1:671b-cloud',
|
|
5111
|
+
'moonshotai/kimi-k2.5',
|
|
5112
|
+
'vigthoria-cloud-pro',
|
|
5113
|
+
'vigthoria-cloud-k2',
|
|
5114
|
+
'vigthoria-cloud-ultra',
|
|
5115
|
+
'vigthoria-cloud-fast',
|
|
5116
|
+
'vigthoria-cloud-balanced',
|
|
5117
|
+
'vigthoria-cloud-code',
|
|
5118
|
+
'vigthoria-cloud-power',
|
|
5119
|
+
'vigthoria-cloud-maximum',
|
|
5120
|
+
'cloud-fast',
|
|
5121
|
+
'cloud-balanced',
|
|
5122
|
+
'cloud-code',
|
|
5123
|
+
'cloud-power',
|
|
5124
|
+
'cloud-maximum',
|
|
5125
|
+
'cloud',
|
|
5126
|
+
'cloud-reason',
|
|
5127
|
+
'ultra',
|
|
5128
|
+
]);
|
|
5129
|
+
return cloudIds.has(normalized) || normalized.includes('vigthoria-cloud-');
|
|
5130
|
+
}
|
|
5131
|
+
buildDirectChatHeaders(isCloud) {
|
|
5132
|
+
const headers = {
|
|
5133
|
+
'x-vigthoria-route-class': 'direct-chat',
|
|
5134
|
+
};
|
|
5135
|
+
if (isCloud) {
|
|
5136
|
+
headers['x-vigthoria-cloud-approved'] = 'true';
|
|
5137
|
+
}
|
|
5138
|
+
return headers;
|
|
3716
5139
|
}
|
|
3717
5140
|
canUseCloudModel() {
|
|
3718
5141
|
return this.config.hasCloudAccess();
|
|
@@ -4701,7 +6124,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4701
6124
|
resolveModelId(shortName) {
|
|
4702
6125
|
const modelMap = {
|
|
4703
6126
|
// ═══════════════════════════════════════════════════════════════
|
|
4704
|
-
//
|
|
6127
|
+
// Vigthoria server infrastructure models
|
|
4705
6128
|
// ═══════════════════════════════════════════════════════════════
|
|
4706
6129
|
'fast': 'vigthoria-v3-balanced-4b',
|
|
4707
6130
|
'mini': 'vigthoria-mini-0.6b',
|
|
@@ -4709,7 +6132,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4709
6132
|
'balanced-4b': 'vigthoria-v3-balanced-4b',
|
|
4710
6133
|
'creative': 'vigthoria-creative-9b-v4',
|
|
4711
6134
|
// Code Models - 35B is the default powerhouse
|
|
4712
|
-
'code': 'vigthoria-v3-code-35b', //
|
|
6135
|
+
'code': 'vigthoria-v3-code-35b', // Vigthoria server infrastructure 35B model
|
|
4713
6136
|
'code-30b': 'vigthoria-v3-code-35b',
|
|
4714
6137
|
'code-35b': 'vigthoria-v3-code-35b',
|
|
4715
6138
|
'code-8b': 'vigthoria-v3-code-9b',
|
|
@@ -4754,24 +6177,59 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4754
6177
|
};
|
|
4755
6178
|
}
|
|
4756
6179
|
}
|
|
6180
|
+
isHealthyServicePayload(payload) {
|
|
6181
|
+
const status = String(payload?.status || '').toLowerCase();
|
|
6182
|
+
return payload?.healthy === true
|
|
6183
|
+
|| status === 'healthy'
|
|
6184
|
+
|| status === 'ok'
|
|
6185
|
+
|| status === 'ready'
|
|
6186
|
+
|| status === 'degraded';
|
|
6187
|
+
}
|
|
6188
|
+
extractModelCount(payload) {
|
|
6189
|
+
if (Array.isArray(payload?.data))
|
|
6190
|
+
return payload.data.length;
|
|
6191
|
+
if (Array.isArray(payload?.models))
|
|
6192
|
+
return payload.models.length;
|
|
6193
|
+
if (Array.isArray(payload?.model_list))
|
|
6194
|
+
return payload.model_list.length;
|
|
6195
|
+
if (typeof payload?.total === 'number')
|
|
6196
|
+
return payload.total;
|
|
6197
|
+
return 0;
|
|
6198
|
+
}
|
|
6199
|
+
async probeModelList(client) {
|
|
6200
|
+
const candidates = ['/v1/models', '/models', '/api/models', '/api/tags'];
|
|
6201
|
+
for (const endpoint of candidates) {
|
|
6202
|
+
try {
|
|
6203
|
+
const response = await client.get(endpoint, { timeout: 5000 });
|
|
6204
|
+
const modelCount = this.extractModelCount(response.data);
|
|
6205
|
+
if (modelCount > 0) {
|
|
6206
|
+
return { modelCount, endpoint };
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
catch {
|
|
6210
|
+
// try next endpoint
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
return { modelCount: 0, endpoint: candidates[0] };
|
|
6214
|
+
}
|
|
4757
6215
|
async getModelsHealth() {
|
|
4758
6216
|
const modelsApiUrl = this.config.get('modelsApiUrl');
|
|
4759
6217
|
try {
|
|
4760
|
-
const [healthResponse,
|
|
6218
|
+
const [healthResponse, modelProbe] = await Promise.all([
|
|
4761
6219
|
this.modelRouterClient.get('/health', { timeout: 5000 }),
|
|
4762
|
-
this.modelRouterClient
|
|
6220
|
+
this.probeModelList(this.modelRouterClient),
|
|
4763
6221
|
]);
|
|
4764
|
-
const healthOk = healthResponse.data
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
const modelCount = Array.isArray(modelsResponse.data?.data) ? modelsResponse.data.data.length : 0;
|
|
6222
|
+
const healthOk = this.isHealthyServicePayload(healthResponse.data);
|
|
6223
|
+
const modelCount = modelProbe.modelCount;
|
|
6224
|
+
const ok = healthOk || modelCount > 0;
|
|
4768
6225
|
return {
|
|
4769
6226
|
name: 'Models API',
|
|
4770
6227
|
endpoint: `${modelsApiUrl}/health`,
|
|
4771
|
-
ok
|
|
6228
|
+
ok,
|
|
4772
6229
|
details: {
|
|
4773
6230
|
health: healthResponse.data,
|
|
4774
6231
|
modelCount,
|
|
6232
|
+
modelsEndpoint: modelProbe.endpoint,
|
|
4775
6233
|
},
|
|
4776
6234
|
};
|
|
4777
6235
|
}
|
|
@@ -4790,20 +6248,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4790
6248
|
return null;
|
|
4791
6249
|
}
|
|
4792
6250
|
try {
|
|
4793
|
-
const
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
6251
|
+
const [healthResponse, modelProbe] = await Promise.all([
|
|
6252
|
+
this.selfHostedModelRouterClient.get('/health', { timeout: 5000 }),
|
|
6253
|
+
this.probeModelList(this.selfHostedModelRouterClient),
|
|
6254
|
+
]);
|
|
6255
|
+
const healthOk = this.isHealthyServicePayload(healthResponse.data);
|
|
6256
|
+
const ok = healthOk || modelProbe.modelCount > 0;
|
|
4797
6257
|
return {
|
|
4798
|
-
name: '
|
|
6258
|
+
name: 'Vigthoria Models API',
|
|
4799
6259
|
endpoint: `${selfHostedModelsApiUrl}/health`,
|
|
4800
6260
|
ok,
|
|
4801
|
-
details: {
|
|
6261
|
+
details: {
|
|
6262
|
+
health: healthResponse.data,
|
|
6263
|
+
modelCount: modelProbe.modelCount,
|
|
6264
|
+
modelsEndpoint: modelProbe.endpoint,
|
|
6265
|
+
},
|
|
4802
6266
|
};
|
|
4803
6267
|
}
|
|
4804
6268
|
catch (error) {
|
|
4805
6269
|
return {
|
|
4806
|
-
name: '
|
|
6270
|
+
name: 'Vigthoria Models API',
|
|
4807
6271
|
endpoint: `${selfHostedModelsApiUrl}/health`,
|
|
4808
6272
|
ok: false,
|
|
4809
6273
|
error: error.message,
|
|
@@ -5056,7 +6520,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5056
6520
|
withTimeout(this.getDevtoolsBridgeStatus(), 'DevTools Bridge'),
|
|
5057
6521
|
]);
|
|
5058
6522
|
return {
|
|
5059
|
-
overallOk: v3Agent.ok && hyperLoop.ok
|
|
6523
|
+
overallOk: v3Agent.ok && (hyperLoop.ok || repoMemory.ok),
|
|
5060
6524
|
v3Agent,
|
|
5061
6525
|
hyperLoop,
|
|
5062
6526
|
repoMemory,
|