docs(rfc): RFC-003 — adapter packages + dynamic prompts

- Adapter packages: each adapter in own package (@nerve/adapter-cursor, etc.)
- AgentRegistry accepts adapter factories at construction (plugin model)
- Migration path: move spawn logic from workflow-utils to adapter packages
- Dynamic prompts: RoleSpec.prompt supports string | async function
- Workspace only installs adapters it uses

Ref: #234
This commit is contained in:
2026-04-29 07:08:34 +00:00
parent 03e9d20501
commit 18584641bd
+67
View File
@@ -103,6 +103,73 @@ nerve.yaml#extract → ExtractFn(string, schema) → T (typed meta)
`AgentRegistry` reads config, instantiates adapters, and returns `AgentFn` by name. Role assembly is handled by the runtime — users never call Role factories directly.
### Adapter Packages
Each agent adapter lives in its own package to avoid pulling unnecessary dependencies:
```
packages/
adapter-cursor/ # @nerve/adapter-cursor — cursor-agent CLI
adapter-hermes/ # @nerve/adapter-hermes — hermes CLI subagent
adapter-claude/ # @nerve/adapter-claude — claude-code CLI (future)
adapter-codex/ # @nerve/adapter-codex — codex CLI (future)
```
Each adapter exports a single factory function:
```ts
// @nerve/adapter-cursor
import type { AgentConfig, AgentFn } from "@nerve/core";
export function createCursorAdapter(config: AgentConfig): AgentFn;
```
The factory receives the full `AgentConfig` (type, model, timeout) and returns an `AgentFn` that spawns the CLI tool, passes the prompt, and returns raw output.
**Registration**`AgentRegistry` accepts adapter factories at construction:
```ts
import { createCursorAdapter } from "@nerve/adapter-cursor";
import { createHermesAdapter } from "@nerve/adapter-hermes";
const registry = createAgentRegistry(config.agents, {
cursor: createCursorAdapter,
hermes: createHermesAdapter,
});
```
The daemon's entry point wires installed adapters; adapters not installed are not imported. `nerve validate` checks that referenced adapter types have a registered factory.
**Workspace `package.json`** only lists the adapters it actually uses:
```json
{
"dependencies": {
"@nerve/adapter-cursor": "workspace:*",
"@nerve/adapter-hermes": "workspace:*"
}
}
```
**Migration from `workflow-utils`** — the existing `role-cursor.ts` / `shared/cursor-agent.ts` spawn logic moves to `@nerve/adapter-cursor`. `role-hermes.ts` / `shared/hermes-agent.ts` moves to `@nerve/adapter-hermes`. `workflow-utils` retains only extract, prompt utilities, and shared spawn infrastructure.
### Dynamic Prompts
`RoleSpec.prompt` supports both static strings and async functions:
```ts
type PromptInput = string | ((start: StartStep, messages: WorkflowMessage[]) => Promise<string>);
type RoleSpec<M> = {
agent: string;
prompt: PromptInput;
meta: Schema<M>;
timeout: string | null;
};
```
Static prompts cover simple cases. Dynamic prompts (functions) are needed when the prompt depends on thread context — e.g. reading issue content, injecting prior step results, or resolving repo paths at runtime.
### Timeout Resolution
Two-layer with role override: