diff --git a/packages/workflow-template-document/__tests__/document-template.test.ts b/packages/workflow-template-document/__tests__/document-template.test.ts new file mode 100644 index 0000000..7c950b8 --- /dev/null +++ b/packages/workflow-template-document/__tests__/document-template.test.ts @@ -0,0 +1,87 @@ +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["steps"], +): ModeratorContext { + return { + threadId: "01TEST000000000000000000TR", + depth: 0, + bundleHash: "TESTHASH00001", + start: { role: START, content: "", meta: {}, timestamp: 0, parentState: null }, + steps, + }; +} + +function writerGenerateStep(): RoleStep { + return { + role: "writer", + contentHash: "STUBHASHWRITER001", + meta: { mode: "generate", outputDocx: "/out/output.docx", sourceDocx: null } satisfies WriterMeta, + refs: [], + timestamp: 1, + }; +} + +function writerEditStep(): RoleStep { + return { + role: "writer", + contentHash: "STUBHASHWRITER002", + meta: { mode: "edit", outputDocx: "/out/modified.docx", sourceDocx: "/out/original.docx" } satisfies WriterMeta, + refs: [], + timestamp: 1, + }; +} + +function differStep(): RoleStep { + 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"); + }); +}); diff --git a/packages/workflow-template-document/src/descriptor.ts b/packages/workflow-template-document/src/descriptor.ts new file mode 100644 index 0000000..b8abf77 --- /dev/null +++ b/packages/workflow-template-document/src/descriptor.ts @@ -0,0 +1,11 @@ +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, + }); +} diff --git a/packages/workflow-template-document/src/index.ts b/packages/workflow-template-document/src/index.ts index cb0ff5c..938915c 100644 --- a/packages/workflow-template-document/src/index.ts +++ b/packages/workflow-template-document/src/index.ts @@ -1 +1,27 @@ -export {}; +import type { WorkflowDefinition } from "@uncaged/workflow-runtime"; +import { documentTable } from "./moderator.js"; +import { DOCUMENT_WORKFLOW_DESCRIPTION, type DocumentMeta, 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 = { + description: DOCUMENT_WORKFLOW_DESCRIPTION, + roles: documentRoles, + table: documentTable, +}; diff --git a/packages/workflow-template-document/src/moderator.ts b/packages/workflow-template-document/src/moderator.ts new file mode 100644 index 0000000..c030736 --- /dev/null +++ b/packages/workflow-template-document/src/moderator.ts @@ -0,0 +1,27 @@ +import { + END, + type ModeratorCondition, + type ModeratorTable, + START, +} from "@uncaged/workflow-runtime"; +import type { WriterMeta } from "./roles/writer.js"; +import type { DocumentMeta } from "./roles.js"; + +const writerIsEditMode: ModeratorCondition = { + 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 = { + [START]: [{ condition: "FALLBACK", role: "writer" }], + writer: [ + { condition: writerIsEditMode, role: "differ" }, + { condition: "FALLBACK", role: END }, + ], + differ: [{ condition: "FALLBACK", role: END }], +}; diff --git a/packages/workflow-template-document/src/roles.ts b/packages/workflow-template-document/src/roles.ts new file mode 100644 index 0000000..b5f3c2f --- /dev/null +++ b/packages/workflow-template-document/src/roles.ts @@ -0,0 +1,20 @@ +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; +}; + +export const documentRoles: DocumentRoles = { + writer: writerRole, + differ: differRole, +}; diff --git a/packages/workflow-template-document/src/roles/differ.ts b/packages/workflow-template-document/src/roles/differ.ts new file mode 100644 index 0000000..bf69c69 --- /dev/null +++ b/packages/workflow-template-document/src/roles/differ.ts @@ -0,0 +1,16 @@ +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; + +export const differRole: RoleDefinition = { + description: "Produces a Word-format diff report of the writer's changes (edit mode only).", + systemPrompt: "", + schema: differMetaSchema, +}; diff --git a/packages/workflow-template-document/src/roles/index.ts b/packages/workflow-template-document/src/roles/index.ts new file mode 100644 index 0000000..dc391e7 --- /dev/null +++ b/packages/workflow-template-document/src/roles/index.ts @@ -0,0 +1,4 @@ +export type { DifferMeta } from "./differ.js"; +export { differMetaSchema, differRole } from "./differ.js"; +export type { WriterMeta } from "./writer.js"; +export { writerMetaSchema, writerRole } from "./writer.js"; diff --git a/packages/workflow-template-document/src/roles/writer.ts b/packages/workflow-template-document/src/roles/writer.ts new file mode 100644 index 0000000..5dea205 --- /dev/null +++ b/packages/workflow-template-document/src/roles/writer.ts @@ -0,0 +1,23 @@ +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; + +export const writerRole: RoleDefinition = { + description: "Generates or modifies a Word document via an external agent.", + systemPrompt: "", + schema: writerMetaSchema, +}; diff --git a/packages/workflow-template-document/src/types.ts b/packages/workflow-template-document/src/types.ts new file mode 100644 index 0000000..36bde04 --- /dev/null +++ b/packages/workflow-template-document/src/types.ts @@ -0,0 +1,4 @@ +export type DocumentStartInput = { + prompt: string; + inputDocx: string | null; +};