import * as smoltalk from "smoltalk"; import { type Expiry, type ExpiresAt } from "./messageMetadata.js"; export type MessageThreadJSON = { id?: string; messages: smoltalk.MessageJSON[]; /** Legacy input only: checkpoints written before labels and handoff tags * moved into each message's metadata. `toJSON` never writes them. */ messageLabels?: (string | null)[]; messageScopes?: (string | null)[]; parentId?: string | null; hidden?: boolean; label?: string | null; summary?: string | null; queuedMessages?: QueuedMessage[]; repairs?: number; }; /** A message waiting on a thread for delivery at the thread's next * request-turn (see MessageThread.queueMessage). Plain data — content is * a string or smoltalk user-content parts, both JSON-safe — so the queue * serializes with the thread wherever the thread serializes. No "system" * role: a mid-thread system turn is rejected or reinterpreted by some * providers, and a caller who wants to steer can say role "user". * Assistant content is string-only — smoltalk.assistantMessage takes a * string, and the union makes the compiler reject an array at the * queueMessage call site instead of a producer papering over it. */ export type QueuedMessage = { role: "user"; content: string | smoltalk.UserContentInput; label: string | null; } | { role: "assistant"; content: string; label: string | null; }; /** A conversation: smoltalk messages plus thread-level state. What the * runtime knows about one message (its debug label, and when it expires) * lives in that message's `metadata`, read and written only through * messageMetadata.ts, so every edit of the array is a plain array edit. * See docs/dev/runtime/expiring-messages.md. */ export declare class MessageThread { messages: smoltalk.Message[]; id: string; /** ID of the parent thread when this thread was created via * `ThreadStore.createSubthread()`. `null` for top-level threads * (the default `MessageThread` constructor leaves it null). Used by * `agency.threads.list()` to surface the parent linkage in * ThreadInfo, and by `ThreadStore.resumeExisting()` to reject * resuming a subthread outside its parent's context. */ parentId: string | null; /** When `true`, this thread is excluded from `agency.threads.list()` * (and therefore the stdlib `listThreads()` user-facing surface). * Set by `Runner.thread` at first-create time when the user opts * in via `thread(hidden: true) { ... }`. Round-tripped through * `toJSON`/`fromJSON` so the flag survives interrupt resume. */ hidden: boolean; /** Optional user-supplied label from `thread(label: "...") { ... }`. * Set by `Runner.thread` at first-create time. `null` for threads * created without a label or via `subthread {}` without one. * Surfaces on `agency.threads.list()` so the stdlib `listThreads()` * can attach it to `ThreadInfo`. Round-tripped through * `toJSON`/`fromJSON` so the value survives interrupt resume. */ label: string | null; /** Cached summary produced by the stdlib `summaryFor()` helper on * first `listThreads()` call after the thread closes. Written via * the TS-side `_setThreadSummary` shim so the cache lives on the * per-run `MessageThread` and gets GC'd with the run — no * module-level state in TS. `null` until the summary is computed. * Round-tripped through `toJSON`/`fromJSON`. */ summary: string | null; /** Repair generation: how many times `repairAbandonedTurn` * (threadRepair.ts) has rewritten this thread. A repair answers tool * calls a parked-then-abandoned turn left dangling, so any checkpoint * snapshot taken BEFORE it is stale — restoring one would overwrite * the repair and everything appended since. `isNewerThan` is that * comparison; `markRepaired` is the ONLY writer outside the * serialization paths (fromJSON / adoptFrom). Never assign directly. * Round-tripped through `toJSON`/`fromJSON` so checkpoints record the * generation they captured. */ repairs: number; /** The dispatches whose bodies are running on this thread right now, * innermost last. A system message pushed while one is open expires with * it. Transient: the prompt loop re-enters a scope when it re-runs a * dispatch after a resume, so nothing here is serialized. */ private handoffScopes; /** Messages queued by `queueMessage`, waiting for the thread's next * request-turn. Drained by the turn-boundary machinery in the tool * loop; never sent to the provider directly from here. Serialized * only when non-empty. */ queuedMessages: QueuedMessage[]; constructor(messages?: smoltalk.Message[]); /** Alias for `push` with no label. Kept for the existing public API. */ addMessage(message: smoltalk.Message): void; cloneMessages(): smoltalk.Message[]; getMessages(): smoltalk.Message[]; /** Wholesale rewrite: the restore path and memory compaction. */ setMessages(messages: smoltalk.Message[]): void; removeAt(index: number): void; /** Swap the message at `index` for `message`, keeping the old message's * label: the callers replace an assistant message with an edited copy * of it, and the copy is the same turn. */ replaceAt(index: number, message: smoltalk.Message): void; /** Remove every message at or after `fromIndex` that `matches`. Walks * backwards so each removal leaves the indexes still to visit untouched. */ removeMatching(fromIndex: number, matches: (message: smoltalk.Message) => boolean): void; /** Take on `other`'s messages while keeping this thread's identity. The * resume path needs the alias preserved, so it cannot just swap in the * restored object. */ adoptFrom(other: MessageThread): void; /** Record that this thread was repaired. Monotonic on purpose — the * stale-checkpoint check reads `repairs` as a generation number, so it * must only ever go up. */ markRepaired(): void; /** True when this thread has been repaired since `snapshot` was * captured — which makes restoring `snapshot` a write over newer * history. See restoreThreadForResume in threadRepair.ts. */ isNewerThan(snapshot: MessageThread): boolean; /** The only append. `label` and `expires` are written into the * message's metadata when given; a null argument leaves whatever the * message already carries. A system message pushed while a handoff body * runs, with no expiry of its own, expires with that handoff. */ push(message: smoltalk.Message, label?: string | null, expires?: Expiry | null): void; /** The expiry a system message pushed while a handoff body runs gets, * unless it already carries one. */ private handoffExpiry; /** The label of the message at `index`, or null when unlabeled. */ labelAt(index: number): string | null; /** The expiry of the message at `index`, or null when it stays. */ expiryAt(index: number): Expiry | null; /** Remove every message that expires with this owner. The only removal * path for expiring messages. */ expire(at: ExpiresAt, key: string): void; /** Give `key` to every message that expires with this kind of owner but * names none yet: an `llmCall` message Agency code pushed before the call * that will own it started. */ claimUnkeyed(at: ExpiresAt, key: string): void; /** Mark the start of a handoff body on this thread: every system message * pushed until the matching `exitHandoffScope` belongs to `key`. Scopes * nest, innermost winning, for a handoff dispatched inside a handoff. */ enterHandoffScope(key: string): void; exitHandoffScope(): void; /** How many handoff bodies are running on this thread right now. */ handoffDepth(): number; private currentHandoffScope; /** Queue a message for delivery at this thread's next request-turn: * the start of the thread's next `llm()` call, or after a tool round * within a running call. The mid-turn-safe counterpart to an immediate * `push` — callable at any moment without breaking the tool-call / * tool-result adjacency the provider requires. If no `llm()` ever runs * against this thread again, the message simply waits, serialized with * the thread. */ queueMessage(content: string | smoltalk.UserContentInput, opts?: { role?: "user" | "assistant"; label?: string; }): void; /** Remove and return every queued message, oldest first. Called by the * turn-boundary drain exactly once per delivery point. */ takeQueuedMessages(): QueuedMessage[]; hasQueuedMessages(): boolean; newChild(): MessageThread; /** Build a subthread child seeded with a clone of this thread's * messages. Caller passes the parent's registry id so the child * knows where it descended from — used by `ThreadStore` to * link subthreads back into the registry and by * `agency.threads.list()` to surface the parent relationship. */ newSubthreadChild(parentRegistryId: string | null): MessageThread; toJSON(): MessageThreadJSON; static fromJSON(json: MessageThreadJSON | MessageThread | smoltalk.MessageJSON[] | smoltalk.Message[]): MessageThread; }