rfc-003: adapter as direct function reference, not string

- RoleSpec.adapter: string → AgentFn (direct import)
- Each adapter exports default instance + factory
- No adapter map, no registry, no lookup — compile-time safety
- TypeScript catches missing adapters at import time

Refs #245
小橘 🍊(NEKO Team)
This commit is contained in:
2026-04-29 08:23:59 +00:00
parent 732669fab5
commit 4be465918c
+30 -33
View File
@@ -74,13 +74,16 @@ extract:
Roles declare their adapter directly — no indirection through named agents:
```ts
import { cursorAdapter, createCursorAdapter } from "@uncaged/nerve-adapter-cursor";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
const workflow: WorkflowSpec<MyMeta> = {
name: "develop-workflow",
roles: {
architect: { adapter: "cursor", prompt: architectPrompt, meta: architectSchema },
coder: { adapter: "cursor", prompt: coderPrompt, meta: coderSchema },
reviewer: { adapter: "hermes", prompt: reviewPrompt, meta: reviewSchema, timeout: "60s" },
deployer: { adapter: "hermes", prompt: deployPrompt, meta: deploySchema },
architect: { adapter: cursorAdapter, prompt: architectPrompt, meta: architectSchema },
coder: { adapter: createCursorAdapter({ model: "claude-sonnet-4", timeout: 600 }), prompt: coderPrompt, meta: coderSchema },
reviewer: { adapter: hermesAdapter, prompt: reviewPrompt, meta: reviewSchema, timeout: "60s" },
deployer: { adapter: hermesAdapter, prompt: deployPrompt, meta: deploySchema },
},
moderator,
};
@@ -89,21 +92,12 @@ const workflow: WorkflowSpec<MyMeta> = {
### Runtime Assembly
```
WorkflowSpec → Role(adapter + prompt) → AdapterFn(prompt, ctx) → string
WorkflowSpec → Role(adapter fn + prompt) → adapter(prompt, ctx) → string
nerve.yaml#extract → ExtractFn(string, schema) → T (typed meta)
```
`buildWorkflowSpec` receives an adapter map and wires each role to its adapter function directly. No registry, no lookup.
```ts
const spec = buildWorkflowSpec(workflow, {
adapters: {
cursor: createCursorAdapter({ model: "auto", timeout: 300 }),
hermes: createHermesAdapter({ model: "auto", timeout: 600 }),
},
});
```
Adapter is a direct function reference on each role — no map, no lookup, no registry.
### Adapter Packages
@@ -117,30 +111,32 @@ packages/
adapter-codex/ # @uncaged/nerve-adapter-codex — codex CLI (future)
```
Each adapter exports a single factory function:
Each adapter exports a **default instance** and a **factory** for customization:
```ts
// @uncaged/nerve-adapter-cursor
import type { AgentConfig, AgentFn } from "@uncaged/nerve-core";
// Factory — custom config
export function createCursorAdapter(config: AgentConfig): AgentFn;
// Default — sensible defaults (model: "auto", timeout: 300)
export const cursorAdapter: AgentFn;
```
The factory receives adapter config (model, timeout) and returns an `AgentFn` that spawns the CLI tool, passes the prompt, and returns raw output.
**Wiring**the daemon's entry point passes installed adapters to `buildWorkflowSpec`:
**Wiring**workflows import adapters directly, no daemon-level registry:
```ts
import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor";
import { createHermesAdapter } from "@uncaged/nerve-adapter-hermes";
import { cursorAdapter } from "@uncaged/nerve-adapter-cursor";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
const adapters = {
cursor: createCursorAdapter({ model: "auto", timeout: 300 }),
hermes: createHermesAdapter({ model: "auto", timeout: 600 }),
};
// Use default instances directly in roles
{ adapter: cursorAdapter, prompt: "...", meta: schema }
```
Adapters not installed are simply not passed. `nerve validate` checks that all adapters referenced in workflow roles are available.
Adapters not installed simply can't be imported — TypeScript catches missing dependencies at compile time.
**Workspace `package.json`** only lists the adapters it actually uses:
@@ -163,7 +159,7 @@ Adapters not installed are simply not passed. `nerve validate` checks that all a
type PromptInput = string | ((start: StartStep, messages: WorkflowMessage[]) => Promise<string>);
type RoleSpec<M> = {
adapter: string;
adapter: AgentFn;
prompt: PromptInput;
meta: Schema<M>;
timeout: string | null;
@@ -176,19 +172,20 @@ Static prompts cover simple cases. Dynamic prompts (functions) are needed when t
Two-layer: adapter default + role override.
1. Adapter config provides the default timeout (set at `buildWorkflowSpec` time)
1. Adapter instance carries its default timeout (set at creation or from default export)
2. Role definition can override for specific scenarios
```ts
// Adapter default: 300s
const adapters = {
cursor: createCursorAdapter({ model: "auto", timeout: 300 }),
};
import { cursorAdapter, createCursorAdapter } from "@uncaged/nerve-adapter-cursor";
// cursorAdapter has built-in default (300s)
// Or create with custom timeout:
const longRunAdapter = createCursorAdapter({ model: "auto", timeout: 600 });
// Role override — review is faster
reviewer: { adapter: "cursor", ..., timeout: "60s" }
reviewer: { adapter: cursorAdapter, ..., timeout: "60s" }
// coder uses adapter default (300s)
coder: { adapter: "cursor", ... }
coder: { adapter: cursorAdapter, ... }
```
### No Runtime Fallback
@@ -218,7 +215,7 @@ type WorkflowContext = {
### Configuration Validation
`nerve validate` checks:
- All adapter names referenced in WorkflowSpec roles have a registered adapter
- All roles have a valid adapter function (not null/undefined)
- Adapter CLIs are available (binary exists in PATH)
- Extract provider is configured and reachable