refactor: systemPrompt → pure string, threadId injected by agent layer
- CreateRoleArgs.systemPrompt simplified to string (no more ctx callback) - buildAgentPrompt injects real ctx.threadId in Tools section - All four roles compute prompt at construction time from config only - Removed duplicate ctx.threadId injection from reviewer/committer prompts 小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow";
|
import type { AgentFn, Role } from "@uncaged/workflow";
|
||||||
import { createRole } from "@uncaged/workflow-agent-llm";
|
import { createRole } from "@uncaged/workflow-agent-llm";
|
||||||
import type { LlmProvider } from "@uncaged/workflow-util-role";
|
import type { LlmProvider } from "@uncaged/workflow-util-role";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
@@ -40,7 +40,7 @@ export function createCoderRole(
|
|||||||
return createRole({
|
return createRole({
|
||||||
name: "coder",
|
name: "coder",
|
||||||
schema: coderMetaSchema,
|
schema: coderMetaSchema,
|
||||||
systemPrompt: async (_ctx: ThreadContext) => coderSystemPrompt(config),
|
systemPrompt: coderSystemPrompt(config),
|
||||||
agent: adapter,
|
agent: adapter,
|
||||||
extract: {
|
extract: {
|
||||||
provider: extract.provider,
|
provider: extract.provider,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow";
|
import type { AgentFn, Role } from "@uncaged/workflow";
|
||||||
import {
|
import {
|
||||||
createRole,
|
createRole,
|
||||||
decorateRole,
|
decorateRole,
|
||||||
@@ -46,13 +46,9 @@ function resolveExtractDryRun(extractDryRun: boolean | null): boolean {
|
|||||||
return extractDryRun === true;
|
return extractDryRun === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function committerSystemPrompt(ctx: ThreadContext, config: CommitterConfig): string {
|
function committerSystemPrompt(config: CommitterConfig): string {
|
||||||
return `You are the git committer for this workflow. The project is at \`${config.cwd}\`.
|
return `You are the git committer for this workflow. The project is at \`${config.cwd}\`.
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Use \`uncaged-workflow thread ${ctx.threadId}\` to read the full workflow thread for context on what was done and why.
|
|
||||||
|
|
||||||
## Task
|
## Task
|
||||||
|
|
||||||
Create a branch, commit the changes, and push. Report whether the push succeeded or failed, the branch name, and the commit SHA.
|
Create a branch, commit the changes, and push. Report whether the push succeeded or failed, the branch name, and the commit SHA.
|
||||||
@@ -77,7 +73,7 @@ export function createCommitterRole(
|
|||||||
const inner: Role<CommitterMeta> = createRole({
|
const inner: Role<CommitterMeta> = createRole({
|
||||||
name: "committer",
|
name: "committer",
|
||||||
schema: committerMetaSchema,
|
schema: committerMetaSchema,
|
||||||
systemPrompt: async (ctx) => committerSystemPrompt(ctx, config),
|
systemPrompt: committerSystemPrompt(config),
|
||||||
agent: adapter,
|
agent: adapter,
|
||||||
extract,
|
extract,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow";
|
import type { AgentFn, Role } from "@uncaged/workflow";
|
||||||
import { createRole } from "@uncaged/workflow-agent-llm";
|
import { createRole } from "@uncaged/workflow-agent-llm";
|
||||||
import type { LlmProvider } from "@uncaged/workflow-util-role";
|
import type { LlmProvider } from "@uncaged/workflow-util-role";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
@@ -26,22 +26,18 @@ Each phase must have: a short **name** (stable identifier), a **description** of
|
|||||||
|
|
||||||
Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases. Do not emit separate file lists or a free-form "approach" field — put that detail inside phase descriptions.`;
|
Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases. Do not emit separate file lists or a free-form "approach" field — put that detail inside phase descriptions.`;
|
||||||
|
|
||||||
function plannerSystemPrompt(_config: PlannerConfig): string {
|
|
||||||
return PLANNER_SYSTEM;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Planner role: produces ordered implementation phases for the coder to execute sequentially.
|
* Planner role: produces ordered implementation phases for the coder to execute sequentially.
|
||||||
*/
|
*/
|
||||||
export function createPlannerRole(
|
export function createPlannerRole(
|
||||||
adapter: AgentFn,
|
adapter: AgentFn,
|
||||||
extract: { provider: LlmProvider; dryRun: boolean | null; dryRunMeta: PlannerMeta },
|
extract: { provider: LlmProvider; dryRun: boolean | null; dryRunMeta: PlannerMeta },
|
||||||
config: PlannerConfig = DEFAULT_PLANNER_CONFIG,
|
_config: PlannerConfig = DEFAULT_PLANNER_CONFIG,
|
||||||
): Role<PlannerMeta> {
|
): Role<PlannerMeta> {
|
||||||
return createRole({
|
return createRole({
|
||||||
name: "planner",
|
name: "planner",
|
||||||
schema: plannerMetaSchema,
|
schema: plannerMetaSchema,
|
||||||
systemPrompt: async (_ctx: ThreadContext) => plannerSystemPrompt(config),
|
systemPrompt: PLANNER_SYSTEM,
|
||||||
agent: adapter,
|
agent: adapter,
|
||||||
extract: {
|
extract: {
|
||||||
provider: extract.provider,
|
provider: extract.provider,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ describe("createReviewerRole", () => {
|
|||||||
expect(out.meta).toEqual({ status: "rejected", issues: ["secrets in code"] });
|
expect(out.meta).toEqual({ status: "rejected", issues: ["secrets in code"] });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("prompt includes threadId from context", async () => {
|
test("system prompt includes configured cwd (thread CLI hint comes from agent layer)", async () => {
|
||||||
globalThis.fetch = (() =>
|
globalThis.fetch = (() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
toolCallResponse(JSON.stringify({ status: "approved" })),
|
toolCallResponse(JSON.stringify({ status: "approved" })),
|
||||||
@@ -106,6 +106,8 @@ describe("createReviewerRole", () => {
|
|||||||
{ cwd: "/proj" },
|
{ cwd: "/proj" },
|
||||||
);
|
);
|
||||||
await role(makeCtx());
|
await role(makeCtx());
|
||||||
expect(seen).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
expect(seen).toContain("/proj");
|
||||||
|
expect(seen).toContain("code reviewer");
|
||||||
|
expect(seen).not.toContain("uncaged-workflow thread");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow";
|
import type { AgentFn, Role } from "@uncaged/workflow";
|
||||||
import { createRole } from "@uncaged/workflow-agent-llm";
|
import { createRole } from "@uncaged/workflow-agent-llm";
|
||||||
import type { LlmProvider } from "@uncaged/workflow-util-role";
|
import type { LlmProvider } from "@uncaged/workflow-util-role";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
@@ -22,15 +22,11 @@ export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
|
|||||||
cwd: ".",
|
cwd: ".",
|
||||||
};
|
};
|
||||||
|
|
||||||
function reviewerPrompt(config: ReviewerConfig, ctx: ThreadContext): string {
|
function reviewerPrompt(config: ReviewerConfig): string {
|
||||||
const { cwd } = config;
|
const { cwd } = config;
|
||||||
|
|
||||||
return `You are a code reviewer. The project is at \`${cwd}\`.
|
return `You are a code reviewer. The project is at \`${cwd}\`.
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Use \`uncaged-workflow thread ${ctx.threadId}\` to read the full workflow thread for context on what was done and why.
|
|
||||||
|
|
||||||
## Task
|
## Task
|
||||||
|
|
||||||
Review the current git diff in \`${cwd}\`. Give a clear **approve** or **reject** verdict.
|
Review the current git diff in \`${cwd}\`. Give a clear **approve** or **reject** verdict.
|
||||||
@@ -51,7 +47,7 @@ export function createReviewerRole(
|
|||||||
return createRole({
|
return createRole({
|
||||||
name: "reviewer",
|
name: "reviewer",
|
||||||
schema: reviewerMetaSchema,
|
schema: reviewerMetaSchema,
|
||||||
systemPrompt: async (ctx) => reviewerPrompt(config, ctx),
|
systemPrompt: reviewerPrompt(config),
|
||||||
agent: adapter,
|
agent: adapter,
|
||||||
extract: {
|
extract: {
|
||||||
provider: extract.provider,
|
provider: extract.provider,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ describe("buildAgentPrompt", () => {
|
|||||||
expect(text).toContain("only step full body");
|
expect(text).toContain("only step full body");
|
||||||
expect(text).toContain('Meta: {"files":["a.ts"]}');
|
expect(text).toContain('Meta: {"files":["a.ts"]}');
|
||||||
expect(text).toContain("## Tools");
|
expect(text).toContain("## Tools");
|
||||||
expect(text).toContain("uncaged-workflow thread <threadId>");
|
expect(text).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("two or more steps: previous steps are meta-only; latest step is full", () => {
|
test("two or more steps: previous steps are meta-only; latest step is full", () => {
|
||||||
@@ -78,6 +78,7 @@ describe("buildAgentPrompt", () => {
|
|||||||
expect(text).toContain("last step full content");
|
expect(text).toContain("last step full content");
|
||||||
expect(text).toContain('Meta: {"done":true}');
|
expect(text).toContain('Meta: {"done":true}');
|
||||||
expect(text).toContain("## Tools");
|
expect(text).toContain("## Tools");
|
||||||
|
expect(text).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("middle steps show meta summary only, not full content", () => {
|
test("middle steps show meta summary only, not full content", () => {
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export function buildAgentPrompt(systemPrompt: string, ctx: ThreadContext): stri
|
|||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("## Tools");
|
lines.push("## Tools");
|
||||||
lines.push("Use `uncaged-workflow thread <threadId>` to read full details of any previous step.");
|
lines.push(
|
||||||
|
`Use \`uncaged-workflow thread ${ctx.threadId}\` to read full details of any previous step.`,
|
||||||
|
);
|
||||||
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,26 +96,6 @@ describe("createRole", () => {
|
|||||||
expect(seen[0].steps).toEqual([]);
|
expect(seen[0].steps).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resolves dynamic systemPrompt functions before AgentFn", async () => {
|
|
||||||
globalThis.fetch = (() =>
|
|
||||||
Promise.resolve(toolCallResponse(JSON.stringify({ n: 99 })))) as unknown as typeof fetch;
|
|
||||||
|
|
||||||
const schema = z.object({ n: z.number() });
|
|
||||||
const agent: AgentFn = async (_ctx, prompt) => prompt;
|
|
||||||
const role = createRole({
|
|
||||||
name: "test",
|
|
||||||
schema,
|
|
||||||
systemPrompt: async (ctx) => `rounds=${ctx.steps.length}`,
|
|
||||||
agent,
|
|
||||||
extract: { provider, dryRun: null, dryRunMeta: { n: 0 } },
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx = makeCtx();
|
|
||||||
const out = await role(ctx);
|
|
||||||
expect(out.content).toBe("rounds=0");
|
|
||||||
expect(out.meta).toEqual({ n: 99 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("extract dryRun null runs live extract path", async () => {
|
test("extract dryRun null runs live extract path", async () => {
|
||||||
const spy = spyOn(extractMetaModule, "extractMetaOrThrow").mockResolvedValue({ n: 0 });
|
const spy = spyOn(extractMetaModule, "extractMetaOrThrow").mockResolvedValue({ n: 0 });
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { LlmProvider } from "./types.js";
|
|||||||
export type CreateRoleArgs<M extends Record<string, unknown>> = {
|
export type CreateRoleArgs<M extends Record<string, unknown>> = {
|
||||||
name: string;
|
name: string;
|
||||||
schema: z.ZodType<M>;
|
schema: z.ZodType<M>;
|
||||||
systemPrompt: string | ((ctx: ThreadContext) => Promise<string>);
|
systemPrompt: string;
|
||||||
agent: AgentFn;
|
agent: AgentFn;
|
||||||
extract: {
|
extract: {
|
||||||
provider: LlmProvider;
|
provider: LlmProvider;
|
||||||
@@ -23,9 +23,7 @@ function resolveExtractDryRun(extractDryRun: boolean | null): boolean {
|
|||||||
/** Builds a {@link Role} from an {@link AgentFn}, system prompt, Zod meta schema, and extract wiring. */
|
/** Builds a {@link Role} from an {@link AgentFn}, system prompt, Zod meta schema, and extract wiring. */
|
||||||
export function createRole<M extends Record<string, unknown>>(args: CreateRoleArgs<M>): Role<M> {
|
export function createRole<M extends Record<string, unknown>>(args: CreateRoleArgs<M>): Role<M> {
|
||||||
return async (ctx: ThreadContext) => {
|
return async (ctx: ThreadContext) => {
|
||||||
const promptText =
|
const raw = await args.agent(ctx, args.systemPrompt);
|
||||||
typeof args.systemPrompt === "string" ? args.systemPrompt : await args.systemPrompt(ctx);
|
|
||||||
const raw = await args.agent(ctx, promptText);
|
|
||||||
const meta = await extractMetaOrThrow(args.name, raw, args.schema, {
|
const meta = await extractMetaOrThrow(args.name, raw, args.schema, {
|
||||||
provider: args.extract.provider,
|
provider: args.extract.provider,
|
||||||
dryRun: resolveExtractDryRun(args.extract.dryRun),
|
dryRun: resolveExtractDryRun(args.extract.dryRun),
|
||||||
|
|||||||
Reference in New Issue
Block a user