diff --git a/packages/cli-workflow/__tests__/commands.test.ts b/packages/cli-workflow/__tests__/commands.test.ts index 2041053..4800fb3 100644 --- a/packages/cli-workflow/__tests__/commands.test.ts +++ b/packages/cli-workflow/__tests__/commands.test.ts @@ -3,13 +3,9 @@ import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promise import { tmpdir } from "node:os"; import { join } from "node:path"; -import { - createContentMerkleNode, - getGlobalCasDir, - getRegisteredWorkflow, - readWorkflowRegistry, - serializeMerkleNode, -} from "@uncaged/workflow"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; +import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow-cas"; +import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register"; import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "../src/commands/cas/index.js"; import { cmdAdd, @@ -25,7 +21,7 @@ import { addCliArgs } from "./bundle-fixture.js"; const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {} }; `; -const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow"; +const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas"; `; function casStoredForm(raw: string): string { diff --git a/packages/cli-workflow/__tests__/fork-cli.test.ts b/packages/cli-workflow/__tests__/fork-cli.test.ts index 9a0f385..ac2e053 100644 --- a/packages/cli-workflow/__tests__/fork-cli.test.ts +++ b/packages/cli-workflow/__tests__/fork-cli.test.ts @@ -2,7 +2,8 @@ import { afterEach, beforeEach, 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 { createCasStore, getContentMerklePayload, getGlobalCasDir } from "@uncaged/workflow"; +import { createCasStore, getContentMerklePayload } from "@uncaged/workflow-cas"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; import { cmdFork, cmdRun } from "../src/commands/thread/index.js"; import { cmdAdd } from "../src/commands/workflow/index.js"; import { pathExists } from "../src/fs-utils.js"; @@ -10,7 +11,7 @@ import { addCliArgs } from "./bundle-fixture.js"; import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js"; /** Three-role workflow that respects `input.steps` for fork/resume. */ -const threeRoleBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow"; +const threeRoleBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow-cas"; export const descriptor = { description: "fork-cli", diff --git a/packages/cli-workflow/__tests__/gc-cli.test.ts b/packages/cli-workflow/__tests__/gc-cli.test.ts index c5e7d2a..49a0654 100644 --- a/packages/cli-workflow/__tests__/gc-cli.test.ts +++ b/packages/cli-workflow/__tests__/gc-cli.test.ts @@ -4,12 +4,9 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { - createCasStore, - garbageCollectCas, - getGlobalCasDir, - putContentMerkleNode, -} from "@uncaged/workflow"; +import { createCasStore, putContentMerkleNode } from "@uncaged/workflow-cas"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; +import { garbageCollectCas } from "@uncaged/workflow-execute"; import { cmdThreadRemove } from "../src/commands/thread/index.js"; import { pathExists } from "../src/fs-utils.js"; diff --git a/packages/cli-workflow/__tests__/init-template.test.ts b/packages/cli-workflow/__tests__/init-template.test.ts index c1423e9..5190da8 100644 --- a/packages/cli-workflow/__tests__/init-template.test.ts +++ b/packages/cli-workflow/__tests__/init-template.test.ts @@ -50,7 +50,6 @@ describe("init template", () => { dependencies: Record; }; expect(pkg.type).toBe("module"); - expect(pkg.dependencies["@uncaged/workflow"]).toBeDefined(); expect(pkg.dependencies["@uncaged/workflow-runtime"]).toBeDefined(); expect(pkg.dependencies.zod).toBeDefined(); expect(pkg.name).toContain("review-pr"); diff --git a/packages/cli-workflow/__tests__/init-workspace.test.ts b/packages/cli-workflow/__tests__/init-workspace.test.ts index 28e8265..93d924c 100644 --- a/packages/cli-workflow/__tests__/init-workspace.test.ts +++ b/packages/cli-workflow/__tests__/init-workspace.test.ts @@ -46,7 +46,7 @@ describe("init workspace", () => { dependencies: Record; }; expect(wfPkg.type).toBe("module"); - expect(wfPkg.dependencies["@uncaged/workflow"]).toBeDefined(); + expect(wfPkg.dependencies["@uncaged/workflow-runtime"]).toBeDefined(); expect(wfPkg.dependencies.zod).toBeDefined(); const tsconfig = JSON.parse(await readFile(join(root, "tsconfig.json"), "utf8")) as { diff --git a/packages/cli-workflow/__tests__/live.test.ts b/packages/cli-workflow/__tests__/live.test.ts index 5c3fa19..8f0b4ae 100644 --- a/packages/cli-workflow/__tests__/live.test.ts +++ b/packages/cli-workflow/__tests__/live.test.ts @@ -5,7 +5,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { createCasStore, getGlobalCasDir, putContentMerkleNode } from "@uncaged/workflow"; +import { createCasStore, putContentMerkleNode } from "@uncaged/workflow-cas"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; import { formatLiveDebugLine, diff --git a/packages/cli-workflow/__tests__/serve.test.ts b/packages/cli-workflow/__tests__/serve.test.ts index 155f5de..3910a8a 100644 --- a/packages/cli-workflow/__tests__/serve.test.ts +++ b/packages/cli-workflow/__tests__/serve.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow"; +import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow-cas"; import { createApp } from "../src/commands/serve/app.js"; diff --git a/packages/cli-workflow/__tests__/storage-env.test.ts b/packages/cli-workflow/__tests__/storage-env.test.ts index 518ce8c..d4d5f60 100644 --- a/packages/cli-workflow/__tests__/storage-env.test.ts +++ b/packages/cli-workflow/__tests__/storage-env.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow"; +import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util"; import { resolveWorkflowStorageRoot } from "../src/storage-env.js"; describe("resolveWorkflowStorageRoot", () => { diff --git a/packages/cli-workflow/__tests__/thread-cli.test.ts b/packages/cli-workflow/__tests__/thread-cli.test.ts index 715508b..ef13120 100644 --- a/packages/cli-workflow/__tests__/thread-cli.test.ts +++ b/packages/cli-workflow/__tests__/thread-cli.test.ts @@ -4,7 +4,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { getGlobalCasDir } from "@uncaged/workflow"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; import { cmdCasPut } from "../src/commands/cas/index.js"; import { cmdKill, @@ -21,7 +21,7 @@ import { pathExists, readTextFileIfExists } from "../src/fs-utils.js"; import { addCliArgs } from "./bundle-fixture.js"; import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js"; -const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow"; +const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas"; `; const threadFixtureDescriptor = `export const descriptor = { diff --git a/packages/cli-workflow/package.json b/packages/cli-workflow/package.json index c0bb241..6579618 100644 --- a/packages/cli-workflow/package.json +++ b/packages/cli-workflow/package.json @@ -6,8 +6,12 @@ "uncaged-workflow": "src/cli.ts" }, "dependencies": { + "@uncaged/workflow-protocol": "workspace:*", + "@uncaged/workflow-util": "workspace:*", + "@uncaged/workflow-cas": "workspace:*", + "@uncaged/workflow-execute": "workspace:*", + "@uncaged/workflow-register": "workspace:*", "@uncaged/workflow-runtime": "workspace:*", - "@uncaged/workflow": "workspace:*", "hono": "^4.12.18", "yaml": "^2.8.4" }, diff --git a/packages/cli-workflow/src/bundle-store.ts b/packages/cli-workflow/src/bundle-store.ts index 83d9af2..1305c36 100644 --- a/packages/cli-workflow/src/bundle-store.ts +++ b/packages/cli-workflow/src/bundle-store.ts @@ -1,7 +1,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { pathExists } from "./fs-utils.js"; diff --git a/packages/cli-workflow/src/commands/cas/gc.ts b/packages/cli-workflow/src/commands/cas/gc.ts index cf1aab9..e00b41f 100644 --- a/packages/cli-workflow/src/commands/cas/gc.ts +++ b/packages/cli-workflow/src/commands/cas/gc.ts @@ -1,4 +1,5 @@ -import { type GcResult, garbageCollectCas, type Result } from "@uncaged/workflow"; +import type { Result } from "@uncaged/workflow-protocol"; +import { type GcResult, garbageCollectCas } from "@uncaged/workflow-execute"; export async function cmdGc(storageRoot: string): Promise> { return garbageCollectCas(storageRoot); diff --git a/packages/cli-workflow/src/commands/cas/get.ts b/packages/cli-workflow/src/commands/cas/get.ts index 2ecdccd..7eb9c4c 100644 --- a/packages/cli-workflow/src/commands/cas/get.ts +++ b/packages/cli-workflow/src/commands/cas/get.ts @@ -1,4 +1,6 @@ -import { createCasStore, err, getGlobalCasDir, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; +import { createCasStore } from "@uncaged/workflow-cas"; export async function cmdCasGet( storageRoot: string, diff --git a/packages/cli-workflow/src/commands/cas/list.ts b/packages/cli-workflow/src/commands/cas/list.ts index fbfafad..cbe5e59 100644 --- a/packages/cli-workflow/src/commands/cas/list.ts +++ b/packages/cli-workflow/src/commands/cas/list.ts @@ -1,4 +1,6 @@ -import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow"; +import { ok, type Result } from "@uncaged/workflow-protocol"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; +import { createCasStore } from "@uncaged/workflow-cas"; export async function cmdCasList(storageRoot: string): Promise> { const cas = createCasStore(getGlobalCasDir(storageRoot)); diff --git a/packages/cli-workflow/src/commands/cas/put.ts b/packages/cli-workflow/src/commands/cas/put.ts index a6745e5..65fe431 100644 --- a/packages/cli-workflow/src/commands/cas/put.ts +++ b/packages/cli-workflow/src/commands/cas/put.ts @@ -1,4 +1,6 @@ -import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow"; +import { ok, type Result } from "@uncaged/workflow-protocol"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; +import { createCasStore } from "@uncaged/workflow-cas"; export async function cmdCasPut( storageRoot: string, diff --git a/packages/cli-workflow/src/commands/cas/rm.ts b/packages/cli-workflow/src/commands/cas/rm.ts index aec6980..9140e12 100644 --- a/packages/cli-workflow/src/commands/cas/rm.ts +++ b/packages/cli-workflow/src/commands/cas/rm.ts @@ -1,4 +1,6 @@ -import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow"; +import { ok, type Result } from "@uncaged/workflow-protocol"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; +import { createCasStore } from "@uncaged/workflow-cas"; export async function cmdCasRm(storageRoot: string, hash: string): Promise> { const cas = createCasStore(getGlobalCasDir(storageRoot)); diff --git a/packages/cli-workflow/src/commands/init/template.ts b/packages/cli-workflow/src/commands/init/template.ts index d788343..f3a6c38 100644 --- a/packages/cli-workflow/src/commands/init/template.ts +++ b/packages/cli-workflow/src/commands/init/template.ts @@ -1,7 +1,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { pathExists } from "../../fs-utils.js"; diff --git a/packages/cli-workflow/src/commands/init/templates.ts b/packages/cli-workflow/src/commands/init/templates.ts index 5ccebc1..65a280b 100644 --- a/packages/cli-workflow/src/commands/init/templates.ts +++ b/packages/cli-workflow/src/commands/init/templates.ts @@ -6,7 +6,6 @@ export function templatePackageJson(templateName: string): string { private: true, type: "module", dependencies: { - "@uncaged/workflow": "^0.1.0", "@uncaged/workflow-runtime": "^0.1.0", zod: "^4.0.0", }, diff --git a/packages/cli-workflow/src/commands/init/validate.ts b/packages/cli-workflow/src/commands/init/validate.ts index e39ad19..55dbdb3 100644 --- a/packages/cli-workflow/src/commands/init/validate.ts +++ b/packages/cli-workflow/src/commands/init/validate.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; /** Validates a single path segment for workspace / template names (no separators, not `.` / `..`). */ export function validateWorkspaceSegment(name: string): Result { diff --git a/packages/cli-workflow/src/commands/init/workspace.ts b/packages/cli-workflow/src/commands/init/workspace.ts index ba6acae..b05aa1b 100644 --- a/packages/cli-workflow/src/commands/init/workspace.ts +++ b/packages/cli-workflow/src/commands/init/workspace.ts @@ -1,7 +1,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { pathExists } from "../../fs-utils.js"; import type { CmdInitWorkspaceSuccess } from "./types.js"; @@ -28,7 +28,7 @@ function workflowsPackageJson(): string { private: true, type: "module", dependencies: { - "@uncaged/workflow": "^0.1.0", + "@uncaged/workflow-runtime": "^0.1.0", zod: "^4.0.0", }, }, diff --git a/packages/cli-workflow/src/commands/serve/routes-cas.ts b/packages/cli-workflow/src/commands/serve/routes-cas.ts index 6ad827a..f796a18 100644 --- a/packages/cli-workflow/src/commands/serve/routes-cas.ts +++ b/packages/cli-workflow/src/commands/serve/routes-cas.ts @@ -1,4 +1,6 @@ -import { createCasStore, garbageCollectCas, getGlobalCasDir } from "@uncaged/workflow"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; +import { createCasStore } from "@uncaged/workflow-cas"; +import { garbageCollectCas } from "@uncaged/workflow-execute"; import { Hono } from "hono"; export function createCasRoutes(storageRoot: string): Hono { diff --git a/packages/cli-workflow/src/commands/serve/routes-workflow.ts b/packages/cli-workflow/src/commands/serve/routes-workflow.ts index ea728c3..d06a39d 100644 --- a/packages/cli-workflow/src/commands/serve/routes-workflow.ts +++ b/packages/cli-workflow/src/commands/serve/routes-workflow.ts @@ -2,7 +2,7 @@ import { getRegisteredWorkflow, listRegisteredWorkflowNames, readWorkflowRegistry, -} from "@uncaged/workflow"; +} from "@uncaged/workflow-register"; import { Hono } from "hono"; export function createWorkflowRoutes(storageRoot: string): Hono { diff --git a/packages/cli-workflow/src/commands/serve/serve.ts b/packages/cli-workflow/src/commands/serve/serve.ts index 8609efb..1d894fd 100644 --- a/packages/cli-workflow/src/commands/serve/serve.ts +++ b/packages/cli-workflow/src/commands/serve/serve.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { serve } from "bun"; import { printCliLine } from "../../cli-output.js"; diff --git a/packages/cli-workflow/src/commands/thread/control.ts b/packages/cli-workflow/src/commands/thread/control.ts index 3aa85b8..feba302 100644 --- a/packages/cli-workflow/src/commands/thread/control.ts +++ b/packages/cli-workflow/src/commands/thread/control.ts @@ -1,4 +1,4 @@ -import type { Result } from "@uncaged/workflow"; +import type { Result } from "@uncaged/workflow-protocol"; import { readWorkerCtl, diff --git a/packages/cli-workflow/src/commands/thread/fork-argv.ts b/packages/cli-workflow/src/commands/thread/fork-argv.ts index f85b2a1..a3bf6f7 100644 --- a/packages/cli-workflow/src/commands/thread/fork-argv.ts +++ b/packages/cli-workflow/src/commands/thread/fork-argv.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import type { ParsedForkArgv } from "./types.js"; diff --git a/packages/cli-workflow/src/commands/thread/fork.ts b/packages/cli-workflow/src/commands/thread/fork.ts index 8003d66..0d8748c 100644 --- a/packages/cli-workflow/src/commands/thread/fork.ts +++ b/packages/cli-workflow/src/commands/thread/fork.ts @@ -1,6 +1,8 @@ import { join } from "node:path"; -import { buildForkPlan, err, generateUlid, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; +import { generateUlid } from "@uncaged/workflow-util"; +import { buildForkPlan } from "@uncaged/workflow-execute"; import { pathExists, readTextFileIfExists } from "../../fs-utils.js"; import { resolveThreadDataPath } from "../../thread-scan.js"; diff --git a/packages/cli-workflow/src/commands/thread/list.ts b/packages/cli-workflow/src/commands/thread/list.ts index 214d532..8816210 100644 --- a/packages/cli-workflow/src/commands/thread/list.ts +++ b/packages/cli-workflow/src/commands/thread/list.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { listHistoricalThreads } from "../../thread-scan.js"; import { validateCliWorkflowName } from "../../workflow-name.js"; diff --git a/packages/cli-workflow/src/commands/thread/live.ts b/packages/cli-workflow/src/commands/thread/live.ts index bb9b684..36ca209 100644 --- a/packages/cli-workflow/src/commands/thread/live.ts +++ b/packages/cli-workflow/src/commands/thread/live.ts @@ -2,15 +2,10 @@ import { watch } from "node:fs"; import { readFile } from "node:fs/promises"; import { dirname, join } from "node:path"; -import { - type CasStore, - createCasStore, - getContentMerklePayload, - getGlobalCasDir, - tryParseRoleStepRecord, - tryParseWorkflowResultRecord, -} from "@uncaged/workflow"; -import type { WorkflowCompletion } from "@uncaged/workflow-runtime"; +import type { CasStore, WorkflowCompletion } from "@uncaged/workflow-protocol"; +import { getGlobalCasDir } from "@uncaged/workflow-util"; +import { createCasStore, getContentMerklePayload } from "@uncaged/workflow-cas"; +import { tryParseRoleStepRecord, tryParseWorkflowResultRecord } from "@uncaged/workflow-execute"; import { dimGreyLine, highlightLiveRole } from "../../cli-color.js"; import { printCliError, printCliLine } from "../../cli-output.js"; diff --git a/packages/cli-workflow/src/commands/thread/rm.ts b/packages/cli-workflow/src/commands/thread/rm.ts index 2e88e12..3cf94e1 100644 --- a/packages/cli-workflow/src/commands/thread/rm.ts +++ b/packages/cli-workflow/src/commands/thread/rm.ts @@ -1,7 +1,8 @@ import { unlink } from "node:fs/promises"; import { dirname, join } from "node:path"; -import { err, garbageCollectCas, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; +import { garbageCollectCas } from "@uncaged/workflow-execute"; import { resolveThreadDataPath } from "../../thread-scan.js"; diff --git a/packages/cli-workflow/src/commands/thread/run.ts b/packages/cli-workflow/src/commands/thread/run.ts index 7af87ce..4008833 100644 --- a/packages/cli-workflow/src/commands/thread/run.ts +++ b/packages/cli-workflow/src/commands/thread/run.ts @@ -1,13 +1,8 @@ import { join } from "node:path"; -import { - err, - generateUlid, - getRegisteredWorkflow, - ok, - type Result, - readWorkflowRegistry, -} from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; +import { generateUlid } from "@uncaged/workflow-util"; +import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register"; import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js"; import { validateCliWorkflowName } from "../../workflow-name.js"; diff --git a/packages/cli-workflow/src/commands/thread/show.ts b/packages/cli-workflow/src/commands/thread/show.ts index 460ca5f..3669674 100644 --- a/packages/cli-workflow/src/commands/thread/show.ts +++ b/packages/cli-workflow/src/commands/thread/show.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { readTextFileIfExists } from "../../fs-utils.js"; import { resolveThreadDataPath } from "../../thread-scan.js"; diff --git a/packages/cli-workflow/src/commands/workflow/add-argv.ts b/packages/cli-workflow/src/commands/workflow/add-argv.ts index d6009f2..91167c0 100644 --- a/packages/cli-workflow/src/commands/workflow/add-argv.ts +++ b/packages/cli-workflow/src/commands/workflow/add-argv.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import type { ParsedAddArgv } from "./types.js"; diff --git a/packages/cli-workflow/src/commands/workflow/add.ts b/packages/cli-workflow/src/commands/workflow/add.ts index bd736cf..2b866d4 100644 --- a/packages/cli-workflow/src/commands/workflow/add.ts +++ b/packages/cli-workflow/src/commands/workflow/add.ts @@ -1,18 +1,16 @@ import { readFile, stat } from "node:fs/promises"; import { basename, resolve } from "node:path"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; +import { hashWorkflowBundleBytes } from "@uncaged/workflow-cas"; import { - err, extractBundleExports, - hashWorkflowBundleBytes, - ok, - type Result, readWorkflowRegistry, registerWorkflowVersion, stringifyWorkflowDescriptor, validateWorkflowBundle, writeWorkflowRegistry, -} from "@uncaged/workflow"; +} from "@uncaged/workflow-register"; import { storeWorkflowBundleArtifacts } from "../../bundle-store.js"; import { validateCliWorkflowName } from "../../workflow-name.js"; diff --git a/packages/cli-workflow/src/commands/workflow/history.ts b/packages/cli-workflow/src/commands/workflow/history.ts index ecf41d1..9d94fde 100644 --- a/packages/cli-workflow/src/commands/workflow/history.ts +++ b/packages/cli-workflow/src/commands/workflow/history.ts @@ -1,10 +1,5 @@ -import { - err, - getRegisteredWorkflow, - ok, - type Result, - readWorkflowRegistry, -} from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; +import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register"; import { validateCliWorkflowName } from "../../workflow-name.js"; diff --git a/packages/cli-workflow/src/commands/workflow/list.ts b/packages/cli-workflow/src/commands/workflow/list.ts index 82d60d5..7c1a21c 100644 --- a/packages/cli-workflow/src/commands/workflow/list.ts +++ b/packages/cli-workflow/src/commands/workflow/list.ts @@ -1,11 +1,9 @@ +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { - err, listRegisteredWorkflowNames, - ok, - type Result, readWorkflowRegistry, type WorkflowRegistryFile, -} from "@uncaged/workflow"; +} from "@uncaged/workflow-register"; export async function cmdList(storageRoot: string): Promise> { const reg = await readWorkflowRegistry(storageRoot); diff --git a/packages/cli-workflow/src/commands/workflow/rm.ts b/packages/cli-workflow/src/commands/workflow/rm.ts index 0a1a7cf..5906bc4 100644 --- a/packages/cli-workflow/src/commands/workflow/rm.ts +++ b/packages/cli-workflow/src/commands/workflow/rm.ts @@ -1,11 +1,9 @@ +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { - err, - ok, - type Result, readWorkflowRegistry, unregisterWorkflow, writeWorkflowRegistry, -} from "@uncaged/workflow"; +} from "@uncaged/workflow-register"; import { validateCliWorkflowName } from "../../workflow-name.js"; diff --git a/packages/cli-workflow/src/commands/workflow/rollback.ts b/packages/cli-workflow/src/commands/workflow/rollback.ts index 77d4852..dd9c6ea 100644 --- a/packages/cli-workflow/src/commands/workflow/rollback.ts +++ b/packages/cli-workflow/src/commands/workflow/rollback.ts @@ -1,14 +1,12 @@ import { join } from "node:path"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { - err, getRegisteredWorkflow, - ok, - type Result, readWorkflowRegistry, rollbackWorkflowToHistoryHash, writeWorkflowRegistry, -} from "@uncaged/workflow"; +} from "@uncaged/workflow-register"; import { pathExists } from "../../fs-utils.js"; import { validateCliWorkflowName } from "../../workflow-name.js"; diff --git a/packages/cli-workflow/src/commands/workflow/show.ts b/packages/cli-workflow/src/commands/workflow/show.ts index 5b4543a..128335f 100644 --- a/packages/cli-workflow/src/commands/workflow/show.ts +++ b/packages/cli-workflow/src/commands/workflow/show.ts @@ -1,11 +1,9 @@ +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import { - err, getRegisteredWorkflow, - ok, - type Result, readWorkflowRegistry, type WorkflowRegistryEntry, -} from "@uncaged/workflow"; +} from "@uncaged/workflow-register"; import { stringify } from "yaml"; import { validateCliWorkflowName } from "../../workflow-name.js"; diff --git a/packages/cli-workflow/src/live-argv.ts b/packages/cli-workflow/src/live-argv.ts index 9119acc..78735db 100644 --- a/packages/cli-workflow/src/live-argv.ts +++ b/packages/cli-workflow/src/live-argv.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; export type ParsedLiveArgv = { threadId: string | null; diff --git a/packages/cli-workflow/src/run-argv.ts b/packages/cli-workflow/src/run-argv.ts index 1107cfe..11af542 100644 --- a/packages/cli-workflow/src/run-argv.ts +++ b/packages/cli-workflow/src/run-argv.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; export type ParsedRunArgv = { name: string; diff --git a/packages/cli-workflow/src/storage-env.ts b/packages/cli-workflow/src/storage-env.ts index 08d39ea..100a0d6 100644 --- a/packages/cli-workflow/src/storage-env.ts +++ b/packages/cli-workflow/src/storage-env.ts @@ -1,4 +1,4 @@ -import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow"; +import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util"; /** * Resolve storage root with env var override support. diff --git a/packages/cli-workflow/src/worker-spawn.ts b/packages/cli-workflow/src/worker-spawn.ts index 35456e0..b8c6836 100644 --- a/packages/cli-workflow/src/worker-spawn.ts +++ b/packages/cli-workflow/src/worker-spawn.ts @@ -3,7 +3,8 @@ import { mkdir, readdir, unlink, writeFile } from "node:fs/promises"; import { createConnection } from "node:net"; import { join } from "node:path"; -import { err, getWorkerHostScriptPath, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; +import { getWorkerHostScriptPath } from "@uncaged/workflow-execute"; import { pathExists, readTextFileIfExists } from "./fs-utils.js"; diff --git a/packages/cli-workflow/src/workflow-name.ts b/packages/cli-workflow/src/workflow-name.ts index ae27666..2e0c4f4 100644 --- a/packages/cli-workflow/src/workflow-name.ts +++ b/packages/cli-workflow/src/workflow-name.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "@uncaged/workflow"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; const WORKFLOW_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/; diff --git a/packages/cli-workflow/tsconfig.json b/packages/cli-workflow/tsconfig.json index 764b246..9ca68cf 100644 --- a/packages/cli-workflow/tsconfig.json +++ b/packages/cli-workflow/tsconfig.json @@ -17,6 +17,13 @@ "rootDir": "src", "types": ["bun-types"] }, - "references": [{ "path": "../workflow-runtime" }, { "path": "../workflow" }], + "references": [ + { "path": "../workflow-runtime" }, + { "path": "../workflow-protocol" }, + { "path": "../workflow-util" }, + { "path": "../workflow-cas" }, + { "path": "../workflow-execute" }, + { "path": "../workflow-register" } + ], "include": ["src/**/*.ts"] } diff --git a/packages/workflow-agent-cursor/tsconfig.json b/packages/workflow-agent-cursor/tsconfig.json index ed217ff..d9141ff 100644 --- a/packages/workflow-agent-cursor/tsconfig.json +++ b/packages/workflow-agent-cursor/tsconfig.json @@ -6,5 +6,5 @@ "composite": true }, "include": ["src/**/*.ts"], - "references": [{ "path": "../workflow" }, { "path": "../workflow-util-agent" }] + "references": [{ "path": "../workflow-runtime" }, { "path": "../workflow-util-agent" }] } diff --git a/packages/workflow-agent-hermes/tsconfig.json b/packages/workflow-agent-hermes/tsconfig.json index ed217ff..d9141ff 100644 --- a/packages/workflow-agent-hermes/tsconfig.json +++ b/packages/workflow-agent-hermes/tsconfig.json @@ -6,5 +6,5 @@ "composite": true }, "include": ["src/**/*.ts"], - "references": [{ "path": "../workflow" }, { "path": "../workflow-util-agent" }] + "references": [{ "path": "../workflow-runtime" }, { "path": "../workflow-util-agent" }] } diff --git a/packages/workflow-agent-llm/package.json b/packages/workflow-agent-llm/package.json index 3a791b1..f9162e8 100644 --- a/packages/workflow-agent-llm/package.json +++ b/packages/workflow-agent-llm/package.json @@ -8,7 +8,6 @@ "test": "bun test" }, "dependencies": { - "@uncaged/workflow": "workspace:*", "@uncaged/workflow-runtime": "workspace:*" } } diff --git a/packages/workflow-agent-llm/tsconfig.json b/packages/workflow-agent-llm/tsconfig.json index 2816fef..1187cda 100644 --- a/packages/workflow-agent-llm/tsconfig.json +++ b/packages/workflow-agent-llm/tsconfig.json @@ -6,5 +6,5 @@ "composite": true }, "include": ["src/**/*.ts"], - "references": [{ "path": "../workflow" }] + "references": [{ "path": "../workflow-runtime" }] } diff --git a/packages/workflow-cas/package.json b/packages/workflow-cas/package.json new file mode 100644 index 0000000..158ebd3 --- /dev/null +++ b/packages/workflow-cas/package.json @@ -0,0 +1,20 @@ +{ + "name": "@uncaged/workflow-cas", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.ts" + } + }, + "dependencies": { + "@uncaged/workflow-protocol": "workspace:*", + "@uncaged/workflow-util": "workspace:*", + "xxhashjs": "^0.2.2", + "yaml": "^2.7.1" + }, + "devDependencies": { + "@types/bun": "latest" + } +} diff --git a/packages/workflow/src/cas/cas.ts b/packages/workflow-cas/src/cas.ts similarity index 100% rename from packages/workflow/src/cas/cas.ts rename to packages/workflow-cas/src/cas.ts diff --git a/packages/workflow/src/cas/hash.ts b/packages/workflow-cas/src/hash.ts similarity index 92% rename from packages/workflow/src/cas/hash.ts rename to packages/workflow-cas/src/hash.ts index edf7207..9d1dc98 100644 --- a/packages/workflow/src/cas/hash.ts +++ b/packages/workflow-cas/src/hash.ts @@ -2,7 +2,7 @@ import { Buffer } from "node:buffer"; import XXH from "xxhashjs"; -import { encodeUint64AsCrockford } from "../util/index.js"; +import { encodeUint64AsCrockford } from "@uncaged/workflow-util"; function digestToUint64(digest: { toString(radix?: number): string }): bigint { const hex = digest.toString(16).padStart(16, "0"); diff --git a/packages/workflow/src/cas/index.ts b/packages/workflow-cas/src/index.ts similarity index 100% rename from packages/workflow/src/cas/index.ts rename to packages/workflow-cas/src/index.ts diff --git a/packages/workflow/src/cas/merkle.ts b/packages/workflow-cas/src/merkle.ts similarity index 100% rename from packages/workflow/src/cas/merkle.ts rename to packages/workflow-cas/src/merkle.ts diff --git a/packages/workflow/src/cas/types.ts b/packages/workflow-cas/src/types.ts similarity index 87% rename from packages/workflow/src/cas/types.ts rename to packages/workflow-cas/src/types.ts index 35e150a..fd706cf 100644 --- a/packages/workflow/src/cas/types.ts +++ b/packages/workflow-cas/src/types.ts @@ -1,4 +1,4 @@ -export type { CasStore } from "@uncaged/workflow-runtime"; +export type { CasStore } from "@uncaged/workflow-protocol"; export type MerkleNodeType = "content" | "step" | "thread"; diff --git a/packages/workflow-cas/tsconfig.json b/packages/workflow-cas/tsconfig.json new file mode 100644 index 0000000..0e1e428 --- /dev/null +++ b/packages/workflow-cas/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"], + "references": [ + { "path": "../workflow-protocol" }, + { "path": "../workflow-util" } + ] +} diff --git a/packages/workflow-execute/package.json b/packages/workflow-execute/package.json new file mode 100644 index 0000000..026e574 --- /dev/null +++ b/packages/workflow-execute/package.json @@ -0,0 +1,29 @@ +{ + "name": "@uncaged/workflow-execute", + "version": "0.2.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.ts" + } + }, + "scripts": { + "test": "bun test" + }, + "dependencies": { + "@uncaged/workflow-protocol": "workspace:*", + "@uncaged/workflow-runtime": "workspace:*", + "@uncaged/workflow-util": "workspace:*", + "@uncaged/workflow-cas": "workspace:*", + "@uncaged/workflow-reactor": "workspace:*", + "@uncaged/workflow-register": "workspace:*", + "yaml": "^2.7.1" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "devDependencies": { + "zod": "^4.0.0" + } +} diff --git a/packages/workflow/src/engine/create-workflow.ts b/packages/workflow-execute/src/engine/create-workflow.ts similarity index 100% rename from packages/workflow/src/engine/create-workflow.ts rename to packages/workflow-execute/src/engine/create-workflow.ts diff --git a/packages/workflow/src/engine/engine.ts b/packages/workflow-execute/src/engine/engine.ts similarity index 98% rename from packages/workflow/src/engine/engine.ts rename to packages/workflow-execute/src/engine/engine.ts index 0cf4154..16eafc4 100644 --- a/packages/workflow/src/engine/engine.ts +++ b/packages/workflow-execute/src/engine/engine.ts @@ -15,11 +15,11 @@ import { getContentMerklePayload, putStepMerkleNode, putThreadMerkleNode, -} from "../cas/index.js"; -import { resolveModel } from "../config/index.js"; +} from "@uncaged/workflow-cas"; +import { resolveModel } from "@uncaged/workflow-register"; import { createExtract } from "../extract/index.js"; -import { readWorkflowRegistry, type WorkflowConfig } from "../registry/index.js"; -import { err, type LogFn, normalizeRefsField, ok, type Result } from "../util/index.js"; +import { readWorkflowRegistry, type WorkflowConfig } from "@uncaged/workflow-register"; +import { err, type LogFn, normalizeRefsField, ok, type Result } from "@uncaged/workflow-util"; import { runSupervisor } from "./supervisor.js"; import type { ExecuteThreadIo, ExecuteThreadOptions } from "./types.js"; diff --git a/packages/workflow/src/engine/fork-thread.ts b/packages/workflow-execute/src/engine/fork-thread.ts similarity index 98% rename from packages/workflow/src/engine/fork-thread.ts rename to packages/workflow-execute/src/engine/fork-thread.ts index 8b52b24..b564973 100644 --- a/packages/workflow/src/engine/fork-thread.ts +++ b/packages/workflow-execute/src/engine/fork-thread.ts @@ -1,5 +1,5 @@ import type { WorkflowCompletion } from "@uncaged/workflow-runtime"; -import { err, normalizeRefsField, ok, type Result } from "../util/index.js"; +import { err, normalizeRefsField, ok, type Result } from "@uncaged/workflow-util"; import type { ForkHistoricalStep, ForkPlan, ParsedThreadStartRecord } from "./types.js"; diff --git a/packages/workflow/src/engine/gc.ts b/packages/workflow-execute/src/engine/gc.ts similarity index 95% rename from packages/workflow/src/engine/gc.ts rename to packages/workflow-execute/src/engine/gc.ts index f566688..3d1007d 100644 --- a/packages/workflow/src/engine/gc.ts +++ b/packages/workflow-execute/src/engine/gc.ts @@ -1,7 +1,7 @@ import { readdir, readFile } from "node:fs/promises"; import { join } from "node:path"; -import { type CasStore, createCasStore } from "../cas/index.js"; -import { err, getGlobalCasDir, ok, type Result } from "../util/index.js"; +import { type CasStore, createCasStore } from "@uncaged/workflow-cas"; +import { err, getGlobalCasDir, ok, type Result } from "@uncaged/workflow-util"; import { parseThreadDataJsonl } from "./fork-thread.js"; import type { GcResult } from "./types.js"; diff --git a/packages/workflow/src/engine/index.ts b/packages/workflow-execute/src/engine/index.ts similarity index 100% rename from packages/workflow/src/engine/index.ts rename to packages/workflow-execute/src/engine/index.ts diff --git a/packages/workflow/src/engine/supervisor.ts b/packages/workflow-execute/src/engine/supervisor.ts similarity index 91% rename from packages/workflow/src/engine/supervisor.ts rename to packages/workflow-execute/src/engine/supervisor.ts index cef8777..b5debe0 100644 --- a/packages/workflow/src/engine/supervisor.ts +++ b/packages/workflow-execute/src/engine/supervisor.ts @@ -1,10 +1,10 @@ import * as z from "zod/v4"; -import { resolveModel } from "../config/index.js"; +import { resolveModel } from "@uncaged/workflow-register"; import { extractFunctionToolFromZodSchema } from "../extract/index.js"; -import { createLlmFn, createThreadReactor } from "../reactor/index.js"; -import type { WorkflowConfig } from "../registry/index.js"; -import { err, type LogFn, ok, type Result } from "../util/index.js"; +import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor"; +import type { WorkflowConfig } from "@uncaged/workflow-register"; +import { err, type LogFn, ok, type Result } from "@uncaged/workflow-util"; import type { SupervisorDecision } from "./types.js"; diff --git a/packages/workflow/src/engine/thread-pause-gate.ts b/packages/workflow-execute/src/engine/thread-pause-gate.ts similarity index 95% rename from packages/workflow/src/engine/thread-pause-gate.ts rename to packages/workflow-execute/src/engine/thread-pause-gate.ts index 6a14aef..3927a27 100644 --- a/packages/workflow/src/engine/thread-pause-gate.ts +++ b/packages/workflow-execute/src/engine/thread-pause-gate.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "../util/index.js"; +import { err, ok, type Result } from "@uncaged/workflow-util"; import type { ThreadPauseGate } from "./types.js"; diff --git a/packages/workflow/src/engine/types.ts b/packages/workflow-execute/src/engine/types.ts similarity index 95% rename from packages/workflow/src/engine/types.ts rename to packages/workflow-execute/src/engine/types.ts index 8788096..32f7b54 100644 --- a/packages/workflow/src/engine/types.ts +++ b/packages/workflow-execute/src/engine/types.ts @@ -1,6 +1,6 @@ import type { RoleOutput } from "@uncaged/workflow-runtime"; -import type { CasStore } from "../cas/index.js"; -import type { Result } from "../util/index.js"; +import type { CasStore } from "@uncaged/workflow-cas"; +import type { Result } from "@uncaged/workflow-util"; export type SupervisorDecision = "continue" | "stop"; diff --git a/packages/workflow/src/engine/worker-entry-path.ts b/packages/workflow-execute/src/engine/worker-entry-path.ts similarity index 100% rename from packages/workflow/src/engine/worker-entry-path.ts rename to packages/workflow-execute/src/engine/worker-entry-path.ts diff --git a/packages/workflow/src/engine/worker.ts b/packages/workflow-execute/src/engine/worker.ts similarity index 99% rename from packages/workflow/src/engine/worker.ts rename to packages/workflow-execute/src/engine/worker.ts index e83ab7d..6c85901 100644 --- a/packages/workflow/src/engine/worker.ts +++ b/packages/workflow-execute/src/engine/worker.ts @@ -2,8 +2,8 @@ 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 { ensureUncagedWorkflowSymlink, importWorkflowBundleModule } from "@uncaged/workflow-register"; +import { createCasStore } from "@uncaged/workflow-cas"; import { createLogger, err, @@ -11,7 +11,7 @@ import { normalizeRefsField, ok, type Result, -} from "../util/index.js"; +} from "@uncaged/workflow-util"; import { executeThread } from "./engine.js"; import { createThreadPauseGate } from "./thread-pause-gate.js"; import type { ExecuteThreadIo, PrefilledDiskStep, ThreadPauseGate } from "./types.js"; diff --git a/packages/workflow/src/extract/extract-fn.ts b/packages/workflow-execute/src/extract/extract-fn.ts similarity index 96% rename from packages/workflow/src/extract/extract-fn.ts rename to packages/workflow-execute/src/extract/extract-fn.ts index b278562..83e2d26 100644 --- a/packages/workflow/src/extract/extract-fn.ts +++ b/packages/workflow-execute/src/extract/extract-fn.ts @@ -1,7 +1,7 @@ import type { ExtractContext, ExtractFn, LlmProvider } from "@uncaged/workflow-runtime"; import type * as z from "zod/v4"; -import { type CasStore, getContentMerklePayload } from "../cas/index.js"; -import { createLlmFn, createThreadReactor } from "../reactor/index.js"; +import { type CasStore, getContentMerklePayload } from "@uncaged/workflow-cas"; +import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor"; import { extractFunctionToolFromZodSchema } from "./llm-extract.js"; export type ExtractDeps = { diff --git a/packages/workflow/src/extract/index.ts b/packages/workflow-execute/src/extract/index.ts similarity index 100% rename from packages/workflow/src/extract/index.ts rename to packages/workflow-execute/src/extract/index.ts diff --git a/packages/workflow/src/extract/llm-extract.ts b/packages/workflow-execute/src/extract/llm-extract.ts similarity index 98% rename from packages/workflow/src/extract/llm-extract.ts rename to packages/workflow-execute/src/extract/llm-extract.ts index 0e2dc50..09e0926 100644 --- a/packages/workflow/src/extract/llm-extract.ts +++ b/packages/workflow-execute/src/extract/llm-extract.ts @@ -1,6 +1,6 @@ import * as z from "zod/v4"; -import { err, ok, type Result } from "../util/index.js"; +import { err, ok, type Result } from "@uncaged/workflow-util"; import type { LlmError, LlmExtractArgs } from "./types.js"; diff --git a/packages/workflow/src/extract/types.ts b/packages/workflow-execute/src/extract/types.ts similarity index 100% rename from packages/workflow/src/extract/types.ts rename to packages/workflow-execute/src/extract/types.ts diff --git a/packages/workflow-execute/src/index.ts b/packages/workflow-execute/src/index.ts new file mode 100644 index 0000000..2571d7e --- /dev/null +++ b/packages/workflow-execute/src/index.ts @@ -0,0 +1,35 @@ +export { createWorkflow } from "./engine/create-workflow.js"; +export { executeThread } from "./engine/engine.js"; +export { + buildForkPlan, + parseThreadDataJsonl, + selectForkHistoricalSteps, + tryParseRoleStepRecord, + tryParseWorkflowResultRecord, +} from "./engine/fork-thread.js"; +export { garbageCollectCas } from "./engine/gc.js"; +export { createThreadPauseGate } from "./engine/thread-pause-gate.js"; +export type { + ExecuteThreadIo, + ExecuteThreadOptions, + ForkHistoricalStep, + ForkPlan, + GcResult, + ParsedThreadStartRecord, + PrefilledDiskStep, + SupervisorDecision, + ThreadPauseGate, +} from "./engine/types.js"; +export { getWorkerHostScriptPath } from "./engine/worker-entry-path.js"; +export { + buildExtractUserContent, + createExtract, + type ExtractThreadContext, +} from "./extract/index.js"; +export { + extractFunctionToolFromZodSchema, + llmErrorToCause, + llmExtract, +} from "./extract/index.js"; +export type { ExtractFn, LlmError, LlmExtractArgs } from "./extract/index.js"; +export { workflowAsAgent, type WorkflowAsAgentOptions } from "./workflow-as-agent.js"; diff --git a/packages/workflow/src/workflow-as-agent.ts b/packages/workflow-execute/src/workflow-as-agent.ts similarity index 91% rename from packages/workflow/src/workflow-as-agent.ts rename to packages/workflow-execute/src/workflow-as-agent.ts index d6ac8b1..2813361 100644 --- a/packages/workflow/src/workflow-as-agent.ts +++ b/packages/workflow-execute/src/workflow-as-agent.ts @@ -1,17 +1,17 @@ import { join } from "node:path"; import type { AgentContext, AgentFn } from "@uncaged/workflow-runtime"; -import { extractBundleExports } from "./bundle/index.js"; -import { createCasStore } from "./cas/index.js"; +import { extractBundleExports } from "@uncaged/workflow-register"; +import { createCasStore } from "@uncaged/workflow-cas"; 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 { WorkflowConfig } from "@uncaged/workflow-register"; +import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register"; import { createLogger, generateUlid, getDefaultWorkflowStorageRoot, getGlobalCasDir, -} from "./util/index.js"; +} from "@uncaged/workflow-util"; const DEFAULT_WORKFLOW_AS_AGENT_MAX_DEPTH = 3; diff --git a/packages/workflow-execute/tsconfig.json b/packages/workflow-execute/tsconfig.json new file mode 100644 index 0000000..2b204f7 --- /dev/null +++ b/packages/workflow-execute/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"], + "references": [ + { "path": "../workflow-protocol" }, + { "path": "../workflow-runtime" }, + { "path": "../workflow-util" }, + { "path": "../workflow-cas" }, + { "path": "../workflow-reactor" }, + { "path": "../workflow-register" } + ] +} diff --git a/packages/workflow-protocol/package.json b/packages/workflow-protocol/package.json new file mode 100644 index 0000000..c0aebdc --- /dev/null +++ b/packages/workflow-protocol/package.json @@ -0,0 +1,18 @@ +{ + "name": "@uncaged/workflow-protocol", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.ts" + } + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "devDependencies": { + "zod": "^4.0.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/workflow-protocol/src/index.ts b/packages/workflow-protocol/src/index.ts new file mode 100644 index 0000000..5992630 --- /dev/null +++ b/packages/workflow-protocol/src/index.ts @@ -0,0 +1,40 @@ +// ── Types ────────────────────────────────────────────────────────── + +export type { + Result, + CasStore, + WorkflowRoleSchema, + WorkflowRoleDescriptor, + WorkflowDescriptor, + RoleMeta, + RoleOutput, + StartStep, + RoleStep, + ThreadContext, + ModeratorContext, + AgentContext, + ExtractContext, + WorkflowCompletion, + WorkflowResult, + LlmProvider, + ProviderConfig, + ResolvedModel, + WorkflowConfig, + ExtractFn, + AgentFn, + AgentBinding, + WorkflowRuntime, + WorkflowFn, + RoleDefinition, + Moderator, + WorkflowDefinition, + AdvanceOutcome, +} from "./types.js"; + +// ── Constants ────────────────────────────────────────────────────── + +export { START, END } from "./types.js"; + +// ── Constructor functions ────────────────────────────────────────── + +export { ok, err } from "./result.js"; diff --git a/packages/workflow-protocol/src/result.ts b/packages/workflow-protocol/src/result.ts new file mode 100644 index 0000000..f9b25c0 --- /dev/null +++ b/packages/workflow-protocol/src/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-protocol/src/types.ts b/packages/workflow-protocol/src/types.ts new file mode 100644 index 0000000..07bf57e --- /dev/null +++ b/packages/workflow-protocol/src/types.ts @@ -0,0 +1,167 @@ +import type * as z from "zod/v4"; + +// ── Constants ────────────────────────────────────────────────────── + +export const START = "__start__" as const; +export const END = "__end__" as const; + +// ── Result ───────────────────────────────────────────────────────── + +export type Result = { ok: true; value: T } | { ok: false; error: E }; + +// ── CAS ──────────────────────────────────────────────────────────── + +export type CasStore = { + put(content: string): Promise; + get(hash: string): Promise; + delete(hash: string): Promise; + list(): Promise; +}; + +// ── Workflow Descriptor ──────────────────────────────────────────── + +export type WorkflowRoleSchema = Record; + +export type WorkflowRoleDescriptor = { + description: string; + schema: WorkflowRoleSchema; +}; + +export type WorkflowDescriptor = { + description: string; + roles: Record; +}; + +// ── Role & Thread ────────────────────────────────────────────────── + +export type RoleMeta = Record>; + +export type RoleOutput = { + role: string; + contentHash: string; + meta: Record; + refs: string[]; +}; + +export type StartStep = { + role: typeof START; + content: string; + meta: { maxRounds: number }; + timestamp: number; +}; + +export type RoleStep = { + [K in keyof M & string]: { + role: K; + meta: M[K]; + contentHash: string; + refs: string[]; + timestamp: number; + }; +}[keyof M & string]; + +export type ThreadContext = { + threadId: string; + depth: number; + start: StartStep; + steps: RoleStep[]; +}; + +export type ModeratorContext = ThreadContext; + +export type AgentContext = ModeratorContext & { + currentRole: { + name: string; + systemPrompt: string; + }; +}; + +export type ExtractContext = AgentContext & { + agentContent: string; +}; + +// ── Workflow Completion ──────────────────────────────────────────── + +export type WorkflowCompletion = { + returnCode: number; + summary: string; +}; + +export type WorkflowResult = WorkflowCompletion & { + rootHash: string; +}; + +// ── LLM Provider ─────────────────────────────────────────────────── + +export type LlmProvider = { + baseUrl: string; + apiKey: string; + model: string; +}; + +export type ProviderConfig = { + baseUrl: string; + apiKey: string; +}; + +export type ResolvedModel = { + baseUrl: string; + apiKey: string; + model: string; +}; + +export type WorkflowConfig = { + maxDepth: number; + supervisorInterval: number; + providers: Record; + models: Record; +}; + +// ── Functions ────────────────────────────────────────────────────── + +export type ExtractFn = >( + schema: z.ZodType, + prompt: string, + ctx: ExtractContext, +) => Promise; + +export type AgentFn = (ctx: AgentContext) => Promise; + +export type AgentBinding = { + agent: AgentFn; + overrides: Partial> | null; +}; + +// ── Workflow Runtime & Definition ────────────────────────────────── + +export type WorkflowRuntime = { + cas: CasStore; + extract: ExtractFn; +}; + +export type WorkflowFn = ( + thread: ThreadContext, + runtime: WorkflowRuntime, +) => AsyncGenerator; + +export type RoleDefinition> = { + description: string; + systemPrompt: string; + extractPrompt: string; + schema: z.ZodType; + extractRefs: ((meta: Meta) => string[]) | null; +}; + +export type Moderator = ( + ctx: ModeratorContext, +) => (keyof M & string) | typeof END; + +export type WorkflowDefinition = { + description: string; + roles: { [K in keyof M & string]: RoleDefinition }; + moderator: Moderator; +}; + +export type AdvanceOutcome = + | { kind: "complete"; completion: WorkflowCompletion } + | { kind: "yield"; output: RoleOutput; step: RoleStep }; diff --git a/packages/workflow-protocol/tsconfig.json b/packages/workflow-protocol/tsconfig.json new file mode 100644 index 0000000..75eba9f --- /dev/null +++ b/packages/workflow-protocol/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/workflow-reactor/package.json b/packages/workflow-reactor/package.json new file mode 100644 index 0000000..2f338b7 --- /dev/null +++ b/packages/workflow-reactor/package.json @@ -0,0 +1,21 @@ +{ + "name": "@uncaged/workflow-reactor", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.ts" + } + }, + "dependencies": { + "@uncaged/workflow-protocol": "workspace:*" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "devDependencies": { + "zod": "^4.0.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/workflow/src/reactor/index.ts b/packages/workflow-reactor/src/index.ts similarity index 100% rename from packages/workflow/src/reactor/index.ts rename to packages/workflow-reactor/src/index.ts diff --git a/packages/workflow/src/reactor/llm-fn.ts b/packages/workflow-reactor/src/llm-fn.ts similarity index 92% rename from packages/workflow/src/reactor/llm-fn.ts rename to packages/workflow-reactor/src/llm-fn.ts index fd5c911..26f406d 100644 --- a/packages/workflow/src/reactor/llm-fn.ts +++ b/packages/workflow-reactor/src/llm-fn.ts @@ -1,6 +1,6 @@ -import type { LlmProvider } from "@uncaged/workflow-runtime"; +import type { LlmProvider } from "@uncaged/workflow-protocol"; -import { err, ok } from "../util/index.js"; +import { err, ok } from "@uncaged/workflow-protocol"; import type { ChatMessage, LlmFn, ToolDefinition } from "./types.js"; diff --git a/packages/workflow/src/reactor/thread-reactor.ts b/packages/workflow-reactor/src/thread-reactor.ts similarity index 99% rename from packages/workflow/src/reactor/thread-reactor.ts rename to packages/workflow-reactor/src/thread-reactor.ts index 4931bd4..ae7f291 100644 --- a/packages/workflow/src/reactor/thread-reactor.ts +++ b/packages/workflow-reactor/src/thread-reactor.ts @@ -1,6 +1,6 @@ import type * as z from "zod/v4"; -import { err, ok, type Result } from "../util/index.js"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; import type { ChatMessage, diff --git a/packages/workflow/src/reactor/types.ts b/packages/workflow-reactor/src/types.ts similarity index 96% rename from packages/workflow/src/reactor/types.ts rename to packages/workflow-reactor/src/types.ts index 5d9d499..158d179 100644 --- a/packages/workflow/src/reactor/types.ts +++ b/packages/workflow-reactor/src/types.ts @@ -1,6 +1,6 @@ import type * as z from "zod/v4"; -import type { Result } from "../util/index.js"; +import type { Result } from "@uncaged/workflow-protocol"; export type ToolCall = { id: string; diff --git a/packages/workflow-reactor/tsconfig.json b/packages/workflow-reactor/tsconfig.json new file mode 100644 index 0000000..778d958 --- /dev/null +++ b/packages/workflow-reactor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"], + "references": [ + { "path": "../workflow-protocol" } + ] +} diff --git a/packages/workflow-register/package.json b/packages/workflow-register/package.json new file mode 100644 index 0000000..6bb67fd --- /dev/null +++ b/packages/workflow-register/package.json @@ -0,0 +1,26 @@ +{ + "name": "@uncaged/workflow-register", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.ts" + } + }, + "dependencies": { + "@uncaged/workflow-protocol": "workspace:*", + "@uncaged/workflow-util": "workspace:*" + }, + "peerDependencies": { + "acorn": "^8.0.0", + "yaml": "^2.0.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "acorn": "^8.14.1", + "yaml": "^2.7.1", + "zod": "^4.0.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/workflow/src/bundle/build-descriptor.ts b/packages/workflow-register/src/bundle/build-descriptor.ts similarity index 98% rename from packages/workflow/src/bundle/build-descriptor.ts rename to packages/workflow-register/src/bundle/build-descriptor.ts index b81fd7c..2bf4a7b 100644 --- a/packages/workflow/src/bundle/build-descriptor.ts +++ b/packages/workflow-register/src/bundle/build-descriptor.ts @@ -1,4 +1,4 @@ -import type { RoleMeta, WorkflowDefinition } from "@uncaged/workflow-runtime"; +import type { RoleMeta, WorkflowDefinition } from "@uncaged/workflow-protocol"; import * as z from "zod/v4"; import type { WorkflowDescriptor, WorkflowRoleSchema } from "./types.js"; diff --git a/packages/workflow/src/bundle/bundle-import-env.ts b/packages/workflow-register/src/bundle/bundle-import-env.ts similarity index 100% rename from packages/workflow/src/bundle/bundle-import-env.ts rename to packages/workflow-register/src/bundle/bundle-import-env.ts diff --git a/packages/workflow/src/bundle/bundle-validator.ts b/packages/workflow-register/src/bundle/bundle-validator.ts similarity index 97% rename from packages/workflow/src/bundle/bundle-validator.ts rename to packages/workflow-register/src/bundle/bundle-validator.ts index b140145..8538cf0 100644 --- a/packages/workflow/src/bundle/bundle-validator.ts +++ b/packages/workflow-register/src/bundle/bundle-validator.ts @@ -12,7 +12,7 @@ import type { } from "acorn"; import * as acorn from "acorn"; -import { err, ok, type Result } from "../util/index.js"; +import { err, ok, type Result } from "@uncaged/workflow-util"; import type { WorkflowBundleValidationInput } from "./types.js"; @@ -38,7 +38,11 @@ function isAllowedImportSpecifier(spec: string): boolean { if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("file:")) { return false; } - if (spec === "@uncaged/workflow" || spec === "@uncaged/workflow-runtime") { + if ( + spec === "@uncaged/workflow" || + spec === "@uncaged/workflow-runtime" || + spec === "@uncaged/workflow-cas" + ) { return true; } return isBuiltin(spec); @@ -294,7 +298,7 @@ function validateImportDeclaration(node: ImportDeclaration): string | null { return "only static string import specifiers are allowed"; } if (!isAllowedImportSpecifier(spec)) { - return `disallowed import specifier "${spec}" (only Node built-ins and "@uncaged/workflow" are allowed)`; + return `disallowed import specifier "${spec}" (only Node built-ins and @uncaged/workflow-* packages are allowed)`; } return null; } @@ -309,7 +313,7 @@ function validateExportSource( return staticMessage; } if (!isAllowedImportSpecifier(spec)) { - return `${disallowedPrefix} "${spec}" (only Node built-ins and "@uncaged/workflow" are allowed)`; + return `${disallowedPrefix} "${spec}" (only Node built-ins and @uncaged/workflow-* packages are allowed)`; } return null; } diff --git a/packages/workflow-register/src/bundle/ensure-uncaged-workflow-symlink.ts b/packages/workflow-register/src/bundle/ensure-uncaged-workflow-symlink.ts new file mode 100644 index 0000000..cecc319 --- /dev/null +++ b/packages/workflow-register/src/bundle/ensure-uncaged-workflow-symlink.ts @@ -0,0 +1,56 @@ +import { mkdir, readlink, symlink, unlink } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** This module lives in `@uncaged/workflow-register/src/bundle`; grandparent dir is the package root. */ +function installedWorkflowPackageDir(): string { + return fileURLToPath(new URL("../..", import.meta.url)); +} + +/** + * Resolve sibling @uncaged/* package directory relative to workflow-register. + * In a monorepo workspace layout the sibling packages live next to workflow-register. + */ +function siblingPackageDir(packageName: string): string { + const registerRoot = installedWorkflowPackageDir(); + return path.resolve(registerRoot, "..", packageName); +} + +async function ensureSymlink(linkDir: string, name: string, target: string): Promise { + const linkPath = path.join(linkDir, name); + await mkdir(linkDir, { recursive: true }); + try { + const existing = await readlink(linkPath); + const normalizedExisting = path.resolve(linkDir, existing); + if (normalizedExisting === target) { + return; + } + await unlink(linkPath); + } catch (e) { + const errObj = e as NodeJS.ErrnoException; + if (errObj.code !== "ENOENT" && errObj.code !== "EINVAL") { + throw e; + } + } + const linkType = process.platform === "win32" ? "junction" : "dir"; + await symlink(target, linkPath, linkType); +} + +/** + * Ensures `/node_modules/@uncaged/*` symlinks point at installed packages + * so workflow bundles loaded from `/bundles/*.esm.js` can resolve their imports. + */ +export async function ensureUncagedWorkflowSymlink(storageRoot: string): Promise { + const linkDir = path.join(storageRoot, "node_modules", "@uncaged"); + + const packages = [ + { name: "workflow", dir: siblingPackageDir("workflow") }, + { name: "workflow-runtime", dir: siblingPackageDir("workflow-runtime") }, + { name: "workflow-cas", dir: siblingPackageDir("workflow-cas") }, + { name: "workflow-protocol", dir: siblingPackageDir("workflow-protocol") }, + ]; + + for (const pkg of packages) { + await ensureSymlink(linkDir, pkg.name, pkg.dir); + } +} diff --git a/packages/workflow/src/bundle/extract-bundle-exports.ts b/packages/workflow-register/src/bundle/extract-bundle-exports.ts similarity index 92% rename from packages/workflow/src/bundle/extract-bundle-exports.ts rename to packages/workflow-register/src/bundle/extract-bundle-exports.ts index 6184b08..8ddf99d 100644 --- a/packages/workflow/src/bundle/extract-bundle-exports.ts +++ b/packages/workflow-register/src/bundle/extract-bundle-exports.ts @@ -1,5 +1,5 @@ -import type { WorkflowFn } from "@uncaged/workflow-runtime"; -import { err, ok, type Result } from "../util/index.js"; +import type { WorkflowFn } from "@uncaged/workflow-protocol"; +import { err, ok, type Result } from "@uncaged/workflow-util"; import { importWorkflowBundleModule } from "./bundle-import-env.js"; import { ensureUncagedWorkflowSymlink } from "./ensure-uncaged-workflow-symlink.js"; import type { ExtractBundleExportsOptions, ExtractedBundleExports } from "./types.js"; diff --git a/packages/workflow/src/bundle/generate-descriptor.ts b/packages/workflow-register/src/bundle/generate-descriptor.ts similarity index 100% rename from packages/workflow/src/bundle/generate-descriptor.ts rename to packages/workflow-register/src/bundle/generate-descriptor.ts diff --git a/packages/workflow/src/bundle/index.ts b/packages/workflow-register/src/bundle/index.ts similarity index 100% rename from packages/workflow/src/bundle/index.ts rename to packages/workflow-register/src/bundle/index.ts diff --git a/packages/workflow/src/bundle/types.ts b/packages/workflow-register/src/bundle/types.ts similarity index 89% rename from packages/workflow/src/bundle/types.ts rename to packages/workflow-register/src/bundle/types.ts index 5776cdf..020967e 100644 --- a/packages/workflow/src/bundle/types.ts +++ b/packages/workflow-register/src/bundle/types.ts @@ -1,10 +1,11 @@ -import type { WorkflowDescriptor, WorkflowFn } from "@uncaged/workflow-runtime"; +import type { WorkflowDescriptor, WorkflowFn } from "@uncaged/workflow-protocol"; export type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema, -} from "@uncaged/workflow-runtime"; + WorkflowFn, +} from "@uncaged/workflow-protocol"; 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-register/src/bundle/workflow-descriptor.ts similarity index 96% rename from packages/workflow/src/bundle/workflow-descriptor.ts rename to packages/workflow-register/src/bundle/workflow-descriptor.ts index e21851b..a1941e1 100644 --- a/packages/workflow/src/bundle/workflow-descriptor.ts +++ b/packages/workflow-register/src/bundle/workflow-descriptor.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "../util/index.js"; +import { err, ok, type Result } from "@uncaged/workflow-util"; import type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js"; diff --git a/packages/workflow/src/config/index.ts b/packages/workflow-register/src/config/index.ts similarity index 100% rename from packages/workflow/src/config/index.ts rename to packages/workflow-register/src/config/index.ts diff --git a/packages/workflow/src/config/resolve-model.ts b/packages/workflow-register/src/config/resolve-model.ts similarity index 88% rename from packages/workflow/src/config/resolve-model.ts rename to packages/workflow-register/src/config/resolve-model.ts index e766b44..e9d0e5d 100644 --- a/packages/workflow/src/config/resolve-model.ts +++ b/packages/workflow-register/src/config/resolve-model.ts @@ -1,5 +1,5 @@ -import type { WorkflowConfig } from "../registry/index.js"; -import { err, ok, type Result } from "../util/index.js"; +import type { WorkflowConfig } from "@uncaged/workflow-protocol"; +import { err, ok, type Result } from "@uncaged/workflow-util"; import { splitProviderModelRef } from "./split-provider-model-ref.js"; import type { ResolvedModel } from "./types.js"; diff --git a/packages/workflow/src/config/split-provider-model-ref.ts b/packages/workflow-register/src/config/split-provider-model-ref.ts similarity index 91% rename from packages/workflow/src/config/split-provider-model-ref.ts rename to packages/workflow-register/src/config/split-provider-model-ref.ts index 0002920..8fa52ce 100644 --- a/packages/workflow/src/config/split-provider-model-ref.ts +++ b/packages/workflow-register/src/config/split-provider-model-ref.ts @@ -1,4 +1,4 @@ -import { err, ok, type Result } from "../util/index.js"; +import { err, ok, type Result } from "@uncaged/workflow-util"; /** Parses `providerName/modelName` references used in {@link WorkflowConfig.models}. */ export function splitProviderModelRef( diff --git a/packages/workflow-register/src/config/types.ts b/packages/workflow-register/src/config/types.ts new file mode 100644 index 0000000..ca533e4 --- /dev/null +++ b/packages/workflow-register/src/config/types.ts @@ -0,0 +1 @@ +export type { ProviderConfig, ResolvedModel } from "@uncaged/workflow-protocol"; diff --git a/packages/workflow-register/src/index.ts b/packages/workflow-register/src/index.ts new file mode 100644 index 0000000..7bf4be7 --- /dev/null +++ b/packages/workflow-register/src/index.ts @@ -0,0 +1,39 @@ +export { + buildDescriptor, + importWorkflowBundleModule, + validateWorkflowBundle, + ensureUncagedWorkflowSymlink, + extractBundleExports, + stringifyWorkflowDescriptor, + validateWorkflowDescriptor, +} from "./bundle/index.js"; +export type { + ExtractBundleExportsOptions, + ExtractedBundleExports, + WorkflowBundleValidationInput, + WorkflowDescriptor, + WorkflowRoleDescriptor, + WorkflowRoleSchema, +} from "./bundle/index.js"; + +export { + getRegisteredWorkflow, + listRegisteredWorkflowNames, + parseWorkflowRegistryYaml, + readWorkflowRegistry, + registerWorkflowVersion, + rollbackWorkflowToHistoryHash, + stringifyWorkflowRegistryYaml, + unregisterWorkflow, + workflowRegistryPath, + writeWorkflowRegistry, +} from "./registry/index.js"; +export type { + WorkflowConfig, + WorkflowHistoryEntry, + WorkflowRegistryEntry, + WorkflowRegistryFile, +} from "./registry/index.js"; + +export { resolveModel, splitProviderModelRef } from "./config/index.js"; +export type { ProviderConfig, ResolvedModel } from "./config/index.js"; diff --git a/packages/workflow/src/registry/index.ts b/packages/workflow-register/src/registry/index.ts similarity index 100% rename from packages/workflow/src/registry/index.ts rename to packages/workflow-register/src/registry/index.ts diff --git a/packages/workflow/src/registry/registry-normalize.ts b/packages/workflow-register/src/registry/registry-normalize.ts similarity index 97% rename from packages/workflow/src/registry/registry-normalize.ts rename to packages/workflow-register/src/registry/registry-normalize.ts index 5f493fc..7d0baa4 100644 --- a/packages/workflow/src/registry/registry-normalize.ts +++ b/packages/workflow-register/src/registry/registry-normalize.ts @@ -1,5 +1,6 @@ -import { type ProviderConfig, splitProviderModelRef } from "../config/index.js"; -import { createLogger, err, ok, type Result } from "../util/index.js"; +import type { ProviderConfig } from "@uncaged/workflow-protocol"; +import { splitProviderModelRef } from "../config/index.js"; +import { createLogger, err, ok, type Result } from "@uncaged/workflow-util"; import type { WorkflowConfig, WorkflowHistoryEntry, diff --git a/packages/workflow/src/registry/registry.ts b/packages/workflow-register/src/registry/registry.ts similarity index 98% rename from packages/workflow/src/registry/registry.ts rename to packages/workflow-register/src/registry/registry.ts index 947bc4f..0dc04a2 100644 --- a/packages/workflow/src/registry/registry.ts +++ b/packages/workflow-register/src/registry/registry.ts @@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { parseDocument, stringify } from "yaml"; -import { err, ok, type Result } from "../util/index.js"; +import { err, ok, type Result } from "@uncaged/workflow-util"; import { normalizeWorkflowRegistryRoot } from "./registry-normalize.js"; import type { WorkflowHistoryEntry, WorkflowRegistryEntry, WorkflowRegistryFile } from "./types.js"; diff --git a/packages/workflow/src/registry/types.ts b/packages/workflow-register/src/registry/types.ts similarity index 50% rename from packages/workflow/src/registry/types.ts rename to packages/workflow-register/src/registry/types.ts index 8cba10f..e314d60 100644 --- a/packages/workflow/src/registry/types.ts +++ b/packages/workflow-register/src/registry/types.ts @@ -1,4 +1,6 @@ -import type { ProviderConfig } from "../config/index.js"; +import type { WorkflowConfig } from "@uncaged/workflow-protocol"; + +export type { WorkflowConfig } from "@uncaged/workflow-protocol"; export type WorkflowHistoryEntry = { hash: string; @@ -11,14 +13,6 @@ export type WorkflowRegistryEntry = { history: WorkflowHistoryEntry[]; }; -export type WorkflowConfig = { - maxDepth: number; - /** Run supervisor LLM every N completed role rounds (0 = disabled). Default from YAML: 3. */ - supervisorInterval: number; - providers: Record; - models: Record; -}; - export type WorkflowRegistryFile = { config: WorkflowConfig | null; workflows: Record; diff --git a/packages/workflow/tsconfig.json b/packages/workflow-register/tsconfig.json similarity index 78% rename from packages/workflow/tsconfig.json rename to packages/workflow-register/tsconfig.json index 72ffea1..7ec8f89 100644 --- a/packages/workflow/tsconfig.json +++ b/packages/workflow-register/tsconfig.json @@ -1,5 +1,8 @@ { - "references": [{ "path": "../workflow-runtime" }], + "references": [ + { "path": "../workflow-protocol" }, + { "path": "../workflow-util" } + ], "compilerOptions": { "target": "ES2022", "lib": ["ES2022"], @@ -18,5 +21,5 @@ "rootDir": "src", "types": ["bun-types"] }, - "include": ["src/**/*.ts", "xxhashjs.d.ts"] + "include": ["src/**/*.ts"] } diff --git a/packages/workflow-runtime/package.json b/packages/workflow-runtime/package.json index d3d5f70..37833b9 100644 --- a/packages/workflow-runtime/package.json +++ b/packages/workflow-runtime/package.json @@ -7,6 +7,9 @@ "scripts": { "test": "bun test" }, + "dependencies": { + "@uncaged/workflow-protocol": "workspace:*" + }, "peerDependencies": { "zod": "^4.0.0" }, diff --git a/packages/workflow-runtime/src/create-workflow.ts b/packages/workflow-runtime/src/create-workflow.ts index a899edd..3ef9fa3 100644 --- a/packages/workflow-runtime/src/create-workflow.ts +++ b/packages/workflow-runtime/src/create-workflow.ts @@ -1,6 +1,7 @@ import type * as z from "zod/v4"; import { + type AdvanceOutcome, type AgentBinding, type AgentContext, type AgentFn, @@ -48,10 +49,6 @@ function agentForRole(binding: AgentBinding, roleName: string): AgentFn { 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, diff --git a/packages/workflow-runtime/src/index.ts b/packages/workflow-runtime/src/index.ts index c1f5c8b..37366ad 100644 --- a/packages/workflow-runtime/src/index.ts +++ b/packages/workflow-runtime/src/index.ts @@ -1,29 +1,29 @@ export { createWorkflow } from "./create-workflow.js"; export { err, ok } from "./result.js"; export type { - AgentBinding, - AgentContext, - AgentFn, - CasStore, - ExtractContext, - ExtractFn, - LlmProvider, - Moderator, - ModeratorContext, - Result, - RoleDefinition, - RoleMeta, - RoleOutput, - RoleStep, - StartStep, - ThreadContext, - WorkflowCompletion, - WorkflowDefinition, - WorkflowDescriptor, - WorkflowFn, - WorkflowResult, - WorkflowRoleDescriptor, - WorkflowRoleSchema, - WorkflowRuntime, + AgentBinding, + AgentContext, + AgentFn, + CasStore, + ExtractContext, + ExtractFn, + LlmProvider, + Moderator, + ModeratorContext, + Result, + RoleDefinition, + RoleMeta, + RoleOutput, + RoleStep, + StartStep, + ThreadContext, + WorkflowCompletion, + WorkflowDefinition, + WorkflowDescriptor, + WorkflowFn, + WorkflowResult, + WorkflowRoleDescriptor, + WorkflowRoleSchema, + WorkflowRuntime, } from "./types.js"; export { END, START } from "./types.js"; diff --git a/packages/workflow-runtime/src/result.ts b/packages/workflow-runtime/src/result.ts index 6f92f67..0c2f52f 100644 --- a/packages/workflow-runtime/src/result.ts +++ b/packages/workflow-runtime/src/result.ts @@ -1,9 +1,2 @@ -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 }; -} +// Re-export from protocol for backward compatibility. +export { err, ok } from "@uncaged/workflow-protocol"; diff --git a/packages/workflow-runtime/src/types.ts b/packages/workflow-runtime/src/types.ts index c6f3f2c..e4d5e53 100644 --- a/packages/workflow-runtime/src/types.ts +++ b/packages/workflow-runtime/src/types.ts @@ -1,165 +1,33 @@ -import type * as z from "zod/v4"; +// Re-export all types from the protocol package. +// This file exists for backward compatibility — downstream code that +// imports from "@uncaged/workflow-runtime" continues to work. -/** Sentinel values for automaton control flow. */ -export const START = "__start__" as const; -export const END = "__end__" as const; +export type { + AgentBinding, + AgentContext, + AgentFn, + AdvanceOutcome, + CasStore, + ExtractContext, + ExtractFn, + LlmProvider, + Moderator, + ModeratorContext, + Result, + RoleDefinition, + RoleMeta, + RoleOutput, + RoleStep, + StartStep, + ThreadContext, + WorkflowCompletion, + WorkflowDefinition, + WorkflowDescriptor, + WorkflowFn, + WorkflowResult, + WorkflowRoleDescriptor, + WorkflowRoleSchema, + WorkflowRuntime, +} from "@uncaged/workflow-protocol"; -export type CasStore = { - put(content: string): Promise; - get(hash: string): Promise; - delete(hash: string): Promise; - list(): Promise; -}; - -/** 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; -}; - -/** Expected success/failure outcome without throwing for recoverable errors. */ -export type Result = { ok: true; value: T } | { ok: false; error: E }; - -/** Maps role names → their meta types. Single generic drives all inference. */ -export type RoleMeta = Record>; - -/** OpenAI-compatible LLM endpoint used for structured meta extraction. */ -export type LlmProvider = { - baseUrl: string; - apiKey: string; - model: string; -}; - -/** What each generator yield produces — one role's output (engine adds `timestamp` when persisting). */ -export type RoleOutput = { - role: string; - /** CAS hash of the serialized Merkle content node for this step's body text. */ - contentHash: string; - meta: Record; - /** CAS hashes produced or consumed by this step (for GC traceability). */ - refs: string[]; -}; - -/** Generator completion value from a workflow bundle (`run` export). Root hash is added by the engine. */ -export type WorkflowCompletion = { - returnCode: number; - summary: string; -}; - -/** Final thread outcome from executeThread, including Merkle thread root CAS hash. */ -export type WorkflowResult = WorkflowCompletion & { - rootHash: string; -}; - -/** Runtime dependencies passed to a workflow bundle's `run` export (engine-provided). */ -export type WorkflowRuntime = { - /** Global CAS store for Merkle content blobs (role step bodies). */ - cas: CasStore; - /** Structured meta extraction; resolved from workflow.yaml `extract` scene by the engine. */ - extract: ExtractFn; -}; - -/** Bundle contract — named export `run` is a function returning an AsyncGenerator. */ -export type WorkflowFn = ( - thread: ThreadContext, - runtime: WorkflowRuntime, -) => AsyncGenerator; - -/** Engine start frame: initial prompt + thread identity. */ -export type StartStep = { - role: typeof START; - content: string; - meta: { maxRounds: number }; - timestamp: number; -}; - -/** A completed role step in the thread. */ -export type RoleStep = { - [K in keyof M & string]: { - role: K; - meta: M[K]; - contentHash: string; - refs: string[]; - timestamp: number; - }; -}[keyof M & string]; - -/** Thread runtime context shared by moderator/agent/extractor phases. */ -export type ThreadContext = { - threadId: string; - /** Nesting depth for workflow-as-agent chains; root threads use `0`. */ - depth: number; - start: StartStep; - steps: RoleStep[]; -}; - -/** Phase 1: Moderator decides next role. */ -export type ModeratorContext = ThreadContext; - -/** Phase 2: Agent executes — knows its role and prompt. */ -export type AgentContext = ModeratorContext & { - currentRole: { - name: string; - systemPrompt: string; - }; -}; - -/** Phase 3: Extractor runs — has agent output; the extraction instruction is a separate argument to the extract function. */ -export type ExtractContext = AgentContext & { - agentContent: string; -}; - -export type ExtractFn = >( - schema: z.ZodType, - prompt: string, - ctx: ExtractContext, -) => Promise; - -/** Raw string output from an LLM/CLI adapter; meta is extracted by the engine. */ -export type AgentFn = (ctx: AgentContext) => Promise; - -/** Runtime agent assignment (explicit null when no per-role overrides). */ -export type AgentBinding = { - agent: AgentFn; - overrides: Partial> | null; -}; - -/** Role wiring: prompts, schema, and human-readable description. */ -export type RoleDefinition> = { - description: string; - systemPrompt: string; - extractPrompt: string; - schema: z.ZodType; - /** When non-null, produces CAS hashes to persist on this role's steps (see `RoleOutput.refs`). */ - extractRefs: ((meta: Meta) => string[]) | null; -}; - -/** - * The Moderator — a pure routing function. - * Receives the full thread context (start + all prior steps). - * On initial call, `steps` is empty. - * Returns the next role name or END to terminate. - */ -export type Moderator = ( - ctx: ModeratorContext, -) => (keyof M & string) | typeof END; - -/** Complete workflow definition as authored by users. */ -export type WorkflowDefinition = { - description: string; - roles: { [K in keyof M & string]: RoleDefinition }; - moderator: Moderator; -}; - -/** Internal outcome of advancing one moderator round inside {@link createWorkflow}. */ -export type AdvanceOutcome = - | { kind: "complete"; completion: WorkflowCompletion } - | { kind: "yield"; output: RoleOutput; step: RoleStep }; +export { END, START } from "@uncaged/workflow-protocol"; diff --git a/packages/workflow-runtime/tsconfig.json b/packages/workflow-runtime/tsconfig.json index cfef04b..bd7eaf3 100644 --- a/packages/workflow-runtime/tsconfig.json +++ b/packages/workflow-runtime/tsconfig.json @@ -17,5 +17,8 @@ "rootDir": "src", "types": ["bun-types"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], + "references": [ + { "path": "../workflow-protocol" } + ] } diff --git a/packages/workflow-template-develop/__tests__/develop-template.test.ts b/packages/workflow-template-develop/__tests__/develop-template.test.ts index 02c37ae..8f80296 100644 --- a/packages/workflow-template-develop/__tests__/develop-template.test.ts +++ b/packages/workflow-template-develop/__tests__/develop-template.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { validateWorkflowDescriptor } from "@uncaged/workflow"; +import { validateWorkflowDescriptor } from "@uncaged/workflow-register"; import { END, type ModeratorContext, type RoleStep, START } from "@uncaged/workflow-runtime"; import { buildDevelopDescriptor } from "../src/descriptor.js"; import { developModerator } from "../src/index.js"; diff --git a/packages/workflow-template-develop/package.json b/packages/workflow-template-develop/package.json index 19e9d46..46509c1 100644 --- a/packages/workflow-template-develop/package.json +++ b/packages/workflow-template-develop/package.json @@ -2,13 +2,17 @@ "name": "@uncaged/workflow-template-develop", "version": "0.2.0", "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.ts" + } + }, "scripts": { "test": "bun test" }, "dependencies": { - "@uncaged/workflow": "workspace:*", + "@uncaged/workflow-register": "workspace:*", "@uncaged/workflow-runtime": "workspace:*", "zod": "^4.0.0" } diff --git a/packages/workflow-template-develop/src/descriptor.ts b/packages/workflow-template-develop/src/descriptor.ts index 1dc4057..f5683c6 100644 --- a/packages/workflow-template-develop/src/descriptor.ts +++ b/packages/workflow-template-develop/src/descriptor.ts @@ -1,4 +1,4 @@ -import { buildDescriptor } from "@uncaged/workflow"; +import { buildDescriptor } from "@uncaged/workflow-register"; import { developModerator } from "./moderator.js"; import { DEVELOP_WORKFLOW_DESCRIPTION, developRoles } from "./roles.js"; diff --git a/packages/workflow-template-develop/tsconfig.json b/packages/workflow-template-develop/tsconfig.json index 2816fef..af22af0 100644 --- a/packages/workflow-template-develop/tsconfig.json +++ b/packages/workflow-template-develop/tsconfig.json @@ -6,5 +6,8 @@ "composite": true }, "include": ["src/**/*.ts"], - "references": [{ "path": "../workflow" }] + "references": [ + { "path": "../workflow-register" }, + { "path": "../workflow-runtime" } + ] } diff --git a/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts b/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts index 463e7b2..7eff978 100644 --- a/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts +++ b/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts @@ -2,12 +2,10 @@ 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 { - createCasStore, - createExtract, - createWorkflow, - validateWorkflowDescriptor, -} from "@uncaged/workflow"; +import { createCasStore } from "@uncaged/workflow-cas"; +import { createExtract } from "@uncaged/workflow-execute"; +import { validateWorkflowDescriptor } from "@uncaged/workflow-register"; +import { createWorkflow } from "@uncaged/workflow-runtime"; import { END, type ModeratorContext, type RoleStep, START } from "@uncaged/workflow-runtime"; import { buildSolveIssueDescriptor } from "../src/descriptor.js"; import type { DeveloperMeta } from "../src/developer.js"; diff --git a/packages/workflow-template-solve-issue/package.json b/packages/workflow-template-solve-issue/package.json index dd9d02e..5a96bb2 100644 --- a/packages/workflow-template-solve-issue/package.json +++ b/packages/workflow-template-solve-issue/package.json @@ -2,14 +2,22 @@ "name": "@uncaged/workflow-template-solve-issue", "version": "0.2.0", "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.ts" + } + }, "scripts": { "test": "bun test" }, "dependencies": { - "@uncaged/workflow": "workspace:*", + "@uncaged/workflow-register": "workspace:*", "@uncaged/workflow-runtime": "workspace:*", "zod": "^4.0.0" + }, + "devDependencies": { + "@uncaged/workflow-cas": "workspace:*", + "@uncaged/workflow-execute": "workspace:*" } } diff --git a/packages/workflow-template-solve-issue/src/descriptor.ts b/packages/workflow-template-solve-issue/src/descriptor.ts index 21d1832..44a6999 100644 --- a/packages/workflow-template-solve-issue/src/descriptor.ts +++ b/packages/workflow-template-solve-issue/src/descriptor.ts @@ -1,4 +1,4 @@ -import { buildDescriptor } from "@uncaged/workflow"; +import { buildDescriptor } from "@uncaged/workflow-register"; import { solveIssueModerator } from "./moderator.js"; import { SOLVE_ISSUE_WORKFLOW_DESCRIPTION, solveIssueRoles } from "./roles.js"; diff --git a/packages/workflow-template-solve-issue/tsconfig.json b/packages/workflow-template-solve-issue/tsconfig.json index 2816fef..af22af0 100644 --- a/packages/workflow-template-solve-issue/tsconfig.json +++ b/packages/workflow-template-solve-issue/tsconfig.json @@ -6,5 +6,8 @@ "composite": true }, "include": ["src/**/*.ts"], - "references": [{ "path": "../workflow" }] + "references": [ + { "path": "../workflow-register" }, + { "path": "../workflow-runtime" } + ] } diff --git a/packages/workflow-util-agent/package.json b/packages/workflow-util-agent/package.json index 0f7e239..c9904cc 100644 --- a/packages/workflow-util-agent/package.json +++ b/packages/workflow-util-agent/package.json @@ -14,7 +14,6 @@ "test": "bun test" }, "dependencies": { - "@uncaged/workflow": "workspace:*", "@uncaged/workflow-runtime": "workspace:*" } } diff --git a/packages/workflow-util-agent/tsconfig.json b/packages/workflow-util-agent/tsconfig.json index 2816fef..1187cda 100644 --- a/packages/workflow-util-agent/tsconfig.json +++ b/packages/workflow-util-agent/tsconfig.json @@ -6,5 +6,5 @@ "composite": true }, "include": ["src/**/*.ts"], - "references": [{ "path": "../workflow" }] + "references": [{ "path": "../workflow-runtime" }] } diff --git a/packages/workflow-util/package.json b/packages/workflow-util/package.json new file mode 100644 index 0000000..db6afd0 --- /dev/null +++ b/packages/workflow-util/package.json @@ -0,0 +1,17 @@ +{ + "name": "@uncaged/workflow-util", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.ts" + } + }, + "dependencies": { + "@uncaged/workflow-protocol": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.8.3" + } +} diff --git a/packages/workflow/src/util/base32.ts b/packages/workflow-util/src/base32.ts similarity index 96% rename from packages/workflow/src/util/base32.ts rename to packages/workflow-util/src/base32.ts index 330fa14..a8b0c80 100644 --- a/packages/workflow/src/util/base32.ts +++ b/packages/workflow-util/src/base32.ts @@ -1,5 +1,4 @@ -import { err, ok } from "./result.js"; -import type { Result } from "./types.js"; +import { err, ok, type Result } from "@uncaged/workflow-protocol"; /** Crockford Base32 alphabet (no I, L, O, U) — exactly 32 symbols. */ export const CROCKFORD_BASE32_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; diff --git a/packages/workflow/src/util/index.ts b/packages/workflow-util/src/index.ts similarity index 65% rename from packages/workflow/src/util/index.ts rename to packages/workflow-util/src/index.ts index 85ee59b..b55077c 100644 --- a/packages/workflow/src/util/index.ts +++ b/packages/workflow-util/src/index.ts @@ -1,13 +1,13 @@ export { - CROCKFORD_BASE32_ALPHABET, - decodeCrockfordBase32Bits, - decodeCrockfordToUint64, - encodeCrockfordBase32Bits, - encodeUint64AsCrockford, + CROCKFORD_BASE32_ALPHABET, + decodeCrockfordBase32Bits, + decodeCrockfordToUint64, + encodeCrockfordBase32Bits, + encodeUint64AsCrockford, } from "./base32.js"; export { createLogger } from "./logger.js"; export { mergeRefsWithContentHash, normalizeRefsField } from "./refs-field.js"; -export { err, ok } from "./result.js"; +export { ok, err } from "@uncaged/workflow-protocol"; export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js"; export type { CreateLoggerOptions, LogFn, LoggerSink, Result } from "./types.js"; export { generateUlid } from "./ulid.js"; diff --git a/packages/workflow/src/util/logger.ts b/packages/workflow-util/src/logger.ts similarity index 100% rename from packages/workflow/src/util/logger.ts rename to packages/workflow-util/src/logger.ts diff --git a/packages/workflow/src/util/refs-field.ts b/packages/workflow-util/src/refs-field.ts similarity index 100% rename from packages/workflow/src/util/refs-field.ts rename to packages/workflow-util/src/refs-field.ts diff --git a/packages/workflow/src/util/storage-root.ts b/packages/workflow-util/src/storage-root.ts similarity index 100% rename from packages/workflow/src/util/storage-root.ts rename to packages/workflow-util/src/storage-root.ts diff --git a/packages/workflow/src/util/types.ts b/packages/workflow-util/src/types.ts similarity index 70% rename from packages/workflow/src/util/types.ts rename to packages/workflow-util/src/types.ts index 800c37f..01ca3dd 100644 --- a/packages/workflow/src/util/types.ts +++ b/packages/workflow-util/src/types.ts @@ -1,9 +1,9 @@ -export type { Result } from "@uncaged/workflow-runtime"; +export type { Result } from "@uncaged/workflow-protocol"; export type LoggerSink = { kind: "stderr" } | { kind: "file"; path: string }; export type CreateLoggerOptions = { - sink: LoggerSink; + sink: LoggerSink; }; export type LogFn = (tag: string, content: string) => void; diff --git a/packages/workflow/src/util/ulid.ts b/packages/workflow-util/src/ulid.ts similarity index 100% rename from packages/workflow/src/util/ulid.ts rename to packages/workflow-util/src/ulid.ts diff --git a/packages/workflow-util/tsconfig.json b/packages/workflow-util/tsconfig.json new file mode 100644 index 0000000..778d958 --- /dev/null +++ b/packages/workflow-util/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"], + "references": [ + { "path": "../workflow-protocol" } + ] +} diff --git a/packages/workflow/README.md b/packages/workflow/README.md deleted file mode 100644 index db319e3..0000000 --- a/packages/workflow/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# @uncaged/workflow - -Core workflow engine: registry, CAS, thread execution, bundle validation, and role/workflow types. - -This package implements the three-phase engine loop that runs single-file ESM workflow bundles (each exports `run` and `descriptor`). It persists threads under `~/.uncaged/workflow/` by default and hashes bundles with XXH64 (Crockford Base32). See the repo root [README](../../README.md) for workflow, bundle, thread, role, and registry concepts. - -## Install - -```bash -bun add @uncaged/workflow zod -``` - -In this monorepo, depend with `"@uncaged/workflow": "workspace:*"`. `zod` is a peer dependency (used by bundle/shape validation at the integration boundary). - -## Usage - -```typescript -import { createWorkflow, readWorkflowRegistry, executeThread } from "@uncaged/workflow"; -// Wire a WorkflowDefinition + AgentBinding + extract + optional LlmProvider into createWorkflow, -// then run the returned WorkflowFn inside your host (or use executeThread for disk-backed runs). -``` - -## API overview - -| Area | Exports (representative) | -|------|--------------------------| -| **Types** | `WorkflowDefinition`, `WorkflowFn`, `AgentFn`, `AgentBinding`, `Moderator`, `RoleDefinition`, `ThreadContext`, `LlmProvider`, `Result` shape via `ok` / `err`, `START` / `END` | -| **Bundle** | `buildDescriptor`, `extractBundleExports`, `validateWorkflowBundle`, `validateWorkflowDescriptor`, `WorkflowDescriptor`, `WorkflowRoleDescriptor` | -| **Registry** | `readWorkflowRegistry`, `writeWorkflowRegistry`, `registerWorkflowVersion`, `workflowRegistryPath`, YAML helpers | -| **CAS** | `createCasStore`, Merkle helpers (`putStepMerkleNode`, `getContentMerklePayload`, …), `hashWorkflowBundleBytes` | -| **Engine** | `createWorkflow`, `executeThread`, `parseThreadDataJsonl`, fork helpers, `garbageCollectCas` | -| **Extract / LLM tools** | `llmExtract`, `createExtract`, `createThreadReactor`, `createLlmFn`, `getExtractProvider` | -| **Agent bridge** | `workflowAsAgent` — expose a registered workflow as an agent-backed role | -| **Utilities** | `createLogger`, ULID / Crockford Base32 codecs, `getDefaultWorkflowStorageRoot`, paths | - -Full surface is re-exported from `src/index.ts`. diff --git a/packages/workflow/__tests__/base32.test.ts b/packages/workflow/__tests__/base32.test.ts deleted file mode 100644 index 88f09d5..0000000 --- a/packages/workflow/__tests__/base32.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - decodeCrockfordBase32Bits, - decodeCrockfordToUint64, - encodeCrockfordBase32Bits, - encodeUint64AsCrockford, -} from "../src/util/base32.js"; - -describe("Crockford Base32", () => { - test("roundtrip 64-bit hash encoding", () => { - const value = 0xef46_db37_51d8_e999n; - const encoded = encodeUint64AsCrockford(value); - expect(encoded.length).toBe(13); - const decoded = decodeCrockfordToUint64(encoded); - expect(decoded.ok).toBe(true); - if (decoded.ok) { - expect(decoded.value).toBe(value); - } - }); - - test("roundtrip arbitrary bit widths used by ULID (128-bit)", () => { - const rand = 0x1234567890abcdef12n & ((1n << 80n) - 1n); - const payload = (12345n << 80n) | rand; - const encoded = encodeCrockfordBase32Bits(payload, 128); - expect(encoded.length).toBe(26); - const decoded = decodeCrockfordBase32Bits(encoded, 128); - expect(decoded.ok).toBe(true); - if (decoded.ok) { - expect(decoded.value).toBe(payload); - } - }); - - test("reject invalid characters", () => { - const decoded = decodeCrockfordToUint64("!!!!!!!!!!!!!"); - expect(decoded.ok).toBe(false); - }); -}); diff --git a/packages/workflow/__tests__/build-descriptor.test.ts b/packages/workflow/__tests__/build-descriptor.test.ts deleted file mode 100644 index e60884e..0000000 --- a/packages/workflow/__tests__/build-descriptor.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -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"; - -describe("buildDescriptor", () => { - test("produces a descriptor that validates and includes JSON schemas per role", () => { - const schema = z.object({ - title: z.string(), - count: z.number(), - }); - - type M = { analyst: z.infer }; - - const descriptor = buildDescriptor({ - description: "Demo workflow", - roles: { - analyst: { - description: "Analyzes input", - systemPrompt: "You are an analyst.", - extractPrompt: "Extract title and count from the analysis.", - schema, - extractRefs: null, - }, - }, - moderator: () => END, - }); - - const validated = validateWorkflowDescriptor(descriptor); - expect(validated.ok).toBe(true); - if (!validated.ok) { - return; - } - - expect(validated.value.description).toBe("Demo workflow"); - const analyst = validated.value.roles.analyst; - expect(analyst.description).toBe("Analyzes input"); - expect(analyst.schema.type).toBe("object"); - const props = analyst.schema.properties as Record; - expect(props.title).toMatchObject({ type: "string" }); - expect(props.count).toMatchObject({ type: "number" }); - }); -}); diff --git a/packages/workflow/__tests__/bundle-validator.test.ts b/packages/workflow/__tests__/bundle-validator.test.ts deleted file mode 100644 index ad7af01..0000000 --- a/packages/workflow/__tests__/bundle-validator.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { validateWorkflowBundle } from "../src/bundle/bundle-validator.js"; - -const minimalDescriptor = `export const descriptor = { description: "x", roles: {} }; -`; - -describe("validateWorkflowBundle", () => { - test("accepts export { local as run } when local is a call expression result", () => { - const source = `${minimalDescriptor}var wf = createFn({}); -export { wf as run }; -`; - const r = validateWorkflowBundle({ filePath: "/tmp/w.esm.js", source }); - expect(r.ok).toBe(true); - }); - - test("accepts minimal valid builtin-only bundle", () => { - const source = `${minimalDescriptor}import fs from "node:fs"; - -export const run = async function* (input) { - fs.existsSync("."); - return { returnCode: 0, summary: input.prompt }; -}; -`; - const r = validateWorkflowBundle({ filePath: "/tmp/w.esm.js", source }); - expect(r.ok).toBe(true); - }); - - test("allows static import of @uncaged/workflow", () => { - const source = `${minimalDescriptor}import { putContentMerkleNode } from "@uncaged/workflow"; - -export const run = async function* (_input, options) { - const cas = options.cas; - const h = await putContentMerkleNode(cas, "x"); - return { returnCode: 0, summary: h }; -}; -`; - const r = validateWorkflowBundle({ filePath: "/tmp/w.esm.js", source }); - expect(r.ok).toBe(true); - }); - - test("allows static import of @uncaged/workflow-runtime", () => { - const source = `${minimalDescriptor}import { createWorkflow } from "@uncaged/workflow-runtime"; -import { putContentMerkleNode } from "@uncaged/workflow"; - -export const run = createWorkflow({ description: "x", roles: {}, moderator: () => "END" }, {}); -`; - const r = validateWorkflowBundle({ filePath: "/tmp/w.esm.js", source }); - expect(r.ok).toBe(true); - }); - - test("rejects wrong filename suffix", () => { - const r = validateWorkflowBundle({ - filePath: "/tmp/w.js", - source: `${minimalDescriptor}export const run = async function* (input) { return { returnCode: 0, summary: input.prompt }; }\n`, - }); - expect(r.ok).toBe(false); - }); - - test("rejects default export", () => { - const r = validateWorkflowBundle({ - filePath: "/tmp/w.esm.js", - source: `${minimalDescriptor}export default async function* (input) { return { returnCode: 0, summary: input.prompt }; }\n`, - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("default export"); - } - }); - - test("rejects run export that is not a callable bundle shape", () => { - const r = validateWorkflowBundle({ - filePath: "/tmp/w.esm.js", - source: `${minimalDescriptor}export const run = { x: 1 }; -`, - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("run"); - } - }); - - test("rejects missing run export", () => { - const r = validateWorkflowBundle({ - filePath: "/tmp/w.esm.js", - source: `${minimalDescriptor}export const x = 1;\n`, - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("run"); - } - }); - - test("rejects missing descriptor export", () => { - const r = validateWorkflowBundle({ - filePath: "/tmp/w.esm.js", - source: `export const run = async function* (input) { - return { returnCode: 0, summary: input.prompt }; -}; -`, - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("descriptor"); - } - }); - - test("rejects non-builtin imports", () => { - const r = validateWorkflowBundle({ - filePath: "/tmp/w.esm.js", - source: `${minimalDescriptor}import x from "some-package"; -export const run = async function* (input) { return { returnCode: 0, summary: input.prompt }; } -`, - }); - expect(r.ok).toBe(false); - }); - - test("rejects dynamic import", () => { - const r = validateWorkflowBundle({ - filePath: "/tmp/w.esm.js", - source: `${minimalDescriptor}export const run = async function* (input) { await import("fs"); return { returnCode: 0, summary: input.prompt }; } -`, - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("dynamic import"); - } - }); - - test("rejects require()", () => { - const r = validateWorkflowBundle({ - filePath: "/tmp/w.esm.js", - source: `${minimalDescriptor}export const run = async function* (input) { require("fs"); return { returnCode: 0, summary: input.prompt }; } -`, - }); - expect(r.ok).toBe(false); - }); -}); diff --git a/packages/workflow/__tests__/cas.test.ts b/packages/workflow/__tests__/cas.test.ts deleted file mode 100644 index f04ecb6..0000000 --- a/packages/workflow/__tests__/cas.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -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; - - beforeEach(async () => { - casDir = await mkdtemp(join(tmpdir(), "cas-test-")); - }); - - afterEach(async () => { - await rm(casDir, { recursive: true, force: true }); - }); - - test("put returns consistent hash for same content", async () => { - const cas = createCasStore(casDir); - 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 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(stored)); - }); - - 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(stored); - }); - - test("get returns null for missing hash", async () => { - const cas = createCasStore(casDir); - const result = await cas.get("0000000000000"); - expect(result).toBeNull(); - }); - - test("delete removes entry", async () => { - const cas = createCasStore(casDir); - const h = await cas.put("to be deleted"); - await cas.delete(h); - const result = await cas.get(h); - expect(result).toBeNull(); - }); - - test("delete on missing hash does not throw", async () => { - const cas = createCasStore(casDir); - await cas.delete("0000000000000"); - }); - - test("list returns all stored hashes", async () => { - const cas = createCasStore(casDir); - const h1 = await cas.put("aaa"); - const h2 = await cas.put("bbb"); - const h3 = await cas.put("ccc"); - const hashes = await cas.list(); - expect(hashes.sort()).toEqual([h1, h2, h3].sort()); - }); - - test("list returns empty array when cas dir does not exist", async () => { - const cas = createCasStore(join(casDir, "nonexistent")); - const hashes = await cas.list(); - expect(hashes).toEqual([]); - }); - - test("put is idempotent — same content written twice causes no error", async () => { - const cas = createCasStore(casDir); - 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(stored); - }); - - test("different content produces different hashes", async () => { - const cas = createCasStore(casDir); - const h1 = await cas.put("alpha"); - const h2 = await cas.put("beta"); - expect(h1).not.toBe(h2); - }); -}); diff --git a/packages/workflow/__tests__/engine.test.ts b/packages/workflow/__tests__/engine.test.ts deleted file mode 100644 index cb2d92f..0000000 --- a/packages/workflow/__tests__/engine.test.ts +++ /dev/null @@ -1,773 +0,0 @@ -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, - getContentMerklePayload, - parseMerkleNode, - serializeMerkleNode, -} from "../src/cas/merkle.js"; -import { createWorkflow } from "../src/engine/create-workflow.js"; -import { executeThread } from "../src/engine/engine.js"; -import { createLogger } from "../src/util/logger.js"; - -const plannerMetaSchema = z.object({ - plan: z.string(), - files: z.array(z.string()), -}); - -const coderMetaSchema = z.object({ - diff: z.string(), -}); - -type DemoMeta = { - planner: z.infer; - coder: z.infer; -}; - -function installMockChatCompletions(sequence: ReadonlyArray>): () => void { - const origFetch = globalThis.fetch; - let i = 0; - const mockFetch = async ( - _input: Parameters[0], - _init?: RequestInit, - ): Promise => { - const args = sequence[i] ?? sequence[sequence.length - 1]; - if (args === undefined) { - throw new Error("installMockChatCompletions: empty sequence"); - } - i += 1; - return new Response( - JSON.stringify({ - choices: [{ message: { content: JSON.stringify(args) } }], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }; - globalThis.fetch = Object.assign(mockFetch, { - preconnect: origFetch.preconnect.bind(origFetch), - }) as typeof fetch; - return () => { - globalThis.fetch = origFetch; - }; -} - -const EXTRACT_REGISTRY_YAML = `config: - maxDepth: 3 - providers: - stub: - baseUrl: http://127.0.0.1:9 - apiKey: test - models: - default: stub/model -workflows: {} -`; - -async function writeExtractRegistryConfig(storageRoot: string): Promise { - await writeFile(join(storageRoot, "workflow.yaml"), EXTRACT_REGISTRY_YAML, "utf8"); -} - -const SUPERVISOR_INTERVAL_REGISTRY_YAML = `config: - maxDepth: 3 - supervisorInterval: 2 - providers: - stub: - baseUrl: http://127.0.0.1:9 - apiKey: test - models: - extract: stub/model - supervisor: stub/supervisor-cheap -workflows: {} -`; - -const SUPERVISOR_LONG_INTERVAL_REGISTRY_YAML = `config: - maxDepth: 3 - supervisorInterval: 10 - providers: - stub: - baseUrl: http://127.0.0.1:9 - apiKey: test - models: - extract: stub/model - supervisor: stub/supervisor-cheap -workflows: {} -`; - -async function writeRegistryYaml(storageRoot: string, yaml: string): Promise { - await writeFile(join(storageRoot, "workflow.yaml"), yaml, "utf8"); -} - -/** Extract and supervisor both run via {@link createThreadReactor}; differentiate by `body.model`. */ -function installMockExtractThenSupervisor(params: { - extractArgs: ReadonlyArray>; - supervisorDecision: "continue" | "stop"; - onSupervisorCall?: () => void; -}): () => void { - const origFetch = globalThis.fetch; - let extractI = 0; - const mockFetch = async ( - _input: Parameters[0], - init?: RequestInit, - ): Promise => { - const body = init?.body ? (JSON.parse(String(init.body)) as Record) : {}; - const model = typeof body.model === "string" ? body.model : ""; - const isSupervisor = model.startsWith("supervisor-"); - if (!isSupervisor) { - const args = - params.extractArgs[extractI] ?? params.extractArgs[params.extractArgs.length - 1]; - if (args === undefined) { - throw new Error("installMockExtractThenSupervisor: empty extractArgs"); - } - extractI += 1; - return new Response( - JSON.stringify({ - choices: [{ message: { content: JSON.stringify(args) } }], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - params.onSupervisorCall?.(); - return new Response( - JSON.stringify({ - choices: [ - { message: { content: JSON.stringify({ decision: params.supervisorDecision }) } }, - ], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }; - globalThis.fetch = Object.assign(mockFetch, { - preconnect: origFetch.preconnect.bind(origFetch), - }) as typeof fetch; - return () => { - globalThis.fetch = origFetch; - }; -} - -const demoWorkflow = createWorkflow( - { - roles: { - planner: { - description: "Demo planner", - systemPrompt: "You are a planner.", - extractPrompt: "Extract plan text and affected files list.", - schema: plannerMetaSchema, - extractRefs: null, - }, - coder: { - description: "Demo coder", - systemPrompt: "You are a coder.", - extractPrompt: "Extract the code diff summary.", - schema: coderMetaSchema, - extractRefs: null, - }, - }, - moderator: (ctx) => { - if (ctx.steps.length === 0) { - return "planner"; - } - if (ctx.steps.length === 1) { - return "coder"; - } - return END; - }, - }, - { - agent: async () => "unused", - overrides: { - planner: async () => "plan-body", - coder: async () => "code-body", - }, - }, -); - -describe("executeThread", () => { - let restoreFetch: (() => void) | null = null; - - afterEach(() => { - restoreFetch?.(); - restoreFetch = null; - }); - - test("writes RFC-001 `.data.jsonl` start + role records and `.info.jsonl` logs", async () => { - restoreFetch = installMockChatCompletions([ - { plan: "do-it", files: ["a.ts"] }, - { diff: "+ok" }, - ]); - - const root = await mkdtemp(join(tmpdir(), "wf-engine-")); - try { - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const hash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", hash), { recursive: true }); - await writeExtractRegistryConfig(root); - const cas = createCasStore(join(root, "cas")); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const result = await executeThread( - demoWorkflow, - "demo-flow", - { prompt: "Fix the login redirect bug in #3", steps: [] }, - { - maxRounds: 5, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: null, - prefilledDiskSteps: null, - storageRoot: root, - }, - { threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - expect(result.returnCode).toBe(0); - expect(typeof result.rootHash).toBe("string"); - expect(result.rootHash.length).toBeGreaterThan(0); - - const rootYaml = await cas.get(result.rootHash); - expect(rootYaml).not.toBeNull(); - const rootNode = parseMerkleNode(rootYaml ?? ""); - expect(rootNode.type).toBe("thread"); - const rootPayload = rootNode.payload as Record; - expect(rootPayload.workflow).toBe("demo-flow"); - expect(rootPayload.threadId).toBe(threadId); - const rootResult = rootPayload.result as Record; - expect(rootResult.returnCode).toBe(0); - expect(rootNode.children.length).toBe(2); - - const dataText = await readFile(dataPath, "utf8"); - const lines = dataText - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(lines.length).toBe(3); - - const start = JSON.parse(lines[0] ?? "{}") as Record; - expect(start.name).toBe("demo-flow"); - expect(start.hash).toBe(hash); - expect(start.threadId).toBe(threadId); - expect(typeof start.timestamp).toBe("number"); - - const params = start.parameters as Record; - expect(params.prompt).toBe("Fix the login redirect bug in #3"); - const opts = params.options as Record; - expect(opts.maxRounds).toBe(5); - expect(opts.depth).toBe(0); - expect(Object.keys(opts).sort()).toEqual(["depth", "maxRounds"]); - - const role1 = JSON.parse(lines[1] ?? "{}") as Record; - expect(role1.role).toBe("planner"); - expect(typeof role1.contentHash).toBe("string"); - expect(await getContentMerklePayload(cas, String(role1.contentHash))).toBe("plan-body"); - expect(role1.meta).toEqual({ plan: "do-it", files: ["a.ts"] }); - expect(role1.refs).toEqual([role1.contentHash]); - expect(typeof role1.timestamp).toBe("number"); - - const role2 = JSON.parse(lines[2] ?? "{}") as Record; - expect(role2.role).toBe("coder"); - expect(role2.refs).toEqual([role2.contentHash]); - - const step1Yaml = await cas.get(rootNode.children[0] ?? ""); - const step2Yaml = await cas.get(rootNode.children[1] ?? ""); - expect(step1Yaml).not.toBeNull(); - expect(step2Yaml).not.toBeNull(); - const step1Node = parseMerkleNode(step1Yaml ?? ""); - const step2Node = parseMerkleNode(step2Yaml ?? ""); - expect(step1Node.type).toBe("step"); - expect(step2Node.type).toBe("step"); - expect(step1Node.children).toEqual([String(role1.contentHash)]); - expect(step2Node.children).toEqual([String(role2.contentHash)]); - const step1Payload = step1Node.payload as Record; - expect(step1Payload.role).toBe("planner"); - expect(step1Payload.meta).toEqual({ plan: "do-it", files: ["a.ts"] }); - - const infoText = await readFile(infoPath, "utf8"); - const infoLines = infoText - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(infoLines.length).toBeGreaterThan(0); - const log0 = JSON.parse(infoLines[0] ?? "{}") as Record; - expect(typeof log0.tag).toBe("string"); - expect(String(log0.tag).length).toBe(8); - expect(typeof log0.content).toBe("string"); - expect(typeof log0.timestamp).toBe("number"); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("pre-filled input.steps skips roles already present", async () => { - restoreFetch = installMockChatCompletions([{ diff: "+ok" }]); - - const root = await mkdtemp(join(tmpdir(), "wf-engine-fork-")); - try { - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const hash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", hash), { recursive: true }); - await writeExtractRegistryConfig(root); - const cas = createCasStore(join(root, "cas")); - const plannerHash = await cas.put(serializeMerkleNode(createContentMerkleNode("plan-body"))); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const histTs = 9_000_000; - const mergedPlannerRefs = ["CAS111AAAAAAA", plannerHash]; - const result = await executeThread( - demoWorkflow, - "demo-flow", - { - prompt: "continue from planner", - steps: [ - { - role: "planner", - contentHash: plannerHash, - meta: { plan: "do-it", files: ["a.ts"] }, - refs: mergedPlannerRefs, - }, - ], - }, - { - maxRounds: 5, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: "01SRC1111111111111111111", - prefilledDiskSteps: [ - { - role: "planner", - contentHash: plannerHash, - meta: { plan: "do-it", files: ["a.ts"] }, - refs: mergedPlannerRefs, - timestamp: histTs, - }, - ], - storageRoot: root, - }, - { threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - expect(result.returnCode).toBe(0); - expect(typeof result.rootHash).toBe("string"); - - const rootYaml = await cas.get(result.rootHash); - const rootNode = parseMerkleNode(rootYaml ?? ""); - expect(rootNode.children.length).toBe(2); - - const dataText = await readFile(dataPath, "utf8"); - const lines = dataText - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(lines.length).toBe(3); - - const start = JSON.parse(lines[0] ?? "{}") as Record; - expect(start.forkFrom).toEqual({ threadId: "01SRC1111111111111111111" }); - - const role0 = JSON.parse(lines[1] ?? "{}") as Record; - expect(role0.role).toBe("planner"); - expect(role0.timestamp).toBe(histTs); - expect(role0.refs).toEqual(mergedPlannerRefs); - - const role1 = JSON.parse(lines[2] ?? "{}") as Record; - expect(role1.role).toBe("coder"); - expect(await getContentMerklePayload(cas, String(role1.contentHash))).toBe("code-body"); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("respects maxRounds=0 (start record only)", async () => { - const root = await mkdtemp(join(tmpdir(), "wf-engine-max0-")); - try { - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const hash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", hash), { recursive: true }); - const cas = createCasStore(join(root, "cas")); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const result = await executeThread( - demoWorkflow, - "demo-flow", - { prompt: "hello", steps: [] }, - { - maxRounds: 0, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: null, - prefilledDiskSteps: null, - storageRoot: root, - }, - { threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - expect(result.returnCode).toBe(0); - expect(typeof result.rootHash).toBe("string"); - - const rootYaml = await cas.get(result.rootHash); - const rootNode = parseMerkleNode(rootYaml ?? ""); - expect(rootNode.type).toBe("thread"); - expect(rootNode.children.length).toBe(0); - - const dataText = await readFile(dataPath, "utf8"); - const lines = dataText - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(lines.length).toBe(1); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("Merkle DAG: root → step nodes → content for full thread traversal", async () => { - restoreFetch = installMockChatCompletions([ - { plan: "do-it", files: ["a.ts"] }, - { diff: "+ok" }, - ]); - - const root = await mkdtemp(join(tmpdir(), "wf-engine-dag-")); - try { - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const hash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", hash), { recursive: true }); - await writeExtractRegistryConfig(root); - const cas = createCasStore(join(root, "cas")); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const result = await executeThread( - demoWorkflow, - "demo-flow", - { prompt: "DAG test", steps: [] }, - { - maxRounds: 5, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: null, - prefilledDiskSteps: null, - storageRoot: root, - }, - { threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - const dataText = await readFile(dataPath, "utf8"); - const lines = dataText - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(lines.length).toBe(3); - - const rolePlanner = JSON.parse(lines[1] ?? "{}") as Record; - const roleCoder = JSON.parse(lines[2] ?? "{}") as Record; - - const threadYaml = await cas.get(result.rootHash); - expect(threadYaml).not.toBeNull(); - const threadNode = parseMerkleNode(threadYaml ?? ""); - expect(threadNode.type).toBe("thread"); - - const bodies: string[] = []; - for (const stepHash of threadNode.children) { - const stepYaml = await cas.get(stepHash); - expect(stepYaml).not.toBeNull(); - const stepNode = parseMerkleNode(stepYaml ?? ""); - expect(stepNode.type).toBe("step"); - expect(stepNode.children.length).toBe(1); - const contentHash = stepNode.children[0]; - expect(contentHash).toBeDefined(); - const body = await getContentMerklePayload(cas, contentHash ?? ""); - expect(body).not.toBeNull(); - bodies.push(body ?? ""); - } - - expect(bodies.sort()).toEqual(["code-body", "plan-body"].sort()); - expect(rolePlanner.role).toBe("planner"); - expect(roleCoder.role).toBe("coder"); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("extract traverses CAS DAG via cas_get during extraction", async () => { - const dagMetaSchema = z.object({ leafPayload: z.string() }); - type DagDemoMeta = { walker: z.infer }; - - const origFetch = globalThis.fetch; - restoreFetch = () => { - globalThis.fetch = origFetch; - }; - let fetchRound = 0; - - const root = await mkdtemp(join(tmpdir(), "wf-engine-react-")); - try { - const cas = createCasStore(join(root, "cas")); - const leafYaml = serializeMerkleNode(createContentMerkleNode("needle-from-leaf")); - const leafHash = await cas.put(leafYaml); - const rootYaml = serializeMerkleNode({ - type: "thread", - payload: { - workflow: "dag-demo", - threadId: "01DAG00000000000000000001", - result: { returnCode: 0, summary: "" }, - }, - children: [leafHash], - }); - const dagRootHash = await cas.put(rootYaml); - - globalThis.fetch = Object.assign( - async (_input: Parameters[0], _init?: RequestInit) => { - fetchRound += 1; - if (fetchRound === 1) { - return new Response( - JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - id: "c1", - type: "function", - function: { - name: "cas_get", - arguments: JSON.stringify({ hash: dagRootHash }), - }, - }, - ], - }, - }, - ], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - if (fetchRound === 2) { - return new Response( - JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - id: "c2", - type: "function", - function: { - name: "cas_get", - arguments: JSON.stringify({ hash: leafHash }), - }, - }, - ], - }, - }, - ], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return new Response( - JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - id: "c3", - type: "function", - function: { - name: "extract", - arguments: JSON.stringify({ leafPayload: "needle-from-leaf" }), - }, - }, - ], - }, - }, - ], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }, - { preconnect: origFetch.preconnect.bind(origFetch) }, - ) as typeof fetch; - - const dagWorkflow = createWorkflow( - { - roles: { - walker: { - description: "DAG walker", - systemPrompt: "Output only the root CAS hash.", - extractPrompt: - "Set leafPayload to the string payload of the content Merkle node under the root.", - schema: dagMetaSchema, - extractRefs: null, - }, - }, - moderator: (ctx) => (ctx.steps.length === 0 ? "walker" : END), - }, - { agent: async () => dagRootHash, overrides: null }, - ); - - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const hash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", hash), { recursive: true }); - await writeExtractRegistryConfig(root); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const result = await executeThread( - dagWorkflow, - "dag-demo", - { prompt: "traverse", steps: [] }, - { - maxRounds: 5, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: null, - prefilledDiskSteps: null, - storageRoot: root, - }, - { threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - expect(result.returnCode).toBe(0); - expect(fetchRound).toBe(3); - - const dataText = await readFile(dataPath, "utf8"); - const lines = dataText - .trim() - .split("\n") - .filter((l) => l !== ""); - const roleRec = JSON.parse(lines[1] ?? "{}") as Record; - expect(roleRec.role).toBe("walker"); - expect(roleRec.meta).toEqual({ leafPayload: "needle-from-leaf" }); - } finally { - globalThis.fetch = origFetch; - await rm(root, { recursive: true, force: true }); - } - }); - - test("supervisor stops thread when interval elapses and model returns stop", async () => { - restoreFetch = installMockExtractThenSupervisor({ - extractArgs: [{ plan: "do-it", files: ["a.ts"] }, { diff: "+ok" }], - supervisorDecision: "stop", - }); - - const root = await mkdtemp(join(tmpdir(), "wf-engine-sup-stop-")); - try { - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const hash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", hash), { recursive: true }); - await writeRegistryYaml(root, SUPERVISOR_INTERVAL_REGISTRY_YAML); - const cas = createCasStore(join(root, "cas")); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const result = await executeThread( - demoWorkflow, - "demo-flow", - { prompt: "supervisor-stop-case", steps: [] }, - { - maxRounds: 20, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: null, - prefilledDiskSteps: null, - storageRoot: root, - }, - { threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - expect(result.returnCode).toBe(0); - expect(result.summary).toBe("completed: supervisor stopped thread"); - - const dataText = await readFile(dataPath, "utf8"); - const lines = dataText - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(lines.length).toBe(3); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("supervisor is not invoked before supervisorInterval rounds", async () => { - let supervisorCalls = 0; - restoreFetch = installMockExtractThenSupervisor({ - extractArgs: [{ plan: "do-it", files: ["a.ts"] }, { diff: "+ok" }], - supervisorDecision: "stop", - onSupervisorCall: () => { - supervisorCalls += 1; - }, - }); - - const root = await mkdtemp(join(tmpdir(), "wf-engine-sup-skip-")); - try { - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const hash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", hash), { recursive: true }); - await writeRegistryYaml(root, SUPERVISOR_LONG_INTERVAL_REGISTRY_YAML); - const cas = createCasStore(join(root, "cas")); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const result = await executeThread( - demoWorkflow, - "demo-flow", - { prompt: "no-supervisor-yet", steps: [] }, - { - maxRounds: 20, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: null, - prefilledDiskSteps: null, - storageRoot: root, - }, - { threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - expect(supervisorCalls).toBe(0); - expect(result.returnCode).toBe(0); - expect(result.summary).toBe("completed: moderator returned END"); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -}); diff --git a/packages/workflow/__tests__/fork-thread.test.ts b/packages/workflow/__tests__/fork-thread.test.ts deleted file mode 100644 index 27bd253..0000000 --- a/packages/workflow/__tests__/fork-thread.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - buildForkPlan, - parseThreadDataJsonl, - selectForkHistoricalSteps, -} from "../src/engine/fork-thread.js"; - -const sampleDataJsonl = `{"name":"demo","hash":"C9NMV6V2TQT81","threadId":"01AAA1111111111111111111","parameters":{"prompt":"hi","options":{"maxRounds":5}},"timestamp":100} -{"role":"planner","contentHash":"HP0000000000000000000001","meta":{},"refs":[],"timestamp":101} -{"role":"coder","contentHash":"HP0000000000000000000002","meta":{},"refs":[],"timestamp":102} -{"role":"reviewer","contentHash":"HP0000000000000000000003","meta":{},"refs":[],"timestamp":103} -`; - -describe("fork-thread", () => { - test("parseThreadDataJsonl reads start + role steps", () => { - const r = parseThreadDataJsonl(sampleDataJsonl); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.start.workflowName).toBe("demo"); - expect(r.value.start.hash).toBe("C9NMV6V2TQT81"); - expect(r.value.start.threadId).toBe("01AAA1111111111111111111"); - expect(r.value.start.prompt).toBe("hi"); - expect(r.value.start.maxRounds).toBe(5); - expect(r.value.start.depth).toBe(0); - expect(r.value.roleSteps.length).toBe(3); - expect(r.value.roleSteps[0]?.role).toBe("planner"); - }); - - test("selectForkHistoricalSteps: --from-role keeps through first matching role", () => { - const parsed = parseThreadDataJsonl(sampleDataJsonl); - expect(parsed.ok).toBe(true); - if (!parsed.ok) { - return; - } - const sel = selectForkHistoricalSteps(parsed.value.roleSteps, "planner"); - expect(sel.ok).toBe(true); - if (!sel.ok) { - return; - } - expect(sel.value.length).toBe(1); - expect(sel.value[0]?.role).toBe("planner"); - }); - - test("selectForkHistoricalSteps: retry last drops final step", () => { - const parsed = parseThreadDataJsonl(sampleDataJsonl); - expect(parsed.ok).toBe(true); - if (!parsed.ok) { - return; - } - const sel = selectForkHistoricalSteps(parsed.value.roleSteps, null); - expect(sel.ok).toBe(true); - if (!sel.ok) { - return; - } - expect(sel.value.map((s) => s.role)).toEqual(["planner", "coder"]); - }); - - test("selectForkHistoricalSteps: unknown role lists available names", () => { - const parsed = parseThreadDataJsonl(sampleDataJsonl); - expect(parsed.ok).toBe(true); - if (!parsed.ok) { - return; - } - const sel = selectForkHistoricalSteps(parsed.value.roleSteps, "nope"); - expect(sel.ok).toBe(false); - if (sel.ok) { - return; - } - expect(sel.error).toContain("planner"); - expect(sel.error).toContain("coder"); - expect(sel.error).toContain("reviewer"); - }); - - test("buildForkPlan composes worker payload", () => { - const r = buildForkPlan(sampleDataJsonl, "planner"); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.sourceThreadId).toBe("01AAA1111111111111111111"); - expect(r.value.workflowName).toBe("demo"); - expect(r.value.historicalSteps.length).toBe(1); - expect(r.value.historicalSteps[0]?.timestamp).toBe(101); - expect(r.value.runOptions).toEqual({ maxRounds: 5, depth: 0 }); - }); - - test("parseThreadDataJsonl ignores trailing WorkflowResult line", () => { - const text = `${sampleDataJsonl.trim()}\n{"returnCode":0,"summary":"done"}\n`; - const r = parseThreadDataJsonl(text); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.roleSteps.length).toBe(3); - expect(r.value.roleSteps[2]?.role).toBe("reviewer"); - }); - - test("parseThreadDataJsonl errors when WorkflowResult is not last", () => { - const text = `{"name":"demo","hash":"H","threadId":"01ZZZZZZZZZZZZZZZZZZZZZZ","parameters":{"prompt":"p","options":{"maxRounds":3}},"timestamp":1} -{"returnCode":0,"summary":"early"} -{"role":"planner","content":"x","meta":{},"timestamp":2} -`; - const r = parseThreadDataJsonl(text); - expect(r.ok).toBe(false); - }); - - test("parseThreadDataJsonl reads explicit depth from start record", () => { - const text = `{"name":"demo","hash":"H","threadId":"01ZZZZZZZZZZZZZZZZZZZZZZ","parameters":{"prompt":"p","options":{"maxRounds":3,"depth":2}},"timestamp":1} -{"role":"planner","contentHash":"HP0000000000000000000099","meta":{},"refs":[],"timestamp":2} -`; - const r = parseThreadDataJsonl(text); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.start.depth).toBe(2); - const plan = buildForkPlan(text, null); - expect(plan.ok).toBe(true); - if (!plan.ok) { - return; - } - expect(plan.value.runOptions).toEqual({ maxRounds: 3, depth: 2 }); - }); -}); diff --git a/packages/workflow/__tests__/hash.test.ts b/packages/workflow/__tests__/hash.test.ts deleted file mode 100644 index b8b345b..0000000 --- a/packages/workflow/__tests__/hash.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { hashWorkflowBundleBytes } from "../src/cas/hash.js"; -import { decodeCrockfordToUint64 } from "../src/util/base32.js"; - -describe("hashWorkflowBundleBytes", () => { - test("matches XXH64 reference for empty input", () => { - const encoder = new TextEncoder(); - const digest = hashWorkflowBundleBytes(encoder.encode("")); - const decoded = decodeCrockfordToUint64(digest); - expect(decoded.ok).toBe(true); - if (decoded.ok) { - expect(decoded.value).toBe(0xef46_db37_51d8_e999n); - } - }); - - test("stable for identical content", () => { - const encoder = new TextEncoder(); - const data = encoder.encode( - `export const descriptor = { description: "x", roles: {} }; -export const run = async function* (input) { return { returnCode: 0, summary: input.prompt }; } -`, - ); - expect(hashWorkflowBundleBytes(data)).toBe(hashWorkflowBundleBytes(data)); - }); -}); diff --git a/packages/workflow/__tests__/logger.test.ts b/packages/workflow/__tests__/logger.test.ts deleted file mode 100644 index d6c39b9..0000000 --- a/packages/workflow/__tests__/logger.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { mkdir, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { createLogger } from "../src/util/logger.js"; - -describe("createLogger", () => { - test("writes JSONL records to a file sink", async () => { - const dir = join(tmpdir(), `wf-log-${process.pid}-${Date.now()}`); - await mkdir(dir, { recursive: true }); - const logPath = join(dir, "test.log"); - const log = createLogger({ sink: { kind: "file", path: logPath } }); - log("01ABCDEF", "hello"); - const text = await readFile(logPath, "utf8"); - const line = text.trim().split("\n")[0]; - expect(line).toBeDefined(); - const obj = JSON.parse(line ?? "{}") as { tag: string; content: string; timestamp: number }; - expect(obj.tag).toBe("01ABCDEF"); - expect(obj.content).toBe("hello"); - expect(typeof obj.timestamp).toBe("number"); - await rm(dir, { recursive: true, force: true }); - }); - - test("rejects invalid tags", () => { - const log = createLogger({ sink: { kind: "stderr" } }); - expect(() => log("BAD", "x")).toThrow(); - expect(() => log("01abcdefg", "x")).toThrow(); - expect(() => log("01ABCDEO", "x")).toThrow(); - }); -}); diff --git a/packages/workflow/__tests__/merkle.test.ts b/packages/workflow/__tests__/merkle.test.ts deleted file mode 100644 index 3598cd7..0000000 --- a/packages/workflow/__tests__/merkle.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - createContentMerkleNode, - parseMerkleNode, - serializeMerkleNode, -} from "../src/cas/merkle.js"; - -describe("merkle", () => { - test("content node roundtrips through YAML", () => { - const node = createContentMerkleNode("hello\nworld"); - const yaml = serializeMerkleNode(node); - const back = parseMerkleNode(yaml); - expect(back).toEqual(node); - }); - - test("step node with object payload roundtrips", () => { - const node = { - type: "step" as const, - payload: { role: "planner", foo: 1 }, - children: ["ABC123", "DEF456"], - }; - const yaml = serializeMerkleNode(node); - const back = parseMerkleNode(yaml); - expect(back.type).toBe("step"); - expect(back.payload).toEqual({ role: "planner", foo: 1 }); - expect(back.children).toEqual(["ABC123", "DEF456"]); - }); - - test("parse rejects invalid YAML root", () => { - expect(() => parseMerkleNode("[]")).toThrow(); - }); -}); diff --git a/packages/workflow/__tests__/refs-tracking.test.ts b/packages/workflow/__tests__/refs-tracking.test.ts deleted file mode 100644 index 075689d..0000000 --- a/packages/workflow/__tests__/refs-tracking.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -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 { createLogger } from "../src/util/logger.js"; - -const phaseSchema = z.object({ - hash: z.string(), - title: z.string(), -}); - -const plannerMetaSchema = z.object({ - phases: z.array(phaseSchema), -}); - -type RefsDemoMeta = { - planner: z.infer; -}; - -function installMockChatCompletions(sequence: ReadonlyArray>): () => void { - const origFetch = globalThis.fetch; - let i = 0; - const mockFetch = async ( - _input: Parameters[0], - _init?: RequestInit, - ): Promise => { - const args = sequence[i] ?? sequence[sequence.length - 1]; - if (args === undefined) { - throw new Error("installMockChatCompletions: empty sequence"); - } - i += 1; - return new Response( - JSON.stringify({ - choices: [{ message: { content: JSON.stringify(args) } }], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }; - globalThis.fetch = Object.assign(mockFetch, { - preconnect: origFetch.preconnect.bind(origFetch), - }) as typeof fetch; - return () => { - globalThis.fetch = origFetch; - }; -} - -const EXTRACT_REGISTRY_YAML = `config: - maxDepth: 3 - providers: - stub: - baseUrl: http://127.0.0.1:9 - apiKey: test - models: - default: stub/model -workflows: {} -`; - -const refsDemoWorkflow = createWorkflow( - { - roles: { - planner: { - description: "Planner with phase hashes", - systemPrompt: "Plan.", - extractPrompt: "Extract phases with CAS hashes.", - schema: plannerMetaSchema, - extractRefs: (meta) => meta.phases.map((p) => p.hash), - }, - }, - moderator: (ctx) => (ctx.steps.length === 0 ? "planner" : END), - }, - { - agent: async () => "plan-output", - overrides: null, - }, -); - -describe("RoleStep refs tracking", () => { - let restoreFetch: (() => void) | null = null; - - afterEach(() => { - restoreFetch?.(); - restoreFetch = null; - }); - - test("parseThreadDataJsonl reads refs and defaults missing refs to []", () => { - const text = `{"name":"demo","hash":"C9NMV6V2TQT81","threadId":"01AAA1111111111111111111","parameters":{"prompt":"hi","options":{"maxRounds":5}},"timestamp":100} -{"role":"planner","contentHash":"HPAYLOAD111111","meta":{},"refs":["H111AAAAAAAAA","H222AAAAAAAAA"],"timestamp":101} -{"role":"coder","contentHash":"HPAYLOAD222222","meta":{},"timestamp":102} -`; - const r = parseThreadDataJsonl(text); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.roleSteps[0]?.refs).toEqual(["H111AAAAAAAAA", "H222AAAAAAAAA"]); - expect(r.value.roleSteps[1]?.refs).toEqual([]); - }); - - test("executeThread persists refs from extractRefs on role yields", async () => { - restoreFetch = installMockChatCompletions([ - { - phases: [ - { hash: "C9NMV6V2TQT81", title: "phase-a" }, - { hash: "C9NMV6V2TQT82", title: "phase-b" }, - ], - }, - ]); - - const root = await mkdtemp(join(tmpdir(), "wf-refs-")); - try { - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const hash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", hash), { recursive: true }); - await writeFile(join(root, "workflow.yaml"), EXTRACT_REGISTRY_YAML, "utf8"); - const cas = createCasStore(join(root, "cas")); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const result = await executeThread( - refsDemoWorkflow, - "refs-demo", - { prompt: "task", steps: [] }, - { - maxRounds: 5, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: null, - prefilledDiskSteps: null, - storageRoot: root, - }, - { threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - expect(result.returnCode).toBe(0); - expect(typeof result.rootHash).toBe("string"); - expect(result.rootHash.length).toBeGreaterThan(0); - - const dataText = await readFile(dataPath, "utf8"); - const lines = dataText - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(lines.length).toBe(2); - - const role1 = JSON.parse(lines[1] ?? "{}") as Record; - expect(role1.role).toBe("planner"); - const refs = role1.refs as string[]; - expect(refs).toContain("C9NMV6V2TQT81"); - expect(refs).toContain("C9NMV6V2TQT82"); - expect(typeof role1.contentHash).toBe("string"); - expect(refs).toContain(String(role1.contentHash)); - expect(refs.length).toBe(3); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("buildForkPlan carries refs on historical steps", () => { - const text = `{"name":"demo","hash":"C9NMV6V2TQT81","threadId":"01AAA1111111111111111111","parameters":{"prompt":"hi","options":{"maxRounds":5}},"timestamp":100} -{"role":"planner","contentHash":"HP111111111111","meta":{},"refs":["KEEPREFAAAAAA"],"timestamp":101} -{"role":"coder","contentHash":"HP222222222222","meta":{},"refs":["CODERHASHAAAA"],"timestamp":102} -`; - const plan = buildForkPlan(text, null); - expect(plan.ok).toBe(true); - if (!plan.ok) { - return; - } - expect(plan.value.historicalSteps.length).toBe(1); - expect(plan.value.historicalSteps[0]?.refs).toEqual(["KEEPREFAAAAAA"]); - }); -}); diff --git a/packages/workflow/__tests__/registry.test.ts b/packages/workflow/__tests__/registry.test.ts deleted file mode 100644 index 20a7642..0000000 --- a/packages/workflow/__tests__/registry.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { mkdir, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { - parseWorkflowRegistryYaml, - readWorkflowRegistry, - registerWorkflowVersion, - rollbackWorkflowToHistoryHash, - unregisterWorkflow, - writeWorkflowRegistry, -} from "../src/registry/registry.js"; - -describe("workflow registry", () => { - test("roundtrips through workflow.yaml", async () => { - const dir = join(tmpdir(), `wf-reg-${process.pid}-${Date.now()}`); - await mkdir(dir, { recursive: true }); - - const empty = await readWorkflowRegistry(dir); - expect(empty.ok).toBe(true); - if (!empty.ok) { - return; - } - expect(empty.value.config).toBeNull(); - - const r1 = registerWorkflowVersion(empty.value, "solve-issue", "AAAAAAAAAAAAA", 100); - const w1 = await writeWorkflowRegistry(dir, r1); - expect(w1.ok).toBe(true); - - const back = await readWorkflowRegistry(dir); - expect(back.ok).toBe(true); - if (!back.ok) { - await rm(dir, { recursive: true, force: true }); - return; - } - expect(back.value.workflows["solve-issue"]?.hash).toBe("AAAAAAAAAAAAA"); - - const r2 = registerWorkflowVersion(back.value, "solve-issue", "BBBBBBBBBBBBB", 200); - expect(r2.workflows["solve-issue"]?.history[0]?.hash).toBe("AAAAAAAAAAAAA"); - - const removed = unregisterWorkflow(r2, "solve-issue"); - expect(removed.ok).toBe(true); - if (!removed.ok) { - await rm(dir, { recursive: true, force: true }); - return; - } - - const w2 = await writeWorkflowRegistry(dir, removed.value); - expect(w2.ok).toBe(true); - - const finalRead = await readWorkflowRegistry(dir); - expect(finalRead.ok).toBe(true); - if (finalRead.ok) { - expect(finalRead.value.workflows["solve-issue"]).toBeUndefined(); - } - - await rm(dir, { recursive: true, force: true }); - }); - - test("treats missing registry as empty", async () => { - const dir = join(tmpdir(), `wf-reg2-${process.pid}-${Date.now()}`); - await mkdir(dir, { recursive: true }); - const empty = await readWorkflowRegistry(dir); - expect(empty.ok).toBe(true); - if (empty.ok) { - expect(Object.keys(empty.value.workflows).length).toBe(0); - } - await rm(dir, { recursive: true, force: true }); - }); - - test("rollbackWorkflowToHistoryHash swaps head with a prior version", () => { - let reg = registerWorkflowVersion({ config: null, workflows: {} }, "solve-issue", "H1", 100); - reg = registerWorkflowVersion(reg, "solve-issue", "H2", 200); - reg = registerWorkflowVersion(reg, "solve-issue", "H3", 300); - const entry = reg.workflows["solve-issue"]; - expect(entry).toBeDefined(); - if (entry === undefined) { - return; - } - expect(entry.hash).toBe("H3"); - expect(entry.history.map((h) => h.hash)).toEqual(["H2", "H1"]); - - const toH2 = rollbackWorkflowToHistoryHash(entry, null); - expect(toH2.ok).toBe(true); - if (!toH2.ok) { - return; - } - expect(toH2.value.hash).toBe("H2"); - expect(toH2.value.history.map((h) => h.hash)).toEqual(["H3", "H1"]); - - const toH1 = rollbackWorkflowToHistoryHash(toH2.value, "H1"); - expect(toH1.ok).toBe(true); - if (!toH1.ok) { - return; - } - expect(toH1.value.hash).toBe("H1"); - expect(toH1.value.history.map((h) => h.hash)).toEqual(["H2", "H3"]); - - const bad = rollbackWorkflowToHistoryHash(toH1.value, "NONE"); - expect(bad.ok).toBe(false); - }); - - test("parses config section and literal apiKey", () => { - const yaml = ` -config: - maxDepth: 3 - providers: - dashscope: - baseUrl: https://example.com/v1 - apiKey: secret-key - models: - default: dashscope/qwen-turbo - extract: dashscope/qwen-plus -workflows: - solve-issue: - hash: SPVR4BDMSGC1W - timestamp: 1 - history: [] -`; - const r = parseWorkflowRegistryYaml(yaml); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.config).not.toBeNull(); - if (r.value.config === null) { - return; - } - expect(r.value.config.maxDepth).toBe(3); - expect(r.value.config.providers.dashscope?.baseUrl).toBe("https://example.com/v1"); - expect(r.value.config.providers.dashscope?.apiKey).toBe("secret-key"); - expect(r.value.config.models.extract).toBe("dashscope/qwen-plus"); - expect(r.value.config.models.default).toBe("dashscope/qwen-turbo"); - expect(r.value.config.supervisorInterval).toBe(3); - }); - - test("defaults supervisorInterval to 3 when omitted", () => { - const yaml = ` -config: - maxDepth: 0 - providers: - p: - baseUrl: https://example.com - apiKey: k - models: - default: p/m -workflows: {} -`; - const r = parseWorkflowRegistryYaml(yaml); - expect(r.ok).toBe(true); - if (!r.ok || r.value.config === null) { - return; - } - expect(r.value.config.supervisorInterval).toBe(3); - }); - - test("parses explicit supervisorInterval", () => { - const yaml = ` -config: - maxDepth: 0 - supervisorInterval: 7 - providers: - p: - baseUrl: https://example.com - apiKey: k - models: - default: p/m -workflows: {} -`; - const r = parseWorkflowRegistryYaml(yaml); - expect(r.ok).toBe(true); - if (!r.ok || r.value.config === null) { - return; - } - expect(r.value.config.supervisorInterval).toBe(7); - }); - - test("parse errors when supervisorInterval is negative", () => { - const yaml = ` -config: - maxDepth: 0 - supervisorInterval: -1 - providers: - p: - baseUrl: https://example.com - apiKey: k - models: - default: p/m -workflows: {} -`; - const r = parseWorkflowRegistryYaml(yaml); - expect(r.ok).toBe(false); - }); - - test("parses config apiKey env: prefix from process.env", () => { - const prev = process.env.WF_REGISTRY_TEST_API_KEY; - process.env.WF_REGISTRY_TEST_API_KEY = "from-env"; - try { - const yaml = ` -config: - maxDepth: 1 - providers: - dashscope: - baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1 - apiKey: env:WF_REGISTRY_TEST_API_KEY - models: - default: dashscope/qwen-plus - extract: dashscope/qwen-plus -workflows: {} -`; - const r = parseWorkflowRegistryYaml(yaml); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.config?.providers.dashscope?.apiKey).toBe("from-env"); - } finally { - if (prev === undefined) { - delete process.env.WF_REGISTRY_TEST_API_KEY; - } else { - process.env.WF_REGISTRY_TEST_API_KEY = prev; - } - } - }); - - test("parse errors when env: apiKey variable is unset", () => { - const prev = process.env.WF_REGISTRY_TEST_API_KEY_UNSET; - delete process.env.WF_REGISTRY_TEST_API_KEY_UNSET; - try { - const yaml = ` -config: - maxDepth: 1 - providers: - p: - baseUrl: https://example.com - apiKey: env:WF_REGISTRY_TEST_API_KEY_UNSET - models: - default: p/m -workflows: {} -`; - const r = parseWorkflowRegistryYaml(yaml); - expect(r.ok).toBe(false); - } finally { - if (prev !== undefined) { - process.env.WF_REGISTRY_TEST_API_KEY_UNSET = prev; - } - } - }); - - test("parse errors on invalid shape", async () => { - const dir = join(tmpdir(), `wf-reg3-${process.pid}-${Date.now()}`); - await mkdir(dir, { recursive: true }); - await writeFile(join(dir, "workflow.yaml"), 'workflows: "broken"\n', "utf8"); - const bad = await readWorkflowRegistry(dir); - expect(bad.ok).toBe(false); - await rm(dir, { recursive: true, force: true }); - }); -}); diff --git a/packages/workflow/__tests__/resolve-model.test.ts b/packages/workflow/__tests__/resolve-model.test.ts deleted file mode 100644 index 7612cc0..0000000 --- a/packages/workflow/__tests__/resolve-model.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { resolveModel } from "../src/config/resolve-model.js"; -import type { WorkflowConfig } from "../src/registry/index.js"; - -function sampleConfig(): WorkflowConfig { - return { - maxDepth: 3, - supervisorInterval: 3, - providers: { - dashscope: { - baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", - apiKey: "secret", - }, - other: { - baseUrl: "https://other.example/v1", - apiKey: "k2", - }, - }, - models: { - default: "dashscope/qwen-plus", - extract: "other/foo/bar-model", - }, - }; -} - -describe("resolveModel", () => { - test("uses explicit scene mapping", () => { - const config = sampleConfig(); - const r = resolveModel(config, "extract"); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.baseUrl).toBe("https://other.example/v1"); - expect(r.value.apiKey).toBe("k2"); - expect(r.value.model).toBe("foo/bar-model"); - }); - - test("falls back to models.default when scene is missing", () => { - const config = sampleConfig(); - const r = resolveModel(config, "unknown-scene"); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value.model).toBe("qwen-plus"); - expect(r.value.baseUrl).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1"); - }); - - test("errs when scene missing and no default", () => { - const config: WorkflowConfig = { - maxDepth: 1, - supervisorInterval: 3, - providers: { - p: { baseUrl: "https://x", apiKey: "k" }, - }, - models: { - extract: "p/m", - }, - }; - const r = resolveModel(config, "other"); - expect(r.ok).toBe(false); - if (r.ok) { - return; - } - expect(r.error).toContain("no model mapping"); - expect(r.error).toContain("default"); - }); - - test("errs when provider is unknown", () => { - const config: WorkflowConfig = { - maxDepth: 1, - supervisorInterval: 3, - providers: { - p: { baseUrl: "https://x", apiKey: "k" }, - }, - models: { - default: "missing/m", - }, - }; - const r = resolveModel(config, "any"); - expect(r.ok).toBe(false); - if (r.ok) { - return; - } - expect(r.error).toContain("unknown provider"); - }); - - test("errs on invalid model reference shape", () => { - const config: WorkflowConfig = { - maxDepth: 1, - supervisorInterval: 3, - providers: { - p: { baseUrl: "https://x", apiKey: "k" }, - }, - models: { - default: "no-slash-model", - }, - }; - const r = resolveModel(config, "x"); - expect(r.ok).toBe(false); - }); -}); diff --git a/packages/workflow/__tests__/result.test.ts b/packages/workflow/__tests__/result.test.ts deleted file mode 100644 index 14f3c4b..0000000 --- a/packages/workflow/__tests__/result.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { err, ok } from "../src/util/result.js"; - -describe("result helpers", () => { - test("ok wraps value", () => { - const r = ok(42); - expect(r.ok).toBe(true); - if (r.ok) { - expect(r.value).toBe(42); - } - }); - - test("err wraps error", () => { - const r = err("nope"); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toBe("nope"); - } - }); -}); diff --git a/packages/workflow/__tests__/storage-root.test.ts b/packages/workflow/__tests__/storage-root.test.ts deleted file mode 100644 index 9a3ca2d..0000000 --- a/packages/workflow/__tests__/storage-root.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { join } from "node:path"; - -import { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "../src/util/storage-root.js"; - -describe("getGlobalCasDir", () => { - test("joins cas segment under explicit storage root", () => { - expect(getGlobalCasDir("/tmp/wf-root")).toBe(join("/tmp/wf-root", "cas")); - }); - - test("defaults to default workflow root when storage root is undefined", () => { - expect(getGlobalCasDir(undefined)).toBe(join(getDefaultWorkflowStorageRoot(), "cas")); - }); -}); diff --git a/packages/workflow/__tests__/supervisor.test.ts b/packages/workflow/__tests__/supervisor.test.ts deleted file mode 100644 index 3591ae8..0000000 --- a/packages/workflow/__tests__/supervisor.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test"; - -import { runSupervisor } from "../src/engine/supervisor.js"; -import type { WorkflowConfig } from "../src/registry/index.js"; -import type { LogFn } from "../src/util/index.js"; - -const noopLogger: LogFn = () => {}; - -function supervisorOnlyConfig(): WorkflowConfig { - return { - maxDepth: 3, - supervisorInterval: 3, - providers: { - stub: { baseUrl: "http://127.0.0.1:9/v1", apiKey: "k" }, - }, - models: { - extract: "stub/extract-model", - supervisor: "stub/supervisor-model", - }, - }; -} - -function jsonResponse(body: Record, status = 200): Response { - return new Response(JSON.stringify(body), { - status, - headers: { "Content-Type": "application/json" }, - }); -} - -function installFetchMock(impl: (init?: RequestInit) => Promise): () => void { - const origFetch = globalThis.fetch; - globalThis.fetch = Object.assign( - async (_input: Parameters[0], init?: RequestInit) => impl(init), - { preconnect: origFetch.preconnect.bind(origFetch) }, - ) as typeof fetch; - return () => { - globalThis.fetch = origFetch; - }; -} - -describe("runSupervisor", () => { - let restoreFetch: (() => void) | null = null; - - afterEach(() => { - restoreFetch?.(); - restoreFetch = null; - }); - - test("returns continue when supervisor model cannot be resolved (no fetch)", async () => { - restoreFetch = installFetchMock(async () => { - throw new Error("fetch should not run when supervisor is not configured"); - }); - - const config: WorkflowConfig = { - maxDepth: 1, - supervisorInterval: 3, - providers: { - stub: { baseUrl: "http://127.0.0.1:9/v1", apiKey: "k" }, - }, - models: { - extract: "stub/m", - }, - }; - - const r = await runSupervisor({ - config, - prompt: "task", - recentSteps: [{ role: "planner", summary: "{}" }], - logger: noopLogger, - }); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value).toBe("continue"); - }); - - test("returns stop from structured tool call", async () => { - restoreFetch = installFetchMock(async () => - jsonResponse({ - choices: [ - { - message: { - tool_calls: [ - { - id: "t1", - type: "function", - function: { - name: "supervisor_decision", - arguments: JSON.stringify({ decision: "stop" }), - }, - }, - ], - }, - }, - ], - }), - ); - - const r = await runSupervisor({ - config: supervisorOnlyConfig(), - prompt: "do X", - recentSteps: [{ role: "a", summary: "{}" }], - logger: noopLogger, - }); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value).toBe("stop"); - }); - - test("returns continue from plain JSON content (reactor short-circuit)", async () => { - restoreFetch = installFetchMock(async () => - jsonResponse({ - choices: [{ message: { content: '{"decision":"continue"}' } }], - }), - ); - - const r = await runSupervisor({ - config: supervisorOnlyConfig(), - prompt: "do Y", - recentSteps: [], - logger: noopLogger, - }); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - expect(r.value).toBe("continue"); - }); - - test("returns err when reactor cannot validate the schema within max rounds", async () => { - restoreFetch = installFetchMock(async () => - jsonResponse({ - choices: [{ message: { content: "not-json" } }], - }), - ); - - const r = await runSupervisor({ - config: supervisorOnlyConfig(), - prompt: "p", - recentSteps: [], - logger: noopLogger, - }); - expect(r.ok).toBe(false); - }); - - test("returns err on HTTP failure", async () => { - restoreFetch = installFetchMock(async () => new Response("boom", { status: 500 })); - - const r = await runSupervisor({ - config: supervisorOnlyConfig(), - prompt: "p", - recentSteps: [], - logger: noopLogger, - }); - expect(r.ok).toBe(false); - }); -}); diff --git a/packages/workflow/__tests__/thread-jsonl-format.test.ts b/packages/workflow/__tests__/thread-jsonl-format.test.ts deleted file mode 100644 index 92fea30..0000000 --- a/packages/workflow/__tests__/thread-jsonl-format.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -describe("RFC-001 thread JSONL shapes", () => { - test("documents the `.data.jsonl` start record + role record keys", () => { - const startRecord = { - name: "solve-issue", - hash: "C9NMV6V2TQT81", - threadId: "01KQXKW18CT8G75T53R8F4G7YG", - parameters: { - prompt: "Fix the login redirect bug in #3", - options: { - maxRounds: 5, - depth: 0, - }, - }, - timestamp: 1714963200000, - }; - - const roleRecord = { - role: "planner", - contentHash: "CPHASH000000000000000001", - meta: { plan: "...", files: ["src/auth.ts"] }, - refs: [] as string[], - timestamp: 1714963201000, - }; - - expect(Object.keys(startRecord).sort()).toEqual( - ["hash", "name", "parameters", "threadId", "timestamp"].sort(), - ); - expect(Object.keys(roleRecord).sort()).toEqual( - ["contentHash", "meta", "refs", "role", "timestamp"].sort(), - ); - }); - - test("documents the `.info.jsonl` debug record keys", () => { - const infoRecord = { - tag: "4KNMR2PX", - content: "Loading workflow bundle...", - timestamp: 1714963200500, - }; - - expect(Object.keys(infoRecord).sort()).toEqual(["content", "tag", "timestamp"].sort()); - }); -}); diff --git a/packages/workflow/__tests__/thread-pause-gate.test.ts b/packages/workflow/__tests__/thread-pause-gate.test.ts deleted file mode 100644 index 9b568e0..0000000 --- a/packages/workflow/__tests__/thread-pause-gate.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { createThreadPauseGate } from "../src/engine/thread-pause-gate.js"; - -describe("createThreadPauseGate", () => { - test("pause blocks awaitAfterYield until resume", async () => { - const gate = createThreadPauseGate(); - gate.pause(); - - let progressed = false; - const wait = (async () => { - await gate.awaitAfterYield(); - progressed = true; - })(); - - await new Promise((r) => setTimeout(r, 30)); - expect(progressed).toBe(false); - - gate.resume(); - await wait; - expect(progressed).toBe(true); - }); - - test("duplicate pause and resume are rejected", () => { - const gate = createThreadPauseGate(); - expect(gate.pause().ok).toBe(true); - expect(gate.pause().ok).toBe(false); - expect(gate.resume().ok).toBe(true); - expect(gate.resume().ok).toBe(false); - }); -}); diff --git a/packages/workflow/__tests__/thread-reactor.test.ts b/packages/workflow/__tests__/thread-reactor.test.ts deleted file mode 100644 index c880df1..0000000 --- a/packages/workflow/__tests__/thread-reactor.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -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 { extractFunctionToolFromZodSchema } from "../src/extract/llm-extract.js"; -import { createLlmFn, createThreadReactor } from "../src/reactor/index.js"; - -const metaSchema = z.object({ seen: z.string() }); - -const provider: LlmProvider = { - baseUrl: "http://127.0.0.1:9", - apiKey: "test", - model: "test", -}; - -const CAS_GET_TOOL_DEFINITION = { - type: "function" as const, - function: { - name: "cas_get", - description: "Read CAS node", - parameters: { - type: "object", - properties: { - hash: { type: "string", description: "hash" }, - }, - required: ["hash"], - }, - }, -}; - -type ThreadCtx = { cas: ReturnType }; - -function createTestReactor() { - const llm = createLlmFn(provider); - return createThreadReactor({ - llm, - maxRounds: 10, - staticTools: [CAS_GET_TOOL_DEFINITION], - structuredToolFromSchema: (schema) => { - const t = extractFunctionToolFromZodSchema(schema); - return { - name: t.name, - tool: { - type: "function" as const, - function: { - name: t.name, - description: t.description, - parameters: t.parameters, - }, - }, - }; - }, - systemPromptForStructuredTool: (structuredToolName) => - `Extract metadata. Use cas_get when needed. Call ${structuredToolName} with JSON args matching the schema, or reply with plain JSON.`, - toolHandler: async (call, thread) => { - if (call.function.name !== "cas_get") { - return `unexpected tool ${call.function.name}`; - } - const ta = JSON.parse(call.function.arguments) as { hash: string }; - const blob = await thread.cas.get(ta.hash); - return blob === null ? "null" : blob; - }, - }); -} - -describe("createThreadReactor (extract-shaped)", () => { - let restoreFetch: (() => void) | null = null; - - afterEach(() => { - restoreFetch?.(); - restoreFetch = null; - }); - - test("cas_get rounds then extract tool yields validated meta", async () => { - const casDir = await mkdtemp(join(tmpdir(), "thread-reactor-")); - const cas = createCasStore(casDir); - try { - const blob = serializeMerkleNode(createContentMerkleNode("needle")); - const h = await cas.put(blob); - - const origFetch = globalThis.fetch; - let round = 0; - restoreFetch = () => { - globalThis.fetch = origFetch; - }; - globalThis.fetch = Object.assign( - async (_input: Parameters[0], _init?: RequestInit) => { - round += 1; - if (round === 1) { - return new Response( - JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - id: "t1", - type: "function", - function: { - name: "cas_get", - arguments: JSON.stringify({ hash: h }), - }, - }, - ], - }, - }, - ], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return new Response( - JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - id: "t2", - type: "function", - function: { - name: "extract", - arguments: JSON.stringify({ seen: "needle" }), - }, - }, - ], - }, - }, - ], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }, - { preconnect: origFetch.preconnect.bind(origFetch) }, - ) as typeof fetch; - - const reactor = createTestReactor(); - const text = `## Agent Output\n${h}\n## Extraction Instruction\nExtract seen from CAS.`; - const result = await reactor({ - thread: { cas }, - input: text, - schema: metaSchema, - }); - - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } - expect(result.value).toEqual({ seen: "needle" }); - expect(round).toBe(2); - } finally { - await rm(casDir, { recursive: true, force: true }); - } - }); - - test("stops after max tool rounds when model keeps calling cas_get", async () => { - const casDir = await mkdtemp(join(tmpdir(), "thread-reactor-max-")); - const cas = createCasStore(casDir); - try { - const blob = serializeMerkleNode(createContentMerkleNode("x")); - const h = await cas.put(blob); - - const origFetch = globalThis.fetch; - let round = 0; - restoreFetch = () => { - globalThis.fetch = origFetch; - }; - globalThis.fetch = Object.assign( - async (_input: Parameters[0], _init?: RequestInit) => { - round += 1; - return new Response( - JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - id: `loop-${round}`, - type: "function", - function: { - name: "cas_get", - arguments: JSON.stringify({ hash: h }), - }, - }, - ], - }, - }, - ], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }, - { preconnect: origFetch.preconnect.bind(origFetch) }, - ) as typeof fetch; - - const reactor = createTestReactor(); - const result = await reactor({ - thread: { cas }, - input: "## Agent Output\nnoop\n## Extraction Instruction\nExtract seen.", - schema: metaSchema, - }); - - expect(result.ok).toBe(false); - if (result.ok) { - return; - } - expect(result.error).toBe("max_react_rounds_exceeded"); - expect(round).toBe(10); - } finally { - await rm(casDir, { recursive: true, force: true }); - } - }); - - test("passthrough JSON assistant message without tool calls", async () => { - const casDir = await mkdtemp(join(tmpdir(), "thread-reactor-pass-")); - const cas = createCasStore(casDir); - try { - const origFetch = globalThis.fetch; - restoreFetch = () => { - globalThis.fetch = origFetch; - }; - globalThis.fetch = Object.assign( - async (_input: Parameters[0], _init?: RequestInit) => - new Response( - JSON.stringify({ - choices: [ - { - message: { - content: '{"seen":"direct"}', - }, - }, - ], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - { preconnect: origFetch.preconnect.bind(origFetch) }, - ) as typeof fetch; - - const reactor = createTestReactor(); - const result = await reactor({ - thread: { cas }, - input: "## Agent Output\nok\n## Extraction Instruction\nExtract.", - schema: metaSchema, - }); - - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } - expect(result.value).toEqual({ seen: "direct" }); - } finally { - await rm(casDir, { recursive: true, force: true }); - } - }); -}); diff --git a/packages/workflow/__tests__/ulid.test.ts b/packages/workflow/__tests__/ulid.test.ts deleted file mode 100644 index 7608cc3..0000000 --- a/packages/workflow/__tests__/ulid.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { decodeCrockfordBase32Bits } from "../src/util/base32.js"; -import { generateUlid } from "../src/util/ulid.js"; - -describe("generateUlid", () => { - test("length and decodable Crockford payload", () => { - const id = generateUlid(1_714_963_200_000); - expect(id.length).toBe(26); - const decoded = decodeCrockfordBase32Bits(id, 128); - expect(decoded.ok).toBe(true); - }); - - test("embeds 48-bit timestamp at the MSB of the 128-bit payload", () => { - const ts = 9_999_888_777_666; - const id = generateUlid(ts); - const decoded = decodeCrockfordBase32Bits(id, 128); - expect(decoded.ok).toBe(true); - if (decoded.ok) { - const recoveredMs = decoded.value >> 80n; - expect(Number(recoveredMs)).toBe(ts); - } - }); - - test("rejects out-of-range timestamps", () => { - expect(() => generateUlid(-1)).toThrow(); - expect(() => generateUlid(2 ** 48)).toThrow(); - }); -}); diff --git a/packages/workflow/__tests__/worker.test.ts b/packages/workflow/__tests__/worker.test.ts deleted file mode 100644 index 07b353b..0000000 --- a/packages/workflow/__tests__/worker.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { spawn } from "node:child_process"; -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { createConnection } from "node:net"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { createCasStore } from "../src/cas/cas.js"; -import { createContentMerkleNode, serializeMerkleNode } from "../src/cas/merkle.js"; -import { getWorkerHostScriptPath } from "../src/engine/worker-entry-path.js"; - -const WORKER_REGISTRY_YAML = `config: - maxDepth: 3 - providers: - stub: - baseUrl: http://127.0.0.1:9 - apiKey: test - models: - default: stub/model -workflows: {} -`; - -const bundleSource = `import { putContentMerkleNode } from "@uncaged/workflow"; - -export const descriptor = { - description: "worker-test", - roles: { - planner: { description: "planner", schema: {} }, - coder: { description: "coder", schema: {} }, - }, -}; -export const run = async function* (input, options) { - const cas = options.cas; - const has = (r) => input.steps.some((s) => s.role === r); - if (!has("planner")) { - const h = await putContentMerkleNode(cas, "p"); - yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] }; - } - if (!has("coder")) { - const h = await putContentMerkleNode(cas, "c"); - yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] }; - } - return { returnCode: 0, summary: "completed: moderator returned END" }; -}; -`; - -async function readReadyPort(child: import("node:child_process").ChildProcess): Promise { - return await new Promise((resolve, reject) => { - if (child.stdout === null) { - reject(new Error("missing stdout")); - return; - } - - let buf = ""; - function cleanup(): void { - child.stdout?.off("data", onData); - child.off("exit", onExit); - } - - function onData(chunk: Buffer): void { - buf += chunk.toString("utf8"); - const nl = buf.indexOf("\n"); - if (nl < 0) { - return; - } - cleanup(); - const line = buf.slice(0, nl).trim(); - const prefix = "READY "; - if (!line.startsWith(prefix)) { - reject(new Error(`unexpected READY line: ${line}`)); - return; - } - resolve(Number(line.slice(prefix.length))); - } - - function onExit(code: number | null): void { - cleanup(); - reject(new Error(`worker exited before READY (code ${code})`)); - } - - child.stdout.on("data", onData); - child.on("exit", onExit); - }); -} - -async function sendJson(port: number, payload: unknown): Promise { - await new Promise((resolve, reject) => { - const socket = createConnection({ host: "127.0.0.1", port }, () => { - socket.write(`${JSON.stringify(payload)}\n`); - socket.end(); - }); - socket.on("error", reject); - socket.on("close", () => resolve()); - }); -} - -describe("worker process", () => { - test("loads bundle, runs a thread over TCP, then exits when idle", async () => { - const root = await mkdtemp(join(tmpdir(), "wf-worker-")); - try { - const hash = "C9NMV6V2TQT81"; - await mkdir(join(root, "bundles"), { recursive: true }); - await writeFile(join(root, "workflow.yaml"), WORKER_REGISTRY_YAML, "utf8"); - const bundlePath = join(root, "bundles", `${hash}.esm.js`); - await writeFile(bundlePath, bundleSource, "utf8"); - - const scriptPath = getWorkerHostScriptPath(); - const child = spawn(process.execPath, [scriptPath, bundlePath, root, hash], { - stdio: ["ignore", "pipe", "inherit"], - }); - - if (child.stdout === null) { - throw new Error("missing stdout"); - } - - const port = await readReadyPort(child); - - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - await sendJson(port, { - type: "run", - threadId, - workflowName: "demo-flow", - prompt: "hello", - options: { maxRounds: 5, depth: 0 }, - }); - - const exitCode: number = await new Promise((resolve) => { - child.on("exit", (code) => resolve(code ?? 1)); - }); - - expect(exitCode).toBe(0); - - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const text = await readFile(dataPath, "utf8"); - expect( - text - .trim() - .split("\n") - .filter((l) => l !== "").length, - ).toBe(4); - } finally { - await rm(root, { recursive: true, force: true }); - } - }, 15_000); - - test("run with historical steps + forkSourceThreadId replays then continues", async () => { - const root = await mkdtemp(join(tmpdir(), "wf-worker-fork-")); - try { - const hash = "C9NMV6V2TQT81"; - await mkdir(join(root, "bundles"), { recursive: true }); - await writeFile(join(root, "workflow.yaml"), WORKER_REGISTRY_YAML, "utf8"); - const bundlePath = join(root, "bundles", `${hash}.esm.js`); - await writeFile(bundlePath, bundleSource, "utf8"); - - const scriptPath = getWorkerHostScriptPath(); - const child = spawn(process.execPath, [scriptPath, bundlePath, root, hash], { - stdio: ["ignore", "pipe", "inherit"], - }); - - if (child.stdout === null) { - throw new Error("missing stdout"); - } - - const port = await readReadyPort(child); - - const cas = createCasStore(join(root, "cas")); - const plannerReplayHash = await cas.put( - serializeMerkleNode(createContentMerkleNode("p-old")), - ); - - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const srcId = "01SRCMMMMMMMMMMMMMMMMMMMM"; - await sendJson(port, { - type: "run", - threadId, - workflowName: "demo-flow", - prompt: "hello", - options: { maxRounds: 5, depth: 0 }, - steps: [ - { - role: "planner", - contentHash: plannerReplayHash, - meta: { plan: "z" }, - refs: [plannerReplayHash], - timestamp: 555, - }, - ], - forkSourceThreadId: srcId, - }); - - const exitCode: number = await new Promise((resolve) => { - child.on("exit", (code) => resolve(code ?? 1)); - }); - - expect(exitCode).toBe(0); - - const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`); - const text = await readFile(dataPath, "utf8"); - const lines = text - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(lines.length).toBe(4); - const start = JSON.parse(lines[0] ?? "{}") as Record; - expect(start.forkFrom).toEqual({ threadId: srcId }); - const replay = JSON.parse(lines[1] ?? "{}") as Record; - expect(replay.role).toBe("planner"); - expect(replay.timestamp).toBe(555); - const coder = JSON.parse(lines[2] ?? "{}") as Record; - expect(coder.role).toBe("coder"); - const done = JSON.parse(lines[3] ?? "{}") as Record; - expect(done.returnCode).toBe(0); - } finally { - await rm(root, { recursive: true, force: true }); - } - }, 15_000); -}); diff --git a/packages/workflow/__tests__/workflow-as-agent-integration.test.ts b/packages/workflow/__tests__/workflow-as-agent-integration.test.ts deleted file mode 100644 index bb690a8..0000000 --- a/packages/workflow/__tests__/workflow-as-agent-integration.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -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"; -import { createWorkflow } from "../src/engine/create-workflow.js"; -import { executeThread } from "../src/engine/engine.js"; -import { - readWorkflowRegistry, - registerWorkflowVersion, - writeWorkflowRegistry, -} from "../src/registry/registry.js"; -import { createLogger } from "../src/util/logger.js"; -import { workflowAsAgent } from "../src/workflow-as-agent.js"; - -const callerMetaSchema = z.object({ done: z.literal(true) }); - -type ParentMeta = { - caller: z.infer; -}; - -function installMockChatCompletions(sequence: ReadonlyArray>): () => void { - const origFetch = globalThis.fetch; - let i = 0; - const mockFetch = async ( - _input: Parameters[0], - _init?: RequestInit, - ): Promise => { - const args = sequence[i] ?? sequence[sequence.length - 1]; - if (args === undefined) { - throw new Error("installMockChatCompletions: empty sequence"); - } - i += 1; - return new Response( - JSON.stringify({ - choices: [{ message: { content: JSON.stringify(args) } }], - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }; - globalThis.fetch = Object.assign(mockFetch, { - preconnect: origFetch.preconnect.bind(origFetch), - }) as typeof fetch; - return () => { - globalThis.fetch = origFetch; - }; -} - -const PARENT_REGISTRY_WITH_CONFIG = `config: - maxDepth: 3 - providers: - stub: - baseUrl: http://127.0.0.1:9 - apiKey: test - models: - default: stub/m -workflows: {} -`; - -const childBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow"; - -export const descriptor = { - description: "child-integration", - roles: { - agent: { - description: "agent", - schema: { type: "object", properties: {}, additionalProperties: true }, - }, - }, -}; -export async function* run(thread, runtime) { - const cas = runtime.cas; - const h = await putContentMerkleNode(cas, "child-body"); - yield { role: "agent", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "child-done:" + thread.start.content }; -} -`; - -async function installChildWorkflow(storageRoot: string): Promise<{ hash: string }> { - const bytes = new TextEncoder().encode(childBundleSource); - const hash = hashWorkflowBundleBytes(bytes); - await mkdir(join(storageRoot, "bundles"), { recursive: true }); - await writeFile(join(storageRoot, "bundles", `${hash}.esm.js`), childBundleSource, "utf8"); - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - throw reg.error; - } - const next = registerWorkflowVersion(reg.value, "child-wf", hash, Date.now()); - const wr = await writeWorkflowRegistry(storageRoot, next); - if (!wr.ok) { - throw wr.error; - } - return { hash }; -} - -describe("workflowAsAgent integration", () => { - let restoreFetch: (() => void) | null = null; - - afterEach(() => { - restoreFetch?.(); - restoreFetch = null; - }); - - test("createWorkflow parent invokes nested workflow via workflowAsAgent", async () => { - restoreFetch = installMockChatCompletions([{ done: true }]); - - const root = await mkdtemp(join(tmpdir(), "wf-waa-int-")); - try { - await mkdir(root, { recursive: true }); - await writeFile(join(root, "workflow.yaml"), PARENT_REGISTRY_WITH_CONFIG, "utf8"); - const { hash: childHash } = await installChildWorkflow(root); - - const parentWorkflow = createWorkflow( - { - roles: { - caller: { - description: "delegates to child workflow", - systemPrompt: "system", - extractPrompt: "extract done flag", - schema: callerMetaSchema, - extractRefs: null, - }, - }, - moderator: (ctx) => (ctx.steps.length === 0 ? "caller" : END), - }, - { agent: workflowAsAgent("child-wf", { storageRoot: root }), overrides: null }, - ); - - const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; - const parentHash = "C9NMV6V2TQT81"; - const dataPath = join(root, "logs", parentHash, `${threadId}.data.jsonl`); - const infoPath = join(root, "logs", parentHash, `${threadId}.info.jsonl`); - await mkdir(join(root, "logs", parentHash), { recursive: true }); - const cas = createCasStore(join(root, "cas")); - - const logger = createLogger({ sink: { kind: "file", path: infoPath } }); - const ac = new AbortController(); - - const result = await executeThread( - parentWorkflow, - "parent-wf", - { prompt: "from-parent", steps: [] }, - { - maxRounds: 5, - depth: 0, - signal: ac.signal, - awaitAfterEachYield: async () => {}, - forkSourceThreadId: null, - prefilledDiskSteps: null, - storageRoot: root, - }, - { threadId, hash: parentHash, dataJsonlPath: dataPath, infoJsonlPath: infoPath, cas }, - logger, - ); - - expect(result.returnCode).toBe(0); - expect(typeof result.rootHash).toBe("string"); - - const parentText = await readFile(dataPath, "utf8"); - const parentLines = parentText - .trim() - .split("\n") - .filter((l) => l !== ""); - expect(parentLines.length).toBe(2); - const callerLine = JSON.parse(parentLines[1] ?? "{}") as Record; - expect(callerLine.role).toBe("caller"); - const childRootHash = await getContentMerklePayload(cas, String(callerLine.contentHash)); - expect(childRootHash).not.toBeNull(); - const childThreadYaml = await cas.get(childRootHash ?? ""); - expect(childThreadYaml).not.toBeNull(); - const childThreadNode = parseMerkleNode(childThreadYaml ?? ""); - expect(childThreadNode.type).toBe("thread"); - const childPayload = childThreadNode.payload as Record; - expect(childPayload.workflow).toBe("child-wf"); - const childResult = childPayload.result as Record; - expect(childResult.summary).toBe("child-done:from-parent"); - - const childDir = join(root, "logs", childHash); - const childFiles = await readdir(childDir); - const childDataName = childFiles.find((n) => n.endsWith(".data.jsonl")); - expect(childDataName).toBeDefined(); - - const childText = await readFile(join(childDir, childDataName ?? ""), "utf8"); - const childStart = JSON.parse( - childText - .trim() - .split("\n") - .filter((l) => l !== "")[0] ?? "{}", - ) as Record; - expect(childStart.forkFrom).toEqual({ threadId }); - const childOpts = (childStart.parameters as Record).options as Record< - string, - unknown - >; - expect(childOpts.depth).toBe(1); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -}); diff --git a/packages/workflow/__tests__/workflow-as-agent.test.ts b/packages/workflow/__tests__/workflow-as-agent.test.ts deleted file mode 100644 index 18219e1..0000000 --- a/packages/workflow/__tests__/workflow-as-agent.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -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"; -import { - readWorkflowRegistry, - registerWorkflowVersion, - writeWorkflowRegistry, -} from "../src/registry/registry.js"; -import { workflowAsAgent } from "../src/workflow-as-agent.js"; - -function makeAgentCtx(params: { - storageRoot: string; - depth: number; - prompt: string; - maxRounds: number; -}): AgentContext { - const ts = Date.now(); - return { - threadId: "01PARENT000000000000000001AA", - depth: params.depth, - start: { - role: START, - content: params.prompt, - meta: { maxRounds: params.maxRounds }, - timestamp: ts, - }, - steps: [], - currentRole: { - name: "caller", - systemPrompt: "caller", - }, - }; -} - -const childBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow"; - -export const descriptor = { - description: "child-test", - roles: { - agent: { - description: "agent", - schema: { type: "object", properties: {}, additionalProperties: true }, - }, - }, -}; -export async function* run(thread, runtime) { - const cas = runtime.cas; - const h = await putContentMerkleNode(cas, "child-body"); - yield { role: "agent", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "child-done:" + thread.start.content }; -} -`; - -async function installChildWorkflow(storageRoot: string): Promise<{ hash: string }> { - const bytes = new TextEncoder().encode(childBundleSource); - const hash = hashWorkflowBundleBytes(bytes); - await mkdir(join(storageRoot, "bundles"), { recursive: true }); - await writeFile(join(storageRoot, "bundles", `${hash}.esm.js`), childBundleSource, "utf8"); - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - throw reg.error; - } - const next = registerWorkflowVersion(reg.value, "child-wf", hash, Date.now()); - const wr = await writeWorkflowRegistry(storageRoot, next); - if (!wr.ok) { - throw wr.error; - } - return { hash }; -} - -describe("workflowAsAgent", () => { - test("returns error when workflow name is not registered", async () => { - const root = await mkdtemp(join(tmpdir(), "wf-waa-missing-")); - try { - const agent = workflowAsAgent("missing-wf", { storageRoot: root }); - const out = await agent( - makeAgentCtx({ storageRoot: root, depth: 0, prompt: "x", maxRounds: 5 }), - ); - expect(out).toContain("not found in registry"); - expect(out).toContain("missing-wf"); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("runs registered workflow and returns child thread root CAS hash", async () => { - const root = await mkdtemp(join(tmpdir(), "wf-waa-ok-")); - try { - await mkdir(root, { recursive: true }); - await writeFile( - join(root, "workflow.yaml"), - `config: - maxDepth: 3 - providers: - stub: - baseUrl: http://127.0.0.1:9 - apiKey: test - models: - default: stub/m -workflows: {} -`, - "utf8", - ); - await installChildWorkflow(root); - const agent = workflowAsAgent("child-wf", { storageRoot: root }); - const out = await agent( - makeAgentCtx({ storageRoot: root, depth: 0, prompt: "hello-parent", maxRounds: 5 }), - ); - const cas = createCasStore(join(root, "cas")); - const threadYaml = await cas.get(out); - expect(threadYaml).not.toBeNull(); - const node = parseMerkleNode(threadYaml ?? ""); - expect(node.type).toBe("thread"); - const payload = node.payload as Record; - expect(payload.workflow).toBe("child-wf"); - const resultObj = payload.result as Record; - expect(resultObj.summary).toBe("child-done:hello-parent"); - expect(node.children.length).toBe(1); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("enforces depth limit (returns error string, does not throw)", async () => { - const root = await mkdtemp(join(tmpdir(), "wf-waa-depth-")); - try { - const agent = workflowAsAgent("child-wf", { storageRoot: root }); - const out = await agent( - makeAgentCtx({ storageRoot: root, depth: 3, prompt: "x", maxRounds: 5 }), - ); - expect(out).toContain("depth limit"); - expect(out).toContain("max 3"); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); - - test("uses registry config maxDepth when set", async () => { - const root = await mkdtemp(join(tmpdir(), "wf-waa-maxdepth-cfg-")); - try { - await installChildWorkflow(root); - const reg = await readWorkflowRegistry(root); - expect(reg.ok).toBe(true); - if (!reg.ok) { - return; - } - const withCfg = { - ...reg.value, - config: { - maxDepth: 2, - supervisorInterval: 3, - providers: { - local: { - baseUrl: "http://127.0.0.1:9", - apiKey: "k", - }, - }, - models: { - default: "local/m", - extract: "local/m", - }, - }, - }; - const wr = await writeWorkflowRegistry(root, withCfg); - expect(wr.ok).toBe(true); - - const agent = workflowAsAgent("child-wf", { storageRoot: root }); - const okOut = await agent( - makeAgentCtx({ storageRoot: root, depth: 1, prompt: "nest-once", maxRounds: 5 }), - ); - expect(okOut).not.toContain("depth limit"); - - const badOut = await agent( - makeAgentCtx({ storageRoot: root, depth: 2, prompt: "x", maxRounds: 5 }), - ); - expect(badOut).toContain("depth limit"); - expect(badOut).toContain("max 2"); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -}); diff --git a/packages/workflow/__tests__/workflow-descriptor.test.ts b/packages/workflow/__tests__/workflow-descriptor.test.ts deleted file mode 100644 index fd4708d..0000000 --- a/packages/workflow/__tests__/workflow-descriptor.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { validateWorkflowDescriptor } from "../src/bundle/workflow-descriptor.js"; - -describe("validateWorkflowDescriptor", () => { - // 1. Valid minimal descriptor - test("accepts a minimal descriptor with empty roles", () => { - const result = validateWorkflowDescriptor({ description: "x", roles: {} }); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.value.description).toBe("x"); - expect(result.value.roles).toEqual({}); - } - }); - - // 2. Valid descriptor with one role - test("accepts a descriptor with one role", () => { - const result = validateWorkflowDescriptor({ - description: "workflow", - roles: { - solver: { description: "solves things", schema: { type: "object" } }, - }, - }); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.value.description).toBe("workflow"); - expect(result.value.roles.solver.description).toBe("solves things"); - expect(result.value.roles.solver.schema).toEqual({ type: "object" }); - } - }); - - // 3. Valid descriptor with multiple roles - test("accepts a descriptor with multiple roles", () => { - const result = validateWorkflowDescriptor({ - description: "multi", - roles: { - a: { description: "role a", schema: {} }, - b: { description: "role b", schema: { type: "string" } }, - }, - }); - expect(result.ok).toBe(true); - if (result.ok) { - expect(Object.keys(result.value.roles)).toEqual(["a", "b"]); - } - }); - - // 4-6. Root is null / array / string / number / undefined - test("rejects null", () => { - const result = validateWorkflowDescriptor(null); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); - }); - - test("rejects an array", () => { - const result = validateWorkflowDescriptor([]); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); - }); - - test("rejects a string", () => { - const result = validateWorkflowDescriptor("hello"); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); - }); - - test("rejects a number", () => { - const result = validateWorkflowDescriptor(42); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); - }); - - test("rejects undefined", () => { - const result = validateWorkflowDescriptor(undefined); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); - }); - - // 7-8. Missing or non-string description - test("rejects missing description", () => { - const result = validateWorkflowDescriptor({ roles: {} }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.description must be a string"); - }); - - test("rejects numeric description", () => { - const result = validateWorkflowDescriptor({ description: 123, roles: {} }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.description must be a string"); - }); - - test("rejects null description", () => { - const result = validateWorkflowDescriptor({ description: null, roles: {} }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.description must be a string"); - }); - - test("rejects boolean description", () => { - const result = validateWorkflowDescriptor({ description: true, roles: {} }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.description must be a string"); - }); - - // 9-11. Missing / null / array roles - test("rejects missing roles", () => { - const result = validateWorkflowDescriptor({ description: "x" }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.roles must be a non-array object"); - }); - - test("rejects null roles", () => { - const result = validateWorkflowDescriptor({ description: "x", roles: null }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.roles must be a non-array object"); - }); - - test("rejects array roles", () => { - const result = validateWorkflowDescriptor({ description: "x", roles: [] }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.roles must be a non-array object"); - }); - - // 12-13. Role entry is null / array - test("rejects null role entry", () => { - const result = validateWorkflowDescriptor({ description: "x", roles: { bad: null } }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.roles.bad must be a non-array object"); - }); - - test("rejects array role entry", () => { - const result = validateWorkflowDescriptor({ description: "x", roles: { bad: [] } }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.roles.bad must be a non-array object"); - }); - - // 14-15. Role missing description / non-string description - test("rejects role with missing description", () => { - const result = validateWorkflowDescriptor({ - description: "x", - roles: { r: { schema: {} } }, - }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.roles.r.description must be a string"); - }); - - test("rejects role with non-string description", () => { - const result = validateWorkflowDescriptor({ - description: "x", - roles: { r: { description: 99, schema: {} } }, - }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.roles.r.description must be a string"); - }); - - // 16-18. Role schema null / array / missing - test("rejects role with null schema", () => { - const result = validateWorkflowDescriptor({ - description: "x", - roles: { r: { description: "d", schema: null } }, - }); - expect(result.ok).toBe(false); - if (!result.ok) - expect(result.error).toBe("descriptor.roles.r.schema must be a non-array object"); - }); - - test("rejects role with array schema", () => { - const result = validateWorkflowDescriptor({ - description: "x", - roles: { r: { description: "d", schema: [] } }, - }); - expect(result.ok).toBe(false); - if (!result.ok) - expect(result.error).toBe("descriptor.roles.r.schema must be a non-array object"); - }); - - test("rejects role with missing schema", () => { - const result = validateWorkflowDescriptor({ - description: "x", - roles: { r: { description: "d" } }, - }); - expect(result.ok).toBe(false); - if (!result.ok) - expect(result.error).toBe("descriptor.roles.r.schema must be a non-array object"); - }); - - // 19. First role valid, second role invalid - test("rejects at first invalid role when earlier roles are valid", () => { - const result = validateWorkflowDescriptor({ - description: "x", - roles: { - good: { description: "ok", schema: {} }, - bad: { description: 123, schema: {} }, - }, - }); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toBe("descriptor.roles.bad.description must be a string"); - }); -}); diff --git a/packages/workflow/package.json b/packages/workflow/package.json deleted file mode 100644 index 3cce1c0..0000000 --- a/packages/workflow/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@uncaged/workflow", - "version": "0.2.0", - "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", - "scripts": { - "test": "bun test" - }, - "dependencies": { - "@uncaged/workflow-runtime": "workspace:*", - "acorn": "^8.16.0", - "xxhashjs": "^0.2.2", - "yaml": "^2.8.4" - }, - "peerDependencies": { - "zod": "^4.0.0" - }, - "devDependencies": { - "@types/acorn": "^6.0.4", - "zod": "^4.0.0" - } -} diff --git a/packages/workflow/src/bundle/ensure-uncaged-workflow-symlink.ts b/packages/workflow/src/bundle/ensure-uncaged-workflow-symlink.ts deleted file mode 100644 index 247444b..0000000 --- a/packages/workflow/src/bundle/ensure-uncaged-workflow-symlink.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { mkdir, readlink, symlink, unlink } from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -/** This module lives in `@uncaged/workflow/src/bundle`; grandparent dir is the package root. */ -function installedWorkflowPackageDir(): string { - return fileURLToPath(new URL("../..", import.meta.url)); -} - -/** - * Ensures `/node_modules/@uncaged/workflow` points at the installed `@uncaged/workflow` - * package so workflow bundles loaded from `/bundles/*.esm.js` can resolve `import "@uncaged/workflow"`. - */ -export async function ensureUncagedWorkflowSymlink(storageRoot: string): Promise { - const target = installedWorkflowPackageDir(); - const linkDir = path.join(storageRoot, "node_modules", "@uncaged"); - const linkPath = path.join(linkDir, "workflow"); - await mkdir(linkDir, { recursive: true }); - - try { - const existing = await readlink(linkPath); - const normalizedExisting = path.resolve(linkDir, existing); - if (normalizedExisting === target) { - return; - } - await unlink(linkPath); - } catch (e) { - const errObj = e as NodeJS.ErrnoException; - if (errObj.code !== "ENOENT" && errObj.code !== "EINVAL") { - throw e; - } - } - - const linkType = process.platform === "win32" ? "junction" : "dir"; - await symlink(target, linkPath, linkType); -} diff --git a/packages/workflow/src/config/types.ts b/packages/workflow/src/config/types.ts deleted file mode 100644 index b65d1e8..0000000 --- a/packages/workflow/src/config/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type ProviderConfig = { - baseUrl: string; - apiKey: string; -}; - -export type ResolvedModel = { - baseUrl: string; - apiKey: string; - model: string; -}; diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts deleted file mode 100644 index 896f865..0000000 --- a/packages/workflow/src/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -export { - buildDescriptor, - type ExtractedBundleExports, - extractBundleExports, - stringifyWorkflowDescriptor, - validateWorkflowBundle, - validateWorkflowDescriptor, - type WorkflowBundleValidationInput, - type WorkflowDescriptor, - type WorkflowRoleDescriptor, - type WorkflowRoleSchema, -} from "./bundle/index.js"; -export { - type CasStore, - createCasStore, - createContentMerkleNode, - getContentMerklePayload, - hashString, - hashWorkflowBundleBytes, - type MerkleNode, - type MerkleNodeType, - parseMerkleNode, - putContentMerkleNode, - putStepMerkleNode, - putThreadMerkleNode, - type StepMerklePayload, - serializeMerkleNode, - type ThreadMerklePayload, -} from "./cas/index.js"; -export { - type ProviderConfig, - type ResolvedModel, - resolveModel, -} from "./config/index.js"; -export { - buildForkPlan, - createThreadPauseGate, - createWorkflow, - type ExecuteThreadIo, - type ExecuteThreadOptions, - executeThread, - type ForkHistoricalStep, - type ForkPlan, - type GcResult, - garbageCollectCas, - getWorkerHostScriptPath, - type ParsedThreadStartRecord, - type PrefilledDiskStep, - parseThreadDataJsonl, - type SupervisorDecision, - selectForkHistoricalSteps, - type ThreadPauseGate, - tryParseRoleStepRecord, - tryParseWorkflowResultRecord, -} from "./engine/index.js"; -export { - createExtract, - type ExtractFn, - type ExtractThreadContext, - type LlmError, - llmErrorToCause, - llmExtract, -} from "./extract/index.js"; -export { - type ChatMessage, - createLlmFn, - createThreadReactor, - type LlmFn, - type StructuredToolSpec, - type ThreadReactorConfig, - type ThreadReactorFn, - type ThreadReactorInvokeArgs, - type ToolCall, - type ToolDefinition, -} from "./reactor/index.js"; -export { - getRegisteredWorkflow, - listRegisteredWorkflowNames, - parseWorkflowRegistryYaml, - readWorkflowRegistry, - registerWorkflowVersion, - rollbackWorkflowToHistoryHash, - stringifyWorkflowRegistryYaml, - unregisterWorkflow, - type WorkflowConfig, - type WorkflowHistoryEntry, - type WorkflowRegistryEntry, - type WorkflowRegistryFile, - workflowRegistryPath, - writeWorkflowRegistry, -} from "./registry/index.js"; -export { - CROCKFORD_BASE32_ALPHABET, - type CreateLoggerOptions, - createLogger, - decodeCrockfordBase32Bits, - decodeCrockfordToUint64, - encodeCrockfordBase32Bits, - encodeUint64AsCrockford, - err, - generateUlid, - getDefaultWorkflowStorageRoot, - getGlobalCasDir, - type LogFn, - type LoggerSink, - ok, - type Result, -} from "./util/index.js"; -export { type WorkflowAsAgentOptions, workflowAsAgent } from "./workflow-as-agent.js"; diff --git a/packages/workflow/src/util/result.ts b/packages/workflow/src/util/result.ts deleted file mode 100644 index c6cede3..0000000 --- a/packages/workflow/src/util/result.ts +++ /dev/null @@ -1 +0,0 @@ -export { err, ok } from "@uncaged/workflow-runtime"; diff --git a/tsconfig.json b/tsconfig.json index 9257070..d2e95ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,12 @@ }, "references": [ { "path": "packages/workflow-runtime" }, - { "path": "packages/workflow" }, + { "path": "packages/workflow-protocol" }, + { "path": "packages/workflow-util" }, + { "path": "packages/workflow-cas" }, + { "path": "packages/workflow-reactor" }, + { "path": "packages/workflow-register" }, + { "path": "packages/workflow-execute" }, { "path": "packages/workflow-agent-llm" }, { "path": "packages/workflow-agent-cursor" }, { "path": "packages/workflow-agent-hermes" },