# Agent Adapters (RFC-003) Adapter = capability. Role = scenario. Workflows declare adapters directly via import. ## AgentFn Protocol ```ts type AgentFn = (ctx: ThreadContext, systemPrompt: string) => Promise ``` - Input: thread context (`{ threadId, start, steps }`) + system prompt (role identity) - Output: **single-shot `Promise`** — no streaming support - Adapter handles tool-specific details internally ### Streaming Limitations The `AgentFn` protocol does **not** support streaming responses (`AsyncIterable` or `ReadableStream`). It's strictly limited to single-shot `Promise` returns. For long-running or incremental agent outputs: - CLI tools buffer full output until completion - Timeout enforcement via `timeoutMs` (default 300s) - No intermediate results exposed to workflow logic - Progress indication happens at the CLI tool level only ## Available Adapters | Package | Adapter | Tool | |---------|---------|------| | `@uncaged/nerve-adapter-cursor` | `cursorAdapter` / `createCursorAdapter()` | cursor-agent CLI | | `@uncaged/nerve-adapter-hermes` | `hermesAdapter` / `createHermesAdapter()` | hermes chat CLI | | `@uncaged/nerve-workflow-utils` | `createLlmAdapter(provider)` | OpenAI-compatible HTTP chat (single-turn) | The Cursor and Hermes adapter packages each export a **default instance** (sensible defaults) and a **factory** for custom config. `createLlmAdapter` is a factory on `@uncaged/nerve-workflow-utils` only. ## createLlmAdapter `createLlmAdapter` builds an `AgentFn` from an `LlmProvider` (`baseUrl`, `apiKey`, `model`). One chat completion per role step: **system** = the string passed by `createRole` (your prompt); **user** = `ctx.start.content` (the thread’s start frame). On failure it throws with a formatted LLM error. ```ts import { createLlmAdapter, createRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; const metaSchema = z.object({ ok: z.boolean() }); const planner = createRole( createLlmAdapter({ baseUrl: "https://api.example.com/v1", apiKey: "…", model: "gpt-4o-mini" }), "You are a planner…", metaSchema, extractConfig, ); ``` Use this when you want a role backed by an HTTP LLM instead of a subprocess CLI adapter. ## Usage in Workflows Adapters are passed directly to `createRole`: ```ts import { createRole } from "@uncaged/nerve-workflow-utils"; import { cursorAdapter } from "@uncaged/nerve-adapter-cursor"; const coder = createRole(cursorAdapter, prompt, schema, extractConfig); ``` No registry, no config indirection. TypeScript catches missing adapters at compile time. ## Extract Layer Parses agent raw string → typed meta. Configured in `nerve.yaml`: ```yaml extract: provider: dashscope model: qwen-plus ``` Two-level merge: global → role override. Retry once on parse failure (feeds error back to LLM), then throw `ExtractError`. ## Error Handling When adapters' underlying CLI tools (e.g., `cursor-agent` or `hermes`) fail, errors are surfaced **synchronously via rejection** with no fallback/retry logic: - **Missing/unavailable tool**: `spawn_failed` error when CLI binary not found in `$PATH` - **Non-zero exit code**: `non_zero_exit` error with captured stdout/stderr - **Timeout**: `timeout` error when execution exceeds configured `timeoutMs` - **Abort signal**: `aborted` error when `AbortSignal` triggers cancellation All errors are immediately thrown as `Error` instances with descriptive messages (e.g., `"cursor-agent: exitCode=7 stdout=... stderr=..."`). No automatic retries or fallback adapters.