Archived
refactor: type session event protocol
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# 06. Type the session event protocol
|
# 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.
|
Define a shared discriminated union for session UI events and use it across server event publishing, sockets, and transcript handling.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { globalSessionEvents, sessionEvents } from "./api";
|
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 {
|
export class SessionSocket {
|
||||||
private socket: WebSocket | undefined;
|
private socket: WebSocket | undefined;
|
||||||
@@ -63,12 +63,12 @@ export class SessionSocket {
|
|||||||
|
|
||||||
export class GlobalSessionSocket {
|
export class GlobalSessionSocket {
|
||||||
private socket: WebSocket | undefined;
|
private socket: WebSocket | undefined;
|
||||||
private onEvent: ((event: Extract<SessionUiEvent, { type: "status.update" | "activity.update" }>) => void) | undefined;
|
private onEvent: ((event: GlobalSessionEvent) => void) | undefined;
|
||||||
private reconnectTimer?: number;
|
private reconnectTimer?: number;
|
||||||
private reconnectDelay = 500;
|
private reconnectDelay = 500;
|
||||||
private shouldReconnect = false;
|
private shouldReconnect = false;
|
||||||
|
|
||||||
connect(onEvent: (event: Extract<SessionUiEvent, { type: "status.update" | "activity.update" }>) => void): void {
|
connect(onEvent: (event: GlobalSessionEvent) => void): void {
|
||||||
this.close();
|
this.close();
|
||||||
this.onEvent = onEvent;
|
this.onEvent = onEvent;
|
||||||
this.shouldReconnect = true;
|
this.shouldReconnect = true;
|
||||||
@@ -114,10 +114,10 @@ export class GlobalSessionSocket {
|
|||||||
|
|
||||||
function isSessionUiEvent(event: unknown): event is SessionUiEvent {
|
function isSessionUiEvent(event: unknown): event is SessionUiEvent {
|
||||||
const type = eventType(event);
|
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<SessionUiEvent, { type: "status.update" | "activity.update" }> {
|
function isGlobalSessionEvent(event: unknown): event is GlobalSessionEvent {
|
||||||
const type = eventType(event);
|
const type = eventType(event);
|
||||||
return type === "status.update" || type === "activity.update";
|
return type === "status.update" || type === "activity.update";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes.js";
|
||||||
import type { WebSocket } from "ws";
|
import type { WebSocket } from "ws";
|
||||||
|
|
||||||
export class SessionEventHub {
|
export class SessionEventHub {
|
||||||
@@ -21,14 +22,14 @@ export class SessionEventHub {
|
|||||||
socket.on("close", () => this.globalSockets.delete(socket));
|
socket.on("close", () => this.globalSockets.delete(socket));
|
||||||
}
|
}
|
||||||
|
|
||||||
publish(sessionId: string, event: unknown): void {
|
publish(sessionId: string, event: SessionUiEvent): void {
|
||||||
const payload = JSON.stringify(event);
|
const payload = JSON.stringify(event);
|
||||||
for (const socket of this.socketsBySession.get(sessionId) ?? []) {
|
for (const socket of this.socketsBySession.get(sessionId) ?? []) {
|
||||||
if (socket.readyState === socket.OPEN) socket.send(payload);
|
if (socket.readyState === socket.OPEN) socket.send(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
publishGlobal(event: unknown): void {
|
publishGlobal(event: GlobalSessionEvent): void {
|
||||||
const payload = JSON.stringify(event);
|
const payload = JSON.stringify(event);
|
||||||
for (const socket of this.globalSockets) {
|
for (const socket of this.globalSockets) {
|
||||||
if (socket.readyState === socket.OPEN) socket.send(payload);
|
if (socket.readyState === socket.OPEN) socket.send(payload);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type AgentSession,
|
type AgentSession,
|
||||||
type CreateAgentSessionRuntimeFactory,
|
type CreateAgentSessionRuntimeFactory,
|
||||||
} from "@earendil-works/pi-coding-agent";
|
} 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 type { SessionEventHub } from "../realtime/sessionEventHub.js";
|
||||||
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
||||||
import { SessionCommandService } from "./sessionCommandService.js";
|
import { SessionCommandService } from "./sessionCommandService.js";
|
||||||
@@ -141,10 +141,10 @@ export class PiSessionService {
|
|||||||
this.events.publish(session.sessionId, {
|
this.events.publish(session.sessionId, {
|
||||||
type: "shell.end",
|
type: "shell.end",
|
||||||
output: result.output,
|
output: result.output,
|
||||||
exitCode: result.exitCode,
|
...(result.exitCode === undefined ? {} : { exitCode: result.exitCode }),
|
||||||
cancelled: result.cancelled,
|
cancelled: result.cancelled,
|
||||||
truncated: result.truncated,
|
truncated: result.truncated,
|
||||||
fullOutputPath: result.fullOutputPath,
|
...(result.fullOutputPath === undefined ? {} : { fullOutputPath: result.fullOutputPath }),
|
||||||
});
|
});
|
||||||
this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
|
this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
|
||||||
this.publishStatus(session);
|
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)));
|
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 eventType = getString(event, "type");
|
||||||
const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
|
const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
|
||||||
if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") {
|
if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") {
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export type {
|
|||||||
CommandResult as ClientCommandResult,
|
CommandResult as ClientCommandResult,
|
||||||
SessionActivity as ClientSessionActivity,
|
SessionActivity as ClientSessionActivity,
|
||||||
SessionUiEvent,
|
SessionUiEvent,
|
||||||
|
GlobalSessionEvent,
|
||||||
} from "../shared/apiTypes.js";
|
} from "../shared/apiTypes.js";
|
||||||
|
|||||||
@@ -132,12 +132,18 @@ export type CommandResult =
|
|||||||
|
|
||||||
export type SessionUiEvent =
|
export type SessionUiEvent =
|
||||||
| { type: "assistant.delta"; text: string }
|
| { type: "assistant.delta"; text: string }
|
||||||
| { type: "tool.start"; toolName: string; summary: string; args?: unknown }
|
| { type: "tool.start"; toolName: string; toolCallId: string; summary: string; args?: unknown }
|
||||||
| { type: "tool.end"; toolName: string; text: string; isError: boolean; content?: unknown }
|
| { type: "tool.end"; toolName: string; toolCallId: string; text: string; isError: boolean; content?: unknown }
|
||||||
| { type: "shell.start"; command: string; excludeFromContext?: boolean }
|
| { type: "shell.start"; command: string; excludeFromContext?: boolean }
|
||||||
| { type: "shell.chunk"; chunk: string }
|
| { type: "shell.chunk"; chunk: string }
|
||||||
| { type: "shell.end"; output?: string; exitCode?: number | null; cancelled?: boolean; truncated?: boolean; fullOutputPath?: string; isError?: boolean }
|
| { 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: "status.update"; status: SessionStatus }
|
||||||
| { type: "activity.update"; activity: SessionActivity }
|
| { type: "activity.update"; activity: SessionActivity }
|
||||||
| { type: "command.output"; level: "info" | "success" | "error"; message: string }
|
| { 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<SessionUiEvent, { type: "status.update" | "activity.update" }>;
|
||||||
|
|||||||
Reference in New Issue
Block a user