From 34f5e655d18a52a56d00e552eacc2e7d95a8997e Mon Sep 17 00:00:00 2001 From: Scott Wei Date: Fri, 8 May 2026 15:35:12 +0800 Subject: [PATCH 1/4] refactor(workflow): unify extraction behind ExtractFn Route createExtract through reactExtract with plain-JSON correction retry. Remove WorkflowFnOptions.llmProvider, ExtractMode, RoleDefinition.extractMode, ResolveRoleMetaFn. Runtime createWorkflow calls options.extract directly; engine passes extract only. Update templates, CLI skill docs, and tests. Co-authored-by: Cursor --- packages/cli-workflow/src/skill.ts | 1 - .../src/engine/create-workflow.ts | 21 +++---- packages/workflow-runtime/src/index.ts | 2 - packages/workflow-runtime/src/types.ts | 13 ----- packages/workflow-template-develop/README.md | 2 +- .../src/roles/coder.ts | 1 - .../src/roles/committer.ts | 1 - .../src/roles/planner.ts | 1 - .../src/roles/reviewer.ts | 1 - .../src/roles/tester.ts | 1 - .../workflow-template-solve-issue/README.md | 2 +- .../__tests__/solve-issue-template.test.ts | 58 +------------------ .../__tests__/submitter.test.ts | 3 +- .../src/developer.ts | 1 - .../src/roles/preparer.ts | 1 - .../src/roles/submitter.ts | 1 - .../__tests__/build-descriptor.test.ts | 1 - packages/workflow/__tests__/engine.test.ts | 56 ++---------------- .../workflow/__tests__/refs-tracking.test.ts | 31 +--------- .../workflow-as-agent-integration.test.ts | 31 +--------- .../workflow/src/engine/create-workflow.ts | 25 ++------ packages/workflow/src/engine/engine.ts | 4 +- .../workflow/src/engine/resolve-role-meta.ts | 42 -------------- packages/workflow/src/extract/extract-fn.ts | 11 ++-- .../workflow/src/extract/react-extract.ts | 26 ++++++++- 25 files changed, 66 insertions(+), 271 deletions(-) delete mode 100644 packages/workflow/src/engine/resolve-role-meta.ts diff --git a/packages/cli-workflow/src/skill.ts b/packages/cli-workflow/src/skill.ts index e6222bd..a697641 100644 --- a/packages/cli-workflow/src/skill.ts +++ b/packages/cli-workflow/src/skill.ts @@ -203,7 +203,6 @@ Each role has: | \`extractPrompt\` | string | Instruction for extracting structured meta | | \`schema\` | ZodSchema | Validates the extracted meta | | \`extractRefs\` | fn or null | Extracts CAS hashes from meta for DAG linking | -| \`extractMode\` | "single" | Extraction mode | ## Development Workflow diff --git a/packages/workflow-runtime/src/engine/create-workflow.ts b/packages/workflow-runtime/src/engine/create-workflow.ts index 27db1fb..8be5ded 100644 --- a/packages/workflow-runtime/src/engine/create-workflow.ts +++ b/packages/workflow-runtime/src/engine/create-workflow.ts @@ -1,3 +1,5 @@ +import type * as z from "zod/v4"; + import type { CasStore } from "../cas/types.js"; import { type AgentBinding, @@ -6,7 +8,6 @@ import { END, type ExtractContext, type ModeratorContext, - type ResolveRoleMetaFn, type RoleDefinition, type RoleMeta, type RoleOutput, @@ -55,7 +56,6 @@ type AdvanceOutcome = async function advanceOneRound( def: Pick, "roles" | "moderator">, binding: AgentBinding, - resolveRoleMeta: ResolveRoleMetaFn, params: { start: ModeratorContext["start"]; steps: RoleStep[]; @@ -97,10 +97,10 @@ async function advanceOneRound( agentContent: raw, }; - const meta = await resolveRoleMeta( - roleDef as unknown as RoleDefinition>, - extractCtx, - options, + const meta = await options.extract( + roleDef.schema as unknown as z.ZodType>, + roleDef.extractPrompt, + extractCtx as unknown as ExtractContext, ); const contentHash = await putContentBlob(options.cas, raw); @@ -131,13 +131,14 @@ async function advanceOneRound( /** * Binds pure role definitions + moderator to runtime agents. - * Assign with `export const run = createWorkflow(def, binding)` via `@uncaged/workflow-runtime`, - * which supplies {@link ResolveRoleMetaFn}. + * Assign with `export const run = createWorkflow(def, binding)`. + * + * Structured meta extraction is delegated to {@link WorkflowFnOptions.extract}, which the + * engine resolves from the workflow registry's `extract` scene. */ export function createWorkflow( def: Pick, "roles" | "moderator">, binding: AgentBinding, - resolveRoleMeta: ResolveRoleMetaFn, ): WorkflowFn { return async function* workflowLoop( input: ThreadInput, @@ -168,7 +169,7 @@ export function createWorkflow( }; } - const outcome = await advanceOneRound(def, binding, resolveRoleMeta, { + const outcome = await advanceOneRound(def, binding, { start, steps, options, diff --git a/packages/workflow-runtime/src/index.ts b/packages/workflow-runtime/src/index.ts index 481d9c6..9fa7afa 100644 --- a/packages/workflow-runtime/src/index.ts +++ b/packages/workflow-runtime/src/index.ts @@ -12,11 +12,9 @@ export type { AgentContext, AgentFn, ExtractContext, - ExtractMode, LlmProvider, Moderator, ModeratorContext, - ResolveRoleMetaFn, RoleDefinition, RoleMeta, RoleOutput, diff --git a/packages/workflow-runtime/src/types.ts b/packages/workflow-runtime/src/types.ts index 1774740..5142874 100644 --- a/packages/workflow-runtime/src/types.ts +++ b/packages/workflow-runtime/src/types.ts @@ -17,9 +17,6 @@ export type LlmProvider = { model: string; }; -/** How the engine runs meta extraction for a role after the agent phase. */ -export type ExtractMode = "single" | "react"; - /** What each generator yield produces — one role's output (engine adds `timestamp` when persisting). */ export type RoleOutput = { role: string; @@ -57,8 +54,6 @@ export type WorkflowFnOptions = { cas: CasStore; /** Structured meta extraction; resolved from workflow.yaml `extract` scene by the engine. */ extract: ExtractFn; - /** Provider for `extractMode: "react"` roles; same backing config as `extract`. */ - llmProvider: LlmProvider | null; }; /** Bundle contract — named export `run` is a function returning an AsyncGenerator. */ @@ -129,7 +124,6 @@ export type RoleDefinition> = { schema: z.ZodType; /** When non-null, produces CAS hashes to persist on this role's steps (see `RoleOutput.refs`). */ extractRefs: ((meta: Meta) => string[]) | null; - extractMode: ExtractMode; }; /** @@ -148,10 +142,3 @@ export type WorkflowDefinition = { roles: { [K in keyof M & string]: RoleDefinition }; moderator: Moderator; }; - -/** Engine-injected meta extraction for workflow loops (single + react modes). */ -export type ResolveRoleMetaFn = ( - roleDef: RoleDefinition>, - extractCtx: ExtractContext, - options: WorkflowFnOptions, -) => Promise>; diff --git a/packages/workflow-template-develop/README.md b/packages/workflow-template-develop/README.md index d43bf56..614d991 100644 --- a/packages/workflow-template-develop/README.md +++ b/packages/workflow-template-develop/README.md @@ -17,7 +17,7 @@ In this monorepo: `workspace:*` for `@uncaged/workflow-template-develop` and `@u ```typescript import { createDevelopRun, developWorkflowDefinition } from "@uncaged/workflow-template-develop"; -const run = createDevelopRun(binding, extract, llmProvider); +const run = createDevelopRun(binding); // run(...) executes the develop moderator graph with your AgentBinding ``` diff --git a/packages/workflow-template-develop/src/roles/coder.ts b/packages/workflow-template-develop/src/roles/coder.ts index a30bea1..7fd5153 100644 --- a/packages/workflow-template-develop/src/roles/coder.ts +++ b/packages/workflow-template-develop/src/roles/coder.ts @@ -31,5 +31,4 @@ export const coderRole: RoleDefinition = { "Extract completedPhase: the planner phase hash finished this round (exact hash string from the plan). If multiple phases were finished in one round, use the last finished phase hash. Extract filesChanged and a summary of the work.", schema: coderMetaSchema, extractRefs: (meta) => [meta.completedPhase], - extractMode: "single", }; diff --git a/packages/workflow-template-develop/src/roles/committer.ts b/packages/workflow-template-develop/src/roles/committer.ts index 62c35d7..760df71 100644 --- a/packages/workflow-template-develop/src/roles/committer.ts +++ b/packages/workflow-template-develop/src/roles/committer.ts @@ -32,5 +32,4 @@ export const committerRole: RoleDefinition = { "Extract the commit result: committed (with branch and SHA), recoverable failure, or unrecoverable failure. Include error details and log references if applicable.", schema: committerMetaSchema, extractRefs: null, - extractMode: "single", }; diff --git a/packages/workflow-template-develop/src/roles/planner.ts b/packages/workflow-template-develop/src/roles/planner.ts index 85119c0..395899c 100644 --- a/packages/workflow-template-develop/src/roles/planner.ts +++ b/packages/workflow-template-develop/src/roles/planner.ts @@ -48,5 +48,4 @@ export const plannerRole: RoleDefinition = { "Extract the implementation phases from the agent's output. Each phase has a hash (the CAS content-hash returned by the cas put command) and a title (one-line summary).", schema: plannerMetaSchema, extractRefs: (meta) => meta.phases.map((p) => p.hash), - extractMode: "single", }; diff --git a/packages/workflow-template-develop/src/roles/reviewer.ts b/packages/workflow-template-develop/src/roles/reviewer.ts index 5d2d27e..2276efa 100644 --- a/packages/workflow-template-develop/src/roles/reviewer.ts +++ b/packages/workflow-template-develop/src/roles/reviewer.ts @@ -41,5 +41,4 @@ export const reviewerRole: RoleDefinition = { "Extract the review verdict: approved or rejected. If rejected, list the blocking issues.", schema: reviewerMetaSchema, extractRefs: null, - extractMode: "single", }; diff --git a/packages/workflow-template-develop/src/roles/tester.ts b/packages/workflow-template-develop/src/roles/tester.ts index f912588..5fb4263 100644 --- a/packages/workflow-template-develop/src/roles/tester.ts +++ b/packages/workflow-template-develop/src/roles/tester.ts @@ -23,5 +23,4 @@ export const testerRole: RoleDefinition = { "Extract the verification result: passed with summary details, or failed with details of what broke.", schema: testerMetaSchema, extractRefs: null, - extractMode: "single", }; diff --git a/packages/workflow-template-solve-issue/README.md b/packages/workflow-template-solve-issue/README.md index 29ed002..f5f056c 100644 --- a/packages/workflow-template-solve-issue/README.md +++ b/packages/workflow-template-solve-issue/README.md @@ -17,7 +17,7 @@ In this monorepo: `workspace:*` for this package and `@uncaged/workflow`. ```typescript import { createSolveIssueRun, solveIssueWorkflowDefinition } from "@uncaged/workflow-template-solve-issue"; -const run = createSolveIssueRun(binding, extract, llmProvider); +const run = createSolveIssueRun(binding); ``` ## Roles 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 c0a2c35..2ea355c 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 @@ -23,46 +23,7 @@ function jsonResponse(payload: Record): Response { }); } -function readToolListFromBody(init: RequestInit | undefined): readonly Record[] { - if (init === undefined || init.body === undefined || init.body === null) { - return []; - } - const body = JSON.parse(String(init.body)) as Record; - const tools = body.tools; - if (!Array.isArray(tools)) { - return []; - } - return tools.filter((t): t is Record => t !== null && typeof t === "object"); -} - -function singleToolName(tools: readonly Record[]): string { - if (tools.length === 0) { - return "extract"; - } - const fn = tools[0].function as Record | undefined; - return typeof fn?.name === "string" ? fn.name : "extract"; -} - -function buildSingleModeResponse(args: Record, toolName: string): Response { - return jsonResponse({ - choices: [ - { - message: { - tool_calls: [ - { - type: "function", - function: { name: toolName, arguments: JSON.stringify(args) }, - }, - ], - }, - }, - ], - }); -} - -function buildReactModeResponse(args: Record): Response { - // reactExtract accepts a plain-JSON assistant message and validates it - // directly against the schema, so we skip the cas_get / extract tool dance. +function buildPlainJsonResponse(args: Record): Response { return jsonResponse({ choices: [{ message: { content: JSON.stringify(args) } }], }); @@ -73,18 +34,14 @@ function installMockChatCompletions(sequence: ReadonlyArray[0], - init?: RequestInit, + _init?: RequestInit, ): Promise => { const args = sequence[i] ?? sequence[sequence.length - 1]; if (args === undefined) { throw new Error("installMockChatCompletions: empty sequence"); } i += 1; - const tools = readToolListFromBody(init); - if (tools.length > 1) { - return buildReactModeResponse(args); - } - return buildSingleModeResponse(args, singleToolName(tools)); + return buildPlainJsonResponse(args); }; globalThis.fetch = Object.assign(mockFetch, { preconnect: origFetch.preconnect.bind(origFetch), @@ -166,12 +123,6 @@ const stubExtract = createExtract({ model: "test", }); -const stubLlmProvider = { - baseUrl: "http://127.0.0.1:9", - apiKey: "", - model: "test", -}; - describe("solveIssueModerator", () => { test("routes initial → preparer → developer → submitter → END", () => { expect(solveIssueModerator(makeCtx(20, []))).toBe("preparer"); @@ -261,7 +212,6 @@ describe("createSolveIssueRun", () => { depth: 0, cas, extract: stubExtract, - llmProvider: stubLlmProvider, }, ); const first = await gen.next(); @@ -324,7 +274,6 @@ describe("createSolveIssueRun", () => { depth: 0, cas, extract: stubExtract, - llmProvider: stubLlmProvider, }, ); await gen.next(); @@ -375,7 +324,6 @@ describe("createSolveIssueRun", () => { depth: 0, cas, extract: stubExtract, - llmProvider: stubLlmProvider, }, ); // preparer diff --git a/packages/workflow-template-solve-issue/__tests__/submitter.test.ts b/packages/workflow-template-solve-issue/__tests__/submitter.test.ts index 9f68d57..71c86f7 100644 --- a/packages/workflow-template-solve-issue/__tests__/submitter.test.ts +++ b/packages/workflow-template-solve-issue/__tests__/submitter.test.ts @@ -32,8 +32,7 @@ describe("submitterRole", () => { expect(submitterRole.systemPrompt).toContain("pull request"); }); - test("uses single extract mode without refs", () => { - expect(submitterRole.extractMode).toBe("single"); + test("has no refs extractor", () => { expect(submitterRole.extractRefs).toBeNull(); }); }); diff --git a/packages/workflow-template-solve-issue/src/developer.ts b/packages/workflow-template-solve-issue/src/developer.ts index ae44087..9d4e019 100644 --- a/packages/workflow-template-solve-issue/src/developer.ts +++ b/packages/workflow-template-solve-issue/src/developer.ts @@ -33,5 +33,4 @@ export const developerRole: RoleDefinition = { extractPrompt: DEVELOPER_EXTRACT_PROMPT, schema: developerMetaSchema, extractRefs: () => [], - extractMode: "react", }; diff --git a/packages/workflow-template-solve-issue/src/roles/preparer.ts b/packages/workflow-template-solve-issue/src/roles/preparer.ts index 30c7e13..3bc6535 100644 --- a/packages/workflow-template-solve-issue/src/roles/preparer.ts +++ b/packages/workflow-template-solve-issue/src/roles/preparer.ts @@ -48,5 +48,4 @@ export const preparerRole: RoleDefinition = { "Extract repoPath (absolute path), defaultBranch, conventions (summary string or null), and toolchain (packageManager, testCommand, lintCommand, buildCommand — each string or null).", schema: preparerMetaSchema, extractRefs: null, - extractMode: "single", }; diff --git a/packages/workflow-template-solve-issue/src/roles/submitter.ts b/packages/workflow-template-solve-issue/src/roles/submitter.ts index cc5aad8..89a8405 100644 --- a/packages/workflow-template-solve-issue/src/roles/submitter.ts +++ b/packages/workflow-template-solve-issue/src/roles/submitter.ts @@ -40,5 +40,4 @@ export const submitterRole: RoleDefinition = { extractPrompt: SUBMITTER_EXTRACT_PROMPT, schema: submitterMetaSchema, extractRefs: null, - extractMode: "single", }; diff --git a/packages/workflow/__tests__/build-descriptor.test.ts b/packages/workflow/__tests__/build-descriptor.test.ts index d8aea98..e60884e 100644 --- a/packages/workflow/__tests__/build-descriptor.test.ts +++ b/packages/workflow/__tests__/build-descriptor.test.ts @@ -22,7 +22,6 @@ describe("buildDescriptor", () => { extractPrompt: "Extract title and count from the analysis.", schema, extractRefs: null, - extractMode: "single", }, }, moderator: () => END, diff --git a/packages/workflow/__tests__/engine.test.ts b/packages/workflow/__tests__/engine.test.ts index 5c2aa7a..c217e48 100644 --- a/packages/workflow/__tests__/engine.test.ts +++ b/packages/workflow/__tests__/engine.test.ts @@ -33,41 +33,17 @@ function installMockChatCompletions(sequence: ReadonlyArray[0], - init?: RequestInit, + _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; - void input; - const body = init?.body ? (JSON.parse(String(init.body)) as Record) : {}; - const tools = body.tools; - const firstTool = - Array.isArray(tools) && tools.length > 0 && tools[0] !== null && typeof tools[0] === "object" - ? (tools[0] as Record) - : null; - const fn = - firstTool !== null ? (firstTool.function as Record | undefined) : undefined; - const toolName = typeof fn?.name === "string" ? fn.name : "extract"; return new Response( JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - type: "function", - function: { - name: toolName, - arguments: JSON.stringify(args), - }, - }, - ], - }, - }, - ], + choices: [{ message: { content: JSON.stringify(args) } }], }), { status: 200, headers: { "Content-Type": "application/json" } }, ); @@ -125,7 +101,7 @@ async function writeRegistryYaml(storageRoot: string, yaml: string): Promise>; supervisorContent: string; @@ -147,26 +123,9 @@ function installMockExtractThenSupervisor(params: { throw new Error("installMockExtractThenSupervisor: empty extractArgs"); } extractI += 1; - const firstTool = tools[0] as Record; - const fn = firstTool.function as Record | undefined; - const toolName = typeof fn?.name === "string" ? fn.name : "extract"; return new Response( JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - type: "function", - function: { - name: toolName, - arguments: JSON.stringify(args), - }, - }, - ], - }, - }, - ], + choices: [{ message: { content: JSON.stringify(args) } }], }), { status: 200, headers: { "Content-Type": "application/json" } }, ); @@ -196,7 +155,6 @@ const demoWorkflow = createWorkflow( extractPrompt: "Extract plan text and affected files list.", schema: plannerMetaSchema, extractRefs: null, - extractMode: "single", }, coder: { description: "Demo coder", @@ -204,7 +162,6 @@ const demoWorkflow = createWorkflow( extractPrompt: "Extract the code diff summary.", schema: coderMetaSchema, extractRefs: null, - extractMode: "single", }, }, moderator: (ctx) => { @@ -553,7 +510,7 @@ describe("executeThread", () => { } }); - test("extractMode react traverses CAS DAG via cas_get during extraction", async () => { + test("extract traverses CAS DAG via cas_get during extraction", async () => { const dagMetaSchema = z.object({ leafPayload: z.string() }); type DagDemoMeta = { walker: z.infer }; @@ -663,7 +620,6 @@ describe("executeThread", () => { "Set leafPayload to the string payload of the content Merkle node under the root.", schema: dagMetaSchema, extractRefs: null, - extractMode: "react", }, }, moderator: (ctx) => (ctx.steps.length === 0 ? "walker" : END), diff --git a/packages/workflow/__tests__/refs-tracking.test.ts b/packages/workflow/__tests__/refs-tracking.test.ts index 17804af..075689d 100644 --- a/packages/workflow/__tests__/refs-tracking.test.ts +++ b/packages/workflow/__tests__/refs-tracking.test.ts @@ -27,41 +27,17 @@ function installMockChatCompletions(sequence: ReadonlyArray[0], - init?: RequestInit, + _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; - void input; - const body = init?.body ? (JSON.parse(String(init.body)) as Record) : {}; - const tools = body.tools; - const firstTool = - Array.isArray(tools) && tools.length > 0 && tools[0] !== null && typeof tools[0] === "object" - ? (tools[0] as Record) - : null; - const fn = - firstTool !== null ? (firstTool.function as Record | undefined) : undefined; - const toolName = typeof fn?.name === "string" ? fn.name : "extract"; return new Response( JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - type: "function", - function: { - name: toolName, - arguments: JSON.stringify(args), - }, - }, - ], - }, - }, - ], + choices: [{ message: { content: JSON.stringify(args) } }], }), { status: 200, headers: { "Content-Type": "application/json" } }, ); @@ -94,7 +70,6 @@ const refsDemoWorkflow = createWorkflow( extractPrompt: "Extract phases with CAS hashes.", schema: plannerMetaSchema, extractRefs: (meta) => meta.phases.map((p) => p.hash), - extractMode: "single", }, }, moderator: (ctx) => (ctx.steps.length === 0 ? "planner" : END), diff --git a/packages/workflow/__tests__/workflow-as-agent-integration.test.ts b/packages/workflow/__tests__/workflow-as-agent-integration.test.ts index 1c1f678..3d55c3e 100644 --- a/packages/workflow/__tests__/workflow-as-agent-integration.test.ts +++ b/packages/workflow/__tests__/workflow-as-agent-integration.test.ts @@ -27,41 +27,17 @@ function installMockChatCompletions(sequence: ReadonlyArray[0], - init?: RequestInit, + _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; - void input; - const body = init?.body ? (JSON.parse(String(init.body)) as Record) : {}; - const tools = body.tools; - const firstTool = - Array.isArray(tools) && tools.length > 0 && tools[0] !== null && typeof tools[0] === "object" - ? (tools[0] as Record) - : null; - const fn = - firstTool !== null ? (firstTool.function as Record | undefined) : undefined; - const toolName = typeof fn?.name === "string" ? fn.name : "extract"; return new Response( JSON.stringify({ - choices: [ - { - message: { - tool_calls: [ - { - type: "function", - function: { - name: toolName, - arguments: JSON.stringify(args), - }, - }, - ], - }, - }, - ], + choices: [{ message: { content: JSON.stringify(args) } }], }), { status: 200, headers: { "Content-Type": "application/json" } }, ); @@ -147,7 +123,6 @@ describe("workflowAsAgent integration", () => { extractPrompt: "extract done flag", schema: callerMetaSchema, extractRefs: null, - extractMode: "single", }, }, moderator: (ctx) => (ctx.steps.length === 0 ? "caller" : END), diff --git a/packages/workflow/src/engine/create-workflow.ts b/packages/workflow/src/engine/create-workflow.ts index 8132476..c4628ff 100644 --- a/packages/workflow/src/engine/create-workflow.ts +++ b/packages/workflow/src/engine/create-workflow.ts @@ -1,21 +1,8 @@ -import type { - AgentBinding, - RoleMeta, - WorkflowDefinition, - WorkflowFn, -} from "@uncaged/workflow-runtime"; -import { createWorkflow as createWorkflowRuntime } from "@uncaged/workflow-runtime"; - -import { resolveRoleMeta } from "./resolve-role-meta.js"; - /** - * Binds pure role definitions + moderator to runtime agents. - * Assign with `export const run = createWorkflow(def, binding)`. - * The engine supplies {@link WorkflowFnOptions.extract} and {@link WorkflowFnOptions.llmProvider} from workflow.yaml. + * 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 + * supplies (resolved from the `extract` scene in workflow.yaml). */ -export function createWorkflow( - def: Pick, "roles" | "moderator">, - binding: AgentBinding, -): WorkflowFn { - return createWorkflowRuntime(def, binding, resolveRoleMeta); -} +export { createWorkflow } from "@uncaged/workflow-runtime"; diff --git a/packages/workflow/src/engine/engine.ts b/packages/workflow/src/engine/engine.ts index fd5cbbd..653c4cc 100644 --- a/packages/workflow/src/engine/engine.ts +++ b/packages/workflow/src/engine/engine.ts @@ -26,7 +26,6 @@ async function resolveEngineRegistryRuntime(storageRoot: string): Promise< Result< { extract: ReturnType; - llmProvider: LlmProvider; workflowConfig: WorkflowConfig; }, string @@ -50,7 +49,7 @@ async function resolveEngineRegistryRuntime(storageRoot: string): Promise< apiKey: ex.apiKey, model: ex.model, }; - return ok({ extract: createExtract(llmProvider), llmProvider, workflowConfig: cfg }); + return ok({ extract: createExtract(llmProvider), workflowConfig: cfg }); } async function appendDataLine(path: string, record: unknown): Promise { @@ -378,7 +377,6 @@ export async function executeThread( depth: options.depth, cas: io.cas, extract: registryRuntime.value.extract, - llmProvider: registryRuntime.value.llmProvider, }; return await driveWorkflowGenerator({ diff --git a/packages/workflow/src/engine/resolve-role-meta.ts b/packages/workflow/src/engine/resolve-role-meta.ts deleted file mode 100644 index ce1657a..0000000 --- a/packages/workflow/src/engine/resolve-role-meta.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { - ExtractContext, - RoleDefinition, - RoleMeta, - WorkflowFnOptions, -} from "@uncaged/workflow-runtime"; - -import { buildExtractUserContent } from "../extract/extract-fn.js"; -import { reactExtract } from "../extract/react-extract.js"; - -export async function resolveRoleMeta( - roleDef: RoleDefinition>, - extractCtx: ExtractContext, - options: WorkflowFnOptions, -): Promise> { - if (roleDef.extractMode === "react") { - if (options.llmProvider === null) { - throw new Error( - 'createWorkflow: WorkflowFnOptions.llmProvider is required when a role uses extractMode "react"', - ); - } - const text = await buildExtractUserContent( - extractCtx as unknown as ExtractContext, - roleDef.extractPrompt, - ); - const reactResult = await reactExtract({ - text, - schema: roleDef.schema, - provider: options.llmProvider, - cas: options.cas, - }); - if (!reactResult.ok) { - throw new Error(`react extract failed: ${reactResult.error}`); - } - return reactResult.value as Record; - } - return (await options.extract( - roleDef.schema, - roleDef.extractPrompt, - extractCtx as unknown as ExtractContext, - )) as Record; -} diff --git a/packages/workflow/src/extract/extract-fn.ts b/packages/workflow/src/extract/extract-fn.ts index 4705da8..1fc046e 100644 --- a/packages/workflow/src/extract/extract-fn.ts +++ b/packages/workflow/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 { getContentMerklePayload } from "../cas/index.js"; -import { llmExtractWithRetry } from "./llm-extract.js"; +import { reactExtract } from "./react-extract.js"; /** Builds the user-side extraction prompt (thread + agent output + instruction). */ export async function buildExtractUserContent( @@ -39,7 +39,10 @@ export async function buildExtractUserContent( /** * Create an ExtractFn backed by an LLM provider. - * Builds prompt text from {@link ExtractContext} plus `prompt` and calls structured extraction. + * + * Internally runs a multi-turn ReAct loop with two tools (`cas_get` for traversing the + * Merkle DAG and a schema-shaped `extract` tool); the loop also accepts a plain-JSON + * assistant reply as a short-circuit, which covers the legacy "single" extraction path. */ export function createExtract(provider: LlmProvider): ExtractFn { return async >( @@ -48,9 +51,9 @@ export function createExtract(provider: LlmProvider): ExtractFn { ctx: ExtractContext, ): Promise => { const text = await buildExtractUserContent(ctx, prompt); - const result = await llmExtractWithRetry({ text, schema, provider }); + const result = await reactExtract({ text, schema, provider, cas: ctx.cas }); if (!result.ok) { - throw new Error(`extract failed: ${JSON.stringify(result.error)}`); + throw new Error(`extract failed: ${result.error}`); } return result.value; }; diff --git a/packages/workflow/src/extract/react-extract.ts b/packages/workflow/src/extract/react-extract.ts index c090a06..5c0c456 100644 --- a/packages/workflow/src/extract/react-extract.ts +++ b/packages/workflow/src/extract/react-extract.ts @@ -57,6 +57,7 @@ type ChatMessage = content: string | null; tool_calls: ToolCall[]; } + | { role: "assistant"; content: string } | { role: "tool"; tool_call_id: string; content: string }; type AssistantTurn = @@ -111,10 +112,14 @@ function normalizeToolCalls(toolCallsRaw: unknown[]): Result return ok(toolCalls); } +type AssistantTurnOrCorrection> = + | AssistantTurn + | { kind: "plain_json_invalid"; rawContent: string; correction: string }; + function classifyAssistantTurn>( messageObj: Record, schema: z.ZodType, -): Result, string> { +): Result, string> { const toolCallsRaw = messageObj.tool_calls; if (!Array.isArray(toolCallsRaw) || toolCallsRaw.length === 0) { const content = messageObj.content; @@ -123,11 +128,20 @@ function classifyAssistantTurn>( } const jsonParsed = tryParseJsonContent(content); if (jsonParsed === null) { - return err("no_tool_calls_and_content_not_json"); + return ok({ + kind: "plain_json_invalid", + rawContent: content, + correction: + "Your previous reply was not valid JSON and contained no tool calls. Reply with a single JSON object that matches the schema, or call the extract tool with the structured arguments.", + }); } const validated = schema.safeParse(jsonParsed); if (!validated.success) { - return err(`schema_validation_failed:${validated.error.message}`); + return ok({ + kind: "plain_json_invalid", + rawContent: content, + correction: `Your previous JSON reply did not satisfy the schema: ${validated.error.message}. Reply again with a JSON object that matches the schema, or call the extract tool with the structured arguments.`, + }); } return ok({ kind: "plain_json", value: validated.data }); } @@ -298,6 +312,12 @@ export async function reactExtract>( return ok(turn.value); } + if (turn.kind === "plain_json_invalid") { + messages.push({ role: "assistant", content: turn.rawContent }); + messages.push({ role: "user", content: turn.correction }); + continue; + } + messages.push({ role: "assistant", content: turn.assistantContent, -- 2.43.0 From a11cc62a8113c90563a6e02643c95316c5d9f2c0 Mon Sep 17 00:00:00 2001 From: Scott Wei Date: Fri, 8 May 2026 17:00:21 +0800 Subject: [PATCH 2/4] refactor(workflow-runtime): use full ThreadContext in WorkflowFn Redefine WorkflowFn to accept a complete ThreadContext plus WorkflowRuntime dependencies, removing ThreadInput and WorkflowFnOptions. Move thread context construction into engine executeThread, update runtime loop/agent paths, and align templates/docs/tests with template-only definition exports. Co-authored-by: Cursor --- .../src/commands/init/workspace.ts | 2 +- .../__tests__/create-llm-adapter.test.ts | 4 +- .../src/engine/create-workflow.ts | 67 ++++++--------- packages/workflow-runtime/src/index.ts | 3 +- packages/workflow-runtime/src/types.ts | 30 +++---- packages/workflow-template-develop/README.md | 9 +- .../workflow-template-develop/src/index.ts | 7 +- .../workflow-template-solve-issue/README.md | 8 +- .../__tests__/solve-issue-template.test.ts | 85 +++++-------------- .../src/index.ts | 22 +---- .../__tests__/build-agent-prompt.test.ts | 12 +-- packages/workflow/__tests__/engine.test.ts | 2 +- .../workflow-as-agent-integration.test.ts | 6 +- .../__tests__/workflow-as-agent.test.ts | 6 +- .../workflow/src/engine/create-workflow.ts | 2 +- packages/workflow/src/engine/engine.ts | 49 +++++++---- packages/workflow/src/engine/types.ts | 2 +- packages/workflow/src/workflow-as-agent.ts | 6 +- 18 files changed, 122 insertions(+), 200 deletions(-) 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: [], }; -- 2.43.0 From 884ff852055ff27a26168a5081a0073ae7c09770 Mon Sep 17 00:00:00 2001 From: Scott Wei Date: Fri, 8 May 2026 17:10:31 +0800 Subject: [PATCH 3/4] refactor(workflow): remove dead extract retry export Drop unused llmExtractWithRetry implementation and public exports. Add solve-issue template coverage for tool_calls extraction path. Co-authored-by: Cursor --- .../__tests__/solve-issue-template.test.ts | 80 +++++++++++++++++++ packages/workflow/src/extract/index.ts | 1 - packages/workflow/src/extract/llm-extract.ts | 51 ------------ packages/workflow/src/index.ts | 1 - 4 files changed, 80 insertions(+), 53 deletions(-) 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 b38298c..dc2bfdb 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 @@ -51,6 +51,49 @@ function installMockChatCompletions(sequence: ReadonlyArray): Response { + return jsonResponse({ + choices: [ + { + message: { + tool_calls: [ + { + id: "tc_extract_1", + type: "function", + function: { + name: "extract", + arguments: JSON.stringify(args), + }, + }, + ], + }, + }, + ], + }); +} + +function installMockToolCallCompletions(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("installMockToolCallCompletions: empty sequence"); + } + i += 1; + return buildToolCallResponse(args); + }; + globalThis.fetch = Object.assign(mockFetch, { + preconnect: origFetch.preconnect.bind(origFetch), + }) as typeof fetch; + return () => { + globalThis.fetch = origFetch; + }; +} + function makeStart(maxRounds: number): ModeratorContext["start"] { return { role: START, @@ -233,6 +276,43 @@ describe("solveIssueWorkflowDefinition + createWorkflow", () => { expect(first.value.meta).toEqual(EXPECT_PREPARER_META); }); + test("structured extraction also accepts tool_calls extraction path", async () => { + const EXPECT_PREPARER_META: PreparerMeta = { + repoPath: "/home/user/repos/tool-call", + defaultBranch: "main", + conventions: null, + toolchain: { + packageManager: "bun", + testCommand: "bun test", + lintCommand: null, + buildCommand: "bun run build", + }, + }; + restoreFetch = installMockToolCallCompletions([EXPECT_PREPARER_META]); + + casDir = await mkdtemp(join(tmpdir(), "solve-issue-cas-")); + const cas = createCasStore(casDir); + + const run = createWorkflow(solveIssueWorkflowDefinition, { + agent: async () => "", + overrides: { developer: async () => "stub-root-hash" }, + }); + const gen = run( + makeThread("task"), + { + cas, + extract: stubExtract, + }, + ); + const first = await gen.next(); + expect(first.done).toBe(false); + if (first.done) { + throw new Error("expected yield"); + } + expect(first.value.role).toBe("preparer"); + expect(first.value.meta).toEqual(EXPECT_PREPARER_META); + }); + test("per-role agent overrides default", async () => { const PREPARER_META: PreparerMeta = { repoPath: "/tmp/r", diff --git a/packages/workflow/src/extract/index.ts b/packages/workflow/src/extract/index.ts index 40b650d..cf502d8 100644 --- a/packages/workflow/src/extract/index.ts +++ b/packages/workflow/src/extract/index.ts @@ -6,7 +6,6 @@ export { extractFunctionToolFromZodSchema, llmErrorToCause, llmExtract, - llmExtractWithRetry, } from "./llm-extract.js"; export { reactExtract } from "./react-extract.js"; export type { diff --git a/packages/workflow/src/extract/llm-extract.ts b/packages/workflow/src/extract/llm-extract.ts index ca9a4f3..0e2dc50 100644 --- a/packages/workflow/src/extract/llm-extract.ts +++ b/packages/workflow/src/extract/llm-extract.ts @@ -92,20 +92,6 @@ function readToolArgumentsJson(parsed: unknown, previewSource: string): Result( export async function llmExtract(options: LlmExtractArgs): Promise> { return performLlmExtract({ ...options, userContent: options.text }); } - -/** - * Runs extract up to two times: on the first schema/tool-args parse failure, resends the agent - * output plus the error so the model can correct the tool call. - */ -export async function llmExtractWithRetry( - options: LlmExtractArgs, -): Promise> { - const first = await performLlmExtract({ - ...options, - userContent: options.text, - }); - if (first.ok) { - return first; - } - if (!isRetryableExtractError(first.error)) { - return first; - } - - const hint = describeRetryHint(first.error); - const correction = `The previous extraction attempt failed. - -${hint} - -Respond again with a single tool call whose \`arguments\` JSON strictly matches the schema.`; - - const secondContent = `${options.text} - ---- - -${correction}`; - - return performLlmExtract({ - ...options, - userContent: secondContent, - }); -} diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 685791e..70897a1 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -59,7 +59,6 @@ export { type LlmError, llmErrorToCause, llmExtract, - llmExtractWithRetry, type ReactExtractArgs, reactExtract, } from "./extract/index.js"; -- 2.43.0 From cc3f2b576cc4585e82db00509964c70e587c3b44 Mon Sep 17 00:00:00 2001 From: Scott Wei Date: Fri, 8 May 2026 17:30:07 +0800 Subject: [PATCH 4/4] refactor(workflow): decouple agent context from CAS and fix monorepo checks Move CAS access into extract dependencies so AgentContext stays state-only, and clean up type/lint/check regressions across CLI/dashboard to keep full check green. Co-authored-by: Cursor --- biome.json | 2 +- .../src/commands/serve/routes-live.ts | 7 +-- packages/dashboard/src/api.ts | 8 ++- packages/dashboard/src/app.tsx | 14 +++-- .../dashboard/src/components/run-dialog.tsx | 27 ++++++++-- packages/dashboard/src/components/sidebar.tsx | 10 +++- .../dashboard/src/components/status-bar.tsx | 1 + .../src/components/thread-detail.tsx | 13 +++-- .../dashboard/src/components/thread-list.tsx | 4 +- .../src/components/workflow-list.tsx | 8 ++- packages/dashboard/src/hooks.ts | 1 + packages/dashboard/vite.config.ts | 1 + .../__tests__/create-llm-adapter.test.ts | 10 +--- .../src/engine/create-workflow.ts | 1 - packages/workflow-runtime/src/index.ts | 2 +- packages/workflow-runtime/src/types.ts | 1 - .../__tests__/solve-issue-template.test.ts | 53 +++++++++---------- .../__tests__/build-agent-prompt.test.ts | 53 +++++-------------- .../src/build-agent-prompt.ts | 17 +----- .../__tests__/workflow-as-agent.test.ts | 1 - packages/workflow/src/engine/engine.ts | 11 ++-- packages/workflow/src/extract/extract-fn.ts | 15 ++++-- tsconfig.json | 1 + 23 files changed, 131 insertions(+), 130 deletions(-) diff --git a/biome.json b/biome.json index 61030ca..34776b4 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json", "files": { - "includes": ["**", "!**/dist", "!**/node_modules"] + "includes": ["**", "!**/dist", "!**/node_modules", "!packages/workflow/workflow"] }, "assist": { "actions": { "source": { "organizeImports": "on" } } }, "formatter": { diff --git a/packages/cli-workflow/src/commands/serve/routes-live.ts b/packages/cli-workflow/src/commands/serve/routes-live.ts index 8301ea3..c28dd1c 100644 --- a/packages/cli-workflow/src/commands/serve/routes-live.ts +++ b/packages/cli-workflow/src/commands/serve/routes-live.ts @@ -60,8 +60,9 @@ export function createLiveRoutes(storageRoot: string): Hono { if (dataPath === null) { return c.json({ error: `thread not found: ${threadId}` }, 404); } + const resolvedDataPath = dataPath; - const infoPath = join(dirname(dataPath), `${threadId}.info.jsonl`); + const infoPath = join(dirname(resolvedDataPath), `${threadId}.info.jsonl`); return streamSSE(c, async (stream) => { const dataState: PumpState = { contentOffset: 0, carry: "" }; @@ -71,7 +72,7 @@ export function createLiveRoutes(storageRoot: string): Hono { async function pumpData(): Promise { let text: string; try { - text = await readFile(dataPath, "utf8"); + text = await readFile(resolvedDataPath, "utf8"); } catch { return false; } @@ -131,7 +132,7 @@ export function createLiveRoutes(storageRoot: string): Hono { const controller = new AbortController(); let completed = false; - const dataWatcher = watch(dataPath, async () => { + const dataWatcher = watch(resolvedDataPath, async () => { if (completed) return; const finished = await pumpData(); if (finished) { diff --git a/packages/dashboard/src/api.ts b/packages/dashboard/src/api.ts index 6bf0410..4afe307 100644 --- a/packages/dashboard/src/api.ts +++ b/packages/dashboard/src/api.ts @@ -7,7 +7,7 @@ async function postJson(path: string, body: unknown): Promise { body: JSON.stringify(body), }); if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })) as { error: string }; + const err = (await res.json().catch(() => ({ error: res.statusText }))) as { error: string }; throw new Error(err.error || `API ${res.status}`); } return res.json() as Promise; @@ -59,7 +59,11 @@ export function getThread(id: string): Promise<{ records: ThreadRecord[] }> { return fetchJson(`/threads/${id}`); } -export function runThread(workflow: string, prompt: string, maxRounds: number = 10): Promise<{ threadId: string }> { +export function runThread( + workflow: string, + prompt: string, + maxRounds: number = 10, +): Promise<{ threadId: string }> { return postJson("/threads", { workflow, prompt, maxRounds }); } diff --git a/packages/dashboard/src/app.tsx b/packages/dashboard/src/app.tsx index 6cf9b9d..0d65b8c 100644 --- a/packages/dashboard/src/app.tsx +++ b/packages/dashboard/src/app.tsx @@ -1,10 +1,10 @@ import { useState } from "react"; -import { Sidebar } from "./components/sidebar.tsx"; -import { ThreadList } from "./components/thread-list.tsx"; -import { ThreadDetail } from "./components/thread-detail.tsx"; -import { WorkflowList } from "./components/workflow-list.tsx"; -import { StatusBar } from "./components/status-bar.tsx"; import { RunDialog } from "./components/run-dialog.tsx"; +import { Sidebar } from "./components/sidebar.tsx"; +import { StatusBar } from "./components/status-bar.tsx"; +import { ThreadDetail } from "./components/thread-detail.tsx"; +import { ThreadList } from "./components/thread-list.tsx"; +import { WorkflowList } from "./components/workflow-list.tsx"; type View = "threads" | "workflows"; @@ -19,9 +19,7 @@ export function App() {
setShowRun(true)} />
- {view === "threads" && !selectedThread && ( - - )} + {view === "threads" && !selectedThread && } {view === "threads" && selectedThread && ( setSelectedThread(null)} /> )} diff --git a/packages/dashboard/src/components/run-dialog.tsx b/packages/dashboard/src/components/run-dialog.tsx index d4362c3..84a79d5 100644 --- a/packages/dashboard/src/components/run-dialog.tsx +++ b/packages/dashboard/src/components/run-dialog.tsx @@ -41,10 +41,15 @@ export function RunDialog({ onClose, onCreated }: Props) {

Run Thread

-
-