From 495c0003564f92b84e3c50645d7cebe22252514d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 8 May 2026 06:29:49 +0000 Subject: [PATCH] refactor(workflow): split @uncaged/workflow-runtime from engine (Phase 1) Create packages/workflow-runtime with the minimal runtime subset: - Types (WorkflowFn, RoleOutput, AgentBinding, etc.) - createWorkflow (pure orchestration, zero I/O) - validateWorkflowDescriptor - Result/ok/err, START/END constants Zero external dependencies (zod as peer only). Zero node:fs/node:path imports. Engine (@uncaged/workflow) now depends on workflow-runtime and provides CAS/merkle/extract implementations via injection. Refs #121, relates #122 --- .../cli-workflow/__tests__/commands.test.ts | 20 +- packages/cli-workflow/__tests__/serve.test.ts | 8 +- packages/workflow-runtime/package.json | 16 ++ packages/workflow-runtime/src/bundle/index.ts | 2 + packages/workflow-runtime/src/bundle/types.ts | 13 ++ .../src/bundle/workflow-descriptor.ts | 40 ++++ packages/workflow-runtime/src/cas/index.ts | 1 + packages/workflow-runtime/src/cas/types.ts | 6 + .../src/engine/create-workflow.ts | 185 ++++++++++++++++++ packages/workflow-runtime/src/engine/index.ts | 1 + .../workflow-runtime/src/extract/index.ts | 1 + .../workflow-runtime/src/extract/types.ts | 9 + packages/workflow-runtime/src/index.ts | 35 ++++ .../src/types.ts | 13 +- packages/workflow-runtime/src/util/index.ts | 3 + .../workflow-runtime/src/util/refs-field.ts | 8 + packages/workflow-runtime/src/util/result.ts | 9 + packages/workflow-runtime/src/util/types.ts | 1 + packages/workflow-runtime/tsconfig.json | 21 ++ .../__tests__/build-descriptor.test.ts | 3 +- packages/workflow/__tests__/cas.test.ts | 30 ++- packages/workflow/__tests__/engine.test.ts | 5 +- .../workflow/__tests__/react-extract.test.ts | 3 +- .../workflow/__tests__/refs-tracking.test.ts | 4 +- .../workflow-as-agent-integration.test.ts | 5 +- .../__tests__/workflow-as-agent.test.ts | 3 +- packages/workflow/package.json | 1 + .../workflow/src/bundle/build-descriptor.ts | 3 +- .../src/bundle/extract-bundle-exports.ts | 2 +- packages/workflow/src/bundle/types.ts | 20 +- .../src/bundle/workflow-descriptor.ts | 41 +--- packages/workflow/src/cas/cas.ts | 16 +- packages/workflow/src/cas/merkle.ts | 5 +- packages/workflow/src/cas/types.ts | 7 +- .../workflow/src/engine/create-workflow.ts | 172 +--------------- packages/workflow/src/engine/engine.ts | 17 +- packages/workflow/src/engine/fork-thread.ts | 2 +- .../workflow/src/engine/resolve-role-meta.ts | 42 ++++ packages/workflow/src/engine/types.ts | 2 +- packages/workflow/src/engine/worker.ts | 2 +- packages/workflow/src/extract/extract-fn.ts | 4 +- .../workflow/src/extract/react-extract.ts | 4 +- packages/workflow/src/extract/types.ts | 10 +- packages/workflow/src/index.ts | 48 ++--- packages/workflow/src/util/result.ts | 10 +- packages/workflow/src/util/types.ts | 2 +- packages/workflow/src/workflow-as-agent.ts | 3 +- packages/workflow/tsconfig.json | 1 + tsconfig.json | 1 + 49 files changed, 536 insertions(+), 324 deletions(-) create mode 100644 packages/workflow-runtime/package.json create mode 100644 packages/workflow-runtime/src/bundle/index.ts create mode 100644 packages/workflow-runtime/src/bundle/types.ts create mode 100644 packages/workflow-runtime/src/bundle/workflow-descriptor.ts create mode 100644 packages/workflow-runtime/src/cas/index.ts create mode 100644 packages/workflow-runtime/src/cas/types.ts create mode 100644 packages/workflow-runtime/src/engine/create-workflow.ts create mode 100644 packages/workflow-runtime/src/engine/index.ts create mode 100644 packages/workflow-runtime/src/extract/index.ts create mode 100644 packages/workflow-runtime/src/extract/types.ts create mode 100644 packages/workflow-runtime/src/index.ts rename packages/{workflow => workflow-runtime}/src/types.ts (90%) create mode 100644 packages/workflow-runtime/src/util/index.ts create mode 100644 packages/workflow-runtime/src/util/refs-field.ts create mode 100644 packages/workflow-runtime/src/util/result.ts create mode 100644 packages/workflow-runtime/src/util/types.ts create mode 100644 packages/workflow-runtime/tsconfig.json create mode 100644 packages/workflow/src/engine/resolve-role-meta.ts diff --git a/packages/cli-workflow/__tests__/commands.test.ts b/packages/cli-workflow/__tests__/commands.test.ts index ef96700..2041053 100644 --- a/packages/cli-workflow/__tests__/commands.test.ts +++ b/packages/cli-workflow/__tests__/commands.test.ts @@ -3,7 +3,13 @@ import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promise import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getGlobalCasDir, getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow"; +import { + createContentMerkleNode, + getGlobalCasDir, + getRegisteredWorkflow, + readWorkflowRegistry, + serializeMerkleNode, +} from "@uncaged/workflow"; import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "../src/commands/cas/index.js"; import { cmdAdd, @@ -22,6 +28,10 @@ const fixtureDescriptor = `export const descriptor = { description: "fixture", r const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow"; `; +function casStoredForm(raw: string): string { + return serializeMerkleNode(createContentMerkleNode(raw)); +} + describe("cli workflow commands", () => { let prevEnv: string | undefined; let storageRoot: string; @@ -402,21 +412,23 @@ export const run = async function* (input, options) { }); test("cas put/get/list/rm use global cas dir (thread id not required for storage)", async () => { - const put = await cmdCasPut(storageRoot, "phase doc"); + const raw = "phase doc"; + const stored = casStoredForm(raw); + const put = await cmdCasPut(storageRoot, raw); expect(put.ok).toBe(true); if (!put.ok) { return; } const hash = put.value; const blobPath = join(getGlobalCasDir(storageRoot), `${hash}.txt`); - expect(await readFile(blobPath, "utf8")).toBe("phase doc"); + expect(await readFile(blobPath, "utf8")).toBe(stored); const got = await cmdCasGet(storageRoot, hash); expect(got.ok).toBe(true); if (!got.ok) { return; } - expect(got.value).toBe("phase doc"); + expect(got.value).toBe(stored); const listed = await cmdCasList(storageRoot); expect(listed.ok).toBe(true); diff --git a/packages/cli-workflow/__tests__/serve.test.ts b/packages/cli-workflow/__tests__/serve.test.ts index bba5047..1672e8d 100644 --- a/packages/cli-workflow/__tests__/serve.test.ts +++ b/packages/cli-workflow/__tests__/serve.test.ts @@ -1,7 +1,13 @@ import { describe, expect, test } from "bun:test"; +import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow"; + import { createApp } from "../src/commands/serve/app.js"; +function casStoredForm(raw: string): string { + return serializeMerkleNode(createContentMerkleNode(raw)); +} + function buildApp(storageRoot: string) { const app = createApp(storageRoot); return { @@ -89,7 +95,7 @@ describe("serve CAS round-trip", () => { const getRes = await fetch(`/api/cas/${putBody.hash}`); expect(getRes.status).toBe(200); const getBody = (await getRes.json()) as { content: string }; - expect(getBody.content).toBe("hello world"); + expect(getBody.content).toBe(casStoredForm("hello world")); // cleanup const delRes = await fetch(`/api/cas/${putBody.hash}`, { method: "DELETE" }); diff --git a/packages/workflow-runtime/package.json b/packages/workflow-runtime/package.json new file mode 100644 index 0000000..d3d5f70 --- /dev/null +++ b/packages/workflow-runtime/package.json @@ -0,0 +1,16 @@ +{ + "name": "@uncaged/workflow-runtime", + "version": "0.2.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "test": "bun test" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "devDependencies": { + "zod": "^4.0.0" + } +} diff --git a/packages/workflow-runtime/src/bundle/index.ts b/packages/workflow-runtime/src/bundle/index.ts new file mode 100644 index 0000000..6a2f60d --- /dev/null +++ b/packages/workflow-runtime/src/bundle/index.ts @@ -0,0 +1,2 @@ +export type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js"; +export { validateWorkflowDescriptor } from "./workflow-descriptor.js"; diff --git a/packages/workflow-runtime/src/bundle/types.ts b/packages/workflow-runtime/src/bundle/types.ts new file mode 100644 index 0000000..a1b4478 --- /dev/null +++ b/packages/workflow-runtime/src/bundle/types.ts @@ -0,0 +1,13 @@ +/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */ +export type WorkflowRoleSchema = Record; + +export type WorkflowRoleDescriptor = { + description: string; + schema: WorkflowRoleSchema; +}; + +/** Workflow metadata exported as `export const descriptor` from `.esm.js` bundles. */ +export type WorkflowDescriptor = { + description: string; + roles: Record; +}; diff --git a/packages/workflow-runtime/src/bundle/workflow-descriptor.ts b/packages/workflow-runtime/src/bundle/workflow-descriptor.ts new file mode 100644 index 0000000..e21851b --- /dev/null +++ b/packages/workflow-runtime/src/bundle/workflow-descriptor.ts @@ -0,0 +1,40 @@ +import { err, ok, type Result } from "../util/index.js"; + +import type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js"; + +export function validateWorkflowDescriptor(value: unknown): Result { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return err("descriptor must be a non-array object"); + } + const root = value as Record; + const description = root.description; + if (typeof description !== "string") { + return err("descriptor.description must be a string"); + } + const rolesRaw = root.roles; + if (rolesRaw === null || typeof rolesRaw !== "object" || Array.isArray(rolesRaw)) { + return err("descriptor.roles must be a non-array object"); + } + + const roles: Record = {}; + for (const [roleName, specUnknown] of Object.entries(rolesRaw)) { + if (specUnknown === null || typeof specUnknown !== "object" || Array.isArray(specUnknown)) { + return err(`descriptor.roles.${roleName} must be a non-array object`); + } + const spec = specUnknown as Record; + const roleDesc = spec.description; + if (typeof roleDesc !== "string") { + return err(`descriptor.roles.${roleName}.description must be a string`); + } + const schema = spec.schema; + if (schema === null || typeof schema !== "object" || Array.isArray(schema)) { + return err(`descriptor.roles.${roleName}.schema must be a non-array object`); + } + roles[roleName] = { + description: roleDesc, + schema: schema as WorkflowRoleSchema, + }; + } + + return ok({ description, roles }); +} diff --git a/packages/workflow-runtime/src/cas/index.ts b/packages/workflow-runtime/src/cas/index.ts new file mode 100644 index 0000000..5bfc042 --- /dev/null +++ b/packages/workflow-runtime/src/cas/index.ts @@ -0,0 +1 @@ +export type { CasStore } from "./types.js"; diff --git a/packages/workflow-runtime/src/cas/types.ts b/packages/workflow-runtime/src/cas/types.ts new file mode 100644 index 0000000..2be4d74 --- /dev/null +++ b/packages/workflow-runtime/src/cas/types.ts @@ -0,0 +1,6 @@ +export type CasStore = { + put(content: string): Promise; + get(hash: string): Promise; + delete(hash: string): Promise; + list(): Promise; +}; diff --git a/packages/workflow-runtime/src/engine/create-workflow.ts b/packages/workflow-runtime/src/engine/create-workflow.ts new file mode 100644 index 0000000..14c8c8c --- /dev/null +++ b/packages/workflow-runtime/src/engine/create-workflow.ts @@ -0,0 +1,185 @@ +import type { CasStore } from "../cas/index.js"; +import { + type AgentBinding, + type AgentContext, + type AgentFn, + END, + type ExtractContext, + type ModeratorContext, + type ResolveRoleMetaFn, + type RoleDefinition, + type RoleMeta, + type RoleOutput, + type RoleStep, + START, + type ThreadInput, + type WorkflowCompletion, + type WorkflowDefinition, + type WorkflowFn, + type WorkflowFnOptions, +} from "../types.js"; +import { mergeRefsWithContentHash } from "../util/index.js"; + +function isRoleNext( + next: (keyof M & string) | typeof END, +): next is keyof M & string { + return next !== END; +} + +function resolveExtractedRefs( + roleDef: RoleDefinition>, + meta: unknown, +): string[] { + const extractRefsFn = roleDef.extractRefs; + if (extractRefsFn === null || typeof extractRefsFn !== "function") { + return []; + } + return extractRefsFn(meta as Record); +} + +async function putContentBlob(store: CasStore, raw: string): Promise { + return store.put(raw); +} + +function agentForRole(binding: AgentBinding, roleName: string): AgentFn { + const overrides = binding.overrides; + const overrideFn: AgentFn | undefined = + overrides !== null ? overrides[roleName as keyof typeof overrides] : undefined; + return overrideFn !== undefined ? overrideFn : binding.agent; +} + +type AdvanceOutcome = + | { kind: "complete"; completion: WorkflowCompletion } + | { kind: "yield"; output: RoleOutput; step: RoleStep }; + +async function advanceOneRound( + def: Pick, "roles" | "moderator">, + binding: AgentBinding, + resolveRoleMeta: ResolveRoleMetaFn, + params: { + start: ModeratorContext["start"]; + steps: RoleStep[]; + options: WorkflowFnOptions; + }, +): Promise> { + const { start, steps, options } = params; + const modCtx: ModeratorContext = { + threadId: options.threadId, + depth: options.depth, + start, + steps, + }; + + const next = def.moderator(modCtx); + if (!isRoleNext(next)) { + return { + kind: "complete", + completion: { returnCode: 0, summary: "completed: moderator returned END" }, + }; + } + + const roleDef = def.roles[next]; + if (roleDef === undefined) { + return { kind: "complete", completion: { returnCode: 1, summary: `unknown role: ${next}` } }; + } + + const agentCtx: AgentContext = { + ...modCtx, + currentRole: { name: next, systemPrompt: roleDef.systemPrompt }, + cas: options.cas, + }; + + const agent = agentForRole(binding, next); + const raw = await agent(agentCtx as unknown as AgentContext); + + const extractCtx: ExtractContext = { + ...agentCtx, + agentContent: raw, + }; + + const meta = await resolveRoleMeta( + roleDef as unknown as RoleDefinition>, + extractCtx, + options, + ); + + const contentHash = await putContentBlob(options.cas, raw); + const refs = mergeRefsWithContentHash( + resolveExtractedRefs(roleDef as unknown as RoleDefinition>, meta), + contentHash, + ); + + const step = { + role: next, + contentHash, + meta, + refs, + timestamp: Date.now(), + } as RoleStep; + + return { + kind: "yield", + output: { + role: step.role, + contentHash: step.contentHash, + meta: step.meta, + refs: step.refs, + }, + step, + }; +} + +/** + * Binds pure role definitions + moderator to runtime agents. + * Assign with `export const run = createWorkflow(def, binding)` via `@uncaged/workflow`, + * which supplies {@link ResolveRoleMetaFn}. + */ +export function createWorkflow( + def: Pick, "roles" | "moderator">, + binding: AgentBinding, + resolveRoleMeta: ResolveRoleMetaFn, +): WorkflowFn { + return async function* workflowLoop( + input: ThreadInput, + options: WorkflowFnOptions, + ): AsyncGenerator { + const nowMs = Date.now(); + const start: ModeratorContext["start"] = { + role: START, + content: input.prompt, + meta: { maxRounds: options.maxRounds }, + timestamp: nowMs, + }; + + const baseTs = Date.now(); + let steps: RoleStep[] = input.steps.map((out, i) => ({ + role: out.role, + contentHash: out.contentHash, + meta: out.meta, + refs: out.refs, + timestamp: baseTs + i, + })) as RoleStep[]; + + while (true) { + if (steps.length >= options.maxRounds) { + return { + returnCode: 0, + summary: `completed: reached maxRounds (${options.maxRounds})`, + }; + } + + const outcome = await advanceOneRound(def, binding, resolveRoleMeta, { + start, + steps, + options, + }); + + if (outcome.kind === "complete") { + return outcome.completion; + } + + yield outcome.output; + steps = [...steps, outcome.step]; + } + }; +} diff --git a/packages/workflow-runtime/src/engine/index.ts b/packages/workflow-runtime/src/engine/index.ts new file mode 100644 index 0000000..2b6e901 --- /dev/null +++ b/packages/workflow-runtime/src/engine/index.ts @@ -0,0 +1 @@ +export { createWorkflow } from "./create-workflow.js"; diff --git a/packages/workflow-runtime/src/extract/index.ts b/packages/workflow-runtime/src/extract/index.ts new file mode 100644 index 0000000..02695ad --- /dev/null +++ b/packages/workflow-runtime/src/extract/index.ts @@ -0,0 +1 @@ +export type { ExtractFn } from "./types.js"; diff --git a/packages/workflow-runtime/src/extract/types.ts b/packages/workflow-runtime/src/extract/types.ts new file mode 100644 index 0000000..6c04f01 --- /dev/null +++ b/packages/workflow-runtime/src/extract/types.ts @@ -0,0 +1,9 @@ +import type * as z from "zod/v4"; + +import type { ExtractContext } from "../types.js"; + +export type ExtractFn = >( + schema: z.ZodType, + prompt: string, + ctx: ExtractContext, +) => Promise; diff --git a/packages/workflow-runtime/src/index.ts b/packages/workflow-runtime/src/index.ts new file mode 100644 index 0000000..481d9c6 --- /dev/null +++ b/packages/workflow-runtime/src/index.ts @@ -0,0 +1,35 @@ +export type { + WorkflowDescriptor, + WorkflowRoleDescriptor, + WorkflowRoleSchema, +} from "./bundle/types.js"; +export { validateWorkflowDescriptor } from "./bundle/workflow-descriptor.js"; +export type { CasStore } from "./cas/index.js"; +export { createWorkflow } from "./engine/index.js"; +export type { ExtractFn } from "./extract/index.js"; +export type { + AgentBinding, + AgentContext, + AgentFn, + ExtractContext, + ExtractMode, + LlmProvider, + Moderator, + ModeratorContext, + ResolveRoleMetaFn, + RoleDefinition, + RoleMeta, + RoleOutput, + RoleStep, + StartStep, + ThreadContext, + ThreadInput, + WorkflowCompletion, + WorkflowDefinition, + WorkflowFn, + WorkflowFnOptions, + WorkflowResult, +} from "./types.js"; +export { END, START } from "./types.js"; +export type { Result } from "./util/index.js"; +export { err, ok } from "./util/index.js"; diff --git a/packages/workflow/src/types.ts b/packages/workflow-runtime/src/types.ts similarity index 90% rename from packages/workflow/src/types.ts rename to packages/workflow-runtime/src/types.ts index 60a600c..1774740 100644 --- a/packages/workflow/src/types.ts +++ b/packages/workflow-runtime/src/types.ts @@ -36,7 +36,7 @@ export type WorkflowCompletion = { summary: string; }; -/** Final thread outcome from {@link executeThread}, including Merkle thread root CAS hash. */ +/** Final thread outcome from executeThread, including Merkle thread root CAS hash. */ export type WorkflowResult = WorkflowCompletion & { rootHash: string; }; @@ -115,10 +115,10 @@ export type ThreadContext = AgentContext; /** Raw string output from an LLM/CLI adapter; meta is extracted by the engine. */ export type AgentFn = (ctx: AgentContext) => Promise; -/** Runtime agent assignment (optional per-role overrides). */ +/** Runtime agent assignment (explicit null when no per-role overrides). */ export type AgentBinding = { agent: AgentFn; - overrides?: Partial>; + overrides: Partial> | null; }; /** Role wiring: prompts, schema, and human-readable description. */ @@ -148,3 +148,10 @@ export type WorkflowDefinition = { roles: { [K in keyof M & string]: RoleDefinition }; moderator: Moderator; }; + +/** Engine-injected meta extraction for workflow loops (single + react modes). */ +export type ResolveRoleMetaFn = ( + roleDef: RoleDefinition>, + extractCtx: ExtractContext, + options: WorkflowFnOptions, +) => Promise>; diff --git a/packages/workflow-runtime/src/util/index.ts b/packages/workflow-runtime/src/util/index.ts new file mode 100644 index 0000000..01b6b02 --- /dev/null +++ b/packages/workflow-runtime/src/util/index.ts @@ -0,0 +1,3 @@ +export { mergeRefsWithContentHash } from "./refs-field.js"; +export { err, ok } from "./result.js"; +export type { Result } from "./types.js"; diff --git a/packages/workflow-runtime/src/util/refs-field.ts b/packages/workflow-runtime/src/util/refs-field.ts new file mode 100644 index 0000000..687d2d0 --- /dev/null +++ b/packages/workflow-runtime/src/util/refs-field.ts @@ -0,0 +1,8 @@ +/** Append `contentHash` to `refs` when not already present (dedupe by first occurrence order). */ +export function mergeRefsWithContentHash(refs: string[], contentHash: string): string[] { + const out = [...refs]; + if (!out.includes(contentHash)) { + out.push(contentHash); + } + return out; +} diff --git a/packages/workflow-runtime/src/util/result.ts b/packages/workflow-runtime/src/util/result.ts new file mode 100644 index 0000000..6f92f67 --- /dev/null +++ b/packages/workflow-runtime/src/util/result.ts @@ -0,0 +1,9 @@ +import type { Result } from "./types.js"; + +export function ok(value: T): Result { + return { ok: true, value }; +} + +export function err(error: E): Result { + return { ok: false, error }; +} diff --git a/packages/workflow-runtime/src/util/types.ts b/packages/workflow-runtime/src/util/types.ts new file mode 100644 index 0000000..3a97ab3 --- /dev/null +++ b/packages/workflow-runtime/src/util/types.ts @@ -0,0 +1 @@ +export type Result = { ok: true; value: T } | { ok: false; error: E }; diff --git a/packages/workflow-runtime/tsconfig.json b/packages/workflow-runtime/tsconfig.json new file mode 100644 index 0000000..cfef04b --- /dev/null +++ b/packages/workflow-runtime/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": true, + "outDir": "dist", + "rootDir": "src", + "types": ["bun-types"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/workflow/__tests__/build-descriptor.test.ts b/packages/workflow/__tests__/build-descriptor.test.ts index 3487f61..d8aea98 100644 --- a/packages/workflow/__tests__/build-descriptor.test.ts +++ b/packages/workflow/__tests__/build-descriptor.test.ts @@ -1,9 +1,8 @@ import { describe, expect, test } from "bun:test"; +import { END } from "@uncaged/workflow-runtime"; import * as z from "zod/v4"; - import { buildDescriptor } from "../src/bundle/build-descriptor.js"; import { validateWorkflowDescriptor } from "../src/bundle/workflow-descriptor.js"; -import { END } from "../src/types.js"; describe("buildDescriptor", () => { test("produces a descriptor that validates and includes JSON schemas per role", () => { diff --git a/packages/workflow/__tests__/cas.test.ts b/packages/workflow/__tests__/cas.test.ts index 99d77cb..f04ecb6 100644 --- a/packages/workflow/__tests__/cas.test.ts +++ b/packages/workflow/__tests__/cas.test.ts @@ -5,6 +5,11 @@ import { join } from "node:path"; import { createCasStore } from "../src/cas/cas.js"; import { hashString } from "../src/cas/hash.js"; +import { createContentMerkleNode, serializeMerkleNode } from "../src/cas/merkle.js"; + +function casStoredForm(raw: string): string { + return serializeMerkleNode(createContentMerkleNode(raw)); +} describe("createCasStore", () => { let casDir: string; @@ -19,25 +24,30 @@ describe("createCasStore", () => { test("put returns consistent hash for same content", async () => { const cas = createCasStore(casDir); - const h1 = await cas.put("hello world"); - const h2 = await cas.put("hello world"); + const raw = "hello world"; + const stored = casStoredForm(raw); + const h1 = await cas.put(raw); + const h2 = await cas.put(raw); expect(h1).toBe(h2); + expect(h1).toBe(hashString(stored)); expect(h1).toHaveLength(13); }); - test("put returns hash matching hashString", async () => { + test("put returns hash matching hashString of merkle-stored form", async () => { const cas = createCasStore(casDir); const content = "some content to store"; + const stored = casStoredForm(content); const h = await cas.put(content); - expect(h).toBe(hashString(content)); + expect(h).toBe(hashString(stored)); }); - test("get returns stored content", async () => { + test("get returns merkle-serialized blob for raw puts", async () => { const cas = createCasStore(casDir); const content = "line1\nline2\nline3"; + const stored = casStoredForm(content); const h = await cas.put(content); const retrieved = await cas.get(h); - expect(retrieved).toBe(content); + expect(retrieved).toBe(stored); }); test("get returns null for missing hash", async () => { @@ -76,11 +86,13 @@ describe("createCasStore", () => { test("put is idempotent — same content written twice causes no error", async () => { const cas = createCasStore(casDir); - const h1 = await cas.put("idempotent"); - const h2 = await cas.put("idempotent"); + const raw = "idempotent"; + const stored = casStoredForm(raw); + const h1 = await cas.put(raw); + const h2 = await cas.put(raw); expect(h1).toBe(h2); const content = await cas.get(h1); - expect(content).toBe("idempotent"); + expect(content).toBe(stored); }); test("different content produces different hashes", async () => { diff --git a/packages/workflow/__tests__/engine.test.ts b/packages/workflow/__tests__/engine.test.ts index 1e668ed..5c2aa7a 100644 --- a/packages/workflow/__tests__/engine.test.ts +++ b/packages/workflow/__tests__/engine.test.ts @@ -2,8 +2,8 @@ import { afterEach, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { END } from "@uncaged/workflow-runtime"; import * as z from "zod/v4"; - import { createCasStore } from "../src/cas/cas.js"; import { createContentMerkleNode, @@ -13,7 +13,6 @@ import { } from "../src/cas/merkle.js"; import { createWorkflow } from "../src/engine/create-workflow.js"; import { executeThread } from "../src/engine/engine.js"; -import { END } from "../src/types.js"; import { createLogger } from "../src/util/logger.js"; const plannerMetaSchema = z.object({ @@ -669,7 +668,7 @@ describe("executeThread", () => { }, moderator: (ctx) => (ctx.steps.length === 0 ? "walker" : END), }, - { agent: async () => dagRootHash }, + { agent: async () => dagRootHash, overrides: null }, ); const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; diff --git a/packages/workflow/__tests__/react-extract.test.ts b/packages/workflow/__tests__/react-extract.test.ts index cfd659d..ed3faa1 100644 --- a/packages/workflow/__tests__/react-extract.test.ts +++ b/packages/workflow/__tests__/react-extract.test.ts @@ -2,12 +2,11 @@ import { afterEach, describe, expect, test } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import type { LlmProvider } from "@uncaged/workflow-runtime"; import * as z from "zod/v4"; - import { createCasStore } from "../src/cas/cas.js"; import { createContentMerkleNode, serializeMerkleNode } from "../src/cas/merkle.js"; import { reactExtract } from "../src/extract/react-extract.js"; -import type { LlmProvider } from "../src/types.js"; const metaSchema = z.object({ seen: z.string() }); diff --git a/packages/workflow/__tests__/refs-tracking.test.ts b/packages/workflow/__tests__/refs-tracking.test.ts index acaef7d..17804af 100644 --- a/packages/workflow/__tests__/refs-tracking.test.ts +++ b/packages/workflow/__tests__/refs-tracking.test.ts @@ -2,13 +2,12 @@ import { afterEach, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { END } from "@uncaged/workflow-runtime"; import * as z from "zod/v4"; - import { createCasStore } from "../src/cas/cas.js"; import { createWorkflow } from "../src/engine/create-workflow.js"; import { executeThread } from "../src/engine/engine.js"; import { buildForkPlan, parseThreadDataJsonl } from "../src/engine/fork-thread.js"; -import { END } from "../src/types.js"; import { createLogger } from "../src/util/logger.js"; const phaseSchema = z.object({ @@ -102,6 +101,7 @@ const refsDemoWorkflow = createWorkflow( }, { agent: async () => "plan-output", + overrides: null, }, ); diff --git a/packages/workflow/__tests__/workflow-as-agent-integration.test.ts b/packages/workflow/__tests__/workflow-as-agent-integration.test.ts index 93008c7..1c1f678 100644 --- a/packages/workflow/__tests__/workflow-as-agent-integration.test.ts +++ b/packages/workflow/__tests__/workflow-as-agent-integration.test.ts @@ -2,8 +2,8 @@ import { afterEach, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { END } from "@uncaged/workflow-runtime"; import * as z from "zod/v4"; - import { createCasStore } from "../src/cas/cas.js"; import { hashWorkflowBundleBytes } from "../src/cas/hash.js"; import { getContentMerklePayload, parseMerkleNode } from "../src/cas/merkle.js"; @@ -14,7 +14,6 @@ import { registerWorkflowVersion, writeWorkflowRegistry, } from "../src/registry/registry.js"; -import { END } from "../src/types.js"; import { createLogger } from "../src/util/logger.js"; import { workflowAsAgent } from "../src/workflow-as-agent.js"; @@ -153,7 +152,7 @@ describe("workflowAsAgent integration", () => { }, moderator: (ctx) => (ctx.steps.length === 0 ? "caller" : END), }, - { agent: workflowAsAgent("child-wf", { storageRoot: root }) }, + { agent: workflowAsAgent("child-wf", { storageRoot: root }), overrides: null }, ); const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; diff --git a/packages/workflow/__tests__/workflow-as-agent.test.ts b/packages/workflow/__tests__/workflow-as-agent.test.ts index 8237d73..0002eeb 100644 --- a/packages/workflow/__tests__/workflow-as-agent.test.ts +++ b/packages/workflow/__tests__/workflow-as-agent.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; - +import { type AgentContext, START } from "@uncaged/workflow-runtime"; import { createCasStore } from "../src/cas/cas.js"; import { hashWorkflowBundleBytes } from "../src/cas/hash.js"; import { parseMerkleNode } from "../src/cas/merkle.js"; @@ -11,7 +11,6 @@ import { registerWorkflowVersion, writeWorkflowRegistry, } from "../src/registry/registry.js"; -import { type AgentContext, START } from "../src/types.js"; import { workflowAsAgent } from "../src/workflow-as-agent.js"; function makeAgentCtx(params: { diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 1f79f8d..3cce1c0 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -8,6 +8,7 @@ "test": "bun test" }, "dependencies": { + "@uncaged/workflow-runtime": "workspace:*", "acorn": "^8.16.0", "xxhashjs": "^0.2.2", "yaml": "^2.8.4" diff --git a/packages/workflow/src/bundle/build-descriptor.ts b/packages/workflow/src/bundle/build-descriptor.ts index cdf7bf0..b81fd7c 100644 --- a/packages/workflow/src/bundle/build-descriptor.ts +++ b/packages/workflow/src/bundle/build-descriptor.ts @@ -1,6 +1,5 @@ +import type { RoleMeta, WorkflowDefinition } from "@uncaged/workflow-runtime"; import * as z from "zod/v4"; - -import type { RoleMeta, WorkflowDefinition } from "../types.js"; import type { WorkflowDescriptor, WorkflowRoleSchema } from "./types.js"; function stripJsonSchemaMeta(json: Record): WorkflowRoleSchema { diff --git a/packages/workflow/src/bundle/extract-bundle-exports.ts b/packages/workflow/src/bundle/extract-bundle-exports.ts index 72f99a9..6184b08 100644 --- a/packages/workflow/src/bundle/extract-bundle-exports.ts +++ b/packages/workflow/src/bundle/extract-bundle-exports.ts @@ -1,4 +1,4 @@ -import type { WorkflowFn } from "../types.js"; +import type { WorkflowFn } from "@uncaged/workflow-runtime"; import { err, ok, type Result } from "../util/index.js"; import { importWorkflowBundleModule } from "./bundle-import-env.js"; import { ensureUncagedWorkflowSymlink } from "./ensure-uncaged-workflow-symlink.js"; diff --git a/packages/workflow/src/bundle/types.ts b/packages/workflow/src/bundle/types.ts index 92c5416..5776cdf 100644 --- a/packages/workflow/src/bundle/types.ts +++ b/packages/workflow/src/bundle/types.ts @@ -1,18 +1,10 @@ -import type { WorkflowFn } from "../types.js"; +import type { WorkflowDescriptor, WorkflowFn } from "@uncaged/workflow-runtime"; -/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */ -export type WorkflowRoleSchema = Record; - -export type WorkflowRoleDescriptor = { - description: string; - schema: WorkflowRoleSchema; -}; - -/** Workflow metadata exported as `export const descriptor` from `.esm.js` bundles. */ -export type WorkflowDescriptor = { - description: string; - roles: Record; -}; +export type { + WorkflowDescriptor, + WorkflowRoleDescriptor, + WorkflowRoleSchema, +} from "@uncaged/workflow-runtime"; export type WorkflowBundleValidationInput = { /** Absolute or relative path (used for `.esm.js` suffix checks). */ diff --git a/packages/workflow/src/bundle/workflow-descriptor.ts b/packages/workflow/src/bundle/workflow-descriptor.ts index e21851b..fb586fa 100644 --- a/packages/workflow/src/bundle/workflow-descriptor.ts +++ b/packages/workflow/src/bundle/workflow-descriptor.ts @@ -1,40 +1 @@ -import { err, ok, type Result } from "../util/index.js"; - -import type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js"; - -export function validateWorkflowDescriptor(value: unknown): Result { - if (value === null || typeof value !== "object" || Array.isArray(value)) { - return err("descriptor must be a non-array object"); - } - const root = value as Record; - const description = root.description; - if (typeof description !== "string") { - return err("descriptor.description must be a string"); - } - const rolesRaw = root.roles; - if (rolesRaw === null || typeof rolesRaw !== "object" || Array.isArray(rolesRaw)) { - return err("descriptor.roles must be a non-array object"); - } - - const roles: Record = {}; - for (const [roleName, specUnknown] of Object.entries(rolesRaw)) { - if (specUnknown === null || typeof specUnknown !== "object" || Array.isArray(specUnknown)) { - return err(`descriptor.roles.${roleName} must be a non-array object`); - } - const spec = specUnknown as Record; - const roleDesc = spec.description; - if (typeof roleDesc !== "string") { - return err(`descriptor.roles.${roleName}.description must be a string`); - } - const schema = spec.schema; - if (schema === null || typeof schema !== "object" || Array.isArray(schema)) { - return err(`descriptor.roles.${roleName}.schema must be a non-array object`); - } - roles[roleName] = { - description: roleDesc, - schema: schema as WorkflowRoleSchema, - }; - } - - return ok({ description, roles }); -} +export { validateWorkflowDescriptor } from "@uncaged/workflow-runtime"; diff --git a/packages/workflow/src/cas/cas.ts b/packages/workflow/src/cas/cas.ts index e5502f5..b6a1cce 100644 --- a/packages/workflow/src/cas/cas.ts +++ b/packages/workflow/src/cas/cas.ts @@ -2,8 +2,19 @@ import { mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/pro import { join } from "node:path"; import { hashString } from "./hash.js"; +import { createContentMerkleNode, parseMerkleNode, serializeMerkleNode } from "./merkle.js"; import type { CasStore } from "./types.js"; +/** Raw strings become content merkle YAML; already-valid merkle documents pass through. */ +function normalizeCasPutContent(content: string): string { + try { + parseMerkleNode(content); + return content; + } catch { + return serializeMerkleNode(createContentMerkleNode(content)); + } +} + export function createCasStore(casDir: string): CasStore { async function ensureDir(): Promise { await mkdir(casDir, { recursive: true }); @@ -15,11 +26,12 @@ export function createCasStore(casDir: string): CasStore { return { async put(content: string): Promise { - const hash = hashString(content); + const toStore = normalizeCasPutContent(content); + const hash = hashString(toStore); await ensureDir(); const target = filePath(hash); const tmp = `${target}.tmp.${Date.now()}`; - await writeFile(tmp, content, "utf8"); + await writeFile(tmp, toStore, "utf8"); await rename(tmp, target); return hash; }, diff --git a/packages/workflow/src/cas/merkle.ts b/packages/workflow/src/cas/merkle.ts index 1838c1e..178f51e 100644 --- a/packages/workflow/src/cas/merkle.ts +++ b/packages/workflow/src/cas/merkle.ts @@ -77,10 +77,9 @@ export async function putThreadMerkleNode( return store.put(serializeMerkleNode(node)); } -/** Serializes a content Merkle node and stores it in CAS; returns its hash. */ +/** Stores agent/content text via CAS; {@link createCasStore} wraps raw strings as merkle content nodes. */ export async function putContentMerkleNode(store: CasStore, content: string): Promise { - const yamlText = serializeMerkleNode(createContentMerkleNode(content)); - return store.put(yamlText); + return store.put(content); } /** Loads a CAS blob and returns the payload string for a `content` Merkle node. */ diff --git a/packages/workflow/src/cas/types.ts b/packages/workflow/src/cas/types.ts index 9295cb5..35e150a 100644 --- a/packages/workflow/src/cas/types.ts +++ b/packages/workflow/src/cas/types.ts @@ -1,9 +1,4 @@ -export type CasStore = { - put(content: string): Promise; - get(hash: string): Promise; - delete(hash: string): Promise; - list(): Promise; -}; +export type { CasStore } from "@uncaged/workflow-runtime"; export type MerkleNodeType = "content" | "step" | "thread"; diff --git a/packages/workflow/src/engine/create-workflow.ts b/packages/workflow/src/engine/create-workflow.ts index fc2f95b..8132476 100644 --- a/packages/workflow/src/engine/create-workflow.ts +++ b/packages/workflow/src/engine/create-workflow.ts @@ -1,73 +1,12 @@ -import { putContentMerkleNode } from "../cas/index.js"; -import { buildExtractUserContent, reactExtract } from "../extract/index.js"; -import { - type AgentBinding, - type AgentContext, - END, - type ExtractContext, - type ModeratorContext, - type RoleDefinition, - type RoleMeta, - type RoleOutput, - type RoleStep, - START, - type ThreadInput, - type WorkflowCompletion, - type WorkflowDefinition, - type WorkflowFn, - type WorkflowFnOptions, -} from "../types.js"; -import { mergeRefsWithContentHash } from "../util/index.js"; +import type { + AgentBinding, + RoleMeta, + WorkflowDefinition, + WorkflowFn, +} from "@uncaged/workflow-runtime"; +import { createWorkflow as createWorkflowRuntime } from "@uncaged/workflow-runtime"; -function isRoleNext( - next: (keyof M & string) | typeof END, -): next is keyof M & string { - return next !== END; -} - -function resolveExtractedRefs( - roleDef: RoleDefinition>, - meta: unknown, -): string[] { - const extractRefsFn = roleDef.extractRefs; - if (extractRefsFn === null || typeof extractRefsFn !== "function") { - return []; - } - return extractRefsFn(meta as Record); -} - -async function resolveRoleMeta( - roleDef: RoleDefinition>, - extractCtx: ExtractContext, - options: WorkflowFnOptions, -): Promise> { - if (roleDef.extractMode === "react") { - if (options.llmProvider === null) { - throw new Error( - 'createWorkflow: WorkflowFnOptions.llmProvider is required when a role uses extractMode "react"', - ); - } - const text = await buildExtractUserContent( - extractCtx as unknown as ExtractContext, - roleDef.extractPrompt, - ); - const reactResult = await reactExtract({ - text, - schema: roleDef.schema, - provider: options.llmProvider, - cas: options.cas, - }); - if (!reactResult.ok) { - throw new Error(`react extract failed: ${reactResult.error}`); - } - return reactResult.value as Record; - } - return (await options.extract( - roleDef.schema, - roleDef.extractPrompt, - extractCtx as unknown as ExtractContext, - )) as Record; -} +import { resolveRoleMeta } from "./resolve-role-meta.js"; /** * Binds pure role definitions + moderator to runtime agents. @@ -78,98 +17,5 @@ export function createWorkflow( def: Pick, "roles" | "moderator">, binding: AgentBinding, ): WorkflowFn { - return async function* workflowLoop( - input: ThreadInput, - options: WorkflowFnOptions, - ): AsyncGenerator { - const nowMs = Date.now(); - const start: ModeratorContext["start"] = { - role: START, - content: input.prompt, - meta: { maxRounds: options.maxRounds }, - timestamp: nowMs, - }; - - const baseTs = Date.now(); - let steps: RoleStep[] = input.steps.map((out, i) => ({ - role: out.role, - contentHash: out.contentHash, - meta: out.meta, - refs: out.refs, - timestamp: baseTs + i, - })) as RoleStep[]; - - while (true) { - if (steps.length >= options.maxRounds) { - return { - returnCode: 0, - summary: `completed: reached maxRounds (${options.maxRounds})`, - }; - } - - const modCtx: ModeratorContext = { - threadId: options.threadId, - depth: options.depth, - start, - steps, - }; - - const next = def.moderator(modCtx); - - if (!isRoleNext(next)) { - return { returnCode: 0, summary: "completed: moderator returned END" }; - } - - const roleDef = def.roles[next]; - if (roleDef === undefined) { - return { returnCode: 1, summary: `unknown role: ${next}` }; - } - - const agentCtx: AgentContext = { - ...modCtx, - currentRole: { name: next, systemPrompt: roleDef.systemPrompt }, - cas: options.cas, - }; - - const agent = binding.overrides?.[next] ?? binding.agent; - - const raw = await agent(agentCtx as unknown as AgentContext); - - const extractCtx: ExtractContext = { - ...agentCtx, - agentContent: raw, - }; - - const meta = await resolveRoleMeta( - roleDef as unknown as RoleDefinition>, - extractCtx, - options, - ); - - const contentHash = await putContentMerkleNode(options.cas, raw); - - const refs = mergeRefsWithContentHash( - resolveExtractedRefs(roleDef as unknown as RoleDefinition>, meta), - contentHash, - ); - - const ts = Date.now(); - const step = { - role: next, - contentHash, - meta, - refs, - timestamp: ts, - } as RoleStep; - - yield { - role: step.role, - contentHash: step.contentHash, - meta: step.meta, - refs: step.refs, - }; - - steps = [...steps, step]; - } - }; + return createWorkflowRuntime(def, binding, resolveRoleMeta); } diff --git a/packages/workflow/src/engine/engine.ts b/packages/workflow/src/engine/engine.ts index 03cb5b7..fd5cbbd 100644 --- a/packages/workflow/src/engine/engine.ts +++ b/packages/workflow/src/engine/engine.ts @@ -1,6 +1,13 @@ import { appendFile, mkdir } from "node:fs/promises"; import { dirname } from "node:path"; - +import type { + LlmProvider, + ThreadInput, + WorkflowCompletion, + WorkflowFn, + WorkflowFnOptions, + WorkflowResult, +} from "@uncaged/workflow-runtime"; import { type CasStore, getContentMerklePayload, @@ -10,14 +17,6 @@ import { import { resolveModel } from "../config/index.js"; import { createExtract } from "../extract/index.js"; import { readWorkflowRegistry, type WorkflowConfig } from "../registry/index.js"; -import type { - LlmProvider, - ThreadInput, - WorkflowCompletion, - WorkflowFn, - WorkflowFnOptions, - WorkflowResult, -} from "../types.js"; import { err, type LogFn, normalizeRefsField, ok, type Result } from "../util/index.js"; import { runSupervisor } from "./supervisor.js"; diff --git a/packages/workflow/src/engine/fork-thread.ts b/packages/workflow/src/engine/fork-thread.ts index 10dfd02..8b52b24 100644 --- a/packages/workflow/src/engine/fork-thread.ts +++ b/packages/workflow/src/engine/fork-thread.ts @@ -1,4 +1,4 @@ -import type { WorkflowCompletion } from "../types.js"; +import type { WorkflowCompletion } from "@uncaged/workflow-runtime"; import { err, normalizeRefsField, ok, type Result } from "../util/index.js"; import type { ForkHistoricalStep, ForkPlan, ParsedThreadStartRecord } from "./types.js"; diff --git a/packages/workflow/src/engine/resolve-role-meta.ts b/packages/workflow/src/engine/resolve-role-meta.ts new file mode 100644 index 0000000..ce1657a --- /dev/null +++ b/packages/workflow/src/engine/resolve-role-meta.ts @@ -0,0 +1,42 @@ +import type { + ExtractContext, + RoleDefinition, + RoleMeta, + WorkflowFnOptions, +} from "@uncaged/workflow-runtime"; + +import { buildExtractUserContent } from "../extract/extract-fn.js"; +import { reactExtract } from "../extract/react-extract.js"; + +export async function resolveRoleMeta( + roleDef: RoleDefinition>, + extractCtx: ExtractContext, + options: WorkflowFnOptions, +): Promise> { + if (roleDef.extractMode === "react") { + if (options.llmProvider === null) { + throw new Error( + 'createWorkflow: WorkflowFnOptions.llmProvider is required when a role uses extractMode "react"', + ); + } + const text = await buildExtractUserContent( + extractCtx as unknown as ExtractContext, + roleDef.extractPrompt, + ); + const reactResult = await reactExtract({ + text, + schema: roleDef.schema, + provider: options.llmProvider, + cas: options.cas, + }); + if (!reactResult.ok) { + throw new Error(`react extract failed: ${reactResult.error}`); + } + return reactResult.value as Record; + } + return (await options.extract( + roleDef.schema, + roleDef.extractPrompt, + extractCtx as unknown as ExtractContext, + )) as Record; +} diff --git a/packages/workflow/src/engine/types.ts b/packages/workflow/src/engine/types.ts index 8c2d645..9228547 100644 --- a/packages/workflow/src/engine/types.ts +++ b/packages/workflow/src/engine/types.ts @@ -1,5 +1,5 @@ +import type { RoleOutput } from "@uncaged/workflow-runtime"; import type { CasStore } from "../cas/index.js"; -import type { RoleOutput } from "../types.js"; import type { Result } from "../util/index.js"; export type SupervisorDecision = "continue" | "stop"; diff --git a/packages/workflow/src/engine/worker.ts b/packages/workflow/src/engine/worker.ts index 3ab969b..e83ab7d 100644 --- a/packages/workflow/src/engine/worker.ts +++ b/packages/workflow/src/engine/worker.ts @@ -1,9 +1,9 @@ import { appendFile, mkdir, unlink, writeFile } from "node:fs/promises"; import { createServer, type Socket } from "node:net"; import { dirname, join } from "node:path"; +import type { RoleOutput, WorkflowFn, WorkflowResult } from "@uncaged/workflow-runtime"; import { ensureUncagedWorkflowSymlink, importWorkflowBundleModule } from "../bundle/index.js"; import { createCasStore } from "../cas/index.js"; -import type { RoleOutput, WorkflowFn, WorkflowResult } from "../types.js"; import { createLogger, err, diff --git a/packages/workflow/src/extract/extract-fn.ts b/packages/workflow/src/extract/extract-fn.ts index 86da016..4705da8 100644 --- a/packages/workflow/src/extract/extract-fn.ts +++ b/packages/workflow/src/extract/extract-fn.ts @@ -1,9 +1,7 @@ +import type { ExtractContext, ExtractFn, LlmProvider } from "@uncaged/workflow-runtime"; import type * as z from "zod/v4"; - import { getContentMerklePayload } from "../cas/index.js"; -import type { ExtractContext, LlmProvider } from "../types.js"; import { llmExtractWithRetry } from "./llm-extract.js"; -import type { ExtractFn } from "./types.js"; /** Builds the user-side extraction prompt (thread + agent output + instruction). */ export async function buildExtractUserContent( diff --git a/packages/workflow/src/extract/react-extract.ts b/packages/workflow/src/extract/react-extract.ts index 5801b20..c090a06 100644 --- a/packages/workflow/src/extract/react-extract.ts +++ b/packages/workflow/src/extract/react-extract.ts @@ -1,7 +1,5 @@ +import type { CasStore, LlmProvider } from "@uncaged/workflow-runtime"; import type * as z from "zod/v4"; - -import type { CasStore } from "../cas/index.js"; -import type { LlmProvider } from "../types.js"; import { err, ok, type Result } from "../util/index.js"; import { extractFunctionToolFromZodSchema } from "./llm-extract.js"; diff --git a/packages/workflow/src/extract/types.ts b/packages/workflow/src/extract/types.ts index 011db70..016ab60 100644 --- a/packages/workflow/src/extract/types.ts +++ b/packages/workflow/src/extract/types.ts @@ -1,13 +1,7 @@ +import type { CasStore, LlmProvider } from "@uncaged/workflow-runtime"; import type * as z from "zod/v4"; -import type { CasStore } from "../cas/index.js"; -import type { ExtractContext, LlmProvider } from "../types.js"; - -export type ExtractFn = >( - schema: z.ZodType, - prompt: string, - ctx: ExtractContext, -) => Promise; +export type { ExtractFn } from "@uncaged/workflow-runtime"; export type ReactExtractArgs> = { text: string; diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index c856d8d..5c2ea34 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -1,3 +1,27 @@ +export { + type AgentBinding, + type AgentContext, + type AgentFn, + END, + type ExtractContext, + type ExtractMode, + type LlmProvider, + type Moderator, + type ModeratorContext, + type RoleDefinition, + type RoleMeta, + type RoleOutput, + type RoleStep, + START, + type StartStep, + type ThreadContext, + type ThreadInput, + type WorkflowCompletion, + type WorkflowDefinition, + type WorkflowFn, + type WorkflowFnOptions, + type WorkflowResult, +} from "@uncaged/workflow-runtime"; export { buildDescriptor, type ExtractedBundleExports, @@ -79,30 +103,6 @@ export { workflowRegistryPath, writeWorkflowRegistry, } from "./registry/index.js"; -export { - type AgentBinding, - type AgentContext, - type AgentFn, - END, - type ExtractContext, - type ExtractMode, - type LlmProvider, - type Moderator, - type ModeratorContext, - type RoleDefinition, - type RoleMeta, - type RoleOutput, - type RoleStep, - START, - type StartStep, - type ThreadContext, - type ThreadInput, - type WorkflowCompletion, - type WorkflowDefinition, - type WorkflowFn, - type WorkflowFnOptions, - type WorkflowResult, -} from "./types.js"; export { CROCKFORD_BASE32_ALPHABET, type CreateLoggerOptions, diff --git a/packages/workflow/src/util/result.ts b/packages/workflow/src/util/result.ts index 6f92f67..c6cede3 100644 --- a/packages/workflow/src/util/result.ts +++ b/packages/workflow/src/util/result.ts @@ -1,9 +1 @@ -import type { Result } from "./types.js"; - -export function ok(value: T): Result { - return { ok: true, value }; -} - -export function err(error: E): Result { - return { ok: false, error }; -} +export { err, ok } from "@uncaged/workflow-runtime"; diff --git a/packages/workflow/src/util/types.ts b/packages/workflow/src/util/types.ts index e55327e..800c37f 100644 --- a/packages/workflow/src/util/types.ts +++ b/packages/workflow/src/util/types.ts @@ -1,4 +1,4 @@ -export type Result = { ok: true; value: T } | { ok: false; error: E }; +export type { Result } from "@uncaged/workflow-runtime"; export type LoggerSink = { kind: "stderr" } | { kind: "file"; path: string }; diff --git a/packages/workflow/src/workflow-as-agent.ts b/packages/workflow/src/workflow-as-agent.ts index bd15122..86a8130 100644 --- a/packages/workflow/src/workflow-as-agent.ts +++ b/packages/workflow/src/workflow-as-agent.ts @@ -1,12 +1,11 @@ import { join } from "node:path"; - +import type { AgentContext, AgentFn, ThreadInput } from "@uncaged/workflow-runtime"; import { extractBundleExports } from "./bundle/index.js"; import { createCasStore } from "./cas/index.js"; import type { ExecuteThreadIo } from "./engine/index.js"; import { executeThread } from "./engine/index.js"; import type { WorkflowConfig } from "./registry/index.js"; import { getRegisteredWorkflow, readWorkflowRegistry } from "./registry/index.js"; -import type { AgentContext, AgentFn, ThreadInput } from "./types.js"; import { createLogger, generateUlid, diff --git a/packages/workflow/tsconfig.json b/packages/workflow/tsconfig.json index 66434a9..72ffea1 100644 --- a/packages/workflow/tsconfig.json +++ b/packages/workflow/tsconfig.json @@ -1,4 +1,5 @@ { + "references": [{ "path": "../workflow-runtime" }], "compilerOptions": { "target": "ES2022", "lib": ["ES2022"], diff --git a/tsconfig.json b/tsconfig.json index 8de7ef5..d207f42 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "types": ["bun-types"] }, "references": [ + { "path": "packages/workflow-runtime" }, { "path": "packages/workflow" }, { "path": "packages/workflow-agent-llm" }, { "path": "packages/workflow-agent-cursor" },