feat(listen): add protocol_v2, move ws server to v2 (#1387)

Co-authored-by: Shubham Naik <shub@letta.com>
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-03-16 14:46:56 -07:00
committed by GitHub
parent 8ecf39798c
commit 3edaf91ee4
12 changed files with 4215 additions and 2106 deletions

View File

@@ -365,6 +365,35 @@ export type QueueItemKind =
| "approval_result"
| "overlay_action";
/**
* Canonical queue item wire shape used by listener state snapshots
* and queue lifecycle transport events.
*/
export interface QueueRuntimeItemWire {
/** Stable queue item identifier. */
id: string;
/** Correlates this queue item back to the originating client submit payload. */
client_message_id: string;
kind: QueueItemKind;
source: QueueItemSource;
/** Full queue item content; renderers may truncate for display. */
content: MessageCreate["content"] | string;
/** ISO8601 UTC enqueue timestamp. */
enqueued_at: string;
}
/**
* Queue item shape used by static queue_snapshot events.
* Includes legacy item_id for compatibility and allows optional expanded fields.
*/
export interface QueueSnapshotItem
extends Omit<Partial<QueueRuntimeItemWire>, "kind" | "source"> {
/** @deprecated Use `id` when present. */
item_id: string;
kind: QueueItemKind;
source: QueueItemSource;
}
/**
* Emitted synchronously when an item enters the queue.
* A queue item is a discrete, submitted unit of work (post-Enter for user
@@ -377,13 +406,13 @@ export interface QueueItemEnqueuedEvent extends MessageEnvelope {
/** @deprecated Use `id`. */
item_id: string;
/** Correlates this queue item back to the originating client submit payload. */
client_message_id: string;
client_message_id: QueueRuntimeItemWire["client_message_id"];
source: QueueItemSource;
kind: QueueItemKind;
/** Full queue item content; renderers may truncate for display. */
content?: MessageCreate["content"] | string;
content?: QueueRuntimeItemWire["content"];
/** ISO8601 UTC enqueue timestamp. */
enqueued_at?: string;
enqueued_at?: QueueRuntimeItemWire["enqueued_at"];
queue_len: number;
}
@@ -782,14 +811,7 @@ export interface TranscriptBackfillMessage extends MessageEnvelope {
export interface QueueSnapshotMessage extends MessageEnvelope {
type: "queue_snapshot";
/** Items currently in the queue, in enqueue order. */
items: Array<{
/** Stable queue item identifier. Preferred field. */
id?: string;
/** @deprecated Use `id`. */
item_id: string;
kind: QueueItemKind;
source: QueueItemSource;
}>;
items: QueueSnapshotItem[];
}
/**

372
src/types/protocol_v2.ts Normal file
View File

@@ -0,0 +1,372 @@
/**
* Protocol V2 (alpha hard-cut contract)
*
* This file defines the runtime-scoped websocket contract for device-mode UIs.
* It is intentionally self-defined and does not import transport/event shapes
* from the legacy protocol.ts surface.
*/
import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents";
import type { LettaStreamingResponse } from "@letta-ai/letta-client/resources/agents/messages";
import type { StopReasonType } from "@letta-ai/letta-client/resources/runs/runs";
/**
* Runtime identity for all state and delta events.
*/
export interface RuntimeScope {
agent_id: string;
conversation_id: string;
}
/**
* Base envelope shared by all v2 websocket messages.
*/
export interface RuntimeEnvelope {
runtime: RuntimeScope;
event_seq: number;
emitted_at: string;
idempotency_key: string;
}
export type DevicePermissionMode =
| "default"
| "acceptEdits"
| "plan"
| "bypassPermissions";
export type ToolsetName =
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none";
export type ToolsetPreference = ToolsetName | "auto";
export interface AvailableSkillSummary {
id: string;
name: string;
description: string;
path: string;
source: "bundled" | "global" | "agent" | "project";
}
export interface BashBackgroundProcessSummary {
process_id: string;
kind: "bash";
command: string;
started_at_ms: number | null;
status: string;
exit_code: number | null;
}
export interface AgentTaskBackgroundProcessSummary {
process_id: string;
kind: "agent_task";
task_type: string;
description: string;
started_at_ms: number;
status: string;
subagent_id: string | null;
error?: string;
}
export type BackgroundProcessSummary =
| BashBackgroundProcessSummary
| AgentTaskBackgroundProcessSummary;
export interface DiffHunkLine {
type: "context" | "add" | "remove";
content: string;
}
export interface DiffHunk {
oldStart: number;
oldLines: number;
newStart: number;
newLines: number;
lines: DiffHunkLine[];
}
export type DiffPreview =
| { mode: "advanced"; fileName: string; hunks: DiffHunk[] }
| { mode: "fallback"; fileName: string; reason: string }
| { mode: "unpreviewable"; fileName: string; reason: string };
export interface CanUseToolControlRequestBody {
subtype: "can_use_tool";
tool_name: string;
input: Record<string, unknown>;
tool_call_id: string;
permission_suggestions: string[];
blocked_path: string | null;
diffs?: DiffPreview[];
}
export type ControlRequestBody = CanUseToolControlRequestBody;
export interface ControlRequest {
type: "control_request";
request_id: string;
request: ControlRequestBody;
agent_id?: string;
conversation_id?: string;
}
export interface PendingControlRequest {
request_id: string;
request: ControlRequestBody;
}
/**
* Bottom-bar and device execution context state.
*/
export interface DeviceStatus {
current_connection_id: string | null;
connection_name: string | null;
is_online: boolean;
is_processing: boolean;
current_permission_mode: DevicePermissionMode;
current_working_directory: string | null;
letta_code_version: string | null;
current_toolset: ToolsetName | null;
current_toolset_preference: ToolsetPreference;
current_loaded_tools: string[];
current_available_skills: AvailableSkillSummary[];
background_processes: BackgroundProcessSummary[];
pending_control_requests: PendingControlRequest[];
}
export type LoopStatus =
| "SENDING_API_REQUEST"
| "WAITING_FOR_API_RESPONSE"
| "RETRYING_API_REQUEST"
| "PROCESSING_API_RESPONSE"
| "EXECUTING_CLIENT_SIDE_TOOL"
| "EXECUTING_COMMAND"
| "WAITING_ON_APPROVAL"
| "WAITING_ON_INPUT";
export type QueueMessageKind =
| "message"
| "task_notification"
| "approval_result"
| "overlay_action";
export type QueueMessageSource =
| "user"
| "task_notification"
| "subagent"
| "system";
export interface QueueMessage {
id: string;
client_message_id: string;
kind: QueueMessageKind;
source: QueueMessageSource;
content: MessageCreate["content"] | string;
enqueued_at: string;
}
/**
* Loop state is intentionally small and finite.
* Message-level details are projected from runtime deltas.
*
* Queue state is delivered separately via `update_queue` messages.
*/
export interface LoopState {
status: LoopStatus;
active_run_ids: string[];
}
export interface DeviceStatusUpdateMessage extends RuntimeEnvelope {
type: "update_device_status";
device_status: DeviceStatus;
}
export interface LoopStatusUpdateMessage extends RuntimeEnvelope {
type: "update_loop_status";
loop_status: LoopState;
}
/**
* Full snapshot of the turn queue.
* Emitted on every queue mutation (enqueue, dequeue, clear, drop).
* Queue is typically 0-5 items so full snapshot is cheap and idempotent.
*/
export interface QueueUpdateMessage extends RuntimeEnvelope {
type: "update_queue";
queue: QueueMessage[];
}
/**
* Standard Letta message delta forwarded through the stream channel.
*/
export type MessageDelta = { type: "message" } & LettaStreamingResponse;
export interface UmiLifecycleMessageBase {
id: string;
date: string;
message_type: string;
run_id?: string;
}
export interface ClientToolStartMessage extends UmiLifecycleMessageBase {
message_type: "client_tool_start";
tool_call_id: string;
}
export interface ClientToolEndMessage extends UmiLifecycleMessageBase {
message_type: "client_tool_end";
tool_call_id: string;
status: "success" | "error";
}
export interface CommandStartMessage extends UmiLifecycleMessageBase {
message_type: "command_start";
command_id: string;
input: string;
}
export interface CommandEndMessage extends UmiLifecycleMessageBase {
message_type: "command_end";
command_id: string;
input: string;
output: string;
success: boolean;
dim_output?: boolean;
preformatted?: boolean;
}
export interface StatusMessage extends UmiLifecycleMessageBase {
message_type: "status";
message: string;
level: "info" | "success" | "warning";
}
export interface RetryMessage extends UmiLifecycleMessageBase {
message_type: "retry";
message: string;
reason: StopReasonType;
attempt: number;
max_attempts: number;
delay_ms: number;
}
export interface LoopErrorMessage extends UmiLifecycleMessageBase {
message_type: "loop_error";
message: string;
stop_reason: StopReasonType;
is_terminal: boolean;
api_error?: LettaStreamingResponse.LettaErrorMessage;
}
/**
* Expanded message-delta union.
* stream_delta is the only message stream event the WS server emits in v2.
*/
export type StreamDelta =
| MessageDelta
| ClientToolStartMessage
| ClientToolEndMessage
| CommandStartMessage
| CommandEndMessage
| StatusMessage
| RetryMessage
| LoopErrorMessage;
export interface StreamDeltaMessage extends RuntimeEnvelope {
type: "stream_delta";
delta: StreamDelta;
}
export interface ApprovalResponseAllowDecision {
behavior: "allow";
updated_input?: Record<string, unknown> | null;
updated_permissions?: string[];
}
export interface ApprovalResponseDenyDecision {
behavior: "deny";
message: string;
}
export type ApprovalResponseDecision =
| ApprovalResponseAllowDecision
| ApprovalResponseDenyDecision;
export type ApprovalResponseBody =
| {
request_id: string;
decision: ApprovalResponseDecision;
}
| {
request_id: string;
error: string;
};
/**
* Controller -> execution-environment commands.
* In v2, the WS server accepts only:
* - input (chat-loop ingress envelope)
* - change_device_state (device runtime mutation)
* - abort_message (abort request)
*/
export interface InputCreateMessagePayload {
kind: "create_message";
messages: Array<MessageCreate & { client_message_id?: string }>;
}
export type InputApprovalResponsePayload = {
kind: "approval_response";
} & ApprovalResponseBody;
export type InputPayload =
| InputCreateMessagePayload
| InputApprovalResponsePayload;
export interface InputCommand {
type: "input";
runtime: RuntimeScope;
payload: InputPayload;
}
export interface ChangeDeviceStatePayload {
mode?: DevicePermissionMode;
cwd?: string;
agent_id?: string | null;
conversation_id?: string | null;
}
export interface ChangeDeviceStateCommand {
type: "change_device_state";
runtime: RuntimeScope;
payload: ChangeDeviceStatePayload;
}
export interface AbortMessageCommand {
type: "abort_message";
runtime: RuntimeScope;
request_id?: string;
run_id?: string | null;
}
export interface SyncCommand {
type: "sync";
runtime: RuntimeScope;
}
export type WsProtocolCommand =
| InputCommand
| ChangeDeviceStateCommand
| AbortMessageCommand
| SyncCommand;
export type WsProtocolMessage =
| DeviceStatusUpdateMessage
| LoopStatusUpdateMessage
| QueueUpdateMessage
| StreamDeltaMessage;
export type { StopReasonType };