bosun 0.37.1 → 0.37.2
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/.env.example +4 -1
- package/agent-tool-config.mjs +14 -3
- package/bosun-skills.mjs +59 -4
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +48 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +2 -1
- package/setup-web-server.mjs +71 -10
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +110 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +3 -0
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +260 -38
- package/ui/modules/voice-client.js +662 -58
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +219 -9
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +22 -5
- package/ui-server.mjs +961 -103
- package/voice-relay.mjs +119 -11
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/ui-server.mjs
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
matchAgentProfile,
|
|
71
71
|
loadManifest,
|
|
72
72
|
getManifestPath,
|
|
73
|
+
scaffoldAgentProfiles,
|
|
73
74
|
} from "./library-manager.mjs";
|
|
74
75
|
import {
|
|
75
76
|
listCatalog,
|
|
@@ -161,7 +162,7 @@ const repoRoot = resolveRepoRoot();
|
|
|
161
162
|
const uiRootPreferred = resolve(__dirname, "ui");
|
|
162
163
|
const uiRootFallback = resolve(__dirname, "site", "ui");
|
|
163
164
|
const uiRoot = existsSync(uiRootPreferred) ? uiRootPreferred : uiRootFallback;
|
|
164
|
-
|
|
165
|
+
const libraryInitAttemptedRoots = new Set();
|
|
165
166
|
const MAX_VISION_FRAME_BYTES = Math.max(
|
|
166
167
|
128_000,
|
|
167
168
|
Number.parseInt(process.env.VISION_FRAME_MAX_BYTES || "", 10) || 2_000_000,
|
|
@@ -208,21 +209,238 @@ function parseVisionFrameDataUrl(dataUrl) {
|
|
|
208
209
|
return { ok: true, mimeType, base64Data, approxBytes, raw };
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
function ensureLibraryInitialized() {
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
function ensureLibraryInitialized(rootDir = repoRoot) {
|
|
213
|
+
const normalizedRoot = normalizeCandidatePath(rootDir) || repoRoot;
|
|
214
|
+
if (libraryInitAttemptedRoots.has(normalizedRoot)) return;
|
|
215
|
+
libraryInitAttemptedRoots.add(normalizedRoot);
|
|
214
216
|
try {
|
|
215
|
-
const manifestPath = getManifestPath(
|
|
216
|
-
const manifest = loadManifest(
|
|
217
|
+
const manifestPath = getManifestPath(normalizedRoot);
|
|
218
|
+
const manifest = loadManifest(normalizedRoot);
|
|
217
219
|
if (!existsSync(manifestPath) || !Array.isArray(manifest?.entries) || manifest.entries.length === 0) {
|
|
218
|
-
const result = initLibrary(
|
|
220
|
+
const result = initLibrary(normalizedRoot);
|
|
219
221
|
const count = result?.manifest?.entries?.length ?? 0;
|
|
220
222
|
if (count > 0) {
|
|
221
|
-
console.log(`[ui] Library initialized (${count} entries).`);
|
|
223
|
+
console.log(`[ui] Library initialized (${count} entries) at ${normalizedRoot}.`);
|
|
222
224
|
}
|
|
223
225
|
}
|
|
226
|
+
const scaffoldResult = scaffoldAgentProfiles(normalizedRoot);
|
|
227
|
+
if (Array.isArray(scaffoldResult?.written) && scaffoldResult.written.length > 0) {
|
|
228
|
+
rebuildManifest(normalizedRoot);
|
|
229
|
+
}
|
|
224
230
|
} catch (err) {
|
|
225
|
-
console.warn(`[ui] Library init failed: ${err.message}`);
|
|
231
|
+
console.warn(`[ui] Library init failed for ${normalizedRoot}: ${err.message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const VOICE_TOOL_ID_MAP = Object.freeze({
|
|
236
|
+
"search-files": ["search_code", "list_directory"],
|
|
237
|
+
"read-file": ["read_file_content"],
|
|
238
|
+
"edit-file": ["delegate_to_agent", "run_workspace_command"],
|
|
239
|
+
"run-command": ["run_command", "run_workspace_command"],
|
|
240
|
+
"web-search": ["ask_agent_context"],
|
|
241
|
+
"code-search": ["search_code", "get_workspace_context"],
|
|
242
|
+
"git-operations": ["run_workspace_command", "get_pr_status"],
|
|
243
|
+
"create-task": ["create_task"],
|
|
244
|
+
"delegate-task": ["delegate_to_agent", "ask_agent_context", "poll_background_session"],
|
|
245
|
+
"fetch-url": ["run_workspace_command"],
|
|
246
|
+
"list-directory": ["list_directory"],
|
|
247
|
+
"grep-search": ["search_code"],
|
|
248
|
+
"task-management": ["list_tasks", "get_task", "search_tasks", "get_task_stats", "delete_task", "comment_on_task"],
|
|
249
|
+
"notifications": ["dispatch_action"],
|
|
250
|
+
"vision-analysis": ["query_live_view"],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const BUILTIN_TOOL_ID_SET = new Set(
|
|
254
|
+
(Array.isArray(DEFAULT_BUILTIN_TOOLS) ? DEFAULT_BUILTIN_TOOLS : [])
|
|
255
|
+
.map((tool) => String(tool?.id || "").trim())
|
|
256
|
+
.filter(Boolean),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
function mapToolConfigIdsToVoiceToolNames(enabledIds = []) {
|
|
260
|
+
const resolved = new Set();
|
|
261
|
+
for (const rawId of Array.isArray(enabledIds) ? enabledIds : []) {
|
|
262
|
+
const id = String(rawId || "").trim();
|
|
263
|
+
if (!id) continue;
|
|
264
|
+
const mapped = VOICE_TOOL_ID_MAP[id];
|
|
265
|
+
if (Array.isArray(mapped) && mapped.length > 0) {
|
|
266
|
+
for (const toolName of mapped) resolved.add(String(toolName || "").trim());
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// If this is a known built-in tool id without a runtime mapping, skip it
|
|
270
|
+
// instead of treating the id as a voice runtime tool name.
|
|
271
|
+
if (BUILTIN_TOOL_ID_SET.has(id)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
resolved.add(id);
|
|
275
|
+
}
|
|
276
|
+
return resolved;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isVoiceAgentProfileEntry(entry, profile) {
|
|
280
|
+
if (!entry || entry.type !== "agent") return false;
|
|
281
|
+
const id = String(entry.id || "").trim().toLowerCase();
|
|
282
|
+
const tags = Array.isArray(entry.tags) ? entry.tags.map((t) => String(t || "").trim().toLowerCase()) : [];
|
|
283
|
+
const profileType = String(profile?.agentType || "").trim().toLowerCase();
|
|
284
|
+
if (profileType === "voice") return true;
|
|
285
|
+
if (id.startsWith("voice-agent")) return true;
|
|
286
|
+
if (tags.includes("voice") || tags.includes("audio-agent") || tags.includes("realtime")) return true;
|
|
287
|
+
if (profile && typeof profile === "object" && profile.voiceAgent === true) return true;
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function resolveAgentProfileType(entry, profile) {
|
|
292
|
+
const explicit = String(profile?.agentType || "").trim().toLowerCase();
|
|
293
|
+
if (explicit === "voice" || explicit === "task" || explicit === "chat") return explicit;
|
|
294
|
+
if (isVoiceAgentProfileEntry(entry, profile)) return "voice";
|
|
295
|
+
return "task";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveVoiceLibraryRoot(callContext = {}) {
|
|
299
|
+
const sessionId = String(callContext?.sessionId || "").trim();
|
|
300
|
+
if (!sessionId) return repoRoot;
|
|
301
|
+
try {
|
|
302
|
+
const tracker = getSessionTracker();
|
|
303
|
+
const session = tracker.getSessionById
|
|
304
|
+
? tracker.getSessionById(sessionId)
|
|
305
|
+
: (tracker.getSession ? tracker.getSession(sessionId) : null);
|
|
306
|
+
const workspaceDir = String(
|
|
307
|
+
session?.workspaceDir
|
|
308
|
+
|| session?.metadata?.workspaceDir
|
|
309
|
+
|| "",
|
|
310
|
+
).trim();
|
|
311
|
+
if (workspaceDir) return workspaceDir;
|
|
312
|
+
} catch {
|
|
313
|
+
// best effort
|
|
314
|
+
}
|
|
315
|
+
return repoRoot;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function listVoiceAgentProfiles(rootDir = repoRoot) {
|
|
319
|
+
const libraryRoot = rootDir || repoRoot;
|
|
320
|
+
ensureLibraryInitialized(libraryRoot);
|
|
321
|
+
const entries = listEntries(libraryRoot, { type: "agent" }) || [];
|
|
322
|
+
const profiles = [];
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
const profile = getEntryContent(libraryRoot, entry);
|
|
325
|
+
if (!isVoiceAgentProfileEntry(entry, profile)) continue;
|
|
326
|
+
profiles.push({
|
|
327
|
+
id: entry.id,
|
|
328
|
+
name: entry.name || entry.id,
|
|
329
|
+
description: entry.description || "",
|
|
330
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
331
|
+
model: String(profile?.model || "").trim() || null,
|
|
332
|
+
voicePersona: String(profile?.voicePersona || "").trim() || "neutral",
|
|
333
|
+
voiceInstructions: String(profile?.voiceInstructions || "").trim() || "",
|
|
334
|
+
skills: Array.isArray(profile?.skills) ? profile.skills.map((s) => String(s || "").trim()).filter(Boolean) : [],
|
|
335
|
+
promptOverride: String(profile?.promptOverride || "").trim() || null,
|
|
336
|
+
agentType: resolveAgentProfileType(entry, profile),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
profiles.sort((a, b) => {
|
|
341
|
+
const rank = (id) => {
|
|
342
|
+
if (id === "voice-agent-female") return 0;
|
|
343
|
+
if (id === "voice-agent-male") return 1;
|
|
344
|
+
if (id === "voice-agent") return 2;
|
|
345
|
+
return 3;
|
|
346
|
+
};
|
|
347
|
+
const ra = rank(a.id);
|
|
348
|
+
const rb = rank(b.id);
|
|
349
|
+
if (ra !== rb) return ra - rb;
|
|
350
|
+
return String(a.name || a.id).localeCompare(String(b.name || b.id));
|
|
351
|
+
});
|
|
352
|
+
return profiles;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function resolveActiveVoiceAgent(rootDir = repoRoot, requestedAgentId = "") {
|
|
356
|
+
const agents = listVoiceAgentProfiles(rootDir);
|
|
357
|
+
if (agents.length === 0) return { agents: [], selected: null };
|
|
358
|
+
const requested = String(requestedAgentId || "").trim();
|
|
359
|
+
const selected = agents.find((a) => a.id === requested)
|
|
360
|
+
|| agents.find((a) => a.id === "voice-agent-female")
|
|
361
|
+
|| agents[0];
|
|
362
|
+
return { agents, selected };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function applyVoiceAgentToolFilters(allTools, toolConfig = null) {
|
|
366
|
+
const tools = Array.isArray(allTools) ? [...allTools] : [];
|
|
367
|
+
if (!toolConfig || typeof toolConfig !== "object") return tools;
|
|
368
|
+
|
|
369
|
+
const disabledBuiltinIds = Array.isArray(toolConfig.disabledBuiltinTools)
|
|
370
|
+
? toolConfig.disabledBuiltinTools
|
|
371
|
+
: [];
|
|
372
|
+
const enabledIds = Array.isArray(toolConfig.enabledTools) ? toolConfig.enabledTools : null;
|
|
373
|
+
const enabledServers = Array.isArray(toolConfig.enabledMcpServers) ? toolConfig.enabledMcpServers : [];
|
|
374
|
+
|
|
375
|
+
const disabledNames = mapToolConfigIdsToVoiceToolNames(disabledBuiltinIds);
|
|
376
|
+
const enabledNames = enabledIds ? mapToolConfigIdsToVoiceToolNames(enabledIds) : null;
|
|
377
|
+
|
|
378
|
+
return tools.filter((tool) => {
|
|
379
|
+
const name = String(tool?.name || "").trim();
|
|
380
|
+
if (!name) return false;
|
|
381
|
+
if (name === "invoke_mcp_tool" && enabledServers.length === 0) return false;
|
|
382
|
+
if (enabledNames && enabledNames.size > 0) {
|
|
383
|
+
return enabledNames.has(name);
|
|
384
|
+
}
|
|
385
|
+
if (disabledNames.has(name)) return false;
|
|
386
|
+
return true;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildVoiceToolCapabilityPrompt(tools = [], toolConfig = null, selectedVoiceAgent = null) {
|
|
391
|
+
const runtimeTools = Array.isArray(tools) ? tools : [];
|
|
392
|
+
const enabledServers = Array.isArray(toolConfig?.enabledMcpServers)
|
|
393
|
+
? toolConfig.enabledMcpServers
|
|
394
|
+
: [];
|
|
395
|
+
const toolLines = runtimeTools
|
|
396
|
+
.slice(0, 48)
|
|
397
|
+
.map((tool) => {
|
|
398
|
+
const name = String(tool?.name || "").trim();
|
|
399
|
+
if (!name) return null;
|
|
400
|
+
const description = String(tool?.description || "").replace(/\s+/g, " ").trim();
|
|
401
|
+
return description ? `- ${name}: ${description}` : `- ${name}`;
|
|
402
|
+
})
|
|
403
|
+
.filter(Boolean);
|
|
404
|
+
const skills = Array.isArray(selectedVoiceAgent?.skills)
|
|
405
|
+
? selectedVoiceAgent.skills.map((skill) => String(skill || "").trim()).filter(Boolean)
|
|
406
|
+
: [];
|
|
407
|
+
const agentName = String(selectedVoiceAgent?.name || selectedVoiceAgent?.id || "Voice Agent").trim();
|
|
408
|
+
|
|
409
|
+
return [
|
|
410
|
+
"",
|
|
411
|
+
"## Active Voice Agent Capability Contract",
|
|
412
|
+
`Agent profile: ${agentName}.`,
|
|
413
|
+
"You can execute tools directly in this voice session. Do not claim you cannot use tools or must rely on chat-only help.",
|
|
414
|
+
"When the user asks for action or facts, call the most relevant tool first, then report results briefly.",
|
|
415
|
+
toolLines.length > 0
|
|
416
|
+
? "Enabled runtime tools:\n" + toolLines.join("\n")
|
|
417
|
+
: "Enabled runtime tools: none (tool calls unavailable for this profile).",
|
|
418
|
+
enabledServers.length > 0
|
|
419
|
+
? `Enabled MCP servers (for invoke_mcp_tool): ${enabledServers.join(", ")}.`
|
|
420
|
+
: "Enabled MCP servers: none.",
|
|
421
|
+
skills.length > 0
|
|
422
|
+
? `Voice agent skills: ${skills.join(", ")}.`
|
|
423
|
+
: "Voice agent skills: none specified.",
|
|
424
|
+
"",
|
|
425
|
+
"If you need the current tool list, call get_admin_help.",
|
|
426
|
+
].join("\n");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function listBosunRuntimeTools(context = {}) {
|
|
430
|
+
try {
|
|
431
|
+
const { getVoiceToolDefinitions } = await import("./voice-relay.mjs");
|
|
432
|
+
const defs = await getVoiceToolDefinitions({ delegateOnly: false, context });
|
|
433
|
+
return (Array.isArray(defs) ? defs : [])
|
|
434
|
+
.map((tool) => ({
|
|
435
|
+
id: String(tool?.name || "").trim(),
|
|
436
|
+
name: String(tool?.name || "").trim(),
|
|
437
|
+
description: String(tool?.description || "").trim(),
|
|
438
|
+
category: "Bosun",
|
|
439
|
+
}))
|
|
440
|
+
.filter((tool) => Boolean(tool.id))
|
|
441
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
442
|
+
} catch {
|
|
443
|
+
return [];
|
|
226
444
|
}
|
|
227
445
|
}
|
|
228
446
|
|
|
@@ -231,10 +449,27 @@ let _wfEngine;
|
|
|
231
449
|
let _wfNodes;
|
|
232
450
|
let _wfTemplates;
|
|
233
451
|
let _wfServicesReady = false;
|
|
452
|
+
let _wfServices = null;
|
|
234
453
|
let _wfRecommendedInstalled = false;
|
|
454
|
+
const _wfRecommendedInstalledByWorkspace = new Set();
|
|
455
|
+
const _wfEngineByWorkspace = new Map();
|
|
235
456
|
let _wfInitPromise = null;
|
|
236
457
|
let _wfInitDone = false;
|
|
237
458
|
let _wfLoadedBase = null;
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Test-only: inject a mock workflow engine module and pre-seed the per-workspace
|
|
462
|
+
* engine cache so that dispatchWorkflowEvent uses the specified mock.
|
|
463
|
+
*/
|
|
464
|
+
let _testDefaultEngine = null;
|
|
465
|
+
export function _testInjectWorkflowEngine(mockModule, mockEngine) {
|
|
466
|
+
_wfEngine = mockModule;
|
|
467
|
+
_wfInitDone = true;
|
|
468
|
+
_wfInitPromise = null;
|
|
469
|
+
_testDefaultEngine = mockEngine || null;
|
|
470
|
+
_wfEngineByWorkspace.clear();
|
|
471
|
+
}
|
|
472
|
+
|
|
238
473
|
let workflowEventDedupWindowMs = (() => {
|
|
239
474
|
const parsed = Number.parseInt(process.env.WORKFLOW_EVENT_DEDUP_WINDOW_MS || "15000", 10);
|
|
240
475
|
if (!Number.isFinite(parsed) || parsed <= 0) return 15_000;
|
|
@@ -441,6 +676,7 @@ async function getWorkflowEngineModule() {
|
|
|
441
676
|
meeting: meetingService,
|
|
442
677
|
prompts: promptBundle?.prompts || null,
|
|
443
678
|
};
|
|
679
|
+
_wfServices = services;
|
|
444
680
|
_wfEngine.getWorkflowEngine({ services });
|
|
445
681
|
_wfServicesReady = true;
|
|
446
682
|
|
|
@@ -533,6 +769,100 @@ async function getWorkflowEngineModule() {
|
|
|
533
769
|
return _wfEngine;
|
|
534
770
|
}
|
|
535
771
|
|
|
772
|
+
function getWorkflowWorkspaceKey(workspaceDir = "") {
|
|
773
|
+
const normalized = normalizeCandidatePath(workspaceDir) || repoRoot;
|
|
774
|
+
if (process.platform === "win32") {
|
|
775
|
+
return normalized.toLowerCase();
|
|
776
|
+
}
|
|
777
|
+
return normalized;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function getWorkflowStoragePaths(workspaceDir = "") {
|
|
781
|
+
const root = normalizeCandidatePath(workspaceDir) || repoRoot;
|
|
782
|
+
return {
|
|
783
|
+
workspaceRoot: root,
|
|
784
|
+
workflowDir: resolve(root, ".bosun", "workflows"),
|
|
785
|
+
runsDir: resolve(root, ".bosun", "workflow-runs"),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function maybeBootstrapWorkspaceWorkflowTemplates(engine, workspaceKey, workspaceLabel) {
|
|
790
|
+
if (!engine || !_wfTemplates) return;
|
|
791
|
+
if (_wfRecommendedInstalledByWorkspace.has(workspaceKey)) return;
|
|
792
|
+
try {
|
|
793
|
+
const selection = resolveWorkflowBootstrapSelection(_wfTemplates);
|
|
794
|
+
let result = { installed: [], skipped: [], errors: [] };
|
|
795
|
+
if (selection.enabled) {
|
|
796
|
+
if (
|
|
797
|
+
Array.isArray(selection.templateIds) &&
|
|
798
|
+
selection.templateIds.length > 0 &&
|
|
799
|
+
typeof _wfTemplates.installTemplateSet === "function"
|
|
800
|
+
) {
|
|
801
|
+
result = _wfTemplates.installTemplateSet(engine, selection.templateIds);
|
|
802
|
+
} else if (
|
|
803
|
+
selection.source === "recommended" &&
|
|
804
|
+
typeof _wfTemplates.installRecommendedTemplates === "function"
|
|
805
|
+
) {
|
|
806
|
+
result = _wfTemplates.installRecommendedTemplates(engine);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (typeof _wfTemplates.reconcileInstalledTemplates === "function") {
|
|
810
|
+
_wfTemplates.reconcileInstalledTemplates(engine, {
|
|
811
|
+
autoUpdateUnmodified: true,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (result.installed.length) {
|
|
815
|
+
console.log(
|
|
816
|
+
`[workflows] Installed ${result.installed.length} default workflow templates for workspace ${workspaceLabel}`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
} catch (err) {
|
|
820
|
+
console.warn(
|
|
821
|
+
`[workflows] Default template install failed for workspace ${workspaceLabel}: ${err.message}`,
|
|
822
|
+
);
|
|
823
|
+
} finally {
|
|
824
|
+
_wfRecommendedInstalledByWorkspace.add(workspaceKey);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function getWorkflowRequestContext(reqUrl) {
|
|
829
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(reqUrl, { allowAll: false });
|
|
830
|
+
if (!workspaceContext) {
|
|
831
|
+
return { ok: false, status: 400, error: "Unknown workspace. Set a valid workspace query value." };
|
|
832
|
+
}
|
|
833
|
+
const wfMod = await getWorkflowEngineModule();
|
|
834
|
+
if (!wfMod?.WorkflowEngine) {
|
|
835
|
+
return { ok: false, status: 503, error: "Workflow engine not available" };
|
|
836
|
+
}
|
|
837
|
+
const paths = getWorkflowStoragePaths(workspaceContext.workspaceDir);
|
|
838
|
+
const workspaceKey = getWorkflowWorkspaceKey(paths.workspaceRoot);
|
|
839
|
+
let engine = _wfEngineByWorkspace.get(workspaceKey) || null;
|
|
840
|
+
if (!engine) {
|
|
841
|
+
if (_testDefaultEngine) {
|
|
842
|
+
engine = _testDefaultEngine;
|
|
843
|
+
} else {
|
|
844
|
+
engine = new wfMod.WorkflowEngine({
|
|
845
|
+
workflowDir: paths.workflowDir,
|
|
846
|
+
runsDir: paths.runsDir,
|
|
847
|
+
services: _wfServices || {},
|
|
848
|
+
});
|
|
849
|
+
engine.load();
|
|
850
|
+
}
|
|
851
|
+
_wfEngineByWorkspace.set(workspaceKey, engine);
|
|
852
|
+
}
|
|
853
|
+
maybeBootstrapWorkspaceWorkflowTemplates(
|
|
854
|
+
engine,
|
|
855
|
+
workspaceKey,
|
|
856
|
+
workspaceContext.workspaceId || workspaceKey,
|
|
857
|
+
);
|
|
858
|
+
return {
|
|
859
|
+
ok: true,
|
|
860
|
+
wfMod,
|
|
861
|
+
engine,
|
|
862
|
+
workspaceContext: { ...workspaceContext, workspaceDir: paths.workspaceRoot },
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
536
866
|
function allowWorkflowEvent(dedupKey, windowMs = workflowEventDedupWindowMs) {
|
|
537
867
|
if (!dedupKey) return true;
|
|
538
868
|
const now = Date.now();
|
|
@@ -576,10 +906,11 @@ async function dispatchWorkflowEvent(eventType, eventData = {}, opts = {}) {
|
|
|
576
906
|
return false;
|
|
577
907
|
}
|
|
578
908
|
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
909
|
+
const wfCtx = await getWorkflowRequestContext(
|
|
910
|
+
new URL(`http://localhost?workspace=active`),
|
|
911
|
+
);
|
|
912
|
+
if (!wfCtx?.ok) return false;
|
|
913
|
+
const engine = wfCtx.engine;
|
|
583
914
|
if (!engine?.evaluateTriggers || !engine?.execute) return false;
|
|
584
915
|
|
|
585
916
|
const payload = buildWorkflowEventPayload(eventType, eventData, "ui-server");
|
|
@@ -1295,6 +1626,79 @@ function resolveActiveWorkspaceExecutionContext() {
|
|
|
1295
1626
|
};
|
|
1296
1627
|
}
|
|
1297
1628
|
|
|
1629
|
+
function resolveWorkspaceContextById(workspaceId = "") {
|
|
1630
|
+
const requestedId = String(workspaceId || "").trim().toLowerCase();
|
|
1631
|
+
if (!requestedId) return resolveActiveWorkspaceExecutionContext();
|
|
1632
|
+
const configDir = resolveUiConfigDir();
|
|
1633
|
+
if (!configDir) return null;
|
|
1634
|
+
const listed = listManagedWorkspaces(configDir, { repoRoot });
|
|
1635
|
+
const workspace = listed.find(
|
|
1636
|
+
(entry) => String(entry?.id || "").trim().toLowerCase() === requestedId,
|
|
1637
|
+
);
|
|
1638
|
+
if (!workspace) return null;
|
|
1639
|
+
const id = String(workspace.id || "").trim();
|
|
1640
|
+
return {
|
|
1641
|
+
workspaceId: id,
|
|
1642
|
+
workspaceDir: pickWorkspaceRepoDir(workspace) || repoRoot,
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function resolveWorkspaceContextFromRequest(reqUrl, opts = {}) {
|
|
1647
|
+
const allowAll = opts.allowAll !== false;
|
|
1648
|
+
const workspaceRaw = String(reqUrl?.searchParams?.get("workspace") || "").trim();
|
|
1649
|
+
const workspaceKey = workspaceRaw.toLowerCase();
|
|
1650
|
+
if (allowAll && (workspaceKey === "all" || workspaceKey === "*")) {
|
|
1651
|
+
return {
|
|
1652
|
+
allWorkspaces: true,
|
|
1653
|
+
workspaceId: "",
|
|
1654
|
+
workspaceDir: repoRoot,
|
|
1655
|
+
workspaceFilter: "",
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (!workspaceKey || workspaceKey === "active") {
|
|
1659
|
+
const active = resolveActiveWorkspaceExecutionContext();
|
|
1660
|
+
return {
|
|
1661
|
+
allWorkspaces: false,
|
|
1662
|
+
workspaceId: String(active.workspaceId || "").trim(),
|
|
1663
|
+
workspaceDir: normalizeCandidatePath(active.workspaceDir) || repoRoot,
|
|
1664
|
+
workspaceFilter: String(active.workspaceId || "").trim().toLowerCase(),
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
const explicit = resolveWorkspaceContextById(workspaceKey);
|
|
1668
|
+
if (!explicit) return null;
|
|
1669
|
+
return {
|
|
1670
|
+
allWorkspaces: false,
|
|
1671
|
+
workspaceId: String(explicit.workspaceId || "").trim(),
|
|
1672
|
+
workspaceDir: normalizeCandidatePath(explicit.workspaceDir) || repoRoot,
|
|
1673
|
+
workspaceFilter: String(explicit.workspaceId || "").trim().toLowerCase(),
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function resolveSessionWorkspaceMeta(session) {
|
|
1678
|
+
const metadata =
|
|
1679
|
+
session && typeof session.metadata === "object" && session.metadata
|
|
1680
|
+
? session.metadata
|
|
1681
|
+
: null;
|
|
1682
|
+
return {
|
|
1683
|
+
workspaceId: String(metadata?.workspaceId || "").trim().toLowerCase(),
|
|
1684
|
+
workspaceDir: normalizeCandidatePath(metadata?.workspaceDir),
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function sessionMatchesWorkspaceContext(session, workspaceContext) {
|
|
1689
|
+
if (!session) return false;
|
|
1690
|
+
if (!workspaceContext || workspaceContext.allWorkspaces) return true;
|
|
1691
|
+
const sessionWorkspace = resolveSessionWorkspaceMeta(session);
|
|
1692
|
+
if (sessionWorkspace.workspaceId) {
|
|
1693
|
+
return sessionWorkspace.workspaceId === String(workspaceContext.workspaceFilter || "").trim().toLowerCase();
|
|
1694
|
+
}
|
|
1695
|
+
const activeWorkspaceDir = normalizeCandidatePath(workspaceContext.workspaceDir);
|
|
1696
|
+
if (sessionWorkspace.workspaceDir && activeWorkspaceDir) {
|
|
1697
|
+
return sessionWorkspace.workspaceDir === activeWorkspaceDir;
|
|
1698
|
+
}
|
|
1699
|
+
return !workspaceContext.workspaceFilter;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1298
1702
|
function resolveSessionWorkspaceDir(session = null) {
|
|
1299
1703
|
const metadata =
|
|
1300
1704
|
session && typeof session.metadata === "object" && session.metadata
|
|
@@ -5634,6 +6038,128 @@ function summarizeTelemetry(metrics, days) {
|
|
|
5634
6038
|
};
|
|
5635
6039
|
}
|
|
5636
6040
|
|
|
6041
|
+
// ── Usage Analytics ─────────────────────────────────────────────────────────
|
|
6042
|
+
|
|
6043
|
+
/**
|
|
6044
|
+
* Build comprehensive usage analytics from agent-work-stream.jsonl.
|
|
6045
|
+
* Aggregates agent runs, skill invocations, MCP tool calls, and daily trends.
|
|
6046
|
+
*
|
|
6047
|
+
* @param {number} [days] - Look-back window in days; 0 = all time.
|
|
6048
|
+
* @returns {Promise<Object>}
|
|
6049
|
+
*/
|
|
6050
|
+
async function buildUsageAnalytics(days) {
|
|
6051
|
+
const logDir = resolveAgentWorkLogDir();
|
|
6052
|
+
const streamPath = resolve(logDir, "agent-work-stream.jsonl");
|
|
6053
|
+
const events = await readJsonlTail(streamPath, 100_000);
|
|
6054
|
+
|
|
6055
|
+
const cutoff = days ? Date.now() - days * 24 * 60 * 60 * 1000 : 0;
|
|
6056
|
+
|
|
6057
|
+
let agentRuns = 0;
|
|
6058
|
+
let skillInvocations = 0;
|
|
6059
|
+
let mcpToolCalls = 0;
|
|
6060
|
+
let oldestTs = Infinity;
|
|
6061
|
+
let newestTs = 0;
|
|
6062
|
+
|
|
6063
|
+
/** @type {Map<string,number>} */
|
|
6064
|
+
const agents = new Map();
|
|
6065
|
+
/** @type {Map<string,number>} */
|
|
6066
|
+
const skills = new Map();
|
|
6067
|
+
/** @type {Map<string,number>} */
|
|
6068
|
+
const mcpTools = new Map();
|
|
6069
|
+
|
|
6070
|
+
/** dailyAgents[date][executor] = count */
|
|
6071
|
+
const dailyAgents = {};
|
|
6072
|
+
/** dailySkills[date][skill] = count */
|
|
6073
|
+
const dailySkills = {};
|
|
6074
|
+
/** dailyMcp[date][tool] = count */
|
|
6075
|
+
const dailyMcp = {};
|
|
6076
|
+
|
|
6077
|
+
const allDates = new Set();
|
|
6078
|
+
|
|
6079
|
+
for (const e of events) {
|
|
6080
|
+
const ts = Date.parse(e.timestamp || "");
|
|
6081
|
+
if (!Number.isFinite(ts)) continue;
|
|
6082
|
+
if (cutoff && ts < cutoff) continue;
|
|
6083
|
+
if (ts < oldestTs) oldestTs = ts;
|
|
6084
|
+
if (ts > newestTs) newestTs = ts;
|
|
6085
|
+
const day = (e.timestamp || "").slice(0, 10);
|
|
6086
|
+
if (day) allDates.add(day);
|
|
6087
|
+
|
|
6088
|
+
if (e.event_type === "session_start") {
|
|
6089
|
+
agentRuns++;
|
|
6090
|
+
const exec = e.executor || "unknown";
|
|
6091
|
+
agents.set(exec, (agents.get(exec) || 0) + 1);
|
|
6092
|
+
if (day) {
|
|
6093
|
+
(dailyAgents[day] = dailyAgents[day] || {})[exec] =
|
|
6094
|
+
(dailyAgents[day][exec] || 0) + 1;
|
|
6095
|
+
}
|
|
6096
|
+
} else if (e.event_type === "skill_invoke") {
|
|
6097
|
+
skillInvocations++;
|
|
6098
|
+
const skill = e.data?.skill_name || e.skill_name || "unknown";
|
|
6099
|
+
skills.set(skill, (skills.get(skill) || 0) + 1);
|
|
6100
|
+
if (day) {
|
|
6101
|
+
(dailySkills[day] = dailySkills[day] || {})[skill] =
|
|
6102
|
+
(dailySkills[day][skill] || 0) + 1;
|
|
6103
|
+
}
|
|
6104
|
+
} else if (e.event_type === "tool_call") {
|
|
6105
|
+
mcpToolCalls++;
|
|
6106
|
+
const tool = e.data?.tool_name || e.tool_name || "unknown";
|
|
6107
|
+
mcpTools.set(tool, (mcpTools.get(tool) || 0) + 1);
|
|
6108
|
+
if (day) {
|
|
6109
|
+
(dailyMcp[day] = dailyMcp[day] || {})[tool] =
|
|
6110
|
+
(dailyMcp[day][tool] || 0) + 1;
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
|
|
6115
|
+
const sortedDates = [...allDates].sort();
|
|
6116
|
+
const dayCount = sortedDates.length || 1;
|
|
6117
|
+
const total = agentRuns + skillInvocations + mcpToolCalls;
|
|
6118
|
+
const avgPerDay = Math.round(total / dayCount);
|
|
6119
|
+
|
|
6120
|
+
const topAgents = [...agents.entries()]
|
|
6121
|
+
.sort((a, b) => b[1] - a[1])
|
|
6122
|
+
.slice(0, 8)
|
|
6123
|
+
.map(([name, count]) => ({ name, count }));
|
|
6124
|
+
const topSkills = [...skills.entries()]
|
|
6125
|
+
.sort((a, b) => b[1] - a[1])
|
|
6126
|
+
.slice(0, 8)
|
|
6127
|
+
.map(([name, count]) => ({ name, count }));
|
|
6128
|
+
const topMcpTools = [...mcpTools.entries()]
|
|
6129
|
+
.sort((a, b) => b[1] - a[1])
|
|
6130
|
+
.slice(0, 8)
|
|
6131
|
+
.map(([name, count]) => ({ name, count }));
|
|
6132
|
+
|
|
6133
|
+
// Build trend series for top-6 items per category
|
|
6134
|
+
const topAgentNames = topAgents.slice(0, 6).map((a) => a.name);
|
|
6135
|
+
const topSkillNames = topSkills.slice(0, 6).map((s) => s.name);
|
|
6136
|
+
const topMcpNames = topMcpTools.slice(0, 6).map((t) => t.name);
|
|
6137
|
+
|
|
6138
|
+
const trend = { dates: sortedDates, agents: {}, skills: {}, mcpTools: {} };
|
|
6139
|
+
for (const name of topAgentNames) {
|
|
6140
|
+
trend.agents[name] = sortedDates.map((d) => dailyAgents[d]?.[name] || 0);
|
|
6141
|
+
}
|
|
6142
|
+
for (const name of topSkillNames) {
|
|
6143
|
+
trend.skills[name] = sortedDates.map((d) => dailySkills[d]?.[name] || 0);
|
|
6144
|
+
}
|
|
6145
|
+
for (const name of topMcpNames) {
|
|
6146
|
+
trend.mcpTools[name] = sortedDates.map((d) => dailyMcp[d]?.[name] || 0);
|
|
6147
|
+
}
|
|
6148
|
+
|
|
6149
|
+
return {
|
|
6150
|
+
agentRuns,
|
|
6151
|
+
skillInvocations,
|
|
6152
|
+
mcpToolCalls,
|
|
6153
|
+
avgPerDay,
|
|
6154
|
+
lastActiveAt: newestTs < Infinity && newestTs > 0 ? new Date(newestTs).toISOString() : null,
|
|
6155
|
+
sinceAt: oldestTs < Infinity ? new Date(oldestTs).toISOString() : null,
|
|
6156
|
+
topAgents,
|
|
6157
|
+
topSkills,
|
|
6158
|
+
topMcpTools,
|
|
6159
|
+
trend,
|
|
6160
|
+
};
|
|
6161
|
+
}
|
|
6162
|
+
|
|
5637
6163
|
function resolveAgentWorkLogDir() {
|
|
5638
6164
|
const candidates = [
|
|
5639
6165
|
resolve(repoRoot, ".cache", "agent-work-logs"),
|
|
@@ -5942,7 +6468,15 @@ async function handleApi(req, res, url) {
|
|
|
5942
6468
|
if (path === "/api/tasks") {
|
|
5943
6469
|
const status = url.searchParams.get("status") || "";
|
|
5944
6470
|
const projectId = url.searchParams.get("project") || "";
|
|
5945
|
-
const
|
|
6471
|
+
const workspaceQueryRaw = String(url.searchParams.get("workspace") || "").trim();
|
|
6472
|
+
let workspaceFilter = workspaceQueryRaw.toLowerCase();
|
|
6473
|
+
if (!workspaceFilter || workspaceFilter === "active") {
|
|
6474
|
+
const activeWorkspace = getActiveManagedWorkspace(resolveUiConfigDir());
|
|
6475
|
+
workspaceFilter = String(activeWorkspace?.id || "").trim().toLowerCase();
|
|
6476
|
+
}
|
|
6477
|
+
if (workspaceFilter === "*" || workspaceFilter === "all") {
|
|
6478
|
+
workspaceFilter = "";
|
|
6479
|
+
}
|
|
5946
6480
|
const repositoryFilter = (url.searchParams.get("repository") || "").trim().toLowerCase();
|
|
5947
6481
|
const page = Math.max(0, Number(url.searchParams.get("page") || "0"));
|
|
5948
6482
|
const pageSize = Math.min(
|
|
@@ -6545,14 +7079,34 @@ async function handleApi(req, res, url) {
|
|
|
6545
7079
|
|
|
6546
7080
|
if (path === "/api/library") {
|
|
6547
7081
|
try {
|
|
6548
|
-
|
|
7082
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7083
|
+
if (!workspaceContext) {
|
|
7084
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7085
|
+
return;
|
|
7086
|
+
}
|
|
7087
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7088
|
+
ensureLibraryInitialized(libraryRoot);
|
|
6549
7089
|
const typeRaw = (url.searchParams.get("type") || "").trim();
|
|
7090
|
+
const agentTypeRaw = String(url.searchParams.get("agentType") || "").trim().toLowerCase();
|
|
6550
7091
|
const search = (url.searchParams.get("search") || "").trim();
|
|
6551
7092
|
const type = typeRaw && typeRaw !== "all" ? typeRaw : "";
|
|
6552
|
-
|
|
7093
|
+
let data = listEntries(libraryRoot, {
|
|
6553
7094
|
type: type || undefined,
|
|
6554
7095
|
search: search || undefined,
|
|
6555
7096
|
});
|
|
7097
|
+
data = data.map((entry) => {
|
|
7098
|
+
if (entry?.type !== "agent") return entry;
|
|
7099
|
+
const profile = getEntryContent(libraryRoot, entry);
|
|
7100
|
+
return {
|
|
7101
|
+
...entry,
|
|
7102
|
+
agentType: resolveAgentProfileType(entry, profile),
|
|
7103
|
+
};
|
|
7104
|
+
});
|
|
7105
|
+
if (type === "agent" && (agentTypeRaw === "voice" || agentTypeRaw === "task" || agentTypeRaw === "chat")) {
|
|
7106
|
+
data = data.filter((entry) => {
|
|
7107
|
+
return String(entry?.agentType || "").trim().toLowerCase() === agentTypeRaw;
|
|
7108
|
+
});
|
|
7109
|
+
}
|
|
6556
7110
|
jsonResponse(res, 200, { ok: true, data });
|
|
6557
7111
|
} catch (err) {
|
|
6558
7112
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -6562,19 +7116,25 @@ async function handleApi(req, res, url) {
|
|
|
6562
7116
|
|
|
6563
7117
|
if (path === "/api/library/entry") {
|
|
6564
7118
|
try {
|
|
6565
|
-
|
|
7119
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7120
|
+
if (!workspaceContext) {
|
|
7121
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7122
|
+
return;
|
|
7123
|
+
}
|
|
7124
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7125
|
+
ensureLibraryInitialized(libraryRoot);
|
|
6566
7126
|
if (req.method === "GET") {
|
|
6567
7127
|
const id = (url.searchParams.get("id") || "").trim();
|
|
6568
7128
|
if (!id) {
|
|
6569
7129
|
jsonResponse(res, 400, { ok: false, error: "id required" });
|
|
6570
7130
|
return;
|
|
6571
7131
|
}
|
|
6572
|
-
const entry = getEntry(
|
|
7132
|
+
const entry = getEntry(libraryRoot, id);
|
|
6573
7133
|
if (!entry) {
|
|
6574
7134
|
jsonResponse(res, 404, { ok: false, error: "not found" });
|
|
6575
7135
|
return;
|
|
6576
7136
|
}
|
|
6577
|
-
const content = getEntryContent(
|
|
7137
|
+
const content = getEntryContent(libraryRoot, entry);
|
|
6578
7138
|
jsonResponse(res, 200, { ok: true, data: { ...entry, content } });
|
|
6579
7139
|
return;
|
|
6580
7140
|
}
|
|
@@ -6582,7 +7142,7 @@ async function handleApi(req, res, url) {
|
|
|
6582
7142
|
if (req.method === "POST") {
|
|
6583
7143
|
const body = await readJsonBody(req);
|
|
6584
7144
|
const { content, ...entryData } = body || {};
|
|
6585
|
-
const entry = upsertEntry(
|
|
7145
|
+
const entry = upsertEntry(libraryRoot, entryData, content);
|
|
6586
7146
|
jsonResponse(res, 200, { ok: true, data: entry });
|
|
6587
7147
|
return;
|
|
6588
7148
|
}
|
|
@@ -6594,7 +7154,7 @@ async function handleApi(req, res, url) {
|
|
|
6594
7154
|
jsonResponse(res, 400, { ok: false, error: "id required" });
|
|
6595
7155
|
return;
|
|
6596
7156
|
}
|
|
6597
|
-
const deleted = deleteEntry(
|
|
7157
|
+
const deleted = deleteEntry(libraryRoot, id, { deleteFile: Boolean(body?.deleteFile) });
|
|
6598
7158
|
if (!deleted) {
|
|
6599
7159
|
jsonResponse(res, 404, { ok: false, error: "not found" });
|
|
6600
7160
|
return;
|
|
@@ -6612,8 +7172,14 @@ async function handleApi(req, res, url) {
|
|
|
6612
7172
|
|
|
6613
7173
|
if (path === "/api/library/scopes") {
|
|
6614
7174
|
try {
|
|
6615
|
-
|
|
6616
|
-
|
|
7175
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7176
|
+
if (!workspaceContext) {
|
|
7177
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7178
|
+
return;
|
|
7179
|
+
}
|
|
7180
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7181
|
+
ensureLibraryInitialized(libraryRoot);
|
|
7182
|
+
const result = detectScopes(libraryRoot);
|
|
6617
7183
|
jsonResponse(res, 200, { ok: true, data: result?.scopes || [] });
|
|
6618
7184
|
} catch (err) {
|
|
6619
7185
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -6623,7 +7189,13 @@ async function handleApi(req, res, url) {
|
|
|
6623
7189
|
|
|
6624
7190
|
if (path === "/api/library/init" && req.method === "POST") {
|
|
6625
7191
|
try {
|
|
6626
|
-
const
|
|
7192
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7193
|
+
if (!workspaceContext) {
|
|
7194
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7195
|
+
return;
|
|
7196
|
+
}
|
|
7197
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7198
|
+
const result = initLibrary(libraryRoot);
|
|
6627
7199
|
const entriesCount = result?.manifest?.entries?.length ?? 0;
|
|
6628
7200
|
const scaffoldedCount = result?.scaffolded?.written?.length ?? 0;
|
|
6629
7201
|
jsonResponse(res, 200, {
|
|
@@ -6638,7 +7210,13 @@ async function handleApi(req, res, url) {
|
|
|
6638
7210
|
|
|
6639
7211
|
if (path === "/api/library/rebuild" && req.method === "POST") {
|
|
6640
7212
|
try {
|
|
6641
|
-
const
|
|
7213
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7214
|
+
if (!workspaceContext) {
|
|
7215
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7216
|
+
return;
|
|
7217
|
+
}
|
|
7218
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7219
|
+
const result = rebuildManifest(libraryRoot);
|
|
6642
7220
|
jsonResponse(res, 200, {
|
|
6643
7221
|
ok: true,
|
|
6644
7222
|
data: {
|
|
@@ -6655,9 +7233,15 @@ async function handleApi(req, res, url) {
|
|
|
6655
7233
|
|
|
6656
7234
|
if (path === "/api/library/match-profile") {
|
|
6657
7235
|
try {
|
|
6658
|
-
|
|
7236
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7237
|
+
if (!workspaceContext) {
|
|
7238
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7239
|
+
return;
|
|
7240
|
+
}
|
|
7241
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7242
|
+
ensureLibraryInitialized(libraryRoot);
|
|
6659
7243
|
const title = (url.searchParams.get("title") || "").trim();
|
|
6660
|
-
const match = matchAgentProfile(
|
|
7244
|
+
const match = matchAgentProfile(libraryRoot, title);
|
|
6661
7245
|
jsonResponse(res, 200, { ok: true, data: match || null });
|
|
6662
7246
|
} catch (err) {
|
|
6663
7247
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -6757,7 +7341,14 @@ async function handleApi(req, res, url) {
|
|
|
6757
7341
|
if (path === "/api/agent-tools/available") {
|
|
6758
7342
|
try {
|
|
6759
7343
|
const available = await listAvailableTools(repoRoot);
|
|
6760
|
-
|
|
7344
|
+
const bosunTools = await listBosunRuntimeTools({});
|
|
7345
|
+
jsonResponse(res, 200, {
|
|
7346
|
+
ok: true,
|
|
7347
|
+
data: {
|
|
7348
|
+
...available,
|
|
7349
|
+
bosunTools,
|
|
7350
|
+
},
|
|
7351
|
+
});
|
|
6761
7352
|
} catch (err) {
|
|
6762
7353
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
6763
7354
|
}
|
|
@@ -6775,7 +7366,29 @@ async function handleApi(req, res, url) {
|
|
|
6775
7366
|
return;
|
|
6776
7367
|
}
|
|
6777
7368
|
const effective = getEffectiveTools(repoRoot, agentId);
|
|
6778
|
-
|
|
7369
|
+
const bosunTools = await listBosunRuntimeTools({});
|
|
7370
|
+
const raw = getAgentToolConfig(repoRoot, agentId);
|
|
7371
|
+
const enabledSet = Array.isArray(raw?.enabledTools) && raw.enabledTools.length > 0
|
|
7372
|
+
? new Set(raw.enabledTools.map((id) => String(id || "").trim()).filter(Boolean))
|
|
7373
|
+
: null;
|
|
7374
|
+
const bosunToolIdSet = new Set(
|
|
7375
|
+
bosunTools.map((tool) => String(tool?.id || "").trim()).filter(Boolean),
|
|
7376
|
+
);
|
|
7377
|
+
const hasBosunAllowlist = Boolean(
|
|
7378
|
+
enabledSet && [...enabledSet].some((id) => bosunToolIdSet.has(id)),
|
|
7379
|
+
);
|
|
7380
|
+
const effectiveBosunTools = bosunTools.map((tool) => ({
|
|
7381
|
+
...tool,
|
|
7382
|
+
enabled: hasBosunAllowlist ? enabledSet.has(tool.id) : true,
|
|
7383
|
+
}));
|
|
7384
|
+
jsonResponse(res, 200, {
|
|
7385
|
+
ok: true,
|
|
7386
|
+
data: {
|
|
7387
|
+
...effective,
|
|
7388
|
+
bosunTools: effectiveBosunTools,
|
|
7389
|
+
enabledTools: raw?.enabledTools ?? null,
|
|
7390
|
+
},
|
|
7391
|
+
});
|
|
6779
7392
|
return;
|
|
6780
7393
|
}
|
|
6781
7394
|
|
|
@@ -6892,7 +7505,7 @@ async function handleApi(req, res, url) {
|
|
|
6892
7505
|
ok: true,
|
|
6893
7506
|
activeId: String(active?.id || wsId),
|
|
6894
7507
|
});
|
|
6895
|
-
broadcastUiEvent(["workspaces", "tasks", "overview"], "invalidate", {
|
|
7508
|
+
broadcastUiEvent(["workspaces", "tasks", "overview", "sessions", "workflows", "library"], "invalidate", {
|
|
6896
7509
|
reason: "workspace-switched",
|
|
6897
7510
|
workspaceId: wsId,
|
|
6898
7511
|
});
|
|
@@ -7319,6 +7932,17 @@ async function handleApi(req, res, url) {
|
|
|
7319
7932
|
return;
|
|
7320
7933
|
}
|
|
7321
7934
|
|
|
7935
|
+
if (path === "/api/analytics/usage") {
|
|
7936
|
+
try {
|
|
7937
|
+
const days = Number(url.searchParams.get("days") || "30");
|
|
7938
|
+
const data = await buildUsageAnalytics(days || 0);
|
|
7939
|
+
jsonResponse(res, 200, { ok: true, data });
|
|
7940
|
+
} catch (err) {
|
|
7941
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7942
|
+
}
|
|
7943
|
+
return;
|
|
7944
|
+
}
|
|
7945
|
+
|
|
7322
7946
|
if (path === "/api/agent-logs/context") {
|
|
7323
7947
|
try {
|
|
7324
7948
|
const query = url.searchParams.get("query") || "";
|
|
@@ -7886,14 +8510,14 @@ async function handleApi(req, res, url) {
|
|
|
7886
8510
|
* Workflow API endpoints
|
|
7887
8511
|
* ═══════════════════════════════════════════════════════════ */
|
|
7888
8512
|
|
|
7889
|
-
// Use module-scope getWorkflowEngineModule() for cross-request caching.
|
|
7890
|
-
const getWorkflowEngine = getWorkflowEngineModule;
|
|
7891
|
-
|
|
7892
8513
|
if (path === "/api/workflows") {
|
|
7893
8514
|
try {
|
|
7894
|
-
const
|
|
7895
|
-
if (!
|
|
7896
|
-
|
|
8515
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8516
|
+
if (!wfCtx.ok) {
|
|
8517
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8518
|
+
return;
|
|
8519
|
+
}
|
|
8520
|
+
const engine = wfCtx.engine;
|
|
7897
8521
|
const all = engine.list();
|
|
7898
8522
|
jsonResponse(res, 200, { ok: true, workflows: all.map(w => ({
|
|
7899
8523
|
id: w.id, name: w.name, description: w.description, category: w.category,
|
|
@@ -7910,9 +8534,12 @@ async function handleApi(req, res, url) {
|
|
|
7910
8534
|
if (path === "/api/workflows/save") {
|
|
7911
8535
|
try {
|
|
7912
8536
|
const body = await readJsonBody(req);
|
|
7913
|
-
const
|
|
7914
|
-
if (!
|
|
7915
|
-
|
|
8537
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8538
|
+
if (!wfCtx.ok) {
|
|
8539
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8540
|
+
return;
|
|
8541
|
+
}
|
|
8542
|
+
const engine = wfCtx.engine;
|
|
7916
8543
|
if (typeof _wfTemplates?.applyWorkflowTemplateState === "function") {
|
|
7917
8544
|
_wfTemplates.applyWorkflowTemplateState(body);
|
|
7918
8545
|
}
|
|
@@ -7926,8 +8553,11 @@ async function handleApi(req, res, url) {
|
|
|
7926
8553
|
|
|
7927
8554
|
if (path === "/api/workflows/templates") {
|
|
7928
8555
|
try {
|
|
7929
|
-
const
|
|
7930
|
-
if (!
|
|
8556
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8557
|
+
if (!wfCtx.ok) {
|
|
8558
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8559
|
+
return;
|
|
8560
|
+
}
|
|
7931
8561
|
const tplMod = _wfTemplates;
|
|
7932
8562
|
const list = tplMod.listTemplates();
|
|
7933
8563
|
jsonResponse(res, 200, { ok: true, templates: list });
|
|
@@ -7940,10 +8570,13 @@ async function handleApi(req, res, url) {
|
|
|
7940
8570
|
if (path === "/api/workflows/install-template") {
|
|
7941
8571
|
try {
|
|
7942
8572
|
const body = await readJsonBody(req);
|
|
7943
|
-
const
|
|
7944
|
-
if (!
|
|
8573
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8574
|
+
if (!wfCtx.ok) {
|
|
8575
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8576
|
+
return;
|
|
8577
|
+
}
|
|
7945
8578
|
const tplMod = _wfTemplates;
|
|
7946
|
-
const engine =
|
|
8579
|
+
const engine = wfCtx.engine;
|
|
7947
8580
|
const wf = await tplMod.installTemplate(body.templateId, engine, body.overrides);
|
|
7948
8581
|
jsonResponse(res, 200, { ok: true, workflow: wf });
|
|
7949
8582
|
} catch (err) {
|
|
@@ -7954,9 +8587,12 @@ async function handleApi(req, res, url) {
|
|
|
7954
8587
|
|
|
7955
8588
|
if (path === "/api/workflows/template-updates") {
|
|
7956
8589
|
try {
|
|
7957
|
-
const
|
|
7958
|
-
if (!
|
|
7959
|
-
|
|
8590
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8591
|
+
if (!wfCtx.ok) {
|
|
8592
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8593
|
+
return;
|
|
8594
|
+
}
|
|
8595
|
+
const engine = wfCtx.engine;
|
|
7960
8596
|
if (typeof _wfTemplates?.reconcileInstalledTemplates === "function") {
|
|
7961
8597
|
_wfTemplates.reconcileInstalledTemplates(engine, {
|
|
7962
8598
|
autoUpdateUnmodified: true,
|
|
@@ -7988,9 +8624,12 @@ async function handleApi(req, res, url) {
|
|
|
7988
8624
|
|
|
7989
8625
|
if (path.startsWith("/api/workflows/") && path.endsWith("/template-update")) {
|
|
7990
8626
|
try {
|
|
7991
|
-
const
|
|
7992
|
-
if (!
|
|
7993
|
-
|
|
8627
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8628
|
+
if (!wfCtx.ok) {
|
|
8629
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8630
|
+
return;
|
|
8631
|
+
}
|
|
8632
|
+
const engine = wfCtx.engine;
|
|
7994
8633
|
const workflowId = decodeURIComponent(path.split("/")[3] || "");
|
|
7995
8634
|
if (!workflowId) {
|
|
7996
8635
|
jsonResponse(res, 400, { ok: false, error: "Missing workflow id" });
|
|
@@ -8013,8 +8652,12 @@ async function handleApi(req, res, url) {
|
|
|
8013
8652
|
|
|
8014
8653
|
if (path === "/api/workflows/node-types") {
|
|
8015
8654
|
try {
|
|
8016
|
-
const
|
|
8017
|
-
if (!
|
|
8655
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8656
|
+
if (!wfCtx.ok) {
|
|
8657
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8658
|
+
return;
|
|
8659
|
+
}
|
|
8660
|
+
const wfMod = wfCtx.wfMod;
|
|
8018
8661
|
const types = wfMod.listNodeTypes();
|
|
8019
8662
|
jsonResponse(res, 200, { ok: true, nodeTypes: types.map(nt => ({
|
|
8020
8663
|
type: nt.type,
|
|
@@ -8030,9 +8673,12 @@ async function handleApi(req, res, url) {
|
|
|
8030
8673
|
|
|
8031
8674
|
if (path === "/api/workflows/runs") {
|
|
8032
8675
|
try {
|
|
8033
|
-
const
|
|
8034
|
-
if (!
|
|
8035
|
-
|
|
8676
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8677
|
+
if (!wfCtx.ok) {
|
|
8678
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8679
|
+
return;
|
|
8680
|
+
}
|
|
8681
|
+
const engine = wfCtx.engine;
|
|
8036
8682
|
const rawLimit = Number(url.searchParams.get("limit"));
|
|
8037
8683
|
const limit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
8038
8684
|
? Math.min(rawLimit, 500)
|
|
@@ -8047,9 +8693,12 @@ async function handleApi(req, res, url) {
|
|
|
8047
8693
|
|
|
8048
8694
|
if (path.startsWith("/api/workflows/runs/")) {
|
|
8049
8695
|
try {
|
|
8050
|
-
const
|
|
8051
|
-
if (!
|
|
8052
|
-
|
|
8696
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8697
|
+
if (!wfCtx.ok) {
|
|
8698
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8699
|
+
return;
|
|
8700
|
+
}
|
|
8701
|
+
const engine = wfCtx.engine;
|
|
8053
8702
|
const subPath = path.replace("/api/workflows/runs/", "");
|
|
8054
8703
|
const segments = subPath.split("/").map(decodeURIComponent);
|
|
8055
8704
|
const runId = (segments[0] || "").trim();
|
|
@@ -8131,9 +8780,12 @@ async function handleApi(req, res, url) {
|
|
|
8131
8780
|
const action = segments[1] || "";
|
|
8132
8781
|
|
|
8133
8782
|
try {
|
|
8134
|
-
const
|
|
8135
|
-
if (!
|
|
8136
|
-
|
|
8783
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8784
|
+
if (!wfCtx.ok) {
|
|
8785
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8786
|
+
return;
|
|
8787
|
+
}
|
|
8788
|
+
const engine = wfCtx.engine;
|
|
8137
8789
|
|
|
8138
8790
|
if (action === "execute" && req.method === "POST") {
|
|
8139
8791
|
const body = await readJsonBody(req);
|
|
@@ -9018,11 +9670,20 @@ async function handleApi(req, res, url) {
|
|
|
9018
9670
|
if (path === "/api/sessions" && req.method === "GET") {
|
|
9019
9671
|
try {
|
|
9020
9672
|
const tracker = getSessionTracker();
|
|
9673
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: true });
|
|
9674
|
+
if (!workspaceContext) {
|
|
9675
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
9676
|
+
return;
|
|
9677
|
+
}
|
|
9021
9678
|
let sessions = tracker.listAllSessions();
|
|
9022
9679
|
const typeFilter = url.searchParams.get("type");
|
|
9023
9680
|
const statusFilter = url.searchParams.get("status");
|
|
9024
9681
|
if (typeFilter) sessions = sessions.filter((s) => s.type === typeFilter);
|
|
9025
9682
|
if (statusFilter) sessions = sessions.filter((s) => s.status === statusFilter);
|
|
9683
|
+
sessions = sessions.filter((session) => {
|
|
9684
|
+
const detailed = tracker.getSessionById(session.id) || session;
|
|
9685
|
+
return sessionMatchesWorkspaceContext(detailed, workspaceContext);
|
|
9686
|
+
});
|
|
9026
9687
|
jsonResponse(res, 200, { ok: true, sessions });
|
|
9027
9688
|
} catch (err) {
|
|
9028
9689
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -9068,10 +9729,24 @@ async function handleApi(req, res, url) {
|
|
|
9068
9729
|
if (sessionMatch) {
|
|
9069
9730
|
const sessionId = decodeURIComponent(sessionMatch[1]);
|
|
9070
9731
|
const action = sessionMatch[2] || null;
|
|
9732
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
9733
|
+
if (!workspaceContext) {
|
|
9734
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
9735
|
+
return;
|
|
9736
|
+
}
|
|
9737
|
+
const tracker = getSessionTracker();
|
|
9738
|
+
const getScopedSession = () => {
|
|
9739
|
+
const session = tracker.getSessionById(sessionId);
|
|
9740
|
+
if (!session) return null;
|
|
9741
|
+
return sessionMatchesWorkspaceContext(session, workspaceContext) ? session : null;
|
|
9742
|
+
};
|
|
9071
9743
|
|
|
9072
9744
|
if (!action && req.method === "GET") {
|
|
9073
9745
|
try {
|
|
9074
|
-
|
|
9746
|
+
if (!getScopedSession()) {
|
|
9747
|
+
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9748
|
+
return;
|
|
9749
|
+
}
|
|
9075
9750
|
const session = tracker.getSessionMessages(sessionId);
|
|
9076
9751
|
if (!session) {
|
|
9077
9752
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
@@ -9112,8 +9787,7 @@ async function handleApi(req, res, url) {
|
|
|
9112
9787
|
|
|
9113
9788
|
if (action === "attachments" && req.method === "POST") {
|
|
9114
9789
|
try {
|
|
9115
|
-
const
|
|
9116
|
-
const session = tracker.getSessionById(sessionId);
|
|
9790
|
+
const session = getScopedSession();
|
|
9117
9791
|
if (!session) {
|
|
9118
9792
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9119
9793
|
return;
|
|
@@ -9165,8 +9839,7 @@ async function handleApi(req, res, url) {
|
|
|
9165
9839
|
|
|
9166
9840
|
if (action === "stop" && req.method === "POST") {
|
|
9167
9841
|
try {
|
|
9168
|
-
const
|
|
9169
|
-
const session = tracker.getSessionById(sessionId);
|
|
9842
|
+
const session = getScopedSession();
|
|
9170
9843
|
if (!session) {
|
|
9171
9844
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9172
9845
|
return;
|
|
@@ -9201,8 +9874,7 @@ async function handleApi(req, res, url) {
|
|
|
9201
9874
|
|
|
9202
9875
|
if (action === "message" && req.method === "POST") {
|
|
9203
9876
|
try {
|
|
9204
|
-
const
|
|
9205
|
-
const session = tracker.getSessionById(sessionId);
|
|
9877
|
+
const session = getScopedSession();
|
|
9206
9878
|
if (!session) {
|
|
9207
9879
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9208
9880
|
return;
|
|
@@ -9356,8 +10028,7 @@ async function handleApi(req, res, url) {
|
|
|
9356
10028
|
|
|
9357
10029
|
if (action === "message/edit" && req.method === "POST") {
|
|
9358
10030
|
try {
|
|
9359
|
-
const
|
|
9360
|
-
const session = tracker.getSessionById(sessionId);
|
|
10031
|
+
const session = getScopedSession();
|
|
9361
10032
|
if (!session) {
|
|
9362
10033
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9363
10034
|
return;
|
|
@@ -9394,8 +10065,7 @@ async function handleApi(req, res, url) {
|
|
|
9394
10065
|
|
|
9395
10066
|
if (action === "archive" && req.method === "POST") {
|
|
9396
10067
|
try {
|
|
9397
|
-
const
|
|
9398
|
-
const session = tracker.getSessionById(sessionId);
|
|
10068
|
+
const session = getScopedSession();
|
|
9399
10069
|
if (!session) {
|
|
9400
10070
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9401
10071
|
return;
|
|
@@ -9414,8 +10084,7 @@ async function handleApi(req, res, url) {
|
|
|
9414
10084
|
|
|
9415
10085
|
if (action === "resume" && req.method === "POST") {
|
|
9416
10086
|
try {
|
|
9417
|
-
const
|
|
9418
|
-
const session = tracker.getSessionById(sessionId);
|
|
10087
|
+
const session = getScopedSession();
|
|
9419
10088
|
if (!session) {
|
|
9420
10089
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9421
10090
|
return;
|
|
@@ -9431,8 +10100,7 @@ async function handleApi(req, res, url) {
|
|
|
9431
10100
|
|
|
9432
10101
|
if (action === "delete" && req.method === "POST") {
|
|
9433
10102
|
try {
|
|
9434
|
-
const
|
|
9435
|
-
const session = tracker.getSessionById(sessionId);
|
|
10103
|
+
const session = getScopedSession();
|
|
9436
10104
|
if (!session) {
|
|
9437
10105
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9438
10106
|
return;
|
|
@@ -9451,8 +10119,7 @@ async function handleApi(req, res, url) {
|
|
|
9451
10119
|
|
|
9452
10120
|
if (action === "rename" && req.method === "POST") {
|
|
9453
10121
|
try {
|
|
9454
|
-
const
|
|
9455
|
-
const session = tracker.getSessionById(sessionId);
|
|
10122
|
+
const session = getScopedSession();
|
|
9456
10123
|
if (!session) {
|
|
9457
10124
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9458
10125
|
return;
|
|
@@ -9474,8 +10141,7 @@ async function handleApi(req, res, url) {
|
|
|
9474
10141
|
|
|
9475
10142
|
if (action === "diff" && req.method === "GET") {
|
|
9476
10143
|
try {
|
|
9477
|
-
const
|
|
9478
|
-
const session = tracker.getSessionById(sessionId);
|
|
10144
|
+
const session = getScopedSession();
|
|
9479
10145
|
if (!session) {
|
|
9480
10146
|
jsonResponse(res, 200, {
|
|
9481
10147
|
ok: true,
|
|
@@ -9668,9 +10334,10 @@ async function handleApi(req, res, url) {
|
|
|
9668
10334
|
// Respect it as final and issue the probe directly.
|
|
9669
10335
|
testUrl = base;
|
|
9670
10336
|
} else {
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
10337
|
+
// Always use /openai/models — works on both classic Azure OpenAI and
|
|
10338
|
+
// Azure AI Foundry (Global Standard) where /openai/deployments/{name}
|
|
10339
|
+
// returns 404.
|
|
10340
|
+
testUrl = `${base}/openai/models?api-version=2024-10-21`;
|
|
9674
10341
|
}
|
|
9675
10342
|
if (apiKey) headers["api-key"] = apiKey;
|
|
9676
10343
|
else {
|
|
@@ -9728,8 +10395,37 @@ async function handleApi(req, res, url) {
|
|
|
9728
10395
|
clearTimeout(timer);
|
|
9729
10396
|
const latencyMs = Date.now() - start;
|
|
9730
10397
|
if (resp.ok || resp.status === 200) {
|
|
9731
|
-
//
|
|
9732
|
-
|
|
10398
|
+
// Credentials verified via /openai/models. If a deployment name was
|
|
10399
|
+
// provided, do a secondary check to confirm the deployment exists.
|
|
10400
|
+
if (provider === "azure" && deployment) {
|
|
10401
|
+
const dep = String(deployment).trim();
|
|
10402
|
+
try {
|
|
10403
|
+
let base2 = String(azureEndpoint || "").replace(/\/+$/, "");
|
|
10404
|
+
try { const u2 = new URL(base2); base2 = `${u2.protocol}//${u2.host}`; } catch { /* keep */ }
|
|
10405
|
+
const depUrl = `${base2}/openai/deployments/${encodeURIComponent(dep)}/chat/completions?api-version=2024-10-21`;
|
|
10406
|
+
const depCtrl = new AbortController();
|
|
10407
|
+
const depTimer = setTimeout(() => depCtrl.abort(), 8_000);
|
|
10408
|
+
const depResp = await fetch(depUrl, {
|
|
10409
|
+
method: "POST",
|
|
10410
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
10411
|
+
body: JSON.stringify({ messages: [{ role: "user", content: "test" }], max_tokens: 1 }),
|
|
10412
|
+
signal: depCtrl.signal,
|
|
10413
|
+
});
|
|
10414
|
+
clearTimeout(depTimer);
|
|
10415
|
+
// 200 = chat model works, 400 = realtime model (expected), both confirm deployment exists
|
|
10416
|
+
if (depResp.ok || depResp.status === 400) {
|
|
10417
|
+
jsonResponse(res, 200, { ok: true, latencyMs, deployment: dep });
|
|
10418
|
+
} else if (depResp.status === 404) {
|
|
10419
|
+
jsonResponse(res, 200, { ok: false, error: `Credentials valid but deployment "${dep}" not found — check the deployment name in Azure AI Foundry`, latencyMs });
|
|
10420
|
+
} else {
|
|
10421
|
+
jsonResponse(res, 200, { ok: true, latencyMs, warning: `Credentials valid. Could not verify deployment "${dep}" (HTTP ${depResp.status})` });
|
|
10422
|
+
}
|
|
10423
|
+
} catch {
|
|
10424
|
+
jsonResponse(res, 200, { ok: true, latencyMs, warning: `Credentials valid. Could not verify deployment "${dep}" (timeout)` });
|
|
10425
|
+
}
|
|
10426
|
+
} else {
|
|
10427
|
+
jsonResponse(res, 200, { ok: true, latencyMs });
|
|
10428
|
+
}
|
|
9733
10429
|
} else {
|
|
9734
10430
|
const text = await resp.text().catch(() => "");
|
|
9735
10431
|
let errMsg = `HTTP ${resp.status}`;
|
|
@@ -9738,9 +10434,13 @@ async function handleApi(req, res, url) {
|
|
|
9738
10434
|
const missing = String(errMsg || "").match(/Missing scopes?:\s*([A-Za-z0-9._:\s-]+)/i)?.[1]?.trim() || "required scopes";
|
|
9739
10435
|
errMsg = `OpenAI Connected Account token is missing scopes (${missing}). Sign out and reconnect OpenAI in Connected Accounts. Also verify role access: org Owner/Reader, project Owner/Member, and workspace RBAC API/dashboard permissions.`;
|
|
9740
10436
|
}
|
|
9741
|
-
//
|
|
9742
|
-
if (
|
|
9743
|
-
|
|
10437
|
+
// Azure-specific: provide helpful messages for common errors
|
|
10438
|
+
if (provider === "azure") {
|
|
10439
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
10440
|
+
errMsg = `Authentication failed (HTTP ${resp.status}) — check API key and endpoint URL`;
|
|
10441
|
+
} else if (resp.status === 404) {
|
|
10442
|
+
errMsg = `Endpoint not found (HTTP 404) — check the Azure endpoint URL. Use https://<resource>.openai.azure.com`;
|
|
10443
|
+
}
|
|
9744
10444
|
}
|
|
9745
10445
|
jsonResponse(res, 200, { ok: false, error: errMsg, latencyMs });
|
|
9746
10446
|
}
|
|
@@ -9967,10 +10667,33 @@ async function handleApi(req, res, url) {
|
|
|
9967
10667
|
return;
|
|
9968
10668
|
}
|
|
9969
10669
|
|
|
10670
|
+
// GET /api/voice/agents
|
|
10671
|
+
if (path === "/api/voice/agents" && req.method === "GET") {
|
|
10672
|
+
try {
|
|
10673
|
+
const callContext = {
|
|
10674
|
+
sessionId: String(url.searchParams.get("sessionId") || "").trim() || undefined,
|
|
10675
|
+
};
|
|
10676
|
+
const libraryRoot = resolveVoiceLibraryRoot(callContext);
|
|
10677
|
+
const { agents, selected } = resolveActiveVoiceAgent(
|
|
10678
|
+
libraryRoot,
|
|
10679
|
+
String(url.searchParams.get("voiceAgentId") || "").trim(),
|
|
10680
|
+
);
|
|
10681
|
+
jsonResponse(res, 200, {
|
|
10682
|
+
ok: true,
|
|
10683
|
+
agents,
|
|
10684
|
+
defaultAgentId: selected?.id || null,
|
|
10685
|
+
});
|
|
10686
|
+
} catch (err) {
|
|
10687
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
10688
|
+
}
|
|
10689
|
+
return;
|
|
10690
|
+
}
|
|
10691
|
+
|
|
9970
10692
|
// POST /api/voice/token
|
|
9971
10693
|
if (path === "/api/voice/token" && req.method === "POST") {
|
|
9972
10694
|
try {
|
|
9973
10695
|
const body = await readJsonBody(req).catch(() => ({}));
|
|
10696
|
+
const requestedVoiceAgentId = String(body?.voiceAgentId || "").trim();
|
|
9974
10697
|
const callContext = {
|
|
9975
10698
|
sessionId: String(body?.sessionId || "").trim() || undefined,
|
|
9976
10699
|
executor: String(body?.executor || "").trim() || undefined,
|
|
@@ -9978,31 +10701,56 @@ async function handleApi(req, res, url) {
|
|
|
9978
10701
|
model: String(body?.model || "").trim() || undefined,
|
|
9979
10702
|
authSource: String(authResult?.source || "").trim() || undefined,
|
|
9980
10703
|
};
|
|
10704
|
+
const libraryRoot = resolveVoiceLibraryRoot(callContext);
|
|
10705
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
10706
|
+
libraryRoot,
|
|
10707
|
+
requestedVoiceAgentId,
|
|
10708
|
+
);
|
|
10709
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
9981
10710
|
const { createEphemeralToken, getVoiceToolDefinitions, getVoiceConfig, isPrivilegedVoiceContext } = await import("./voice-relay.mjs");
|
|
9982
10711
|
const privileged = isPrivilegedVoiceContext(callContext);
|
|
9983
10712
|
const delegateOnly =
|
|
9984
10713
|
body?.delegateOnly === true && !privileged;
|
|
9985
10714
|
let tools = await getVoiceToolDefinitions({ delegateOnly, context: callContext });
|
|
9986
10715
|
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
tools = tools.filter(t => !disabled.includes(String(t?.name || "")));
|
|
9995
|
-
}
|
|
9996
|
-
}
|
|
9997
|
-
} catch { /* agent-tool-config not critical */ }
|
|
10716
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
10717
|
+
tools = applyVoiceAgentToolFilters(tools, voiceToolCfg);
|
|
10718
|
+
const capabilityPrompt = buildVoiceToolCapabilityPrompt(
|
|
10719
|
+
tools,
|
|
10720
|
+
voiceToolCfg,
|
|
10721
|
+
selectedVoiceAgent,
|
|
10722
|
+
);
|
|
9998
10723
|
|
|
9999
|
-
const
|
|
10724
|
+
const voiceCallContext = {
|
|
10725
|
+
...callContext,
|
|
10726
|
+
voiceAgentId: activeVoiceAgentId,
|
|
10727
|
+
voiceAgentName: selectedVoiceAgent?.name || undefined,
|
|
10728
|
+
voiceAgentInstructions: selectedVoiceAgent?.voiceInstructions || undefined,
|
|
10729
|
+
voiceAgentSkills: Array.isArray(selectedVoiceAgent?.skills) ? selectedVoiceAgent.skills : undefined,
|
|
10730
|
+
voiceToolCapabilityPrompt: capabilityPrompt,
|
|
10731
|
+
enabledMcpServers: Array.isArray(voiceToolCfg?.enabledMcpServers)
|
|
10732
|
+
? voiceToolCfg.enabledMcpServers
|
|
10733
|
+
: undefined,
|
|
10734
|
+
};
|
|
10735
|
+
const tokenData = await createEphemeralToken(tools, voiceCallContext);
|
|
10736
|
+
tokenData.voiceAgentId = activeVoiceAgentId;
|
|
10737
|
+
tokenData.voiceAgentName = selectedVoiceAgent?.name || null;
|
|
10738
|
+
tokenData.voiceAgentSkills = Array.isArray(selectedVoiceAgent?.skills) ? selectedVoiceAgent.skills : [];
|
|
10739
|
+
tokenData.enabledMcpServers = Array.isArray(voiceToolCfg?.enabledMcpServers)
|
|
10740
|
+
? voiceToolCfg.enabledMcpServers
|
|
10741
|
+
: [];
|
|
10742
|
+
tokenData.tools = Array.isArray(tools) ? tools : [];
|
|
10000
10743
|
|
|
10001
10744
|
// When client requests sdkMode, include extra fields for @openai/agents SDK
|
|
10002
10745
|
if (body?.sdkMode === true) {
|
|
10003
10746
|
const voiceCfg = getVoiceConfig();
|
|
10004
|
-
tokenData.instructions = voiceCfg.instructions ||
|
|
10005
|
-
|
|
10747
|
+
tokenData.instructions = [voiceCfg.instructions || "", capabilityPrompt]
|
|
10748
|
+
.filter(Boolean)
|
|
10749
|
+
.join("\n\n")
|
|
10750
|
+
.trim() || undefined;
|
|
10751
|
+
if (selectedVoiceAgent?.voiceInstructions) {
|
|
10752
|
+
tokenData.instructions = `${tokenData.instructions || ""}\n\n${selectedVoiceAgent.voiceInstructions}`.trim();
|
|
10753
|
+
}
|
|
10006
10754
|
if (tokenData.provider === "azure") {
|
|
10007
10755
|
tokenData.azureEndpoint = voiceCfg.azureEndpoint || undefined;
|
|
10008
10756
|
tokenData.azureDeployment = voiceCfg.azureDeployment || undefined;
|
|
@@ -10031,6 +10779,7 @@ async function handleApi(req, res, url) {
|
|
|
10031
10779
|
executor: String(body?.executor || "").trim() || undefined,
|
|
10032
10780
|
mode: String(body?.mode || "").trim() || undefined,
|
|
10033
10781
|
model: String(body?.model || "").trim() || undefined,
|
|
10782
|
+
voiceAgentId: String(body?.voiceAgentId || "").trim() || undefined,
|
|
10034
10783
|
};
|
|
10035
10784
|
const options = {
|
|
10036
10785
|
voiceId: String(body?.voiceId || "").trim() || undefined,
|
|
@@ -10049,6 +10798,7 @@ async function handleApi(req, res, url) {
|
|
|
10049
10798
|
if (path === "/api/agents/tools" && req.method === "GET") {
|
|
10050
10799
|
try {
|
|
10051
10800
|
const { getVoiceToolDefinitions, getAllowedVoiceTools } = await import("./voice-relay.mjs");
|
|
10801
|
+
const requestedVoiceAgentId = String(url.searchParams.get("voiceAgentId") || "").trim();
|
|
10052
10802
|
const context = {
|
|
10053
10803
|
sessionId: String(url.searchParams.get("sessionId") || "").trim() || undefined,
|
|
10054
10804
|
executor: String(url.searchParams.get("executor") || "").trim() || undefined,
|
|
@@ -10059,14 +10809,23 @@ async function handleApi(req, res, url) {
|
|
|
10059
10809
|
const allTools = await getVoiceToolDefinitions({ delegateOnly: false, context });
|
|
10060
10810
|
const allowed = await getAllowedVoiceTools(context);
|
|
10061
10811
|
const allowedTools = allowed instanceof Set ? allowed : null;
|
|
10062
|
-
|
|
10812
|
+
let tools = allowedTools
|
|
10063
10813
|
? (Array.isArray(allTools) ? allTools : []).filter((tool) => allowedTools.has(String(tool?.name || "").trim()))
|
|
10064
10814
|
: allTools;
|
|
10815
|
+
const libraryRoot = resolveVoiceLibraryRoot(context);
|
|
10816
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
10817
|
+
libraryRoot,
|
|
10818
|
+
requestedVoiceAgentId,
|
|
10819
|
+
);
|
|
10820
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
10821
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
10822
|
+
tools = applyVoiceAgentToolFilters(tools, voiceToolCfg);
|
|
10065
10823
|
jsonResponse(res, 200, {
|
|
10066
10824
|
ok: true,
|
|
10067
10825
|
tools: Array.isArray(tools) ? tools : [],
|
|
10068
10826
|
allowedTools: allowedTools ? Array.from(allowedTools.values()).sort() : null,
|
|
10069
10827
|
totalTools: Array.isArray(tools) ? tools.length : 0,
|
|
10828
|
+
voiceAgentId: activeVoiceAgentId,
|
|
10070
10829
|
});
|
|
10071
10830
|
} catch (err) {
|
|
10072
10831
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -10092,6 +10851,7 @@ async function handleApi(req, res, url) {
|
|
|
10092
10851
|
executor,
|
|
10093
10852
|
mode,
|
|
10094
10853
|
model,
|
|
10854
|
+
voiceAgentId,
|
|
10095
10855
|
} = body || {};
|
|
10096
10856
|
const normalizedToolName = String(toolName || "").trim();
|
|
10097
10857
|
if (!normalizedToolName) {
|
|
@@ -10129,8 +10889,16 @@ async function handleApi(req, res, url) {
|
|
|
10129
10889
|
mode: String(mode || "").trim() || undefined,
|
|
10130
10890
|
model: String(model || "").trim() || undefined,
|
|
10131
10891
|
authSource: String(authResult?.source || "").trim() || undefined,
|
|
10892
|
+
voiceAgentId: String(voiceAgentId || "").trim() || undefined,
|
|
10132
10893
|
};
|
|
10133
10894
|
const normalizedArgs = normalizeVoiceToolArgs(normalizedToolName, args || {});
|
|
10895
|
+
const libraryRoot = resolveVoiceLibraryRoot(context);
|
|
10896
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
10897
|
+
libraryRoot,
|
|
10898
|
+
context.voiceAgentId || "",
|
|
10899
|
+
);
|
|
10900
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
10901
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
10134
10902
|
let tracker = null;
|
|
10135
10903
|
let session = null;
|
|
10136
10904
|
if (context.sessionId) {
|
|
@@ -10171,6 +10939,37 @@ async function handleApi(req, res, url) {
|
|
|
10171
10939
|
return;
|
|
10172
10940
|
}
|
|
10173
10941
|
}
|
|
10942
|
+
const toolEnabledForAgent =
|
|
10943
|
+
applyVoiceAgentToolFilters([{ name: normalizedToolName }], voiceToolCfg).length > 0;
|
|
10944
|
+
if (!toolEnabledForAgent) {
|
|
10945
|
+
const deniedMessage = `Tool "${normalizedToolName}" is not enabled for voice agent "${activeVoiceAgentId}"`;
|
|
10946
|
+
if (isVoiceToolRoute) {
|
|
10947
|
+
jsonResponse(res, 403, { error: deniedMessage });
|
|
10948
|
+
} else {
|
|
10949
|
+
jsonResponse(res, 403, { ok: false, error: deniedMessage });
|
|
10950
|
+
}
|
|
10951
|
+
return;
|
|
10952
|
+
}
|
|
10953
|
+
if (
|
|
10954
|
+
normalizedToolName === "invoke_mcp_tool"
|
|
10955
|
+
&& Array.isArray(voiceToolCfg?.enabledMcpServers)
|
|
10956
|
+
&& voiceToolCfg.enabledMcpServers.length > 0
|
|
10957
|
+
) {
|
|
10958
|
+
const requestedServer = String(
|
|
10959
|
+
normalizedArgs?.server
|
|
10960
|
+
|| normalizedArgs?.serverId
|
|
10961
|
+
|| "",
|
|
10962
|
+
).trim();
|
|
10963
|
+
if (requestedServer && !voiceToolCfg.enabledMcpServers.includes(requestedServer)) {
|
|
10964
|
+
const deniedMessage = `MCP server "${requestedServer}" is not enabled for voice agent "${activeVoiceAgentId}"`;
|
|
10965
|
+
if (isVoiceToolRoute) {
|
|
10966
|
+
jsonResponse(res, 403, { error: deniedMessage });
|
|
10967
|
+
} else {
|
|
10968
|
+
jsonResponse(res, 403, { ok: false, error: deniedMessage });
|
|
10969
|
+
}
|
|
10970
|
+
return;
|
|
10971
|
+
}
|
|
10972
|
+
}
|
|
10174
10973
|
const result = await executeVoiceTool(normalizedToolName, normalizedArgs, context);
|
|
10175
10974
|
if (tracker && context.sessionId) {
|
|
10176
10975
|
if (result?.error) {
|
|
@@ -10905,6 +11704,7 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
10905
11704
|
executor,
|
|
10906
11705
|
mode,
|
|
10907
11706
|
model,
|
|
11707
|
+
voiceAgentId,
|
|
10908
11708
|
} = message;
|
|
10909
11709
|
const normalizedToolName = String(toolName || "").trim();
|
|
10910
11710
|
if (!normalizedToolName) {
|
|
@@ -10926,6 +11726,7 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
10926
11726
|
mode: String(mode || "").trim() || undefined,
|
|
10927
11727
|
model: String(model || "").trim() || undefined,
|
|
10928
11728
|
authSource: String(socket.__authSource || "").trim() || undefined,
|
|
11729
|
+
voiceAgentId: String(voiceAgentId || "").trim() || undefined,
|
|
10929
11730
|
};
|
|
10930
11731
|
const normalizedArgs = relay.normalizeVoiceToolArgs(normalizedToolName, args || {});
|
|
10931
11732
|
relay.getAllowedVoiceTools(context).then((allowed) => {
|
|
@@ -10938,6 +11739,44 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
10938
11739
|
});
|
|
10939
11740
|
return;
|
|
10940
11741
|
}
|
|
11742
|
+
const libraryRoot = resolveVoiceLibraryRoot(context);
|
|
11743
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
11744
|
+
libraryRoot,
|
|
11745
|
+
context.voiceAgentId || "",
|
|
11746
|
+
);
|
|
11747
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
11748
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
11749
|
+
const toolEnabledForAgent =
|
|
11750
|
+
applyVoiceAgentToolFilters([{ name: normalizedToolName }], voiceToolCfg).length > 0;
|
|
11751
|
+
if (!toolEnabledForAgent) {
|
|
11752
|
+
sendWsMessage(socket, {
|
|
11753
|
+
type: "voice-tool-result",
|
|
11754
|
+
callId,
|
|
11755
|
+
error: `Tool "${normalizedToolName}" is not enabled for voice agent "${activeVoiceAgentId}"`,
|
|
11756
|
+
ts: Date.now(),
|
|
11757
|
+
});
|
|
11758
|
+
return;
|
|
11759
|
+
}
|
|
11760
|
+
if (
|
|
11761
|
+
normalizedToolName === "invoke_mcp_tool"
|
|
11762
|
+
&& Array.isArray(voiceToolCfg?.enabledMcpServers)
|
|
11763
|
+
&& voiceToolCfg.enabledMcpServers.length > 0
|
|
11764
|
+
) {
|
|
11765
|
+
const requestedServer = String(
|
|
11766
|
+
normalizedArgs?.server
|
|
11767
|
+
|| normalizedArgs?.serverId
|
|
11768
|
+
|| "",
|
|
11769
|
+
).trim();
|
|
11770
|
+
if (requestedServer && !voiceToolCfg.enabledMcpServers.includes(requestedServer)) {
|
|
11771
|
+
sendWsMessage(socket, {
|
|
11772
|
+
type: "voice-tool-result",
|
|
11773
|
+
callId,
|
|
11774
|
+
error: `MCP server "${requestedServer}" is not enabled for voice agent "${activeVoiceAgentId}"`,
|
|
11775
|
+
ts: Date.now(),
|
|
11776
|
+
});
|
|
11777
|
+
return;
|
|
11778
|
+
}
|
|
11779
|
+
}
|
|
10941
11780
|
// Tool is allowed — execute it
|
|
10942
11781
|
relay.executeVoiceTool(normalizedToolName, normalizedArgs, context).then((result) => {
|
|
10943
11782
|
sendWsMessage(socket, {
|
|
@@ -10980,8 +11819,27 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
10980
11819
|
mode: String(mode || "").trim() || undefined,
|
|
10981
11820
|
model: String(model || "").trim() || undefined,
|
|
10982
11821
|
authSource: String(socket.__authSource || "").trim() || undefined,
|
|
11822
|
+
voiceAgentId: String(voiceAgentId || "").trim() || undefined,
|
|
10983
11823
|
};
|
|
10984
11824
|
const normalizedArgs = relay.normalizeVoiceToolArgs(normalizedToolName, args || {});
|
|
11825
|
+
const libraryRoot = resolveVoiceLibraryRoot(context);
|
|
11826
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
11827
|
+
libraryRoot,
|
|
11828
|
+
context.voiceAgentId || "",
|
|
11829
|
+
);
|
|
11830
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
11831
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
11832
|
+
const toolEnabledForAgent =
|
|
11833
|
+
applyVoiceAgentToolFilters([{ name: normalizedToolName }], voiceToolCfg).length > 0;
|
|
11834
|
+
if (!toolEnabledForAgent) {
|
|
11835
|
+
sendWsMessage(socket, {
|
|
11836
|
+
type: "voice-tool-result",
|
|
11837
|
+
callId,
|
|
11838
|
+
error: `Tool "${normalizedToolName}" is not enabled for voice agent "${activeVoiceAgentId}"`,
|
|
11839
|
+
ts: Date.now(),
|
|
11840
|
+
});
|
|
11841
|
+
return;
|
|
11842
|
+
}
|
|
10985
11843
|
const result = await relay.executeVoiceTool(normalizedToolName, normalizedArgs, context);
|
|
10986
11844
|
sendWsMessage(socket, {
|
|
10987
11845
|
type: "voice-tool-result",
|