feat(agent): implement cursor-cli, pi-agent, and stub backend adapters
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
9bcf458529
commit
6ee1c5b6c7
@@ -0,0 +1,39 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/config"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
WorkDir string `json:"work_dir"`
|
||||
Prompt string `json:"prompt"`
|
||||
Instructions string `json:"instructions"`
|
||||
Targets []string `json:"targets"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Success bool `json:"success"`
|
||||
Patches map[string][]byte `json:"patches"`
|
||||
Logs string `json:"logs"`
|
||||
ErrorReason string `json:"error_reason"`
|
||||
}
|
||||
|
||||
type Backend interface {
|
||||
Name() string
|
||||
Execute(ctx context.Context, req Request) (*Result, error)
|
||||
}
|
||||
|
||||
func NewBackend(name string, cfg *config.Config) Backend {
|
||||
switch name {
|
||||
case "pi-agent":
|
||||
return &PiAgentBackend{cfg: cfg}
|
||||
case "cursor-cli":
|
||||
return &CursorCLIBackend{cfg: cfg}
|
||||
default:
|
||||
return &StubBackend{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package agent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/agent"
|
||||
"chat-agent-system/internal/config"
|
||||
)
|
||||
|
||||
func TestStubBackendExecution(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &config.Config{AgentBackend: "stub"}
|
||||
|
||||
backend := agent.NewBackend("stub", cfg)
|
||||
if backend.Name() != "stub" {
|
||||
t.Errorf("expected stub backend, got %s", backend.Name())
|
||||
}
|
||||
|
||||
req := agent.Request{
|
||||
WorkDir: tmpDir,
|
||||
Prompt: "请编写用户初始需求收集表",
|
||||
Targets: []string{"output.md"},
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
res, err := backend.Execute(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("stub execute failed: %v", err)
|
||||
}
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success")
|
||||
}
|
||||
|
||||
outputPath := filepath.Join(tmpDir, "output.md")
|
||||
if _, err := os.Stat(outputPath); err != nil {
|
||||
t.Fatalf("expected output.md to exist: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"chat-agent-system/internal/config"
|
||||
)
|
||||
|
||||
type CursorCLIBackend struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func (c *CursorCLIBackend) Name() string { return "cursor-cli" }
|
||||
|
||||
func (c *CursorCLIBackend) Execute(ctx context.Context, req Request) (*Result, error) {
|
||||
if len(req.Targets) == 0 {
|
||||
req.Targets = []string{"output.md"}
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"workspace": req.WorkDir,
|
||||
"api_key": c.cfg.CursorAPIKey,
|
||||
"model": c.cfg.CursorModel,
|
||||
"prompt": req.Prompt,
|
||||
"targets": req.Targets,
|
||||
})
|
||||
|
||||
bridgeBin := c.cfg.CursorBridgeBin
|
||||
if !filepath.IsAbs(bridgeBin) {
|
||||
wd, _ := os.Getwd()
|
||||
bridgeBin = filepath.Join(wd, bridgeBin)
|
||||
}
|
||||
|
||||
timeout := req.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = c.cfg.AgentTimeout
|
||||
}
|
||||
runCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(runCtx, bridgeBin)
|
||||
cmd.Dir = req.WorkDir
|
||||
cmd.Stdin = bytes.NewReader(payload)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"CURSOR_API_KEY="+c.cfg.CursorAPIKey,
|
||||
"CURSOR_MODEL="+c.cfg.CursorModel,
|
||||
)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return &Result{
|
||||
Success: false,
|
||||
ErrorReason: fmt.Sprintf("cursor execution error: %v, stderr: %s", err, stderr.String()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
patches := make(map[string][]byte)
|
||||
for _, t := range req.Targets {
|
||||
p := filepath.Join(req.WorkDir, t)
|
||||
if b, err := os.ReadFile(p); err == nil {
|
||||
patches[t] = b
|
||||
}
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Success: true,
|
||||
Patches: patches,
|
||||
Logs: stdout.String(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"chat-agent-system/internal/config"
|
||||
)
|
||||
|
||||
type PiAgentBackend struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func (p *PiAgentBackend) Name() string { return "pi-agent" }
|
||||
|
||||
func (p *PiAgentBackend) Execute(ctx context.Context, req Request) (*Result, error) {
|
||||
if len(req.Targets) == 0 {
|
||||
req.Targets = []string{"output.md"}
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"workspace": req.WorkDir,
|
||||
"model": p.cfg.PiAgentModel,
|
||||
"provider": p.cfg.PiAgentProvider,
|
||||
"prompt": req.Prompt,
|
||||
"targets": req.Targets,
|
||||
})
|
||||
|
||||
bridgeBin := p.cfg.PiAgentBridgeBin
|
||||
if !filepath.IsAbs(bridgeBin) {
|
||||
wd, _ := os.Getwd()
|
||||
bridgeBin = filepath.Join(wd, bridgeBin)
|
||||
}
|
||||
|
||||
timeout := req.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = p.cfg.AgentTimeout
|
||||
}
|
||||
runCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(runCtx, bridgeBin)
|
||||
cmd.Dir = req.WorkDir
|
||||
cmd.Stdin = bytes.NewReader(payload)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"PI_AGENT_BIN="+p.cfg.PiAgentBin,
|
||||
"PI_AGENT_MODEL="+p.cfg.PiAgentModel,
|
||||
"PI_AGENT_PROVIDER="+p.cfg.PiAgentProvider,
|
||||
)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return &Result{
|
||||
Success: false,
|
||||
ErrorReason: fmt.Sprintf("pi execution error: %v, stderr: %s", err, stderr.String()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
patches := make(map[string][]byte)
|
||||
for _, t := range req.Targets {
|
||||
p := filepath.Join(req.WorkDir, t)
|
||||
if b, err := os.ReadFile(p); err == nil {
|
||||
patches[t] = b
|
||||
}
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Success: true,
|
||||
Patches: patches,
|
||||
Logs: stdout.String(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type StubBackend struct{}
|
||||
|
||||
func (s *StubBackend) Name() string { return "stub" }
|
||||
|
||||
func (s *StubBackend) Execute(ctx context.Context, req Request) (*Result, error) {
|
||||
patches := make(map[string][]byte)
|
||||
for _, target := range req.Targets {
|
||||
content := []byte(fmt.Sprintf("# 模拟生成结果\n\n- 目标文件: %s\n- 提示词摘要: %s\n", target, req.Prompt))
|
||||
outPath := filepath.Join(req.WorkDir, target)
|
||||
_ = os.MkdirAll(filepath.Dir(outPath), 0o755)
|
||||
_ = os.WriteFile(outPath, content, 0o644)
|
||||
patches[target] = content
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Success: true,
|
||||
Patches: patches,
|
||||
Logs: "Stub agent execution completed.",
|
||||
}, nil
|
||||
}
|
||||
Executable
+188
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Cursor SDK bridge (stdio JSON).
|
||||
*
|
||||
* Input (stdin):
|
||||
* {
|
||||
* "workspace": "/abs/path",
|
||||
* "api_key": "...",
|
||||
* "model": "auto",
|
||||
* "prompt": "...",
|
||||
* "targets": ["docs/RD-UN.md"]
|
||||
* }
|
||||
*
|
||||
* Output (stdout, last line JSON):
|
||||
* {
|
||||
* "ok": true,
|
||||
* "agent_id": "...",
|
||||
* "run_id": "...",
|
||||
* "patches": [{ "rel_path": "docs/RD-UN.md", "content": "..." }],
|
||||
* "fallback_reason": ""
|
||||
* }
|
||||
*/
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { resolve, relative, isAbsolute } from "node:path";
|
||||
import { Agent, CursorAgentError } from "@cursor/sdk";
|
||||
|
||||
async function readStdin() {
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
function fail(reason, extra = {}) {
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
patches: [],
|
||||
fallback_reason: reason,
|
||||
...extra,
|
||||
}) + "\n",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function sanitizeApiKey(raw) {
|
||||
if (!raw) return "";
|
||||
let key = String(raw).trim();
|
||||
const commentIdx = key.indexOf(" #");
|
||||
if (commentIdx !== -1) {
|
||||
key = key.slice(0, commentIdx).trim();
|
||||
}
|
||||
const tabCommentIdx = key.indexOf("\t#");
|
||||
if (tabCommentIdx !== -1) {
|
||||
key = key.slice(0, tabCommentIdx).trim();
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function safeRel(workspace, target) {
|
||||
const abs = isAbsolute(target) ? target : resolve(workspace, target);
|
||||
const rel = relative(workspace, abs);
|
||||
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
||||
return null;
|
||||
}
|
||||
return rel.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let req;
|
||||
try {
|
||||
const raw = await readStdin();
|
||||
req = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
fail("invalid_json: " + (e && e.message ? e.message : String(e)));
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = req.workspace;
|
||||
const apiKey = sanitizeApiKey(req.api_key || process.env.CURSOR_API_KEY || "");
|
||||
const model = (req.model || process.env.CURSOR_MODEL || "auto").trim();
|
||||
const prompt = req.prompt || "";
|
||||
const targets = Array.isArray(req.targets) ? req.targets : [];
|
||||
|
||||
if (!workspace || !existsSync(workspace)) {
|
||||
fail("workspace_missing");
|
||||
return;
|
||||
}
|
||||
if (!apiKey) {
|
||||
fail("api_key_missing");
|
||||
return;
|
||||
}
|
||||
if (!prompt.trim()) {
|
||||
fail("prompt_empty");
|
||||
return;
|
||||
}
|
||||
|
||||
const allowed = new Set();
|
||||
for (const t of targets) {
|
||||
const rel = safeRel(workspace, t);
|
||||
if (rel) allowed.add(rel);
|
||||
}
|
||||
if (allowed.size === 0) {
|
||||
fail("no_targets");
|
||||
return;
|
||||
}
|
||||
|
||||
let agentId = "";
|
||||
let runId = "";
|
||||
try {
|
||||
const result = await Agent.prompt(prompt, {
|
||||
apiKey,
|
||||
model: { id: model },
|
||||
local: {
|
||||
cwd: workspace,
|
||||
settingSources: [],
|
||||
},
|
||||
});
|
||||
agentId = result?.agentId || result?.agent_id || "";
|
||||
runId = result?.id || result?.runId || result?.run_id || "";
|
||||
if (result?.status === "error") {
|
||||
fail("run_error", { agent_id: agentId, run_id: runId });
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
const rawMsg =
|
||||
e instanceof CursorAgentError
|
||||
? e.message
|
||||
: e && e.message
|
||||
? e.message
|
||||
: String(e);
|
||||
let category = "startup_or_run";
|
||||
const lower = rawMsg.toLowerCase();
|
||||
if (
|
||||
lower.includes("invalid user api key") ||
|
||||
lower.includes("unauthorized") ||
|
||||
lower.includes("401") ||
|
||||
lower.includes("authentication")
|
||||
) {
|
||||
category = "cursor_auth_failed";
|
||||
} else if (
|
||||
lower.includes("timeout") ||
|
||||
lower.includes("timed out") ||
|
||||
lower.includes("deadline")
|
||||
) {
|
||||
category = "cursor_timeout";
|
||||
} else if (
|
||||
lower.includes("enotfound") ||
|
||||
lower.includes("econnrefused") ||
|
||||
lower.includes("fetch failed") ||
|
||||
lower.includes("network")
|
||||
) {
|
||||
category = "cursor_network_error";
|
||||
} else if (lower.includes("cannot use this model") || lower.includes("available models")) {
|
||||
category = "cursor_model_unsupported";
|
||||
}
|
||||
fail(category + ": " + rawMsg, { agent_id: agentId, run_id: runId });
|
||||
return;
|
||||
}
|
||||
|
||||
const patches = [];
|
||||
for (const rel of allowed) {
|
||||
const abs = resolve(workspace, rel);
|
||||
if (!existsSync(abs)) continue;
|
||||
const content = readFileSync(abs, "utf8");
|
||||
patches.push({ rel_path: rel, content });
|
||||
}
|
||||
|
||||
if (patches.length === 0) {
|
||||
fail("empty_patches", { agent_id: agentId, run_id: runId });
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
agent_id: agentId,
|
||||
run_id: runId,
|
||||
patches,
|
||||
fallback_reason: "",
|
||||
}) + "\n",
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
fail("unhandled: " + (e && e.message ? e.message : String(e)));
|
||||
});
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* pi-coding-agent bridge (stdio JSON).
|
||||
*
|
||||
* Input (stdin):
|
||||
* {
|
||||
* "workspace": "/abs/path",
|
||||
* "model": "claude-sonnet-4-5",
|
||||
* "prompt": "...",
|
||||
* "targets": ["docs/RD-UN.md"]
|
||||
* }
|
||||
*
|
||||
* Output (stdout, last line JSON): 同 cursor-bridge / opencode-bridge 协议
|
||||
*
|
||||
* 设计:与 cursor-bridge / opencode-bridge 完全相同的协议层。
|
||||
*
|
||||
* 调用的 pi 命令:
|
||||
* pi --mode rpc --no-session --provider <p> --model <m>
|
||||
* --append-system-prompt INSTRUCTIONS.md
|
||||
* --tools read,write,edit --approve
|
||||
*
|
||||
* 工作目录:Go 端 cmd.Dir 设(pi 没有 --cwd flag;node 桥 spawn 时再覆盖一次)
|
||||
*
|
||||
* 完成信号:
|
||||
* - agent_settled → 成功("最稳妥" 标志,自动重试/队列全走完才算)
|
||||
* - agent_error → 失败
|
||||
* - turn_end → 单轮结束,不一定等于整体任务完成
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { resolve, relative, isAbsolute } from "node:path";
|
||||
|
||||
async function readStdin() {
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
function fail(reason, extra = {}, stderr = "") {
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
patches: [],
|
||||
fallback_reason: reason,
|
||||
stderr: stderr ? stderr.slice(0, 400) : undefined,
|
||||
...extra,
|
||||
}) + "\n",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function safeRel(workspace, target) {
|
||||
const abs = isAbsolute(target) ? target : resolve(workspace, target);
|
||||
const rel = relative(workspace, abs);
|
||||
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
||||
return null;
|
||||
}
|
||||
return rel.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let req;
|
||||
try {
|
||||
const raw = await readStdin();
|
||||
req = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
fail("invalid_json: " + (e && e.message ? e.message : String(e)));
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = req.workspace;
|
||||
const model = req.model || process.env.PI_AGENT_MODEL || "claude-sonnet-4-5";
|
||||
const provider = req.provider || process.env.PI_AGENT_PROVIDER || "anthropic";
|
||||
const prompt = req.prompt || "";
|
||||
const targets = Array.isArray(req.targets) ? req.targets : [];
|
||||
|
||||
if (!workspace || !existsSync(workspace)) {
|
||||
fail("workspace_missing");
|
||||
return;
|
||||
}
|
||||
if (!prompt.trim()) {
|
||||
fail("prompt_empty");
|
||||
return;
|
||||
}
|
||||
|
||||
const allowed = new Set();
|
||||
for (const t of targets) {
|
||||
const rel = safeRel(workspace, t);
|
||||
if (rel) allowed.add(rel);
|
||||
}
|
||||
if (allowed.size === 0) {
|
||||
fail("no_targets");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. spawn pi --mode rpc
|
||||
const piBin = process.env.PI_AGENT_BIN || "pi";
|
||||
const args = [
|
||||
"--mode", "rpc",
|
||||
"--no-session",
|
||||
"--provider", provider,
|
||||
"--model", model,
|
||||
];
|
||||
|
||||
const sysPromptFile = process.env.PI_AGENT_SYSTEM_PROMPT_FILE || "INSTRUCTIONS.md";
|
||||
const absSysPrompt = resolve(workspace, sysPromptFile);
|
||||
if (existsSync(absSysPrompt)) {
|
||||
args.push("--append-system-prompt", sysPromptFile);
|
||||
}
|
||||
|
||||
args.push(
|
||||
"--tools", "read,write,edit",
|
||||
"--approve",
|
||||
);
|
||||
|
||||
const env = { ...process.env };
|
||||
env.PI_AGENT_MODEL = model;
|
||||
env.PI_AGENT_PROVIDER = provider;
|
||||
|
||||
let agentId = "";
|
||||
let runId = "";
|
||||
let lastErr = "";
|
||||
let succeeded = false;
|
||||
|
||||
try {
|
||||
const child = spawn(piBin, args, {
|
||||
cwd: workspace,
|
||||
env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
// 2. 写 JSONL prompt 命令到 stdin
|
||||
const cmdId = "req-" + Date.now();
|
||||
child.stdin.write(
|
||||
JSON.stringify({
|
||||
id: cmdId,
|
||||
type: "prompt",
|
||||
message: prompt,
|
||||
}) + "\n",
|
||||
);
|
||||
|
||||
// 3. 按行解析 ndjson 事件流
|
||||
let buf = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
buf += chunk.toString("utf8");
|
||||
let nl;
|
||||
while ((nl = buf.indexOf("\n")) >= 0) {
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (!line) continue;
|
||||
let ev;
|
||||
try { ev = JSON.parse(line); } catch { continue; }
|
||||
if (!ev || typeof ev !== "object") continue;
|
||||
const t = ev.type || "";
|
||||
if (t === "session" && ev.id) {
|
||||
agentId = ev.id;
|
||||
}
|
||||
if (t === "agent_settled") {
|
||||
succeeded = true;
|
||||
// agent_settled 表示任务真正结束;关闭 stdin 让 pi 退出
|
||||
child.stdin.end();
|
||||
}
|
||||
if (t === "agent_error") {
|
||||
lastErr = ev.error?.message || ev.message || "agent_error";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stderrBuf = "";
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderrBuf += chunk.toString("utf8");
|
||||
lastErr = (stderrBuf + " " + lastErr).slice(-400);
|
||||
});
|
||||
|
||||
await new Promise((resolveDone) => {
|
||||
child.on("close", () => resolveDone());
|
||||
child.on("error", (err) => {
|
||||
lastErr = err && err.message ? err.message : String(err);
|
||||
resolveDone();
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
fail("startup_or_run: " + (e && e.message ? e.message : String(e)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!succeeded) {
|
||||
fail(lastErr || "agent_not_settled", { agent_id: agentId, run_id: runId }, lastErr);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 读 workspace 内目标文件
|
||||
const patches = [];
|
||||
for (const rel of allowed) {
|
||||
const abs = resolve(workspace, rel);
|
||||
if (!existsSync(abs)) continue;
|
||||
const content = readFileSync(abs, "utf8");
|
||||
patches.push({ rel_path: rel, content });
|
||||
}
|
||||
|
||||
if (patches.length === 0) {
|
||||
fail("empty_patches", { agent_id: agentId, run_id: runId });
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
agent_id: agentId,
|
||||
run_id: runId,
|
||||
patches,
|
||||
fallback_reason: "",
|
||||
}) + "\n",
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
fail("unhandled: " + (e && e.message ? e.message : String(e)));
|
||||
});
|
||||
Reference in New Issue
Block a user