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:
2026-05-07 01:05:31 +00:00
parent 99a137422c
commit d472de1247
19 changed files with 137 additions and 152 deletions
+7 -4
View File
@@ -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", () => {
+8 -6
View File
@@ -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,
}, },
}, },
+8 -3
View File
@@ -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,
}, },
}, },
+18 -43
View File
@@ -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 = {
+13 -13
View File
@@ -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;
}; };
};
} }
-23
View File
@@ -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;
}
+3 -2
View File
@@ -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,
+22 -13
View File
@@ -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. */