import type { PlatformAdapter, DownloadedFile, IncomingMessage } from '../platform/index.js'; import type { ContextUsage, SessionContextUsage } from '../core/types/agent-types.js'; import { type TurnMutationRelease } from './turn-mutation-lock.js'; type Enqueuer = (channel: string, fn: () => Promise) => boolean; type Tracker = (delta: number) => void; type PlatformFileLoader = () => Promise; type Executor = (ctx: AgentRunnerCtx, mutationRelease: TurnMutationRelease, loadPlatformFiles: PlatformFileLoader) => Promise; /** Attempt mid-turn injection; true ⇒ the message was delivered into the live turn and must NOT * be queued. Injectable so the routing branch is testable without a live backend. */ type Injector = (ctx: AgentRunnerCtx, loadPlatformFiles: PlatformFileLoader) => Promise; interface AgentConfig { effectiveMessage: string; profileForRun: string; defaultAgentName: string | null; claudeAgent: string | null; systemPrompt: string | null; outputStyle: string | null; tools: string | null; pluginDirs: string[] | null; } export interface AgentRunnerCtx { message: IncomingMessage; channel: string; adapter: PlatformAdapter; threadAnchorId: string | null; hasFiles: boolean; userMessage: string; agentMessage: string; mutationRelease?: TurnMutationRelease; } export declare class AgentRunner { readonly _enqueue: Enqueuer; readonly _track: Tracker; /** Injectable for unit tests — allows verification of track(-1)-in-finally without spawning Claude. */ readonly _execute: Executor; readonly _tryInject: Injector; constructor(opts?: { enqueue?: Enqueuer; track?: Tracker; execute?: Executor; tryInject?: Injector; }); route(ctx: AgentRunnerCtx): Promise; private _routeWithAdmission; private _runQueued; /** * Production mid-turn injection: resolve this channel's live turn and session, then hand the * message to `mid-turn-inject`. Ordered cheapest-gate-first because route() is the hot path for * every inbound message — the store lookups only run once a live turn is actually present. */ private _tryInjectReal; private _executeReal; } export declare const agentRunner: AgentRunner; export interface SessionContextUsagePersistenceDeps { now: () => string; update: (sessionName: string, updates: { contextUsage: SessionContextUsage; }) => Promise; publish: (snapshot: { sessionId: string; channel: string; } & SessionContextUsage) => void; } /** Persist first, then publish the identical live snapshot so query and event clients converge. */ export declare function persistSessionContextUsage(input: { sessionName: string; sessionId: string; channel: string; usage: ContextUsage; }, deps?: SessionContextUsagePersistenceDeps): Promise; /** Dependencies for {@link emitTurnProgress} — side effects injected for testability. */ export interface TurnProgressDeps { sessionId: string | null; channel: string; executionId: string | null; /** Update the live agent-turn count on the running execution (snapshot for sessions.list). */ setNumTurns: (numTurns: number) => void; /** Publish the `session.turn` delta for the live composer. */ publish: (numTurns: number) => void; } /** * Translate an adapter `turn_progress`/`turn_complete` payload into the S4 chat's live agent-turn * signals: update the running execution's numTurns snapshot (when an executionId is known) and * publish a `session.turn` delta (when a sessionId is known). No-op unless `num_turns` is a finite * number — a progress event without a turn count carries nothing to show. Exposed for unit testing. */ export declare function emitTurnProgress(deps: TurnProgressDeps, progress: { num_turns?: unknown; } | null | undefined): void; /** Exposed for unit testing. */ export declare function resolveDefaultAgent(agentMessage: string, channel?: string): AgentConfig; export declare function resolveSessionName(sessionId: string | null, channel: string, userMessage: string, adapter: PlatformAdapter): Promise; /** * Build interactive callbacks for plan_written, ask_user_question, and tool_use events. * These fire during the turn (not after) and publish bus events so the existing * Slack interaction flow handles them. * * Exported so that all runThread call sites (thread-executor, scheduled-task, * task-dispatch) can wire these callbacks — without them, ask_user_question * events are silently dropped and the subprocess blocks forever. * * ORDERING INVARIANT: onToolUse fires synchronously before onAskUserQuestion * within the same event-loop tick (guaranteed by the PI adapter's sequential * event processing in event-parser.ts). The closure variables pendingAskInput * and pendingExitPlanMode rely on this ordering — onToolUse captures state * that onAskUserQuestion consumes. If the adapter ever processes events * asynchronously or out of order, this contract breaks silently. */ export declare function buildInteractiveCallbacks(channel: string, sessionId: string | null, threadId?: string | null): { onPlanWritten: (event: { path: string; content: string; toolUseId: string; }) => void; onAskUserQuestion: (event: { toolUseId: string; questions: Array<{ question: string; options?: string[]; multi?: boolean; }>; }) => void; onToolUse: (name: string, input: any) => void; }; export {};