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 type { LlmProvider } from "@uncaged/workflow-util-role";
|
||||
import * as z from "zod/v4";
|
||||
@@ -40,7 +40,7 @@ export function createCoderRole(
|
||||
return createRole({
|
||||
name: "coder",
|
||||
schema: coderMetaSchema,
|
||||
systemPrompt: async (_ctx: ThreadContext) => coderSystemPrompt(config),
|
||||
systemPrompt: coderSystemPrompt(config),
|
||||
agent: adapter,
|
||||
extract: {
|
||||
provider: extract.provider,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow";
|
||||
import type { AgentFn, Role } from "@uncaged/workflow";
|
||||
import {
|
||||
createRole,
|
||||
decorateRole,
|
||||
@@ -46,13 +46,9 @@ function resolveExtractDryRun(extractDryRun: boolean | null): boolean {
|
||||
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}\`.
|
||||
|
||||
## Context
|
||||
|
||||
Use \`uncaged-workflow thread ${ctx.threadId}\` to read the full workflow thread for context on what was done and why.
|
||||
|
||||
## Task
|
||||
|
||||
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({
|
||||
name: "committer",
|
||||
schema: committerMetaSchema,
|
||||
systemPrompt: async (ctx) => committerSystemPrompt(ctx, config),
|
||||
systemPrompt: committerSystemPrompt(config),
|
||||
agent: adapter,
|
||||
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 type { LlmProvider } from "@uncaged/workflow-util-role";
|
||||
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.`;
|
||||
|
||||
function plannerSystemPrompt(_config: PlannerConfig): string {
|
||||
return PLANNER_SYSTEM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Planner role: produces ordered implementation phases for the coder to execute sequentially.
|
||||
*/
|
||||
export function createPlannerRole(
|
||||
adapter: AgentFn,
|
||||
extract: { provider: LlmProvider; dryRun: boolean | null; dryRunMeta: PlannerMeta },
|
||||
config: PlannerConfig = DEFAULT_PLANNER_CONFIG,
|
||||
_config: PlannerConfig = DEFAULT_PLANNER_CONFIG,
|
||||
): Role<PlannerMeta> {
|
||||
return createRole({
|
||||
name: "planner",
|
||||
schema: plannerMetaSchema,
|
||||
systemPrompt: async (_ctx: ThreadContext) => plannerSystemPrompt(config),
|
||||
systemPrompt: PLANNER_SYSTEM,
|
||||
agent: adapter,
|
||||
extract: {
|
||||
provider: extract.provider,
|
||||
|
||||
@@ -88,7 +88,7 @@ describe("createReviewerRole", () => {
|
||||
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 = (() =>
|
||||
Promise.resolve(
|
||||
toolCallResponse(JSON.stringify({ status: "approved" })),
|
||||
@@ -106,6 +106,8 @@ describe("createReviewerRole", () => {
|
||||
{ cwd: "/proj" },
|
||||
);
|
||||
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 type { LlmProvider } from "@uncaged/workflow-util-role";
|
||||
import * as z from "zod/v4";
|
||||
@@ -22,15 +22,11 @@ export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
|
||||
cwd: ".",
|
||||
};
|
||||
|
||||
function reviewerPrompt(config: ReviewerConfig, ctx: ThreadContext): string {
|
||||
function reviewerPrompt(config: ReviewerConfig): string {
|
||||
const { cwd } = config;
|
||||
|
||||
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
|
||||
|
||||
Review the current git diff in \`${cwd}\`. Give a clear **approve** or **reject** verdict.
|
||||
@@ -51,7 +47,7 @@ export function createReviewerRole(
|
||||
return createRole({
|
||||
name: "reviewer",
|
||||
schema: reviewerMetaSchema,
|
||||
systemPrompt: async (ctx) => reviewerPrompt(config, ctx),
|
||||
systemPrompt: reviewerPrompt(config),
|
||||
agent: adapter,
|
||||
extract: {
|
||||
provider: extract.provider,
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("buildAgentPrompt", () => {
|
||||
expect(text).toContain("only step full body");
|
||||
expect(text).toContain('Meta: {"files":["a.ts"]}');
|
||||
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", () => {
|
||||
@@ -78,6 +78,7 @@ describe("buildAgentPrompt", () => {
|
||||
expect(text).toContain("last step full content");
|
||||
expect(text).toContain('Meta: {"done":true}');
|
||||
expect(text).toContain("## Tools");
|
||||
expect(text).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
||||
});
|
||||
|
||||
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("## 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");
|
||||
}
|
||||
|
||||
@@ -96,26 +96,6 @@ describe("createRole", () => {
|
||||
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 () => {
|
||||
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>> = {
|
||||
name: string;
|
||||
schema: z.ZodType<M>;
|
||||
systemPrompt: string | ((ctx: ThreadContext) => Promise<string>);
|
||||
systemPrompt: string;
|
||||
agent: AgentFn;
|
||||
extract: {
|
||||
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. */
|
||||
export function createRole<M extends Record<string, unknown>>(args: CreateRoleArgs<M>): Role<M> {
|
||||
return async (ctx: ThreadContext) => {
|
||||
const promptText =
|
||||
typeof args.systemPrompt === "string" ? args.systemPrompt : await args.systemPrompt(ctx);
|
||||
const raw = await args.agent(ctx, promptText);
|
||||
const raw = await args.agent(ctx, args.systemPrompt);
|
||||
const meta = await extractMetaOrThrow(args.name, raw, args.schema, {
|
||||
provider: args.extract.provider,
|
||||
dryRun: resolveExtractDryRun(args.extract.dryRun),
|
||||
|
||||
Reference in New Issue
Block a user