feat(FN-4400): complete Step 7 — add prompt-size api and dashboard sparkline

Fusion-Task-Id: FN-4400
Fusion-Task-Lineage: 0d4f9c48-5b8b-49eb-9cf3-d1d585ffe7fc
This commit is contained in:
Fusion
2026-05-14 03:59:36 -07:00
committed by gsxdsm
parent 1b99648acc
commit 6b2607dae5
8 changed files with 332 additions and 9 deletions

View File

@@ -4714,6 +4714,14 @@ import type {
} from "@fusion/core";
export type { Agent, AgentDetail, AgentCapability, AgentState, AgentHeartbeatEvent, AgentHeartbeatRun, AgentCreateInput, AgentUpdateInput, AgentTaskSession, AgentStats, HeartbeatInvocationSource, OrgTreeNode, AgentReflection, AgentPerformanceSummary, ReflectionTrigger, AgentBudgetStatus };
export interface AgentPromptSizePoint {
runId: string;
createdAt: string;
systemChars: number;
execChars: number;
totalChars: number;
}
function withProjectId(path: string, projectId?: string): string {
if (!projectId) return path;
const separator = path.includes("?") ? "&" : "?";
@@ -4928,6 +4936,15 @@ export function fetchAgentRunLogs(agentId: string, runId: string, projectId?: st
return api<AgentLogEntry[]>(withProjectId(`/agents/${encodeURIComponent(agentId)}/runs/${encodeURIComponent(runId)}/logs`, projectId));
}
/** Fetch recent prompt size points for an agent */
export function fetchAgentPromptSizes(agentId: string, limit?: number, projectId?: string): Promise<AgentPromptSizePoint[]> {
const params = new URLSearchParams();
if (limit !== undefined) params.set("limit", String(limit));
if (projectId) params.set("projectId", projectId);
const query = params.size > 0 ? `?${params.toString()}` : "";
return api<AgentPromptSizePoint[]>(`/agents/${encodeURIComponent(agentId)}/prompt-sizes${query}`);
}
/** Manually start a heartbeat run for an agent */
export function startAgentRun(
agentId: string,

View File

@@ -892,6 +892,38 @@
font-size: calc(var(--space-sm) + var(--space-xs));
}
.prompt-size-summary {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.prompt-size-sparkline {
width: calc(var(--space-2xl) * 2);
height: calc(var(--space-xl) + var(--space-md));
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
}
.prompt-size-sparkline-grid {
fill: none;
stroke: var(--text-muted);
stroke-width: 1;
}
.prompt-size-sparkline-line {
fill: none;
stroke: var(--accent);
stroke-width: 2;
}
.prompt-size-values {
font-family: var(--font-mono);
font-size: calc(var(--space-sm) + var(--space-xs));
color: var(--text-muted);
}
.run-agent-logs-section {
margin-top: var(--space-xs);
padding-top: var(--space-sm);

View File

@@ -11,8 +11,8 @@ import {
} from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { AgentDetail, AgentState, AgentHeartbeatRun, AgentBudgetStatus, ModelInfo, MemoryFileInfo, AgentCapability, PluginRuntimeInfo, SkillContent, AgentOnboardingSummary, AgentMailboxResponse } from "../api";
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogsWithMeta, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentMemoryFiles, fetchAgentMemoryFile, saveAgentMemoryFile, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchModels, fetchPluginRuntimes, fetchAgents, upgradeAgentHeartbeatProcedure, updateGlobalSettings, fetchSkillContent, uploadAgentAvatar, deleteAgentAvatar, fetchAgentMailbox, markMessageRead } from "../api";
import type { AgentDetail, AgentState, AgentHeartbeatRun, AgentBudgetStatus, ModelInfo, MemoryFileInfo, AgentCapability, PluginRuntimeInfo, SkillContent, AgentOnboardingSummary, AgentMailboxResponse, AgentPromptSizePoint } from "../api";
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogsWithMeta, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentMemoryFiles, fetchAgentMemoryFile, saveAgentMemoryFile, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchModels, fetchPluginRuntimes, fetchAgents, upgradeAgentHeartbeatProcedure, updateGlobalSettings, fetchSkillContent, uploadAgentAvatar, deleteAgentAvatar, fetchAgentMailbox, markMessageRead, fetchAgentPromptSizes } from "../api";
import type { Agent } from "../api";
import type { AgentLogEntry, Task, Message, ParticipantType } from "@fusion/core";
import { getErrorMessage, isEphemeralAgent } from "@fusion/core";
@@ -1684,6 +1684,7 @@ function RunsTab({
const [detailRun, setDetailRun] = useState<AgentHeartbeatRun | null>(null);
const [isLoadingDetail, setIsLoadingDetail] = useState(false);
const [tokenUsageSummary, setTokenUsageSummary] = useState<AgentTokenUsageSummary | null>(null);
const [promptSizes, setPromptSizes] = useState<AgentPromptSizePoint[]>([]);
const hasAutoExpandedInitialRunRef = useRef(false);
const didMountRunNowRefreshRef = useRef(false);
@@ -1706,12 +1707,12 @@ function RunsTab({
useEffect(() => {
if (isEphemeral) {
setTokenUsageSummary(null);
setPromptSizes([]);
return;
}
const controller = new AbortController();
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
void fetch(`/api/agents/${encodeURIComponent(agentId)}/token-usage${query}`, { signal: controller.signal })
void fetch(`/api/agents/${encodeURIComponent(agentId)}/token-usage${query}`)
.then(async (res) => {
if (!res.ok) {
if (res.status === 400) {
@@ -1724,13 +1725,19 @@ function RunsTab({
setTokenUsageSummary(data);
})
.catch((err) => {
if (err instanceof Error && err.name === "AbortError") {
return;
}
addToast(`Failed to load cache hit ratio: ${getErrorMessage(err)}`, "error");
});
return () => controller.abort();
void fetchAgentPromptSizes(agentId, 7, projectId)
.then((data) => setPromptSizes(data))
.catch((err) => {
const message = getErrorMessage(err).toLowerCase();
if (message.includes("ephemeral") || message.includes("400")) {
setPromptSizes([]);
return;
}
addToast(`Failed to load prompt sizes: ${getErrorMessage(err)}`, "error");
});
}, [agentId, projectId, addToast, isEphemeral]);
useEffect(() => {
@@ -2100,8 +2107,33 @@ function RunsTab({
</div>
);
const latestPrompt = promptSizes[0];
const promptPoints = [...promptSizes].reverse();
const maxExecChars = Math.max(1, ...promptPoints.map((point) => point.execChars));
const promptPolyline = promptPoints
.map((point, index) => {
const x = promptPoints.length <= 1 ? 0 : (index / (promptPoints.length - 1)) * 100;
const y = 100 - Math.round((point.execChars / maxExecChars) * 100);
return `${x},${y}`;
})
.join(" ");
return (
<div className="runs-tab">
{promptSizes.length > 0 && latestPrompt && (
<div className="run-output-section">
<div className="run-output-label">Prompt Size</div>
<div className="prompt-size-summary">
<svg className="prompt-size-sparkline" viewBox="0 0 100 100" role="img" aria-label="Execution prompt size over last 7 runs">
<polyline className="prompt-size-sparkline-grid" points="0,100 100,100" />
<polyline className="prompt-size-sparkline-line" points={promptPolyline} />
</svg>
<span className="prompt-size-values">
{latestPrompt.systemChars.toLocaleString()} / {latestPrompt.execChars.toLocaleString()} / {latestPrompt.totalChars.toLocaleString()}
</span>
</div>
</div>
)}
{tokenUsageSummary && (
<div className="run-output-section">
<div className="run-output-label">Cache hit ratio</div>
@@ -3250,6 +3282,11 @@ function deriveHeartbeatScopeDiscipline(runtimeConfig: AgentDetail["runtimeConfi
return mode === "strict" || mode === "lite" || mode === "off" ? mode : "";
}
function deriveHeartbeatPromptTemplate(runtimeConfig: AgentDetail["runtimeConfig"] | undefined): "default" | "compact" | "" {
const template = runtimeConfig?.heartbeatPromptTemplate;
return template === "default" || template === "compact" ? template : "";
}
function deriveBudgetValues(runtimeConfig: AgentDetail["runtimeConfig"] | undefined): Record<string, string> {
const bc = (runtimeConfig ?? {}).budgetConfig as Record<string, unknown> | undefined;
const nextValues: Record<string, string> = {};
@@ -3625,6 +3662,9 @@ function ConfigTab({
const [heartbeatScopeDiscipline, setHeartbeatScopeDiscipline] = useState<"strict" | "lite" | "off" | "">(
() => deriveHeartbeatScopeDiscipline(agent.runtimeConfig),
);
const [heartbeatPromptTemplate, setHeartbeatPromptTemplate] = useState<"default" | "compact" | "">(
() => deriveHeartbeatPromptTemplate(agent.runtimeConfig),
);
// Budget config state initialised from agent.runtimeConfig.budgetConfig
const [budgetValues, setBudgetValues] = useState<Record<string, string>>(
@@ -3913,6 +3953,7 @@ function ConfigTab({
if (allowParallelExecution !== deriveAllowParallelExecution(agent.runtimeConfig)) return true;
if (skipHeartbeatWhenIdle !== deriveSkipHeartbeatWhenIdle(agent.runtimeConfig)) return true;
if (heartbeatScopeDiscipline !== deriveHeartbeatScopeDiscipline(agent.runtimeConfig)) return true;
if (heartbeatPromptTemplate !== deriveHeartbeatPromptTemplate(agent.runtimeConfig)) return true;
for (const key of ["heartbeatIntervalMs", "heartbeatTimeoutMs", "maxConcurrentRuns", "messageResponseMode", "autoClaimCandidatesInPrompt"] as const) {
const current = heartbeatValues[key]?.trim() ?? "";
let persisted = rc[key] !== undefined && rc[key] !== null ? String(rc[key]) : "";
@@ -4171,6 +4212,12 @@ function ConfigTab({
newRuntimeConfig.heartbeatScopeDiscipline = heartbeatScopeDiscipline;
}
if (!heartbeatPromptTemplate) {
delete newRuntimeConfig.heartbeatPromptTemplate;
} else {
newRuntimeConfig.heartbeatPromptTemplate = heartbeatPromptTemplate;
}
if (runtimeMode === "runtime") {
if (selectedRuntimeId.trim()) {
newRuntimeConfig.runtimeHint = selectedRuntimeId.trim();
@@ -4249,7 +4296,7 @@ function ConfigTab({
runtimeConfig: newRuntimeConfig,
bundleConfig: newBundleConfig,
};
}, [agent.metadata, agent.runtimeConfig, allowParallelExecution, autoClaimRelevantTasksEnabled, budgetValues, bundleEntryFile, bundleExternalPath, bundleFiles, bundleMode, formValues, heartbeatEnabled, heartbeatScopeDiscipline, heartbeatValues, iconValue, modelValue, nameValue, reportsToValue, roleValue, runMissedHeartbeatOnStartup, runtimeMode, selectedRuntimeId, selectedSkills, skipHeartbeatWhenIdle, titleValue, validationErrors]);
}, [agent.metadata, agent.runtimeConfig, allowParallelExecution, autoClaimRelevantTasksEnabled, budgetValues, bundleEntryFile, bundleExternalPath, bundleFiles, bundleMode, formValues, heartbeatEnabled, heartbeatPromptTemplate, heartbeatScopeDiscipline, heartbeatValues, iconValue, modelValue, nameValue, reportsToValue, roleValue, runMissedHeartbeatOnStartup, runtimeMode, selectedRuntimeId, selectedSkills, skipHeartbeatWhenIdle, titleValue, validationErrors]);
const persistSettings = useCallback(async (showValidationToast: boolean, source: "auto" | "manual") => {
const payload = buildSavePayload();
@@ -4743,6 +4790,24 @@ function ConfigTab({
<span className="config-hint">Strict coordination-focused; higher per-tick tokens. Lite pre-2026-05-11 behavior. Off minimal procedure.</span>
</div>
<div className="config-field">
<label htmlFor="hb-heartbeatPromptTemplate">Heartbeat Prompt Template</label>
<select
id="hb-heartbeatPromptTemplate"
className="select"
value={heartbeatPromptTemplate}
onChange={(e) => {
const value = e.target.value;
setHeartbeatPromptTemplate(value === "default" || value === "compact" ? value : "");
void scheduleAutoSave();
}}
>
<option value="">Inherit project default</option>
<option value="default">Default</option>
<option value="compact">Compact</option>
</select>
</div>
<div className="config-field">
<label htmlFor="hb-heartbeatIntervalMs">Heartbeat Interval (s)</label>
<input

View File

@@ -20,6 +20,7 @@ import {
mockFetchAgentMemoryFiles,
mockFetchAgentRunDetail,
mockFetchAgentRunLogs,
mockFetchAgentPromptSizes,
mockFetchAgentRuns,
mockFetchAgentTasks,
mockFetchAgents,
@@ -257,6 +258,10 @@ describe("Tasks tab", () => {
describe("Runs Tab — click to show logs", () => {
it("shows cache hit ratio section for permanent agents", async () => {
const user = userEvent.setup();
mockFetchAgentPromptSizes.mockResolvedValueOnce([
{ runId: "run-1", createdAt: "2026-05-14T00:00:00.000Z", systemChars: 1500, execChars: 4800, totalChars: 6300 },
{ runId: "run-2", createdAt: "2026-05-13T23:00:00.000Z", systemChars: 1200, execChars: 3900, totalChars: 5100 },
]);
render(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={vi.fn()} />);
await user.click(await screen.findByText("Runs"));
@@ -264,10 +269,13 @@ describe("Runs Tab — click to show logs", () => {
expect(screen.getByText("Cache hit ratio")).toBeInTheDocument();
expect(screen.getByText(/Last 24h:/)).toBeInTheDocument();
expect(screen.getAllByText(/33.3%/)).toHaveLength(3);
expect(screen.getByText("Prompt Size")).toBeInTheDocument();
expect(screen.getByText("1,500 / 4,800 / 6,300")).toBeInTheDocument();
});
});
it("hides cache hit ratio section for ephemeral agents", async () => {
mockFetchAgentPromptSizes.mockRejectedValueOnce(new Error("400 Prompt sizes are not available for ephemeral agents"));
const user = userEvent.setup();
mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: { type: "spawned" } as any }));
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {

View File

@@ -732,6 +732,48 @@ describe("Config autosave", () => {
expect(latestPayload.runtimeConfig).toBeDefined();
expect(latestPayload.runtimeConfig).not.toHaveProperty("heartbeatScopeDiscipline");
});
it("saves heartbeat prompt template override", async () => {
mockFetchAgent.mockResolvedValue(createMockAgent({
runtimeConfig: { heartbeatPromptTemplate: "default" },
} as any));
const user = userEvent.setup();
render(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={vi.fn()} />);
await openSettings(user);
const select = screen.getByLabelText("Heartbeat Prompt Template") as HTMLSelectElement;
await user.selectOptions(select, "compact");
await waitFor(() => {
expect(mockUpdateAgent).toHaveBeenCalled();
}, { timeout: 3000 });
expect(mockUpdateAgent.mock.calls.at(-1)?.[1]).toMatchObject({
runtimeConfig: expect.objectContaining({ heartbeatPromptTemplate: "compact" }),
});
});
it("clears heartbeat prompt template when inherit project default is selected", async () => {
mockFetchAgent.mockResolvedValue(createMockAgent({
runtimeConfig: { heartbeatPromptTemplate: "compact" },
} as any));
const user = userEvent.setup();
render(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={vi.fn()} />);
await openSettings(user);
const select = screen.getByLabelText("Heartbeat Prompt Template") as HTMLSelectElement;
await user.selectOptions(select, "");
await waitFor(() => {
expect(mockUpdateAgent).toHaveBeenCalled();
}, { timeout: 3000 });
const latestPayload = mockUpdateAgent.mock.calls.at(-1)?.[1] as { runtimeConfig?: Record<string, unknown> };
expect(latestPayload.runtimeConfig).toBeDefined();
expect(latestPayload.runtimeConfig).not.toHaveProperty("heartbeatPromptTemplate");
});
});
});

View File

@@ -15,6 +15,7 @@ export const mockFetchAgentChildren = vi.fn<ApiModule["fetchAgentChildren"]>();
export const mockFetchAgentRunLogs = vi.fn<ApiModule["fetchAgentRunLogs"]>();
export const mockFetchAgentRuns = vi.fn<ApiModule["fetchAgentRuns"]>();
export const mockFetchAgentRunDetail = vi.fn<ApiModule["fetchAgentRunDetail"]>();
export const mockFetchAgentPromptSizes = vi.fn<ApiModule["fetchAgentPromptSizes"]>();
export const mockFetchAgentTasks = vi.fn<ApiModule["fetchAgentTasks"]>();
export const mockFetchChainOfCommand = vi.fn<ApiModule["fetchChainOfCommand"]>();
export const mockFetchAgentBudgetStatus = vi.fn<ApiModule["fetchAgentBudgetStatus"]>();
@@ -60,6 +61,7 @@ vi.mock("../../api", () => ({
fetchAgentChildren: (...args: Parameters<ApiModule["fetchAgentChildren"]>) => mockFetchAgentChildren(...args),
fetchAgentRuns: (...args: Parameters<ApiModule["fetchAgentRuns"]>) => mockFetchAgentRuns(...args),
fetchAgentRunDetail: (...args: Parameters<ApiModule["fetchAgentRunDetail"]>) => mockFetchAgentRunDetail(...args),
fetchAgentPromptSizes: (...args: Parameters<ApiModule["fetchAgentPromptSizes"]>) => mockFetchAgentPromptSizes(...args),
startAgentRun: (...args: Parameters<ApiModule["startAgentRun"]>) => mockStartAgentRun(...args),
stopAgentRun: vi.fn(),
updateAgentInstructions: (...args: Parameters<ApiModule["updateAgentInstructions"]>) => mockUpdateAgentInstructions(...args),
@@ -257,6 +259,7 @@ export function setupAgentDetailMocks() {
]);
mockFetchAgentRunLogs.mockResolvedValue([]);
mockFetchAgentRunDetail.mockResolvedValue(mockAgent.completedRuns[0]);
mockFetchAgentPromptSizes.mockResolvedValue([]);
mockFetchAgentChildren.mockResolvedValue([]);
mockFetchAgentTasks.mockResolvedValue([]);
mockFetchChainOfCommand.mockResolvedValue([mockAgent]);

View File

@@ -0,0 +1,98 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { EventEmitter } from "node:events";
import { get } from "../test-request.js";
import { createServer } from "../server.js";
const {
mockInit,
mockGetAgent,
mockIsEphemeralAgent,
mockChatStoreInit,
mockAll,
} = vi.hoisted(() => ({
mockInit: vi.fn().mockResolvedValue(undefined),
mockGetAgent: vi.fn(),
mockIsEphemeralAgent: vi.fn(),
mockChatStoreInit: vi.fn().mockResolvedValue(undefined),
mockAll: vi.fn(),
}));
vi.mock("@fusion/core", () => ({
AgentStore: class MockAgentStore {
init = mockInit;
getAgent = mockGetAgent;
},
isEphemeralAgent: mockIsEphemeralAgent,
ChatStore: class MockChatStore {
init = mockChatStoreInit;
},
}));
class MockStore extends EventEmitter {
getRootDir(): string {
return "/tmp/fn-4400-test";
}
getFusionDir(): string {
return "/tmp/fn-4400-test/.fusion";
}
getDatabase() {
return {
exec: vi.fn(),
prepare: vi.fn().mockReturnValue({
run: vi.fn().mockReturnValue({ changes: 0 }),
get: vi.fn(),
all: mockAll,
}),
};
}
}
describe("GET /api/agents/:id/prompt-sizes", () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetAgent.mockResolvedValue({ id: "agent-001", role: "executor", metadata: {} });
mockIsEphemeralAgent.mockReturnValue(false);
mockAll.mockReturnValue([
{
runId: "run-1",
createdAt: "2026-05-14T00:00:00.000Z",
systemChars: 120,
execChars: 880,
totalChars: 1000,
},
{
runId: "run-2",
createdAt: "2026-05-13T23:00:00.000Z",
systemChars: 0,
execChars: 0,
totalChars: 0,
},
]);
});
it("returns recent prompt size rows", async () => {
const app = createServer(new MockStore() as any);
const res = await get(app, "/api/agents/agent-001/prompt-sizes");
expect(res.status).toBe(200);
expect(res.body).toEqual([
expect.objectContaining({ runId: "run-1", totalChars: 1000 }),
expect.objectContaining({ runId: "run-2", systemChars: 0, execChars: 0, totalChars: 0 }),
]);
});
it("returns 404 when agent is missing", async () => {
mockGetAgent.mockResolvedValueOnce(null);
const app = createServer(new MockStore() as any);
const res = await get(app, "/api/agents/missing/prompt-sizes");
expect(res.status).toBe(404);
});
it("returns 400 for ephemeral agents", async () => {
mockIsEphemeralAgent.mockReturnValueOnce(true);
const app = createServer(new MockStore() as any);
const res = await get(app, "/api/agents/agent-001/prompt-sizes");
expect(res.status).toBe(400);
});
});

View File

@@ -631,6 +631,64 @@ export function registerAgentRuntimeRoutes(ctx: ApiRoutesContext, deps: AgentRun
}
});
/**
* GET /api/agents/:id/prompt-sizes
* Get recent prompt-size points for a permanent agent.
*/
router.get("/agents/:id/prompt-sizes", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
const { AgentStore, isEphemeralAgent } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
await agentStore.init();
const agent = await agentStore.getAgent(req.params.id);
if (!agent) {
throw notFound("Agent not found");
}
if (isEphemeralAgent(agent)) {
throw badRequest("Prompt sizes are not available for ephemeral agents");
}
const rawLimit = typeof req.query.limit === "string" ? Number.parseInt(req.query.limit, 10) : 7;
if (!Number.isInteger(rawLimit) || rawLimit <= 0) {
throw badRequest("limit must be a positive integer");
}
const limit = Math.min(rawLimit, 30);
const rows = scopedStore.getDatabase().prepare(`
SELECT
id AS runId,
createdAt,
COALESCE(length(json_extract(data, '$.systemPrompt')), 0) AS systemChars,
COALESCE(length(json_extract(data, '$.executionPrompt')), 0) AS execChars,
COALESCE(length(json_extract(data, '$.systemPrompt')), 0)
+ COALESCE(length(json_extract(data, '$.executionPrompt')), 0) AS totalChars
FROM agentRuns
WHERE json_extract(data, '$.agentId') = ?
ORDER BY createdAt DESC
LIMIT ?
`).all(req.params.id, limit) as Array<{
runId: string;
createdAt: string;
systemChars: number;
execChars: number;
totalChars: number;
}>;
res.json(rows);
} catch (err: unknown) {
if (err instanceof ApiError) {
throw err;
}
if ((err instanceof Error ? err.message : String(err)).includes("not found")) {
throw notFound(err instanceof Error ? err.message : String(err));
} else {
rethrowAsApiError(err);
}
}
});
/**
* GET /api/agents/:id/token-usage
* Get cache/token usage summary windows for a permanent agent.