From 577fb27470cbdf248b122e7dfadf00007405b228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 26 May 2026 17:24:48 +0000 Subject: [PATCH] feat: add adapter skill + fix commit scope (#549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../cli-workflow/src/__tests__/skill.test.ts | 12 ++ packages/cli-workflow/src/cli.ts | 8 + packages/cli-workflow/src/commands/skill.ts | 2 + .../workflow-util/src/adapter-reference.ts | 163 ++++++++++++++++++ .../workflow-util/src/developer-reference.ts | 2 +- packages/workflow-util/src/index.ts | 1 + 6 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 packages/workflow-util/src/adapter-reference.ts diff --git a/packages/cli-workflow/src/__tests__/skill.test.ts b/packages/cli-workflow/src/__tests__/skill.test.ts index f2fe69e..9ab9fd0 100644 --- a/packages/cli-workflow/src/__tests__/skill.test.ts +++ b/packages/cli-workflow/src/__tests__/skill.test.ts @@ -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"); }); }); diff --git a/packages/cli-workflow/src/cli.ts b/packages/cli-workflow/src/cli.ts index 55ba6e8..9ee2e8d 100755 --- a/packages/cli-workflow/src/cli.ts +++ b/packages/cli-workflow/src/cli.ts @@ -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)") diff --git a/packages/cli-workflow/src/commands/skill.ts b/packages/cli-workflow/src/commands/skill.ts index fb59fae..d685664 100644 --- a/packages/cli-workflow/src/commands/skill.ts +++ b/packages/cli-workflow/src/commands/skill.ts @@ -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 { diff --git a/packages/workflow-util/src/adapter-reference.ts b/packages/workflow-util/src/adapter-reference.ts new file mode 100644 index 0000000..1b7aa44 --- /dev/null +++ b/packages/workflow-util/src/adapter-reference.ts @@ -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- --thread --role --prompt +\`\`\` + +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 => { + // 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-") + 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 --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 \` +`; +} diff --git a/packages/workflow-util/src/developer-reference.ts b/packages/workflow-util/src/developer-reference.ts index 9409553..f8447c5 100644 --- a/packages/workflow-util/src/developer-reference.ts +++ b/packages/workflow-util/src/developer-reference.ts @@ -134,7 +134,7 @@ All data is CAS-addressed via \`@uncaged/json-cas\`: (): 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 \`\`\` `; } diff --git a/packages/workflow-util/src/index.ts b/packages/workflow-util/src/index.ts index 76ea7f0..393a416 100644 --- a/packages/workflow-util/src/index.ts +++ b/packages/workflow-util/src/index.ts @@ -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";