Files
united-workforce/docs/superpowers/plans/2026-05-18-office-agent-document-template.md
T

1406 lines
41 KiB
Markdown

# Office-Agent Document Template 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 新增 `workflow-template-document``workflow-agent-office``workflow-agent-docx-diff` 三个包,通过 `office-agent` CLI 实现 Word 文档生成/编辑 workflow。
**Architecture:** `workflow-template-document` 定义纯结构(roles / moderator / descriptor);`workflow-agent-office` 直接实现 `AdapterFn`,调用 `office-agent` CLI 生成或编辑文档;`workflow-agent-docx-diff` 直接实现 `AdapterFn`,调用 `docx-diff` CLI 产出差异报告。两个 agent 均跳过 LLM extraction,直接 `schema.parse(JSON.parse(raw))`
**Tech Stack:** Bun, TypeScript (NodeNext), zod/v4, Biome, bun:test
**Branch:** `user/jiayiyan/feat_office-agent-document-template`(已从最新 main 创建)
---
## 文件清单
### workflow-template-document
| 操作 | 路径 |
|------|------|
| 新建 | `packages/workflow-template-document/package.json` |
| 新建 | `packages/workflow-template-document/tsconfig.json` |
| 新建 | `packages/workflow-template-document/src/types.ts` |
| 新建 | `packages/workflow-template-document/src/roles/writer.ts` |
| 新建 | `packages/workflow-template-document/src/roles/differ.ts` |
| 新建 | `packages/workflow-template-document/src/roles/index.ts` |
| 新建 | `packages/workflow-template-document/src/roles.ts` |
| 新建 | `packages/workflow-template-document/src/moderator.ts` |
| 新建 | `packages/workflow-template-document/src/descriptor.ts` |
| 新建 | `packages/workflow-template-document/src/index.ts` |
| 新建 | `packages/workflow-template-document/__tests__/document-template.test.ts` |
### workflow-agent-office
| 操作 | 路径 |
|------|------|
| 新建 | `packages/workflow-agent-office/package.json` |
| 新建 | `packages/workflow-agent-office/tsconfig.json` |
| 新建 | `packages/workflow-agent-office/src/types.ts` |
| 新建 | `packages/workflow-agent-office/src/runner.ts` |
| 新建 | `packages/workflow-agent-office/src/agent.ts` |
| 新建 | `packages/workflow-agent-office/src/package-descriptor.ts` |
| 新建 | `packages/workflow-agent-office/src/index.ts` |
| 新建 | `packages/workflow-agent-office/__tests__/runner.test.ts` |
| 新建 | `packages/workflow-agent-office/__tests__/agent.test.ts` |
### workflow-agent-docx-diff
| 操作 | 路径 |
|------|------|
| 新建 | `packages/workflow-agent-docx-diff/package.json` |
| 新建 | `packages/workflow-agent-docx-diff/tsconfig.json` |
| 新建 | `packages/workflow-agent-docx-diff/src/types.ts` |
| 新建 | `packages/workflow-agent-docx-diff/src/runner.ts` |
| 新建 | `packages/workflow-agent-docx-diff/src/agent.ts` |
| 新建 | `packages/workflow-agent-docx-diff/src/package-descriptor.ts` |
| 新建 | `packages/workflow-agent-docx-diff/src/index.ts` |
| 新建 | `packages/workflow-agent-docx-diff/__tests__/runner.test.ts` |
| 新建 | `packages/workflow-agent-docx-diff/__tests__/agent.test.ts` |
### 根目录修改
| 操作 | 路径 |
|------|------|
| 修改 | `tsconfig.json` — 追加三个 references |
| 修改 | `docs/architecture.md` — 新增三个包的描述 |
---
## Phase 1:workflow-template-document
### Task 1:包脚手架
**Files:**
- 新建: `packages/workflow-template-document/package.json`
- 新建: `packages/workflow-template-document/tsconfig.json`
- [ ] **Step 1:创建 package.json**
```json
{
"name": "@uncaged/workflow-template-document",
"version": "0.1.0",
"files": ["src", "dist", "package.json"],
"type": "module",
"types": "src/index.ts",
"exports": {
".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"test": "bun test"
},
"dependencies": {
"@uncaged/workflow-register": "workspace:^",
"@uncaged/workflow-runtime": "workspace:^",
"zod": "^4.0.0"
},
"devDependencies": {
"@uncaged/workflow-protocol": "workspace:^"
},
"publishConfig": {
"access": "public"
}
}
```
- [ ] **Step 2:创建 tsconfig.json**
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true
},
"include": ["src/**/*.ts"],
"references": [
{ "path": "../workflow-protocol" },
{ "path": "../workflow-runtime" },
{ "path": "../workflow-register" }
]
}
```
- [ ] **Step 3:在根 tsconfig.json 的 `references` 数组末尾追加**
```json
{ "path": "packages/workflow-template-document" }
```
- [ ] **Step 4:安装依赖**
```bash
cd packages/workflow-template-document && bun install
```
Expected:lockfile 更新,无报错。
---
### Task 2:类型 + 角色 schema(TDD)
**Files:**
- 新建: `packages/workflow-template-document/src/types.ts`
- 新建: `packages/workflow-template-document/src/roles/writer.ts`
- 新建: `packages/workflow-template-document/src/roles/differ.ts`
- 新建: `packages/workflow-template-document/src/roles/index.ts`
- 新建: `packages/workflow-template-document/__tests__/document-template.test.ts`(先写失败测试)
- [ ] **Step 1:创建占位 index.ts**
```typescript
// src/index.ts
export {};
```
- [ ] **Step 2:写失败测试**
```typescript
// __tests__/document-template.test.ts
import { describe, expect, test } from "bun:test";
import { tableToModerator } from "@uncaged/workflow-protocol/moderator-table.js";
import { validateWorkflowDescriptor } from "@uncaged/workflow-register";
import { END, type ModeratorContext, type RoleStep, START } from "@uncaged/workflow-runtime";
import { buildDocumentDescriptor } from "../src/descriptor.js";
import { documentTable } from "../src/moderator.js";
import type { DifferMeta, WriterMeta } from "../src/roles/index.js";
import type { DocumentMeta } from "../src/roles.js";
const documentModerator = tableToModerator(documentTable);
function makeCtx(
steps: ModeratorContext<DocumentMeta>["steps"],
): ModeratorContext<DocumentMeta> {
return {
threadId: "01TEST000000000000000000TR",
depth: 0,
bundleHash: "TESTHASH00001",
start: { role: START, content: "", meta: {}, timestamp: 0, parentState: null },
steps,
};
}
function writerGenerateStep(): RoleStep<DocumentMeta> {
return {
role: "writer",
contentHash: "STUBHASHWRITER001",
meta: { mode: "generate", outputDocx: "/out/output.docx", sourceDocx: null } satisfies WriterMeta,
refs: [],
timestamp: 1,
};
}
function writerEditStep(): RoleStep<DocumentMeta> {
return {
role: "writer",
contentHash: "STUBHASHWRITER002",
meta: { mode: "edit", outputDocx: "/out/modified.docx", sourceDocx: "/out/original.docx" } satisfies WriterMeta,
refs: [],
timestamp: 1,
};
}
function differStep(): RoleStep<DocumentMeta> {
return {
role: "differ",
contentHash: "STUBHASHDIFF001",
meta: {
sourceDocx: "/out/original.docx",
modifiedDocx: "/out/modified.docx",
diffDocx: "/out/diff.docx",
} satisfies DifferMeta,
refs: [],
timestamp: 2,
};
}
describe("documentTable", () => {
test("START → writer", () => {
expect(documentModerator(makeCtx([]))).toBe("writer");
});
test("writer (generate) → END", () => {
expect(documentModerator(makeCtx([writerGenerateStep()]))).toBe(END);
});
test("writer (edit) → differ", () => {
expect(documentModerator(makeCtx([writerEditStep()]))).toBe("differ");
});
test("differ → END", () => {
expect(documentModerator(makeCtx([writerEditStep(), differStep()]))).toBe(END);
});
});
describe("buildDocumentDescriptor", () => {
test("descriptor passes validation", () => {
const descriptor = buildDocumentDescriptor();
expect(() => validateWorkflowDescriptor(descriptor)).not.toThrow();
});
test("descriptor has writer and differ roles", () => {
const descriptor = buildDocumentDescriptor();
expect(Object.keys(descriptor.roles)).toContain("writer");
expect(Object.keys(descriptor.roles)).toContain("differ");
});
});
```
- [ ] **Step 3:运行测试,确认失败**
```bash
cd packages/workflow-template-document && bun test
```
Expected:`Cannot find module '../src/moderator.js'` 或类似模块未找到错误。
- [ ] **Step 4:创建 src/types.ts**
```typescript
export type DocumentStartInput = {
prompt: string;
inputDocx: string | null;
};
```
- [ ] **Step 5:创建 src/roles/writer.ts**
```typescript
import type { RoleDefinition } from "@uncaged/workflow-runtime";
import * as z from "zod/v4";
export const writerMetaSchema = z.discriminatedUnion("mode", [
z.object({
mode: z.literal("generate"),
outputDocx: z.string(),
sourceDocx: z.null(),
}),
z.object({
mode: z.literal("edit"),
outputDocx: z.string(),
sourceDocx: z.string(),
}),
]);
export type WriterMeta = z.infer<typeof writerMetaSchema>;
export const writerRole: RoleDefinition<WriterMeta> = {
description: "Generates or modifies a Word document via an external agent.",
systemPrompt: "",
schema: writerMetaSchema,
};
```
- [ ] **Step 6:创建 src/roles/differ.ts**
```typescript
import type { RoleDefinition } from "@uncaged/workflow-runtime";
import * as z from "zod/v4";
export const differMetaSchema = z.object({
sourceDocx: z.string(),
modifiedDocx: z.string(),
diffDocx: z.string(),
});
export type DifferMeta = z.infer<typeof differMetaSchema>;
export const differRole: RoleDefinition<DifferMeta> = {
description: "Produces a Word-format diff report of the writer's changes (edit mode only).",
systemPrompt: "",
schema: differMetaSchema,
};
```
- [ ] **Step 7:创建 src/roles/index.ts**
```typescript
export { differMetaSchema, differRole } from "./differ.js";
export type { DifferMeta } from "./differ.js";
export { writerMetaSchema, writerRole } from "./writer.js";
export type { WriterMeta } from "./writer.js";
```
---
### Task 3:roles + moderator + descriptor + index 收尾
**Files:**
- 新建: `packages/workflow-template-document/src/roles.ts`
- 新建: `packages/workflow-template-document/src/moderator.ts`
- 新建: `packages/workflow-template-document/src/descriptor.ts`
- 修改: `packages/workflow-template-document/src/index.ts`
- [ ] **Step 1:创建 src/roles.ts**
```typescript
import type { RoleDefinition } from "@uncaged/workflow-runtime";
import { type DifferMeta, differRole } from "./roles/differ.js";
import { type WriterMeta, writerRole } from "./roles/writer.js";
export const DOCUMENT_WORKFLOW_DESCRIPTION =
"Generates a new Word document from a prompt, or edits an existing one and produces a diff report.";
export type DocumentMeta = {
writer: WriterMeta;
differ: DifferMeta;
};
export type DocumentRoles = {
[K in keyof DocumentMeta]: RoleDefinition<DocumentMeta[K]>;
};
export const documentRoles: DocumentRoles = {
writer: writerRole,
differ: differRole,
};
```
- [ ] **Step 2:创建 src/moderator.ts**
```typescript
import {
END,
type ModeratorCondition,
type ModeratorTable,
START,
} from "@uncaged/workflow-runtime";
import type { DocumentMeta } from "./roles.js";
import type { WriterMeta } from "./roles/writer.js";
const writerIsEditMode: ModeratorCondition<DocumentMeta> = {
name: "writerIsEditMode",
description: "Writer ran in edit mode and produced a modified document",
check: (ctx) => {
const writerStep = ctx.steps.find((s) => s.role === "writer");
if (writerStep === undefined) return false;
return (writerStep.meta as WriterMeta).mode === "edit";
},
};
export const documentTable: ModeratorTable<DocumentMeta> = {
[START]: [{ condition: "FALLBACK", role: "writer" }],
writer: [
{ condition: writerIsEditMode, role: "differ" },
{ condition: "FALLBACK", role: END },
],
differ: [{ condition: "FALLBACK", role: END }],
};
```
- [ ] **Step 3:创建 src/descriptor.ts**
```typescript
import { buildDescriptor } from "@uncaged/workflow-register";
import { documentTable } from "./moderator.js";
import { DOCUMENT_WORKFLOW_DESCRIPTION, documentRoles } from "./roles.js";
export function buildDocumentDescriptor() {
return buildDescriptor({
description: DOCUMENT_WORKFLOW_DESCRIPTION,
roles: documentRoles,
table: documentTable,
});
}
```
- [ ] **Step 4:更新 src/index.ts**
```typescript
import type { WorkflowDefinition } from "@uncaged/workflow-runtime";
import { documentTable } from "./moderator.js";
import {
DOCUMENT_WORKFLOW_DESCRIPTION,
type DocumentMeta,
type DocumentRoles,
documentRoles,
} from "./roles.js";
export { buildDocumentDescriptor } from "./descriptor.js";
export { documentTable } from "./moderator.js";
export {
type DifferMeta,
differMetaSchema,
differRole,
type WriterMeta,
writerMetaSchema,
writerRole,
} from "./roles/index.js";
export {
DOCUMENT_WORKFLOW_DESCRIPTION,
type DocumentMeta,
type DocumentRoles,
documentRoles,
} from "./roles.js";
export type { DocumentStartInput } from "./types.js";
export const documentWorkflowDefinition: WorkflowDefinition<DocumentMeta> = {
description: DOCUMENT_WORKFLOW_DESCRIPTION,
roles: documentRoles,
table: documentTable,
};
```
- [ ] **Step 5:运行测试,确认通过**
```bash
cd packages/workflow-template-document && bun test
```
Expected:6 tests pass(4 moderator + 2 descriptor)。
- [ ] **Step 6:运行全量构建检查**
```bash
cd /Users/yanjiayi/workspace/workflow && bun run check
```
Expected:无 TypeScript 错误,无 Biome 警告。
- [ ] **Step 7:Commit**
```bash
git add packages/workflow-template-document/ tsconfig.json
git commit -m "feat(template): add workflow-template-document with writer/differ roles and moderator"
```
---
## Phase 2:workflow-agent-office
### Task 4:包脚手架
**Files:**
- 新建: `packages/workflow-agent-office/package.json`
- 新建: `packages/workflow-agent-office/tsconfig.json`
- [ ] **Step 1:创建 package.json**
```json
{
"name": "@uncaged/workflow-agent-office",
"version": "0.1.0",
"files": ["src", "dist", "package.json"],
"type": "module",
"types": "src/index.ts",
"exports": {
".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"test": "bun test"
},
"dependencies": {
"@uncaged/workflow-runtime": "workspace:^",
"@uncaged/workflow-util": "workspace:^",
"@uncaged/workflow-util-agent": "workspace:^"
},
"publishConfig": {
"access": "public"
}
}
```
- [ ] **Step 2:创建 tsconfig.json**
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true
},
"include": ["src/**/*.ts"],
"references": [
{ "path": "../workflow-protocol" },
{ "path": "../workflow-runtime" },
{ "path": "../workflow-util" },
{ "path": "../workflow-util-agent" }
]
}
```
- [ ] **Step 3:在根 tsconfig.json 的 `references` 数组末尾追加**
```json
{ "path": "packages/workflow-agent-office" }
```
- [ ] **Step 4:安装依赖**
```bash
cd packages/workflow-agent-office && bun install
```
---
### Task 5:runner 实现(TDD)
**Files:**
- 新建: `packages/workflow-agent-office/src/types.ts`
- 新建: `packages/workflow-agent-office/src/runner.ts`
- 新建: `packages/workflow-agent-office/__tests__/runner.test.ts`
- [ ] **Step 1:写失败测试**
```typescript
// __tests__/runner.test.ts
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, mock, test } from "bun:test";
import { ok, err } from "@uncaged/workflow-util";
import type { SpawnCliConfig } from "@uncaged/workflow-util-agent";
import { editDocument, generateDocument } from "../src/runner.js";
type MockSpawnResult = Awaited<ReturnType<typeof import("@uncaged/workflow-util-agent").spawnCli>>;
function makeSpawn(result: MockSpawnResult) {
return mock(async (_cmd: string, _args: string[], _opts: SpawnCliConfig) => result);
}
function tempDir(): string {
const dir = join(tmpdir(), `office-test-${Date.now()}`);
mkdirSync(dir, { recursive: true });
return dir;
}
describe("generateDocument", () => {
test("calls office-agent create with correct args and returns outputDocx path", async () => {
const base = tempDir();
const spawnFn = makeSpawn(ok("agent reply") as MockSpawnResult);
// Simulate CLI creating the file
const outFile = join(base, "thread1", "output.docx");
mkdirSync(join(base, "thread1"), { recursive: true });
writeFileSync(outFile, "");
const result = await generateDocument(
{ outputDir: base, command: "office-agent", timeout: null },
"thread1",
"Write a report",
spawnFn,
);
expect(result.outputDocx).toBe(outFile);
expect(result.sourceDocx).toBeNull();
expect(spawnFn.mock.calls[0][0]).toBe("office-agent");
expect(spawnFn.mock.calls[0][1]).toEqual(["create", "Write a report", "-o", "output.docx"]);
expect(spawnFn.mock.calls[0][2].cwd).toBe(join(base, "thread1"));
});
test("uses PATH office-agent when command is null", async () => {
const base = tempDir();
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
mkdirSync(join(base, "t2"), { recursive: true });
writeFileSync(join(base, "t2", "output.docx"), "");
await generateDocument(
{ outputDir: base, command: null, timeout: null },
"t2",
"Generate",
spawnFn,
);
expect(spawnFn.mock.calls[0][0]).toBe("office-agent");
});
test("throws on non_zero_exit", async () => {
const base = tempDir();
const spawnFn = makeSpawn(
err({ kind: "non_zero_exit", exitCode: 1, stdout: "", stderr: "error" }) as MockSpawnResult,
);
await expect(
generateDocument({ outputDir: base, command: null, timeout: null }, "t3", "fail", spawnFn),
).rejects.toThrow("office-agent failed (exit 1)");
});
test("throws on timeout", async () => {
const base = tempDir();
const spawnFn = makeSpawn(err({ kind: "timeout" }) as MockSpawnResult);
await expect(
generateDocument({ outputDir: base, command: null, timeout: null }, "t4", "slow", spawnFn),
).rejects.toThrow("office-agent: timed out");
});
test("throws when output file not created", async () => {
const base = tempDir();
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
// Do NOT create output.docx
await expect(
generateDocument({ outputDir: base, command: null, timeout: null }, "t5", "no file", spawnFn),
).rejects.toThrow("output file not found");
});
});
describe("editDocument", () => {
test("copies input to original.docx and modified.docx, calls edit, returns paths", async () => {
const base = tempDir();
// Create a fake inputDocx
const inputFile = join(base, "source.docx");
writeFileSync(inputFile, "original content");
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
// Simulate CLI overwriting modified.docx
const outDir = join(base, "te1");
mkdirSync(outDir, { recursive: true });
writeFileSync(join(outDir, "modified.docx"), "modified content");
const result = await editDocument(
{ outputDir: base, command: "office-agent", timeout: null },
"te1",
"Edit the doc",
inputFile,
spawnFn,
);
expect(result.outputDocx).toBe(join(outDir, "modified.docx"));
expect(result.sourceDocx).toBe(join(outDir, "original.docx"));
expect(spawnFn.mock.calls[0][1]).toEqual(["edit", "modified.docx", "Edit the doc"]);
});
test("throws on spawn_failed", async () => {
const base = tempDir();
const inputFile = join(base, "src.docx");
writeFileSync(inputFile, "");
const spawnFn = makeSpawn(
err({ kind: "spawn_failed", message: "not found" }) as MockSpawnResult,
);
await expect(
editDocument({ outputDir: base, command: null, timeout: null }, "te2", "edit", inputFile, spawnFn),
).rejects.toThrow("spawn failed");
});
});
```
- [ ] **Step 2:运行测试,确认失败**
```bash
cd packages/workflow-agent-office && bun test __tests__/runner.test.ts
```
Expected:`Cannot find module '../src/runner.js'`
- [ ] **Step 3:创建 src/types.ts**
```typescript
export type OfficeAgentConfig = {
outputDir: string;
command: string | null;
timeout: number | null;
};
```
- [ ] **Step 4:创建 src/runner.ts**
```typescript
import { copyFile, mkdir, stat } from "node:fs/promises";
import { join } from "node:path";
import { spawnCli } from "@uncaged/workflow-util-agent";
import type { OfficeAgentConfig } from "./types.js";
type SpawnCliFn = typeof spawnCli;
function throwSpawnError(e: Awaited<ReturnType<SpawnCliFn>> extends { ok: false; error: infer E } ? E : never): never {
if (e.kind === "non_zero_exit")
throw new Error(`office-agent failed (exit ${e.exitCode}): ${e.stderr}`);
if (e.kind === "timeout")
throw new Error("office-agent: timed out");
throw new Error(`office-agent: spawn failed: ${e.message}`);
}
async function assertFileExists(path: string): Promise<void> {
try {
await stat(path);
} catch {
throw new Error(`office-agent: output file not found: ${path}`);
}
}
export async function generateDocument(
config: OfficeAgentConfig,
threadId: string,
prompt: string,
spawnCliFn: SpawnCliFn = spawnCli,
): Promise<{ outputDocx: string; sourceDocx: null }> {
const outputDir = join(config.outputDir, threadId);
await mkdir(outputDir, { recursive: true });
const command = config.command ?? "office-agent";
const result = await spawnCliFn(command, ["create", prompt, "-o", "output.docx"], {
cwd: outputDir,
timeoutMs: config.timeout,
});
if (!result.ok) throwSpawnError(result.error);
const outputDocx = join(outputDir, "output.docx");
await assertFileExists(outputDocx);
return { outputDocx, sourceDocx: null };
}
export async function editDocument(
config: OfficeAgentConfig,
threadId: string,
prompt: string,
inputDocx: string,
spawnCliFn: SpawnCliFn = spawnCli,
): Promise<{ outputDocx: string; sourceDocx: string }> {
const outputDir = join(config.outputDir, threadId);
await mkdir(outputDir, { recursive: true });
const originalDocx = join(outputDir, "original.docx");
const modifiedDocx = join(outputDir, "modified.docx");
await copyFile(inputDocx, originalDocx);
await copyFile(inputDocx, modifiedDocx);
const command = config.command ?? "office-agent";
const result = await spawnCliFn(command, ["edit", "modified.docx", prompt], {
cwd: outputDir,
timeoutMs: config.timeout,
});
if (!result.ok) throwSpawnError(result.error);
await assertFileExists(modifiedDocx);
return { outputDocx: modifiedDocx, sourceDocx: originalDocx };
}
```
> **注意**:`throwSpawnError` 的参数类型可以简化为 `import type { SpawnCliError } from "@uncaged/workflow-util-agent"` 直接 import,避免复杂类型推导。把 `throwSpawnError` 的签名改成:
> ```typescript
> import type { SpawnCliError } from "@uncaged/workflow-util-agent";
> function throwSpawnError(e: SpawnCliError): never { ... }
> ```
- [ ] **Step 5:运行测试,确认通过**
```bash
cd packages/workflow-agent-office && bun test __tests__/runner.test.ts
```
Expected:7 tests pass。
---
### Task 6:agent + package-descriptor + index(TDD)
**Files:**
- 新建: `packages/workflow-agent-office/src/agent.ts`
- 新建: `packages/workflow-agent-office/src/package-descriptor.ts`
- 修改: `packages/workflow-agent-office/src/index.ts`(新建)
- [ ] **Step 1:写 agent 测试**
```typescript
// __tests__/agent.test.ts
import { describe, expect, test } from "bun:test";
import { packageDescriptor } from "../src/package-descriptor.js";
import { createOfficeAgent } from "../src/agent.js";
describe("createOfficeAgent", () => {
test("returns an AdapterFn (function)", () => {
const agent = createOfficeAgent({ outputDir: "/tmp", command: null, timeout: null });
expect(typeof agent).toBe("function");
});
test("AdapterFn returns a RoleFn (function)", () => {
const agent = createOfficeAgent({ outputDir: "/tmp", command: null, timeout: null });
const roleFn = agent("", expect.anything() as never);
expect(typeof roleFn).toBe("function");
});
});
describe("packageDescriptor", () => {
test("has correct name", () => {
expect(packageDescriptor.name).toBe("@uncaged/workflow-agent-office");
});
test("has outputDir in configSchema required", () => {
const schema = packageDescriptor.configSchema as { required: string[] };
expect(schema.required).toContain("outputDir");
});
});
```
- [ ] **Step 2:运行测试,确认失败**
```bash
cd packages/workflow-agent-office && bun test __tests__/agent.test.ts
```
Expected:`Cannot find module '../src/agent.js'`
- [ ] **Step 3:创建 src/agent.ts**
```typescript
import * as z from "zod/v4";
import { join } from "node:path";
import type { AdapterFn, RoleResult, ThreadContext, WorkflowRuntime } from "@uncaged/workflow-runtime";
import { createLogger } from "@uncaged/workflow-util";
import { editDocument, generateDocument } from "./runner.js";
import type { OfficeAgentConfig } from "./types.js";
const log = createLogger();
type ParsedInput = { prompt: string; inputDocx: string | null };
function parseStartInput(content: string): ParsedInput {
try {
const parsed = JSON.parse(content) as Record<string, unknown>;
if (typeof parsed.prompt === "string") {
return {
prompt: parsed.prompt,
inputDocx: typeof parsed.inputDocx === "string" ? parsed.inputDocx : null,
};
}
} catch {
// not JSON — treat whole content as prompt, generate mode
}
return { prompt: content, inputDocx: null };
}
export function createOfficeAgent(config: OfficeAgentConfig): AdapterFn {
return <T>(_systemPrompt: string, schema: z.ZodType<T>) =>
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
const { prompt, inputDocx } = parseStartInput(ctx.start.content);
log("8FQKP3NV", `office-agent: mode=${inputDocx === null ? "generate" : "edit"} thread=${ctx.threadId}`);
let raw: string;
if (inputDocx === null) {
const result = await generateDocument(config, ctx.threadId, prompt);
raw = JSON.stringify({ mode: "generate", outputDocx: result.outputDocx, sourceDocx: null });
} else {
const result = await editDocument(config, ctx.threadId, prompt, inputDocx);
raw = JSON.stringify({ mode: "edit", outputDocx: result.outputDocx, sourceDocx: result.sourceDocx });
}
const meta = schema.parse(JSON.parse(raw)) as T;
return { meta, childThread: null };
};
}
```
- [ ] **Step 4:创建 src/package-descriptor.ts**
```typescript
import type { PackageDescriptor } from "@uncaged/workflow-runtime";
export const packageDescriptor: PackageDescriptor = {
name: "@uncaged/workflow-agent-office",
version: "0.1.0",
capabilities: ["office-agent-cli", "docx-generate", "docx-edit"],
configSchema: {
type: "object",
required: ["outputDir"],
properties: {
outputDir: {
type: "string",
description: "Root directory for workflow outputs; subdirs are created per threadId.",
},
command: {
anyOf: [{ type: "string" }, { type: "null" }],
description: "Path to office-agent CLI binary; null uses PATH.",
},
timeout: {
anyOf: [{ type: "number" }, { type: "null" }],
description: "Timeout in milliseconds; null means no limit.",
},
},
additionalProperties: false,
},
};
```
- [ ] **Step 5:创建 src/index.ts**
```typescript
export { createOfficeAgent } from "./agent.js";
export { packageDescriptor } from "./package-descriptor.js";
export type { OfficeAgentConfig } from "./types.js";
```
- [ ] **Step 6:运行所有测试,确认通过**
```bash
cd packages/workflow-agent-office && bun test
```
Expected:全部通过(runner + agent)。
- [ ] **Step 7:运行全量构建检查**
```bash
cd /Users/yanjiayi/workspace/workflow && bun run check
```
Expected:无 TypeScript 错误,无 Biome 警告。
- [ ] **Step 8:Commit**
```bash
git add packages/workflow-agent-office/ tsconfig.json
git commit -m "feat(agent): add workflow-agent-office with generate/edit AdapterFn"
```
---
## Phase 3:workflow-agent-docx-diff
### Task 7:包脚手架
**Files:**
- 新建: `packages/workflow-agent-docx-diff/package.json`
- 新建: `packages/workflow-agent-docx-diff/tsconfig.json`
- [ ] **Step 1:创建 package.json**
```json
{
"name": "@uncaged/workflow-agent-docx-diff",
"version": "0.1.0",
"files": ["src", "dist", "package.json"],
"type": "module",
"types": "src/index.ts",
"exports": {
".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"test": "bun test"
},
"dependencies": {
"@uncaged/workflow-runtime": "workspace:^",
"@uncaged/workflow-util-agent": "workspace:^",
"@uncaged/workflow-template-document": "workspace:^"
},
"publishConfig": {
"access": "public"
}
}
```
- [ ] **Step 2:创建 tsconfig.json**
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true
},
"include": ["src/**/*.ts"],
"references": [
{ "path": "../workflow-protocol" },
{ "path": "../workflow-runtime" },
{ "path": "../workflow-util-agent" },
{ "path": "../workflow-template-document" }
]
}
```
- [ ] **Step 3:在根 tsconfig.json 的 `references` 数组末尾追加**
```json
{ "path": "packages/workflow-agent-docx-diff" }
```
- [ ] **Step 4:安装依赖**
```bash
cd packages/workflow-agent-docx-diff && bun install
```
---
### Task 8:runner 实现(TDD)
**Files:**
- 新建: `packages/workflow-agent-docx-diff/src/types.ts`
- 新建: `packages/workflow-agent-docx-diff/src/runner.ts`
- 新建: `packages/workflow-agent-docx-diff/__tests__/runner.test.ts`
- [ ] **Step 1:写失败测试**
```typescript
// __tests__/runner.test.ts
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, mock, test } from "bun:test";
import { ok, err } from "@uncaged/workflow-util";
import type { SpawnCliConfig } from "@uncaged/workflow-util-agent";
import { runDocxDiff } from "../src/runner.js";
type MockSpawnResult = Awaited<ReturnType<typeof import("@uncaged/workflow-util-agent").spawnCli>>;
function makeSpawn(result: MockSpawnResult) {
return mock(async (_cmd: string, _args: string[], _opts: SpawnCliConfig) => result);
}
function tempDir(): string {
const dir = join(tmpdir(), `diff-test-${Date.now()}`);
mkdirSync(dir, { recursive: true });
return dir;
}
describe("runDocxDiff", () => {
test("exit 0: success, returns DifferMeta JSON", async () => {
const dir = tempDir();
const sourceDocx = join(dir, "original.docx");
const modifiedDocx = join(dir, "modified.docx");
const diffDocx = join(dir, "diff.docx");
writeFileSync(sourceDocx, "");
writeFileSync(modifiedDocx, "");
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
// simulate docx-diff creating the diff file
writeFileSync(diffDocx, "");
const raw = await runDocxDiff(
{ command: "docx-diff" },
sourceDocx,
modifiedDocx,
diffDocx,
spawnFn,
);
const meta = JSON.parse(raw);
expect(meta.sourceDocx).toBe(sourceDocx);
expect(meta.modifiedDocx).toBe(modifiedDocx);
expect(meta.diffDocx).toBe(diffDocx);
expect(spawnFn.mock.calls[0][1]).toEqual([
sourceDocx,
modifiedDocx,
"--output",
"docx",
"--out-file",
diffDocx,
]);
});
test("exit 1 (changes found): treated as success", async () => {
const dir = tempDir();
const sourceDocx = join(dir, "s.docx");
const modifiedDocx = join(dir, "m.docx");
const diffDocx = join(dir, "diff.docx");
writeFileSync(sourceDocx, "");
writeFileSync(modifiedDocx, "");
writeFileSync(diffDocx, "");
const spawnFn = makeSpawn(
err({ kind: "non_zero_exit", exitCode: 1, stdout: "", stderr: "" }) as MockSpawnResult,
);
await expect(
runDocxDiff({ command: "docx-diff" }, sourceDocx, modifiedDocx, diffDocx, spawnFn),
).resolves.toBeDefined();
});
test("exit 2: throws error", async () => {
const dir = tempDir();
const spawnFn = makeSpawn(
err({ kind: "non_zero_exit", exitCode: 2, stdout: "", stderr: "fatal error" }) as MockSpawnResult,
);
await expect(
runDocxDiff({ command: null }, "s.docx", "m.docx", "diff.docx", spawnFn),
).rejects.toThrow("docx-diff failed");
});
test("timeout: throws error", async () => {
const spawnFn = makeSpawn(err({ kind: "timeout" }) as MockSpawnResult);
await expect(
runDocxDiff({ command: null }, "s.docx", "m.docx", "diff.docx", spawnFn),
).rejects.toThrow("timed out");
});
test("throws when diff file not created", async () => {
const dir = tempDir();
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
// do NOT create diffDocx
await expect(
runDocxDiff({ command: null }, "s.docx", "m.docx", join(dir, "missing.docx"), spawnFn),
).rejects.toThrow("diff file not found");
});
test("uses PATH docx-diff when command is null", async () => {
const dir = tempDir();
const diffDocx = join(dir, "diff.docx");
writeFileSync(diffDocx, "");
const spawnFn = makeSpawn(ok("") as MockSpawnResult);
await runDocxDiff({ command: null }, "s.docx", "m.docx", diffDocx, spawnFn);
expect(spawnFn.mock.calls[0][0]).toBe("docx-diff");
});
});
```
- [ ] **Step 2:运行测试,确认失败**
```bash
cd packages/workflow-agent-docx-diff && bun test __tests__/runner.test.ts
```
Expected:`Cannot find module '../src/runner.js'`
- [ ] **Step 3:创建 src/types.ts**
```typescript
export type DocxDiffAgentConfig = {
command: string | null;
};
```
- [ ] **Step 4:创建 src/runner.ts**
```typescript
import { stat } from "node:fs/promises";
import { spawnCli } from "@uncaged/workflow-util-agent";
import type { SpawnCliError } from "@uncaged/workflow-util-agent";
import type { DocxDiffAgentConfig } from "./types.js";
type SpawnCliFn = typeof spawnCli;
function throwSpawnError(e: SpawnCliError): never {
if (e.kind === "non_zero_exit")
throw new Error(`docx-diff failed (exit ${e.exitCode}): ${e.stderr}`);
if (e.kind === "timeout")
throw new Error("docx-diff: timed out");
throw new Error(`docx-diff: spawn failed: ${e.message}`);
}
export async function runDocxDiff(
config: DocxDiffAgentConfig,
sourceDocx: string,
modifiedDocx: string,
diffDocx: string,
spawnCliFn: SpawnCliFn = spawnCli,
): Promise<string> {
const command = config.command ?? "docx-diff";
const result = await spawnCliFn(
command,
[sourceDocx, modifiedDocx, "--output", "docx", "--out-file", diffDocx],
{ cwd: null, timeoutMs: null },
);
if (!result.ok) {
const e = result.error;
// exit 1 = changes found (normal)
if (e.kind === "non_zero_exit" && e.exitCode === 1) {
// fall through to file check
} else {
throwSpawnError(e);
}
}
try {
await stat(diffDocx);
} catch {
throw new Error(`docx-diff: diff file not found: ${diffDocx}`);
}
return JSON.stringify({ sourceDocx, modifiedDocx, diffDocx });
}
```
- [ ] **Step 5:运行测试,确认通过**
```bash
cd packages/workflow-agent-docx-diff && bun test __tests__/runner.test.ts
```
Expected:6 tests pass。
---
### Task 9:agent + package-descriptor + index(TDD)
**Files:**
- 新建: `packages/workflow-agent-docx-diff/src/agent.ts`
- 新建: `packages/workflow-agent-docx-diff/src/package-descriptor.ts`
- 新建: `packages/workflow-agent-docx-diff/src/index.ts`
- 新建: `packages/workflow-agent-docx-diff/__tests__/agent.test.ts`
- [ ] **Step 1:写 agent 测试**
```typescript
// __tests__/agent.test.ts
import { describe, expect, test } from "bun:test";
import { packageDescriptor } from "../src/package-descriptor.js";
import { createDocxDiffAgent } from "../src/agent.js";
describe("createDocxDiffAgent", () => {
test("returns an AdapterFn (function)", () => {
const agent = createDocxDiffAgent({ command: null });
expect(typeof agent).toBe("function");
});
test("AdapterFn returns a RoleFn (function)", () => {
const agent = createDocxDiffAgent({ command: null });
const roleFn = agent("", expect.anything() as never);
expect(typeof roleFn).toBe("function");
});
});
describe("packageDescriptor", () => {
test("has correct name", () => {
expect(packageDescriptor.name).toBe("@uncaged/workflow-agent-docx-diff");
});
});
```
- [ ] **Step 2:运行测试,确认失败**
```bash
cd packages/workflow-agent-docx-diff && bun test __tests__/agent.test.ts
```
Expected:`Cannot find module '../src/agent.js'`
- [ ] **Step 3:创建 src/agent.ts**
```typescript
import * as z from "zod/v4";
import { join, dirname } from "node:path";
import type { AdapterFn, RoleResult, ThreadContext, WorkflowRuntime } from "@uncaged/workflow-runtime";
import type { WriterMeta } from "@uncaged/workflow-template-document";
import { runDocxDiff } from "./runner.js";
import type { DocxDiffAgentConfig } from "./types.js";
export function createDocxDiffAgent(config: DocxDiffAgentConfig): AdapterFn {
return <T>(_prompt: string, schema: z.ZodType<T>) =>
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
const writerStep = ctx.steps.find((s) => s.role === "writer");
if (writerStep === undefined) throw new Error("differ: no writer step found");
const writerMeta = writerStep.meta as WriterMeta;
if (writerMeta.mode !== "edit")
throw new Error("differ: writer did not run in edit mode");
const diffDocx = join(dirname(writerMeta.outputDocx), "diff.docx");
const raw = await runDocxDiff(
config,
writerMeta.sourceDocx,
writerMeta.outputDocx,
diffDocx,
);
const meta = schema.parse(JSON.parse(raw)) as T;
return { meta, childThread: null };
};
}
```
- [ ] **Step 4:创建 src/package-descriptor.ts**
```typescript
import type { PackageDescriptor } from "@uncaged/workflow-runtime";
export const packageDescriptor: PackageDescriptor = {
name: "@uncaged/workflow-agent-docx-diff",
version: "0.1.0",
capabilities: ["docx-diff-cli", "docx-diff-report"],
configSchema: {
type: "object",
properties: {
command: {
anyOf: [{ type: "string" }, { type: "null" }],
description: "Path to docx-diff CLI binary; null uses PATH.",
},
},
additionalProperties: false,
},
};
```
- [ ] **Step 5:创建 src/index.ts**
```typescript
export { createDocxDiffAgent } from "./agent.js";
export { packageDescriptor } from "./package-descriptor.js";
export type { DocxDiffAgentConfig } from "./types.js";
```
- [ ] **Step 6:运行所有测试,确认通过**
```bash
cd packages/workflow-agent-docx-diff && bun test
```
Expected:全部通过(runner + agent)。
- [ ] **Step 7:运行全量构建检查**
```bash
cd /Users/yanjiayi/workspace/workflow && bun run check
```
Expected:无 TypeScript 错误,无 Biome 警告。
- [ ] **Step 8:Commit**
```bash
git add packages/workflow-agent-docx-diff/ tsconfig.json
git commit -m "feat(agent): add workflow-agent-docx-diff with docx-diff AdapterFn"
```
---
## Phase 4:收尾
### Task 10:更新 architecture.md + 全量验证
**Files:**
- 修改: `docs/architecture.md`
- [ ] **Step 1:在 architecture.md 的 Package map 表格中补充三个包**
`Agent adapters` 分组下(`workflow-agent-hermes` 行之后)追加:
```markdown
| | `@uncaged/workflow-agent-office``workflow-agent-office` | `AdapterFn` via `office-agent` CLI; generates or edits Word documents, stores outputs per threadId. |
| | `@uncaged/workflow-agent-docx-diff``workflow-agent-docx-diff` | `AdapterFn` via `docx-diff` CLI; produces Word-format diff reports for document edit workflows. |
```
`Templates` 分组下(`workflow-template-solve-issue` 行之后)追加:
```markdown
| | `@uncaged/workflow-template-document``workflow-template-document` | Document generation/editing workflow definition (writer + differ roles, moderator table, descriptor). |
```
- [ ] **Step 2:运行全量测试**
```bash
cd /Users/yanjiayi/workspace/workflow && bun test
```
Expected:所有测试通过,无新增失败。
- [ ] **Step 3:Commit**
```bash
git add docs/architecture.md
git commit -m "docs(architecture): add workflow-agent-office, workflow-agent-docx-diff, workflow-template-document"
```
---
## 验收标准
- [ ] `bun run check` 无错误(TypeScript + Biome)
- [ ] `bun test` 全部通过
- [ ] 三个包均在 `tsconfig.json` references 中
- [ ] `workflow-template-document` 的 moderator 4 个路径均有测试覆盖
- [ ] `workflow-agent-office` runner 测试覆盖:正常生成、正常编辑、非零退出、超时、文件未生成
- [ ] `workflow-agent-docx-diff` runner 测试覆盖:exit 0、exit 1(正常)、exit 2(错误)、超时、diff 文件未生成