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:
@@ -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
372
src/types/protocol_v2.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user