summaryrefslogtreecommitdiffhomepage
path: root/packages/session-orchestrator/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 17:46:53 +0900
committerAdam Malczewski <[email protected]>2026-06-07 17:46:53 +0900
commit7c459c7d919d1e08a228e8abc56129be174d8abe (patch)
tree93011125c001945723ac9b9358c4ddd450f87f72 /packages/session-orchestrator/src
parent5746cf4e545cd5b0d7faf0595554f273f236f3a9 (diff)
downloaddispatch-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.ts1
-rw-r--r--packages/session-orchestrator/src/orchestrator.test.ts47
-rw-r--r--packages/session-orchestrator/src/orchestrator.ts3
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);