mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
This commit is contained in:
@@ -110,14 +110,21 @@ describe('<ComposerTokenStats />', () => {
|
||||
]);
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
const bd = screen.getByTestId('composer-token-breakdown');
|
||||
// Orchestrator = totals − sub-agents: tokens 600−240=360, cost 0.01−0.004=0.006.
|
||||
expect(within(bd).getByText('token.orchestrator')).toBeInTheDocument();
|
||||
expect(within(bd).getByText(/360/)).toBeInTheDocument();
|
||||
expect(within(bd).getByText(/\$0\.006/)).toBeInTheDocument();
|
||||
// Orchestrator row = totals − sub-agents: tokens 600−240=360, cost 0.01−0.004=0.006.
|
||||
// Scope to the row's <li> because the orchestrator-only context numerator (#4271)
|
||||
// is also 360 for a single turn, so an unscoped /360/ matches twice.
|
||||
const orchRow = within(bd).getByText('token.orchestrator').closest('li') as HTMLElement;
|
||||
expect(within(orchRow).getByText(/360/)).toBeInTheDocument();
|
||||
expect(within(orchRow).getByText(/\$0\.006/)).toBeInTheDocument();
|
||||
// Sub-agent row: 200 + 40 = 240 combined tokens, its own cost.
|
||||
expect(within(bd).getByText('researcher')).toBeInTheDocument();
|
||||
expect(within(bd).getByText(/240/)).toBeInTheDocument();
|
||||
expect(within(bd).getByText(/\$0\.004/)).toBeInTheDocument();
|
||||
const subRow = within(bd).getByText('researcher').closest('li') as HTMLElement;
|
||||
expect(within(subRow).getByText(/240/)).toBeInTheDocument();
|
||||
expect(within(subRow).getByText(/\$0\.004/)).toBeInTheDocument();
|
||||
// Context-usage row shows the orchestrator-only numerator (360), not the
|
||||
// combined 600 (parent + sub-agent), so the gauge can't overflow (#4271).
|
||||
const ctxRow = within(bd).getByText('token.popContext').closest('div') as HTMLElement;
|
||||
expect(within(ctxRow).getByText(/\b360\b/)).toBeInTheDocument();
|
||||
expect(within(ctxRow).queryByText(/\b600\b/)).toBeNull();
|
||||
});
|
||||
|
||||
it('reads the active thread bucket when a threadId is provided', () => {
|
||||
|
||||
@@ -114,6 +114,49 @@ describe('chatRuntimeSlice recordChatTurnUsage', () => {
|
||||
expect(subs.coder.inputTokens).toBe(80);
|
||||
});
|
||||
|
||||
it('excludes sub-agent tokens from the context gauge numerator (#4271)', () => {
|
||||
const store = makeStore();
|
||||
// Core sends combined parent+sub-agent turn totals; the gauge must reflect
|
||||
// the orchestrator thread's own window only, never the sum across agents.
|
||||
store.dispatch(
|
||||
recordChatTurnUsage({
|
||||
inputTokens: 1_000_000,
|
||||
outputTokens: 50_000,
|
||||
contextWindow: 1_000_000,
|
||||
subAgents: [
|
||||
{ agentId: 'researcher', inputTokens: 600_000, outputTokens: 30_000, costUsd: 0.6 },
|
||||
{ agentId: 'context_scout', inputTokens: 150_000, outputTokens: 10_000, costUsd: 0.15 },
|
||||
],
|
||||
})
|
||||
);
|
||||
const usage = store.getState().chatRuntime.sessionTokenUsage;
|
||||
// orchestrator-only = (1_000_000 + 50_000) - (600_000+30_000 + 150_000+10_000)
|
||||
expect(usage.lastTurnContextUsed).toBe(260_000);
|
||||
// Gauge stays within its window: 260_000 / 1_000_000 = 26% ≤ 100%.
|
||||
expect(usage.lastTurnContextUsed).toBeLessThanOrEqual(usage.contextWindow);
|
||||
});
|
||||
|
||||
it('keeps the full turn as the gauge numerator with no sub-agents (#4271)', () => {
|
||||
const store = makeStore();
|
||||
store.dispatch(
|
||||
recordChatTurnUsage({ inputTokens: 800, outputTokens: 120, contextWindow: 200_000 })
|
||||
);
|
||||
// Single-agent path is unchanged: nothing to subtract.
|
||||
expect(store.getState().chatRuntime.sessionTokenUsage.lastTurnContextUsed).toBe(920);
|
||||
});
|
||||
|
||||
it('clamps the gauge numerator to zero when sub-agents exceed the turn total (#4271)', () => {
|
||||
const store = makeStore();
|
||||
store.dispatch(
|
||||
recordChatTurnUsage({
|
||||
inputTokens: 50,
|
||||
outputTokens: 10,
|
||||
subAgents: [{ agentId: 'researcher', inputTokens: 100, outputTokens: 20, costUsd: 0.001 }],
|
||||
})
|
||||
);
|
||||
expect(store.getState().chatRuntime.sessionTokenUsage.lastTurnContextUsed).toBe(0);
|
||||
});
|
||||
|
||||
it('keeps the prior context window when a turn reports an unknown (0) window', () => {
|
||||
const store = makeStore();
|
||||
store.dispatch(
|
||||
|
||||
@@ -245,7 +245,12 @@ export interface SessionTokenUsage {
|
||||
* real value; the UI falls back to a default when unknown.
|
||||
*/
|
||||
contextWindow: number;
|
||||
/** Last turn's input+output tokens — the context-window gauge numerator. */
|
||||
/**
|
||||
* Last turn's **orchestrator-only** input+output tokens — the context-window
|
||||
* gauge numerator. Sub-agent spend is excluded so the gauge tracks the parent
|
||||
* thread's own window (each sub-agent runs in its own context window); summing
|
||||
* them in let the gauge exceed 100% in multi-agent sessions (#4271).
|
||||
*/
|
||||
lastTurnContextUsed: number;
|
||||
/** Per-sub-agent spend for the session, keyed by archetype id. */
|
||||
subAgents: Record<string, SubAgentUsage>;
|
||||
@@ -300,13 +305,20 @@ function applyTurnUsage(usage: SessionTokenUsage, payload: ChatTurnUsagePayload)
|
||||
usage.lastUpdated = Date.now();
|
||||
usage.lastTurnInputTokens = inTok;
|
||||
usage.lastTurnOutputTokens = outTok;
|
||||
usage.lastTurnContextUsed = inTok + outTok;
|
||||
// Only overwrite the known context window when the turn reported a real value
|
||||
// (>0); an unknown-window turn leaves the prior value intact.
|
||||
const ctxWindow = nonNeg(payload.contextWindow);
|
||||
if (ctxWindow > 0) usage.contextWindow = ctxWindow;
|
||||
// `inTok`/`outTok` are combined parent+sub-agent turn totals (the core sends
|
||||
// one number for cost), but the context window is the orchestrator model's
|
||||
// alone. Subtract this turn's sub-agent spend so the gauge numerator is the
|
||||
// orchestrator thread's own occupancy and can't overflow its window (#4271).
|
||||
let subTurnTokens = 0;
|
||||
for (const sub of payload.subAgents ?? []) {
|
||||
if (!sub || typeof sub.agentId !== 'string' || sub.agentId.length === 0) continue;
|
||||
const subIn = nonNeg(sub.inputTokens);
|
||||
const subOut = nonNeg(sub.outputTokens);
|
||||
subTurnTokens += subIn + subOut;
|
||||
const existing = usage.subAgents[sub.agentId] ?? {
|
||||
agentId: sub.agentId,
|
||||
inputTokens: 0,
|
||||
@@ -314,12 +326,13 @@ function applyTurnUsage(usage: SessionTokenUsage, payload: ChatTurnUsagePayload)
|
||||
costUsd: 0,
|
||||
runs: 0,
|
||||
};
|
||||
existing.inputTokens += nonNeg(sub.inputTokens);
|
||||
existing.outputTokens += nonNeg(sub.outputTokens);
|
||||
existing.inputTokens += subIn;
|
||||
existing.outputTokens += subOut;
|
||||
existing.costUsd += nonNeg(sub.costUsd);
|
||||
existing.runs += 1;
|
||||
usage.subAgents[sub.agentId] = existing;
|
||||
}
|
||||
usage.lastTurnContextUsed = Math.max(0, inTok + outTok - subTurnTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user