feat: expose worker model and context metadata

This commit is contained in:
2026-10-02 15:29:45 +09:00
parent 43e3b42055
commit cf3cf4b766
25 changed files with 834 additions and 57 deletions
File diff suppressed because one or more lines are too long
+30 -9
View File
@@ -200,21 +200,42 @@ export type SubmissionDisposition = "started" | "queued";
export type SessionSnapshot = { pending_submissions: PendingSubmissionsSnapshot, entries: Array<SessionSnapshotEntry>, };
export type ReasoningConfig = { "kind": "effort", effort: string, } | { "kind": "budget_tokens", budget_tokens: number, };
export type ContextTokenSource = "measured" | "estimated";
export type ContextUsage = { tokens: number, source: ContextTokenSource, };
export type Greeting = { worker_name: string, cwd: string, provider: string, model: string,
/**
* Effective reasoning control. Absent means unspecified or unavailable.
*/
reasoning?: ReasoningConfig | null, scope_summary: string, tools: Array<string>,
/**
* Resolved effective model context window in tokens. Legacy producers use
* zero when this is unavailable; clients must treat that as unknown.
*/
context_window: number,
/**
* Legacy numeric mirror retained for mixed-version clients.
*/
context_tokens: number,
/**
* Typed current-context value. Older snapshots omit it; clients may fall
* back to `context_tokens` only when `context_window` is known.
*/
context_usage?: ContextUsage | null, };
export type InternalWorkerKind = "sub_worker" | { "service": { kind: string, } };
export type InternalWorkerRef = { session_id: string, name: string, parent_session_id?: string | null, kind: InternalWorkerKind, };
export type InternalWorkerSnapshot = { worker: InternalWorkerRef, revision: number, session: SessionSnapshot, status: WorkerStatus, error?: string | null, in_flight?: InFlightSnapshot, internal_workers?: Array<InternalWorkerSnapshot>, };
export type Greeting = { worker_name: string, cwd: string, provider: string, model: string, scope_summary: string, tools: Array<string>,
export type InternalWorkerSnapshot = { worker: InternalWorkerRef, revision: number, session: SessionSnapshot,
/**
* Model context window in tokens. Always filled by the Worker greeting.
* Public execution metadata for this child. Older snapshots and service
* workers may omit it; clients must never substitute the parent's values.
*/
context_window: number,
/**
* Estimated current session context tokens at connect time.
*/
context_tokens: number, };
greeting?: Greeting | null, status: WorkerStatus, error?: string | null, in_flight?: InFlightSnapshot, internal_workers?: Array<InternalWorkerSnapshot>, };
export type Alert = { level: AlertLevel, source: AlertSource, message: string,
/**
@@ -0,0 +1,42 @@
// @vitest-environment happy-dom
import { cleanup, render } from "@testing-library/svelte";
import { afterEach, expect, test } from "vitest";
import WorkerContextStatus from "./WorkerContextStatus.svelte";
afterEach(cleanup);
test("renders resolved model, reasoning, and measured context accessibly", () => {
const view = render(WorkerContextStatus, {
metadata: {
model: "gpt-6-astra",
reasoning: { kind: "effort", effort: "high" },
contextWindow: 272_000,
contextTokens: 142_000,
contextSource: "measured",
},
});
const status = view.getByLabelText("Worker model and context");
expect(status.textContent).toContain("gpt-6-astra · high");
expect(status.textContent).toContain("Context 142.0k / 272.0k (52%)");
});
test("renders honest unavailable state instead of a fabricated zero percent", async () => {
const view = render(WorkerContextStatus, { metadata: null });
expect(view.getByText("Model unavailable")).not.toBeNull();
expect(view.getByText("Context unavailable")).not.toBeNull();
expect(view.container.textContent).not.toContain("0%");
await view.rerender({
metadata: {
model: "claude",
reasoning: { kind: "budget_tokens", budget_tokens: 8_192 },
contextWindow: 64_000,
contextTokens: 12_000,
contextSource: "estimated",
},
});
expect(view.container.textContent).toContain("claude · 8.2k token budget");
expect(view.container.textContent).toContain("Context ~12.0k / 64.0k (19%)");
});
@@ -0,0 +1,69 @@
<script lang="ts">
import type { ConsoleWorkerMetadata } from "./model";
import {
formatContextSummary,
formatModelSummary,
} from "./worker-metadata";
type Props = {
metadata: ConsoleWorkerMetadata | null;
};
let { metadata }: Props = $props();
const modelSummary = $derived(formatModelSummary(metadata));
const contextSummary = $derived(formatContextSummary(metadata));
</script>
<div class="worker-context-status" aria-label="Worker model and context">
<span class="worker-model" title={modelSummary}>{modelSummary}</span>
<span class="separator" aria-hidden="true">|</span>
<span class="worker-context" title={contextSummary}>{contextSummary}</span>
</div>
<style>
.worker-context-status {
display: flex;
min-width: 0;
align-items: center;
gap: var(--space-2);
color: var(--text-muted);
font-family: var(--font-mono);
font-size: var(--font-size-compact);
line-height: var(--line-height-compact);
font-variant-numeric: tabular-nums;
}
.worker-model,
.worker-context {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.worker-model {
color: var(--text);
}
.separator {
flex: 0 0 auto;
color: var(--line-strong);
}
@media (max-width: 640px) {
.worker-context-status {
width: 100%;
flex-wrap: wrap;
row-gap: 0;
}
.separator {
display: none;
}
.worker-model,
.worker-context {
flex: 1 1 100%;
}
}
</style>
@@ -60,7 +60,7 @@
<span aria-hidden="true">・</span>
<span>{requests} {requestLabel}</span>
<span aria-hidden="true">|</span>
<span>↑{formatRunTokens(uploadTokens)}/↓{formatRunTokens(outputTokens)}</span>
<span>Run ↑{formatRunTokens(uploadTokens)}/↓{formatRunTokens(outputTokens)}</span>
</div>
<style>
@@ -210,6 +210,77 @@ function semanticOutput(lines: readonly ConsoleLine[]) {
}));
}
Deno.test("snapshot and usage keep current context separate from run traffic", () => {
const snapshot = snapshotEvent("/repo") as SnapshotEvent;
snapshot.data.greeting.model = "gpt-6-astra";
snapshot.data.greeting.reasoning = { kind: "effort", effort: "high" };
snapshot.data.greeting.context_window = 272_000;
snapshot.data.greeting.context_tokens = 1;
snapshot.data.greeting.context_usage = { tokens: 142_000, source: "estimated" };
const projection = projectConsole([
{ eventId: "snapshot", event: snapshot, observedAtMs: 1 },
{ eventId: "usage", event: { event: "usage", data: {
input_tokens: 150_000,
cache_read_input_tokens: 140_000,
output_tokens: 2_000,
} }, observedAtMs: 2 },
]);
assertEquals(projection.workerMetadata, {
model: "gpt-6-astra",
reasoning: { kind: "effort", effort: "high" },
contextWindow: 272_000,
contextTokens: 150_000,
contextSource: "measured",
});
assertEquals(projection.runActivity.uploadTokens, 10_000);
assertEquals(projection.runActivity.outputTokens, 2_000);
});
Deno.test("snapshot metadata supports budget reasoning and legacy context fallback", () => {
const snapshot = snapshotEvent("/repo") as SnapshotEvent;
snapshot.data.greeting.reasoning = { kind: "budget_tokens", budget_tokens: 16_384 };
delete snapshot.data.greeting.context_usage;
const projection = projectConsole([{ eventId: "snapshot", event: snapshot }]);
assertEquals(projection.workerMetadata, {
model: "model",
reasoning: { kind: "budget_tokens", budget_tokens: 16_384 },
contextWindow: 100,
contextTokens: 20,
contextSource: "estimated",
});
});
Deno.test("unknown and destructive boundaries never fabricate context percentages", () => {
const retained = snapshotEvent("") as SnapshotEvent;
retained.data.greeting.model = "";
retained.data.greeting.context_window = 0;
retained.data.greeting.context_tokens = 0;
delete retained.data.greeting.context_usage;
let projection = projectConsole([{ eventId: "retained", event: retained }]);
assertEquals(projection.workerMetadata, {
model: null,
reasoning: null,
contextWindow: null,
contextTokens: null,
contextSource: null,
});
const live = snapshotEvent("/repo") as SnapshotEvent;
live.data.greeting.context_usage = { tokens: 20, source: "measured" };
projection = projectConsole([
{ eventId: "live", event: live },
{ eventId: "rewind", event: { event: "rewind_applied", data: {
session: canonicalSession([]),
input: [],
summary: { truncated_to_entries: 0, discarded_entries: 1, tool_side_effect_warning: false },
} } },
]);
assertEquals(projection.workerMetadata?.contextTokens, null);
assertEquals(projection.workerMetadata?.contextSource, null);
});
Deno.test("large paste segments project compact artifact metadata", () => {
const body = "secret pasted body";
const text = segmentsToText([{
@@ -2977,6 +3048,18 @@ Deno.test("Internal Worker snapshot output continues without entering the parent
worker,
revision: 4,
session: canonicalSession([]),
greeting: {
worker_name: "research",
cwd: "/child",
provider: "anthropic",
model: "child-model",
reasoning: { kind: "budget_tokens", budget_tokens: 8_192 },
scope_summary: "child scope",
tools: [],
context_window: 64_000,
context_tokens: 12_000,
context_usage: { tokens: 12_000, source: "measured" },
},
status: "running",
in_flight: {
blocks: [{ kind: "text", text: "**chi", finished: false }],
@@ -2991,6 +3074,14 @@ Deno.test("Internal Worker snapshot output continues without entering the parent
projection.internalWorkers[0].console.lines.map((line) => line.body),
["**chi"],
);
assertEquals(projection.workerMetadata?.model, "model");
assertEquals(projection.internalWorkers[0].console.workerMetadata, {
model: "child-model",
reasoning: { kind: "budget_tokens", budget_tokens: 8_192 },
contextWindow: 64_000,
contextTokens: 12_000,
contextSource: "measured",
});
projection = projector.append([
{
@@ -4,11 +4,13 @@ import type {
CommandSnapshot,
CommandStreamSlice,
Event as ProtocolEvent,
Greeting,
InFlightBlock,
InFlightCompaction,
InFlightToolCallState,
InternalWorkerRef,
InternalWorkerSnapshot,
ReasoningConfig,
Segment,
SessionSnapshotEntry,
WorkerState,
@@ -164,6 +166,14 @@ export function resolveConsoleWorkerView(
return views.find((view) => view.sessionId === selectedSessionId) ?? views[0];
}
export type ConsoleWorkerMetadata = {
model: string | null;
reasoning: ReasoningConfig | null;
contextWindow: number | null;
contextTokens: number | null;
contextSource: "measured" | "estimated" | null;
};
export type ConsoleProjection = {
lines: ConsoleLine[];
tasks: ConsoleTask[];
@@ -174,6 +184,7 @@ export type ConsoleProjection = {
usage: string | null;
runActivity: RunActivityStats;
cwd: string | null;
workerMetadata: ConsoleWorkerMetadata | null;
lastEventId: string | null;
internalWorkers: InternalWorkerProjection[];
/** Terminal child-session fences, reset only by an authoritative snapshot. */
@@ -204,6 +215,57 @@ function workerStatusFromState(snapshot: WorkerStateSnapshot): WorkerStatus {
return "running";
}
function safeTokenCount(value: unknown, allowZero = true): number | null {
return typeof value === "number" && Number.isSafeInteger(value) &&
(allowZero ? value >= 0 : value > 0)
? value
: null;
}
function metadataFromGreeting(greeting: Greeting): ConsoleWorkerMetadata {
const contextWindow = safeTokenCount(greeting.context_window, false);
const typedTokens = safeTokenCount(greeting.context_usage?.tokens);
const legacyTokens = contextWindow === null
? null
: safeTokenCount(greeting.context_tokens);
const model = greeting.model.trim();
return {
model: model.length > 0 ? model : null,
reasoning: greeting.reasoning ?? null,
contextWindow,
contextTokens: typedTokens ?? legacyTokens,
contextSource: typedTokens !== null
? greeting.context_usage?.source ?? null
: legacyTokens !== null
? "estimated"
: null,
};
}
function metadataWithUsage(
metadata: ConsoleWorkerMetadata | null,
inputTokens: number | null | undefined,
): ConsoleWorkerMetadata {
const tokens = safeTokenCount(inputTokens);
return {
model: metadata?.model ?? null,
reasoning: metadata?.reasoning ?? null,
contextWindow: metadata?.contextWindow ?? null,
contextTokens: tokens,
contextSource: tokens === null ? null : "measured",
};
}
function metadataWithoutContext(
metadata: ConsoleWorkerMetadata | null,
): ConsoleWorkerMetadata | null {
return metadata && {
...metadata,
contextTokens: null,
contextSource: null,
};
}
export function emptyConsoleProjection(): ConsoleProjection {
return {
lines: [],
@@ -215,6 +277,7 @@ export function emptyConsoleProjection(): ConsoleProjection {
usage: null,
runActivity: emptyRunActivityStats(),
cwd: null,
workerMetadata: null,
lastEventId: null,
internalWorkers: [],
removedInternalWorkers: {},
@@ -779,6 +842,9 @@ function projectInternalWorkerSnapshot(
cwd,
);
console.status = snapshot.status;
console.workerMetadata = snapshot.greeting
? metadataFromGreeting(snapshot.greeting)
: null;
appendSnapshotInFlightLines(
console,
snapshot.in_flight?.blocks ?? [],
@@ -951,6 +1017,7 @@ export function applyProtocolEvent(
envelope.observedAtMs ?? 0,
),
cwd: projection.cwd,
workerMetadata: projection.workerMetadata,
lastEventId: envelope.eventId,
internalWorkers: [...projection.internalWorkers],
removedInternalWorkers: { ...projection.removedInternalWorkers },
@@ -1055,6 +1122,10 @@ export function applyProtocolEvent(
break;
case "usage":
next.usage = usageText(event.data);
next.workerMetadata = metadataWithUsage(
next.workerMetadata,
event.data.input_tokens,
);
break;
case "error":
if (hasDurableRunFailure(next, event.data.message)) break;
@@ -1072,6 +1143,7 @@ export function applyProtocolEvent(
break;
case "snapshot": {
next.cwd = event.data.greeting.cwd;
next.workerMetadata = metadataFromGreeting(event.data.greeting);
const snapshot = snapshotProjectionFromSession(
envelope.eventId,
event.data.session,
@@ -1171,6 +1243,7 @@ export function applyProtocolEvent(
next.lines = [...segment.lines, ...retainedErrors, ...retainedCompaction];
next.tasks = segment.tasks;
next.taskNextId = segment.taskNextId;
next.workerMetadata = metadataWithoutContext(next.workerMetadata);
break;
}
case "invoke_start":
@@ -1196,21 +1269,25 @@ export function applyProtocolEvent(
case "memory_worker":
case "completions":
case "rewind_targets":
case "rewind_applied":
case "workers_listed":
case "worker_restored":
case "peer_registered":
// These are protocol/status/control events. TUI Console does not append
// them to the conversation surface; browser Console should not either.
break;
case "rewind_applied":
next.workerMetadata = metadataWithoutContext(next.workerMetadata);
break;
case "compaction_progress":
return applyInFlightCompaction(next, event.data.compaction ?? null);
case "compact_start":
case "compact_done":
case "compact_failed":
// Durable lifecycle events are retained only as historical protocol
// compatibility. Runtime progress is the sole Console block authority.
break;
case "compact_done":
next.workerMetadata = metadataWithoutContext(next.workerMetadata);
break;
case "shutdown":
next.status = "shutdown";
break;
@@ -2182,6 +2259,7 @@ function snapshotProjectionFromSession(
usage: null,
runActivity: emptyRunActivityStats(),
cwd,
workerMetadata: null,
lastEventId: eventId,
internalWorkers: [],
removedInternalWorkers: {},
@@ -8,6 +8,11 @@ import {
resolveCompactionStatusPresentation,
visibleCompactionProgress,
} from "./run-status.ts";
import {
formatContextSummary,
formatModelSummary,
formatReasoning,
} from "./worker-metadata.ts";
function assertEquals(actual: unknown, expected: unknown): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
@@ -17,6 +22,57 @@ function assertEquals(actual: unknown, expected: unknown): void {
}
}
Deno.test("worker metadata formats effort, budget, estimate, and unknown safely", () => {
assertEquals(
formatModelSummary({
model: "gpt-6-astra",
reasoning: { kind: "effort", effort: "high" },
contextWindow: 272_000,
contextTokens: 142_000,
contextSource: "measured",
}),
"gpt-6-astra · high",
);
assertEquals(
formatReasoning({ kind: "budget_tokens", budget_tokens: 16_384 }),
"16.4k token budget",
);
assertEquals(
formatContextSummary({
model: "model",
reasoning: null,
contextWindow: 272_000,
contextTokens: 142_000,
contextSource: "estimated",
}),
"Context ~142.0k / 272.0k (52%)",
);
assertEquals(formatModelSummary(null), "Model unavailable");
assertEquals(
formatReasoning({ kind: "effort", effort: "" }),
"reasoning unavailable",
);
assertEquals(
formatReasoning({ kind: "budget_tokens", budget_tokens: Number.NaN }),
"reasoning unavailable",
);
assertEquals(
formatReasoning({ kind: "budget_tokens", budget_tokens: -1 }),
"reasoning unavailable",
);
assertEquals(formatContextSummary(null), "Context unavailable");
assertEquals(
formatContextSummary({
model: null,
reasoning: null,
contextWindow: 0,
contextTokens: 0,
contextSource: null,
}),
"Context unavailable",
);
});
Deno.test("run activity follows TUI request and net-token accounting", () => {
let stats = applyRunActivityEvent(
emptyRunActivityStats(),
@@ -720,7 +720,8 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps"
Deno.test("Console spacing and text metrics use existing design tokens", async () => {
const files = [
"ComposerInput.svelte", "ConsoleLineItem.svelte", "RichMarkdown.svelte",
"ConsoleTasks.svelte", "WorkerRunStatus.svelte", "ConsoleDisplayState.svelte",
"ConsoleTasks.svelte", "WorkerRunStatus.svelte", "WorkerContextStatus.svelte",
"ConsoleDisplayState.svelte",
"ConsoleTurnNavigation.svelte",
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
];
@@ -0,0 +1,43 @@
import type { ReasoningConfig } from "$lib/generated/protocol";
import type { ConsoleWorkerMetadata } from "./model";
import { formatRunTokens } from "./run-status";
export function formatReasoning(reasoning: ReasoningConfig | null): string {
if (!reasoning) return "reasoning unavailable";
if (reasoning.kind === "effort") {
const effort = reasoning.effort.trim();
return effort.length > 0 ? effort : "reasoning unavailable";
}
if (
!Number.isSafeInteger(reasoning.budget_tokens) ||
reasoning.budget_tokens <= 0
) {
return "reasoning unavailable";
}
return `${formatRunTokens(reasoning.budget_tokens)} token budget`;
}
export function formatModelSummary(
metadata: ConsoleWorkerMetadata | null,
): string {
if (!metadata?.model) return "Model unavailable";
return `${metadata.model} · ${formatReasoning(metadata.reasoning)}`;
}
export function formatContextSummary(
metadata: ConsoleWorkerMetadata | null,
): string {
const used = metadata?.contextTokens;
const limit = metadata?.contextWindow;
if (
used === null || used === undefined || limit === null ||
limit === undefined || !Number.isSafeInteger(used) ||
!Number.isSafeInteger(limit) || used < 0 || limit <= 0
) return "Context unavailable";
const percent = Math.round((used / limit) * 100);
if (!Number.isFinite(percent)) return "Context unavailable";
const estimate = metadata?.contextSource === "estimated" ? "~" : "";
return `Context ${estimate}${formatRunTokens(used)} / ${
formatRunTokens(limit)
} (${percent}%)`;
}
@@ -21,6 +21,7 @@
} from "$lib/workspace/console/composer-command";
import { FileCompletions } from "$lib/workspace/console/file-completions";
import WorkerRunStatus from "$lib/workspace/console/WorkerRunStatus.svelte";
import WorkerContextStatus from "$lib/workspace/console/WorkerContextStatus.svelte";
import { resolveWorkerControlShortcut } from "$lib/workspace/console/worker-control-shortcuts";
import {
consoleWorkerViews,
@@ -1651,6 +1652,7 @@
<div class="console-shell worker-console-shell">
<section class="console-header card" aria-label="Worker controls">
<WorkerContextStatus metadata={selectedConsoleProjection.workerMetadata} />
<div class="console-header-actions">
<div
class="console-view-modes"
@@ -2119,8 +2121,9 @@
.console-header {
display: flex;
align-items: flex-start;
justify-content: flex-end;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
@@ -2133,6 +2136,7 @@
.console-header-actions {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
@@ -2670,7 +2674,12 @@
}
.console-header {
align-items: stretch;
flex-direction: column;
}
.console-header-actions {
align-self: flex-end;
}
}
</style>