refactor(core): extract shared usage, auth, and display helpers

This commit is contained in:
Peter Steinberger
2026-03-02 08:52:46 +00:00
parent e427826fcf
commit d358b3ac88
11 changed files with 356 additions and 259 deletions

View File

@@ -51,6 +51,18 @@ export function normalizeVerb(value?: string): string | undefined {
return trimmed.replace(/_/g, " ");
}
export function resolveActionArg(args: unknown): string | undefined {
if (!args || typeof args !== "object") {
return undefined;
}
const actionRaw = (args as Record<string, unknown>).action;
if (typeof actionRaw !== "string") {
return undefined;
}
const action = actionRaw.trim();
return action || undefined;
}
export function coerceDisplayValue(
value: unknown,
opts: CoerceDisplayValueOptions = {},
@@ -1118,3 +1130,80 @@ export function resolveDetailFromKeys(
.map((entry) => `${entry.label} ${entry.value}`)
.join(" · ");
}
export function resolveToolVerbAndDetail(params: {
toolKey: string;
args?: unknown;
meta?: string;
action?: string;
spec?: ToolDisplaySpec;
fallbackDetailKeys?: string[];
detailMode: "first" | "summary";
detailCoerce?: CoerceDisplayValueOptions;
detailMaxEntries?: number;
detailFormatKey?: (raw: string) => string;
}): { verb?: string; detail?: string } {
const actionSpec = resolveActionSpec(params.spec, params.action);
const fallbackVerb =
params.toolKey === "web_search"
? "search"
: params.toolKey === "web_fetch"
? "fetch"
: params.toolKey.replace(/_/g, " ").replace(/\./g, " ");
const verb = normalizeVerb(actionSpec?.label ?? params.action ?? fallbackVerb);
let detail: string | undefined;
if (params.toolKey === "exec") {
detail = resolveExecDetail(params.args);
}
if (!detail && params.toolKey === "read") {
detail = resolveReadDetail(params.args);
}
if (
!detail &&
(params.toolKey === "write" || params.toolKey === "edit" || params.toolKey === "attach")
) {
detail = resolveWriteDetail(params.toolKey, params.args);
}
if (!detail && params.toolKey === "web_search") {
detail = resolveWebSearchDetail(params.args);
}
if (!detail && params.toolKey === "web_fetch") {
detail = resolveWebFetchDetail(params.args);
}
const detailKeys =
actionSpec?.detailKeys ?? params.spec?.detailKeys ?? params.fallbackDetailKeys ?? [];
if (!detail && detailKeys.length > 0) {
detail = resolveDetailFromKeys(params.args, detailKeys, {
mode: params.detailMode,
coerce: params.detailCoerce,
maxEntries: params.detailMaxEntries,
formatKey: params.detailFormatKey,
});
}
if (!detail && params.meta) {
detail = params.meta;
}
return { verb, detail };
}
export function formatToolDetailText(
detail: string | undefined,
opts: { prefixWithWith?: boolean } = {},
): string | undefined {
if (!detail) {
return undefined;
}
const normalized = detail.includes(" · ")
? detail
.split(" · ")
.map((part) => part.trim())
.filter((part) => part.length > 0)
.join(", ")
: detail;
if (!normalized) {
return undefined;
}
return opts.prefixWithWith ? `with ${normalized}` : normalized;
}

View File

@@ -2,16 +2,11 @@ import { redactToolDetail } from "../logging/redact.js";
import { shortenHomeInString } from "../utils.js";
import {
defaultTitle,
formatToolDetailText,
formatDetailKey,
normalizeToolName,
normalizeVerb,
resolveActionSpec,
resolveDetailFromKeys,
resolveExecDetail,
resolveReadDetail,
resolveWebFetchDetail,
resolveWebSearchDetail,
resolveWriteDetail,
resolveActionArg,
resolveToolVerbAndDetail,
type ToolDisplaySpec as ToolDisplaySpecBase,
} from "./tool-display-common.js";
import TOOL_DISPLAY_JSON from "./tool-display.json" with { type: "json" };
@@ -69,51 +64,18 @@ export function resolveToolDisplay(params: {
const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
const title = spec?.title ?? defaultTitle(name);
const label = spec?.label ?? title;
const actionRaw =
params.args && typeof params.args === "object"
? ((params.args as Record<string, unknown>).action as string | undefined)
: undefined;
const action = typeof actionRaw === "string" ? actionRaw.trim() : undefined;
const actionSpec = resolveActionSpec(spec, action);
const fallbackVerb =
key === "web_search"
? "search"
: key === "web_fetch"
? "fetch"
: key.replace(/_/g, " ").replace(/\./g, " ");
const verb = normalizeVerb(actionSpec?.label ?? action ?? fallbackVerb);
let detail: string | undefined;
if (key === "exec") {
detail = resolveExecDetail(params.args);
}
if (!detail && key === "read") {
detail = resolveReadDetail(params.args);
}
if (!detail && (key === "write" || key === "edit" || key === "attach")) {
detail = resolveWriteDetail(key, params.args);
}
if (!detail && key === "web_search") {
detail = resolveWebSearchDetail(params.args);
}
if (!detail && key === "web_fetch") {
detail = resolveWebFetchDetail(params.args);
}
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
if (!detail && detailKeys.length > 0) {
detail = resolveDetailFromKeys(params.args, detailKeys, {
mode: "summary",
maxEntries: MAX_DETAIL_ENTRIES,
formatKey: (raw) => formatDetailKey(raw, DETAIL_LABEL_OVERRIDES),
});
}
if (!detail && params.meta) {
detail = params.meta;
}
const action = resolveActionArg(params.args);
let { verb, detail } = resolveToolVerbAndDetail({
toolKey: key,
args: params.args,
meta: params.meta,
action,
spec,
fallbackDetailKeys: FALLBACK.detailKeys,
detailMode: "summary",
detailMaxEntries: MAX_DETAIL_ENTRIES,
detailFormatKey: (raw) => formatDetailKey(raw, DETAIL_LABEL_OVERRIDES),
});
if (detail) {
detail = shortenHomeInString(detail);
@@ -131,18 +93,7 @@ export function resolveToolDisplay(params: {
export function formatToolDetail(display: ToolDisplay): string | undefined {
const detailRaw = display.detail ? redactToolDetail(display.detail) : undefined;
if (!detailRaw) {
return undefined;
}
if (detailRaw.includes(" · ")) {
const compact = detailRaw
.split(" · ")
.map((part) => part.trim())
.filter((part) => part.length > 0)
.join(", ");
return compact ? `with ${compact}` : undefined;
}
return detailRaw;
return formatToolDetailText(detailRaw, { prefixWithWith: true });
}
export function formatToolSummary(display: ToolDisplay): string {

View File

@@ -4,17 +4,13 @@ import {
resolveSessionFilePath,
resolveSessionFilePathOptions,
} from "../../config/sessions/paths.js";
import type { SessionEntry, SessionSystemPromptReport } from "../../config/sessions/types.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import { loadProviderUsageSummary } from "../../infra/provider-usage.js";
import type {
CostUsageSummary,
SessionCostSummary,
SessionDailyLatency,
SessionDailyModelUsage,
SessionMessageCounts,
SessionLatencyStats,
SessionModelUsage,
SessionToolUsage,
} from "../../infra/session-cost-usage.js";
import {
loadCostUsageSummary,
@@ -24,7 +20,16 @@ import {
type DiscoveredSession,
} from "../../infra/session-cost-usage.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { buildUsageAggregateTail } from "../../shared/usage-aggregates.js";
import {
buildUsageAggregateTail,
mergeUsageDailyLatency,
mergeUsageLatency,
} from "../../shared/usage-aggregates.js";
import type {
SessionUsageEntry,
SessionsUsageAggregates,
SessionsUsageResult,
} from "../../shared/usage-types.js";
import {
ErrorCodes,
errorShape,
@@ -340,60 +345,7 @@ export const __test = {
costUsageCache,
};
export type SessionUsageEntry = {
key: string;
label?: string;
sessionId?: string;
updatedAt?: number;
agentId?: string;
channel?: string;
chatType?: string;
origin?: {
label?: string;
provider?: string;
surface?: string;
chatType?: string;
from?: string;
to?: string;
accountId?: string;
threadId?: string | number;
};
modelOverride?: string;
providerOverride?: string;
modelProvider?: string;
model?: string;
usage: SessionCostSummary | null;
contextWeight?: SessionSystemPromptReport | null;
};
export type SessionsUsageAggregates = {
messages: SessionMessageCounts;
tools: SessionToolUsage;
byModel: SessionModelUsage[];
byProvider: SessionModelUsage[];
byAgent: Array<{ agentId: string; totals: CostUsageSummary["totals"] }>;
byChannel: Array<{ channel: string; totals: CostUsageSummary["totals"] }>;
latency?: SessionLatencyStats;
dailyLatency?: SessionDailyLatency[];
modelDaily?: SessionDailyModelUsage[];
daily: Array<{
date: string;
tokens: number;
cost: number;
messages: number;
toolCalls: number;
errors: number;
}>;
};
export type SessionsUsageResult = {
updatedAt: number;
startDate: string;
endDate: string;
sessions: SessionUsageEntry[];
totals: CostUsageSummary["totals"];
aggregates: SessionsUsageAggregates;
};
export type { SessionUsageEntry, SessionsUsageAggregates, SessionsUsageResult };
export const usageHandlers: GatewayRequestHandlers = {
"usage.status": async ({ respond }) => {
@@ -704,35 +656,8 @@ export const usageHandlers: GatewayRequestHandlers = {
}
}
if (usage.latency) {
const { count, avgMs, minMs, maxMs, p95Ms } = usage.latency;
if (count > 0) {
latencyTotals.count += count;
latencyTotals.sum += avgMs * count;
latencyTotals.min = Math.min(latencyTotals.min, minMs);
latencyTotals.max = Math.max(latencyTotals.max, maxMs);
latencyTotals.p95Max = Math.max(latencyTotals.p95Max, p95Ms);
}
}
if (usage.dailyLatency) {
for (const day of usage.dailyLatency) {
const existing = dailyLatencyMap.get(day.date) ?? {
date: day.date,
count: 0,
sum: 0,
min: Number.POSITIVE_INFINITY,
max: 0,
p95Max: 0,
};
existing.count += day.count;
existing.sum += day.avgMs * day.count;
existing.min = Math.min(existing.min, day.minMs);
existing.max = Math.max(existing.max, day.maxMs);
existing.p95Max = Math.max(existing.p95Max, day.p95Ms);
dailyLatencyMap.set(day.date, existing);
}
}
mergeUsageLatency(latencyTotals, usage.latency);
mergeUsageDailyLatency(dailyLatencyMap, usage.dailyLatency);
if (usage.dailyModelUsage) {
for (const entry of usage.dailyModelUsage) {

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { describe, expect, test, vi } from "vitest";
import { WebSocket } from "ws";
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
import { extractFirstTextBlock } from "../shared/chat-message-content.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import {
connectOk,
@@ -290,23 +291,8 @@ describe("gateway server chat", () => {
});
expect(defaultRes.ok).toBe(true);
const defaultMsgs = defaultRes.payload?.messages ?? [];
const firstContentText = (msg: unknown): string | undefined => {
if (!msg || typeof msg !== "object") {
return undefined;
}
const content = (msg as { content?: unknown }).content;
if (!Array.isArray(content) || content.length === 0) {
return undefined;
}
const first = content[0];
if (!first || typeof first !== "object") {
return undefined;
}
const text = (first as { text?: unknown }).text;
return typeof text === "string" ? text : undefined;
};
expect(defaultMsgs.length).toBe(200);
expect(firstContentText(defaultMsgs[0])).toBe("m100");
expect(extractFirstTextBlock(defaultMsgs[0])).toBe("m100");
} finally {
testState.agentConfig = undefined;
testState.sessionStorePath = undefined;

View File

@@ -2,11 +2,12 @@ import fs from "node:fs";
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
import {
clearDeviceAuthTokenFromStore,
type DeviceAuthEntry,
type DeviceAuthStore,
normalizeDeviceAuthRole,
normalizeDeviceAuthScopes,
} from "../shared/device-auth.js";
loadDeviceAuthTokenFromStore,
storeDeviceAuthTokenInStore,
} from "../shared/device-auth-store.js";
import type { DeviceAuthStore } from "../shared/device-auth.js";
const DEVICE_AUTH_FILE = "device-auth.json";
@@ -49,19 +50,11 @@ export function loadDeviceAuthToken(params: {
env?: NodeJS.ProcessEnv;
}): DeviceAuthEntry | null {
const filePath = resolveDeviceAuthPath(params.env);
const store = readStore(filePath);
if (!store) {
return null;
}
if (store.deviceId !== params.deviceId) {
return null;
}
const role = normalizeDeviceAuthRole(params.role);
const entry = store.tokens[role];
if (!entry || typeof entry.token !== "string") {
return null;
}
return entry;
return loadDeviceAuthTokenFromStore({
adapter: { readStore: () => readStore(filePath), writeStore: (_store) => {} },
deviceId: params.deviceId,
role: params.role,
});
}
export function storeDeviceAuthToken(params: {
@@ -72,25 +65,16 @@ export function storeDeviceAuthToken(params: {
env?: NodeJS.ProcessEnv;
}): DeviceAuthEntry {
const filePath = resolveDeviceAuthPath(params.env);
const existing = readStore(filePath);
const role = normalizeDeviceAuthRole(params.role);
const next: DeviceAuthStore = {
version: 1,
return storeDeviceAuthTokenInStore({
adapter: {
readStore: () => readStore(filePath),
writeStore: (store) => writeStore(filePath, store),
},
deviceId: params.deviceId,
tokens:
existing && existing.deviceId === params.deviceId && existing.tokens
? { ...existing.tokens }
: {},
};
const entry: DeviceAuthEntry = {
role: params.role,
token: params.token,
role,
scopes: normalizeDeviceAuthScopes(params.scopes),
updatedAtMs: Date.now(),
};
next.tokens[role] = entry;
writeStore(filePath, next);
return entry;
scopes: params.scopes,
});
}
export function clearDeviceAuthToken(params: {
@@ -99,19 +83,12 @@ export function clearDeviceAuthToken(params: {
env?: NodeJS.ProcessEnv;
}): void {
const filePath = resolveDeviceAuthPath(params.env);
const store = readStore(filePath);
if (!store || store.deviceId !== params.deviceId) {
return;
}
const role = normalizeDeviceAuthRole(params.role);
if (!store.tokens[role]) {
return;
}
const next: DeviceAuthStore = {
version: 1,
deviceId: store.deviceId,
tokens: { ...store.tokens },
};
delete next.tokens[role];
writeStore(filePath, next);
clearDeviceAuthTokenFromStore({
adapter: {
readStore: () => readStore(filePath),
writeStore: (store) => writeStore(filePath, store),
},
deviceId: params.deviceId,
role: params.role,
});
}

View File

@@ -1,27 +1,3 @@
declare module "../../scripts/run-node.mjs" {
export const runNodeWatchedPaths: string[];
export function runNodeMain(params?: {
spawn?: (
cmd: string,
args: string[],
options: unknown,
) => {
on: (
event: "exit",
cb: (code: number | null, signal: string | null) => void,
) => void | undefined;
};
spawnSync?: unknown;
fs?: unknown;
stderr?: { write: (value: string) => void };
execPath?: string;
cwd?: string;
args?: string[];
env?: NodeJS.ProcessEnv;
platform?: NodeJS.Platform;
}): Promise<number>;
}
declare module "../../scripts/watch-node.mjs" {
export function runWatchMain(params?: {
spawn?: (

View File

@@ -0,0 +1,15 @@
export function extractFirstTextBlock(message: unknown): string | undefined {
if (!message || typeof message !== "object") {
return undefined;
}
const content = (message as { content?: unknown }).content;
if (!Array.isArray(content) || content.length === 0) {
return undefined;
}
const first = content[0];
if (!first || typeof first !== "object") {
return undefined;
}
const text = (first as { text?: unknown }).text;
return typeof text === "string" ? text : undefined;
}

View File

@@ -0,0 +1,79 @@
import {
type DeviceAuthEntry,
type DeviceAuthStore,
normalizeDeviceAuthRole,
normalizeDeviceAuthScopes,
} from "./device-auth.js";
export type { DeviceAuthEntry, DeviceAuthStore } from "./device-auth.js";
export type DeviceAuthStoreAdapter = {
readStore: () => DeviceAuthStore | null;
writeStore: (store: DeviceAuthStore) => void;
};
export function loadDeviceAuthTokenFromStore(params: {
adapter: DeviceAuthStoreAdapter;
deviceId: string;
role: string;
}): DeviceAuthEntry | null {
const store = params.adapter.readStore();
if (!store || store.deviceId !== params.deviceId) {
return null;
}
const role = normalizeDeviceAuthRole(params.role);
const entry = store.tokens[role];
if (!entry || typeof entry.token !== "string") {
return null;
}
return entry;
}
export function storeDeviceAuthTokenInStore(params: {
adapter: DeviceAuthStoreAdapter;
deviceId: string;
role: string;
token: string;
scopes?: string[];
}): DeviceAuthEntry {
const role = normalizeDeviceAuthRole(params.role);
const existing = params.adapter.readStore();
const next: DeviceAuthStore = {
version: 1,
deviceId: params.deviceId,
tokens:
existing && existing.deviceId === params.deviceId && existing.tokens
? { ...existing.tokens }
: {},
};
const entry: DeviceAuthEntry = {
token: params.token,
role,
scopes: normalizeDeviceAuthScopes(params.scopes),
updatedAtMs: Date.now(),
};
next.tokens[role] = entry;
params.adapter.writeStore(next);
return entry;
}
export function clearDeviceAuthTokenFromStore(params: {
adapter: DeviceAuthStoreAdapter;
deviceId: string;
role: string;
}): void {
const store = params.adapter.readStore();
if (!store || store.deviceId !== params.deviceId) {
return;
}
const role = normalizeDeviceAuthRole(params.role);
if (!store.tokens[role]) {
return;
}
const next: DeviceAuthStore = {
version: 1,
deviceId: store.deviceId,
tokens: { ...store.tokens },
};
delete next.tokens[role];
params.adapter.writeStore(next);
}

View File

@@ -19,6 +19,52 @@ type DailyLike = {
date: string;
};
type LatencyLike = {
count: number;
avgMs: number;
minMs: number;
maxMs: number;
p95Ms: number;
};
type DailyLatencyInput = LatencyLike & { date: string };
export function mergeUsageLatency(
totals: LatencyTotalsLike,
latency: LatencyLike | undefined,
): void {
if (!latency || latency.count <= 0) {
return;
}
totals.count += latency.count;
totals.sum += latency.avgMs * latency.count;
totals.min = Math.min(totals.min, latency.minMs);
totals.max = Math.max(totals.max, latency.maxMs);
totals.p95Max = Math.max(totals.p95Max, latency.p95Ms);
}
export function mergeUsageDailyLatency(
dailyLatencyMap: Map<string, DailyLatencyLike>,
dailyLatency?: DailyLatencyInput[] | null,
): void {
for (const day of dailyLatency ?? []) {
const existing = dailyLatencyMap.get(day.date) ?? {
date: day.date,
count: 0,
sum: 0,
min: Number.POSITIVE_INFINITY,
max: 0,
p95Max: 0,
};
existing.count += day.count;
existing.sum += day.avgMs * day.count;
existing.min = Math.min(existing.min, day.minMs);
existing.max = Math.max(existing.max, day.maxMs);
existing.p95Max = Math.max(existing.p95Max, day.p95Ms);
dailyLatencyMap.set(day.date, existing);
}
}
export function buildUsageAggregateTail<
TTotals extends { totalCost: number },
TDaily extends DailyLike,

66
src/shared/usage-types.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { SessionSystemPromptReport } from "../config/sessions/types.js";
import type {
CostUsageSummary,
SessionCostSummary,
SessionDailyLatency,
SessionDailyModelUsage,
SessionLatencyStats,
SessionMessageCounts,
SessionModelUsage,
SessionToolUsage,
} from "../infra/session-cost-usage.js";
export type SessionUsageEntry = {
key: string;
label?: string;
sessionId?: string;
updatedAt?: number;
agentId?: string;
channel?: string;
chatType?: string;
origin?: {
label?: string;
provider?: string;
surface?: string;
chatType?: string;
from?: string;
to?: string;
accountId?: string;
threadId?: string | number;
};
modelOverride?: string;
providerOverride?: string;
modelProvider?: string;
model?: string;
usage: SessionCostSummary | null;
contextWeight?: SessionSystemPromptReport | null;
};
export type SessionsUsageAggregates = {
messages: SessionMessageCounts;
tools: SessionToolUsage;
byModel: SessionModelUsage[];
byProvider: SessionModelUsage[];
byAgent: Array<{ agentId: string; totals: CostUsageSummary["totals"] }>;
byChannel: Array<{ channel: string; totals: CostUsageSummary["totals"] }>;
latency?: SessionLatencyStats;
dailyLatency?: SessionDailyLatency[];
modelDaily?: SessionDailyModelUsage[];
daily: Array<{
date: string;
tokens: number;
cost: number;
messages: number;
toolCalls: number;
errors: number;
}>;
};
export type SessionsUsageResult = {
updatedAt: number;
startDate: string;
endDate: string;
sessions: SessionUsageEntry[];
totals: CostUsageSummary["totals"];
aggregates: SessionsUsageAggregates;
};

View File

@@ -8,9 +8,12 @@ import path from "node:path";
import { GatewayClient } from "../../src/gateway/client.js";
import { connectGatewayClient } from "../../src/gateway/test-helpers.e2e.js";
import { loadOrCreateDeviceIdentity } from "../../src/infra/device-identity.js";
import { extractFirstTextBlock } from "../../src/shared/chat-message-content.js";
import { sleep } from "../../src/utils.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../src/utils/message-channel.js";
export { extractFirstTextBlock };
type NodeListPayload = {
nodes?: Array<{ nodeId?: string; connected?: boolean; paired?: boolean }>;
};
@@ -358,22 +361,6 @@ export async function waitForNodeStatus(
throw new Error(`timeout waiting for node status for ${nodeId}`);
}
export function extractFirstTextBlock(message: unknown): string | undefined {
if (!message || typeof message !== "object") {
return undefined;
}
const content = (message as { content?: unknown }).content;
if (!Array.isArray(content) || content.length === 0) {
return undefined;
}
const first = content[0];
if (!first || typeof first !== "object") {
return undefined;
}
const text = (first as { text?: unknown }).text;
return typeof text === "string" ? text : undefined;
}
export async function waitForChatFinalEvent(params: {
events: ChatEventPayload[];
runId: string;