import { type LeafEventBody, type LeafResume, type ToolHost, type LspService, type McpTokenResult } from "@boardwalk-labs/engine/core"; import type { AgentOptions } from "@boardwalk-labs/workflow/runtime"; import type { BudgetMeter } from "./agent/budget.js"; import type { SecretRedactor } from "./agent/secret_redactor.js"; import type { AgentIdentity, RunEventBody, TurnEventSink } from "./agent/events.js"; import type { InferenceProxyTransport } from "./inference_transport.js"; import { type DirectInferenceDeps } from "./direct_inference.js"; import type { LeafExecutor } from "./workflow_host.js"; import type { DesktopSessionManager } from "./desktop_session.js"; /** Builds the per-leaf event sink (`TurnEventSink`) for the leaf's `leafIndex`-numbered turn. */ export type EventSinkFactory = (leafIndex: number, identity: AgentIdentity) => TurnEventSink; /** Per-leaf, per-model token metering input (fire-and-forget → the broker). */ export interface MeterUsageInput { model: string; inputTokens: number; outputTokens: number; /** Cache-served input tokens for this leaf (display-only annotation; omitted when absent). */ cachedReadTokens?: number; cachedWriteTokens?: number; leafIndex: number; } export interface EngineLeafExecutorDeps { /** Streams one model turn through the broker (RunnerControlClient satisfies it). */ inference: InferenceProxyTransport; /** Runner-direct BYO inference (the self-hosted runner design): when a per-`agent()` provider * matches this registry (key-based HTTP sources only), the turn goes STRAIGHT to the org's * endpoint with the engine adapters — the managed lane and bedrock stay brokered. Omitted ⇒ * everything brokered (legacy dispatch without a registry). */ byo?: DirectInferenceDeps; /** RUN-level meter, shared across every leaf so caps + token totals span the whole run. The engine * reports usage after EVERY model call via `reportUsage`; we feed it here and throw on a cap * breach so the loop terminates mid-flight (the budget authority). */ budget: BudgetMeter; /** * Budget clearance, awaited before EVERY model call (docs/SUSPEND_POLICY.md Decision 3). When ANY * of the run's budget caps (`max_usd` / `max_tokens` / `max_compute_seconds`) is breached this * PARKS the run at a gate — the engine just sees a model call that took a long time, because the * VM froze and resumed underneath it — and resolves once a responder raises the breached cap. * Rejects with `BudgetGateCancelled` if they decline. * * Why here and not at the `reportUsage` breach check below: that callback is synchronous and fires * mid-turn, and per SUSPEND_POLICY Decision 1 a partial turn is never discarded. The model-call * boundary is the first safe place to park, which bounds overrun at one in-flight turn per leaf. * (The workflow host adds further park points at its `sleep`/`shell`/`workflows.call` seams for * the continuously-burning compute cap.) * * Absent ⇒ the legacy behavior: a breach fails the run at `reportUsage`. */ budgetGate?: { clear(): Promise; }; /** RUN-level redactor (shared with the secret resolver): every resolved secret value is recorded * here. We seed a fresh engine `Redactor` from it per leaf so the loop scrubs known values out of * the prompt, tool args/results, and transcript before they reach the model. */ redactor: SecretRedactor; /** Builds this leaf's event sink; `leafIndex` (1-based) is only a metering identifier — the sink * owns the run-global cursor. `identity` names the leaf on its turn frames. */ makeEventSink: EventSinkFactory; /** The run's persistent `/workspace` root — memory dirs (`agent({ memory })`) are relative to it. */ workspaceRoot: string; /** Register a memory dir the run actually used, so the workspace store persists it (§3 of * docs/WORKSPACE_PERSISTENCE.md). Memory is undeclared by design, so this callback is the ONLY * signal that the dir must compound — without it a `agent({ memory })`-only workflow silently * persists nothing, which is exactly what shipped. Optional: absent ⇒ validation only. */ onMemoryUsed?: (dir: string) => void; /** Resolves the directory holding this run's bundled files (the extracted program tree, where a * skill lives at `skills/.md`). Known only once the artifact is extracted (mid-run), so it's * a thunk. Null / omitted ⇒ a leaf that names `skills` fails loud. */ skillsDir?: () => string | null; /** Resolves the run's workflow PACKAGE root — the extracted program tree, whose ROOT holds the * author's standing instructions (`/AGENTS.md`) and `skills/` beside them. The engine * reads `capabilities.programDir` for the BUNDLED `AGENTS.md` tier, read by every agent() before * any AGENTS.md the run cloned into its workspace. A thunk for the same reason `skillsDir` is (the * dir is known only once the artifact extracts); it is in fact the parent of `skillsDir`. Null / * omitted ⇒ no bundled tier (only the workspace AGENTS.md applies). */ programDir?: () => string | null; /** Per-leaf token metering seam (fire-and-forget). Reports THIS leaf's tokens + its model to the * broker, which decides `billed_by_boardwalk` per model + meters usage to the platform. Omitted in tests. */ meterUsage?: (input: MeterUsageInput) => void; /** Backend for the engine's host-backed built-in tools (`webfetch` / `web_search` / `artifacts`): * set as the leaf's `capabilities.host` so the engine registers them. Broker-backed on hosted runs * (BrokerToolHost). Omitted ⇒ those three tools are simply absent (the engine never registers a * host-backed tool whose hook the host doesn't provide). */ toolHost?: ToolHost; /** Per-run desktop-session manager. A leaf bound to a desktop session (`opts.session`) gets the * desktop ToolHost hooks (screenshot/click/type/key/scroll/drag) assembled from it PER LEAF — * session-gated by construction — and its model turns are stamped `desktopSession: true` so the * broker's grounder gate applies. Omitted ⇒ a desktop-bound leaf fails clearly. */ desktopSessions?: DesktopSessionManager; /** Engine-native LSP for the `diagnostics` tool + diagnostics-after-edit. Set as the leaf's * `capabilities.lspService`. Constructed ONCE per RUN (not per leaf) so the language server stays * warm across the run's edits/leaves, and closed on the run's teardown. Omitted ⇒ the `diagnostics` * tool and after-edit diagnostics are best-effort-skipped (the correct degradation). */ lspService?: LspService; /** Brokers a short-lived OAuth bearer for a hosted MCP server. The engine calls this REACTIVELY — * only after a server answers 401 to the static `headers` — so static-bearer servers never reach * it. Routes to the Runner Control API's `mcp/token` vend endpoint (the OAuth token state lives in * the control-plane vault, never on the worker). Omitted ⇒ no OAuth brokering: a server that needs * a token gets `{ accessToken: null }` and the leaf fails loud with a clear hint (the correct * degradation — static-bearer and no-auth servers still work). */ brokerMcpToken?: (serverUrl: string, invalidateToken?: string) => Promise; } /** * Per-run leaf executor. The worker constructs one bound to the run + run-level budget, and wires it * as the `LeafExecutor` on the run's WorkflowHost, so every `agent()` the program calls runs here * through the engine's loop. */ export declare class EngineLeafExecutor implements LeafExecutor { private readonly deps; private leafCount; constructor(deps: EngineLeafExecutorDeps); run(prompt: string, opts: AgentOptions | undefined, signal?: AbortSignal, resume?: LeafResume): Promise; /** Resolve a desktop `opts.session` to its per-leaf ToolHost (base hooks + the desktop hooks) and * the handle-stripped opts; null when the leaf has no desktop session. */ private bindDesktopSession; /** Assemble the broker-backed `LeafIo` the engine loop drives for one leaf call. */ private buildLeafIo; /** POST one model turn to the broker's `/inference` and adapt its NDJSON stream into the engine's * ModelTurnResult: each `delta` frame drives providerIo.onDelta; the terminal `result` frame is * the turn. An `error` frame throws (the broker already classified it). An abort mid-stream throws. */ private streamModel; } /** The MCP server refs an `agent({ mcp })` call may name, derived from the SDK's `AgentOptions`. */ type HostedMcpServerRef = NonNullable[number]; /** * Gate the MCP servers a hosted leaf may use: only the `http` transport (no arbitrary `stdio` * processes on the worker) and a parseable URL. Throws a clear VALIDATION_FAILED for the first * offending ref so the leaf fails at its boundary, before the engine connects out. * * Reachability of the host is NOT gated here: a hosted run's egress is OPEN by default (the forward * proxy allows all public destinations; a workflow restricts it via manifest.egress), and the proxy * is the single enforcement point — a server blocked by a restrictive egress fails at the proxy when * the engine connects, not via an MCP-specific allowlist that would be stricter than the platform. */ export declare function assertHostedMcpAllowed(refs: readonly HostedMcpServerRef[] | undefined): void; /** * Project an engine `LeafEventBody` onto the platform's v1 `RunEventBody`. Both are the SDK's v1 * event kinds (the platform consumes `@boardwalk-labs/workflow`; the engine emits the same shapes), * so this is a near-identity copy — `turn_ended` re-stamps the leaf identity the platform tracks, * and the body is otherwise passed through verbatim. A discriminated switch keeps it exhaustive * (any new engine kind surfaces as a compile error here, not a silent drop). */ /** Engine LeafEventBody → the platform's v1 RunEventBody. Exported for testing. */ export declare function toRunEventBody(body: LeafEventBody, identity: AgentIdentity): RunEventBody; export {};