import type { Command, Resume } from "./commands"; import type { Node } from "./node"; import type { SuspendInfo } from "./instance"; /** * Text content block (simplified for storage). */ export interface TextBlock { type: "text"; text: string; } export type ImageDetail = "auto" | "low" | "high"; /** * Image content block (simplified for storage). * Data is base64 bytes (no data: prefix). */ export interface ImageBlock { type: "image"; mimeType: "image/jpeg" | "image/png" | (string & {}); data: string; detail?: ImageDetail; } /** * File content block. * Uses a resolved URL that an executor can hand to the underlying model provider. * Optional fields allow apps to preserve source metadata alongside the file reference. */ export interface FileBlock { type: "file"; url: string; mediaType: string; filename?: string; byteSize?: number; storageId?: string; } /** * Tool use content block (from assistant). */ export interface ToolUseBlock { type: "tool_use"; id: string; name: string; input: unknown; } /** * Thinking content block (simplified for storage). */ export interface ThinkingBlock { type: "thinking"; thinking: string; } /** * Tool result content block (in user message). */ export interface ToolResultBlock { type: "tool_result"; tool_use_id: string; content: string; is_error?: boolean; } /** * Output block from structured output. * Contains the mapped application message. * @typeParam M - The application message type (defaults to unknown). */ export interface OutputBlock { type: "output"; data: M; } // ============================================================================ // Instance Payloads - All instance mutations are modeled as messages // ============================================================================ /** * State update payload - shallow merges patch into instance state. */ export interface StateUpdatePayload { kind: "state"; instanceId: string; patch: Record; } /** * Pack state update payload - shallow merges patch into pack state. */ export interface PackStateUpdatePayload { kind: "packState"; packName: string; patch: Record; } /** * Transition payload - replaces node/state, clears children. */ export interface TransitionPayload { kind: "transition"; instanceId: string; node: Node; state?: unknown; } /** * Spawn payload - adds children to parent instance. */ export interface SpawnPayload { kind: "spawn"; parentInstanceId: string; children: Array<{ node: Node; state?: unknown; }>; } /** * Cede payload - removes instance from tree, optionally with content for parent. */ export interface CedePayload { kind: "cede"; instanceId: string; content?: string | MachineMessage[]; } /** * Suspend payload - marks instance as suspended. */ export interface SuspendPayload { kind: "suspend"; instanceId: string; suspendInfo: SuspendInfo; } /** * Union of all instance mutation payloads. */ export type InstancePayload = | StateUpdatePayload | PackStateUpdatePayload | TransitionPayload | SpawnPayload | CedePayload | SuspendPayload; /** * Union of all machine item types. * @typeParam M - The application message type for OutputBlock (defaults to unknown). */ export type MachineItem = | TextBlock | ImageBlock | FileBlock | ToolUseBlock | ThinkingBlock | ToolResultBlock | OutputBlock | Command | Resume; /** * Check if a machine item is an OutputBlock. */ export function isOutputBlock( block: MachineItem, ): block is OutputBlock { return block.type === "output"; } /** * Source attribution for a message. * - instanceId: ID of the instance that generated this message * - isPrimary: true if from the primary (non-worker) leaf instance * - external: true if message came from outside the machine (user transcript, LiveKit STT, etc.) */ export interface MessageSource { /** ID of the instance that generated this message */ instanceId?: string; /** True if this message is from the primary (non-worker) leaf instance */ isPrimary?: boolean; /** True if message originated from outside the machine (e.g., user speech, external system) */ external?: boolean; } /** * Metadata attached to messages for attribution and tracking. */ export interface MessageMetadata { /** Source attribution for this message */ source?: MessageSource; /** Stable identifier for this message across streaming updates */ messageId?: string; /** Streaming state metadata when streamWhenAvailable is enabled */ stream?: { state: "streaming" | "complete" | "error"; /** Monotonic per-message sequence for stream events */ seq?: number; }; /** * If true, this message is added to history for context but does not trigger * leaf execution (LLM inference). Use for logging/context messages that * should be visible to the LLM on the next real user message. */ silent?: boolean; /** * Collapse messages with the same singleton key at the beginning of runMachine. * Only the most recent message in each group is kept. */ singleton?: string; /** Number of messages collapsed into this singleton group (set during collapse). */ singletonFrameCount?: number; } /** * Base message with common fields. */ interface BaseMessage { /** Optional metadata for message attribution */ metadata?: MessageMetadata; } /** * Conversation message (user, assistant, system, command). * @typeParam M - The application message type for OutputBlock (defaults to unknown). */ export interface ConversationMessage extends BaseMessage { role: "user" | "assistant" | "system" | "command"; items: string | MachineItem[]; } /** * Ephemeral message. * Ephemeral messages are never persisted and are only used for the next run. * They do not trigger waitForQueue and do not trigger inference by themselves. */ export interface EphemeralMessage extends BaseMessage { role: "ephemeral"; items: string | MachineItem[]; } /** * Instance mutation message. * Contains a payload describing a state update, transition, spawn, cede, or suspend. * @typeParam M - The application message type (defaults to unknown). */ export interface InstanceMessage extends BaseMessage { role: "instance"; items: InstancePayload; } /** * Message in the conversation history. * Can be a conversation message or an instance mutation message. * @typeParam M - The application message type for OutputBlock (defaults to unknown). */ export type MachineMessage = | ConversationMessage | EphemeralMessage | InstanceMessage; /** * Create a user message. * @param items - Message items (string or machine items) * @param metadata - Optional metadata (source attribution, silent flag, etc.) */ export function userMessage( items: string | MachineItem[], metadata?: MessageMetadata, ): MachineMessage { return { role: "user", items, ...(metadata && { metadata }), }; } /** * Create an assistant message. * @param items - Message items (string or machine items) * @param metadata - Optional metadata (source attribution, silent flag, etc.) */ export function assistantMessage( items: string | MachineItem[], metadata?: MessageMetadata, ): MachineMessage { return { role: "assistant", items, ...(metadata && { metadata }), }; } /** * Create a system message. * System messages are filtered from history before sending to the model. * Used for internal control flow like Resume. * @param items - Message items (string or machine items) * @param source - Optional source attribution for this message */ export function systemMessage( items: string | MachineItem[], source?: MessageSource, ): MachineMessage { return { role: "system", items, ...(source && { metadata: { source } }), }; } /** * Create a command message. * Command messages are processed with higher precedence than regular messages. * They are drained from the queue first and their results are yielded before * normal execution continues. * @param items - Message items (typically a Command object) * @param source - Optional source attribution for this message */ export function commandMessage( items: string | MachineItem[], source?: MessageSource, ): MachineMessage { return { role: "command", items, ...(source && { metadata: { source } }), }; } /** * Create an instance message. * Instance messages describe mutations to the machine's instance tree. * @param payload - The instance mutation payload * @param source - Optional source attribution for this message */ export function instanceMessage( payload: InstancePayload, source?: MessageSource, ): InstanceMessage { return { role: "instance", items: payload, ...(source && { metadata: { source } }), }; } /** * Create a tool result block. */ export function toolResult( toolUseId: string, content: string, isError?: boolean ): ToolResultBlock { return { type: "tool_result", tool_use_id: toolUseId, content, ...(isError !== undefined && { is_error: isError }), }; } /** * Create an ephemeral message. */ export function ephemeralMessage( items: string | MachineItem[], metadata?: MessageMetadata, ): EphemeralMessage { return { role: "ephemeral", items, ...(metadata && { metadata }), }; } // ============================================================================ // Type Guards // ============================================================================ /** * Check if a message is a conversation message (user, assistant, system, command). */ export function isConversationMessage( message: MachineMessage, ): message is ConversationMessage { return ( message.role === "user" || message.role === "assistant" || message.role === "system" || message.role === "command" ); } /** * Check if a message is an instance mutation message. */ export function isInstanceMessage( message: MachineMessage, ): message is InstanceMessage { return message.role === "instance"; } /** * Check if a message should be sent to the model (user or assistant only). */ export function isModelMessage( message: MachineMessage, ): message is ConversationMessage { return message.role === "user" || message.role === "assistant"; } /** * Check if a message is an ephemeral message. */ export function isEphemeralMessage( message: MachineMessage, ): message is EphemeralMessage { return message.role === "ephemeral"; } /** * Extract text from a message's items. * Returns empty string for instance messages. */ export function getMessageText(message: MachineMessage): string { // Instance messages have no text content if (message.role === "instance") { return ""; } if (typeof message.items === "string") { return message.items; } return message.items .filter((block): block is TextBlock => block.type === "text") .map((block) => block.text) .join(""); }