@fieldwangai/agentflow 0.1.165 → 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 +131 -21
- package/bin/lib/ai-exploration.mjs +293 -0
- package/bin/lib/composer-agent.mjs +11 -0
- package/bin/lib/cursor-api-key-pool.mjs +246 -32
- package/bin/lib/cursor-model-catalog.mjs +152 -0
- 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 +947 -154
- package/bin/lib/workspace-server.mjs +3 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-pVdrZ-Rl.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 +2 -2
- 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 +242 -137
- package/skills/agentflow-cli/runtime/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-BQeq5tdj.css +0 -1
- package/builtin/web-ui/dist/assets/index-Czutb6ai.js +0 -873
|
@@ -7,13 +7,19 @@ import { normalizeCursorModelForCli } from "./model-config.mjs";
|
|
|
7
7
|
import { t } from "./i18n.mjs";
|
|
8
8
|
import { readMergedEnvObject } from "./user-env.mjs";
|
|
9
9
|
import {
|
|
10
|
+
classifyCursorApiKeyLimitError,
|
|
11
|
+
clearCursorApiKeyLaneCooldown,
|
|
10
12
|
createCursorApiKeyAttempts,
|
|
11
13
|
cursorApiKeyCooldownMinutes,
|
|
12
14
|
cursorApiKeyEnv,
|
|
13
15
|
cursorApiKeyLabel,
|
|
16
|
+
isCursorAutoFallbackEligible,
|
|
14
17
|
isCursorQuotaError,
|
|
15
|
-
|
|
18
|
+
markCursorApiKeyLaneBlocked,
|
|
19
|
+
recordCursorApiKeyFallbackModel,
|
|
20
|
+
recordCursorApiKeyUsage,
|
|
16
21
|
} from "./cursor-api-key-pool.mjs";
|
|
22
|
+
import { discoverCursorModels } from "./cursor-model-catalog.mjs";
|
|
17
23
|
import { outputNodeBasename } from "../pipeline/get-exec-id.mjs";
|
|
18
24
|
|
|
19
25
|
function shouldPassCursorModelArg(model) {
|
|
@@ -53,6 +59,16 @@ function nextCursorAttemptOptions(options = {}, attempts = [], attemptIndex = 0)
|
|
|
53
59
|
...options,
|
|
54
60
|
_agentflowCursorApiKeyAttempts: attempts,
|
|
55
61
|
_agentflowCursorApiKeyAttemptIndex: attemptIndex + 1,
|
|
62
|
+
_agentflowCursorModelSelection: undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cursorModelAttemptOptions(options = {}, attempts = [], attemptIndex = 0, modelSelection) {
|
|
67
|
+
return {
|
|
68
|
+
...options,
|
|
69
|
+
_agentflowCursorApiKeyAttempts: attempts,
|
|
70
|
+
_agentflowCursorApiKeyAttemptIndex: attemptIndex,
|
|
71
|
+
_agentflowCursorModelSelection: modelSelection,
|
|
56
72
|
};
|
|
57
73
|
}
|
|
58
74
|
|
|
@@ -108,7 +124,7 @@ function shouldSkipCodexGitCheck(workspace) {
|
|
|
108
124
|
return !hasGitMetadataAncestor(workspace);
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessagePath, promptText, configArgs = [] }) {
|
|
127
|
+
function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessagePath, promptText, configArgs = [], sandboxMode = "", allowDanger = true }) {
|
|
112
128
|
const args = [];
|
|
113
129
|
for (const cfg of Array.isArray(configArgs) ? configArgs : []) {
|
|
114
130
|
const value = String(cfg || "").trim();
|
|
@@ -122,10 +138,10 @@ function buildCodexExecArgs({ workspace, addDirs = [], model, outputLastMessageP
|
|
|
122
138
|
const abs = path.resolve(dir);
|
|
123
139
|
if (abs && abs !== workspace) args.push("--add-dir", abs);
|
|
124
140
|
}
|
|
125
|
-
if (envFlag("AGENTFLOW_CODEX_DANGER", false)) {
|
|
141
|
+
if (allowDanger && envFlag("AGENTFLOW_CODEX_DANGER", false)) {
|
|
126
142
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
127
143
|
} else {
|
|
128
|
-
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");
|
|
129
145
|
}
|
|
130
146
|
if (shouldSkipCodexGitCheck(workspace)) args.push("--skip-git-repo-check");
|
|
131
147
|
if (envFlag("AGENTFLOW_CODEX_EPHEMERAL", false)) args.push("--ephemeral");
|
|
@@ -402,7 +418,7 @@ function tryEmitOpenCodeLineAsNatural(line, emit) {
|
|
|
402
418
|
export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {}) {
|
|
403
419
|
const onStreamEvent = typeof options.onStreamEvent === "function" ? options.onStreamEvent : null;
|
|
404
420
|
const ws = path.resolve(cliWorkspace);
|
|
405
|
-
const
|
|
421
|
+
const requestedModel = normalizeCursorModelForCli(options.model ?? process.env.CURSOR_AGENT_MODEL ?? null);
|
|
406
422
|
const agentCmd = process.env.CURSOR_AGENT_CMD || "agent";
|
|
407
423
|
const {
|
|
408
424
|
baseEnv: cursorBaseEnv,
|
|
@@ -410,11 +426,23 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
410
426
|
attemptIndex: cursorAttemptIndex,
|
|
411
427
|
selection: cursorSelection,
|
|
412
428
|
} = cursorAttemptOptions(options);
|
|
429
|
+
const hasExplicitModel = shouldPassCursorModelArg(requestedModel);
|
|
430
|
+
const cursorModelSelection = hasExplicitModel
|
|
431
|
+
? { lane: "auto", modelId: requestedModel, modelName: requestedModel }
|
|
432
|
+
: options._agentflowCursorModelSelection
|
|
433
|
+
|| cursorSelection?.modelSelection
|
|
434
|
+
|| { lane: "auto", modelId: "auto", modelName: "Auto" };
|
|
435
|
+
const model = hasExplicitModel ? requestedModel : cursorModelSelection.modelId;
|
|
436
|
+
if (cursorSelection) recordCursorApiKeyUsage(cursorSelection, cursorModelSelection);
|
|
413
437
|
// Web UI Composer 需要能无交互执行本机 curl 等命令来刷新画布。
|
|
414
|
-
const args = ["--print", "--output-format", "stream-json"
|
|
415
|
-
|
|
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");
|
|
416
444
|
if (approveMcps) args.push("--approve-mcps");
|
|
417
|
-
args.push("--force");
|
|
445
|
+
if (options.force !== false) args.push("--force");
|
|
418
446
|
if (shouldPassCursorModelArg(model)) args.push("--model", model);
|
|
419
447
|
args.push(promptText);
|
|
420
448
|
|
|
@@ -448,6 +476,12 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
448
476
|
line: `Cursor API Key ${cursorApiKeyLabel(cursorSelection)} / ${cursorAttempts.length}`,
|
|
449
477
|
});
|
|
450
478
|
}
|
|
479
|
+
if (cursorModelSelection.lane === "fallback") {
|
|
480
|
+
emit({
|
|
481
|
+
type: "status",
|
|
482
|
+
line: `Cursor is using Composer fallback: ${cursorModelSelection.modelName}`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
451
485
|
|
|
452
486
|
if (!useStderrInherit) {
|
|
453
487
|
child.stderr.on("data", (chunk) => {
|
|
@@ -523,14 +557,18 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
523
557
|
if (options.onToolCall) options.onToolCall("thinking", "");
|
|
524
558
|
} else if (event.type === "result") {
|
|
525
559
|
lastResult = event;
|
|
526
|
-
const resultNl =
|
|
560
|
+
const resultNl = options.includeJsonResult && typeof event.result === "string"
|
|
561
|
+
? normalizeStreamTextChunk(event.result)
|
|
562
|
+
: extractCursorResultNl(event);
|
|
527
563
|
if (resultNl) emit({ type: "natural", kind: "result", text: resultNl });
|
|
528
564
|
if (event.subtype === "success" && !event.is_error) {
|
|
529
565
|
hadError = false;
|
|
530
566
|
emit({ type: "status", line: t("runner.completed") });
|
|
531
567
|
} else {
|
|
532
568
|
hadError = true;
|
|
533
|
-
const errNl =
|
|
569
|
+
const errNl = options.includeJsonResult && typeof event.result === "string"
|
|
570
|
+
? normalizeStreamTextChunk(event.result)
|
|
571
|
+
: extractCursorResultNl(event);
|
|
534
572
|
if (errNl) emit({ type: "natural", kind: "error", text: errNl });
|
|
535
573
|
emit({
|
|
536
574
|
type: "status",
|
|
@@ -588,20 +626,88 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
588
626
|
}
|
|
589
627
|
const retryCursorQuota = (errorText) => {
|
|
590
628
|
if (!cursorSelection) return false;
|
|
591
|
-
if (cursorAttemptIndex >= cursorAttempts.length - 1) return false;
|
|
592
629
|
if (hadToolActivity) return false;
|
|
593
630
|
if (!isCursorQuotaError(errorText)) return false;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
631
|
+
const errorCategory = classifyCursorApiKeyLimitError(errorText);
|
|
632
|
+
const cooldownMinutes = cursorApiKeyCooldownMinutes(cursorBaseEnv, errorText);
|
|
633
|
+
markCursorApiKeyLaneBlocked(
|
|
634
|
+
cursorSelection,
|
|
635
|
+
cursorModelSelection.lane,
|
|
636
|
+
cooldownMinutes,
|
|
637
|
+
errorText,
|
|
638
|
+
Date.now(),
|
|
639
|
+
{
|
|
640
|
+
modelId: cursorModelSelection.modelId,
|
|
641
|
+
modelName: cursorModelSelection.modelName,
|
|
642
|
+
},
|
|
603
643
|
);
|
|
604
|
-
|
|
644
|
+
const canTryComposer = !hasExplicitModel
|
|
645
|
+
&& cursorModelSelection.lane === "auto"
|
|
646
|
+
&& errorCategory === "explicit_limit"
|
|
647
|
+
&& isCursorAutoFallbackEligible(errorText);
|
|
648
|
+
const hasNextKey = cursorAttemptIndex < cursorAttempts.length - 1;
|
|
649
|
+
if (!canTryComposer && !hasNextKey) return false;
|
|
650
|
+
|
|
651
|
+
const retry = async () => {
|
|
652
|
+
if (canTryComposer) {
|
|
653
|
+
emit({
|
|
654
|
+
type: "status",
|
|
655
|
+
line: `Cursor Auto on API Key ${cursorApiKeyLabel(cursorSelection)} is out of usage; discovering Composer fallback...`,
|
|
656
|
+
});
|
|
657
|
+
const catalog = await discoverCursorModels({
|
|
658
|
+
keyId: cursorSelection.id,
|
|
659
|
+
cwd: ws,
|
|
660
|
+
command: agentCmd,
|
|
661
|
+
env: childEnv(options, cursorApiKeyEnv(cursorSelection)),
|
|
662
|
+
});
|
|
663
|
+
if (catalog.fallbackModel) {
|
|
664
|
+
recordCursorApiKeyFallbackModel(cursorSelection, catalog.fallbackModel);
|
|
665
|
+
const fallbackSelection = {
|
|
666
|
+
lane: "fallback",
|
|
667
|
+
modelId: catalog.fallbackModel.id,
|
|
668
|
+
modelName: catalog.fallbackModel.displayName,
|
|
669
|
+
};
|
|
670
|
+
emit({
|
|
671
|
+
type: "status",
|
|
672
|
+
line: `Switching the same Cursor API Key to ${fallbackSelection.modelName}.`,
|
|
673
|
+
});
|
|
674
|
+
emit({
|
|
675
|
+
type: "raw",
|
|
676
|
+
source: "cursor",
|
|
677
|
+
stream: "runner",
|
|
678
|
+
eventType: "model_fallback",
|
|
679
|
+
text: `auto -> ${fallbackSelection.modelId}`,
|
|
680
|
+
});
|
|
681
|
+
const fallback = runCursorAgentWithPrompt(
|
|
682
|
+
cliWorkspace,
|
|
683
|
+
promptText,
|
|
684
|
+
cursorModelAttemptOptions(options, cursorAttempts, cursorAttemptIndex, fallbackSelection),
|
|
685
|
+
);
|
|
686
|
+
await fallback.finished;
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
emit({
|
|
690
|
+
type: "status",
|
|
691
|
+
line: `Composer fallback is unavailable${catalog.error ? `: ${catalog.error}` : "."}`,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (hasNextKey) {
|
|
696
|
+
emit({
|
|
697
|
+
type: "status",
|
|
698
|
+
line: `Cursor API Key ${cursorApiKeyLabel(cursorSelection)} reached its limit; retrying ${cursorAttemptIndex + 2}/${cursorAttempts.length}.`,
|
|
699
|
+
});
|
|
700
|
+
const next = runCursorAgentWithPrompt(
|
|
701
|
+
cliWorkspace,
|
|
702
|
+
promptText,
|
|
703
|
+
nextCursorAttemptOptions(options, cursorAttempts, cursorAttemptIndex),
|
|
704
|
+
);
|
|
705
|
+
await next.finished;
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
throw new Error(errorText || "Cursor API Key reached its limit.");
|
|
709
|
+
};
|
|
710
|
+
retry().then(resolve).catch(reject);
|
|
605
711
|
return true;
|
|
606
712
|
};
|
|
607
713
|
if (code !== 0 && lastResult == null) {
|
|
@@ -622,6 +728,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
622
728
|
reject(new Error(msg));
|
|
623
729
|
return;
|
|
624
730
|
}
|
|
731
|
+
if (cursorSelection) clearCursorApiKeyLaneCooldown(cursorSelection, cursorModelSelection.lane);
|
|
625
732
|
resolve();
|
|
626
733
|
});
|
|
627
734
|
});
|
|
@@ -748,6 +855,7 @@ export function runClaudeCodeAgentWithPrompt(cliWorkspace, promptText, options =
|
|
|
748
855
|
const model = options.model && String(options.model).trim();
|
|
749
856
|
const claudeCmd = process.env.CLAUDE_CODE_CMD || "claude";
|
|
750
857
|
const bypassPermissions =
|
|
858
|
+
options.allowDanger !== false &&
|
|
751
859
|
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "0" &&
|
|
752
860
|
process.env.AGENTFLOW_CLAUDE_CODE_BYPASS_PERMISSIONS !== "false";
|
|
753
861
|
const args = ["-p", "--output-format", "stream-json", "--verbose", "--add-dir", ws];
|
|
@@ -941,6 +1049,8 @@ export function runCodexAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
941
1049
|
outputLastMessagePath,
|
|
942
1050
|
promptText,
|
|
943
1051
|
configArgs: options.codexConfigArgs,
|
|
1052
|
+
sandboxMode: options.sandboxMode,
|
|
1053
|
+
allowDanger: options.allowDanger !== false,
|
|
944
1054
|
});
|
|
945
1055
|
|
|
946
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)
|