refactor: extract @uncaged/workflow-util-agent + smart prompt
- New package: spawn-cli + build-agent-prompt shared utils - Smart prompt: start + meta summaries for middle steps + last step full - Cursor/Hermes adapters now import from util-agent (no duplicate code) - 109 tests pass, biome clean Closes #14 小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -1,26 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { START, type ThreadContext } from "@uncaged/workflow";
|
||||
|
||||
import { buildAgentPrompt, createCursorAgent, validateCursorAgentConfig } from "../src/index.js";
|
||||
|
||||
function makeCtx(): ThreadContext {
|
||||
return {
|
||||
start: {
|
||||
role: START,
|
||||
content: "user task",
|
||||
meta: { maxRounds: 5 },
|
||||
timestamp: 1,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
role: "coder",
|
||||
content: "first draft",
|
||||
meta: {},
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
import { createCursorAgent, validateCursorAgentConfig } from "../src/index.js";
|
||||
|
||||
describe("validateCursorAgentConfig", () => {
|
||||
test("accepts valid config", () => {
|
||||
@@ -54,16 +33,6 @@ describe("validateCursorAgentConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAgentPrompt", () => {
|
||||
test("includes system prompt, start, and steps", () => {
|
||||
const text = buildAgentPrompt(makeCtx(), "Be helpful.");
|
||||
expect(text).toContain("Be helpful.");
|
||||
expect(text).toContain("user task");
|
||||
expect(text).toContain("coder");
|
||||
expect(text).toContain("first draft");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createCursorAgent", () => {
|
||||
test("returns an AgentFn", () => {
|
||||
const agent = createCursorAgent({
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*"
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"@uncaged/workflow-util-agent": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { ThreadContext } from "@uncaged/workflow";
|
||||
|
||||
/** Combines the role system prompt with thread start content and prior role outputs. */
|
||||
export function buildAgentPrompt(ctx: ThreadContext, systemPrompt: string): string {
|
||||
const blocks: string[] = [];
|
||||
blocks.push("# System instructions");
|
||||
blocks.push(systemPrompt);
|
||||
blocks.push("");
|
||||
blocks.push("# Thread");
|
||||
blocks.push("## Start");
|
||||
blocks.push(ctx.start.content);
|
||||
for (const step of ctx.steps) {
|
||||
blocks.push(`## Role: ${step.role}`);
|
||||
blocks.push(step.content);
|
||||
}
|
||||
return blocks.join("\n");
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { AgentFn } from "@uncaged/workflow";
|
||||
import { buildAgentPrompt, type SpawnCliError, spawnCli } from "@uncaged/workflow-util-agent";
|
||||
|
||||
import { buildAgentPrompt } from "./build-agent-prompt.js";
|
||||
import { type SpawnCliError, spawnCli } from "./spawn-cli.js";
|
||||
import type { CursorAgentConfig } from "./types.js";
|
||||
import { validateCursorAgentConfig } from "./validate-config.js";
|
||||
|
||||
export { buildAgentPrompt } from "./build-agent-prompt.js";
|
||||
export { buildAgentPrompt } from "@uncaged/workflow-util-agent";
|
||||
export type { CursorAgentConfig } from "./types.js";
|
||||
export { validateCursorAgentConfig } from "./validate-config.js";
|
||||
|
||||
@@ -39,7 +38,7 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
||||
const timeoutMs = config.timeout;
|
||||
|
||||
return async (ctx, systemPrompt) => {
|
||||
const fullPrompt = buildAgentPrompt(ctx, systemPrompt);
|
||||
const fullPrompt = buildAgentPrompt(systemPrompt, ctx);
|
||||
const args = [
|
||||
"-p",
|
||||
fullPrompt,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export type SpawnCliError =
|
||||
| { kind: "non_zero_exit"; exitCode: number | null; stdout: string; stderr: string }
|
||||
| { kind: "timeout" }
|
||||
| { kind: "spawn_failed"; message: string };
|
||||
|
||||
export function spawnCli(
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { cwd: string | null; timeoutMs: number | null },
|
||||
): Promise<Result<string, SpawnCliError>> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd === null ? undefined : options.cwd,
|
||||
shell: false,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout?.on("data", (chunk: Buffer) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", (chunk: Buffer) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
let timedOut = false;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
if (options.timeoutMs !== null) {
|
||||
timeoutId = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill("SIGTERM");
|
||||
}, options.timeoutMs);
|
||||
}
|
||||
|
||||
child.on("error", (cause) => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
const message = cause instanceof Error ? cause.message : String(cause);
|
||||
resolve(err({ kind: "spawn_failed", message }));
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
if (timedOut) {
|
||||
resolve(err({ kind: "timeout" }));
|
||||
return;
|
||||
}
|
||||
if (code === 0) {
|
||||
resolve(ok(stdout));
|
||||
return;
|
||||
}
|
||||
resolve(err({ kind: "non_zero_exit", exitCode: code, stdout, stderr }));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [{ "path": "../workflow" }]
|
||||
"references": [{ "path": "../workflow" }, { "path": "../workflow-util-agent" }]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user