/** * TUI-C17 — the managed route for LIVE tool output (a custom/dev toolkit child process's * stdout/stderr chunks and its "🔧 Executing …" announcement). * * The toolkits used to write child output straight to `process.stdout` and announce runs via * `displayInfo`. Under the Ink TUI both bypass Ink's managed frame: the raw text prints out of * order above the agent message, never reaches the `TurnViewModel`, and vanishes on the next * re-render. The fix is an emit/subscribe seam: * * - The toolkits call {@link emitToolOutput} instead of writing to stdout/`displayInfo`. * - With NO subscriber (every non-TUI surface: `--no-tui`, piped, `exec`, the AG-UI server), * the DEFAULT SINK reproduces the historical behaviour byte-for-byte — `displayInfo` for the * notice, `stdout.write` for chunks, `displayWarning`/`displayError` for a failure-path * warning/error — so headless output is unchanged. * - The Ink TUI subscribes for the duration of a turn via {@link mergeToolOutputIntoEvents}, * which converts each chunk into a typed `tool_output` {@link AgentStreamEvent} attributed to * its tool call, merged live into the turn's event stream so it lands in `foldEvents`. * * Single-subscriber by design: one process hosts at most one interactive TUI session, and the * non-TUI surfaces never subscribe. (Concurrent server sessions — AG-UI/ACP — stay on the * default sink, exactly as before.) * * TUI-C31 hardens the seam's edge windows: failure-path warnings/errors now travel the channel * too ({@link GthToolOutputChunk} `kind: 'warning' | 'error'`, residual a); the merge drains a * straggler queued in the unsubscribe microwindow (residual b) and cleans up on early-`return()` * (residual c); and {@link setToolOutputSuppressed} lets the mounted TUI suppress a * post-unsubscribe straggler between turns instead of leaking it to raw stdout (residual d). * * @module */ import type { AgentStreamEvent } from '#src/core/types.js'; /** One emitted piece of live tool output, attributed to the tool (and call) that produced it. */ export interface GthToolOutputChunk { /** * The LangChain tool call id this output belongs to (`ToolRunnableConfig.toolCall.id`), * when the invoking framework supplied one. Lets a consumer nest output under the exact call. */ toolCallId?: string; /** The gth tool name (e.g. `run_shell_command`, a custom tool's name). Always known. */ toolName: string; /** * `notice` — the "🔧 Executing …" announcement (historically `displayInfo`); * `output` — a verbatim child stdout/stderr chunk (historically `stdout.write`); * `warning` / `error` — a failure-path advisory (historically `displayWarning`/`displayError`), * e.g. a hardline refusal or a spawn-level failure. TUI-C31 (a): routing these through the * channel keeps them out of raw stdout while the Ink frame is mounted; the default sink still * renders them via `displayWarning`/`displayError` for every non-TUI surface, byte-for-byte. */ kind: 'notice' | 'output' | 'warning' | 'error'; /** * The text, verbatim. Notices carry NO leading newline — raw-console framing (the historical * leading `\n`) is the default sink's concern, not the producer's. A `warning`/`error` DOES * carry whatever leading framing its historical `displayWarning`/`displayError` call had (the * default sink forwards it unchanged), so headless output stays byte-for-byte identical. */ text: string; } export type GthToolOutputListener = (chunk: GthToolOutputChunk) => void; /** * Subscribe to live tool output, replacing the default stdout/`displayInfo` sink for as long * as the subscription is active. Returns an unsubscribe function. Last subscriber wins; the * returned unsubscribe only clears its OWN registration (a stale unsubscribe can never detach * a newer subscriber). */ export declare function subscribeToolOutput(listener: GthToolOutputListener): () => void; /** * TUI-C31 (d): mark whether a terminal-owning surface (the Ink TUI) is mounted. While mounted, * the default sink SUPPRESSES output that has no per-turn subscriber (a post-turn straggler) * rather than writing it raw over the managed frame. The TUI session sets this `true` around the * whole session (`render()` … `waitUntilExit()`) and back to `false` on unmount, so once the TUI * is gone the default (headless) stdout sink is fully restored. An active per-turn subscriber * always takes precedence, so legitimate in-turn output is untouched. */ export declare function setToolOutputSuppressed(value: boolean): void; /** * Emit one piece of live tool output. Routed to the active subscriber when present (the Ink * TUI), otherwise to the DEFAULT SINK, which reproduces the pre-TUI-C17 behaviour exactly: * the notice via `displayInfo` with its historical leading newline, chunks via `stdout.write`, * a warning via `displayWarning` and an error via `displayError` (TUI-C31 a). While the TUI is * mounted but no subscriber is attached (between turns), a no-subscriber emit is SUPPRESSED * rather than written raw over the managed frame (TUI-C31 d). */ export declare function emitToolOutput(chunk: GthToolOutputChunk): void; /** * Merge live tool output into an agent event stream: subscribes to the channel for the * lifetime of `inner` and yields each emitted chunk as a `tool_output` event, interleaved * with `inner`'s own events in arrival order. Because tool child output arrives WHILE the * graph stream is awaiting its next message, the merge is push-based (a woken queue), so a * long-running command's output streams into the consumer live rather than batching until * the tool finishes. * * Used by the TUI session around `processMessagesWithEvents`; always unsubscribes (restoring * the default stdout sink) when the inner stream completes, throws, or the consumer stops * early. Errors from `inner` (including aborts) propagate unchanged after the queue drains. */ export declare function mergeToolOutputIntoEvents(inner: AsyncGenerator): AsyncGenerator;