/** * PiProcessSessionAdapter — ISessionClient adapter with spawn-based * process session management. * * Composes PiSessionAdapter for read-only delegation (list, get, children, * todo, diff, fork) and implements active session operations (create, prompt, * promptSync, status, abort) via child process spawning of the Pi CLI in * JSON event mode. * * Each active session maintains a ProcessRecord with accumulated messages, * stderr buffer, exit code, and agent configuration. The spawn pattern * follows the project convention established in lsp/client-manager.ts. * * Must NOT import from @earendil-works/pi-coding-agent. */ import type { ISessionClient } from "../../ports/session-client.ts"; import type { IEventBridge } from "../../ports/event-bridge.ts"; import type { SessionInfo, Message, FileDiff, Todo, SessionStatus } from "../../types.ts"; /** * Agent configuration for a spawned Pi process session. * Holds the model identifier, tool list, and system prompt text * that are passed as CLI arguments to the Pi binary. */ export interface PiProcessAgentConfig { /** Model identifier string (e.g., "claude-sonnet-4-20250514"). */ model: string; /** Tool names available to this agent session. */ tools: string[]; /** System prompt text written to a temp file for --append-system-prompt. */ systemPrompt: string; } /** * Build the CLI argument array for a spawned Pi child process. * * Ordering contract (locked by tests/pi-process-session-json.test.ts): * --mode json -p --no-session --model * --tools — only when tools.length > 0 * --append-system-prompt — only when the file exists * [extra args] — only when provided * Task: — final positional * * Pure with respect to the caller: no side effects, no `this`. The * sysPromptPath existence check mirrors the former inline builder so * callers that always write the file first keep identical behavior, and * non-agent configs (empty tools) spawn exactly as before. */ export declare function buildSpawnArgs(model: string, tools: string[], sysPromptPath: string, promptText: string, extra?: string[]): string[]; /** * ISessionClient adapter that manages sessions by spawning Pi CLI * child processes and parsing JSON event output. * * Read-only operations (list, get, children, todo, diff, fork) delegate * to an internal PiSessionAdapter instance. Mutation operations * (create, prompt, promptSync, abort, status) use process management. */ export declare class PiProcessSessionAdapter implements ISessionClient { /** Map of active and completed process sessions keyed by session ID. */ private readonly processes; /** Read-only delegate for filesystem-backed session queries. */ private readonly piSession; /** Logger instance scoped to this adapter. */ private readonly log; /** * Map of named agent configurations keyed by agent identifier. * Populated by the caller (e.g., dispatch system) before create(). */ private readonly agentConfigs; /** Optional event bridge for emitting canonical events (e.g., session.idle on process exit). */ private eventBridge?; /** Liveness activity-heartbeat throttle (ms) for this adapter instance. */ private readonly activityHeartbeatIntervalMs; /** * @param agentConfigs - Optional pre-configured agent configs keyed by agent name. * @param sessionDir - Optional session directory override (passed to PiSessionAdapter). */ constructor(agentConfigs?: Map, sessionDir?: string, options?: { activityHeartbeatIntervalMs?: number; }); /** * Set the event bridge for emitting canonical events. * Must be set before any spawn operations to enable completion evaluation * when child Pi processes exit. */ setEventBridge(bridge: IEventBridge): void; /** * Scan the sidecar directory for orphaned sessions and reconstruct * in-memory ProcessRecords for each one. * * Each orphaned session's sidecar JSONL file is read and its events * replayed through _handleJsonEvent() to reconstruct the Message[]. * The recovered ProcessRecord has `proc: null` and `exitCode: 0`. * * This method should be called during startup/recovery, before any * new sessions are created. * * @returns The number of orphaned sessions recovered. */ recoverOrphanedSessions(): Promise; /** * Register or update an agent configuration for a named agent. * * @param agentId - Agent identifier used in create() options. * @param config - Agent configuration (model, tools, systemPrompt). */ registerAgentConfig(agentId: string, config: PiProcessAgentConfig): void; /** * Create a new process session. * * Generates a UUID as the session ID, allocates a ProcessRecord slot * with the matching agent configuration, and returns a synthetic * SessionInfo object. * * No process is spawned at this point — the actual spawn happens * when prompt() or promptSync() is called. */ create(options: { directory: string; agent?: string; parentID?: string; }): Promise; /** * Prompt a session asynchronously (fire-and-forget). * * Extracts model and tools from the session's ProcessRecord agentConfig, * writes the system prompt to a temporary file, spawns the Pi CLI with * `--mode json`, and parses stdout line-by-line as JSON events to * accumulate Message objects. * * The process continues running in the background. Use status() to * check completion and messages() to read accumulated output. * * Returns the session ID on successful spawn, or null on failure. */ prompt(id: string, options: { parts: Array<{ type: string; text: string; }>; noReply?: boolean; system?: string; agent?: string; model?: { providerID: string; modelID: string; }; }): Promise<{ id: string; } | null>; /** * Prompt a session synchronously — spawns the Pi CLI and awaits * process exit before returning the last assistant text part. * * Accepts an optional AbortSignal for cancellation. */ promptSync(id: string, options: { parts: Array<{ type: string; text: string; }>; agent?: string; signal?: AbortSignal; }): Promise<{ parts: Array<{ type: string; text?: string; }>; } | null>; /** * Return a snapshot copy of accumulated messages for a session. * Delegates to PiSessionAdapter for sessions not managed by this adapter. */ messages(id: string, options?: { directory?: string; limit?: number; }): Promise; /** * Get the session's current status. * * - For process-managed sessions: checks proc.exitCode. * null = busy (process still running), non-null = idle. * - For unknown sessions: delegates to PiSessionAdapter. */ status(id: string, _directory?: string): Promise; /** * Abort a running process session. * * Sends SIGTERM to the child process, with a 5-second fallback to * SIGKILL if the process does not terminate gracefully. */ abort(id: string): Promise; /** * Pi does not support session compaction — always returns false. */ compact(_id: string): Promise; /** * Reopen a session for continuation by reading its accumulated * messages, constructing a context-inclusive prompt that includes * the prior conversation, and spawning a fresh process. * * This is used by the dispatch system's task retry mechanism. * * @param id - The session ID to reopen. * @param newPrompt - The new prompt text to send. * @returns The session ID on success, or null on failure. */ reopenForContinuation(id: string, newPrompt: string): Promise; /** * List sessions. Delegates to PiSessionAdapter. */ list(directory?: string): Promise; /** * Get a single session. Delegates to PiSessionAdapter. */ get(id: string, directory?: string): Promise; /** * Get child sessions. Delegates to PiSessionAdapter. */ children(id: string, directory?: string): Promise; /** * Get todo items. Delegates to PiSessionAdapter. */ todo(id: string, directory?: string): Promise; /** * Get file diffs. Delegates to PiSessionAdapter. */ diff(id: string, options?: { directory?: string; messageID?: string; }): Promise; /** * Fork a session. Delegates to PiSessionAdapter (returns null * as forking via process is not supported). */ fork(id: string, options?: { directory?: string; messageID?: string; }): Promise; /** * Build a flat prompt text from the parts array. * Concatenates text parts with newline separators. */ private _buildPromptText; /** * Serialize accumulated messages into a text block for inclusion * as context in a continuation prompt. */ private _serializeMessagesForContext; /** * Resolve the Pi binary path. * Checks PI_BIN_PATH env var first, falls back to "pi" on PATH. */ private _resolvePiBinary; /** * Default timeout for child Pi processes in milliseconds (10 minutes). */ private static readonly DEFAULT_PROCESS_TIMEOUT_MS; /** * Write the system prompt to a temporary file, spawn the Pi CLI * process, and set up stdout/stderr/data/exit handlers. * * The spawned process uses: * pi --mode json -p --no-session --model --tools * --append-system-prompt "Task: " */ private _spawnProcess; /** * Relay one child-session activity heartbeat to the event bridge, throttled * to at most one per {@link ACTIVITY_HEARTBEAT_INTERVAL_MS} per record. * * The canonical event type mirrors the platform's own host-session mapping * (`tool_call` → `part.created`, `tool_result` / streaming updates → * `part.updated` — see `src/platform/adapters/pi/event-bridge.ts`), so the * graph engine's node-liveness relay (pi-extension.ts `heartbeatOn`) * recognizes it without any new vocabulary. Both types are inert to the * hook pipeline's dispatch-completion switch (`event-handler.ts` has no * `part.*` case) and to the notification manager, so relaying them never * mutates dispatch completion semantics — unlike `session.idle` / * `message.updated`, which the completion evaluator consumes. * * Fire-and-forget with a swallowed rejection, mirroring the existing * `session.idle` / `session.error` emission sites — a throwing bridge must * never break child event parsing. */ private _emitActivityHeartbeat; /** * Handle a single parsed JSON event from the Pi CLI's JSON mode stdout. * Mutates the ProcessRecord's messages array in place. */ private _handleJsonEvent; /** * Find a tool part by its call id, scanning accumulated messages backwards. * pi 0.81.x tool_execution_* events carry toolCallId but no message id, so * the match is keyed on the call id alone. */ private _findToolPart; /** * True for plain objects (not arrays/null) — used to decide whether a * tool_execution_update partialResult should be shallow-merged. */ private _isPlainObject; /** * True when the just-completed turn is the model's FINAL answer: the * last assistant message carries no tool parts (in-flight or settled) * and no tool-related stop reason. pi emits turn_end after EVERY turn, * so a turn_end whose last message still references tool calls is NOT * terminal — the agentic loop continues in a new turn, and completing * early would kill the child before it writes its answer (the "skill * echo" root cause). agent_end/agent_settled apply the SAME guard (they * must not complete a mid-tool-round-trip run); the per-process * timeout remains the backstop for agents that never produce a * tool-free turn. */ private _isFinalTurn; /** * Complete the session from the JSON event stream (turn_end). * * A live pi 0.81.1 `--mode json -p` child does not exit after the turn * finishes (verified empirically: after turn_end/agent_settled the * process idles indefinitely), so completion here mirrors what the * process exit handler used to do exclusively: * * 1. Resolve the pending promptSync wait with the accumulated last * assistant text. * 2. SIGTERM the still-running child so the record is not left * "busy" until the 600s timeout. * 3. Emit session.idle so completion evaluation runs while the * messages are still on the record (the exit handler's own idle * emission is suppressed via record.idleEmitted). * * Idempotent per record — safe if both turn_end and process exit fire. */ private _completeTurn; /** * Extract the last assistant text part from accumulated messages. * Searches messages in reverse order for the first assistant text. */ private _extractLastAssistantText; /** * Clean up the temporary directory and system prompt file. * Best-effort: failures are logged but do not throw. */ private _cleanupTempDir; } //# sourceMappingURL=process-session.d.ts.map