import * as smoltalk from "smoltalk"; import type { Interrupt } from "./interrupts.js"; import type { MessageThread } from "./state/messageThread.js"; import type { StateStack } from "./state/stateStack.js"; /** * INTERNAL — consumed by prompt.ts only. The public way to inject a message * into a running conversation is `MessageThread.queueMessage` (surfaced as * `agency.thread.current().queueMessage(...)`); this module is the delivery * side. It owns the turn boundary: the safe point between "all tool results * are in" and "the next request goes out", where pending messages from * every source get pushed with the step-and-snapshot discipline handled * once, here, instead of hand-woven per source in prompt.ts. * * Three sources ("producers") exist, each keeping its own storage and merge * rule because those genuinely differ: * - reply attachments: harvested per tool onto runnerState, merged * N-into-1 user message; * - guard feedback: the approver's `approve({message})` text, queued * branch-locally on the stack (an approval in a fork branch must follow * THAT branch's next request), joined into one labeled user message; * - queued messages: `queueMessage` entries on the active thread, * delivered FIFO, roles and labels preserved, never collapsed. * Adding a producer is a runtime-maintainer decision; features inject by * calling queueMessage, not by registering here. */ export type TurnMessage = { message: smoltalk.Message; label: string | null; }; export type BoundaryContext = { /** The PromptRunner's step method, bound. Injected so this module is * unit-testable with a recording fake. The body's return type matters: * pr.step treats a returned Interrupt[] as "pause here" and stamps a * checkpoint — that is HOW a guard trip suspends the run. */ step: (key: string, body: () => Promise) => Promise; /** runPrompt's guardGate closure (raiseGuardTripsUntilClear). Its * Interrupt[] return is the pause signal and MUST flow through to * step() unaltered — never wrap this in a void-returning adapter. */ guardGate: () => Promise; /** The live conversation of this llm() call. */ messages: MessageThread; /** Per-llm()-call serialized state; home of replyAttachments. */ runnerState: Record; /** The branch's stack; home of the guard-feedback queue. */ stateStack: StateStack; /** Serializes the given thread into the frame shadow * (self.messagesJSON). Takes the thread as a parameter so the sync * target is explicit rather than a closure over a mutable local. */ snapshot: (thread: MessageThread) => void; }; export type TurnMessageProducer = { /** Step-key fragment, e.g. "attachReplies". */ name: string; /** Destructive read: return what is pending and clear it at the source. * Runs INSIDE the step, so a replayed-and-skipped step never loses * work. Empty array = nothing this time. */ take: (bctx: BoundaryContext) => TurnMessage[]; }; export declare const attachmentsProducer: TurnMessageProducer; export declare const guardFeedbackProducer: TurnMessageProducer; export declare const queuedMessagesProducer: TurnMessageProducer; /** Deliver one producer's pending messages inside an idempotent step. The * step always opens (uniform emission; cross-version checkpoint * compatibility is not promised); an empty take means the step no-ops — * no push, no snapshot, matching the shipped empty paths. */ export declare function drainProducer(p: TurnMessageProducer, stepKey: string, bctx: BoundaryContext): Promise; /** The two-beat figure that appears at four kinds of site in the loop: * raise any pending guard trip, then deliver the approver's feedback. * Keys are passed verbatim — the four sites do not share a naming scheme * and the names are frozen. */ export declare function runGateAndFeedback(gateKey: string, feedbackKey: string, bctx: BoundaryContext): Promise; /** The full round boundary, in the one canonical order: tool artifacts * first, then queued messages, then the guard machinery gets the last * word before the next request. */ export declare function runRoundBoundary(round: number, bctx: BoundaryContext): Promise; /** The call-entry boundary: deliver messages queued before this llm() * call (so a no-tool call still delivers, and queued content precedes * the new prompt — it reviews past work, same positioning as initial * guard feedback), then the gate-and-feedback figure. No attachments * phase: none can exist before the first request. */ export declare function runInitialBoundary(bctx: BoundaryContext): Promise;