Generate AGENT.md at ~/.uncaged-nerve root during nerve init (layout, verb-first workflows, createRole four-tuple, root build, coding style). Role prompts instruct agents to use cat AGENT.md instead of node_modules nerve-skills paths. E2E init test asserts AGENT.md. Retain .knowledge workflow/adapter updates and flat single-file roles guidance from the branch. Fixes #287 Made-with: Cursor
3.5 KiB
Agent Adapters (RFC-003)
Adapter = capability. Role = scenario. Workflows declare adapters directly via import.
AgentFn Protocol
type AgentFn = (ctx: ThreadContext, systemPrompt: string) => Promise<string>
- Input: thread context (
{ threadId, start, steps }) + system prompt (role identity) - Output: single-shot
Promise<string>— no streaming support - Adapter handles tool-specific details internally
Streaming Limitations
The AgentFn protocol does not support streaming responses (AsyncIterable<string> or ReadableStream). It's strictly limited to single-shot Promise<string> 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.
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:
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:
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_failederror when CLI binary not found in$PATH - Non-zero exit code:
non_zero_exiterror with captured stdout/stderr - Timeout:
timeouterror when execution exceeds configuredtimeoutMs - Abort signal:
abortederror whenAbortSignaltriggers 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.