From e7b7b9e83e188bd2f3612e83fab3d0fc8c187b91 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 4 Mar 2026 12:31:26 +0000 Subject: [PATCH] feat: add 2-line status layout with per-type token breakdown and provider name Replace single-line token display with a 2-line layout: - Line 1: running/waiting counts, elapsed time, provider name - Line 2: R(easoning)/I(nput)/C(ached)/O(utput) token breakdown with cache hit rate Add providerName to initializeRuntime event and track per-type token accumulation (reasoning, input, cached input, output) across callTools, completeRun, and retry events. Co-Authored-By: Claude Opus 4.6 --- .changeset/status-line-token-breakdown.md | 7 +++ packages/core/src/adapters/event-creators.ts | 2 + packages/core/src/schemas/runtime.test.ts | 1 + packages/core/src/schemas/runtime.ts | 1 + packages/core/src/utils/event-filter.test.ts | 1 + packages/react/src/hooks/use-run.test.ts | 1 + .../react/src/utils/event-to-activity.test.ts | 1 + .../src/orchestration/coordinator-executor.ts | 1 + packages/tui-components/src/execution/app.tsx | 6 +++ .../execution/components/delegation-tree.tsx | 6 +++ .../execution/components/interface-panel.tsx | 27 ++++++++-- .../hooks/use-delegation-tree.test.ts | 51 +++++++++++++++++++ .../execution/hooks/use-delegation-tree.ts | 40 +++++++++++++++ .../execution/hooks/use-execution-state.ts | 12 +++++ 14 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 .changeset/status-line-token-breakdown.md diff --git a/.changeset/status-line-token-breakdown.md b/.changeset/status-line-token-breakdown.md new file mode 100644 index 00000000..71d66adb --- /dev/null +++ b/.changeset/status-line-token-breakdown.md @@ -0,0 +1,7 @@ +--- +"@perstack/core": patch +"@perstack/runtime": patch +"@perstack/tui-components": patch +--- + +feat: add 2-line status layout with per-type token breakdown and provider name diff --git a/packages/core/src/adapters/event-creators.ts b/packages/core/src/adapters/event-creators.ts index fcbaf3f9..dd34f405 100644 --- a/packages/core/src/adapters/event-creators.ts +++ b/packages/core/src/adapters/event-creators.ts @@ -71,6 +71,7 @@ export function createRuntimeInitEvent( expertName: string, version: string, query?: string, + providerName = "unknown", ): RuntimeEvent { return { type: "initializeRuntime", @@ -82,6 +83,7 @@ export function createRuntimeInitEvent( expertName, experts: [], model: "local:default", + providerName, maxRetries: 0, timeout: 0, query, diff --git a/packages/core/src/schemas/runtime.test.ts b/packages/core/src/schemas/runtime.test.ts index c1926bb6..abc60657 100644 --- a/packages/core/src/schemas/runtime.test.ts +++ b/packages/core/src/schemas/runtime.test.ts @@ -97,6 +97,7 @@ describe("@perstack/core: createRuntimeEvent", () => { expertName: "test-expert", experts: ["expert-1", "expert-2"], model: "claude-sonnet-4-20250514", + providerName: "anthropic", maxRetries: 3, timeout: 30000, }) diff --git a/packages/core/src/schemas/runtime.ts b/packages/core/src/schemas/runtime.ts index 3dfe2a59..a62bb881 100644 --- a/packages/core/src/schemas/runtime.ts +++ b/packages/core/src/schemas/runtime.ts @@ -461,6 +461,7 @@ type RuntimeEventPayloads = { expertName: string experts: string[] model: string + providerName: string maxRetries: number timeout: number query?: string diff --git a/packages/core/src/utils/event-filter.test.ts b/packages/core/src/utils/event-filter.test.ts index ccbefe7a..9840b336 100644 --- a/packages/core/src/utils/event-filter.test.ts +++ b/packages/core/src/utils/event-filter.test.ts @@ -117,6 +117,7 @@ describe("@perstack/core: createFilteredEventListener", () => { timestamp: Date.now(), jobId: "job-1", runId: "run-1", + providerName: "anthropic", } as PerstackEvent const event2 = { type: "skillStarting", diff --git a/packages/react/src/hooks/use-run.test.ts b/packages/react/src/hooks/use-run.test.ts index 39952d8c..9960e546 100644 --- a/packages/react/src/hooks/use-run.test.ts +++ b/packages/react/src/hooks/use-run.test.ts @@ -153,6 +153,7 @@ describe("useRun processing logic", () => { jobId: "job-1", type: "initializeRuntime", timestamp: Date.now(), + providerName: "anthropic", } as PerstackEvent processRunEventToActivity(state, runtimeEvent, addActivity) diff --git a/packages/react/src/utils/event-to-activity.test.ts b/packages/react/src/utils/event-to-activity.test.ts index b327b66f..fd635bd0 100644 --- a/packages/react/src/utils/event-to-activity.test.ts +++ b/packages/react/src/utils/event-to-activity.test.ts @@ -248,6 +248,7 @@ describe("processRunEventToActivity", () => { jobId: "job-1", type: "initializeRuntime", timestamp: Date.now(), + providerName: "anthropic", } as PerstackEvent processRunEventToActivity(state, runtimeEvent, (a) => activities.push(a)) expect(activities).toHaveLength(0) diff --git a/packages/runtime/src/orchestration/coordinator-executor.ts b/packages/runtime/src/orchestration/coordinator-executor.ts index 41778d50..bafff4c7 100644 --- a/packages/runtime/src/orchestration/coordinator-executor.ts +++ b/packages/runtime/src/orchestration/coordinator-executor.ts @@ -207,6 +207,7 @@ export class CoordinatorExecutor { expertName: expertToRun.name, experts: Object.keys(experts), model: resolvedModel, + providerName: setting.providerConfig.providerName, maxRetries: setting.maxRetries, timeout: setting.timeout, query: setting.input.text, diff --git a/packages/tui-components/src/execution/app.tsx b/packages/tui-components/src/execution/app.tsx index c421eddd..bf2070ce 100644 --- a/packages/tui-components/src/execution/app.tsx +++ b/packages/tui-components/src/execution/app.tsx @@ -75,6 +75,12 @@ export const ExecutionApp = (props: ExecutionAppProps) => { runningCount={state.runningCount} waitingCount={state.waitingCount} formattedTotalTokens={state.formattedTotalTokens} + formattedReasoningTokens={state.formattedReasoningTokens} + formattedInputTokens={state.formattedInputTokens} + formattedCachedInputTokens={state.formattedCachedInputTokens} + formattedOutputTokens={state.formattedOutputTokens} + providerName={state.providerName} + cacheHitRate={state.cacheHitRate} elapsedTime={state.elapsedTime} /> diff --git a/packages/tui-components/src/execution/components/delegation-tree.tsx b/packages/tui-components/src/execution/components/delegation-tree.tsx index a7d379db..c919ddcf 100644 --- a/packages/tui-components/src/execution/components/delegation-tree.tsx +++ b/packages/tui-components/src/execution/components/delegation-tree.tsx @@ -65,6 +65,12 @@ function TreeNodeLine({ {node.actionFileArg ? {node.actionFileArg} : null} {showUsage ? ( <> + {node.model ? ( + <> + · + {node.model} + + ) : null} · {usageIcon} {usagePercent}% diff --git a/packages/tui-components/src/execution/components/interface-panel.tsx b/packages/tui-components/src/execution/components/interface-panel.tsx index 2a11cd34..f6672033 100644 --- a/packages/tui-components/src/execution/components/interface-panel.tsx +++ b/packages/tui-components/src/execution/components/interface-panel.tsx @@ -14,6 +14,12 @@ type InterfacePanelProps = { runningCount: number waitingCount: number formattedTotalTokens: string + formattedReasoningTokens: string + formattedInputTokens: string + formattedCachedInputTokens: string + formattedOutputTokens: string + providerName: string | undefined + cacheHitRate: string elapsedTime: string } @@ -24,7 +30,12 @@ export const InterfacePanel = ({ delegationTreeState, runningCount, waitingCount, - formattedTotalTokens, + formattedReasoningTokens, + formattedInputTokens, + formattedCachedInputTokens, + formattedOutputTokens, + providerName, + cacheHitRate, elapsedTime, }: InterfacePanelProps): React.ReactNode => { const { input, handleInput } = useTextInput({ @@ -59,8 +70,18 @@ export const InterfacePanel = ({ {waitingCount > 0 ? · {waitingCount} waiting : null} · {elapsedTime} - · - {formattedTotalTokens} tokens + {providerName ? · {providerName} : null} + + + R + {formattedReasoningTokens} + · I + {formattedInputTokens} + · C + {formattedCachedInputTokens} + · O + {formattedOutputTokens} + (cache: {cacheHitRate}%) )} diff --git a/packages/tui-components/src/execution/hooks/use-delegation-tree.test.ts b/packages/tui-components/src/execution/hooks/use-delegation-tree.test.ts index 4b78a9f8..3817302f 100644 --- a/packages/tui-components/src/execution/hooks/use-delegation-tree.test.ts +++ b/packages/tui-components/src/execution/hooks/use-delegation-tree.test.ts @@ -176,6 +176,7 @@ describe("processDelegationTreeEvent", () => { expertName: "test", experts: [], model: "gpt-4", + providerName: "anthropic", maxRetries: 3, timeout: 30000, query: "Hello world", @@ -183,6 +184,7 @@ describe("processDelegationTreeEvent", () => { const changed = processDelegationTreeEvent(state, event) expect(changed).toBe(true) expect(state.jobStartedAt).toBe(1000000) + expect(state.providerName).toBe("anthropic") }) it("does not overwrite jobStartedAt on subsequent initializeRuntime events", () => { @@ -194,12 +196,14 @@ describe("processDelegationTreeEvent", () => { expertName: "root", experts: [], model: "gpt-4", + providerName: "anthropic", maxRetries: 3, timeout: 30000, query: "Hello", }), ) expect(state.jobStartedAt).toBe(1000000) + expect(state.providerName).toBe("anthropic") // Second initializeRuntime from a delegate should not overwrite processDelegationTreeEvent(state, { id: "evt-99", @@ -211,11 +215,13 @@ describe("processDelegationTreeEvent", () => { expertName: "delegate", experts: [], model: "gpt-4", + providerName: "openai", maxRetries: 3, timeout: 30000, query: "Delegated query", } as PerstackEvent) expect(state.jobStartedAt).toBe(1000000) + expect(state.providerName).toBe("anthropic") }) it("handles startRun for root expert", () => { @@ -290,6 +296,51 @@ describe("processDelegationTreeEvent", () => { expect(node.totalTokens).toBe(500) expect(node.contextWindowUsage).toBe(0.5) }) + + it("accumulates per-type token breakdown across callTools and completeRun", () => { + const state = createInitialDelegationTreeState() + processDelegationTreeEvent( + state, + makeRunEvent("startRun", "run-1", "test@1.0.0", { + initialCheckpoint: makeCheckpoint({ runId: "run-1" }), + inputMessages: [], + }), + ) + processDelegationTreeEvent( + state, + makeRunEvent("callTools", "run-1", "test@1.0.0", { + newMessage: {}, + toolCalls: [makeToolCall("readTextFile", { path: "a.txt" })], + usage: { + inputTokens: 100, + outputTokens: 50, + reasoningTokens: 20, + cachedInputTokens: 80, + totalTokens: 250, + }, + }), + ) + processDelegationTreeEvent( + state, + makeRunEvent("completeRun", "run-1", "test@1.0.0", { + checkpoint: makeCheckpoint({ runId: "run-1" }), + step: { stepNumber: 1, newMessages: [], usage: baseUsage }, + text: "Done", + usage: { + inputTokens: 200, + outputTokens: 30, + reasoningTokens: 10, + cachedInputTokens: 150, + totalTokens: 390, + }, + }), + ) + expect(state.jobReasoningTokens).toBe(30) + expect(state.jobInputTokens).toBe(300) + expect(state.jobCachedInputTokens).toBe(230) + expect(state.jobOutputTokens).toBe(80) + expect(state.jobTotalTokens).toBe(640) + }) }) describe("delegation lifecycle", () => { diff --git a/packages/tui-components/src/execution/hooks/use-delegation-tree.ts b/packages/tui-components/src/execution/hooks/use-delegation-tree.ts index b98a8c1d..5ff45038 100644 --- a/packages/tui-components/src/execution/hooks/use-delegation-tree.ts +++ b/packages/tui-components/src/execution/hooks/use-delegation-tree.ts @@ -16,6 +16,7 @@ export type DelegationTreeNode = { actionLabel: string actionFileArg: string | undefined contextWindowUsage: number + model: string | undefined parentRunId: string | undefined childRunIds: string[] totalTokens: number @@ -25,6 +26,11 @@ export type DelegationTreeState = { nodes: Map rootRunId: string | undefined jobTotalTokens: number + jobReasoningTokens: number + jobInputTokens: number + jobCachedInputTokens: number + jobOutputTokens: number + providerName: string | undefined jobStartedAt: number | undefined runIdAliases: Map } @@ -43,6 +49,11 @@ export function createInitialDelegationTreeState(): DelegationTreeState { nodes: new Map(), rootRunId: undefined, jobTotalTokens: 0, + jobReasoningTokens: 0, + jobInputTokens: 0, + jobCachedInputTokens: 0, + jobOutputTokens: 0, + providerName: undefined, jobStartedAt: undefined, runIdAliases: new Map(), } @@ -194,6 +205,9 @@ export function processDelegationTreeEvent( if (state.jobStartedAt === undefined) { state.jobStartedAt = event.timestamp } + if (state.providerName === undefined) { + state.providerName = event.providerName + } return true } @@ -218,6 +232,7 @@ export function processDelegationTreeEvent( actionLabel: "Starting...", actionFileArg: undefined, contextWindowUsage: initCheckpoint.contextWindowUsage ?? 0, + model: event.model, parentRunId, childRunIds: [], totalTokens: 0, @@ -268,6 +283,7 @@ export function processDelegationTreeEvent( const node = state.nodes.get(nodeId) if (node) { node.status = "running" + node.model = event.model if (checkpoint.contextWindowUsage !== undefined) { node.contextWindowUsage = checkpoint.contextWindowUsage } @@ -304,6 +320,10 @@ export function processDelegationTreeEvent( node.actionFileArg = fileArg node.totalTokens += event.usage.totalTokens state.jobTotalTokens += event.usage.totalTokens + state.jobReasoningTokens += event.usage.reasoningTokens + state.jobInputTokens += event.usage.inputTokens + state.jobCachedInputTokens += event.usage.cachedInputTokens + state.jobOutputTokens += event.usage.outputTokens } return true } @@ -331,6 +351,10 @@ export function processDelegationTreeEvent( node.actionFileArg = undefined node.totalTokens += event.usage.totalTokens state.jobTotalTokens += event.usage.totalTokens + state.jobReasoningTokens += event.usage.reasoningTokens + state.jobInputTokens += event.usage.inputTokens + state.jobCachedInputTokens += event.usage.cachedInputTokens + state.jobOutputTokens += event.usage.outputTokens if (event.checkpoint.contextWindowUsage !== undefined) { node.contextWindowUsage = event.checkpoint.contextWindowUsage } @@ -371,6 +395,10 @@ export function processDelegationTreeEvent( if (node) { node.totalTokens += event.usage.totalTokens state.jobTotalTokens += event.usage.totalTokens + state.jobReasoningTokens += event.usage.reasoningTokens + state.jobInputTokens += event.usage.inputTokens + state.jobCachedInputTokens += event.usage.cachedInputTokens + state.jobOutputTokens += event.usage.outputTokens } return true } @@ -400,5 +428,17 @@ export function useDelegationTree() { processEvent, statusCounts: getStatusCounts(state), formattedTotalTokens: formatTokenCount(state.jobTotalTokens), + formattedReasoningTokens: formatTokenCount(state.jobReasoningTokens), + formattedInputTokens: formatTokenCount(state.jobInputTokens), + formattedCachedInputTokens: formatTokenCount(state.jobCachedInputTokens), + formattedOutputTokens: formatTokenCount(state.jobOutputTokens), + providerName: state.providerName, + cacheHitRate: + state.jobInputTokens + state.jobCachedInputTokens > 0 + ? ( + (state.jobCachedInputTokens / (state.jobInputTokens + state.jobCachedInputTokens)) * + 100 + ).toFixed(2) + : "0.00", } } diff --git a/packages/tui-components/src/execution/hooks/use-execution-state.ts b/packages/tui-components/src/execution/hooks/use-execution-state.ts index 673f46ec..ea2490c1 100644 --- a/packages/tui-components/src/execution/hooks/use-execution-state.ts +++ b/packages/tui-components/src/execution/hooks/use-execution-state.ts @@ -40,6 +40,12 @@ export type ExecutionState = { runningCount: number waitingCount: number formattedTotalTokens: string + formattedReasoningTokens: string + formattedInputTokens: string + formattedCachedInputTokens: string + formattedOutputTokens: string + providerName: string | undefined + cacheHitRate: string elapsedTime: string } @@ -173,6 +179,12 @@ export const useExecutionState = (options: UseExecutionStateOptions): ExecutionS runningCount: delegationTree.statusCounts.running, waitingCount: delegationTree.statusCounts.waiting, formattedTotalTokens: delegationTree.formattedTotalTokens, + formattedReasoningTokens: delegationTree.formattedReasoningTokens, + formattedInputTokens: delegationTree.formattedInputTokens, + formattedCachedInputTokens: delegationTree.formattedCachedInputTokens, + formattedOutputTokens: delegationTree.formattedOutputTokens, + providerName: delegationTree.providerName, + cacheHitRate: delegationTree.cacheHitRate, elapsedTime, } }