refactor: three-phase context (Moderator/Agent/Extract) + extractPrompt + unified ExtractFn
- ModeratorContext → AgentContext → ExtractContext progressive types - ThreadContext is now alias for AgentContext - RoleDefinition adds extractPrompt field - ExtractFn = (schema, ctx: ExtractContext) => Promise<T> - createWorkflow takes ExtractFn, engine loop: moderator → agent → extract - Remove ExtractConfig, extractMetaOrThrow, extract-meta.ts 小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { createWorkflow, END, type RoleDefinition } from "@uncaged/workflow";
|
import { createExtract, createWorkflow, END, type RoleDefinition } from "@uncaged/workflow";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
type Roles = {
|
type Roles = {
|
||||||
@@ -26,12 +26,15 @@ export const descriptor = {
|
|||||||
const greeter: RoleDefinition<Roles["greeter"]> = {
|
const greeter: RoleDefinition<Roles["greeter"]> = {
|
||||||
description: "Generates a greeting",
|
description: "Generates a greeting",
|
||||||
systemPrompt: "You greet the user briefly.",
|
systemPrompt: "You greet the user briefly.",
|
||||||
|
extractPrompt: "Extract the greeting string produced for the user.",
|
||||||
schema: greeterMetaSchema,
|
schema: greeterMetaSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
const extract = {
|
const extract = createExtract({
|
||||||
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "", model: "" },
|
baseUrl: "http://127.0.0.1:9",
|
||||||
} as const;
|
apiKey: "",
|
||||||
|
model: "",
|
||||||
|
});
|
||||||
|
|
||||||
export const run = createWorkflow<Roles>(
|
export const run = createWorkflow<Roles>(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { ExtractFn } from "@uncaged/workflow";
|
import type { ExtractContext, ExtractFn } from "@uncaged/workflow";
|
||||||
|
import type * as z from "zod/v4";
|
||||||
import { createCursorAgent, validateCursorAgentConfig } from "../src/index.js";
|
import { createCursorAgent, validateCursorAgentConfig } from "../src/index.js";
|
||||||
|
|
||||||
const testExtract: ExtractFn = ((_schema, _prompt) => async (_ctx) => ({
|
const testExtract: ExtractFn = async <T extends Record<string, unknown>>(
|
||||||
workspace: "/tmp",
|
_schema: z.ZodType<T>,
|
||||||
})) as ExtractFn;
|
_ctx: ExtractContext,
|
||||||
|
): Promise<T> => ({ workspace: "/tmp" }) as unknown as T;
|
||||||
|
|
||||||
describe("validateCursorAgentConfig", () => {
|
describe("validateCursorAgentConfig", () => {
|
||||||
test("accepts valid config", () => {
|
test("accepts valid config", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AgentFn } from "@uncaged/workflow";
|
import type { AgentFn, ExtractContext } from "@uncaged/workflow";
|
||||||
import { buildAgentPrompt, type SpawnCliError, spawnCli } from "@uncaged/workflow-util-agent";
|
import { buildAgentPrompt, type SpawnCliError, spawnCli } from "@uncaged/workflow-util-agent";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
@@ -43,13 +43,15 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
|||||||
|
|
||||||
const modelFlag = resolveCursorModel(config.model);
|
const modelFlag = resolveCursorModel(config.model);
|
||||||
const timeoutMs = config.timeout > 0 ? config.timeout : null;
|
const timeoutMs = config.timeout > 0 ? config.timeout : null;
|
||||||
const extractWorkspace = config.extract(
|
|
||||||
cursorWorkspaceSchema,
|
|
||||||
"From the thread context, determine the absolute filesystem path where the project/repository is located. Look for clone paths, working directories, or repo paths mentioned in previous steps.",
|
|
||||||
);
|
|
||||||
|
|
||||||
return async (ctx) => {
|
return async (ctx) => {
|
||||||
const { workspace } = await extractWorkspace(ctx);
|
const extractCtx: ExtractContext = {
|
||||||
|
...ctx,
|
||||||
|
agentContent: "",
|
||||||
|
extractPrompt:
|
||||||
|
"From the thread context, determine the absolute filesystem path where the project/repository is located.",
|
||||||
|
};
|
||||||
|
const { workspace } = await config.extract(cursorWorkspaceSchema, extractCtx);
|
||||||
const fullPrompt = buildAgentPrompt(ctx);
|
const fullPrompt = buildAgentPrompt(ctx);
|
||||||
const args = [
|
const args = [
|
||||||
"-p",
|
"-p",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
type AgentContext,
|
||||||
type AgentFn,
|
type AgentFn,
|
||||||
err,
|
err,
|
||||||
type LlmProvider,
|
type LlmProvider,
|
||||||
ok,
|
ok,
|
||||||
type Result,
|
type Result,
|
||||||
type ThreadContext,
|
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
/** OpenAI chat completion message shape (passed to `/chat/completions`). */
|
/** OpenAI chat completion message shape (passed to `/chat/completions`). */
|
||||||
@@ -97,9 +97,9 @@ export async function chatCompletionText(options: {
|
|||||||
return parseAssistantText(res.value);
|
return parseAssistantText(res.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Single-turn chat adapter: system prompt comes from {@link ThreadContext.currentRole}. */
|
/** Single-turn chat adapter: system prompt comes from {@link AgentContext.currentRole}. */
|
||||||
export function createLlmAdapter(provider: LlmProvider): AgentFn {
|
export function createLlmAdapter(provider: LlmProvider): AgentFn {
|
||||||
return async (ctx: ThreadContext) => {
|
return async (ctx: AgentContext) => {
|
||||||
const result = await chatCompletionText({
|
const result = await chatCompletionText({
|
||||||
provider,
|
provider,
|
||||||
messages: [
|
messages: [
|
||||||
|
|||||||
@@ -16,5 +16,7 @@ export const coderRole: RoleDefinition<CoderMeta> = {
|
|||||||
description:
|
description:
|
||||||
"Implements the next incomplete planner phase and reports structured completion metadata.",
|
"Implements the next incomplete planner phase and reports structured completion metadata.",
|
||||||
systemPrompt: CODER_SYSTEM,
|
systemPrompt: CODER_SYSTEM,
|
||||||
|
extractPrompt:
|
||||||
|
"Extract which phase was completed, which files were changed, and a summary of the work done.",
|
||||||
schema: coderMetaSchema,
|
schema: coderMetaSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,5 +28,7 @@ Do not attempt to fix failures yourself.`;
|
|||||||
export const committerRole: RoleDefinition<CommitterMeta> = {
|
export const committerRole: RoleDefinition<CommitterMeta> = {
|
||||||
description: "Creates branch, commits, and pushes when review passes.",
|
description: "Creates branch, commits, and pushes when review passes.",
|
||||||
systemPrompt: COMMITTER_SYSTEM,
|
systemPrompt: COMMITTER_SYSTEM,
|
||||||
|
extractPrompt:
|
||||||
|
"Extract the commit result: committed (with branch and SHA), recoverable failure, or unrecoverable failure. Include error details and log references if applicable.",
|
||||||
schema: committerMetaSchema,
|
schema: committerMetaSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,5 +22,7 @@ Order phases so earlier steps unblock later ones. Cover root cause, edge cases,
|
|||||||
export const plannerRole: RoleDefinition<PlannerMeta> = {
|
export const plannerRole: RoleDefinition<PlannerMeta> = {
|
||||||
description: "Breaks the task into sequential phases for the coder.",
|
description: "Breaks the task into sequential phases for the coder.",
|
||||||
systemPrompt: PLANNER_SYSTEM,
|
systemPrompt: PLANNER_SYSTEM,
|
||||||
|
extractPrompt:
|
||||||
|
"Extract the implementation phases from the agent's analysis. Each phase needs a name, description, and acceptance criteria.",
|
||||||
schema: plannerMetaSchema,
|
schema: plannerMetaSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,5 +18,7 @@ Only reject for blocking issues. End with your verdict.`;
|
|||||||
export const reviewerRole: RoleDefinition<ReviewerMeta> = {
|
export const reviewerRole: RoleDefinition<ReviewerMeta> = {
|
||||||
description: "Runs git diff checks and sets approved when the change is ready.",
|
description: "Runs git diff checks and sets approved when the change is ready.",
|
||||||
systemPrompt: REVIEWER_SYSTEM,
|
systemPrompt: REVIEWER_SYSTEM,
|
||||||
|
extractPrompt:
|
||||||
|
"Extract the review verdict: approved or rejected. If rejected, list the blocking issues.",
|
||||||
schema: reviewerMetaSchema,
|
schema: reviewerMetaSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { afterEach, describe, expect, test } from "bun:test";
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
import {
|
import {
|
||||||
|
createExtract,
|
||||||
END,
|
END,
|
||||||
|
type ModeratorContext,
|
||||||
type RoleStep,
|
type RoleStep,
|
||||||
START,
|
START,
|
||||||
type ThreadContext,
|
|
||||||
validateWorkflowDescriptor,
|
validateWorkflowDescriptor,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ function installMockChatCompletions(sequence: ReadonlyArray<Record<string, unkno
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeStart(maxRounds: number): ThreadContext<SolveIssueMeta>["start"] {
|
function makeStart(maxRounds: number): ModeratorContext<SolveIssueMeta>["start"] {
|
||||||
return {
|
return {
|
||||||
role: START,
|
role: START,
|
||||||
content: "Fix the flaky login test",
|
content: "Fix the flaky login test",
|
||||||
@@ -90,11 +91,10 @@ function makeStart(maxRounds: number): ThreadContext<SolveIssueMeta>["start"] {
|
|||||||
|
|
||||||
function makeCtx(
|
function makeCtx(
|
||||||
maxRounds: number,
|
maxRounds: number,
|
||||||
steps: ThreadContext<SolveIssueMeta>["steps"],
|
steps: ModeratorContext<SolveIssueMeta>["steps"],
|
||||||
): ThreadContext<SolveIssueMeta> {
|
): ModeratorContext<SolveIssueMeta> {
|
||||||
return {
|
return {
|
||||||
threadId: "01TEST000000000000000000TR",
|
threadId: "01TEST000000000000000000TR",
|
||||||
currentRole: { name: START, systemPrompt: "" },
|
|
||||||
start: makeStart(maxRounds),
|
start: makeStart(maxRounds),
|
||||||
steps,
|
steps,
|
||||||
};
|
};
|
||||||
@@ -138,9 +138,11 @@ function committerStep(): RoleStep<SolveIssueMeta> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const stubExtract = {
|
const stubExtract = createExtract({
|
||||||
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "", model: "test" },
|
baseUrl: "http://127.0.0.1:9",
|
||||||
} as const;
|
apiKey: "",
|
||||||
|
model: "test",
|
||||||
|
});
|
||||||
|
|
||||||
describe("solveIssueModerator", () => {
|
describe("solveIssueModerator", () => {
|
||||||
test("routes planner → coder → reviewer → committer → END", () => {
|
test("routes planner → coder → reviewer → committer → END", () => {
|
||||||
@@ -158,7 +160,7 @@ describe("solveIssueModerator", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("reviewer rejects → coder retry when budget allows", () => {
|
test("reviewer rejects → coder retry when budget allows", () => {
|
||||||
const steps: ThreadContext<SolveIssueMeta>["steps"] = [
|
const steps: ModeratorContext<SolveIssueMeta>["steps"] = [
|
||||||
plannerStep(),
|
plannerStep(),
|
||||||
coderStep(),
|
coderStep(),
|
||||||
reviewerStep(false),
|
reviewerStep(false),
|
||||||
@@ -167,7 +169,7 @@ describe("solveIssueModerator", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("reviewer rejects → END when max rounds exhausted", () => {
|
test("reviewer rejects → END when max rounds exhausted", () => {
|
||||||
const steps: ThreadContext<SolveIssueMeta>["steps"] = [
|
const steps: ModeratorContext<SolveIssueMeta>["steps"] = [
|
||||||
plannerStep(),
|
plannerStep(),
|
||||||
coderStep(),
|
coderStep(),
|
||||||
reviewerStep(false),
|
reviewerStep(false),
|
||||||
@@ -192,7 +194,7 @@ describe("solveIssueModerator", () => {
|
|||||||
{ name: "p1", description: "first", acceptance: "a1" },
|
{ name: "p1", description: "first", acceptance: "a1" },
|
||||||
{ name: "p2", description: "second", acceptance: "a2" },
|
{ name: "p2", description: "second", acceptance: "a2" },
|
||||||
];
|
];
|
||||||
const steps: ThreadContext<SolveIssueMeta>["steps"] = [plannerStep(phases), coderStep("p1")];
|
const steps: ModeratorContext<SolveIssueMeta>["steps"] = [plannerStep(phases), coderStep("p1")];
|
||||||
expect(solveIssueModerator(makeCtx(3, steps))).toBe(END);
|
expect(solveIssueModerator(makeCtx(3, steps))).toBe(END);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type AgentBinding,
|
type AgentBinding,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
type ExtractConfig,
|
type ExtractFn,
|
||||||
type WorkflowDefinition,
|
type WorkflowDefinition,
|
||||||
type WorkflowFn,
|
type WorkflowFn,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
@@ -45,6 +45,6 @@ export const solveIssueWorkflowDefinition: WorkflowDefinition<SolveIssueMeta> =
|
|||||||
moderator: solveIssueModerator,
|
moderator: solveIssueModerator,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createSolveIssueRun(binding: AgentBinding, extract: ExtractConfig): WorkflowFn {
|
export function createSolveIssueRun(binding: AgentBinding, extract: ExtractFn): WorkflowFn {
|
||||||
return createWorkflow(solveIssueWorkflowDefinition, binding, extract);
|
return createWorkflow(solveIssueWorkflowDefinition, binding, extract);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Moderator, ThreadContext } from "@uncaged/workflow";
|
import type { Moderator, ModeratorContext } from "@uncaged/workflow";
|
||||||
import { END } from "@uncaged/workflow";
|
import { END } from "@uncaged/workflow";
|
||||||
|
|
||||||
import type { SolveIssueMeta } from "./roles.js";
|
import type { SolveIssueMeta } from "./roles.js";
|
||||||
|
|
||||||
function nextAfterCoder(
|
function nextAfterCoder(
|
||||||
ctx: ThreadContext<SolveIssueMeta>,
|
ctx: ModeratorContext<SolveIssueMeta>,
|
||||||
maxRounds: number,
|
maxRounds: number,
|
||||||
): (keyof SolveIssueMeta & string) | typeof END {
|
): (keyof SolveIssueMeta & string) | typeof END {
|
||||||
const plannerStep = ctx.steps.find((s) => s.role === "planner");
|
const plannerStep = ctx.steps.find((s) => s.role === "planner");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ThreadContext } from "@uncaged/workflow";
|
import type { AgentContext } from "@uncaged/workflow";
|
||||||
|
|
||||||
/** Builds the full agent prompt: system instructions plus summarized thread history. */
|
/** Builds the full agent prompt: system instructions plus summarized thread history. */
|
||||||
export function buildAgentPrompt(ctx: ThreadContext): string {
|
export function buildAgentPrompt(ctx: AgentContext): string {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(ctx.currentRole.systemPrompt);
|
lines.push(ctx.currentRole.systemPrompt);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe("buildDescriptor", () => {
|
|||||||
analyst: {
|
analyst: {
|
||||||
description: "Analyzes input",
|
description: "Analyzes input",
|
||||||
systemPrompt: "You are an analyst.",
|
systemPrompt: "You are an analyst.",
|
||||||
|
extractPrompt: "Extract title and count from the analysis.",
|
||||||
schema,
|
schema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as z from "zod/v4";
|
|||||||
|
|
||||||
import { createWorkflow } from "../src/create-workflow.js";
|
import { createWorkflow } from "../src/create-workflow.js";
|
||||||
import { executeThread } from "../src/engine.js";
|
import { executeThread } from "../src/engine.js";
|
||||||
|
import { createExtract } from "../src/extract-fn.js";
|
||||||
import { createLogger } from "../src/logger.js";
|
import { createLogger } from "../src/logger.js";
|
||||||
import { END } from "../src/types.js";
|
import { END } from "../src/types.js";
|
||||||
|
|
||||||
@@ -74,9 +75,11 @@ function installMockChatCompletions(sequence: ReadonlyArray<Record<string, unkno
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const demoExtract = {
|
const demoExtract = createExtract({
|
||||||
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "test", model: "test" },
|
baseUrl: "http://127.0.0.1:9",
|
||||||
} as const;
|
apiKey: "test",
|
||||||
|
model: "test",
|
||||||
|
});
|
||||||
|
|
||||||
const demoWorkflow = createWorkflow<DemoMeta>(
|
const demoWorkflow = createWorkflow<DemoMeta>(
|
||||||
{
|
{
|
||||||
@@ -84,11 +87,13 @@ const demoWorkflow = createWorkflow<DemoMeta>(
|
|||||||
planner: {
|
planner: {
|
||||||
description: "Demo planner",
|
description: "Demo planner",
|
||||||
systemPrompt: "You are a planner.",
|
systemPrompt: "You are a planner.",
|
||||||
|
extractPrompt: "Extract plan text and affected files list.",
|
||||||
schema: plannerMetaSchema,
|
schema: plannerMetaSchema,
|
||||||
},
|
},
|
||||||
coder: {
|
coder: {
|
||||||
description: "Demo coder",
|
description: "Demo coder",
|
||||||
systemPrompt: "You are a coder.",
|
systemPrompt: "You are a coder.",
|
||||||
|
extractPrompt: "Extract the code diff summary.",
|
||||||
schema: coderMetaSchema,
|
schema: coderMetaSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { extractMetaOrThrow } from "./extract-meta.js";
|
import type { ExtractFn } from "./extract-fn.js";
|
||||||
import {
|
import {
|
||||||
type AgentBinding,
|
type AgentBinding,
|
||||||
|
type AgentContext,
|
||||||
END,
|
END,
|
||||||
type ExtractConfig,
|
type ExtractContext,
|
||||||
|
type ModeratorContext,
|
||||||
type RoleMeta,
|
type RoleMeta,
|
||||||
type RoleOutput,
|
type RoleOutput,
|
||||||
type RoleStep,
|
type RoleStep,
|
||||||
START,
|
START,
|
||||||
type ThreadContext,
|
|
||||||
type ThreadInput,
|
type ThreadInput,
|
||||||
type WorkflowDefinition,
|
type WorkflowDefinition,
|
||||||
type WorkflowFn,
|
type WorkflowFn,
|
||||||
@@ -21,33 +22,6 @@ function isRoleNext<M extends RoleMeta>(
|
|||||||
return next !== END;
|
return next !== END;
|
||||||
}
|
}
|
||||||
|
|
||||||
function moderatorThreadContext<M extends RoleMeta>(params: {
|
|
||||||
threadId: string;
|
|
||||||
start: ThreadContext<M>["start"];
|
|
||||||
steps: RoleStep<M>[];
|
|
||||||
roles: Pick<WorkflowDefinition<M>, "roles">["roles"];
|
|
||||||
}): ThreadContext<M> {
|
|
||||||
const { threadId, start, steps, roles } = params;
|
|
||||||
const last = steps[steps.length - 1];
|
|
||||||
if (last === undefined) {
|
|
||||||
return {
|
|
||||||
threadId,
|
|
||||||
currentRole: { name: START, systemPrompt: "" },
|
|
||||||
start,
|
|
||||||
steps,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const roleName = last.role as keyof M & string;
|
|
||||||
const roleDef = roles[roleName];
|
|
||||||
const systemPrompt = roleDef !== undefined ? roleDef.systemPrompt : "";
|
|
||||||
return {
|
|
||||||
threadId,
|
|
||||||
currentRole: { name: roleName, systemPrompt },
|
|
||||||
start,
|
|
||||||
steps,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds pure role definitions + moderator to runtime agents and structured extraction.
|
* Binds pure role definitions + moderator to runtime agents and structured extraction.
|
||||||
* Assign with `export const run = createWorkflow(def, binding, extract)`.
|
* Assign with `export const run = createWorkflow(def, binding, extract)`.
|
||||||
@@ -55,14 +29,14 @@ function moderatorThreadContext<M extends RoleMeta>(params: {
|
|||||||
export function createWorkflow<M extends RoleMeta>(
|
export function createWorkflow<M extends RoleMeta>(
|
||||||
def: Pick<WorkflowDefinition<M>, "roles" | "moderator">,
|
def: Pick<WorkflowDefinition<M>, "roles" | "moderator">,
|
||||||
binding: AgentBinding,
|
binding: AgentBinding,
|
||||||
extract: ExtractConfig,
|
extract: ExtractFn,
|
||||||
): WorkflowFn {
|
): WorkflowFn {
|
||||||
return async function* workflowLoop(
|
return async function* workflowLoop(
|
||||||
input: ThreadInput,
|
input: ThreadInput,
|
||||||
options: WorkflowFnOptions,
|
options: WorkflowFnOptions,
|
||||||
): AsyncGenerator<RoleOutput, WorkflowResult> {
|
): AsyncGenerator<RoleOutput, WorkflowResult> {
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
const start: ThreadContext<M>["start"] = {
|
const start: ModeratorContext<M>["start"] = {
|
||||||
role: START,
|
role: START,
|
||||||
content: input.prompt,
|
content: input.prompt,
|
||||||
meta: { maxRounds: options.maxRounds },
|
meta: { maxRounds: options.maxRounds },
|
||||||
@@ -85,12 +59,11 @@ export function createWorkflow<M extends RoleMeta>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const modCtx = moderatorThreadContext({
|
const modCtx: ModeratorContext<M> = {
|
||||||
threadId: options.threadId,
|
threadId: options.threadId,
|
||||||
start,
|
start,
|
||||||
steps,
|
steps,
|
||||||
roles: def.roles,
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const next = def.moderator(modCtx);
|
const next = def.moderator(modCtx);
|
||||||
|
|
||||||
@@ -103,20 +76,22 @@ export function createWorkflow<M extends RoleMeta>(
|
|||||||
return { returnCode: 1, summary: `unknown role: ${next}` };
|
return { returnCode: 1, summary: `unknown role: ${next}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx: ThreadContext<M> = {
|
const agentCtx: AgentContext<M> = {
|
||||||
threadId: options.threadId,
|
...modCtx,
|
||||||
currentRole: { name: next, systemPrompt: roleDef.systemPrompt },
|
currentRole: { name: next, systemPrompt: roleDef.systemPrompt },
|
||||||
start,
|
|
||||||
steps,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const agent = binding.overrides?.[next] ?? binding.agent;
|
const agent = binding.overrides?.[next] ?? binding.agent;
|
||||||
|
|
||||||
const raw = await agent(ctx as unknown as ThreadContext);
|
const raw = await agent(agentCtx as unknown as AgentContext);
|
||||||
|
|
||||||
const meta = await extractMetaOrThrow(next, raw, roleDef.schema, {
|
const extractCtx: ExtractContext<M> = {
|
||||||
provider: extract.provider,
|
...agentCtx,
|
||||||
});
|
agentContent: raw,
|
||||||
|
extractPrompt: roleDef.extractPrompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta = await extract(roleDef.schema, extractCtx as unknown as ExtractContext);
|
||||||
|
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
const step = {
|
const step = {
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
import type * as z from "zod/v4";
|
import type * as z from "zod/v4";
|
||||||
|
|
||||||
import { llmExtractWithRetry } from "./llm-extract.js";
|
import { llmExtractWithRetry } from "./llm-extract.js";
|
||||||
import type { LlmProvider, ThreadContext } from "./types.js";
|
import type { ExtractContext, LlmProvider } from "./types.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Curried extract: bind a schema + prompt, get a function that extracts from ThreadContext.
|
|
||||||
*/
|
|
||||||
export type ExtractFn = <T extends Record<string, unknown>>(
|
export type ExtractFn = <T extends Record<string, unknown>>(
|
||||||
schema: z.ZodType<T>,
|
schema: z.ZodType<T>,
|
||||||
prompt: string,
|
ctx: ExtractContext,
|
||||||
) => (ctx: ThreadContext) => Promise<T>;
|
) => Promise<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an ExtractFn backed by an LLM provider.
|
* Create an ExtractFn backed by an LLM provider.
|
||||||
* The returned function uses the thread context (currentRole.systemPrompt + steps) as source text
|
* Builds prompt text from {@link ExtractContext} and calls structured extraction.
|
||||||
* for structured extraction.
|
|
||||||
*/
|
*/
|
||||||
export function createExtract(provider: LlmProvider): ExtractFn {
|
export function createExtract(provider: LlmProvider): ExtractFn {
|
||||||
return <T extends Record<string, unknown>>(schema: z.ZodType<T>, prompt: string) => {
|
return async <T extends Record<string, unknown>>(
|
||||||
return async (ctx: ThreadContext): Promise<T> => {
|
schema: z.ZodType<T>,
|
||||||
|
ctx: ExtractContext,
|
||||||
|
): Promise<T> => {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push("## Current Role");
|
lines.push(`## Role: ${ctx.currentRole.name}`);
|
||||||
lines.push(ctx.currentRole.systemPrompt);
|
lines.push(ctx.currentRole.systemPrompt);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("## Task");
|
lines.push("## Task");
|
||||||
@@ -35,8 +33,11 @@ export function createExtract(provider: LlmProvider): ExtractFn {
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lines.push("## Agent Output");
|
||||||
|
lines.push(ctx.agentContent);
|
||||||
|
lines.push("");
|
||||||
lines.push("## Extraction Instruction");
|
lines.push("## Extraction Instruction");
|
||||||
lines.push(prompt);
|
lines.push(ctx.extractPrompt);
|
||||||
|
|
||||||
const text = lines.join("\n");
|
const text = lines.join("\n");
|
||||||
const result = await llmExtractWithRetry({ text, schema, provider });
|
const result = await llmExtractWithRetry({ text, schema, provider });
|
||||||
@@ -45,5 +46,4 @@ export function createExtract(provider: LlmProvider): ExtractFn {
|
|||||||
}
|
}
|
||||||
return result.value;
|
return result.value;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import type * as z from "zod/v4";
|
|
||||||
|
|
||||||
import { llmExtractWithRetry } from "./llm-extract.js";
|
|
||||||
import type { LlmProvider } from "./types.js";
|
|
||||||
|
|
||||||
export async function extractMetaOrThrow<T extends Record<string, unknown>>(
|
|
||||||
roleName: string,
|
|
||||||
raw: string,
|
|
||||||
schema: z.ZodType<T>,
|
|
||||||
options: { provider: LlmProvider },
|
|
||||||
): Promise<T> {
|
|
||||||
const result = await llmExtractWithRetry({
|
|
||||||
text: raw,
|
|
||||||
schema,
|
|
||||||
provider: options.provider,
|
|
||||||
});
|
|
||||||
if (!result.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Role "${roleName}": structured extraction failed after retry: ${JSON.stringify(result.error)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result.value;
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ export {
|
|||||||
} from "./engine.js";
|
} from "./engine.js";
|
||||||
export { type ExtractedBundleExports, extractBundleExports } from "./extract-bundle-exports.js";
|
export { type ExtractedBundleExports, extractBundleExports } from "./extract-bundle-exports.js";
|
||||||
export { createExtract, type ExtractFn } from "./extract-fn.js";
|
export { createExtract, type ExtractFn } from "./extract-fn.js";
|
||||||
export { extractMetaOrThrow } from "./extract-meta.js";
|
|
||||||
export {
|
export {
|
||||||
buildForkPlan,
|
buildForkPlan,
|
||||||
type ForkHistoricalStep,
|
type ForkHistoricalStep,
|
||||||
@@ -59,11 +58,13 @@ export { getDefaultWorkflowStorageRoot } from "./storage-root.js";
|
|||||||
export { createThreadPauseGate, type ThreadPauseGate } from "./thread-pause-gate.js";
|
export { createThreadPauseGate, type ThreadPauseGate } from "./thread-pause-gate.js";
|
||||||
export {
|
export {
|
||||||
type AgentBinding,
|
type AgentBinding,
|
||||||
|
type AgentContext,
|
||||||
type AgentFn,
|
type AgentFn,
|
||||||
END,
|
END,
|
||||||
type ExtractConfig,
|
type ExtractContext,
|
||||||
type LlmProvider,
|
type LlmProvider,
|
||||||
type Moderator,
|
type Moderator,
|
||||||
|
type ModeratorContext,
|
||||||
type RoleDefinition,
|
type RoleDefinition,
|
||||||
type RoleMeta,
|
type RoleMeta,
|
||||||
type RoleOutput,
|
type RoleOutput,
|
||||||
|
|||||||
@@ -58,19 +58,32 @@ export type RoleStep<M extends RoleMeta> = {
|
|||||||
[K in keyof M & string]: { role: K; meta: M[K]; content: string; timestamp: number };
|
[K in keyof M & string]: { role: K; meta: M[K]; content: string; timestamp: number };
|
||||||
}[keyof M & string];
|
}[keyof M & string];
|
||||||
|
|
||||||
/** Thread-scoped context passed to agents and moderator. */
|
/** Phase 1: Moderator decides next role. */
|
||||||
export type ThreadContext<M extends RoleMeta = RoleMeta> = {
|
export type ModeratorContext<M extends RoleMeta = RoleMeta> = {
|
||||||
threadId: string;
|
threadId: string;
|
||||||
currentRole: {
|
|
||||||
name: string;
|
|
||||||
systemPrompt: string;
|
|
||||||
};
|
|
||||||
start: StartStep;
|
start: StartStep;
|
||||||
steps: RoleStep<M>[];
|
steps: RoleStep<M>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Phase 2: Agent executes — knows its role and prompt. */
|
||||||
|
export type AgentContext<M extends RoleMeta = RoleMeta> = ModeratorContext<M> & {
|
||||||
|
currentRole: {
|
||||||
|
name: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Phase 3: Extractor runs — has agent output and extract instruction. */
|
||||||
|
export type ExtractContext<M extends RoleMeta = RoleMeta> = AgentContext<M> & {
|
||||||
|
agentContent: string;
|
||||||
|
extractPrompt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Alias — most external consumers see the agent-phase context. */
|
||||||
|
export type ThreadContext<M extends RoleMeta = RoleMeta> = AgentContext<M>;
|
||||||
|
|
||||||
/** Raw string output from an LLM/CLI adapter; meta is extracted by the engine. */
|
/** Raw string output from an LLM/CLI adapter; meta is extracted by the engine. */
|
||||||
export type AgentFn = (ctx: ThreadContext) => Promise<string>;
|
export type AgentFn = (ctx: AgentContext) => Promise<string>;
|
||||||
|
|
||||||
/** Runtime agent assignment (optional per-role overrides). */
|
/** Runtime agent assignment (optional per-role overrides). */
|
||||||
export type AgentBinding = {
|
export type AgentBinding = {
|
||||||
@@ -78,15 +91,11 @@ export type AgentBinding = {
|
|||||||
overrides?: Partial<Record<string, AgentFn>>;
|
overrides?: Partial<Record<string, AgentFn>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Structured extraction settings for the workflow engine. */
|
|
||||||
export type ExtractConfig = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Role wiring: prompts, schema, and human-readable description. */
|
/** Role wiring: prompts, schema, and human-readable description. */
|
||||||
export type RoleDefinition<Meta extends Record<string, unknown>> = {
|
export type RoleDefinition<Meta extends Record<string, unknown>> = {
|
||||||
description: string;
|
description: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
|
extractPrompt: string;
|
||||||
schema: z.ZodType<Meta>;
|
schema: z.ZodType<Meta>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,7 +106,7 @@ export type RoleDefinition<Meta extends Record<string, unknown>> = {
|
|||||||
* Returns the next role name or END to terminate.
|
* Returns the next role name or END to terminate.
|
||||||
*/
|
*/
|
||||||
export type Moderator<M extends RoleMeta> = (
|
export type Moderator<M extends RoleMeta> = (
|
||||||
ctx: ThreadContext<M>,
|
ctx: ModeratorContext<M>,
|
||||||
) => (keyof M & string) | typeof END;
|
) => (keyof M & string) | typeof END;
|
||||||
|
|
||||||
/** Complete workflow definition as authored by users. */
|
/** Complete workflow definition as authored by users. */
|
||||||
|
|||||||
Reference in New Issue
Block a user