/** * MessageSegmentAccumulator - Manages accumulation of message segments * * This class handles the logic for: * - Accumulating text segments (appending to existing text) * - Managing tool execution states (EXECUTING_TOOL -> EXECUTED_TOOL) * - Tracking pending approval requests * - Creating approval segments when results arrive */ import type { AskOptionData, MessageSegment, ToolExecutionSegment, ApprovalRequestField, ApprovalRequestSegment, ApprovalBatchExecutionState, ApprovalResolutionHandler, TicketEscalatedData, TicketEventData, PendingApproval, PendingToolCallData, AccumulatorState, ChatApprovalStatus, ExecutingToolState } from '../types'; export interface AccumulatorCallbacks { /** See `ApprovalResolutionHandler` (message.types) — the SSOT for the * approve/reject signature incl. the boolean failure flag. */ onApprove?: ApprovalResolutionHandler; onReject?: ApprovalResolutionHandler; /** Escalation offers resolve through the ticket-escalation mutations, NOT * the tool-approval endpoint, so they carry their own pair of handlers. * Sharing `onApprove`/`onReject` would POST an offer id to the approval * endpoint, which has no record of it. */ onEscalationApprove?: ApprovalResolutionHandler; onEscalationReject?: ApprovalResolutionHandler; } /** * Accumulator for managing message segments during real-time streaming * or historical message processing */ export declare class MessageSegmentAccumulator { private segments; private pendingApprovals; private executingTools; private callbacks; constructor(callbacks?: AccumulatorCallbacks); /** * Set callbacks for approval actions */ setCallbacks(callbacks: AccumulatorCallbacks): void; /** * Initialize accumulator with existing state from an incomplete historical message * Used to continue building messages across page refreshes or reconnections */ initializeWithState(state: { existingSegments?: MessageSegment[]; pendingApprovals?: Map; executingTools?: Map; }): void; /** * Get current segments */ getSegments(): MessageSegment[]; /** * Get the state of the accumulator for serialization */ getState(): AccumulatorState; /** * Reset the accumulator to initial state */ reset(): void; /** * Reset only segments (keep pending state for continued processing) */ resetSegments(): void; /** * Append text to the current message * If the last segment is text, append to it; otherwise create a new text segment */ appendText(text: string): MessageSegment[]; /** * Append thinking text to the current message. * If the last segment is thinking, append to it; otherwise start a new thinking segment. */ appendThinking(text: string): MessageSegment[]; /** * Append guide text to the current message. * If the last segment is guide, append to it; otherwise start a new guide segment. */ appendGuide(text: string): MessageSegment[]; /** * Add a clarification (ask) card. Unlike the three delta streams an ask * arrives whole in one chunk, so it is always a NEW segment — never merged * into a trailing one. Consecutive cards stay separate segments; the renderer * is what pages through a run of them. */ addAsk(question: string, options: AskOptionData[]): MessageSegment[]; /** * Add a tool execution segment. * * Routing: * 1) If `toolExecutionRequestId` matches a tool call inside an existing * `approval_batch` segment, merge the state into that batch's * `executions` map (no standalone segment is pushed). * 2) Otherwise: pair EXECUTING ↔ EXECUTED by `toolExecutionRequestId`. * If no id is present (older backends), fall back to * `(integratedToolType, toolFunction)` so repeat calls of the same * function don't all bucket under one key. */ addToolExecution(segment: ToolExecutionSegment): MessageSegment[]; /** * Try to merge a tool execution event into an existing approval_batch * segment whose `toolCalls` contains the same `toolExecutionRequestId`. * Returns true when a batch was updated, false when no batch matches. */ private applyExecutionToBatch; /** * A tool only ever runs after its approval gate was granted. The legacy / * single `approval_request` segment carries no `toolExecutionRequestId` to * correlate with the execution, and an observer (e.g. a technician * mirroring the client chat) may never receive an `APPROVAL_RESULT` chunk — * only the tool's `EXECUTING_TOOL` / `EXECUTED_TOOL` events. Treat the * arrival of a tool execution as implicit approval of the most recent * still-pending gate so the card does not stay stuck `pending` in realtime. * * The agent stays paused while an approval is outstanding, so there is at * most one relevant gate; flipping only the latest pending one is safe and * monotonic (never downgrades, can't make a correct state wrong — an * unapproved tool cannot execute). `approval_batch` is handled separately by * `applyExecutionToBatch` and is intentionally left untouched here. */ private resolvePendingApprovalForExecution; /** * Track a pending approval request */ trackApprovalRequest(requestId: string, data: PendingApproval): void; /** * Add an approval request segment directly (for CLIENT approvals) */ addApprovalRequest(requestId: string, command: string, explanation: string | undefined, approvalType: string, status?: ChatApprovalStatus, /** Structured label/value rows. The card prefers them over `explanation` * (see `ApprovalRequestData.fields`). Optional because the agent's own * approvals carry prose; a Product Guide card is almost entirely fields — * dropping them here left it as a bare title. */ fields?: ApprovalRequestField[], /** Where the card came from; `'guide'` keeps it inline (see * `ApprovalRequestData.origin`). */ origin?: 'guide'): MessageSegment[]; /** * Build one approval-request segment — THE constructor for this segment type. * * Three paths produce these: `addApprovalRequest` (live stream and replay), * `flushPendingApprovals` (tracked-but-unresolved after a history replay) and * `processApprovalResult` (a result arriving for a tracked request). They used * to hand-write the object each, which is how `fields` and `origin` reached * the card down one path and not the others — the same proposal rendering as a * bare title, or losing the marker that routes its buttons to the hub. */ private buildApprovalRequestSegment; /** * Add a batch approval segment containing multiple tool calls. Upserts by * `approvalRequestId`: when a batch with the same id is already in the * accumulator, the existing segment is updated in place rather than a * second segment being pushed. This matters for the consumer-store replay * path, which feeds `[existing..., new...]` into `replaySegments` and would * otherwise produce two batch segments for the same approval after a * status flip or per-tool execution merge. * * `approvalType` is the highest-privilege type required across the batch. * `executions` is forwarded as-is. On upsert, a new `executions` object * overrides the existing one (so the latest replay wins). */ addApprovalBatch(approvalRequestId: string, approvalType: string, toolCalls: PendingToolCallData[], status?: ChatApprovalStatus, executions?: Record, resolvedByName?: string | null, /** Where the batch came from; `'guide'` keeps it inline and routes it to * the hub, exactly as for a single card (see `ApprovalBatchData.origin`). */ origin?: 'guide'): MessageSegment[]; /** * Process an approval result and create a segment * Returns the pending approval data if found */ processApprovalResult(requestId: string, approved: boolean, approvalType: string): { segment: ApprovalRequestSegment; pendingData: PendingApproval | null; } | null; /** * Add a ticket-escalation offer block. Upserts by `offerId` for the same * reason `addApprovalBatch` does: the consumer-store replay path feeds * `[existing..., new...]` back through `replaySegments`, which would * otherwise yield two cards for one offer. */ addEscalationOffer(offerId: string, text: string, origin: string | undefined, status?: ChatApprovalStatus, resolvedByName?: string | null): MessageSegment[]; /** * Add the handoff receipt. Upserts by `ticketId` so a redelivered block * (JetStream catch-up over hydrated history) can't stack a second notice. */ addTicketEscalated(data: TicketEscalatedData): MessageSegment[]; /** * Add a ticket lifecycle receipt (resolved / reopened / unknown kind). * * Upsert identity is the chunk's stream sequence when BOTH sides know it. * The payload fallback exists for one overlap only: history hydration is * seq-less (the persisted row's seq lives on the message, not the * `messageData`), so a JetStream catch-up redelivery of the same event must * still match its hydrated twin. That twin is necessarily the LATEST ticket * event, so the fallback may consider only that one — scanning older * segments swallowed a genuinely REPEATED event: resolve → reopen → resolve * by the same actor is payload-identical to the first resolve, and matching * the old card meant the final one never rendered. */ addTicketEvent(data: TicketEventData, streamSeq?: number, occurredAt?: Date): MessageSegment[]; /** * Update status of an existing approval segment (single, batch, or * escalation offer). * `resolvedByName` (when provided) is stamped onto the matching batch segment so the * resolved card shows "by {name}"; omit it to leave any existing value untouched. */ updateApprovalStatus(requestId: string, status: ChatApprovalStatus, resolvedByName?: string | null): MessageSegment[]; /** * Get pending approvals that haven't been resolved */ getPendingApprovals(): Map; /** * Check if there are any pending approvals */ hasPendingApprovals(): boolean; /** * Create segments for all remaining pending approvals */ flushPendingApprovals(): ApprovalRequestSegment[]; /** * Add a context compaction segment with 'started' status */ addContextCompaction(): MessageSegment[]; /** * Complete a context compaction segment */ completeContextCompaction(summary?: string): MessageSegment[]; /** * Add an error segment */ addError(title: string, details?: string): MessageSegment[]; /** * Reset and replay a full segment array through the accumulator. */ replaySegments(segments: MessageSegment[]): MessageSegment[]; /** * Check if segments have any content */ hasContent(): boolean; /** * Get the number of segments */ get length(): number; } /** * Create a new accumulator instance with callbacks */ export declare function createMessageSegmentAccumulator(callbacks?: AccumulatorCallbacks): MessageSegmentAccumulator; //# sourceMappingURL=message-segment-accumulator.d.ts.map