fix: address review feedback on cursor agent PR
1. Replace manual OpenAI response parsing with createThreadReactor — workspace extraction now uses the same ReAct loop as extract/summarizer, with a zod schema and structured tool call 2. Remove non-null assertion on llmProvider, replaced with explicit guard 3. Add structured logging (LogFn) to extraction — failures, non-absolute paths, and successful extractions all logged with tag V3KM8QWP 4. export const run = wf is correct: createWorkflow returns WorkflowFn directly, old bundle-entry had stale .run access + unused 3rd arg 小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"@uncaged/workflow-protocol": "workspace:*",
|
||||
"@uncaged/workflow-reactor": "workspace:*",
|
||||
"@uncaged/workflow-runtime": "workspace:*",
|
||||
"@uncaged/workflow-util": "workspace:*",
|
||||
"@uncaged/workflow-util-agent": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { AgentContext, LlmProvider } from "@uncaged/workflow-protocol";
|
||||
import { createLlmFn } from "@uncaged/workflow-reactor";
|
||||
import type { ChatMessage } from "@uncaged/workflow-reactor";
|
||||
import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor";
|
||||
import type { LogFn } from "@uncaged/workflow-util";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
const EXTRACT_SYSTEM = `You are a workspace-path extractor. Given a workflow agent context (task description and previous step outputs), identify the absolute filesystem path of the project workspace where code changes should be made.
|
||||
const workspaceSchema = z.object({
|
||||
workspace: z.string().describe("Absolute filesystem path of the project workspace"),
|
||||
});
|
||||
|
||||
Reply with ONLY the absolute path, nothing else. Example: /home/user/repos/my-project
|
||||
|
||||
If you cannot determine the workspace path, reply with: UNKNOWN`;
|
||||
const EXTRACT_SYSTEM_FN = (_toolName: string) =>
|
||||
`You are a workspace-path extractor. Given a workflow agent context (task description and previous step outputs), identify the absolute filesystem path of the project workspace where code changes should be made. Call the tool with the absolute path.`;
|
||||
|
||||
function buildExtractionInput(ctx: AgentContext): string {
|
||||
const lines: string[] = [];
|
||||
@@ -25,59 +27,47 @@ function buildExtractionInput(ctx: AgentContext): string {
|
||||
export async function extractWorkspacePath(
|
||||
ctx: AgentContext,
|
||||
provider: LlmProvider,
|
||||
logger: LogFn,
|
||||
): Promise<string | null> {
|
||||
const llm = createLlmFn(provider);
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: EXTRACT_SYSTEM },
|
||||
{ role: "user", content: buildExtractionInput(ctx) },
|
||||
];
|
||||
const reactor = createThreadReactor<null>({
|
||||
llm: createLlmFn(provider),
|
||||
maxRounds: 2,
|
||||
staticTools: [],
|
||||
structuredToolFromSchema: (schema) => {
|
||||
const jsonSchema = z.toJSONSchema(schema);
|
||||
return {
|
||||
name: "set_workspace",
|
||||
tool: {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "set_workspace",
|
||||
description: "Set the extracted workspace path",
|
||||
parameters: jsonSchema as Record<string, unknown>,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
systemPromptForStructuredTool: EXTRACT_SYSTEM_FN,
|
||||
toolHandler: async () => "unknown tool",
|
||||
});
|
||||
|
||||
const result = await reactor({
|
||||
thread: null,
|
||||
input: buildExtractionInput(ctx),
|
||||
schema: workspaceSchema,
|
||||
});
|
||||
|
||||
const result = await llm({ messages, tools: [] });
|
||||
if (!result.ok) {
|
||||
logger("V3KM8QWP", `workspace extraction failed: ${result.error}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(result.value) as unknown;
|
||||
} catch {
|
||||
const workspace = result.value.workspace.trim();
|
||||
if (!workspace.startsWith("/")) {
|
||||
logger("V3KM8QWP", `workspace extraction returned non-absolute path: ${workspace}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsed !== "object" ||
|
||||
parsed === null ||
|
||||
!("choices" in parsed) ||
|
||||
!Array.isArray((parsed as Record<string, unknown>).choices)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const choices = (parsed as Record<string, unknown>).choices as unknown[];
|
||||
if (choices.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const first = choices[0];
|
||||
if (
|
||||
typeof first !== "object" ||
|
||||
first === null ||
|
||||
!("message" in first) ||
|
||||
typeof (first as Record<string, unknown>).message !== "object"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = (first as Record<string, unknown>).message as Record<string, unknown>;
|
||||
const content = message.content;
|
||||
if (typeof content !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = content.trim();
|
||||
if (trimmed === "UNKNOWN" || !trimmed.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
logger("V3KM8QWP", `extracted workspace: ${workspace}`);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentFn } from "@uncaged/workflow-runtime";
|
||||
import { createLogger } from "@uncaged/workflow-util";
|
||||
import { buildAgentPrompt, type SpawnCliError, spawnCli } from "@uncaged/workflow-util-agent";
|
||||
|
||||
import { extractWorkspacePath } from "./extract-workspace.js";
|
||||
@@ -36,6 +37,7 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
||||
|
||||
const modelFlag = resolveCursorModel(config.model);
|
||||
const timeoutMs = config.timeout > 0 ? config.timeout : null;
|
||||
const logger = createLogger({ sink: { kind: "stderr" } });
|
||||
|
||||
return async (ctx) => {
|
||||
let workspace: string;
|
||||
@@ -43,7 +45,10 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
||||
if (config.workspace !== null) {
|
||||
workspace = config.workspace;
|
||||
} else {
|
||||
const extracted = await extractWorkspacePath(ctx, config.llmProvider!);
|
||||
if (config.llmProvider === null) {
|
||||
throw new Error("cursor-agent: llmProvider is required when workspace is null");
|
||||
}
|
||||
const extracted = await extractWorkspacePath(ctx, config.llmProvider, logger);
|
||||
if (extracted === null) {
|
||||
throw new Error(
|
||||
"cursor-agent: failed to extract workspace path from context. Provide an explicit workspace or ensure previous steps include a repoPath.",
|
||||
@@ -52,6 +57,7 @@ export function createCursorAgent(config: CursorAgentConfig): AgentFn {
|
||||
workspace = extracted;
|
||||
}
|
||||
|
||||
logger("R5HN3YKQ", `cursor-agent workspace: ${workspace}`);
|
||||
const fullPrompt = await buildAgentPrompt(ctx);
|
||||
const args = [
|
||||
"-p",
|
||||
|
||||
Reference in New Issue
Block a user