Merge pull request 'refactor: simplify ExtractFn to (schema, contentHash)' (#184) from refactor/180-simplify-extract-fn into main

This commit is contained in:
2026-05-11 08:03:48 +00:00
25 changed files with 35 additions and 162 deletions
@@ -50,7 +50,6 @@ const greeterMetaSchema = z.object({
export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = { export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
description: "Says hello — replace with your first role.", description: "Says hello — replace with your first role.",
systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.", systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.",
extractPrompt: "Extract the assistant's greeting as message.",
schema: greeterMetaSchema, schema: greeterMetaSchema,
extractRefs: null, extractRefs: null,
}; };
@@ -93,18 +93,18 @@ Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下
## 2. 核心概念 ## 2. 核心概念
- **RoleMeta**:\`Record<string, Record<string, unknown>>\`,角色名 → 该角色结构化 meta 的形状约定。 - **RoleMeta**:\`Record<string, Record<string, unknown>>\`,角色名 → 该角色结构化 meta 的形状约定。
- **RoleDefinition<Meta>**:纯数据——\`description\`\`systemPrompt\`\`extractPrompt\`\`schema\`(Zod v4)。不含执行逻辑。 - **RoleDefinition<Meta>**:纯数据——\`description\`\`systemPrompt\`\`schema\`(Zod v4)。不含执行逻辑。
- **WorkflowDefinition<M extends RoleMeta>**:\`description\` + \`roles\`(各角色定义)+ **Moderator**。 - **WorkflowDefinition<M extends RoleMeta>**:\`description\` + \`roles\`(各角色定义)+ **Moderator**。
- **Moderator**:\`(ctx: ModeratorContext<M>) => (角色名) | END\`。同步、纯函数,只做路由。 - **Moderator**:\`(ctx: ModeratorContext<M>) => (角色名) | END\`。同步、纯函数,只做路由。
- **AgentFn**:\`(ctx: AgentContext) => Promise<string>\`,原始文本输出;从上下文读取当前角色的 \`systemPrompt\` - **AgentFn**:\`(ctx: AgentContext) => Promise<string>\`,原始文本输出;从上下文读取当前角色的 \`systemPrompt\`
- **ExtractFn**:从上下文与 prompt 解析结构化数据(引擎与 Agent 都可使用)。 - **ExtractFn**:从 CAS content hash 解析结构化数据(引擎与 Agent 都可使用)。
引擎循环简述:**Moderator** → 选角色 → **Agent** 产出文本 → **Extract** 写入 **meta** → 追加 step,重复直至 **END**。详见 \`docs/architecture.md\` 中的三阶段说明。 引擎循环简述:**Moderator** → 选角色 → **Agent** 产出文本 → **Extract** 写入 **meta** → 追加 step,重复直至 **END**。详见 \`docs/architecture.md\` 中的三阶段说明。
## 3. 开发流程 ## 3. 开发流程
1. **定义 RoleMeta**:为每个角色约定 meta 的 TypeScript 类型(与 Zod schema 对齐)。 1. **定义 RoleMeta**:为每个角色约定 meta 的 TypeScript 类型(与 Zod schema 对齐)。
2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`extractPrompt\` / \`description\` 2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`description\`
3. **编写 Moderator**:根据 \`ctx.steps\` 与业务状态返回下一个角色名或 \`END\` 3. **编写 Moderator**:根据 \`ctx.steps\` 与业务状态返回下一个角色名或 \`END\`
4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / moderator 导出)。 4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / moderator 导出)。
5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AgentFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowRuntime\` 5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AgentFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowRuntime\`
-1
View File
@@ -223,7 +223,6 @@ Each role has:
|-------|------|---------| |-------|------|---------|
| \`description\` | string | What the role does | | \`description\` | string | What the role does |
| \`systemPrompt\` | string | System prompt for the agent | | \`systemPrompt\` | string | System prompt for the agent |
| \`extractPrompt\` | string | Instruction for extracting structured meta |
| \`schema\` | ZodSchema | Validates the extracted meta | | \`schema\` | ZodSchema | Validates the extracted meta |
| \`extractRefs\` | fn or null | Extracts CAS hashes from meta for DAG linking | | \`extractRefs\` | fn or null | Extracts CAS hashes from meta for DAG linking |
@@ -1,24 +1,12 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import type { ExtractContext, ExtractFn } from "@uncaged/workflow-runtime";
import type * as z from "zod/v4";
import { createCursorAgent, validateCursorAgentConfig } from "../src/index.js"; import { createCursorAgent, validateCursorAgentConfig } from "../src/index.js";
const testExtract: ExtractFn = async <T extends Record<string, unknown>>(
_schema: z.ZodType<T>,
_prompt: string,
_ctx: ExtractContext,
): Promise<{ meta: T; contentPayload: string; refs: string[] }> => ({
meta: { workspace: "/tmp" } as unknown as T,
contentPayload: "",
refs: [],
});
describe("validateCursorAgentConfig", () => { describe("validateCursorAgentConfig", () => {
test("accepts valid config", () => { test("accepts valid config", () => {
const r = validateCursorAgentConfig({ const r = validateCursorAgentConfig({
model: null, model: null,
timeout: 0, timeout: 0,
extract: testExtract, workspace: "/tmp/test-project",
}); });
expect(r.ok).toBe(true); expect(r.ok).toBe(true);
}); });
@@ -27,11 +15,11 @@ describe("validateCursorAgentConfig", () => {
const r = validateCursorAgentConfig({ const r = validateCursorAgentConfig({
model: null, model: null,
timeout: 0, timeout: 0,
extract: null as unknown as ExtractFn, workspace: "",
}); });
expect(r.ok).toBe(false); expect(r.ok).toBe(false);
if (!r.ok) { if (!r.ok) {
expect(r.error).toContain("extract"); expect(r.error).toContain("workspace");
} }
}); });
@@ -39,7 +27,7 @@ describe("validateCursorAgentConfig", () => {
const r = validateCursorAgentConfig({ const r = validateCursorAgentConfig({
model: null, model: null,
timeout: -1, timeout: -1,
extract: testExtract, workspace: "/tmp/test-project",
}); });
expect(r.ok).toBe(false); expect(r.ok).toBe(false);
}); });
@@ -50,7 +38,7 @@ describe("createCursorAgent", () => {
const agent = createCursorAgent({ const agent = createCursorAgent({
model: null, model: null,
timeout: 0, timeout: 0,
extract: testExtract, workspace: "/tmp/test-project",
}); });
expect(typeof agent).toBe("function"); expect(typeof agent).toBe("function");
}); });
@@ -60,7 +48,7 @@ describe("createCursorAgent", () => {
createCursorAgent({ createCursorAgent({
model: null, model: null,
timeout: -1, timeout: -1,
extract: testExtract, workspace: "/tmp/test-project",
}), }),
).toThrow(); ).toThrow();
}); });
+2 -18
View File
@@ -1,6 +1,5 @@
import type { AgentFn, ExtractContext } from "@uncaged/workflow-runtime"; import type { AgentFn } from "@uncaged/workflow-runtime";
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 type { CursorAgentConfig } from "./types.js"; import type { CursorAgentConfig } from "./types.js";
import { validateCursorAgentConfig } from "./validate-config.js"; import { validateCursorAgentConfig } from "./validate-config.js";
@@ -8,12 +7,6 @@ import { validateCursorAgentConfig } from "./validate-config.js";
export type { CursorAgentConfig } from "./types.js"; export type { CursorAgentConfig } from "./types.js";
export { validateCursorAgentConfig } from "./validate-config.js"; export { validateCursorAgentConfig } from "./validate-config.js";
const cursorWorkspaceSchema = z.object({
workspace: z
.string()
.describe("Absolute path to the project/repository directory the agent should work in"),
});
function throwCursorSpawnError(error: SpawnCliError): never { function throwCursorSpawnError(error: SpawnCliError): never {
if (error.kind === "non_zero_exit") { if (error.kind === "non_zero_exit") {
throw new Error( throw new Error(
@@ -44,16 +37,7 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
const timeoutMs = config.timeout > 0 ? config.timeout : null; const timeoutMs = config.timeout > 0 ? config.timeout : null;
return async (ctx) => { return async (ctx) => {
const extractCtx: ExtractContext = { const workspace = config.workspace;
...ctx,
agentContent: "",
};
const extracted = await config.extract(
cursorWorkspaceSchema,
"From the thread context, determine the absolute filesystem path where the project/repository is located.",
extractCtx,
);
const { workspace } = extracted.meta;
const fullPrompt = await buildAgentPrompt(ctx); const fullPrompt = await buildAgentPrompt(ctx);
const args = [ const args = [
"-p", "-p",
+1 -3
View File
@@ -1,7 +1,5 @@
import type { ExtractFn } from "@uncaged/workflow-runtime";
export type CursorAgentConfig = { export type CursorAgentConfig = {
model: string | null; model: string | null;
timeout: number; timeout: number;
extract: ExtractFn; workspace: string;
}; };
@@ -3,8 +3,8 @@ import { err, ok, type Result } from "@uncaged/workflow-runtime";
import type { CursorAgentConfig } from "./types.js"; import type { CursorAgentConfig } from "./types.js";
export function validateCursorAgentConfig(config: CursorAgentConfig): Result<void, string> { export function validateCursorAgentConfig(config: CursorAgentConfig): Result<void, string> {
if (typeof config.extract !== "function") { if (typeof config.workspace !== "string" || config.workspace.length === 0) {
return err("extract must be a function"); return err("workspace must be a non-empty string (absolute path)");
} }
if (config.timeout < 0) { if (config.timeout < 0) {
return err("timeout must be a non-negative number (milliseconds); use 0 for no limit"); return err("timeout must be a non-negative number (milliseconds); use 0 for no limit");
@@ -2,8 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises"; import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { createCasStore } from "@uncaged/workflow-cas"; import { createCasStore, putContentNodeWithRefs } from "@uncaged/workflow-cas";
import { type ExtractContext, START } from "@uncaged/workflow-runtime";
import * as z from "zod/v4"; import * as z from "zod/v4";
import { createExtract } from "../src/extract/extract-fn.js"; import { createExtract } from "../src/extract/extract-fn.js";
@@ -45,21 +44,9 @@ describe("createExtract — ExtractResult shape", () => {
); );
const schema = z.object({ confidence: z.number() }); const schema = z.object({ confidence: z.number() });
const ctx: ExtractContext = { const contentHash = await putContentNodeWithRefs(cas, "model says hello", []);
threadId: "01THREADTESTAAAAAAAAAAAAAA",
depth: 0,
start: {
role: START,
content: "task text",
meta: { maxRounds: 10 },
timestamp: 100,
},
steps: [],
currentRole: { name: "analyst", systemPrompt: "be precise" },
agentContent: "model says hello",
};
const out = await extract(schema, "extract fields", ctx); const out = await extract(schema, contentHash);
expect(out.meta).toEqual({ confidence: 0.9 }); expect(out.meta).toEqual({ confidence: 0.9 });
expect(out.contentPayload).toBe("model says hello"); expect(out.contentPayload).toBe("model says hello");
@@ -1,7 +1,6 @@
import { type CasStore, getContentMerklePayload } from "@uncaged/workflow-cas"; import { type CasStore, getContentMerklePayload } from "@uncaged/workflow-cas";
import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor"; import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor";
import type { import type {
ExtractContext,
ExtractFn, ExtractFn,
ExtractResult, ExtractResult,
LlmProvider, LlmProvider,
@@ -31,7 +30,7 @@ const CAS_GET_TOOL_DEFINITION = {
}, },
}; };
export type ExtractThreadContext = { type ExtractThreadContext = {
cas: CasStore; cas: CasStore;
}; };
@@ -39,41 +38,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value); return typeof value === "object" && value !== null && !Array.isArray(value);
} }
/** Builds the user-side extraction prompt (thread + agent output + instruction). */
export async function buildExtractUserContent(
ctx: ExtractContext,
prompt: string,
deps: ExtractDeps,
): Promise<string> {
const lines: string[] = [];
lines.push(`## Role: ${ctx.currentRole.name}`);
lines.push(ctx.currentRole.systemPrompt);
lines.push("");
lines.push("## Task");
lines.push(ctx.start.content);
lines.push("");
if (ctx.steps.length > 0) {
lines.push("## Thread History");
for (const step of ctx.steps) {
const body = await getContentMerklePayload(deps.cas, step.contentHash);
if (body === null) {
throw new Error(`extract: missing CAS blob for step ${step.role}: ${step.contentHash}`);
}
lines.push(`### ${step.role}`);
lines.push(body);
lines.push(`Meta: ${JSON.stringify(step.meta)}`);
lines.push("");
}
}
lines.push("## Agent Output");
lines.push(ctx.agentContent);
lines.push("");
lines.push("## Extraction Instruction");
lines.push(prompt);
return lines.join("\n");
}
/** /**
* Create an ExtractFn backed by an LLM provider. * Create an ExtractFn backed by an LLM provider.
* *
@@ -102,7 +66,7 @@ export function createExtract(provider: LlmProvider, deps: ExtractDeps): Extract
}; };
}, },
systemPromptForStructuredTool: (structuredToolName) => systemPromptForStructuredTool: (structuredToolName) =>
`You extract structured metadata from the agent output below. Use cas_get to read Merkle DAG nodes from CAS (YAML: type, payload, refs for content nodes or children for step/thread legacy nodes) when the agent output references hashes you must traverse. When you have the complete structured object, call the ${structuredToolName} tool with JSON arguments matching the schema. You may instead reply with only a JSON object (no prose) when no tools are needed.`, `You extract structured metadata from content. The content is from a CAS node. Use cas_get to read referenced nodes if needed. When ready, call the ${structuredToolName} tool with JSON matching the schema. You may instead reply with only a JSON object (no prose) when no tools are needed.`,
toolHandler: async (call, thread) => { toolHandler: async (call, thread) => {
if (call.function.name !== "cas_get") { if (call.function.name !== "cas_get") {
return `Unexpected tool routed to handler: ${call.function.name}`; return `Unexpected tool routed to handler: ${call.function.name}`;
@@ -124,10 +88,13 @@ export function createExtract(provider: LlmProvider, deps: ExtractDeps): Extract
return async <T extends Record<string, unknown>>( return async <T extends Record<string, unknown>>(
schema: z.ZodType<T>, schema: z.ZodType<T>,
prompt: string, contentHash: string,
ctx: ExtractContext,
): Promise<ExtractResult<T>> => { ): Promise<ExtractResult<T>> => {
const text = await buildExtractUserContent(ctx, prompt, deps); const payload = await getContentMerklePayload(deps.cas, contentHash);
if (payload === null) {
throw new Error(`extract: missing CAS content node for hash ${contentHash}`);
}
const text = `${payload}\n\nExtract structured metadata according to the schema.`;
const result = await reactor({ const result = await reactor({
thread: { cas: deps.cas }, thread: { cas: deps.cas },
input: text, input: text,
@@ -138,7 +105,7 @@ export function createExtract(provider: LlmProvider, deps: ExtractDeps): Extract
} }
return { return {
meta: result.value, meta: result.value,
contentPayload: ctx.agentContent, contentPayload: payload,
refs: [], refs: [],
}; };
}; };
@@ -1,8 +1,4 @@
export { export { createExtract } from "./extract-fn.js";
buildExtractUserContent,
createExtract,
type ExtractThreadContext,
} from "./extract-fn.js";
export { export {
extractFunctionToolFromZodSchema, extractFunctionToolFromZodSchema,
llmErrorToCause, llmErrorToCause,
-2
View File
@@ -37,9 +37,7 @@ export { EMPTY_CHAIN_STATE } from "./engine/types.js";
export { getWorkerHostScriptPath } from "./engine/worker-entry-path.js"; export { getWorkerHostScriptPath } from "./engine/worker-entry-path.js";
export type { ExtractFn, LlmError, LlmExtractArgs } from "./extract/index.js"; export type { ExtractFn, LlmError, LlmExtractArgs } from "./extract/index.js";
export { export {
buildExtractUserContent,
createExtract, createExtract,
type ExtractThreadContext,
extractFunctionToolFromZodSchema, extractFunctionToolFromZodSchema,
llmErrorToCause, llmErrorToCause,
llmExtract, llmExtract,
@@ -108,7 +108,7 @@ export function workflowAsAgent(
io, io,
logger, logger,
); );
return result.rootHash; return `Child workflow "${workflowName}" completed (returnCode=${result.returnCode}).\n\nSummary: ${result.summary}\n\nChild thread root hash: ${result.rootHash}`;
} catch (e) { } catch (e) {
const message = e instanceof Error ? e.message : String(e); const message = e instanceof Error ? e.message : String(e);
return `ERROR: ${message}`; return `ERROR: ${message}`;
-1
View File
@@ -14,7 +14,6 @@ export type {
AgentContext, AgentContext,
AgentFn, AgentFn,
CasStore, CasStore,
ExtractContext,
ExtractFn, ExtractFn,
ExtractResult, ExtractResult,
FALLBACK, FALLBACK,
+1 -7
View File
@@ -76,10 +76,6 @@ export type AgentContext<M extends RoleMeta = RoleMeta> = ModeratorContext<M> &
}; };
}; };
export type ExtractContext<M extends RoleMeta = RoleMeta> = AgentContext<M> & {
agentContent: string;
};
// ── Workflow Completion ──────────────────────────────────────────── // ── Workflow Completion ────────────────────────────────────────────
export type WorkflowCompletion = { export type WorkflowCompletion = {
@@ -128,8 +124,7 @@ export type ExtractResult<T extends Record<string, unknown>> = {
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, contentHash: string,
ctx: ExtractContext,
) => Promise<ExtractResult<T>>; ) => Promise<ExtractResult<T>>;
export type AgentFn = (ctx: AgentContext) => Promise<string>; export type AgentFn = (ctx: AgentContext) => Promise<string>;
@@ -154,7 +149,6 @@ export type WorkflowFn = (
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>;
extractRefs: ((meta: Meta) => string[]) | null; extractRefs: ((meta: Meta) => string[]) | null;
}; };
@@ -7,7 +7,6 @@ import {
type AgentContext, type AgentContext,
type AgentFn, type AgentFn,
END, END,
type ExtractContext,
type ModeratorContext, type ModeratorContext,
type RoleDefinition, type RoleDefinition,
type RoleMeta, type RoleMeta,
@@ -89,15 +88,11 @@ async function advanceOneRound<M extends RoleMeta>(
const agent = agentForRole(binding, next); const agent = agentForRole(binding, next);
const raw = await agent(agentCtx as unknown as AgentContext); const raw = await agent(agentCtx as unknown as AgentContext);
const extractCtx: ExtractContext<M> = { const agentContentHash = await putContentNodeWithRefs(runtime.cas, raw, []);
...agentCtx,
agentContent: raw,
};
const extracted = await runtime.extract( const extracted = await runtime.extract(
roleDef.schema as z.ZodType<Record<string, unknown>>, roleDef.schema as z.ZodType<Record<string, unknown>>,
roleDef.extractPrompt, agentContentHash,
extractCtx as unknown as ExtractContext,
); );
const refsFromMeta = resolveExtractedRefs( const refsFromMeta = resolveExtractedRefs(
@@ -106,11 +101,9 @@ async function advanceOneRound<M extends RoleMeta>(
); );
const artifactRefs = mergeUniqueHashes(extracted.refs, refsFromMeta); const artifactRefs = mergeUniqueHashes(extracted.refs, refsFromMeta);
const contentHash = await putContentNodeWithRefs( const contentHash = artifactRefs.length === 0
runtime.cas, ? agentContentHash
extracted.contentPayload, : await putContentNodeWithRefs(runtime.cas, extracted.contentPayload, artifactRefs);
artifactRefs,
);
const refs = artifactRefs.includes(contentHash) ? artifactRefs : [...artifactRefs, contentHash]; const refs = artifactRefs.includes(contentHash) ? artifactRefs : [...artifactRefs, contentHash];
const step = { const step = {
-1
View File
@@ -6,7 +6,6 @@ export type {
AgentContext, AgentContext,
AgentFn, AgentFn,
CasStore, CasStore,
ExtractContext,
ExtractFn, ExtractFn,
ExtractResult, ExtractResult,
FALLBACK, FALLBACK,
-1
View File
@@ -8,7 +8,6 @@ export type {
AgentContext, AgentContext,
AgentFn, AgentFn,
CasStore, CasStore,
ExtractContext,
ExtractFn, ExtractFn,
ExtractResult, ExtractResult,
FALLBACK, FALLBACK,
@@ -2,7 +2,7 @@ import type { RoleDefinition } from "@uncaged/workflow-runtime";
import * as z from "zod/v4"; import * as z from "zod/v4";
export const coderMetaSchema = z.object({ export const coderMetaSchema = z.object({
completedPhase: z.string(), completedPhase: z.string().describe("The planner phase hash finished this round. If multiple phases were completed, use the last finished phase hash."),
filesChanged: z.array(z.string()), filesChanged: z.array(z.string()),
summary: z.string(), summary: z.string(),
}); });
@@ -27,8 +27,6 @@ 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 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, schema: coderMetaSchema,
extractRefs: (meta) => [meta.completedPhase], extractRefs: (meta) => [meta.completedPhase],
}; };
@@ -28,8 +28,6 @@ Do not attempt to fix failures yourself.`;
export const committerRole: RoleDefinition<CommitterMeta> = { export const committerRole: RoleDefinition<CommitterMeta> = {
description: "Creates a branch and commits changes.", description: "Creates a branch and commits changes.",
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,
extractRefs: null, extractRefs: null,
}; };
@@ -44,8 +44,6 @@ 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 output. Each phase has a hash (the CAS content-hash returned by the cas put command) and a title (one-line summary).",
schema: plannerMetaSchema, schema: plannerMetaSchema,
extractRefs: (meta) => meta.phases.map((p) => p.hash), extractRefs: (meta) => meta.phases.map((p) => p.hash),
}; };
@@ -37,8 +37,6 @@ Be thorough. A false approve costs more than a false reject.`;
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,
extractRefs: null, extractRefs: null,
}; };
@@ -19,8 +19,6 @@ const TESTER_SYSTEM = `You are a tester. Run the project's test suite, build, an
export const testerRole: RoleDefinition<TesterMeta> = { export const testerRole: RoleDefinition<TesterMeta> = {
description: "Runs test, build, and lint commands and reports pass or fail with details.", description: "Runs test, build, and lint commands and reports pass or fail with details.",
systemPrompt: TESTER_SYSTEM, systemPrompt: TESTER_SYSTEM,
extractPrompt:
"Extract the verification result: passed with summary details, or failed with details of what broke.",
schema: testerMetaSchema, schema: testerMetaSchema,
extractRefs: null, extractRefs: null,
}; };
@@ -16,21 +16,10 @@ The actual implementation (planning → coding → reviewing → testing → com
Pass through the task and let the child workflow do the work.`; Pass through the task and let the child workflow do the work.`;
const DEVELOPER_EXTRACT_PROMPT = `The agent output is the root CAS hash of a child workflow thread. Use the cas_get tool to traverse the Merkle DAG and extract the developer summary.
Procedure:
1. cas_get(<rootHash>) — the root node lists all child step hashes (planner, coder, reviewer, tester, committer).
2. Find the committer step. cas_get its hash to read the committer's meta — extract branch and commitSha from there.
3. Find every coder step. cas_get each to read the coder's filesChanged. Union all filesChanged across coder steps.
4. Compose a short human-readable summary describing what the develop child workflow accomplished (drawn from the coder summaries, or a synthesis of them).
Return: { branch, commitSha, filesChanged, summary }.`;
export const developerRole: RoleDefinition<DeveloperMeta> = { export const developerRole: RoleDefinition<DeveloperMeta> = {
description: description:
"Delegates the actual implementation to the develop workflow (workflow-as-agent). Produces a summary by traversing the child thread's Merkle DAG.", "Delegates the actual implementation to the develop workflow (workflow-as-agent). Produces a summary by traversing the child thread's Merkle DAG.",
systemPrompt: DEVELOPER_SYSTEM, systemPrompt: DEVELOPER_SYSTEM,
extractPrompt: DEVELOPER_EXTRACT_PROMPT,
schema: developerMetaSchema, schema: developerMetaSchema,
extractRefs: () => [], extractRefs: () => [],
}; };
@@ -44,8 +44,6 @@ export const preparerRole: RoleDefinition<PreparerMeta> = {
description: description:
"Locates or clones the target repository, ensures it is up to date, and gathers project context (conventions, toolchain).", "Locates or clones the target repository, ensures it is up to date, and gathers project context (conventions, toolchain).",
systemPrompt: PREPARER_SYSTEM, systemPrompt: PREPARER_SYSTEM,
extractPrompt:
"Extract repoPath (absolute path), defaultBranch, conventions (summary string or null), and toolchain (packageManager, testCommand, lintCommand, buildCommand — each string or null).",
schema: preparerMetaSchema, schema: preparerMetaSchema,
extractRefs: null, extractRefs: null,
}; };
@@ -31,13 +31,9 @@ Read the thread for context:
On any failure (push rejected, gh not authenticated, PR creation failed, etc.), report status="failed" with a short error message. Do not retry — surface the error so the moderator can decide.`; On any failure (push rejected, gh not authenticated, PR creation failed, etc.), report status="failed" with a short error message. Do not retry — surface the error so the moderator can decide.`;
const SUBMITTER_EXTRACT_PROMPT =
"Extract the submission result. status='submitted' with prUrl on success, or status='failed' with a short error message on failure.";
export const submitterRole: RoleDefinition<SubmitterMeta> = { export const submitterRole: RoleDefinition<SubmitterMeta> = {
description: "Pushes the developer's branch to the remote and opens a pull request.", description: "Pushes the developer's branch to the remote and opens a pull request.",
systemPrompt: SUBMITTER_SYSTEM, systemPrompt: SUBMITTER_SYSTEM,
extractPrompt: SUBMITTER_EXTRACT_PROMPT,
schema: submitterMetaSchema, schema: submitterMetaSchema,
extractRefs: null, extractRefs: null,
}; };