feat: add adapter skill + fix commit scope (#549)
CI / test (pull_request) Successful in 1m30s

- Add 'uwf skill adapter' — guide for building agent adapters.
  Covers: createAgent factory, AgentContext/AgentRunResult types,
  prompt building helpers, session detail storage, registration.
- Fix developer skill: agent-kit → util-agent in commit scope.

Refs #542
Fixes #549
This commit is contained in:
2026-05-26 17:24:48 +00:00
parent 5475dd3f5c
commit 577fb27470
6 changed files with 187 additions and 1 deletions
@@ -7,6 +7,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
import {
cmdSkillActor,
cmdSkillAdapter,
cmdSkillArchitecture,
cmdSkillAuthor,
cmdSkillCli,
@@ -29,6 +30,7 @@ describe("skill commands", () => {
expect(result).toContain("user");
expect(result).toContain("author");
expect(result).toContain("developer");
expect(result).toContain("adapter");
for (const name of result) {
expect(name).toMatch(/^\S+$/);
}
@@ -108,6 +110,15 @@ describe("skill commands", () => {
expect(result.length).toBeGreaterThan(500);
});
test("skill adapter returns non-empty markdown string", () => {
const result = cmdSkillAdapter();
expect(typeof result).toBe("string");
expect(result).toContain("createAgent");
expect(result).toContain("AgentContext");
expect(result).toContain("frontmatter");
expect(result.length).toBeGreaterThan(500);
});
test("skill help subcommand is suppressed", () => {
const output = execFileSync("bun", ["src/cli.ts", "skill", "--help"], {
cwd: join(__dirname, "..", ".."),
@@ -123,6 +134,7 @@ describe("skill commands", () => {
expect(output).toContain("user");
expect(output).toContain("author");
expect(output).toContain("developer");
expect(output).toContain("adapter");
expect(output).toContain("list");
});
});
+8
View File
@@ -18,6 +18,7 @@ import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js";
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
import {
cmdSkillActor,
cmdSkillAdapter,
cmdSkillArchitecture,
cmdSkillAuthor,
cmdSkillCli,
@@ -514,6 +515,13 @@ skill
console.log(cmdSkillActor());
});
skill
.command("adapter")
.description("Print the adapter reference (building agent adapters)")
.action(() => {
console.log(cmdSkillAdapter());
});
skill
.command("author")
.description("Print the author reference (workflow YAML design guide)")
@@ -1,5 +1,6 @@
export {
generateActorReference as cmdSkillActor,
generateAdapterReference as cmdSkillAdapter,
generateArchitectureReference as cmdSkillArchitecture,
generateAuthorReference as cmdSkillAuthor,
generateCliReference as cmdSkillCli,
@@ -18,6 +19,7 @@ const SKILL_NAMES = [
"user",
"author",
"developer",
"adapter",
] as const;
export function cmdSkillList(): ReadonlyArray<string> {
@@ -0,0 +1,163 @@
export function generateAdapterReference(): string {
return `# Adapter Reference
Guide for building a new agent adapter (CLI binary) for the workflow engine.
## What Is an Adapter
An adapter is a CLI command (e.g. \`uwf-hermes\`, \`uwf-builtin\`) that the engine spawns to execute a role. It bridges the workflow engine and an LLM/agent backend. The engine calls it with:
\`\`\`
uwf-<name> --thread <id> --role <role> --prompt <text>
\`\`\`
The adapter must produce frontmatter markdown output. The engine handles argument parsing, context building, output extraction, and CAS persistence — you just implement the LLM interaction.
## Quick Start
\`\`\`typescript
import { createAgent } from "@uncaged/workflow-util-agent";
import type { AgentContext, AgentRunResult, AgentContinueFn, AgentRunFn } from "@uncaged/workflow-util-agent";
const run: AgentRunFn = async (ctx: AgentContext): Promise<AgentRunResult> => {
// 1. Build your prompt from ctx
// 2. Call your LLM backend
// 3. Return the result
return { output: rawMarkdown, detailHash, sessionId };
};
const continue_: AgentContinueFn = async (sessionId, message, store) => {
// Resume an existing session with a correction message
return { output: correctedMarkdown, detailHash, sessionId };
};
const main = createAgent({ name: "my-agent", run, continue: continue_ });
main();
\`\`\`
## The \`createAgent\` Factory
\`createAgent(options)\` returns an async \`main()\` function that handles the full lifecycle:
1. Parses CLI args (\`--thread\`, \`--role\`, \`--prompt\`)
2. Loads \`.env\` from storage root
3. Builds \`AgentContext\` (thread history, workflow definition, role prompt)
4. Injects \`outputFormatInstruction\` from the role's frontmatter schema
5. Calls your \`run(ctx)\` function
6. Extracts frontmatter from your output via \`tryFrontmatterFastPath()\`
7. If extraction fails, calls your \`continue(sessionId, correctionMessage, store)\` up to 2 times
8. Persists the validated output as a CAS step node
9. Prints the step hash to stdout
You only implement \`run\` and \`continue\`.
## AgentOptions
\`\`\`typescript
type AgentOptions = {
name: string; // Adapter name (used in step records as "uwf-<name>")
run: AgentRunFn; // Execute a role from scratch
continue: AgentContinueFn; // Resume a session for frontmatter correction
};
\`\`\`
## AgentContext
The \`ctx\` object passed to your \`run\` function:
| Field | Type | Description |
|-------|------|-------------|
| \`threadId\` | \`string\` | Thread ULID |
| \`role\` | \`string\` | Role name being executed |
| \`edgePrompt\` | \`string\` | Moderator's task instruction for this step |
| \`workflow\` | \`WorkflowPayload\` | Full workflow definition (roles, graph) |
| \`start\` | \`StartNodePayload\` | Thread start data (workflow hash, user prompt) |
| \`steps\` | \`StepContext[]\` | Previous steps with expanded outputs |
| \`store\` | \`Store\` | CAS store for reading/writing data |
| \`outputFormatInstruction\` | \`string\` | Frontmatter format instruction (inject into system prompt) |
| \`isFirstVisit\` | \`boolean\` | True if this role hasn't run before in this thread |
## AgentRunResult
Your \`run\` and \`continue\` functions must return:
\`\`\`typescript
type AgentRunResult = {
output: string; // Raw markdown with frontmatter (must start with ---)
detailHash: string; // CAS hash of session detail (turn history, metadata)
sessionId: string; // Session ID for potential continue() calls
};
\`\`\`
## Building the Prompt
Use helpers from \`@uncaged/workflow-util-agent\`:
| Helper | Purpose |
|--------|---------|
| \`buildRolePrompt(roleDef)\` | Assemble Goal/Capabilities/Prepare/Procedure/Output sections |
| \`buildContinuationPrompt(steps, role, edgePrompt)\` | For re-entry: steps since last visit + edge prompt |
| \`ctx.outputFormatInstruction\` | Pre-built frontmatter format block (inject into system prompt) |
Typical system prompt structure:
\`\`\`
[outputFormatInstruction]
[rolePrompt from buildRolePrompt()]
[workflow metadata]
\`\`\`
## Storing Session Detail
Store your turn history as a CAS merkle DAG for debugging and replay:
\`\`\`typescript
// Store each turn as a CAS text node
const turnHash = await store.put(textSchema, { content: turnData });
// Build a detail node referencing all turns
const detailHash = await store.put(detailSchema, { turns: turnHashes });
\`\`\`
The \`detailHash\` is preserved from the first \`run()\` call — retry \`continue()\` calls don't overwrite it.
## Registration
Register your adapter in \`~/.uncaged/workflow/config.yaml\`:
\`\`\`yaml
agents:
my-agent:
command: uwf-my-agent
args: []
\`\`\`
Use it:
\`\`\`bash
uwf thread exec <thread-id> --agent my-agent
\`\`\`
Or set as default:
\`\`\`yaml
defaultAgent: my-agent
\`\`\`
## Existing Adapters
| Adapter | Package | Backend |
|---------|---------|---------|
| \`uwf-hermes\` | \`@uncaged/workflow-agent-hermes\` | Hermes ACP (chat sessions) |
| \`uwf-builtin\` | \`@uncaged/workflow-agent-builtin\` | Direct OpenAI API (tools + loop) |
| \`uwf-claude-code\` | \`@uncaged/workflow-agent-claude-code\` | Claude Code CLI |
Study these for patterns on prompt building, session management, and detail storage.
## Checklist
1. Implement \`run(ctx)\` — build prompt, call LLM, return output + detailHash + sessionId
2. Implement \`continue(sessionId, message, store)\` — resume session for frontmatter correction
3. Store session detail as CAS nodes (for debugging)
4. Ensure output starts with \`---\` frontmatter block
5. Add a \`bin\` entry in \`package.json\` for the CLI command
6. Register in config.yaml and test with \`uwf thread exec --agent <name>\`
`;
}
@@ -134,7 +134,7 @@ All data is CAS-addressed via \`@uncaged/json-cas\`:
<type>(<scope>): <description>
type: feat | fix | refactor | docs | chore | test
scope: workflow | cli | moderator | agent-kit | hermes | util | protocol
scope: workflow | cli | moderator | util-agent | hermes | util | protocol
\`\`\`
`;
}
+1
View File
@@ -1,4 +1,5 @@
export { generateActorReference } from "./actor-reference.js";
export { generateAdapterReference } from "./adapter-reference.js";
export { generateArchitectureReference } from "./architecture-reference.js";
export { generateAuthorReference } from "./author-reference.js";
export { encodeUint64AsCrockford } from "./base32.js";