diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 17:46:53 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 17:46:53 +0900 |
| commit | 7c459c7d919d1e08a228e8abc56129be174d8abe (patch) | |
| tree | 93011125c001945723ac9b9358c4ddd450f87f72 /packages/session-orchestrator/src | |
| parent | 5746cf4e545cd5b0d7faf0595554f273f236f3a9 (diff) | |
| download | dispatch-7c459c7d919d1e08a228e8abc56129be174d8abe.tar.gz dispatch-7c459c7d919d1e08a228e8abc56129be174d8abe.zip | |
feat(wire,kernel,session-orchestrator): live turn metrics on the stream
Expose the backend's authoritative token+timing metrics on the live AgentEvent
stream (observability-only -> now also client-facing). All additive/optional.
- [email protected]: new TurnStepCompleteEvent (type:step-complete) with per-step
ttftMs/decodeMs/genTotalMs; usage += stepId; tool-result += durationMs (exec);
done += durationMs (turn wall-clock) + usage (turn total). RunTurnInput += now?.
[email protected] (re-export bump).
- kernel-runtime: when now injected, measures + emits the above (reuses the
ttft/decode first-token detection); omits timing gracefully without a clock.
- session-orchestrator: adds now? to deps, threads into RunTurnInput; extension
activate injects () => Date.now().
- transport/cli/host-bin: untouched (verbatim pass-through; additive fields).
FE handoff: frontend-metrics-handoff.md. typecheck clean; 520 vitest + 89 bun;
biome 0/0. Replay/persistence = deferred Pass 2 (documented in tasks.md).
Diffstat (limited to 'packages/session-orchestrator/src')
| -rw-r--r-- | packages/session-orchestrator/src/extension.ts | 1 | ||||
| -rw-r--r-- | packages/session-orchestrator/src/orchestrator.test.ts | 47 | ||||
| -rw-r--r-- | packages/session-orchestrator/src/orchestrator.ts | 3 |
3 files changed, 51 insertions, 0 deletions
diff --git a/packages/session-orchestrator/src/extension.ts b/packages/session-orchestrator/src/extension.ts index af7d6e0..fbb7c15 100644 --- a/packages/session-orchestrator/src/extension.ts +++ b/packages/session-orchestrator/src/extension.ts @@ -38,6 +38,7 @@ export function activate(host: HostAPI): void { }, runTurn, logger: host.logger, + now: () => Date.now(), }); host.provideService(sessionOrchestratorHandle, orchestrator); diff --git a/packages/session-orchestrator/src/orchestrator.test.ts b/packages/session-orchestrator/src/orchestrator.test.ts index b648d42..3954ffe 100644 --- a/packages/session-orchestrator/src/orchestrator.test.ts +++ b/packages/session-orchestrator/src/orchestrator.test.ts @@ -363,6 +363,53 @@ describe("handleMessage model resolution", () => { expect(captured).toHaveLength(2); expect(captured[1]?.cwd).toBeUndefined(); }); + + it("forwards an injected now into the RunTurnInput passed to runTurn", async () => { + const store = createInMemoryStore(); + const provider: ProviderContract = { id: "p", stream: async function* () {} }; + const { captured, captureRunTurn } = createCapturingRunTurn(); + const fakeNow = () => 42; + + const orchestrator = createSessionOrchestrator({ + conversationStore: store, + resolveProvider: () => provider, + resolveTools: () => [], + runTurn: captureRunTurn, + now: fakeNow, + }); + + await orchestrator.handleMessage({ + conversationId: "conv-now", + text: "hi", + onEvent: () => {}, + }); + + expect(captured).toHaveLength(1); + expect(captured[0]?.now).toBe(fakeNow); + expect(captured[0]?.now?.()).toBe(42); + }); + + it("omits now from RunTurnInput when deps.now is not provided", async () => { + const store = createInMemoryStore(); + const provider: ProviderContract = { id: "p", stream: async function* () {} }; + const { captured, captureRunTurn } = createCapturingRunTurn(); + + const orchestrator = createSessionOrchestrator({ + conversationStore: store, + resolveProvider: () => provider, + resolveTools: () => [], + runTurn: captureRunTurn, + }); + + await orchestrator.handleMessage({ + conversationId: "conv-no-now", + text: "hi", + onEvent: () => {}, + }); + + expect(captured).toHaveLength(1); + expect(captured[0]?.now).toBeUndefined(); + }); }); describe("turn-sealed event", () => { diff --git a/packages/session-orchestrator/src/orchestrator.ts b/packages/session-orchestrator/src/orchestrator.ts index 311b620..04f6ad2 100644 --- a/packages/session-orchestrator/src/orchestrator.ts +++ b/packages/session-orchestrator/src/orchestrator.ts @@ -39,6 +39,8 @@ export interface SessionOrchestratorDeps { readonly runTurn: (input: RunTurnInput) => Promise<RunTurnResult>; /** Base logger (auto-scoped to this extension); childed per turn for span capture. */ readonly logger?: Logger; + /** Injected monotonic-ish clock (ms) forwarded to RunTurnInput for timing events. */ + readonly now?: () => number; } export function createSessionOrchestrator(deps: SessionOrchestratorDeps): SessionOrchestrator { @@ -86,6 +88,7 @@ export function createSessionOrchestrator(deps: SessionOrchestratorDeps): Sessio ...(turnLogger !== undefined ? { logger: turnLogger } : {}), ...(signal !== undefined ? { signal } : {}), ...(cwd !== undefined ? { cwd } : {}), + ...(deps.now !== undefined ? { now: deps.now } : {}), }; const result = await deps.runTurn(opts); |
