import type { ContentBlock } from '../types/blocks.js'; import type { Message } from '../types/messages.js'; import type { Context, TodoItem } from './context.js'; /** * Observable wrapper for mutable conversation state. Production code should * mutate messages, todos, and meta through this API so subscribers see a * deterministic change stream. The underlying Context arrays are still * exposed for read compatibility and legacy tests. * * L1-A invariant: direct mutations of `ctx.messages` / `ctx.todos` bypass * the observer layer. Prefer `ctx.state.appendMessage()` etc. to keep * subscribers in sync. The compatibility arrays exist so existing code * that reads `ctx.messages` directly still works — they are NOT safe for * external writes. */ export type StateChange = { kind: 'message_appended'; message: Message; } | { kind: 'messages_replaced'; messages: readonly Message[]; } | { kind: 'message_updated'; index: number; message: Message; } | { kind: 'todos_replaced'; todos: readonly TodoItem[]; /** * Final all-completed snapshot when the tactical list auto-clears. * Observational mirrors use this to move cards to Done instead of * interpreting the empty active list as "the work vanished". */ completedSnapshot?: readonly TodoItem[] | undefined; } | { kind: 'meta_set'; key: string; value: unknown; } | { kind: 'meta_deleted'; key: string; } | { kind: 'meta_cleared'; }; export type StateChangeHandler = (change: StateChange, state: ConversationState) => void; export interface ReadonlyConversationState { readonly messages: readonly Message[]; readonly todos: readonly TodoItem[]; readonly meta: Readonly>; } export declare class ConversationState { private readonly ctx; private readonly listeners; private _revision; constructor(ctx: Context); get messages(): readonly Message[]; get todos(): readonly TodoItem[]; get meta(): Readonly>; /** Monotonic mutation counter for consumers that need to detect rewrites. */ get revision(): number; /** * Cheap immutable snapshot. Useful for tests and for compaction passes * that need a stable view across an async boundary. * * Uses shallow-freeze instead of deep-freeze: only the wrapper object * and the three content arrays are frozen. Individual message/todo * objects are NOT recursively frozen — they are reconstructed via * spread copies and are immutable by convention. This cuts the freeze * count from O(n·m·d) (n=messages, m=content blocks, d=depth) to O(1). */ snapshot(): ReadonlyConversationState; appendMessage(message: Message): void; /** * Append a content block to the trailing user message's content array. * Mutates only that one message (a single indexed assignment) — avoids * the O(n) array copy + token-cache re-walk that `replaceMessages()` * would do for a single-message edit. Used by the agent loop to fold * btw-notes / queued-mailbox blocks into the conversation. * * The block is folded only into a *user* message (preserves * user/assistant alternation between tool batches). Returns false when * there is no trailing user message to fold into — callers should * `appendMessage({ role: 'user', content: [block] })` instead. */ appendBlockToLastUserMessage(block: ContentBlock): boolean; replaceMessages(messages: Message[]): void; replaceTodos(todos: TodoItem[]): void; setMeta(key: string, value: unknown): void; deleteMeta(key: string): void; clearMeta(): void; /** * Subscribe to mutations that go through this wrapper. Direct mutations of * the compatibility arrays are intentionally not observed. */ onChange(listener: StateChangeHandler): () => void; private emit; } /** * Convenience constructor. The wrapper holds a reference, not a copy. */ export declare function wrapAsState(ctx: Context): ConversationState; //# sourceMappingURL=conversation-state.d.ts.map