@fieldwangai/agentflow 0.1.166 → 0.1.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/agent-runners.mjs +27 -9
- package/bin/lib/ai-exploration.mjs +293 -0
- package/bin/lib/composer-agent.mjs +11 -0
- package/bin/lib/cursor-api-key-pool.mjs +106 -1
- package/bin/lib/repository-index-events.mjs +17 -0
- package/bin/lib/repository-index.mjs +522 -0
- package/bin/lib/ui-server.mjs +167 -0
- package/bin/lib/workspace-routes.mjs +543 -210
- package/bin/lib/workspace-server.mjs +2 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DKdBGu3q.js → WorkflowAssistantThread-B0i4F0Ab.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BLTi7FF5.js +877 -0
- package/builtin/web-ui/dist/assets/index-yplDmRpj.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-ai-exploration/SKILL.md +127 -0
- package/skills/agentflow-ai-exploration/agents/openai.yaml +4 -0
- package/skills/agentflow-ai-exploration/references/protocol.md +120 -0
- package/skills/agentflow-ai-exploration/scripts/agentflow-ai-exploration.mjs +308 -0
- package/skills/agentflow-ai-exploration/scripts/auth-store.mjs +102 -0
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +1 -1
- package/skills/agentflow-cli/runtime/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-B7YuvFR2.css +0 -1
- package/builtin/web-ui/dist/assets/index-BUljbvrW.js +0 -877
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
isCursorQuotaError,
|
|
18
18
|
markCursorApiKeyLaneBlocked,
|
|
19
19
|
recordCursorApiKeyFallbackModel,
|
|
20
|
+
recordCursorApiKeyUsage,
|
|
20
21
|
} from "./cursor-api-key-pool.mjs";
|
|
21
22
|
import { discoverCursorModels } from "./cursor-model-catalog.mjs";
|
|
22
23
|
import { outputNodeBasename } from "../pipeline/get-exec-id.mjs";
|
|
@@ -123,7 +124,7 @@ function shouldSkipCodexGitCheck(workspace) {
|
|
|
123
124
|
return !hasGitMetadataAncestor(workspace);
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessagePath, promptText, configArgs = [] }) {
|
|
127
|
+
function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessagePath, promptText, configArgs = [], sandboxMode = "", allowDanger = true }) {
|
|
127
128
|
const args = [];
|
|
128
129
|
for (const cfg of Array.isArray(configArgs) ? configArgs : []) {
|
|
129
130
|
const value = String(cfg || "").trim();
|
|
@@ -137,10 +138,10 @@ function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessageP
|
|
|
137
138
|
const abs = path.resolve(dir);
|
|
138
139
|
if (abs && abs !== workspace) args.push("--add-dir", abs);
|
|
139
140
|
}
|
|
140
|
-
if (envFlag("AGENTFLOW_CODEX_DANGER", false)) {
|
|
141
|
+
if (allowDanger && envFlag("AGENTFLOW_CODEX_DANGER", false)) {
|
|
141
142
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
142
143
|
} else {
|
|
143
|
-
args.push("--sandbox", String(process.env.AGENTFLOW_CODEX_SANDBOX || "workspace-write").trim() || "workspace-write");
|
|
144
|
+
args.push("--sandbox", String(sandboxMode || process.env.AGENTFLOW_CODEX_SANDBOX || "workspace-write").trim() || "workspace-write");
|
|
144
145
|
}
|
|
145
146
|
if (shouldSkipCodexGitCheck(workspace)) args.push("--skip-git-repo-check");
|
|
146
147
|
if (envFlag("AGENTFLOW_CODEX_EPHEMERAL", false)) args.push("--ephemeral");
|
|
@@ -432,11 +433,16 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
432
433
|
|| cursorSelection?.modelSelection
|
|
433
434
|
|| { lane: "auto", modelId: "auto", modelName: "Auto" };
|
|
434
435
|
const model = hasExplicitModel ? requestedModel : cursorModelSelection.modelId;
|
|
436
|
+
if (cursorSelection) recordCursorApiKeyUsage(cursorSelection, cursorModelSelection);
|
|
435
437
|
// Web UI Composer 需要能无交互执行本机 curl 等命令来刷新画布。
|
|
436
|
-
const args = ["--print", "--output-format", "stream-json"
|
|
437
|
-
|
|
438
|
+
const args = ["--print", "--output-format", "stream-json"];
|
|
439
|
+
if (options.mode) args.push("--mode", String(options.mode));
|
|
440
|
+
args.push("--trust");
|
|
441
|
+
if (options.sandboxDisabled !== false) args.push("--sandbox", "disabled");
|
|
442
|
+
args.push("--workspace", ws);
|
|
443
|
+
const approveMcps = options.approveMcps ?? (process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "0" && process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "false");
|
|
438
444
|
if (approveMcps) args.push("--approve-mcps");
|
|
439
|
-
args.push("--force");
|
|
445
|
+
if (options.force !== false) args.push("--force");
|
|
440
446
|
if (shouldPassCursorModelArg(model)) args.push("--model", model);
|
|
441
447
|
args.push(promptText);
|
|
442
448
|
|
|
@@ -551,14 +557,18 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
551
557
|
if (options.onToolCall) options.onToolCall("thinking", "");
|
|
552
558
|
} else if (event.type === "result") {
|
|
553
559
|
lastResult = event;
|
|
554
|
-
const resultNl =
|
|
560
|
+
const resultNl = options.includeJsonResult && typeof event.result === "string"
|
|
561
|
+
? normalizeStreamTextChunk(event.result)
|
|
562
|
+
: extractCursorResultNl(event);
|
|
555
563
|
if (resultNl) emit({ type: "natural", kind: "result", text: resultNl });
|
|
556
564
|
if (event.subtype === "success" && !event.is_error) {
|
|
557
565
|
hadError = false;
|
|
558
566
|
emit({ type: "status", line: t("runner.completed") });
|
|
559
567
|
} else {
|
|
560
568
|
hadError = true;
|
|
561
|
-
const errNl =
|
|
569
|
+
const errNl = options.includeJsonResult && typeof event.result === "string"
|
|
570
|
+
? normalizeStreamTextChunk(event.result)
|
|
571
|
+
: extractCursorResultNl(event);
|
|
562
572
|
if (errNl) emit({ type: "natural", kind: "error", text: errNl });
|
|
563
573
|
emit({
|
|
564
574
|
type: "status",
|
|
@@ -619,12 +629,17 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
619
629
|
if (hadToolActivity) return false;
|
|
620
630
|
if (!isCursorQuotaError(errorText)) return false;
|
|
621
631
|
const errorCategory = classifyCursorApiKeyLimitError(errorText);
|
|
622
|
-
const cooldownMinutes = cursorApiKeyCooldownMinutes(cursorBaseEnv);
|
|
632
|
+
const cooldownMinutes = cursorApiKeyCooldownMinutes(cursorBaseEnv, errorText);
|
|
623
633
|
markCursorApiKeyLaneBlocked(
|
|
624
634
|
cursorSelection,
|
|
625
635
|
cursorModelSelection.lane,
|
|
626
636
|
cooldownMinutes,
|
|
627
637
|
errorText,
|
|
638
|
+
Date.now(),
|
|
639
|
+
{
|
|
640
|
+
modelId: cursorModelSelection.modelId,
|
|
641
|
+
modelName: cursorModelSelection.modelName,
|
|
642
|
+
},
|
|
628
643
|
);
|
|
629
644
|
const canTryComposer = !hasExplicitModel
|
|
630
645
|
&& cursorModelSelection.lane === "auto"
|
|
@@ -840,6 +855,7 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
|
|
|
840
855
|
const model = options.model && String(options.model).trim();
|
|
841
856
|
const claudeCmd = process.env.CLAUDE_CODE_CMD || "claude";
|
|
842
857
|
const bypassPermissions =
|
|
858
|
+
options.allowDanger !== false &&
|
|
843
859
|
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "0" &&
|
|
844
860
|
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "false";
|
|
845
861
|
const args = ["-p", "--output-format", "stream-json", "--verbose", "--add-dir", ws];
|
|
@@ -1033,6 +1049,8 @@ export function runCodexAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1033
1049
|
outputLastMessagePath,
|
|
1034
1050
|
promptText,
|
|
1035
1051
|
configArgs: options.codexConfigArgs,
|
|
1052
|
+
sandboxMode: options.sandboxMode,
|
|
1053
|
+
allowDanger: options.allowDanger !== false,
|
|
1036
1054
|
});
|
|
1037
1055
|
|
|
1038
1056
|
const useStderrInherit =
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const EXPLORATION_DIR = path.join(".workspace", "agentflow", "explorations");
|
|
6
|
+
const SESSION_ID_RE = /^exp_[a-z0-9_-]{8,80}$/i;
|
|
7
|
+
const EVENT_TYPES = new Set(["run", "turn", "decision", "agent", "tool", "command", "file", "artifact", "status"]);
|
|
8
|
+
const EVENT_STATUSES = new Set(["planned", "running", "success", "error", "blocked", "skipped"]);
|
|
9
|
+
const EVENT_PHASES = new Set(["planned", "simulated", "observed", "materialized"]);
|
|
10
|
+
const SIDE_EFFECTS = new Set(["none", "read", "write", "external"]);
|
|
11
|
+
|
|
12
|
+
function explorationRoot(workspaceRoot) {
|
|
13
|
+
return path.join(path.resolve(workspaceRoot), EXPLORATION_DIR);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sessionDir(workspaceRoot, sessionId) {
|
|
17
|
+
const id = normalizeSessionId(sessionId);
|
|
18
|
+
return path.join(explorationRoot(workspaceRoot), id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sessionMetadataPath(workspaceRoot, sessionId) {
|
|
22
|
+
return path.join(sessionDir(workspaceRoot, sessionId), "session.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sessionEventsPath(workspaceRoot, sessionId) {
|
|
26
|
+
return path.join(sessionDir(workspaceRoot, sessionId), "trace.jsonl");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeSessionId(value) {
|
|
30
|
+
const id = String(value || "").trim();
|
|
31
|
+
if (!SESSION_ID_RE.test(id)) throw new Error("Invalid exploration session id");
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clip(value, max = 2000) {
|
|
36
|
+
return redactSecrets(String(value ?? "")).trim().slice(0, max);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function redactSecrets(value) {
|
|
40
|
+
return String(value || "")
|
|
41
|
+
.replace(/\b(?:sk|key|token|secret)[-_][A-Za-z0-9_.-]{8,}\b/gi, "[redacted]")
|
|
42
|
+
.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s,;]+/gi, "$1[redacted]")
|
|
43
|
+
.replace(/((?:api[_-]?key|access[_-]?token|password|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[redacted]");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeJsonAtomic(filePath, value) {
|
|
47
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
48
|
+
const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
49
|
+
fs.writeFileSync(tempPath, JSON.stringify(value, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
50
|
+
fs.renameSync(tempPath, filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJson(filePath) {
|
|
54
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sessionSummary(raw = {}) {
|
|
58
|
+
return {
|
|
59
|
+
version: 1,
|
|
60
|
+
id: normalizeSessionId(raw.id),
|
|
61
|
+
title: clip(raw.title || "AI 探索运行", 160),
|
|
62
|
+
goal: clip(raw.goal || "", 4000),
|
|
63
|
+
summary: clip(raw.summary || "", 2000),
|
|
64
|
+
mode: EVENT_PHASES.has(String(raw.mode || "")) ? String(raw.mode) : "planned",
|
|
65
|
+
status: ["draft", "planning", "ready", "running", "completed", "failed"].includes(String(raw.status || ""))
|
|
66
|
+
? String(raw.status)
|
|
67
|
+
: "draft",
|
|
68
|
+
source: {
|
|
69
|
+
provider: clip(raw.source?.provider || "agentflow", 80),
|
|
70
|
+
agent: clip(raw.source?.agent || "workspace", 120),
|
|
71
|
+
},
|
|
72
|
+
eventCount: Math.max(0, Number(raw.eventCount || 0) || 0),
|
|
73
|
+
createdAt: String(raw.createdAt || new Date().toISOString()),
|
|
74
|
+
updatedAt: String(raw.updatedAt || raw.createdAt || new Date().toISOString()),
|
|
75
|
+
...(raw.materializedAt ? { materializedAt: String(raw.materializedAt) } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createAiExplorationSession(workspaceRoot, input = {}) {
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
const id = `exp_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
|
|
82
|
+
const session = sessionSummary({
|
|
83
|
+
id,
|
|
84
|
+
title: input.title,
|
|
85
|
+
goal: input.goal,
|
|
86
|
+
mode: input.mode,
|
|
87
|
+
status: input.status || "draft",
|
|
88
|
+
source: input.source,
|
|
89
|
+
createdAt: now,
|
|
90
|
+
updatedAt: now,
|
|
91
|
+
});
|
|
92
|
+
const dir = sessionDir(workspaceRoot, id);
|
|
93
|
+
fs.mkdirSync(path.join(dir, "artifacts"), { recursive: true, mode: 0o700 });
|
|
94
|
+
writeJsonAtomic(sessionMetadataPath(workspaceRoot, id), session);
|
|
95
|
+
fs.writeFileSync(sessionEventsPath(workspaceRoot, id), "", { encoding: "utf-8", mode: 0o600 });
|
|
96
|
+
return session;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function listAiExplorationSessions(workspaceRoot, limit = 50) {
|
|
100
|
+
const root = explorationRoot(workspaceRoot);
|
|
101
|
+
if (!fs.existsSync(root)) return [];
|
|
102
|
+
const sessions = [];
|
|
103
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
104
|
+
if (!entry.isDirectory() || !SESSION_ID_RE.test(entry.name)) continue;
|
|
105
|
+
try {
|
|
106
|
+
sessions.push(sessionSummary(readJson(sessionMetadataPath(workspaceRoot, entry.name))));
|
|
107
|
+
} catch {
|
|
108
|
+
// A corrupt exploration is omitted instead of breaking the Workspace.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return sessions
|
|
112
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || b.id.localeCompare(a.id))
|
|
113
|
+
.slice(0, Math.max(1, Math.min(200, Number(limit) || 50)));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function readAiExplorationSession(workspaceRoot, sessionId) {
|
|
117
|
+
const session = sessionSummary(readJson(sessionMetadataPath(workspaceRoot, sessionId)));
|
|
118
|
+
const events = [];
|
|
119
|
+
const eventPath = sessionEventsPath(workspaceRoot, session.id);
|
|
120
|
+
if (fs.existsSync(eventPath)) {
|
|
121
|
+
for (const line of fs.readFileSync(eventPath, "utf-8").split("\n")) {
|
|
122
|
+
if (!line.trim()) continue;
|
|
123
|
+
try { events.push(JSON.parse(line)); } catch { /* retain readable events */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { ...session, events };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function updateAiExplorationSession(workspaceRoot, sessionId, patch = {}) {
|
|
130
|
+
const current = readAiExplorationSession(workspaceRoot, sessionId);
|
|
131
|
+
const next = sessionSummary({
|
|
132
|
+
...current,
|
|
133
|
+
...patch,
|
|
134
|
+
source: patch.source ? { ...current.source, ...patch.source } : current.source,
|
|
135
|
+
updatedAt: new Date().toISOString(),
|
|
136
|
+
});
|
|
137
|
+
writeJsonAtomic(sessionMetadataPath(workspaceRoot, next.id), next);
|
|
138
|
+
return next;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeArtifacts(raw) {
|
|
142
|
+
return (Array.isArray(raw) ? raw : []).slice(0, 20).map((artifact) => ({
|
|
143
|
+
kind: clip(artifact?.kind || "file", 40),
|
|
144
|
+
path: clip(artifact?.path || "", 500),
|
|
145
|
+
label: clip(artifact?.label || artifact?.path || "artifact", 160),
|
|
146
|
+
...(artifact?.sha256 ? { sha256: clip(artifact.sha256, 80) } : {}),
|
|
147
|
+
})).filter((artifact) => artifact.path || artifact.label);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function normalizeAiTraceEvent(raw = {}, defaults = {}) {
|
|
151
|
+
const now = new Date().toISOString();
|
|
152
|
+
const phase = EVENT_PHASES.has(String(raw.phase || defaults.phase || "")) ? String(raw.phase || defaults.phase) : "observed";
|
|
153
|
+
const type = EVENT_TYPES.has(String(raw.type || "")) ? String(raw.type) : "status";
|
|
154
|
+
const status = EVENT_STATUSES.has(String(raw.status || "")) ? String(raw.status) : (phase === "planned" ? "planned" : "running");
|
|
155
|
+
const sideEffect = SIDE_EFFECTS.has(String(raw.sideEffect || "")) ? String(raw.sideEffect) : "none";
|
|
156
|
+
return {
|
|
157
|
+
id: clip(raw.id || `evt_${crypto.randomUUID().replace(/-/g, "").slice(0, 18)}`, 100),
|
|
158
|
+
traceId: clip(raw.traceId || defaults.traceId || "", 100),
|
|
159
|
+
spanId: clip(raw.spanId || raw.id || `span_${crypto.randomUUID().replace(/-/g, "").slice(0, 18)}`, 100),
|
|
160
|
+
parentSpanId: clip(raw.parentSpanId || "", 100),
|
|
161
|
+
sequence: Math.max(1, Number(raw.sequence || defaults.sequence || 1) || 1),
|
|
162
|
+
phase,
|
|
163
|
+
type,
|
|
164
|
+
name: clip(raw.name || type, 160),
|
|
165
|
+
summary: clip(raw.summary || raw.description || "", 2000),
|
|
166
|
+
status,
|
|
167
|
+
sideEffect,
|
|
168
|
+
requiresApproval: raw.requiresApproval === true || ["write", "external"].includes(sideEffect),
|
|
169
|
+
startedAt: String(raw.startedAt || now),
|
|
170
|
+
...(raw.endedAt ? { endedAt: String(raw.endedAt) } : {}),
|
|
171
|
+
...(raw.inputPreview ? { inputPreview: clip(raw.inputPreview, 2000) } : {}),
|
|
172
|
+
...(raw.outputPreview ? { outputPreview: clip(raw.outputPreview, 2000) } : {}),
|
|
173
|
+
artifacts: normalizeArtifacts(raw.artifacts),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function appendAiTraceEvents(workspaceRoot, sessionId, rawEvents = [], defaults = {}) {
|
|
178
|
+
const session = readAiExplorationSession(workspaceRoot, sessionId);
|
|
179
|
+
const incoming = Array.isArray(rawEvents) ? rawEvents : [rawEvents];
|
|
180
|
+
if (!incoming.length) return { session, events: [] };
|
|
181
|
+
if (session.eventCount + incoming.length > 5000) throw new Error("Exploration trace exceeds 5000 events");
|
|
182
|
+
const events = incoming.map((event, index) => normalizeAiTraceEvent(event, {
|
|
183
|
+
...defaults,
|
|
184
|
+
traceId: session.id,
|
|
185
|
+
sequence: session.eventCount + index + 1,
|
|
186
|
+
}));
|
|
187
|
+
fs.appendFileSync(sessionEventsPath(workspaceRoot, session.id), events.map((event) => JSON.stringify(event)).join("\n") + "\n", "utf-8");
|
|
188
|
+
const next = updateAiExplorationSession(workspaceRoot, session.id, {
|
|
189
|
+
eventCount: session.eventCount + events.length,
|
|
190
|
+
mode: defaults.phase || session.mode,
|
|
191
|
+
});
|
|
192
|
+
return { session: next, events };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function parseAiPlanResult(text, sessionId) {
|
|
196
|
+
const raw = String(text || "").trim();
|
|
197
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
|
|
198
|
+
const candidates = [raw, fenced].filter(Boolean);
|
|
199
|
+
let parsed;
|
|
200
|
+
for (const candidate of candidates) {
|
|
201
|
+
try {
|
|
202
|
+
parsed = JSON.parse(candidate);
|
|
203
|
+
break;
|
|
204
|
+
} catch {
|
|
205
|
+
const start = candidate.indexOf("{");
|
|
206
|
+
const end = candidate.lastIndexOf("}");
|
|
207
|
+
if (start >= 0 && end > start) {
|
|
208
|
+
try { parsed = JSON.parse(candidate.slice(start, end + 1)); break; } catch { /* next */ }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!parsed || typeof parsed !== "object") throw new Error("Plan agent did not return valid JSON");
|
|
213
|
+
const spans = Array.isArray(parsed.spans) ? parsed.spans : Array.isArray(parsed.steps) ? parsed.steps : [];
|
|
214
|
+
if (!spans.length) throw new Error("Plan agent returned no executable spans");
|
|
215
|
+
return {
|
|
216
|
+
title: clip(parsed.title || "AI 执行计划", 160),
|
|
217
|
+
summary: clip(parsed.summary || "", 2000),
|
|
218
|
+
events: spans.slice(0, 120).map((span, index) => normalizeAiTraceEvent({
|
|
219
|
+
...span,
|
|
220
|
+
id: span.id || `plan_${index + 1}`,
|
|
221
|
+
spanId: span.spanId || span.id || `plan_${index + 1}`,
|
|
222
|
+
parentSpanId: span.parentSpanId || span.parentId || "",
|
|
223
|
+
type: span.type || "turn",
|
|
224
|
+
status: "planned",
|
|
225
|
+
phase: "planned",
|
|
226
|
+
sideEffect: span.sideEffect || "none",
|
|
227
|
+
startedAt: new Date().toISOString(),
|
|
228
|
+
}, { traceId: sessionId, phase: "planned", sequence: index + 1 })),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function materializableAiTraceEvents(session) {
|
|
233
|
+
const events = Array.isArray(session?.events) ? session.events : [];
|
|
234
|
+
const planned = events.filter((event) => event.phase === "planned");
|
|
235
|
+
if (planned.length) return planned;
|
|
236
|
+
return events.filter((event) => event.phase === "observed");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function classifyAiToolSideEffect(toolName, subtype = "") {
|
|
240
|
+
const value = `${toolName || ""} ${subtype || ""}`.trim().toLowerCase();
|
|
241
|
+
if (/(?:^|[_\W])thinking(?:$|[_\W])/.test(value)) return "none";
|
|
242
|
+
if (/(?:^|[_\W])(web|http|curl|fetch|mcp|publish|send|notify|deploy)(?:$|[_\W])/.test(value)) return "external";
|
|
243
|
+
if (/(?:^|[_\W])(read|search|find|grep|glob|list|inspect|status|stat|cat|head|tail)(?:$|[_\W])/.test(value)) return "read";
|
|
244
|
+
return "write";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function aiPlanPrompt({ goal = "", workspaceSource = "" } = {}) {
|
|
248
|
+
return [
|
|
249
|
+
"你是 AgentFlow 的只读 Plan Agent。只规划,不执行工具,不修改文件。",
|
|
250
|
+
"把用户目标转换为一张预计 AI 运行图。只输出合法 JSON,不要 Markdown 代码围栏。",
|
|
251
|
+
"JSON 格式:",
|
|
252
|
+
'{"title":"短标题","summary":"计划摘要","spans":[{"id":"step_1","parentSpanId":"","type":"turn|decision|agent|tool|command|file|artifact","name":"步骤名","summary":"做什么以及为什么","sideEffect":"none|read|write|external","requiresApproval":false,"inputPreview":"预计输入","outputPreview":"预期输出"}]}',
|
|
253
|
+
"要求:id 唯一;parentSpanId 表达父子关系;写文件、发布、发送、删除、外部写请求必须标记 requiresApproval=true;搜索与读取标记 read;不要假装已经得到任何执行结果。",
|
|
254
|
+
workspaceSource ? `\n## 当前 Workspace DSL\n\n${workspaceSource}` : "",
|
|
255
|
+
`\n## 用户目标\n\n${clip(goal, 12000)}`,
|
|
256
|
+
].filter(Boolean).join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function aiMaterializationPrompt(session, workspaceSource = "") {
|
|
260
|
+
const events = materializableAiTraceEvents(session);
|
|
261
|
+
const plan = events.map((event) => ({
|
|
262
|
+
spanId: event.spanId,
|
|
263
|
+
parentSpanId: event.parentSpanId,
|
|
264
|
+
type: event.type,
|
|
265
|
+
name: event.name,
|
|
266
|
+
summary: event.summary,
|
|
267
|
+
sideEffect: event.sideEffect,
|
|
268
|
+
requiresApproval: event.requiresApproval,
|
|
269
|
+
}));
|
|
270
|
+
return [
|
|
271
|
+
"你是 AgentFlow 流程固化 Agent。把已审核的 AI Plan 固化到当前 Workspace DSL 调整态。",
|
|
272
|
+
"必须实际编辑 workspace.flow.js;稳定脚本放入 nodes/<name>/index.mjs;不要执行该流程,不要发布。",
|
|
273
|
+
"过滤纯搜索噪声;把输入参数化;把判断映射为 control.if,把受控重复映射为 control.while,把产物映射为 Display;副作用步骤必须保留清晰名称和输入。",
|
|
274
|
+
"修改完成后运行 `agentflow flow dsl lint <当前流程目录>`。最终只简短说明生成了哪些节点以及仍需人工确认的副作用。",
|
|
275
|
+
workspaceSource ? `\n## 当前 Workspace DSL\n\n${workspaceSource}` : "",
|
|
276
|
+
`\n## 探索目标\n\n${clip(session?.goal || "", 8000)}`,
|
|
277
|
+
`\n## 已审核 Plan Trace\n\n${JSON.stringify(plan, null, 2)}`,
|
|
278
|
+
].filter(Boolean).join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function writeAiExplorationMaterialization(workspaceRoot, sessionId, value = {}) {
|
|
282
|
+
const session = readAiExplorationSession(workspaceRoot, sessionId);
|
|
283
|
+
const payload = {
|
|
284
|
+
version: 1,
|
|
285
|
+
sessionId: session.id,
|
|
286
|
+
materializedAt: new Date().toISOString(),
|
|
287
|
+
spanIds: materializableAiTraceEvents(session).map((event) => event.spanId).filter(Boolean),
|
|
288
|
+
nodeIds: (Array.isArray(value.nodeIds) ? value.nodeIds : []).map((id) => clip(id, 160)).filter(Boolean),
|
|
289
|
+
...(value.designRevision ? { designRevision: clip(value.designRevision, 160) } : {}),
|
|
290
|
+
};
|
|
291
|
+
writeJsonAtomic(path.join(sessionDir(workspaceRoot, session.id), "materialization.json"), payload);
|
|
292
|
+
return payload;
|
|
293
|
+
}
|
|
@@ -319,6 +319,11 @@ function runCursorAgentWithPrivateMcp(cliWorkspace, prompt, options, userId) {
|
|
|
319
319
|
* @param {string} [opts.modelKey]
|
|
320
320
|
* @param {Record<string, string>} [opts.extraEnv]
|
|
321
321
|
* @param {boolean} [opts.force]
|
|
322
|
+
* @param {"ask" | "plan"} [opts.mode]
|
|
323
|
+
* @param {boolean} [opts.sandboxDisabled]
|
|
324
|
+
* @param {boolean} [opts.approveMcps]
|
|
325
|
+
* @param {"read-only" | "workspace-write" | "danger-full-access"} [opts.sandboxMode]
|
|
326
|
+
* @param {boolean} [opts.includeJsonResult]
|
|
322
327
|
* @param {(ev: object) => void} [opts.onStreamEvent]
|
|
323
328
|
* @param {(subtype: string, toolName: string) => void} [opts.onToolCall]
|
|
324
329
|
* @returns {{ child: import('child_process').ChildProcess, finished: Promise<void> }}
|
|
@@ -343,6 +348,12 @@ export function startComposerAgent(opts) {
|
|
|
343
348
|
onChild: opts.onChild,
|
|
344
349
|
detached: Boolean(opts.detached),
|
|
345
350
|
force: Boolean(opts.force),
|
|
351
|
+
...(opts.mode ? { mode: String(opts.mode) } : {}),
|
|
352
|
+
...(opts.sandboxDisabled === false ? { sandboxDisabled: false } : {}),
|
|
353
|
+
...(typeof opts.approveMcps === "boolean" ? { approveMcps: opts.approveMcps } : {}),
|
|
354
|
+
...(opts.sandboxMode ? { sandboxMode: String(opts.sandboxMode) } : {}),
|
|
355
|
+
...(opts.allowDanger === false ? { allowDanger: false } : {}),
|
|
356
|
+
...(opts.includeJsonResult === true ? { includeJsonResult: true } : {}),
|
|
346
357
|
env,
|
|
347
358
|
addDirs: Array.isArray(opts.writableDirs)
|
|
348
359
|
? opts.writableDirs.map((dir) => String(dir || "").trim()).filter(Boolean)
|
|
@@ -18,6 +18,7 @@ export function parseCursorApiKeyRecords(value = "") {
|
|
|
18
18
|
id: String(item.id || "").trim() || legacyKeyId(key),
|
|
19
19
|
name: String(item.name || "").trim() || `Key ${index + 1}`,
|
|
20
20
|
key,
|
|
21
|
+
createdAt: String(item.createdAt || "").trim(),
|
|
21
22
|
};
|
|
22
23
|
};
|
|
23
24
|
try {
|
|
@@ -89,6 +90,7 @@ export function markCursorApiKeyLaneBlocked(
|
|
|
89
90
|
cooldownMinutes = 30,
|
|
90
91
|
errorText = "",
|
|
91
92
|
now = Date.now(),
|
|
93
|
+
evidence = {},
|
|
92
94
|
) {
|
|
93
95
|
const keyId = selectionId(keyOrSelection);
|
|
94
96
|
if (!keyId || !["auto", "fallback"].includes(lane)) return 0;
|
|
@@ -97,12 +99,23 @@ export function markCursorApiKeyLaneBlocked(
|
|
|
97
99
|
const currentLane = keyState[lane] || {};
|
|
98
100
|
const blockedUntil = Math.max(currentLane.blockedUntil || 0, now + minutes * 60 * 1000);
|
|
99
101
|
const errorCategory = classifyCursorApiKeyLimitError(errorText);
|
|
102
|
+
const fallbackModel = keyState.fallbackModel;
|
|
103
|
+
const lastFailure = {
|
|
104
|
+
triggeredAt: new Date(now).toISOString(),
|
|
105
|
+
...(errorCategory ? { errorCategory } : {}),
|
|
106
|
+
errorPreview: sanitizeErrorPreview(errorText),
|
|
107
|
+
lane,
|
|
108
|
+
modelId: String(evidence?.modelId || (lane === "auto" ? "auto" : fallbackModel?.id || "fallback")),
|
|
109
|
+
modelName: String(evidence?.modelName || (lane === "auto" ? "Auto" : fallbackModel?.displayName || "降级模型")),
|
|
110
|
+
};
|
|
100
111
|
keyState[lane] = {
|
|
101
112
|
...currentLane,
|
|
102
113
|
blockedUntil,
|
|
103
114
|
...(errorCategory ? { errorCategory } : {}),
|
|
104
115
|
...(lane === "auto" ? { fallbackEligible: isCursorAutoFallbackEligible(errorText) } : {}),
|
|
116
|
+
lastFailure,
|
|
105
117
|
};
|
|
118
|
+
keyState.lastFailure = lastFailure;
|
|
106
119
|
cursorApiKeyStates.set(keyId, keyState);
|
|
107
120
|
return blockedUntil;
|
|
108
121
|
}
|
|
@@ -116,6 +129,72 @@ export function clearCursorApiKeyLaneCooldown(keyOrSelection, lane) {
|
|
|
116
129
|
return true;
|
|
117
130
|
}
|
|
118
131
|
|
|
132
|
+
export function clearCursorApiKeyCooldown(keyOrSelection) {
|
|
133
|
+
const keyState = cursorApiKeyStates.get(selectionId(keyOrSelection));
|
|
134
|
+
if (!keyState) return false;
|
|
135
|
+
let changed = false;
|
|
136
|
+
for (const lane of ["auto", "fallback"]) {
|
|
137
|
+
if (!keyState[lane] || (keyState[lane].blockedUntil || 0) <= 0) continue;
|
|
138
|
+
keyState[lane].blockedUntil = 0;
|
|
139
|
+
delete keyState[lane].errorCategory;
|
|
140
|
+
delete keyState[lane].fallbackEligible;
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
return changed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function recordCursorApiKeyUsage(keyOrSelection, selection = {}, now = Date.now()) {
|
|
147
|
+
const keyId = selectionId(keyOrSelection);
|
|
148
|
+
if (!keyId || keyId === "default") return;
|
|
149
|
+
const keyState = cursorApiKeyStates.get(keyId) || {};
|
|
150
|
+
keyState.lastUsedAt = new Date(now).toISOString();
|
|
151
|
+
keyState.lastSelection = {
|
|
152
|
+
lane: selection?.lane === "fallback" ? "fallback" : "auto",
|
|
153
|
+
modelId: String(selection?.modelId || "auto"),
|
|
154
|
+
modelName: String(selection?.modelName || "Auto"),
|
|
155
|
+
};
|
|
156
|
+
cursorApiKeyStates.set(keyId, keyState);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getCursorApiKeyPoolStatuses(records = [], now = Date.now()) {
|
|
160
|
+
return (Array.isArray(records) ? records : []).map((record) => {
|
|
161
|
+
const id = selectionId(record);
|
|
162
|
+
const keyState = cursorApiKeyStates.get(id);
|
|
163
|
+
const selection = getCursorApiKeyModelSelection(id, now);
|
|
164
|
+
const laneCooldowns = buildLaneCooldowns(keyState, now);
|
|
165
|
+
const common = {
|
|
166
|
+
id,
|
|
167
|
+
...(keyState?.lastUsedAt ? { lastUsedAt: keyState.lastUsedAt } : {}),
|
|
168
|
+
...(keyState?.fallbackModel ? { fallbackModel: keyState.fallbackModel } : {}),
|
|
169
|
+
laneCooldowns,
|
|
170
|
+
...(keyState?.lastFailure ? { lastFailure: keyState.lastFailure } : {}),
|
|
171
|
+
};
|
|
172
|
+
if (selection) {
|
|
173
|
+
return {
|
|
174
|
+
...common,
|
|
175
|
+
status: "available",
|
|
176
|
+
activeLane: selection.lane,
|
|
177
|
+
activeModelId: selection.modelId,
|
|
178
|
+
activeModelName: selection.modelName,
|
|
179
|
+
degraded: selection.lane === "fallback",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const activeCooldowns = laneCooldowns.filter((item) => item.remainingSeconds > 0);
|
|
183
|
+
const earliest = activeCooldowns.reduce(
|
|
184
|
+
(result, item) => !result || item.remainingSeconds < result.remainingSeconds ? item : result,
|
|
185
|
+
undefined,
|
|
186
|
+
);
|
|
187
|
+
const autoState = keyState?.auto;
|
|
188
|
+
return {
|
|
189
|
+
...common,
|
|
190
|
+
status: "cooling_down",
|
|
191
|
+
...(autoState?.errorCategory ? { errorCategory: autoState.errorCategory } : {}),
|
|
192
|
+
blockedUntil: earliest?.blockedUntil || new Date(Math.max(now, autoState?.blockedUntil || now)).toISOString(),
|
|
193
|
+
remainingSeconds: earliest?.remainingSeconds || Math.max(0, Math.ceil(((autoState?.blockedUntil || now) - now) / 1000)),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
119
198
|
export function cursorApiKeyEnv(selection) {
|
|
120
199
|
return selection?.key ? { CURSOR_API_KEY: selection.key } : {};
|
|
121
200
|
}
|
|
@@ -159,7 +238,10 @@ export function isCursorQuotaError(error = "") {
|
|
|
159
238
|
return classifyCursorApiKeyLimitError(error) !== undefined;
|
|
160
239
|
}
|
|
161
240
|
|
|
162
|
-
export function cursorApiKeyCooldownMinutes(env = {}) {
|
|
241
|
+
export function cursorApiKeyCooldownMinutes(env = {}, errorText = "") {
|
|
242
|
+
if (classifyCursorApiKeyLimitError(errorText) === "resource_exhausted") {
|
|
243
|
+
return Math.max(1, Number(env.AGENTFLOW_CURSOR_API_KEY_RESOURCE_EXHAUSTED_COOLDOWN_MINUTES || 3) || 3);
|
|
244
|
+
}
|
|
163
245
|
return Math.max(1, Number(env.AGENTFLOW_CURSOR_API_KEY_COOLDOWN_MINUTES || env.CURSOR_API_KEY_COOLDOWN_MINUTES || 30) || 30);
|
|
164
246
|
}
|
|
165
247
|
|
|
@@ -178,3 +260,26 @@ function selectionId(keyOrSelection) {
|
|
|
178
260
|
function legacyKeyId(key) {
|
|
179
261
|
return `legacy_${createHash("sha256").update(String(key || "")).digest("hex").slice(0, 16)}`;
|
|
180
262
|
}
|
|
263
|
+
|
|
264
|
+
function buildLaneCooldowns(keyState, now) {
|
|
265
|
+
if (!keyState) return [];
|
|
266
|
+
const lanes = [];
|
|
267
|
+
for (const lane of ["auto", "fallback"]) {
|
|
268
|
+
const laneState = keyState[lane];
|
|
269
|
+
if (!laneState || (laneState.blockedUntil || 0) <= now) continue;
|
|
270
|
+
const fallbackModel = keyState.fallbackModel;
|
|
271
|
+
lanes.push({
|
|
272
|
+
lane,
|
|
273
|
+
modelId: lane === "auto" ? "auto" : fallbackModel?.id || "fallback",
|
|
274
|
+
modelName: lane === "auto" ? "Auto" : fallbackModel?.displayName || "降级模型",
|
|
275
|
+
...(laneState.errorCategory ? { errorCategory: laneState.errorCategory } : {}),
|
|
276
|
+
blockedUntil: new Date(laneState.blockedUntil).toISOString(),
|
|
277
|
+
remainingSeconds: Math.ceil((laneState.blockedUntil - now) / 1000),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return lanes;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function sanitizeErrorPreview(errorText) {
|
|
284
|
+
return String(errorText || "").replace(/(?:sk|key)[-_][A-Za-z0-9_-]{8,}/gi, "[redacted]").trim().slice(0, 500);
|
|
285
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const runFinishedListeners = new Set();
|
|
2
|
+
|
|
3
|
+
export function onRepositoryRunFinished(listener) {
|
|
4
|
+
if (typeof listener !== "function") return () => {};
|
|
5
|
+
runFinishedListeners.add(listener);
|
|
6
|
+
return () => runFinishedListeners.delete(listener);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function emitRepositoryRunFinished(workspaceRoot, run, status) {
|
|
10
|
+
for (const listener of runFinishedListeners) {
|
|
11
|
+
try {
|
|
12
|
+
listener(workspaceRoot, run, status);
|
|
13
|
+
} catch {
|
|
14
|
+
// Derived repository updates must never break the authoritative run ledger.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|