From 91957b0fe0dcc62669b6630dab5b6469aa56d3a9 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Thu, 7 May 2026 23:51:26 +0200 Subject: [PATCH] refactor: type session event protocol --- .ai-work/06-type-session-event-protocol.md | 2 +- src/client/src/sessionSocket.ts | 12 ++++++------ src/server/realtime/sessionEventHub.ts | 5 +++-- src/server/sessions/piSessionService.ts | 8 ++++---- src/server/types.ts | 1 + src/shared/apiTypes.ts | 12 +++++++++--- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.ai-work/06-type-session-event-protocol.md b/.ai-work/06-type-session-event-protocol.md index 9efea3d..ba69109 100644 --- a/.ai-work/06-type-session-event-protocol.md +++ b/.ai-work/06-type-session-event-protocol.md @@ -1,5 +1,5 @@ # 06. Type the session event protocol -Status: pending +Status: completed Define a shared discriminated union for session UI events and use it across server event publishing, sockets, and transcript handling. diff --git a/src/client/src/sessionSocket.ts b/src/client/src/sessionSocket.ts index d378dc4..55c35c0 100644 --- a/src/client/src/sessionSocket.ts +++ b/src/client/src/sessionSocket.ts @@ -1,7 +1,7 @@ import { globalSessionEvents, sessionEvents } from "./api"; -import type { SessionUiEvent } from "../../shared/apiTypes"; +import type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes"; -export type { SessionUiEvent } from "../../shared/apiTypes"; +export type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes"; export class SessionSocket { private socket: WebSocket | undefined; @@ -63,12 +63,12 @@ export class SessionSocket { export class GlobalSessionSocket { private socket: WebSocket | undefined; - private onEvent: ((event: Extract) => void) | undefined; + private onEvent: ((event: GlobalSessionEvent) => void) | undefined; private reconnectTimer?: number; private reconnectDelay = 500; private shouldReconnect = false; - connect(onEvent: (event: Extract) => void): void { + connect(onEvent: (event: GlobalSessionEvent) => void): void { this.close(); this.onEvent = onEvent; this.shouldReconnect = true; @@ -114,10 +114,10 @@ export class GlobalSessionSocket { function isSessionUiEvent(event: unknown): event is SessionUiEvent { const type = eventType(event); - return ["assistant.delta", "tool.start", "tool.end", "shell.start", "shell.chunk", "shell.end", "status.update", "activity.update", "command.output", "session.error"].includes(type); + return ["assistant.delta", "tool.start", "tool.end", "shell.start", "shell.chunk", "shell.end", "agent.start", "agent.end", "message.end", "status.update", "activity.update", "command.output", "session.error", "pi.event"].includes(type); } -function isGlobalSessionEvent(event: unknown): event is Extract { +function isGlobalSessionEvent(event: unknown): event is GlobalSessionEvent { const type = eventType(event); return type === "status.update" || type === "activity.update"; } diff --git a/src/server/realtime/sessionEventHub.ts b/src/server/realtime/sessionEventHub.ts index 548e36e..4ece958 100644 --- a/src/server/realtime/sessionEventHub.ts +++ b/src/server/realtime/sessionEventHub.ts @@ -1,3 +1,4 @@ +import type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes.js"; import type { WebSocket } from "ws"; export class SessionEventHub { @@ -21,14 +22,14 @@ export class SessionEventHub { socket.on("close", () => this.globalSockets.delete(socket)); } - publish(sessionId: string, event: unknown): void { + publish(sessionId: string, event: SessionUiEvent): void { const payload = JSON.stringify(event); for (const socket of this.socketsBySession.get(sessionId) ?? []) { if (socket.readyState === socket.OPEN) socket.send(payload); } } - publishGlobal(event: unknown): void { + publishGlobal(event: GlobalSessionEvent): void { const payload = JSON.stringify(event); for (const socket of this.globalSockets) { if (socket.readyState === socket.OPEN) socket.send(payload); diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index 2c1706d..d2999f9 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -9,7 +9,7 @@ import { type AgentSession, type CreateAgentSessionRuntimeFactory, } from "@earendil-works/pi-coding-agent"; -import type { ClientCommand, ClientCommandResult, ClientMessagePage, ClientSession, ClientSessionStatus } from "../types.js"; +import type { ClientCommand, ClientCommandResult, ClientMessagePage, ClientSession, ClientSessionStatus, SessionUiEvent } from "../types.js"; import type { SessionEventHub } from "../realtime/sessionEventHub.js"; import { BUILTIN_COMMANDS } from "./builtinCommands.js"; import { SessionCommandService } from "./sessionCommandService.js"; @@ -141,10 +141,10 @@ export class PiSessionService { this.events.publish(session.sessionId, { type: "shell.end", output: result.output, - exitCode: result.exitCode, + ...(result.exitCode === undefined ? {} : { exitCode: result.exitCode }), cancelled: result.cancelled, truncated: result.truncated, - fullOutputPath: result.fullOutputPath, + ...(result.fullOutputPath === undefined ? {} : { fullOutputPath: result.fullOutputPath }), }); this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command); this.publishStatus(session); @@ -348,7 +348,7 @@ function clampInteger(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, Math.floor(value))); } -function toClientEvent(event: unknown): unknown { +function toClientEvent(event: unknown): SessionUiEvent { const eventType = getString(event, "type"); const assistantMessageEvent = getProperty(event, "assistantMessageEvent"); if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") { diff --git a/src/server/types.ts b/src/server/types.ts index 1bb2024..200720e 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -10,4 +10,5 @@ export type { CommandResult as ClientCommandResult, SessionActivity as ClientSessionActivity, SessionUiEvent, + GlobalSessionEvent, } from "../shared/apiTypes.js"; diff --git a/src/shared/apiTypes.ts b/src/shared/apiTypes.ts index fb2b77c..d087a7e 100644 --- a/src/shared/apiTypes.ts +++ b/src/shared/apiTypes.ts @@ -132,12 +132,18 @@ export type CommandResult = export type SessionUiEvent = | { type: "assistant.delta"; text: string } - | { type: "tool.start"; toolName: string; summary: string; args?: unknown } - | { type: "tool.end"; toolName: string; text: string; isError: boolean; content?: unknown } + | { type: "tool.start"; toolName: string; toolCallId: string; summary: string; args?: unknown } + | { type: "tool.end"; toolName: string; toolCallId: string; text: string; isError: boolean; content?: unknown } | { type: "shell.start"; command: string; excludeFromContext?: boolean } | { type: "shell.chunk"; chunk: string } | { type: "shell.end"; output?: string; exitCode?: number | null; cancelled?: boolean; truncated?: boolean; fullOutputPath?: string; isError?: boolean } + | { type: "agent.start" } + | { type: "agent.end" } + | { type: "message.end" } | { type: "status.update"; status: SessionStatus } | { type: "activity.update"; activity: SessionActivity } | { type: "command.output"; level: "info" | "success" | "error"; message: string } - | { type: "session.error"; message: string }; + | { type: "session.error"; message: string } + | { type: "pi.event"; eventType: string }; + +export type GlobalSessionEvent = Extract;