@fieldwangai/agentflow 0.1.166 → 0.1.168
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 +98 -32
- package/bin/lib/ai-exploration.mjs +293 -0
- package/bin/lib/builtin-node-review.mjs +101 -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 +534 -0
- package/bin/lib/ui-server.mjs +167 -0
- package/bin/lib/workspace-routes.mjs +560 -212
- package/bin/lib/workspace-run-logs.mjs +12 -1
- package/bin/lib/workspace-server.mjs +219 -22
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DKdBGu3q.js → WorkflowAssistantThread-x1JCQpVD.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BPFVqIDM.css +1 -0
- package/builtin/web-ui/dist/assets/index-Y3sCNmlY.js +880 -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";
|
|
@@ -92,6 +93,28 @@ function cursorResultErrorText(event) {
|
|
|
92
93
|
return "";
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
export function isCursorAgentLoopingError(error = "") {
|
|
97
|
+
const text = String(error?.message || error || "");
|
|
98
|
+
return /agent looping detected|got stuck in a repeating response pattern/i.test(text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isCursorReadOnlyToolCall(toolName = "") {
|
|
102
|
+
const name = String(toolName || "").trim();
|
|
103
|
+
if (!name) return false;
|
|
104
|
+
return /^(read|glob|grep|search|semanticSearch|list|find|fetch|webSearch|view|inspect)/i.test(name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function annotateCursorFailure(error, { hadToolActivity = false, hadMutatingToolActivity = false } = {}) {
|
|
108
|
+
const failure = error instanceof Error ? error : new Error(String(error || "Cursor Agent failed."));
|
|
109
|
+
failure.cursorHadToolActivity = Boolean(hadToolActivity);
|
|
110
|
+
failure.cursorHadMutatingToolActivity = Boolean(hadMutatingToolActivity);
|
|
111
|
+
if (isCursorAgentLoopingError(failure)) {
|
|
112
|
+
failure.code = "CURSOR_AGENT_LOOPING";
|
|
113
|
+
failure.agentflowFailureCategory = "agent_looping";
|
|
114
|
+
}
|
|
115
|
+
return failure;
|
|
116
|
+
}
|
|
117
|
+
|
|
95
118
|
function envFlag(name, defaultValue = false) {
|
|
96
119
|
const raw = process.env[name];
|
|
97
120
|
if (raw == null || raw === "") return defaultValue;
|
|
@@ -123,7 +146,7 @@ function shouldSkipCodexGitCheck(workspace) {
|
|
|
123
146
|
return !hasGitMetadataAncestor(workspace);
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessagePath, promptText, configArgs = [] }) {
|
|
149
|
+
function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessagePath, promptText, configArgs = [], sandboxMode = "", allowDanger = true }) {
|
|
127
150
|
const args = [];
|
|
128
151
|
for (const cfg of Array.isArray(configArgs) ? configArgs : []) {
|
|
129
152
|
const value = String(cfg || "").trim();
|
|
@@ -137,10 +160,10 @@ function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessageP
|
|
|
137
160
|
const abs = path.resolve(dir);
|
|
138
161
|
if (abs && abs !== workspace) args.push("--add-dir", abs);
|
|
139
162
|
}
|
|
140
|
-
if (envFlag("AGENTFLOW_CODEX_DANGER", false)) {
|
|
163
|
+
if (allowDanger && envFlag("AGENTFLOW_CODEX_DANGER", false)) {
|
|
141
164
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
142
165
|
} else {
|
|
143
|
-
args.push("--sandbox", String(process.env.AGENTFLOW_CODEX_SANDBOX || "workspace-write").trim() || "workspace-write");
|
|
166
|
+
args.push("--sandbox", String(sandboxMode || process.env.AGENTFLOW_CODEX_SANDBOX || "workspace-write").trim() || "workspace-write");
|
|
144
167
|
}
|
|
145
168
|
if (shouldSkipCodexGitCheck(workspace)) args.push("--skip-git-repo-check");
|
|
146
169
|
if (envFlag("AGENTFLOW_CODEX_EPHEMERAL", false)) args.push("--ephemeral");
|
|
@@ -432,11 +455,16 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
432
455
|
|| cursorSelection?.modelSelection
|
|
433
456
|
|| { lane: "auto", modelId: "auto", modelName: "Auto" };
|
|
434
457
|
const model = hasExplicitModel ? requestedModel : cursorModelSelection.modelId;
|
|
458
|
+
if (cursorSelection) recordCursorApiKeyUsage(cursorSelection, cursorModelSelection);
|
|
435
459
|
// Web UI Composer 需要能无交互执行本机 curl 等命令来刷新画布。
|
|
436
|
-
const args = ["--print", "--output-format", "stream-json"
|
|
437
|
-
|
|
460
|
+
const args = ["--print", "--output-format", "stream-json"];
|
|
461
|
+
if (options.mode) args.push("--mode", String(options.mode));
|
|
462
|
+
args.push("--trust");
|
|
463
|
+
if (options.sandboxDisabled !== false) args.push("--sandbox", "disabled");
|
|
464
|
+
args.push("--workspace", ws);
|
|
465
|
+
const approveMcps = options.approveMcps ?? (process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "0" && process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "false");
|
|
438
466
|
if (approveMcps) args.push("--approve-mcps");
|
|
439
|
-
args.push("--force");
|
|
467
|
+
if (options.force !== false) args.push("--force");
|
|
440
468
|
if (shouldPassCursorModelArg(model)) args.push("--model", model);
|
|
441
469
|
args.push(promptText);
|
|
442
470
|
|
|
@@ -453,6 +481,14 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
453
481
|
let lastResult = null;
|
|
454
482
|
let hadError = false;
|
|
455
483
|
let hadToolActivity = false;
|
|
484
|
+
let hadMutatingToolActivity = false;
|
|
485
|
+
const annotateFailure = (error) => {
|
|
486
|
+
const failure = annotateCursorFailure(error, { hadToolActivity, hadMutatingToolActivity });
|
|
487
|
+
failure.cursorModelLane = cursorModelSelection.lane;
|
|
488
|
+
failure.cursorModelId = cursorModelSelection.modelId;
|
|
489
|
+
failure.cursorModelName = cursorModelSelection.modelName;
|
|
490
|
+
return failure;
|
|
491
|
+
};
|
|
456
492
|
const STDERR_CAP_BYTES = 1024 * 1024;
|
|
457
493
|
const stderrChunks = [];
|
|
458
494
|
let stderrTotalBytes = 0;
|
|
@@ -540,6 +576,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
540
576
|
hadToolActivity = true;
|
|
541
577
|
const toolName =
|
|
542
578
|
event.tool_call && typeof event.tool_call === "object" ? Object.keys(event.tool_call)[0] ?? "?" : "?";
|
|
579
|
+
if (!isCursorReadOnlyToolCall(toolName)) hadMutatingToolActivity = true;
|
|
543
580
|
const subtype = event.subtype ?? "";
|
|
544
581
|
const statusLine = `工具 ${toolName}${subtype ? ` (${subtype})` : ""}`;
|
|
545
582
|
emit({ type: "status", line: statusLine });
|
|
@@ -551,14 +588,18 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
551
588
|
if (options.onToolCall) options.onToolCall("thinking", "");
|
|
552
589
|
} else if (event.type === "result") {
|
|
553
590
|
lastResult = event;
|
|
554
|
-
const resultNl =
|
|
591
|
+
const resultNl = options.includeJsonResult && typeof event.result === "string"
|
|
592
|
+
? normalizeStreamTextChunk(event.result)
|
|
593
|
+
: extractCursorResultNl(event);
|
|
555
594
|
if (resultNl) emit({ type: "natural", kind: "result", text: resultNl });
|
|
556
595
|
if (event.subtype === "success" && !event.is_error) {
|
|
557
596
|
hadError = false;
|
|
558
597
|
emit({ type: "status", line: t("runner.completed") });
|
|
559
598
|
} else {
|
|
560
599
|
hadError = true;
|
|
561
|
-
const errNl =
|
|
600
|
+
const errNl = options.includeJsonResult && typeof event.result === "string"
|
|
601
|
+
? normalizeStreamTextChunk(event.result)
|
|
602
|
+
: extractCursorResultNl(event);
|
|
562
603
|
if (errNl) emit({ type: "natural", kind: "error", text: errNl });
|
|
563
604
|
emit({
|
|
564
605
|
type: "status",
|
|
@@ -573,13 +614,18 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
573
614
|
if (line.includes('"type":"tool_call"') || line.includes('"type": "tool_call"')) {
|
|
574
615
|
hadToolActivity = true;
|
|
575
616
|
let subtype = "?";
|
|
617
|
+
let toolName = "?";
|
|
576
618
|
try {
|
|
577
619
|
const ev = JSON.parse(line);
|
|
578
|
-
if (ev && ev.type === "tool_call")
|
|
620
|
+
if (ev && ev.type === "tool_call") {
|
|
621
|
+
subtype = ev.subtype ?? "?";
|
|
622
|
+
toolName = ev.tool_call && typeof ev.tool_call === "object" ? Object.keys(ev.tool_call)[0] ?? "?" : "?";
|
|
623
|
+
}
|
|
579
624
|
} catch {
|
|
580
625
|
const m = line.match(/"subtype"\s*:\s*"([^"]+)"/);
|
|
581
626
|
if (m) subtype = m[1];
|
|
582
627
|
}
|
|
628
|
+
if (!isCursorReadOnlyToolCall(toolName)) hadMutatingToolActivity = true;
|
|
583
629
|
emit({ type: "status", line: t("runner.tool_call", { subtype }) });
|
|
584
630
|
} else if (isLikelyBase64(line)) {
|
|
585
631
|
emit({ type: "status", line: t("runner.base64_data", { len: line.length }) });
|
|
@@ -614,39 +660,55 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
614
660
|
const rest = stderrComposerBuffer.trim();
|
|
615
661
|
emit({ type: "status", line: `[stderr] ${truncateComposerLine(rest)}` });
|
|
616
662
|
}
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (!
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
663
|
+
const retryCursorFailure = (errorText) => {
|
|
664
|
+
const quotaFailure = isCursorQuotaError(errorText);
|
|
665
|
+
const loopingFailure = isCursorAgentLoopingError(errorText);
|
|
666
|
+
if (!quotaFailure && !loopingFailure) return false;
|
|
667
|
+
if (quotaFailure && hadToolActivity) return false;
|
|
668
|
+
if (loopingFailure && hadMutatingToolActivity) return false;
|
|
669
|
+
const errorCategory = quotaFailure ? classifyCursorApiKeyLimitError(errorText) : "agent_looping";
|
|
670
|
+
if (quotaFailure && cursorSelection) {
|
|
671
|
+
const cooldownMinutes = cursorApiKeyCooldownMinutes(cursorBaseEnv, errorText);
|
|
672
|
+
markCursorApiKeyLaneBlocked(
|
|
673
|
+
cursorSelection,
|
|
674
|
+
cursorModelSelection.lane,
|
|
675
|
+
cooldownMinutes,
|
|
676
|
+
errorText,
|
|
677
|
+
Date.now(),
|
|
678
|
+
{
|
|
679
|
+
modelId: cursorModelSelection.modelId,
|
|
680
|
+
modelName: cursorModelSelection.modelName,
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
}
|
|
629
684
|
const canTryComposer = !hasExplicitModel
|
|
630
685
|
&& cursorModelSelection.lane === "auto"
|
|
631
|
-
&&
|
|
632
|
-
|
|
633
|
-
|
|
686
|
+
&& (
|
|
687
|
+
loopingFailure
|
|
688
|
+
|| (errorCategory === "explicit_limit" && isCursorAutoFallbackEligible(errorText))
|
|
689
|
+
);
|
|
690
|
+
const hasNextKey = quotaFailure && Boolean(cursorSelection) && cursorAttemptIndex < cursorAttempts.length - 1;
|
|
634
691
|
if (!canTryComposer && !hasNextKey) return false;
|
|
635
692
|
|
|
636
693
|
const retry = async () => {
|
|
637
694
|
if (canTryComposer) {
|
|
695
|
+
const authLabel = cursorSelection
|
|
696
|
+
? `API Key ${cursorApiKeyLabel(cursorSelection)}`
|
|
697
|
+
: "login session";
|
|
638
698
|
emit({
|
|
639
699
|
type: "status",
|
|
640
|
-
line:
|
|
700
|
+
line: loopingFailure
|
|
701
|
+
? `Cursor Auto on ${authLabel} entered a response loop; discovering Composer fallback...`
|
|
702
|
+
: `Cursor Auto on ${authLabel} is out of usage; discovering Composer fallback...`,
|
|
641
703
|
});
|
|
642
704
|
const catalog = await discoverCursorModels({
|
|
643
|
-
keyId: cursorSelection.
|
|
705
|
+
keyId: cursorSelection?.id || `login:${cursorBaseEnv.AGENTFLOW_USER_ID || "default"}`,
|
|
644
706
|
cwd: ws,
|
|
645
707
|
command: agentCmd,
|
|
646
708
|
env: childEnv(options, cursorApiKeyEnv(cursorSelection)),
|
|
647
709
|
});
|
|
648
710
|
if (catalog.fallbackModel) {
|
|
649
|
-
recordCursorApiKeyFallbackModel(cursorSelection, catalog.fallbackModel);
|
|
711
|
+
if (cursorSelection) recordCursorApiKeyFallbackModel(cursorSelection, catalog.fallbackModel);
|
|
650
712
|
const fallbackSelection = {
|
|
651
713
|
lane: "fallback",
|
|
652
714
|
modelId: catalog.fallbackModel.id,
|
|
@@ -662,6 +724,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
662
724
|
stream: "runner",
|
|
663
725
|
eventType: "model_fallback",
|
|
664
726
|
text: `auto -> ${fallbackSelection.modelId}`,
|
|
727
|
+
reason: loopingFailure ? "agent_looping" : "usage_limit",
|
|
665
728
|
});
|
|
666
729
|
const fallback = runCursorAgentWithPrompt(
|
|
667
730
|
cliWorkspace,
|
|
@@ -690,7 +753,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
690
753
|
await next.finished;
|
|
691
754
|
return;
|
|
692
755
|
}
|
|
693
|
-
throw new Error(errorText || "Cursor API Key reached its limit.");
|
|
756
|
+
throw annotateFailure(new Error(errorText || (loopingFailure ? "Cursor Agent entered a response loop." : "Cursor API Key reached its limit.")));
|
|
694
757
|
};
|
|
695
758
|
retry().then(resolve).catch(reject);
|
|
696
759
|
return true;
|
|
@@ -698,9 +761,9 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
698
761
|
if (code !== 0 && lastResult == null) {
|
|
699
762
|
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
700
763
|
const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
|
|
701
|
-
if (
|
|
764
|
+
if (retryCursorFailure(stderrTail)) return;
|
|
702
765
|
const stderrSummary = summarizeCursorStderr(stderr);
|
|
703
|
-
const err = new Error(`Cursor CLI exited ${code}. ${stderrSummary || "No result event received."}`);
|
|
766
|
+
const err = annotateFailure(new Error(`Cursor CLI exited ${code}. ${stderrSummary || "No result event received."}`));
|
|
704
767
|
err.cursorStderrTail = stderrTail;
|
|
705
768
|
emit({ type: "status", line: truncateComposerLine(err.message) });
|
|
706
769
|
reject(err);
|
|
@@ -708,9 +771,9 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
708
771
|
}
|
|
709
772
|
if (hadError || (lastResult && lastResult.is_error)) {
|
|
710
773
|
const msg = cursorResultErrorText(lastResult) || "Agent reported error.";
|
|
711
|
-
if (
|
|
774
|
+
if (retryCursorFailure(msg)) return;
|
|
712
775
|
emit({ type: "status", line: truncateComposerLine(msg) });
|
|
713
|
-
reject(new Error(msg));
|
|
776
|
+
reject(annotateFailure(new Error(msg)));
|
|
714
777
|
return;
|
|
715
778
|
}
|
|
716
779
|
if (cursorSelection) clearCursorApiKeyLaneCooldown(cursorSelection, cursorModelSelection.lane);
|
|
@@ -840,6 +903,7 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
|
|
|
840
903
|
const model = options.model && String(options.model).trim();
|
|
841
904
|
const claudeCmd = process.env.CLAUDE_CODE_CMD || "claude";
|
|
842
905
|
const bypassPermissions =
|
|
906
|
+
options.allowDanger !== false &&
|
|
843
907
|
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "0" &&
|
|
844
908
|
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "false";
|
|
845
909
|
const args = ["-p", "--output-format", "stream-json", "--verbose", "--add-dir", ws];
|
|
@@ -1033,6 +1097,8 @@ export function runCodexAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1033
1097
|
outputLastMessagePath,
|
|
1034
1098
|
promptText,
|
|
1035
1099
|
configArgs: options.codexConfigArgs,
|
|
1100
|
+
sandboxMode: options.sandboxMode,
|
|
1101
|
+
allowDanger: options.allowDanger !== false,
|
|
1036
1102
|
});
|
|
1037
1103
|
|
|
1038
1104
|
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
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse as acornParse } from "acorn";
|
|
4
|
+
|
|
5
|
+
import { PACKAGE_ROOT } from "./paths.mjs";
|
|
6
|
+
|
|
7
|
+
const WORKSPACE_RUNTIME_PATH = "bin/lib/workspace-server.mjs";
|
|
8
|
+
|
|
9
|
+
// Only modules explicitly listed here are exposed in full. The runtime adapter is
|
|
10
|
+
// extracted from workspace-server.mjs so the review always follows the code that
|
|
11
|
+
// actually dispatches the built-in node without exposing the whole server file.
|
|
12
|
+
const BUILTIN_NODE_IMPLEMENTATION_FILES = new Map([
|
|
13
|
+
["tool_wecom_send_group_markdown", ["bin/lib/wecom.mjs"]],
|
|
14
|
+
["tool_wecom_send_app_markdown", ["bin/lib/wecom.mjs"]],
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const sourceCache = new Map();
|
|
18
|
+
let runtimeAstCache = null;
|
|
19
|
+
|
|
20
|
+
function readPackageSource(relativePath) {
|
|
21
|
+
const normalized = String(relativePath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
22
|
+
if (!normalized || normalized.includes("..")) return "";
|
|
23
|
+
if (sourceCache.has(normalized)) return sourceCache.get(normalized);
|
|
24
|
+
const absolutePath = path.join(PACKAGE_ROOT, normalized);
|
|
25
|
+
let source = "";
|
|
26
|
+
try {
|
|
27
|
+
if (fs.statSync(absolutePath).isFile()) source = fs.readFileSync(absolutePath, "utf8").replace(/\r\n/g, "\n");
|
|
28
|
+
} catch {}
|
|
29
|
+
sourceCache.set(normalized, source);
|
|
30
|
+
return source;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function walkAst(node, visit) {
|
|
34
|
+
if (!node || typeof node !== "object") return;
|
|
35
|
+
if (typeof node.type === "string") visit(node);
|
|
36
|
+
for (const [key, value] of Object.entries(node)) {
|
|
37
|
+
if (["start", "end", "loc", "type"].includes(key)) continue;
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
for (const child of value) walkAst(child, visit);
|
|
40
|
+
} else if (value && typeof value === "object") {
|
|
41
|
+
walkAst(value, visit);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function astContainsString(node, expected) {
|
|
47
|
+
let found = false;
|
|
48
|
+
walkAst(node, (candidate) => {
|
|
49
|
+
if (candidate.type === "Literal" && candidate.value === expected) found = true;
|
|
50
|
+
});
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runtimeAst() {
|
|
55
|
+
const source = readPackageSource(WORKSPACE_RUNTIME_PATH);
|
|
56
|
+
if (!source) return { source: "", ast: null };
|
|
57
|
+
if (runtimeAstCache?.source === source) return runtimeAstCache;
|
|
58
|
+
let ast = null;
|
|
59
|
+
try {
|
|
60
|
+
ast = acornParse(source, { ecmaVersion: "latest", sourceType: "module", allowHashBang: true });
|
|
61
|
+
} catch {}
|
|
62
|
+
runtimeAstCache = { source, ast };
|
|
63
|
+
return runtimeAstCache;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runtimeAdapterSource(definitionId) {
|
|
67
|
+
const { source, ast } = runtimeAst();
|
|
68
|
+
if (!source || !ast) return "";
|
|
69
|
+
const matches = [];
|
|
70
|
+
walkAst(ast, (node) => {
|
|
71
|
+
if (node.type !== "IfStatement" || !astContainsString(node.test, definitionId)) return;
|
|
72
|
+
matches.push(source.slice(node.start, node.end).trim());
|
|
73
|
+
});
|
|
74
|
+
return matches.join("\n\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function builtinNodeReviewSources(definitionId = "") {
|
|
78
|
+
const id = String(definitionId || "").trim();
|
|
79
|
+
if (!id) return [];
|
|
80
|
+
const sources = [];
|
|
81
|
+
const adapter = runtimeAdapterSource(id);
|
|
82
|
+
if (adapter) {
|
|
83
|
+
sources.push({
|
|
84
|
+
sourcePath: `builtin/${id}/runtime-adapter.mjs`,
|
|
85
|
+
title: "内置运行适配器",
|
|
86
|
+
kind: "builtin-adapter",
|
|
87
|
+
content: adapter,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const relativePath of BUILTIN_NODE_IMPLEMENTATION_FILES.get(id) || []) {
|
|
91
|
+
const content = readPackageSource(relativePath);
|
|
92
|
+
if (!content) continue;
|
|
93
|
+
sources.push({
|
|
94
|
+
sourcePath: relativePath,
|
|
95
|
+
title: `内置实现 · ${path.basename(relativePath)}`,
|
|
96
|
+
kind: "builtin-implementation",
|
|
97
|
+
content,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return sources;
|
|
101
|
+
}
|