/** * serve.ts — WebSocket agent server. * * Exposes the agent runtime over a pluggable transport. Accepts AgentCommand * messages and streams AgentEvent responses. Uses WebSocketServerTransport for * the default transport (pass opts.transport to override for testing/stdio). * * Usage: * skaile serve --project-dir /project --port 8080 * * The platform's session manager connects via AgentClient + WebSocketClientTransport * to drive agent execution inside a local subprocess or Docker container. * * ─── Session Startup Pattern ─────────────────────────────────────────────────── * * All three runner modes (REPL, flow, serve) follow the same 5-step setup: * 1. resolveSettings() → driver, model, provider, API key * 2. loadAgentManifest() → manifest (model/maxTurns/tools); optionally systemPrompt * 3. buildAgentResources() → ConnectorManager, resourcePromptSection, mcpServers * 4. createDriver() → AgentDriver with assembled systemPrompt * 5. driver.start() → subprocess spawn / SDK import * * Differences between modes: * REPL — identity native (agentDir/agentName); systemPrompt = resource docs only; eager start * Flow — identity injected (full SOUL+RULES+knowledge); no transport; single orchestrator prompt * Serve — identity injected + external (SKAILE_SYSTEM_PROMPT_FILE); lazy start (first prompt cmd); * adds EventNormalizer, resource watch callbacks, lifecycle command handling * * The shared `createAgentSession(opts)` helper in session-builder.ts extracts the * common setup sequence. serve.ts retains: EventNormalizer, lazy start (driverStarted flag), * transport setup, and lifecycle command handling. */ import { type ManagedCodexHostOptions } from "./managed-codex-bootstrap.js"; import { type ConnectorHandle, ConnectorManager, type FlowAdapter, type TokenMediator } from "@skaile/workspaces/connectors"; import { type FlowDefinition } from "@skaile/workspaces/factory-assets/connectors/flow/engine"; import { type TurnStimulus } from "@skaile/workspaces/factory-assets/connectors/flow/prompt-fragments"; import type { AgentEvent, ConnectorsAvailableEvent, FlowExecution, FlowExecutionStatus, FlowHydrateReconciliation, InputSchema, Logger, PendingGateDecision, ServerTransport, StateChangedEvent } from "@skaile/workspaces/types"; import { CLAUDE_CODE_CREDENTIALS_KEY, ensureClaudeSettingsDisablesConnectors, extractClaudeAiOauthExpiresAt, writeClaudeCodeCredentialsFile } from "./serve-credentials.js"; import { type AgentSession } from "./session-builder.js"; /** * Emit a `system_prompt_composed` event carrying the labeled section * breakdown from the freshly-built {@link AgentSession}. Called from every * `createAgentSession` consumer in serve.ts (initial bootstrap, configure * re-runs, add_resource flow-connector path, aiResources skill changes, * session_init provisioned path) so the platform's debug panel always sees * the prompt the agent is actually receiving. * * Keeping the emission centralised in serve.ts (rather than inside * session-builder.ts) preserves the builder's transport-agnostic contract — * forge apps and the CLI consume the same builder without a transport. * * Spec: `docs/superpowers/specs/2026-05-13-platform-agent-prompt-design.md` * § 3.3.2 Re-emission requirement. * @since 2026-05 */ export declare function emitSystemPromptComposed(agentSession: AgentSession, sendEvent: (e: AgentEvent) => void): void; /** Minimal view of `ExternalMcpManager` needed to render the wire payload. */ export type McpServerLister = { listServers(): Array<{ id: string; transport: "stdio" | "sse" | "http"; toolCount: number; }>; }; /** * Which ConnectorManager the outer `resourceManager` should point at after a * dispose+recreate. `disposed` (captured pre-dispose) distinguishes the session's * own dead manager from a live lazily-created one that must survive. * Branches, known gap and rationale: `_devlog/notes/2026-07-17-recreate-resource-manager-repoint.md`. */ export declare function pickRepointedManager(next: T | null, current: T | null, disposed: T | null): T | null; /** Preserve standalone SDK compatibility; managed Codex refreshes its native tool snapshot. */ export declare function supportsLiveToolUpdates(driverType?: string, managedCodex?: boolean): boolean; /** * Build the `resources_available` wire payload: connector/mount split plus the * live MCP server list. Each MCP server is stamped with `live` — whether the * agent reaches its tools on the next turn without a session restart. * * A server is live when `driverLive` (bridge drivers recompose tools per turn) * OR it is in `bakedServerIds` — baked into the current driver at its * (re)creation, or stamped by `registerLiveAttachedMcp` for a hot-add whose * rebuild is already queued. A server not in the set gets `live: driverLive`; * omitting the argument is equivalent to an empty set. * * Exported for unit testing. */ export declare function buildResourcesAvailablePayload(manager: ConnectorManager | null, mcpManager: McpServerLister | null, driverLive: boolean, bakedServerIds?: ReadonlySet): ConnectorsAvailableEvent; /** * Whether a flow run has reached a status it can never leave. * * `failed` is deliberately NOT terminal: `retryNode` walks a recoverable * failure back to `available` and the adapter's recompute rolls the flow back * to `running`. `paused` resumes the same way. Only `complete` and `cancelled` * are one-way. * * Exported for unit testing. */ export declare function isTerminalFlowStatus(status: FlowExecutionStatus): boolean; /** The slice of a registered run the terminal-status readers need. */ type FlowStatusProbe = { adapter: Pick; handle: ConnectorHandle; }; /** * Whether any registered run is still live. Gates `flow.start` — a finished * run must not block the next one. * * Exported for unit testing. */ export declare function hasNonTerminalFlow(flows: Iterable): boolean; /** * Register a run, dropping any finished ones it supersedes. Returns the keys * removed (for logging). * * Registration is the ONLY point that ages the map out, and deliberately so: a * finished run must stay addressable (the bare-`"flow"` id rewrite, and the * replay that delivers its terminal snapshot to a host that was detached when * it settled) right up until a new run actually replaces it. Deleting on the * terminal transition instead would drop both. * * Exported for unit testing. */ export declare function registerActiveFlow(flows: Map, runId: string, entry: T): string[]; /** The order-critical steps of a flow-connector bring-up, in the order they run. */ export interface FlowConnectorSetupDeps { /** Recreate the agent session when the driver has already started. */ recreateForFlowTools: () => Promise; /** The ConnectorManager the flow connector belongs on, lazily created if absent. */ resolveManager: () => M; /** Register the flow connector on that manager. */ connect: (manager: M) => Promise; } /** * Bring up a flow connector on the manager that SURVIVES a driver recreate. * * The recreate disposes the session's ConnectorManager (`dispose()` → * `disconnectAll()` clears `active`) and repoints the runner at the new * session's own. Connecting first left the run registered on a torn-down * manager — and `buildSdkFlowTools` closes over it, so its tool handlers were * bound to the dead one too, wedging the run at `running` forever * (workspaces#484). Hence: recreate, THEN resolve, THEN connect. * * Exported for unit testing — this ordering is the fix. */ export declare function setUpFlowConnector(deps: FlowConnectorSetupDeps): Promise<{ manager: M; handle: ConnectorHandle; recreated: boolean; }>; /** * Build the `state_changed` events that replay the current snapshot of every * live state-bearing store, for delivery when a subscriber (re)attaches. * * `sendEvent` is fire-and-forward: state emitted before the host's sync loop * attaches is lost. Rather than buffer the lost events, we re-emit current * snapshots — idempotent latest-wins on the host, so a late subscriber * converges and an already-current one re-applies identical state. * * Flow stores read the authoritative FlowExecution straight off the adapter * (same shape `onStateChange` emits) so there is no per-type read-op guessing. * Shared-state stores are identified by the xstate-store `onStoreChange` hook * and read via `get`; git/mount/MCP connectors have no state op and are skipped. * * A run that already reached a terminal status is replayed too, deliberately * (workspaces#413): the turn loop keeps running with no host attached, so * `complete`/`cancelled` can be reached mid-detach — and this replay is then * the ONLY path by which the host ever learns. Filtering it would leave the * platform's persisted row stuck non-terminal and later re-hydrate a finished * run. Registration prunes the settled entry instead, so the re-emit is * bounded, not forever. * * Exported for unit testing. */ export declare function buildStoreSnapshotReplay(manager: ConnectorManager | null, activeFlows: Iterable<{ runId: string; adapter: FlowAdapter; handle: ConnectorHandle; }>): Promise; /** * Phase 4.8 — Claude Code subscription credentials provisioning. * * The constant + the atomic rewrite helper now live in `./serve-credentials.ts` * so the AI 401 mediation helper can reuse them without importing from this * (large) module. Re-exported here for back-compat with existing test * imports of `serve.js`. * * @docLink packages/runner/dev-guide#environment-variables */ export { CLAUDE_CODE_CREDENTIALS_KEY, ensureClaudeSettingsDisablesConnectors, extractClaudeAiOauthExpiresAt, writeClaudeCodeCredentialsFile, }; /** * Idempotently ensure `~/.gitconfig` includes the runner-managed gitconfig * via `[include] path = `. Fallback path for shells * that strip `GIT_CONFIG_GLOBAL`. The function is idempotent — running it * twice produces the same file content. * * Exported for unit tests. * @docLink packages/runner/capabilities#ensure-git-config-include */ export declare function ensureGitConfigInclude(managedGitconfigPath: string): void; /** * Configuration for {@link startAgentServer}. * @docLink packages/runner/dev-guide#flow-execution-turn-based-model */ export interface ServeOptions { /** Trusted launcher inputs only; credentials arrive through validated session_init. */ managedCodex?: ManagedCodexHostOptions; /** * WebSocket server port. Defaults to `8080`. * Ignored when a custom {@link transport} is provided. */ port?: number; /** Absolute path to the project working directory. */ projectDir: string; /** * GitAgent definition directory (`agent.yaml`, `SOUL.md`, `RULES.md`, * `knowledge/`). When omitted, resolved from `skaile.yaml` via * `resolveAgentDir`. If unresolvable, no agent definition is loaded and the * agent starts without an identity prompt. */ agentDir?: string; /** * Exit the process with 141 when stdout's pipe dies. Defaults to `false`, * because `startAgentServer` is a library entry point and an embedder's * stdout is not ours to kill. The `skaile serve` CLI, which does own its * process, opts in — see the runner's `process-safety-net`. * * The runaway-logging fix does not depend on this: the sinks go quiet * either way. Only the exit is gated. */ exitOnBrokenPipe?: boolean; /** Agent driver backend override (e.g. `"omp"`, `"claude-sdk"`). */ driver?: string; /** LLM model name override. */ model?: string; /** LLM provider override (e.g. `"anthropic"`, `"openrouter"`). */ provider?: string; /** Directory of deployed prompt/command files appended to the system prompt. */ promptsDir?: string; /** Stable session ID for conversation continuity across agent turns. */ sessionId?: string; /** * OMP driver session ID to resume. When set, OMP is spawned with * `--session ` to restore its native conversation context without * needing `` injection. For OMP driver only. */ resumeSessionId?: string; /** Called for each diagnostic log line. Defaults to `console.log`. */ onLog?: (line: string) => void; /** * Pre-built transport to use instead of the default * `WebSocketServerTransport`. Useful for testing (in-process transport, * stdio transport) or embedding the server in another process. */ transport?: ServerTransport; /** * Bearer token the default WebSocket server requires from connecting clients. * Falls back to `SKAILE_WS_AUTH_TOKEN`. When neither is set the server accepts * unauthenticated connections (back-compatible default). Ignored when a custom * {@link transport} is provided. */ authToken?: string; } /** * Map a connector driver to the platform `host.refresh_credential` `kind`, or * `null` when the driver has no in-container refresh path. The platform handler * mints `git-mount`, `sharepoint-mount`, and `webdav-mount`, and the matching * rclone drivers carry the full apply path (proactive scheduler + reactive * `onAuthError` → re-render INI → stop+respawn), so all three refresh in place. * `googledrive` is intentionally excluded: its driver has no refresh controller * yet, so mapping it would mint tokens nothing applies — wire the driver first. */ export declare function connectorRefreshKind(driver?: string): string | null; /** * Build the connector token-refresh mediator. The `ConnectorManager`'s pre-mint * wrapper serves `reason:"initial"` from the pre-minted credentials and forwards * every `refresh` / `retry-401` here, so a backend-auth git mount rotates its * token through `host.refresh_credential` instead of going stale. The platform * handler resolves the credential by **provider link id**, so this passes * `id: providerLinkId` (NOT the connector id). Returns a structured * `not-configured` mint — never throwing — when there is no transport, the * driver has no backend refresh path, or the declaration carries no * `providerLinkId` (standalone/CLI and non-git connectors degrade as before). */ export declare function buildConnectorTokenMediator(deps: { isConnected: () => boolean; invokeRemote: (name: string, input: unknown) => Promise; log: (line: string) => void; }): TokenMediator; /** * Decision the runner makes for a `connector_mutate` that targets the `flow` * connector. Pure so it can be unit-tested without a live server. See * `MIGRATION-flow-connector.md` for the wire contract this encodes. * * - `create-start` — fresh run: auto-create `flow:` from `{ flow, seed }`. * - `create-hydrate`— cold-container rehydration (Protocol 3.5): auto-create * `flow:` directly from the `{ state, flow }` snapshot. * - `dispatch` — route to `ConnectorManager.executeOp(dispatchId, op, payload)`, * rewriting a bare `"flow"` id to the singleton active run. * - `reject` — malformed / conflicting flow op; caller logs `reason` loudly * instead of silently falling through. */ export type FlowMutatePlan = { kind: "create-start"; flowDef: FlowDefinition; seed: { runId: string; startedBy: string; autonomousMode?: boolean; }; } | { kind: "create-hydrate"; flowDef: FlowDefinition; execution: FlowExecution; } | { kind: "dispatch"; dispatchId: string; } | { kind: "reject"; reason: string; }; /** * Resolve a `connector_mutate` into a {@link FlowMutatePlan}. * * Hosts address the flow connector by its bare driver id `"flow"`; the runner * registers the per-run connector at `flow:` and enforces one *running* * flow per session. `start` and (on a cold container) `hydrate` auto-create * that connector; every other op dispatches to the existing run. */ export declare function planFlowMutate(cmd: { id: string; op: string; payload?: Record; }, ctx: { /** `resourceManager.has(cmd.id)` — a connector is registered under this exact id. */ targetExists: boolean; /** The registered target is a `flow` connector. */ isExistingFlow: boolean; /** * `activeFlows.size > 0` — any run is registered, terminal or not. Gates * the cold-bootstrap `hydrate` branch: re-creating a connector at an id * that already exists silently overwrites the live handle. */ hasActiveFlow: boolean; /** * A registered run is still in a non-terminal status. Gates `start` — a * finished run must not block the next one. */ hasRunningFlow: boolean; /** Connector id of the singleton active run, or null when none. */ onlyActiveConnectorId: string | null; }): FlowMutatePlan; /** Fallback bound for {@link resolveFlowMutateConnectorWaitMs}. */ export declare const DEFAULT_FLOW_MUTATE_CONNECTOR_WAIT_MS = 8000; /** * Read the flow-connector readiness bound from * `SKAILE_FLOW_MUTATE_CONNECTOR_WAIT_MS`, falling back to * {@link DEFAULT_FLOW_MUTATE_CONNECTOR_WAIT_MS} on unset / unparseable / * non-positive input. * * Gotcha: the platform's `connector_mutate` ack timeout is deliberately sized * *above* this value, so raising it without raising the host side turns a slow * wait into a host-visible timeout. */ export declare function resolveFlowMutateConnectorWaitMs(env?: Record): number; /** Bounded readiness gate for the flow connector. See {@link createFlowConnectorReadiness}. */ export interface FlowConnectorReadiness { /** * `true` when a flow connector exists already or registers before the bound. * * `isSatisfied` overrides the gate's default predicate for this wait only. * Pass one when the caller needs a *specific* run rather than "any active * flow" — a terminal run lingers in `activeFlows` until the next registers * (see `registerActiveFlow`), so the default would resolve early for a * caller addressing an explicit `flow:`. */ wait(timeoutMs: number, isSatisfied?: () => boolean): Promise; /** Release every parked waiter whose predicate now holds. Called as soon as a run registers. */ drain(): void; /** Waiters still parked — test seam for the no-leak assertion. */ pending(): number; } /** * Narrower sibling of the session-wide `sessionReady` gate, for the flow * connector specifically: a flow-targeted mutate that loses the wake race to * the bootstrap `hydrate` (observed 2 ms apart in production) parks here * instead of failing permanently. * * Invariants: `wait` clears its timer when drained and removes its own waiter * when it times out — this runs on every gated mutate, so either leak is real. * `drain` splices the list before invoking, so a waiter registered *during* a * drain is not dropped. */ export declare function createFlowConnectorReadiness(isReady: () => boolean): FlowConnectorReadiness; /** * Shallow structural check of a replayed gate response against the node's own * {@link InputSchema}. * * Invariants: never throws, returns a boolean, and never reads the response * into a log line, an error message or the acknowledgement — it is unbounded * user content. An unrecognised or absent schema is permissive on purpose: a * schema the runner does not understand must not wedge a legitimate replay. */ export declare function validateInputResponse(schema: InputSchema | undefined, response: unknown): boolean; /** * Classify each `payload.pendingDecisions` entry of a flow `hydrate` against * the snapshot the host sent in the same command, splitting them into the ones * the caller should apply and a structured report for the acknowledgement. * * Pure — no I/O, no logging. `pending` is untrusted wire data of unknown shape. * * Invariants: * - Parked-ness is checked *before* decided-ness, so a re-parked node reads as * `not_parked` rather than `already_decided`. * - An entry with no usable `nodeId` is dropped silently; there is nothing to * report it against. Everything else lands in exactly one of the two lists. * - A skip entry carries only `{ nodeId, reason }` — never the decision payload. */ export declare function reconcilePendingDecisions(execution: FlowExecution, pending: unknown): { toApply: PendingGateDecision[]; reconciled: FlowHydrateReconciliation; }; /** * Map a host-op `connector_mutate` (applyApproval/applyInput/retryNode) to the * typed {@link TurnStimulus} that must explicitly wake the agent; `null` for * ops that don't resume a turn (cancel, setAutonomousMode, hydrate, start). * * These need an explicit kick because the generic mutate→computeStimulus path * only emits `state_changed` and can miss an approved-only change (status/focus * unchanged), so the gate would hang. * * Pure — depends only on its inputs. * * @docLink packages/runner/concepts#host-op-resume-stimulus */ export declare function hostOpResumeStimulus(op: string, payload: Record): TurnStimulus | null; /** The `connector_mutate` fields {@link executeConnectorMutate} reads. */ export type ConnectorMutateInput = { id: string; op: string; payload?: Record; /** Opt-in ack correlation. Absent ⇒ no `connector_mutate_response`, ever. */ requestId?: string; }; /** A registered flow run, as {@link executeConnectorMutate} needs to address it. */ export type ActiveFlowTarget = { runId: string; connectorId: string; /** Whether hydrate compatibility preparation changed the host snapshot. */ hydratedStateChanged?: boolean; }; /** * Everything {@link executeConnectorMutate} needs from the `serve` closure. * Injected rather than captured so the handler's terminal paths — including the * ones that used to be swallowed — are directly testable. */ export interface ConnectorMutateDeps { /** The session-wide readiness gate; rejects when the session build failed. */ sessionReady: Promise; sendEvent: (event: AgentEvent) => void; /** Must *throw* when no ConnectorManager exists yet (3.6 behaviour). */ assertResourcesReady: () => void; hasConnector: (id: string) => boolean; /** Whether the connector registered at `id` is a flow connector. */ isFlowConnector: (id: string) => boolean; executeOp: (id: string, op: string, args: Record) => Promise; /** Re-read on every (re-)plan so a run registered during the wait is seen. */ flowState: () => { hasActiveFlow: boolean; hasRunningFlow: boolean; activeFlow: ActiveFlowTarget | null; }; createActiveFlow: (flowDef: FlowDefinition, seedOrExecution: { seed: { runId: string; startedBy: string; autonomousMode?: boolean; }; } | { execution: FlowExecution; }) => Promise; /** * Bounded flow-connector readiness wait; `true` when one registered in time. * `isSatisfied` narrows the gate to a specific connector id — see * {@link FlowConnectorReadiness.wait}. */ waitForFlowConnector: (timeoutMs: number, isSatisfied?: () => boolean) => Promise; connectorWaitMs: number; /** Fire-and-forget turn kick — must not block the acknowledgement. */ signalStimulus: (target: ActiveFlowTarget, stimulus: TurnStimulus) => void; log: Pick; } /** * Body of the runner's `connector_mutate` handler. * * Emission contract (Protocol 3.7): **exactly one** `connector_mutate_response` * per command that carried a `requestId`, on every terminal path including the * ones that previously only logged — and **never** for a command without one. * That opt-in is the entire 3.6 back-compat story, so the `respond` helper below * is the only place an ack is produced. * * Gotcha: the turn kicks stay fire-and-forget and stay *after* the ack decision. * The ack means "the mutation applied", not "the turn ran". */ export declare function executeConnectorMutate(cmd: ConnectorMutateInput, deps: ConnectorMutateDeps): Promise; /** * Pick the stimulus for a coalesced batch of bus signals. A host-op resume kick * rides in the same batch as the adapter's generic `state_changed`; scanning * from the most recent, the most-specific typed stimulus wins so a trailing * `state_changed` can't mask it. Falls back to `state_changed` for an * all-generic batch. * * Pure — depends only on its inputs. * * @docLink packages/runner/concepts#stimulus-from-metas */ export declare function stimulusFromMetas(metas: Array>): TurnStimulus; /** * Start a WebSocket agent server that the Skaile platform connects to for * host-driven agent execution inside a local subprocess or Docker container. * * The server accepts {@link AgentCommand} messages over a * {@link ServerTransport} (default: `WebSocketServerTransport` on `opts.port`) * and streams {@link AgentEvent} responses back to the connected client. * * Handled commands: * - `provision_secrets` — injects API keys and Claude Code credentials; in * `provisioned` secrets mode, triggers deferred session creation. * - `configure` — registers shared state stores and applies mid-session skill * catalog changes. There is no envelope-level flow rehydration; hosts restore * a persisted run with a single `connector_mutate { id: "flow", op: "hydrate", * payload: { state, flow } }` (auto-creates the connector on a cold container, * Protocol 3.5). See `MIGRATION-flow-connector.md`. * - `prompt` — routes free-chat messages through the SessionStimulusBus when * a flow is active, otherwise forwards directly to the driver; **driver is * lazily started on the first `prompt` command**. * - `connector_mutate` / `connector_query` — generic connector dispatch. * Replaces the 6 typed flow commands removed in Protocol 2.2 (`start_flow`, * `approve_flow_node`, `provide_flow_input`, `cancel_flow`, * `set_flow_autonomous_mode`, `retry_flow_node`); see * `MIGRATION-flow-connector.md` for the per-op mapping table. * - `add_resource` / `remove_resource` — mount/unmount mounts or * connect/disconnect connectors mid-session. When the added connector's * driver is `flow`, the runner injects the per-session stimulus bus into * the connect options. * * The function resolves after the transport starts listening. It does not * resolve until the process exits — shutdown is driven by SIGINT/SIGTERM or * a `shutdown` command from the connected client. * * @param opts - Server configuration. * @docLink packages/runner/dev-guide#flow-execution-turn-based-model */ export declare function startAgentServer(opts: ServeOptions): Promise; //# sourceMappingURL=serve.d.ts.map