Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 104 additions & 2 deletions agent/agent-tool-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@
* setAgentToolConfig(rootDir, agentId, config) — update config for one agent
* getEffectiveTools(rootDir, agentId) — compute final enabled tools list
* listAvailableTools(rootDir) — list all available tools (builtin + MCP)
* measureToolDefinitionChars(toolDefs) — char counts for serialized tool defs
* getToolOverheadReport(rootDir, agentId) — persisted/runtime overhead summary
*/

import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { resolve } from "node:path";
import { basename, resolve } from "node:path";
import { homedir } from "node:os";

// ── Constants ─────────────────────────────────────────────────────────────────
Expand All @@ -51,6 +53,36 @@ function getBosunHome() {
* Default built-in tools available to all voice agents and executors.
* Maps to common capabilities that voice/agent sessions can invoke.
*/
function measureToolDefinitionCharsInternal(toolDefs = []) {
const tools = Array.isArray(toolDefs)
? toolDefs.map((toolDef, index) => {
const serialized = JSON.stringify(toolDef ?? null);
const fallbackId = `tool-${index + 1}`;
return {
id: String(toolDef?.id || toolDef?.name || toolDef?.tool_name || fallbackId),
chars: serialized.length,
};
})
: [];

return {
total: tools.reduce((sum, tool) => sum + tool.chars, 0),
tools,
};
}

export function measureToolDefinitionChars(toolDefs = []) {
return measureToolDefinitionCharsInternal(toolDefs);
}

function normalizeSourceCharMap(bySource = {}) {
return Object.fromEntries(
Object.entries(bySource)
.map(([source, chars]) => [String(source), Math.max(0, Number(chars) || 0)])
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])),
);
}

export const DEFAULT_BUILTIN_TOOLS = Object.freeze([
{
id: "search-files",
Expand Down Expand Up @@ -177,7 +209,11 @@ export const DEFAULT_BUILTIN_TOOLS = Object.freeze([
// ── Config File I/O ───────────────────────────────────────────────────────────

function getConfigPath(rootDir) {
return resolve(rootDir || getBosunHome(), ".bosun", CONFIG_FILE);
const baseDir = rootDir || getBosunHome();
const configDir = basename(resolve(baseDir)) === ".bosun"
? resolve(baseDir)
: resolve(baseDir, ".bosun");
return resolve(configDir, CONFIG_FILE);
}

/**
Expand All @@ -194,6 +230,7 @@ export function loadToolConfig(rootDir) {
builtinTools: DEFAULT_BUILTIN_TOOLS.filter((t) => t.default).map((t) => t.id),
updatedAt: new Date().toISOString(),
},
toolOverhead: {},
};
}
try {
Expand All @@ -205,6 +242,7 @@ export function loadToolConfig(rootDir) {
builtinTools: DEFAULT_BUILTIN_TOOLS.filter((t) => t.default).map((t) => t.id),
updatedAt: new Date().toISOString(),
},
toolOverhead: parsed.toolOverhead || {},
};
} catch {
return {
Expand All @@ -213,6 +251,7 @@ export function loadToolConfig(rootDir) {
builtinTools: DEFAULT_BUILTIN_TOOLS.filter((t) => t.default).map((t) => t.id),
updatedAt: new Date().toISOString(),
},
toolOverhead: {},
};
}
}
Expand Down Expand Up @@ -336,3 +375,66 @@ export async function listAvailableTools(rootDir) {
})),
};
}

function persistToolOverheadReport(rootDir, agentId, report) {
if (!rootDir || !agentId || !report) return report;
const cfg = loadToolConfig(rootDir);
cfg.toolOverhead ||= {};
cfg.toolOverhead[agentId] = {
total: Math.max(0, Number(report.total) || 0),
bySource: normalizeSourceCharMap(report.bySource),
updatedAt: new Date().toISOString(),
};
saveToolConfig(rootDir, cfg);
return cfg.toolOverhead[agentId];
}

export function getToolOverheadReport(rootDir, agentId) {
const cfg = loadToolConfig(rootDir);
const stored = cfg.toolOverhead?.[agentId] || null;
if (!stored) return { total: 0, bySource: {} };
return {
total: Math.max(0, Number(stored.total) || 0),
bySource: normalizeSourceCharMap(stored.bySource),
};
}

export async function refreshToolOverheadReport(rootDir, agentId, options = {}) {
const builtinMeasurement = measureToolDefinitionChars(DEFAULT_BUILTIN_TOOLS);
const report = {
total: builtinMeasurement.total,
bySource: { builtin: builtinMeasurement.total },
};

const agentConfig = agentId ? getAgentToolConfig(rootDir, agentId) : { enabledMcpServers: [] };
const enabledServerIds = Array.isArray(options.serverIds)
? options.serverIds
: Array.isArray(agentConfig.enabledMcpServers)
? agentConfig.enabledMcpServers
: [];

if (enabledServerIds.length > 0) {
try {
const { resolveMcpServersForAgent } = await import("../workflow/mcp-registry.mjs");
const servers = await resolveMcpServersForAgent(rootDir, enabledServerIds);
for (const server of servers) {
const serverDefs = Array.isArray(server?.tools) ? server.tools : [];
const measurement = measureToolDefinitionChars(serverDefs);
report.bySource[server.id || server.name || "unknown-mcp"] = measurement.total;
report.total += measurement.total;
}
} catch {
for (const serverId of enabledServerIds) report.bySource[serverId] ||= 0;
}
}

const persisted = persistToolOverheadReport(rootDir, agentId, report);
if (process.env.BOSUN_LOG_TOOL_OVERHEAD === "1") {
console.log(TAG + " tool definition overhead for " + (agentId || "agent") + ":");
for (const [source, chars] of Object.entries(persisted.bySource)) {
console.log(TAG + " " + source + ": " + chars + " chars");
}
console.log(TAG + " total: " + persisted.total + " chars");
}
return persisted;
}
9 changes: 7 additions & 2 deletions agent/primary-agent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ensureCodexConfig, printConfigSummary } from "../shell/codex-config.mjs
import { ensureRepoConfigs, printRepoConfigSummary } from "../config/repo-config.mjs";
import { resolveRepoRoot } from "../config/repo-root.mjs";
import { buildArchitectEditorFrame } from "../lib/repo-map.mjs";
import { getAgentToolConfig, getEffectiveTools } from "./agent-tool-config.mjs";
import { getAgentToolConfig, getEffectiveTools, refreshToolOverheadReport } from "./agent-tool-config.mjs";
import { getSessionTracker } from "../infra/session-tracker.mjs";
import { buildContextEnvelope } from "../workspace/context-cache.mjs";
import { getEntry, getEntryContent, resolveAgentProfileLibraryMetadata } from "../infra/library-manager.mjs";
Expand Down Expand Up @@ -234,6 +234,12 @@ function buildPrimaryToolCapabilityContract(options = {}) {
const enabledMcpServers = Array.isArray(rawCfg?.enabledMcpServers)
? rawCfg.enabledMcpServers.map((id) => String(id || "").trim()).filter(Boolean)
: [];
if (agentProfileId) {
void refreshToolOverheadReport(rootDir, agentProfileId, { serverIds: enabledMcpServers })
.catch((error) => {
console.warn("[primary-agent] failed to refresh tool overhead report:", error?.message || error);
});
}
const manifest = {
agentProfileId: agentProfileId || null,
enabledBuiltinTools,
Expand Down Expand Up @@ -1532,4 +1538,3 @@ export async function execSdkCommand(command, args = "", adapterName, options =




15 changes: 14 additions & 1 deletion cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,8 @@ async function main() {
console.error(` Error: ${err.message}`);
process.exit(1);
}


process.exit(0);
}

Expand Down Expand Up @@ -2176,9 +2178,12 @@ async function main() {
// Handle --workspace-status
if (args.includes("--workspace-status") || args.includes("workspace-status")) {
const { getWorkspaceStateSummary } = await import("./workspace/workspace-manager.mjs");
const { getToolOverheadReport } = await import("./agent/agent-tool-config.mjs");
const configDirArg = getArgValue("--config-dir");
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
const summary = getWorkspaceStateSummary(configDir);
const toolOverhead = getToolOverheadReport(configDir, "primary");
const overheadSources = Object.entries(toolOverhead.bySource || {});
if (summary.length === 0) {
console.log("\n No workspaces configured.\n");
} else {
Expand All @@ -2196,8 +2201,16 @@ async function main() {
console.log(` enabled workflows: ${ws.enabledWorkflows.join(", ")}`);
}
}
console.log("");
}
if (toolOverhead.total > 0 || overheadSources.length > 0) {
console.log(" Tool Overhead:");
console.log(` Total tool chars: ${toolOverhead.total.toLocaleString("en-US")}`);
for (const [source, chars] of overheadSources) {
const warning = Number(chars) > 10000 ? " WARNING: high overhead" : "";
console.log(` ${source}: ${Number(chars).toLocaleString("en-US")} chars${warning}`);
}
}
console.log("");
process.exit(0);
}

Expand Down
4 changes: 2 additions & 2 deletions site/ui/demo-defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -2000,7 +2000,7 @@
"label": "Pick Conflict PR",
"config": {
"key": "targetPrNumber",
"value": "/* <!-- bosun-created --> */ (() => { const raw = $ctx.getNodeOutput('list-prs')?.output || '[]'; let prs = []; try { prs = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { return ''; } if (!Array.isArray(prs)) return ''; const CONFLICT = new Set(['CONFLICTING', 'BEHIND', 'DIRTY']); const BOSUN_CREATED_LABEL = 'bosun-pr-bosun-created'; const readLabelNames = (pr) => Array.isArray(pr?.labels) ? pr.labels.map((entry) => typeof entry === 'string' ? entry : entry?.name).filter(Boolean) : []; const isBosunCreated = (pr) => readLabelNames(pr).includes(BOSUN_CREATED_LABEL); /* Skip PRs already owned by the watchdog fix agent */ const pr = prs.find((p) => isBosunCreated(p) && CONFLICT.has(String(p?.mergeable || '').toUpperCase()) && !(p.labels || []).some((l) => l.name === 'bosun-needs-fix') ); return pr?.number ? String(pr.number) : '';})()",
"value": "/* <!-- bosun-created --> */ (() => { const raw = $ctx.getNodeOutput('list-prs')?.output || '[]'; let prs = []; try { prs = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { return ''; } if (!Array.isArray(prs)) return ''; const CONFLICT = new Set(['CONFLICTING', 'BEHIND', 'DIRTY']); const BOSUN_CREATED_LABEL = 'bosun-pr-bosun-created'; const readLabelNames = (pr) => Array.isArray(pr?.labels) ? pr.labels.map((entry) => typeof entry === 'string' ? entry : entry?.name).filter(Boolean) : []; const hasBosunCreatedText = (value) => { const text = String(value || ''); const taskIdMatch = text.match(/(?:Bosun-Task|VE-Task|Task-ID|task[_-]?id)[:\\s]+([a-zA-Z0-9_-]{4,64})/i); const hasLegacyTaskSignature = Boolean(taskIdMatch && text.toLowerCase().includes(`automated pr for task ${String(taskIdMatch[1] || '').trim().toLowerCase()}`)); return text.includes('<!-- bosun-created -->') || /Bosun-Origin:\\s*created/i.test(text) || /auto-created by bosun/i.test(text) || hasLegacyTaskSignature; }; const isBosunCreated = (pr) => readLabelNames(pr).includes(BOSUN_CREATED_LABEL) || hasBosunCreatedText(pr?.body); /* Skip PRs already owned by the watchdog fix agent */ const pr = prs.find((p) => isBosunCreated(p) && CONFLICT.has(String(p?.mergeable || '').toUpperCase()) && !(p.labels || []).some((l) => l.name === 'bosun-needs-fix') ); return pr?.number ? String(pr.number) : '';})()",
"isExpression": true
},
"position": {
Expand Down Expand Up @@ -26609,7 +26609,7 @@
"label": "Pick Conflict PR",
"config": {
"key": "targetPrNumber",
"value": "/* <!-- bosun-created --> */ (() => { const raw = $ctx.getNodeOutput('list-prs')?.output || '[]'; let prs = []; try { prs = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { return ''; } if (!Array.isArray(prs)) return ''; const CONFLICT = new Set(['CONFLICTING', 'BEHIND', 'DIRTY']); const BOSUN_CREATED_LABEL = 'bosun-pr-bosun-created'; const readLabelNames = (pr) => Array.isArray(pr?.labels) ? pr.labels.map((entry) => typeof entry === 'string' ? entry : entry?.name).filter(Boolean) : []; const isBosunCreated = (pr) => readLabelNames(pr).includes(BOSUN_CREATED_LABEL); /* Skip PRs already owned by the watchdog fix agent */ const pr = prs.find((p) => isBosunCreated(p) && CONFLICT.has(String(p?.mergeable || '').toUpperCase()) && !(p.labels || []).some((l) => l.name === 'bosun-needs-fix') ); return pr?.number ? String(pr.number) : '';})()",
"value": "/* <!-- bosun-created --> */ (() => { const raw = $ctx.getNodeOutput('list-prs')?.output || '[]'; let prs = []; try { prs = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { return ''; } if (!Array.isArray(prs)) return ''; const CONFLICT = new Set(['CONFLICTING', 'BEHIND', 'DIRTY']); const BOSUN_CREATED_LABEL = 'bosun-pr-bosun-created'; const readLabelNames = (pr) => Array.isArray(pr?.labels) ? pr.labels.map((entry) => typeof entry === 'string' ? entry : entry?.name).filter(Boolean) : []; const hasBosunCreatedText = (value) => { const text = String(value || ''); const taskIdMatch = text.match(/(?:Bosun-Task|VE-Task|Task-ID|task[_-]?id)[:\\s]+([a-zA-Z0-9_-]{4,64})/i); const hasLegacyTaskSignature = Boolean(taskIdMatch && text.toLowerCase().includes(`automated pr for task ${String(taskIdMatch[1] || '').trim().toLowerCase()}`)); return text.includes('<!-- bosun-created -->') || /Bosun-Origin:\\s*created/i.test(text) || /auto-created by bosun/i.test(text) || hasLegacyTaskSignature; }; const isBosunCreated = (pr) => readLabelNames(pr).includes(BOSUN_CREATED_LABEL) || hasBosunCreatedText(pr?.body); /* Skip PRs already owned by the watchdog fix agent */ const pr = prs.find((p) => isBosunCreated(p) && CONFLICT.has(String(p?.mergeable || '').toUpperCase()) && !(p.labels || []).some((l) => l.name === 'bosun-needs-fix') ); return pr?.number ? String(pr.number) : '';})()",
"isExpression": true
},
"position": {
Expand Down
67 changes: 67 additions & 0 deletions tests/agent-tool-config.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { mkdtempSync, existsSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

import {
DEFAULT_BUILTIN_TOOLS,
getToolOverheadReport,
measureToolDefinitionChars,
saveToolConfig,
} from "../agent/agent-tool-config.mjs";

describe("agent tool overhead", () => {
it("measures serialized chars per tool and total", () => {
const toolDefs = [
{ id: "alpha", name: "Alpha", description: "First tool" },
{ id: "beta", schema: { type: "object", properties: { q: { type: "string" } } } },
];

const report = measureToolDefinitionChars(toolDefs);

expect(report.total).toBe(toolDefs.reduce((sum, tool) => sum + JSON.stringify(tool).length, 0));
expect(report.tools).toEqual([
{ id: "alpha", chars: JSON.stringify(toolDefs[0]).length },
{ id: "beta", chars: JSON.stringify(toolDefs[1]).length },
]);
});

it("supports builtin tool definitions", () => {
const report = measureToolDefinitionChars(DEFAULT_BUILTIN_TOOLS);

expect(report.total).toBeGreaterThan(0);
expect(report.tools).toHaveLength(DEFAULT_BUILTIN_TOOLS.length);
expect(report.tools[0]).toHaveProperty("chars");
});

it("persists overhead reports when passed an explicit .bosun config dir", () => {
const rootDir = mkdtempSync(resolve(tmpdir(), "bosun-agent-tools-"));
const bosunDir = resolve(rootDir, ".bosun");

try {
saveToolConfig(bosunDir, {
agents: {},
defaults: {
builtinTools: ["search-files"],
updatedAt: "2026-03-25T00:00:00.000Z",
},
toolOverhead: {
primary: {
total: 23456,
bySource: { builtin: 3456, "huge-server": 20000 },
updatedAt: "2026-03-25T00:00:00.000Z",
},
},
});

expect(existsSync(resolve(bosunDir, "agent-tools.json"))).toBe(true);
expect(existsSync(resolve(bosunDir, ".bosun", "agent-tools.json"))).toBe(false);
expect(getToolOverheadReport(bosunDir, "primary")).toEqual({
total: 23456,
bySource: { builtin: 3456, "huge-server": 20000 },
});
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
});
51 changes: 51 additions & 0 deletions tests/cli-tool-overhead-status.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

describe("cli tool overhead status", () => {
it("shows tool overhead totals and warnings in workspace status", () => {
const rootDir = mkdtempSync(resolve(tmpdir(), "bosun-tool-overhead-"));
const bosunDir = resolve(rootDir, ".bosun");
mkdirSync(bosunDir, { recursive: true });
writeFileSync(resolve(bosunDir, "bosun.config.json"), JSON.stringify({ workspaces: [] }), "utf8");
writeFileSync(
resolve(bosunDir, "agent-tools.json"),
JSON.stringify({
agents: {
primary: {
enabledMcpServers: ["huge-server"],
updatedAt: "2026-03-25T00:00:00.000Z",
},
},
defaults: { builtinTools: ["search-files"], updatedAt: "2026-03-25T00:00:00.000Z" },
toolOverhead: {
primary: {
total: 23456,
bySource: {
builtin: 3456,
"huge-server": 20000,
},
},
},
}),
"utf8",
);

try {
const output = execFileSync(process.execPath, ["cli.mjs", "--workspace-status", "--config-dir", bosunDir], {
cwd: process.cwd(),
encoding: "utf8",
});

expect(output).toContain("Tool Overhead:");
expect(output).toContain("Total tool chars: 23,456");
expect(output).toContain("builtin: 3,456 chars");
expect(output).toContain("huge-server: 20,000 chars");
expect(output).toContain("WARNING");
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
});
Loading
Loading