diff --git a/packages/cli-workflow/src/commands/init/workspace.ts b/packages/cli-workflow/src/commands/init/workspace.ts index 16c3f87..ba6acae 100644 --- a/packages/cli-workflow/src/commands/init/workspace.ts +++ b/packages/cli-workflow/src/commands/init/workspace.ts @@ -107,7 +107,7 @@ Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下 2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`extractPrompt\` / \`description\`。 3. **编写 Moderator**:根据 \`ctx.steps\` 与业务状态返回下一个角色名或 \`END\`。 4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / moderator 导出)。 -5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AgentFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowFnOptions\`。 +5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AgentFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowRuntime\`。 6. **构建**:打包为单个 **.esm.js** bundle,使用 **uncaged-workflow add** 注册。 ## 4. 编码规范 diff --git a/packages/workflow-agent-llm/__tests__/create-llm-adapter.test.ts b/packages/workflow-agent-llm/__tests__/create-llm-adapter.test.ts index 6459909..0065f40 100644 --- a/packages/workflow-agent-llm/__tests__/create-llm-adapter.test.ts +++ b/packages/workflow-agent-llm/__tests__/create-llm-adapter.test.ts @@ -3,14 +3,14 @@ import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createCasStore } from "@uncaged/workflow"; -import { START, type ThreadContext } from "@uncaged/workflow-runtime"; +import { START, type AgentContext } from "@uncaged/workflow-runtime"; import { createLlmAdapter } from "../src/create-llm-adapter.js"; const casDir = mkdtempSync(join(tmpdir(), "wf-llm-adapter-cas-")); const testCas = createCasStore(casDir); -function makeCtx(userContent: string): ThreadContext { +function makeCtx(userContent: string): AgentContext { return { start: { role: START, diff --git a/packages/workflow-runtime/src/engine/create-workflow.ts b/packages/workflow-runtime/src/engine/create-workflow.ts index 8be5ded..4da2983 100644 --- a/packages/workflow-runtime/src/engine/create-workflow.ts +++ b/packages/workflow-runtime/src/engine/create-workflow.ts @@ -13,11 +13,11 @@ import { type RoleOutput, type RoleStep, START, - type ThreadInput, + type ThreadContext, type WorkflowCompletion, type WorkflowDefinition, type WorkflowFn, - type WorkflowFnOptions, + type WorkflowRuntime, } from "../types.js"; import { mergeRefsWithContentHash } from "../util/index.js"; @@ -57,18 +57,12 @@ async function advanceOneRound( def: Pick, "roles" | "moderator">, binding: AgentBinding, params: { - start: ModeratorContext["start"]; - steps: RoleStep[]; - options: WorkflowFnOptions; + thread: ModeratorContext; + runtime: WorkflowRuntime; }, ): Promise> { - const { start, steps, options } = params; - const modCtx: ModeratorContext = { - threadId: options.threadId, - depth: options.depth, - start, - steps, - }; + const { thread, runtime } = params; + const modCtx: ModeratorContext = thread; const next = def.moderator(modCtx); if (!isRoleNext(next)) { @@ -86,7 +80,7 @@ async function advanceOneRound( const agentCtx: AgentContext = { ...modCtx, currentRole: { name: next, systemPrompt: roleDef.systemPrompt }, - cas: options.cas, + cas: runtime.cas, }; const agent = agentForRole(binding, next); @@ -97,13 +91,13 @@ async function advanceOneRound( agentContent: raw, }; - const meta = await options.extract( - roleDef.schema as unknown as z.ZodType>, + const meta = await runtime.extract( + roleDef.schema as z.ZodType>, roleDef.extractPrompt, extractCtx as unknown as ExtractContext, ); - const contentHash = await putContentBlob(options.cas, raw); + const contentHash = await putContentBlob(runtime.cas, raw); const refs = mergeRefsWithContentHash( resolveExtractedRefs(roleDef as unknown as RoleDefinition>, meta), contentHash, @@ -133,7 +127,7 @@ async function advanceOneRound( * Binds pure role definitions + moderator to runtime agents. * Assign with `export const run = createWorkflow(def, binding)`. * - * Structured meta extraction is delegated to {@link WorkflowFnOptions.extract}, which the + * Structured meta extraction is delegated to {@link WorkflowRuntime.extract}, which the * engine resolves from the workflow registry's `extract` scene. */ export function createWorkflow( @@ -141,38 +135,26 @@ export function createWorkflow( binding: AgentBinding, ): WorkflowFn { return async function* workflowLoop( - input: ThreadInput, - options: WorkflowFnOptions, + thread: ThreadContext, + runtime: WorkflowRuntime, ): AsyncGenerator { - const nowMs = Date.now(); - const start: ModeratorContext["start"] = { - role: START, - content: input.prompt, - meta: { maxRounds: options.maxRounds }, - timestamp: nowMs, - }; - - const baseTs = Date.now(); - let steps: RoleStep[] = input.steps.map((out, i) => ({ - role: out.role, - contentHash: out.contentHash, - meta: out.meta, - refs: out.refs, - timestamp: baseTs + i, - })) as RoleStep[]; + if (thread.start.role !== START) { + throw new Error(`workflow loop expected start role to be ${START}`); + } + const maxRounds = thread.start.meta.maxRounds; + let currentThread = thread as ModeratorContext; while (true) { - if (steps.length >= options.maxRounds) { + if (currentThread.steps.length >= maxRounds) { return { returnCode: 0, - summary: `completed: reached maxRounds (${options.maxRounds})`, + summary: `completed: reached maxRounds (${maxRounds})`, }; } const outcome = await advanceOneRound(def, binding, { - start, - steps, - options, + thread: currentThread, + runtime, }); if (outcome.kind === "complete") { @@ -180,7 +162,10 @@ export function createWorkflow( } yield outcome.output; - steps = [...steps, outcome.step]; + currentThread = { + ...currentThread, + steps: [...currentThread.steps, outcome.step], + }; } }; } diff --git a/packages/workflow-runtime/src/index.ts b/packages/workflow-runtime/src/index.ts index 9fa7afa..f853d5e 100644 --- a/packages/workflow-runtime/src/index.ts +++ b/packages/workflow-runtime/src/index.ts @@ -21,11 +21,10 @@ export type { RoleStep, StartStep, ThreadContext, - ThreadInput, WorkflowCompletion, WorkflowDefinition, WorkflowFn, - WorkflowFnOptions, + WorkflowRuntime, WorkflowResult, } from "./types.js"; export { END, START } from "./types.js"; diff --git a/packages/workflow-runtime/src/types.ts b/packages/workflow-runtime/src/types.ts index 5142874..68caefb 100644 --- a/packages/workflow-runtime/src/types.ts +++ b/packages/workflow-runtime/src/types.ts @@ -38,18 +38,8 @@ export type WorkflowResult = WorkflowCompletion & { rootHash: string; }; -/** Input to a workflow — prompt plus optional historical steps for fork/resume. */ -export type ThreadInput = { - prompt: string; - steps: RoleOutput[]; -}; - -/** Options passed to a workflow bundle's `run` export (engine-provided). */ -export type WorkflowFnOptions = { - threadId: string; - maxRounds: number; - /** Nesting depth for workflow-as-agent chains; root threads use `0`. */ - depth: number; +/** 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. */ @@ -58,8 +48,8 @@ export type WorkflowFnOptions = { /** Bundle contract — named export `run` is a function returning an AsyncGenerator. */ export type WorkflowFn = ( - input: ThreadInput, - options: WorkflowFnOptions, + thread: ThreadContext, + runtime: WorkflowRuntime, ) => AsyncGenerator; /** Engine start frame: initial prompt + thread identity. */ @@ -81,15 +71,18 @@ export type RoleStep = { }; }[keyof M & string]; -/** Phase 1: Moderator decides next role. */ -export type ModeratorContext = { +/** Thread runtime context shared by moderator/agent/extractor phases. */ +export type ThreadContext = { threadId: string; - /** Same as `WorkflowFnOptions.depth` for the active thread. */ + /** 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: { @@ -104,9 +97,6 @@ export type ExtractContext = AgentContext & { agentContent: string; }; -/** Alias — most external consumers see the agent-phase context. */ -export type ThreadContext = AgentContext; - /** Raw string output from an LLM/CLI adapter; meta is extracted by the engine. */ export type AgentFn = (ctx: AgentContext) => Promise; diff --git a/packages/workflow-template-develop/README.md b/packages/workflow-template-develop/README.md index 614d991..5b75815 100644 --- a/packages/workflow-template-develop/README.md +++ b/packages/workflow-template-develop/README.md @@ -2,7 +2,7 @@ Reference **develop** workflow template: plan phases, implement in a loop, review, test, then commit. -Export a `WorkflowDefinition` and `createDevelopRun` so a host can bind agents/LLM and run the same graph the bundled `.esm.js` would use. Use `buildDevelopDescriptor()` when assembling `descriptor` metadata for a bundle. +Export a pure `WorkflowDefinition` (`developWorkflowDefinition`) and role/moderator pieces. Workflow instantiation (`createWorkflow(definition, binding)`) happens in the workflow instance layer, not in this template package. ## Install @@ -15,10 +15,10 @@ In this monorepo: `workspace:*` for `@uncaged/workflow-template-develop` and `@u ## Usage ```typescript -import { createDevelopRun, developWorkflowDefinition } from "@uncaged/workflow-template-develop"; +import { createWorkflow } from "@uncaged/workflow"; +import { developWorkflowDefinition } from "@uncaged/workflow-template-develop"; -const run = createDevelopRun(binding); -// run(...) executes the develop moderator graph with your AgentBinding +const run = createWorkflow(developWorkflowDefinition, binding); ``` ## Roles @@ -46,7 +46,6 @@ Also exported: role factories/meta schemas (`plannerRole`, `coderRole`, …), `D | Export | Description | |--------|-------------| -| `createDevelopRun` | `createWorkflow(developWorkflowDefinition, …)` factory | | `developWorkflowDefinition` | `description`, `roles`, `developModerator` | | `developModerator` | `Moderator` | | `buildDevelopDescriptor` | `buildDescriptor({ … })` for bundle metadata | diff --git a/packages/workflow-template-develop/src/index.ts b/packages/workflow-template-develop/src/index.ts index 1f307f7..64937b3 100644 --- a/packages/workflow-template-develop/src/index.ts +++ b/packages/workflow-template-develop/src/index.ts @@ -1,5 +1,4 @@ -import { createWorkflow } from "@uncaged/workflow"; -import type { AgentBinding, WorkflowDefinition, WorkflowFn } from "@uncaged/workflow-runtime"; +import type { WorkflowDefinition } from "@uncaged/workflow-runtime"; import { developModerator } from "./moderator.js"; import { DEVELOP_WORKFLOW_DESCRIPTION, type DevelopMeta, developRoles } from "./roles.js"; @@ -36,7 +35,3 @@ export const developWorkflowDefinition: WorkflowDefinition = { roles: developRoles, moderator: developModerator, }; - -export function createDevelopRun(binding: AgentBinding): WorkflowFn { - return createWorkflow(developWorkflowDefinition, binding); -} diff --git a/packages/workflow-template-solve-issue/README.md b/packages/workflow-template-solve-issue/README.md index f5f056c..7d55e44 100644 --- a/packages/workflow-template-solve-issue/README.md +++ b/packages/workflow-template-solve-issue/README.md @@ -2,7 +2,7 @@ Reference **solve-issue** workflow template: prepare a repo, delegate implementation to the **develop** workflow, then submit (e.g. open a PR). -`createSolveIssueRun` wires the `developer` role to `workflowAsAgent("develop")` by default; `binding.overrides.developer` wins if you pass one (for tests or custom hosts). +This package exports a pure `WorkflowDefinition` (`solveIssueWorkflowDefinition`). Workflow instantiation (`createWorkflow(definition, binding)`) and any role-specific agent wiring (for example delegating `developer` to `workflowAsAgent("develop")`) are done in the workflow instance layer. ## Install @@ -15,9 +15,10 @@ In this monorepo: `workspace:*` for this package and `@uncaged/workflow`. ## Usage ```typescript -import { createSolveIssueRun, solveIssueWorkflowDefinition } from "@uncaged/workflow-template-solve-issue"; +import { createWorkflow } from "@uncaged/workflow"; +import { solveIssueWorkflowDefinition } from "@uncaged/workflow-template-solve-issue"; -const run = createSolveIssueRun(binding); +const run = createWorkflow(solveIssueWorkflowDefinition, binding); ``` ## Roles @@ -41,7 +42,6 @@ Also exported: `preparerRole`, `developerRole`, `submitterRole` and their Zod me | Export | Description | |--------|-------------| -| `createSolveIssueRun` | Merges `developer` override with `workflowAsAgent("develop")`, then `createWorkflow` | | `solveIssueWorkflowDefinition` | `description`, `roles`, `solveIssueModerator` | | `solveIssueModerator` | Linear `Moderator` | | `buildSolveIssueDescriptor` | Descriptor helper for bundles | 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 2ea355c..b38298c 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,7 +2,7 @@ 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 } from "@uncaged/workflow"; +import { createCasStore, createExtract, createWorkflow } from "@uncaged/workflow"; import { END, type ModeratorContext, @@ -12,7 +12,7 @@ import { } from "@uncaged/workflow-runtime"; import { buildSolveIssueDescriptor } from "../src/descriptor.js"; import type { DeveloperMeta } from "../src/developer.js"; -import { createSolveIssueRun, solveIssueModerator } from "../src/index.js"; +import { solveIssueModerator, solveIssueWorkflowDefinition } from "../src/index.js"; import type { PreparerMeta, SubmitterMeta } from "../src/roles/index.js"; import type { SolveIssueMeta } from "../src/roles.js"; @@ -123,6 +123,20 @@ const stubExtract = createExtract({ model: "test", }); +function makeThread(prompt: string) { + return { + threadId: "01TEST000000000000000000TR", + depth: 0, + start: { + role: START, + content: prompt, + meta: { maxRounds: 20 }, + timestamp: Date.now(), + }, + steps: [], + }; +} + describe("solveIssueModerator", () => { test("routes initial → preparer → developer → submitter → END", () => { expect(solveIssueModerator(makeCtx(20, []))).toBe("preparer"); @@ -169,7 +183,7 @@ describe("solveIssueModerator", () => { }); }); -describe("createSolveIssueRun", () => { +describe("solveIssueWorkflowDefinition + createWorkflow", () => { let restoreFetch: (() => void) | null = null; let casDir: string | undefined; @@ -199,17 +213,13 @@ describe("createSolveIssueRun", () => { casDir = await mkdtemp(join(tmpdir(), "solve-issue-cas-")); const cas = createCasStore(casDir); - // Override developer so the test does not spin up a child workflow. - const run = createSolveIssueRun({ + const run = createWorkflow(solveIssueWorkflowDefinition, { agent: async () => "", overrides: { developer: async () => "stub-root-hash" }, }); const gen = run( - { prompt: "task", steps: [] }, + makeThread("task"), { - threadId: "01TEST000000000000000000TR", - maxRounds: 20, - depth: 0, cas, extract: stubExtract, }, @@ -246,7 +256,7 @@ describe("createSolveIssueRun", () => { const cas = createCasStore(casDir); const calls: string[] = []; - const run = createSolveIssueRun({ + const run = createWorkflow(solveIssueWorkflowDefinition, { agent: async () => { calls.push("default"); return ""; @@ -267,11 +277,8 @@ describe("createSolveIssueRun", () => { }, }); const gen = run( - { prompt: "task", steps: [] }, + makeThread("task"), { - threadId: "01TEST000000000000000000TR", - maxRounds: 20, - depth: 0, cas, extract: stubExtract, }, @@ -288,56 +295,6 @@ describe("createSolveIssueRun", () => { expect(calls).toEqual(["submitter"]); }); - test("developer defaults to workflowAsAgent override (caller override still wins)", async () => { - const PREPARER_META: PreparerMeta = { - repoPath: "/tmp/r", - defaultBranch: "main", - conventions: null, - toolchain: { packageManager: null, testCommand: null, lintCommand: null, buildCommand: null }, - }; - const DEVELOPER_META: DeveloperMeta = { - branch: "feat/y", - commitSha: "def5678", - filesChanged: ["b.ts"], - summary: "more work", - }; - restoreFetch = installMockChatCompletions([PREPARER_META, DEVELOPER_META]); - - casDir = await mkdtemp(join(tmpdir(), "solve-issue-cas-")); - const cas = createCasStore(casDir); - - let developerInvocations = 0; - const run = createSolveIssueRun({ - agent: async () => "", - overrides: { - developer: async () => { - developerInvocations += 1; - return "stub-root-hash"; - }, - }, - }); - const gen = run( - { prompt: "task", steps: [] }, - { - threadId: "01TEST000000000000000000TR", - maxRounds: 20, - depth: 0, - cas, - extract: stubExtract, - }, - ); - // preparer - await gen.next(); - // developer (caller override should be invoked, NOT workflowAsAgent default) - const devYield = await gen.next(); - expect(devYield.done).toBe(false); - if (devYield.done) { - throw new Error("expected yield"); - } - expect(devYield.value.role).toBe("developer"); - expect(devYield.value.meta).toEqual(DEVELOPER_META); - expect(developerInvocations).toBe(1); - }); }); describe("buildSolveIssueDescriptor", () => { diff --git a/packages/workflow-template-solve-issue/src/index.ts b/packages/workflow-template-solve-issue/src/index.ts index c7840b7..6323d0a 100644 --- a/packages/workflow-template-solve-issue/src/index.ts +++ b/packages/workflow-template-solve-issue/src/index.ts @@ -1,5 +1,4 @@ -import { createWorkflow, workflowAsAgent } from "@uncaged/workflow"; -import type { AgentBinding, WorkflowDefinition, WorkflowFn } from "@uncaged/workflow-runtime"; +import type { WorkflowDefinition } from "@uncaged/workflow-runtime"; import { solveIssueModerator } from "./moderator.js"; import { SOLVE_ISSUE_WORKFLOW_DESCRIPTION, type SolveIssueMeta, solveIssueRoles } from "./roles.js"; @@ -31,22 +30,3 @@ export const solveIssueWorkflowDefinition: WorkflowDefinition = roles: solveIssueRoles, moderator: solveIssueModerator, }; - -/** - * Build the solve-issue {@link WorkflowFn}. - * - * The `developer` role always delegates to the registered `develop` workflow via - * {@link workflowAsAgent}; if the caller supplies their own `developer` override in - * `binding.overrides`, it takes precedence so tests and custom hosts can stub it. - */ -export function createSolveIssueRun(binding: AgentBinding): WorkflowFn { - const developerOverride = binding.overrides?.developer ?? workflowAsAgent("develop"); - const mergedBinding: AgentBinding = { - agent: binding.agent, - overrides: { - ...(binding.overrides ?? {}), - developer: developerOverride, - }, - }; - return createWorkflow(solveIssueWorkflowDefinition, mergedBinding); -} diff --git a/packages/workflow-util-agent/__tests__/build-agent-prompt.test.ts b/packages/workflow-util-agent/__tests__/build-agent-prompt.test.ts index b940551..bbd5199 100644 --- a/packages/workflow-util-agent/__tests__/build-agent-prompt.test.ts +++ b/packages/workflow-util-agent/__tests__/build-agent-prompt.test.ts @@ -3,11 +3,11 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createCasStore, putContentMerkleNode } from "@uncaged/workflow"; -import { START, type ThreadContext } from "@uncaged/workflow-runtime"; +import { START, type AgentContext } from "@uncaged/workflow-runtime"; import { buildAgentPrompt } from "../src/index.js"; -function startTask(content: string): ThreadContext["start"] { +function startTask(content: string): AgentContext["start"] { return { role: START, content, @@ -29,7 +29,7 @@ describe("buildAgentPrompt", () => { test("includes system prompt and full task; omits tools when there are no steps", async () => { const cas = createCasStore(casRoot); - const ctx: ThreadContext = { + const ctx: AgentContext = { start: startTask("fix the bug"), depth: 0, steps: [], @@ -47,7 +47,7 @@ describe("buildAgentPrompt", () => { test("single step shows full content and meta, and includes tools", async () => { const cas = createCasStore(casRoot); const onlyHash = await putContentMerkleNode(cas, "only step full body"); - const ctx: ThreadContext = { + const ctx: AgentContext = { start: startTask("user task"), depth: 0, threadId: "01TEST000000000000000000TR", @@ -77,7 +77,7 @@ describe("buildAgentPrompt", () => { const cas = createCasStore(casRoot); const plannerHash = await putContentMerkleNode(cas, "PLANNER_SECRET_FULL_TEXT"); const coderHash = await putContentMerkleNode(cas, "last step full content"); - const ctx: ThreadContext = { + const ctx: AgentContext = { start: startTask("first message full: task content here"), depth: 0, threadId: "01TEST000000000000000000TR", @@ -118,7 +118,7 @@ describe("buildAgentPrompt", () => { const ha = await putContentMerkleNode(cas, "HIDDEN_A"); const hb = await putContentMerkleNode(cas, "HIDDEN_B_MIDDLE"); const hc = await putContentMerkleNode(cas, "VISIBLE_LAST"); - const ctx: ThreadContext = { + const ctx: AgentContext = { start: startTask("start"), depth: 0, threadId: "01TEST000000000000000000TR", diff --git a/packages/workflow/__tests__/engine.test.ts b/packages/workflow/__tests__/engine.test.ts index c217e48..9dbaf6c 100644 --- a/packages/workflow/__tests__/engine.test.ts +++ b/packages/workflow/__tests__/engine.test.ts @@ -304,7 +304,7 @@ describe("executeThread", () => { } }); - test("pre-filled ThreadInput.steps skips roles already present", async () => { + test("pre-filled input.steps skips roles already present", async () => { restoreFetch = installMockChatCompletions([{ diff: "+ok" }]); const root = await mkdtemp(join(tmpdir(), "wf-engine-fork-")); diff --git a/packages/workflow/__tests__/workflow-as-agent-integration.test.ts b/packages/workflow/__tests__/workflow-as-agent-integration.test.ts index 3d55c3e..bb690a8 100644 --- a/packages/workflow/__tests__/workflow-as-agent-integration.test.ts +++ b/packages/workflow/__tests__/workflow-as-agent-integration.test.ts @@ -72,11 +72,11 @@ export const descriptor = { }, }, }; -export async function* run(input, options) { - const cas = options.cas; +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:" + input.prompt }; + return { returnCode: 0, summary: "child-done:" + thread.start.content }; } `; diff --git a/packages/workflow/__tests__/workflow-as-agent.test.ts b/packages/workflow/__tests__/workflow-as-agent.test.ts index 0002eeb..daa90bb 100644 --- a/packages/workflow/__tests__/workflow-as-agent.test.ts +++ b/packages/workflow/__tests__/workflow-as-agent.test.ts @@ -49,11 +49,11 @@ export const descriptor = { }, }, }; -export async function* run(input, options) { - const cas = options.cas; +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:" + input.prompt }; + return { returnCode: 0, summary: "child-done:" + thread.start.content }; } `; diff --git a/packages/workflow/src/engine/create-workflow.ts b/packages/workflow/src/engine/create-workflow.ts index c4628ff..b7fc2a7 100644 --- a/packages/workflow/src/engine/create-workflow.ts +++ b/packages/workflow/src/engine/create-workflow.ts @@ -2,7 +2,7 @@ * Re-export of {@link createWorkflow} from `@uncaged/workflow-runtime`. * * The runtime's `createWorkflow` already binds role definitions + agents to a workflow loop - * and delegates structured meta extraction to `WorkflowFnOptions.extract`, which the engine + * and delegates structured meta extraction to `WorkflowRuntime.extract`, which the engine * supplies (resolved from the `extract` scene in workflow.yaml). */ export { createWorkflow } from "@uncaged/workflow-runtime"; diff --git a/packages/workflow/src/engine/engine.ts b/packages/workflow/src/engine/engine.ts index 653c4cc..10b28f8 100644 --- a/packages/workflow/src/engine/engine.ts +++ b/packages/workflow/src/engine/engine.ts @@ -2,12 +2,14 @@ import { appendFile, mkdir } from "node:fs/promises"; import { dirname } from "node:path"; import type { LlmProvider, - ThreadInput, + RoleOutput, + ThreadContext, WorkflowCompletion, WorkflowFn, - WorkflowFnOptions, + WorkflowRuntime, WorkflowResult, } from "@uncaged/workflow-runtime"; +import { START } from "@uncaged/workflow-runtime"; import { type CasStore, getContentMerklePayload, @@ -103,7 +105,7 @@ async function finalizeAbortedThread(params: { async function maybeSupervisorHaltsThread(params: { workflowConfig: WorkflowConfig; - input: ThreadInput; + thread: ThreadContext; written: number; recentSupervisorSteps: readonly { role: string; summary: string }[]; logger: LogFn; @@ -118,7 +120,7 @@ async function maybeSupervisorHaltsThread(params: { } const sup = await runSupervisor({ config: params.workflowConfig, - prompt: params.input.prompt, + prompt: params.thread.start.content, recentSteps: params.recentSupervisorSteps, logger: params.logger, }); @@ -143,8 +145,8 @@ async function driveWorkflowGenerator(params: { fn: WorkflowFn; workflowName: string; workflowConfig: WorkflowConfig; - input: ThreadInput; - bundleOptions: WorkflowFnOptions; + thread: ThreadContext; + runtime: WorkflowRuntime; executeOptions: ExecuteThreadOptions; dataJsonlPath: string; threadId: string; @@ -156,8 +158,8 @@ async function driveWorkflowGenerator(params: { fn, workflowName, workflowConfig, - input, - bundleOptions, + thread, + runtime, executeOptions, dataJsonlPath, threadId, @@ -165,9 +167,9 @@ async function driveWorkflowGenerator(params: { cas, stepMerkleHashes, } = params; - const gen = fn(input, bundleOptions); + const gen = fn(thread, runtime); let written = 0; - const recentSupervisorSteps: { role: string; summary: string }[] = input.steps.map((s) => ({ + const recentSupervisorSteps: { role: string; summary: string }[] = thread.steps.map((s) => ({ role: s.role, summary: JSON.stringify(s.meta), })); @@ -267,7 +269,7 @@ async function driveWorkflowGenerator(params: { const supervised = await maybeSupervisorHaltsThread({ workflowConfig, - input, + thread, written, recentSupervisorSteps, logger, @@ -289,7 +291,7 @@ async function driveWorkflowGenerator(params: { export async function executeThread( fn: WorkflowFn, workflowName: string, - input: ThreadInput, + input: { prompt: string; steps: RoleOutput[] }, options: ExecuteThreadOptions, io: ExecuteThreadIo, logger: LogFn, @@ -371,10 +373,25 @@ export async function executeThread( throw new Error(registryRuntime.error); } - const bundleOptions: WorkflowFnOptions = { + const thread: ThreadContext = { threadId: io.threadId, - maxRounds: options.maxRounds, depth: options.depth, + start: { + role: START, + content: input.prompt, + meta: { maxRounds: options.maxRounds }, + timestamp: nowMs, + }, + steps: input.steps.map((out, i) => ({ + role: out.role, + contentHash: out.contentHash, + meta: out.meta, + refs: out.refs, + timestamp: prefilled?.[i]?.timestamp ?? nowMs + i, + })), + }; + + const runtime: WorkflowRuntime = { cas: io.cas, extract: registryRuntime.value.extract, }; @@ -383,8 +400,8 @@ export async function executeThread( fn, workflowName, workflowConfig: registryRuntime.value.workflowConfig, - input, - bundleOptions, + thread, + runtime, executeOptions: options, dataJsonlPath: io.dataJsonlPath, threadId: io.threadId, diff --git a/packages/workflow/src/engine/types.ts b/packages/workflow/src/engine/types.ts index 9228547..8788096 100644 --- a/packages/workflow/src/engine/types.ts +++ b/packages/workflow/src/engine/types.ts @@ -23,7 +23,7 @@ export type PrefilledDiskStep = { export type ExecuteThreadOptions = { maxRounds: number; - /** Passed to the bundle as `WorkflowFnOptions.depth`. */ + /** Passed to the bundle thread context as `ThreadContext.depth`. */ depth: number; signal: AbortSignal; /** Invoked after each successful yield (and outer-loop checks); used for pause/resume. */ diff --git a/packages/workflow/src/workflow-as-agent.ts b/packages/workflow/src/workflow-as-agent.ts index 86a8130..d6ac8b1 100644 --- a/packages/workflow/src/workflow-as-agent.ts +++ b/packages/workflow/src/workflow-as-agent.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { AgentContext, AgentFn, ThreadInput } from "@uncaged/workflow-runtime"; +import type { AgentContext, AgentFn } from "@uncaged/workflow-runtime"; import { extractBundleExports } from "./bundle/index.js"; import { createCasStore } from "./cas/index.js"; import type { ExecuteThreadIo } from "./engine/index.js"; @@ -36,7 +36,7 @@ function resolveWorkflowAsAgentStorageRoot(options: WorkflowAsAgentOptions | nul /** * Returns an {@link AgentFn} that runs another registered workflow in a new thread, - * using the parent thread's initial prompt (`ctx.start.content`) as the child {@link ThreadInput.prompt}. + * using the parent thread's initial prompt (`ctx.start.content`) as the child prompt. */ export function workflowAsAgent( workflowName: string, @@ -68,7 +68,7 @@ export function workflowAsAgent( return `ERROR: ${bundleExportsResult.error}`; } - const input: ThreadInput = { + const input = { prompt: ctx.start.content, steps: [], };