@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
|
@@ -7,6 +7,11 @@ import { runLedgerId } from "./run-ledger.mjs";
|
|
|
7
7
|
const MAX_EVENT_TEXT_CHARS = 20_000;
|
|
8
8
|
const MAX_INDEX_RECORDS = 10_000;
|
|
9
9
|
const SECRET_KEY_RE = /(token|password|passwd|secret|webhook|authorization|api[_-]?key|access[_-]?key)/i;
|
|
10
|
+
const SECRET_TEXT_PATTERNS = [
|
|
11
|
+
[/\b(Bearer)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 ***"],
|
|
12
|
+
[/((?:token|password|passwd|secret|authorization|api[_-]?key|access[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, "$1***"],
|
|
13
|
+
[/(["'](?:token|password|passwd|secret|authorization|api[_-]?key|access[_-]?key)["']\s*:\s*)("[^"]*"|'[^']*')/gi, "$1\"***\""],
|
|
14
|
+
];
|
|
10
15
|
|
|
11
16
|
function safeSegment(value, fallback = "run") {
|
|
12
17
|
return String(value || fallback)
|
|
@@ -34,9 +39,15 @@ function truncateText(value) {
|
|
|
34
39
|
return `${text.slice(0, MAX_EVENT_TEXT_CHARS)}\n... [truncated ${text.length - MAX_EVENT_TEXT_CHARS} chars]`;
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
function redactText(value) {
|
|
43
|
+
let text = truncateText(value);
|
|
44
|
+
for (const [pattern, replacement] of SECRET_TEXT_PATTERNS) text = text.replace(pattern, replacement);
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
function redact(value, depth = 0) {
|
|
38
49
|
if (depth > 8) return "[MaxDepth]";
|
|
39
|
-
if (typeof value === "string") return
|
|
50
|
+
if (typeof value === "string") return redactText(value);
|
|
40
51
|
if (value == null || typeof value === "number" || typeof value === "boolean") return value;
|
|
41
52
|
if (Array.isArray(value)) return value.slice(0, 200).map((item) => redact(item, depth + 1));
|
|
42
53
|
if (typeof value === "object") {
|
|
@@ -35,6 +35,7 @@ import { log } from "./log.mjs";
|
|
|
35
35
|
import { resolveMarketplaceNodePackage } from "./marketplace.mjs";
|
|
36
36
|
import { marketplaceResourcesForRun, recordMarketplaceRunUsage } from "./marketplace-usage.mjs";
|
|
37
37
|
import { PACKAGE_ROOT, getAgentflowDataRoot, getAgentflowUserDataRoot, listAgentflowUserIds } from "./paths.mjs";
|
|
38
|
+
import { emitRepositoryRunFinished } from "./repository-index-events.mjs";
|
|
38
39
|
import { appendRunLedgerEvent, readRunLedgerEvents, runLedgerId } from "./run-ledger.mjs";
|
|
39
40
|
import { computeNextRunAt } from "./schedule-config.mjs";
|
|
40
41
|
import { listTeams } from "./teams.mjs";
|
|
@@ -845,12 +846,21 @@ function workspaceReleaseSummary(workspaceRoot, marketplaceRoot = "", graph = nu
|
|
|
845
846
|
const currentGraph = graph || readWorkspaceGraph(workspaceRoot, marketplaceRoot).graph;
|
|
846
847
|
const draftRevision = workspaceDesignRevision(currentGraph);
|
|
847
848
|
const stable = registry.releases.find((release) => release.id === registry.stableReleaseId) || null;
|
|
849
|
+
const comparableStableRevisions = new Set([stable?.designRevision || ""].filter(Boolean));
|
|
850
|
+
const stableRoot = stable ? workspaceReleaseSnapshotRoot(workspaceRoot, stable.id) : "";
|
|
851
|
+
if (stableRoot && fs.existsSync(stableRoot)) {
|
|
852
|
+
try {
|
|
853
|
+
comparableStableRevisions.add(workspaceDesignRevision(readWorkspaceGraph(stableRoot, marketplaceRoot).graph));
|
|
854
|
+
} catch {
|
|
855
|
+
// Keep the immutable manifest revision when an old snapshot cannot be normalized.
|
|
856
|
+
}
|
|
857
|
+
}
|
|
848
858
|
return {
|
|
849
859
|
enabled: Boolean(stable),
|
|
850
860
|
stableReleaseId: stable?.id || "",
|
|
851
861
|
stableRevision: stable?.designRevision || "",
|
|
852
862
|
draftRevision,
|
|
853
|
-
hasDraftChanges: Boolean(stable &&
|
|
863
|
+
hasDraftChanges: Boolean(stable && !comparableStableRevisions.has(draftRevision)),
|
|
854
864
|
releases: registry.releases,
|
|
855
865
|
};
|
|
856
866
|
}
|
|
@@ -1170,6 +1180,7 @@ export function appendWorkspaceRunFinished(record, status) {
|
|
|
1170
1180
|
...record,
|
|
1171
1181
|
status,
|
|
1172
1182
|
});
|
|
1183
|
+
emitRepositoryRunFinished(record.workspaceRoot || record.root || "", record, status);
|
|
1173
1184
|
}
|
|
1174
1185
|
|
|
1175
1186
|
function normalizeWorkspaceUsageRecord(parsed, source = "workspace-run") {
|
|
@@ -2785,6 +2796,60 @@ export function workspaceMaterializeAgentResultFile(structured, runPackage) {
|
|
|
2785
2796
|
};
|
|
2786
2797
|
}
|
|
2787
2798
|
|
|
2799
|
+
export function workspaceIsAgentLoopingError(error = "") {
|
|
2800
|
+
const text = String(error?.message || error || "");
|
|
2801
|
+
return error?.code === "CURSOR_AGENT_LOOPING"
|
|
2802
|
+
|| error?.agentflowFailureCategory === "agent_looping"
|
|
2803
|
+
|| /agent looping detected|got stuck in a repeating response pattern/i.test(text);
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
function workspaceValidateRecoveredResultFile(filePath) {
|
|
2807
|
+
try {
|
|
2808
|
+
if (!filePath || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
2809
|
+
return { valid: false, reason: "result file is missing", size: 0 };
|
|
2810
|
+
}
|
|
2811
|
+
const stat = fs.statSync(filePath);
|
|
2812
|
+
if (stat.size <= 0) return { valid: false, reason: "result file is empty", size: 0 };
|
|
2813
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2814
|
+
const text = fs.readFileSync(filePath, "utf-8");
|
|
2815
|
+
if (!text.trim()) return { valid: false, reason: "result file is blank", size: stat.size };
|
|
2816
|
+
if (ext === ".json") {
|
|
2817
|
+
try {
|
|
2818
|
+
JSON.parse(text);
|
|
2819
|
+
} catch {
|
|
2820
|
+
return { valid: false, reason: "result JSON is invalid", size: stat.size };
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
if (ext === ".html" && !/<(?:!doctype\s+html|html|head|body|main|section|article|div|svg|canvas)\b/i.test(text)) {
|
|
2824
|
+
return { valid: false, reason: "result HTML has no renderable root", size: stat.size };
|
|
2825
|
+
}
|
|
2826
|
+
return { valid: true, reason: "", size: stat.size };
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
return { valid: false, reason: error?.message || String(error), size: 0 };
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
/**
|
|
2833
|
+
* Cursor can finish writing the durable artifact and then fail while composing
|
|
2834
|
+
* a redundant final response. In that case the artifact is the authoritative
|
|
2835
|
+
* output, so recover it instead of replaying a node that may have side effects.
|
|
2836
|
+
*/
|
|
2837
|
+
export function workspaceRecoverAgentLoopingOutput(error, runPackage = {}) {
|
|
2838
|
+
if (!workspaceIsAgentLoopingError(error)) return null;
|
|
2839
|
+
const resultFile = workspaceSafeNodeOutputRelPath(runPackage?.resultFileRel || "");
|
|
2840
|
+
const resultFileAbs = String(runPackage?.resultFileAbs || workspaceNodeOutputWritePath(runPackage, resultFile) || "").trim();
|
|
2841
|
+
const validation = workspaceValidateRecoveredResultFile(resultFileAbs);
|
|
2842
|
+
if (!resultFile || !validation.valid) return null;
|
|
2843
|
+
return {
|
|
2844
|
+
category: "agent_looping",
|
|
2845
|
+
strategy: "reuse-result-file",
|
|
2846
|
+
resultFile,
|
|
2847
|
+
resultFileAbs,
|
|
2848
|
+
size: validation.size,
|
|
2849
|
+
content: ["---agentflow", `resultFile: ${resultFile}`, "---end"].join("\n"),
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2788
2853
|
function workspaceCollectNodeOutputFiles(runPackage, maxFiles = 500) {
|
|
2789
2854
|
const roots = [
|
|
2790
2855
|
runPackage?.outputsDir,
|
|
@@ -3095,7 +3160,7 @@ function workspaceDownstreamInputDescription(target, slot) {
|
|
|
3095
3160
|
return "";
|
|
3096
3161
|
}
|
|
3097
3162
|
|
|
3098
|
-
function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
3163
|
+
export function workspaceOutputProtocolRequirements(graph, nodeId, runPackage = {}) {
|
|
3099
3164
|
const instance = graph?.instances?.[nodeId] || {};
|
|
3100
3165
|
const outputSlots = Array.isArray(instance.output) ? instance.output : [];
|
|
3101
3166
|
const displayBindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
|
|
@@ -3118,6 +3183,8 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
3118
3183
|
const resultKind = resultSpec.kind;
|
|
3119
3184
|
const resultFile = resultSpec.resultFile;
|
|
3120
3185
|
const resultKindText = resultKind ? ` ${resultKind}` : "";
|
|
3186
|
+
const fileBackedResultKinds = new Set(["html", "react", "markdown", "code", "mermaid", "ascii", "chart", "table"]);
|
|
3187
|
+
const fileBackedResult = fileBackedResultKinds.has(resultKind) && String(runPackage?.resultFileAbs || "").trim();
|
|
3121
3188
|
const resultGuidance = {
|
|
3122
3189
|
html: "内容必须是可直接放入 iframe 渲染的 HTML;不要使用 Markdown 代码围栏。",
|
|
3123
3190
|
react: "内容必须是 React 工程 JSON,包含 title、entry、files;files 至少包含 src/App.jsx,可包含 CSS 文件。",
|
|
@@ -3131,19 +3198,33 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
3131
3198
|
}[resultKind] || "内容应满足任务要求。";
|
|
3132
3199
|
const envelopeExample = [
|
|
3133
3200
|
"---agentflow",
|
|
3134
|
-
"result: |",
|
|
3135
|
-
` <完整${resultKindText || "结果"}正文,每行缩进两个空格>`,
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
}
|
|
3201
|
+
fileBackedResult ? `resultFile: ${resultFile}` : "result: |",
|
|
3202
|
+
fileBackedResult ? "" : ` <完整${resultKindText || "结果"}正文,每行缩进两个空格>`,
|
|
3203
|
+
...(slots.length
|
|
3204
|
+
? [
|
|
3205
|
+
"outParams:",
|
|
3206
|
+
...slots.slice(0, 3).map((slot) => {
|
|
3207
|
+
const kind = displayByField.get(`outParams.${slot.name}`) || "";
|
|
3208
|
+
const fileLike = ["file", "image", "audio", "video", "binary"].includes(slot.type);
|
|
3209
|
+
return kind || fileLike
|
|
3210
|
+
? ` ${slot.name}: |\n <完整${kind ? ` ${kind}` : ""}正文,每行缩进四个空格>`
|
|
3211
|
+
: ` ${slot.name}: <${slot.name} 的短值>`;
|
|
3212
|
+
}),
|
|
3213
|
+
]
|
|
3214
|
+
: []),
|
|
3144
3215
|
"---end",
|
|
3145
3216
|
].join("\n");
|
|
3146
|
-
const
|
|
3217
|
+
const fileBackedInstructions = fileBackedResult
|
|
3218
|
+
? [
|
|
3219
|
+
`完整结果必须直接写入:\`${runPackage.resultFileAbs}\`(环境变量 \`AGENTFLOW_RESULT_FILE\`)。`,
|
|
3220
|
+
`该文件是本次节点的持久产物,对外路径为 \`${resultFile}\`。不要把完整正文再次放进最终回复。`,
|
|
3221
|
+
"写完后检查文件存在且非空;HTML/JSON 等结构化产物还要确认格式完整。",
|
|
3222
|
+
"最终只输出下面的 agentflow 回执,不要输出正文、解释、进度或其它文字:",
|
|
3223
|
+
"",
|
|
3224
|
+
envelopeExample,
|
|
3225
|
+
]
|
|
3226
|
+
: null;
|
|
3227
|
+
const finalInstructions = fileBackedInstructions || (slots.length
|
|
3147
3228
|
? [
|
|
3148
3229
|
`AgentFlow 会自动把 \`result\` 正文写入 \`${resultFile}\`;不要自行创建该文件,也不要返回 \`resultFile\`。`,
|
|
3149
3230
|
`额外输出:${slots.map((slot) => `\`${slot.name}\``).join("、")}。文件型或展示型内容也直接内联,AgentFlow 负责落盘和传递。`,
|
|
@@ -3154,7 +3235,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
3154
3235
|
: [
|
|
3155
3236
|
`AgentFlow 会自动把最终回复写入 \`${resultFile}\`;不要自行创建该文件,不要返回路径或 agentflow envelope。`,
|
|
3156
3237
|
"最终回复只输出完整结果正文,不要附加解释、进度或其它文字。",
|
|
3157
|
-
];
|
|
3238
|
+
]);
|
|
3158
3239
|
return [
|
|
3159
3240
|
"## 输出",
|
|
3160
3241
|
"",
|
|
@@ -3554,7 +3635,7 @@ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
|
|
|
3554
3635
|
nodeRunDir ? `- 当前执行目录:\`${nodeRunDir}\`。` : "",
|
|
3555
3636
|
nodeTmpDir ? `- 临时文件只能写入:\`${nodeTmpDir}\`,也可通过环境变量 \`AGENTFLOW_NODE_TMP_DIR\` 获取。` : "",
|
|
3556
3637
|
Object.keys(runPackage?.inputMounts || {}).length ? "- 已挂载的输入文件位于本任务 `inputs/`;`inputs/` 只用于读取,正式产物仍写入 `outputs/`。" : "",
|
|
3557
|
-
"-
|
|
3638
|
+
"- 主结果按下方输出协议交付;若协议要求写入 `AGENTFLOW_RESULT_FILE`,必须写入该精确路径并只返回简短回执。",
|
|
3558
3639
|
outputsDir ? `- 可供用户下载的最终产物目录:\`${outputsDir}\`。这不是临时目录,环境变量 \`AGENTFLOW_OUTPUTS_DIR\` 指向这里。` : "",
|
|
3559
3640
|
outputsDir ? `- CSV、图片、压缩包、工程文件等下载产物必须直接写入 \`AGENTFLOW_OUTPUTS_DIR\`,不要写入当前执行目录中的相对 \`outputs/\`。` : "",
|
|
3560
3641
|
outputsDir ? `- 该目录中的文件会自动出现在 Workspace Files,对应相对路径为 \`${outputsRel}/\`;回复中引用下载文件时使用这个相对路径。` : "",
|
|
@@ -4749,7 +4830,7 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
4749
4830
|
const runPackage = typeof nodeTmpDir === "object" && nodeTmpDir ? nodeTmpDir : { nodeTmpDir: String(nodeTmpDir || "") };
|
|
4750
4831
|
const inputBlock = workspaceAgentInputBlock(relevantInputValues, runPackage.inputMounts || {});
|
|
4751
4832
|
const fileBoundary = workspaceNodeFileBoundaryBlock(runPackage);
|
|
4752
|
-
const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
|
|
4833
|
+
const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId, runPackage);
|
|
4753
4834
|
return [
|
|
4754
4835
|
"你正在执行一个独立任务。只使用本提示中的任务、输入、可用能力和文件边界。",
|
|
4755
4836
|
fileBoundary ? `\n${fileBoundary}` : "",
|
|
@@ -5233,7 +5314,18 @@ async function workspaceRunToolNodejsScript({
|
|
|
5233
5314
|
: "";
|
|
5234
5315
|
if (!command) throw new Error("tool_nodejs requires script or scriptRef");
|
|
5235
5316
|
|
|
5236
|
-
|
|
5317
|
+
const script = inlineScript || (scriptAbs ? fs.readFileSync(scriptAbs, "utf-8") : "");
|
|
5318
|
+
const started = Date.now();
|
|
5319
|
+
emit?.({
|
|
5320
|
+
type: "script-start",
|
|
5321
|
+
kind: "tool",
|
|
5322
|
+
script,
|
|
5323
|
+
scriptRef,
|
|
5324
|
+
scriptSha256: crypto.createHash("sha256").update(script, "utf-8").digest("hex"),
|
|
5325
|
+
cwd: runPackage.nodeRunDir,
|
|
5326
|
+
startedAt: new Date(started).toISOString(),
|
|
5327
|
+
status: "running",
|
|
5328
|
+
});
|
|
5237
5329
|
|
|
5238
5330
|
const env = runtimeEnvForUser(userCtx, {
|
|
5239
5331
|
...envOverlay,
|
|
@@ -5247,7 +5339,6 @@ async function workspaceRunToolNodejsScript({
|
|
|
5247
5339
|
AGENTFLOW_OUTPUTS_ABS_JSON: JSON.stringify(outputAbs),
|
|
5248
5340
|
});
|
|
5249
5341
|
|
|
5250
|
-
const started = Date.now();
|
|
5251
5342
|
return await new Promise((resolve, reject) => {
|
|
5252
5343
|
const processGroup = process.platform !== "win32";
|
|
5253
5344
|
const child = spawn(command, [], {
|
|
@@ -5294,14 +5385,50 @@ async function workspaceRunToolNodejsScript({
|
|
|
5294
5385
|
}
|
|
5295
5386
|
stderr += text;
|
|
5296
5387
|
});
|
|
5297
|
-
child.on("error", (error) => finish(() =>
|
|
5388
|
+
child.on("error", (error) => finish(() => {
|
|
5389
|
+
emit?.({
|
|
5390
|
+
type: "script-finish",
|
|
5391
|
+
kind: "error",
|
|
5392
|
+
scriptSha256: crypto.createHash("sha256").update(script, "utf-8").digest("hex"),
|
|
5393
|
+
cwd: runPackage.nodeRunDir,
|
|
5394
|
+
stderr: error.message || String(error),
|
|
5395
|
+
exitCode: null,
|
|
5396
|
+
durationMs: Math.max(0, Date.now() - started),
|
|
5397
|
+
status: "error",
|
|
5398
|
+
});
|
|
5399
|
+
reject(error);
|
|
5400
|
+
}));
|
|
5298
5401
|
child.on("close", (code) => {
|
|
5299
5402
|
if (outputLimitError) {
|
|
5300
|
-
finish(() =>
|
|
5403
|
+
finish(() => {
|
|
5404
|
+
emit?.({
|
|
5405
|
+
type: "script-finish",
|
|
5406
|
+
kind: "error",
|
|
5407
|
+
scriptSha256: crypto.createHash("sha256").update(script, "utf-8").digest("hex"),
|
|
5408
|
+
cwd: runPackage.nodeRunDir,
|
|
5409
|
+
stdout,
|
|
5410
|
+
stderr: outputLimitError.message,
|
|
5411
|
+
exitCode: code,
|
|
5412
|
+
durationMs: Math.max(0, Date.now() - started),
|
|
5413
|
+
status: "error",
|
|
5414
|
+
});
|
|
5415
|
+
reject(outputLimitError);
|
|
5416
|
+
});
|
|
5301
5417
|
return;
|
|
5302
5418
|
}
|
|
5303
5419
|
if (signal?.aborted) {
|
|
5304
5420
|
finish(() => {
|
|
5421
|
+
emit?.({
|
|
5422
|
+
type: "script-finish",
|
|
5423
|
+
kind: "tool",
|
|
5424
|
+
scriptSha256: crypto.createHash("sha256").update(script, "utf-8").digest("hex"),
|
|
5425
|
+
cwd: runPackage.nodeRunDir,
|
|
5426
|
+
stdout,
|
|
5427
|
+
stderr,
|
|
5428
|
+
exitCode: code,
|
|
5429
|
+
durationMs: Math.max(0, Date.now() - started),
|
|
5430
|
+
status: "cancelled",
|
|
5431
|
+
});
|
|
5305
5432
|
const error = new Error("Workspace run stopped");
|
|
5306
5433
|
error.code = "WORKSPACE_RUN_ABORTED";
|
|
5307
5434
|
reject(error);
|
|
@@ -5313,11 +5440,34 @@ async function workspaceRunToolNodejsScript({
|
|
|
5313
5440
|
else emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
|
|
5314
5441
|
}
|
|
5315
5442
|
if (code !== 0) {
|
|
5316
|
-
finish(() =>
|
|
5443
|
+
finish(() => {
|
|
5444
|
+
emit?.({
|
|
5445
|
+
type: "script-finish",
|
|
5446
|
+
kind: "error",
|
|
5447
|
+
scriptSha256: crypto.createHash("sha256").update(script, "utf-8").digest("hex"),
|
|
5448
|
+
cwd: runPackage.nodeRunDir,
|
|
5449
|
+
stdout,
|
|
5450
|
+
stderr,
|
|
5451
|
+
exitCode: code,
|
|
5452
|
+
durationMs: Math.max(0, Date.now() - started),
|
|
5453
|
+
status: "error",
|
|
5454
|
+
});
|
|
5455
|
+
reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`));
|
|
5456
|
+
});
|
|
5317
5457
|
return;
|
|
5318
5458
|
}
|
|
5319
5459
|
const elapsedMs = Math.max(0, Date.now() - started);
|
|
5320
|
-
emit?.({
|
|
5460
|
+
emit?.({
|
|
5461
|
+
type: "script-finish",
|
|
5462
|
+
kind: "tool",
|
|
5463
|
+
scriptSha256: crypto.createHash("sha256").update(script, "utf-8").digest("hex"),
|
|
5464
|
+
cwd: runPackage.nodeRunDir,
|
|
5465
|
+
stdout,
|
|
5466
|
+
stderr,
|
|
5467
|
+
exitCode: code,
|
|
5468
|
+
durationMs: elapsedMs,
|
|
5469
|
+
status: "success",
|
|
5470
|
+
});
|
|
5321
5471
|
// stdout 和输出文件不是二选一:文档里的代码节点范例就是「写 outputs.total + 打印
|
|
5322
5472
|
// 一行进度」,旧写法 `stdout || 信封` 会让那一行 console.log 把所有输出文件全吃掉。
|
|
5323
5473
|
// 规则改成:信封照给,stdout 非空时它就是 result(覆盖 result 文件,不动其它槽)。
|
|
@@ -6690,6 +6840,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
6690
6840
|
let content = "";
|
|
6691
6841
|
const runHistoryEvents = [];
|
|
6692
6842
|
const maxAttempts = 3;
|
|
6843
|
+
let loopingRetryUsed = false;
|
|
6693
6844
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
6694
6845
|
let attemptContent = "";
|
|
6695
6846
|
try {
|
|
@@ -6757,6 +6908,52 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
6757
6908
|
break;
|
|
6758
6909
|
} catch (e) {
|
|
6759
6910
|
if (signal?.aborted || e?.code === "WORKSPACE_RUN_ABORTED") throwIfAborted();
|
|
6911
|
+
const recovered = workspaceRecoverAgentLoopingOutput(e, runPackage);
|
|
6912
|
+
if (recovered) {
|
|
6913
|
+
content = recovered.content;
|
|
6914
|
+
emit({
|
|
6915
|
+
type: "agent-recovery",
|
|
6916
|
+
nodeId,
|
|
6917
|
+
category: recovered.category,
|
|
6918
|
+
strategy: recovered.strategy,
|
|
6919
|
+
status: "success",
|
|
6920
|
+
resultFile: recovered.resultFile,
|
|
6921
|
+
size: recovered.size,
|
|
6922
|
+
message: `Recovered ${recovered.resultFile} after Agent response looping without replaying the node.`,
|
|
6923
|
+
originalError: e?.message || String(e),
|
|
6924
|
+
});
|
|
6925
|
+
emit({
|
|
6926
|
+
type: "natural",
|
|
6927
|
+
kind: "warning",
|
|
6928
|
+
nodeId,
|
|
6929
|
+
text: `Agent 最终响应触发循环检测;已恢复其写入的结果文件 ${recovered.resultFile}(${recovered.size} bytes),未重跑节点。`,
|
|
6930
|
+
});
|
|
6931
|
+
break;
|
|
6932
|
+
}
|
|
6933
|
+
const safeLoopRetry = workspaceIsAgentLoopingError(e)
|
|
6934
|
+
&& e?.cursorHadMutatingToolActivity === false
|
|
6935
|
+
&& e?.cursorModelLane !== "fallback"
|
|
6936
|
+
&& !loopingRetryUsed
|
|
6937
|
+
&& attempt < maxAttempts;
|
|
6938
|
+
if (safeLoopRetry) {
|
|
6939
|
+
loopingRetryUsed = true;
|
|
6940
|
+
emit({
|
|
6941
|
+
type: "agent-retry",
|
|
6942
|
+
nodeId,
|
|
6943
|
+
category: "agent_looping",
|
|
6944
|
+
strategy: "new-turn",
|
|
6945
|
+
status: "retrying",
|
|
6946
|
+
attempt: attempt + 1,
|
|
6947
|
+
model: e?.cursorModelName || e?.cursorModelId || nodeModelKey || "default",
|
|
6948
|
+
message: "Started one clean Turn because looping occurred before any mutating tool call.",
|
|
6949
|
+
});
|
|
6950
|
+
emit({
|
|
6951
|
+
type: "status",
|
|
6952
|
+
nodeId,
|
|
6953
|
+
line: `Agent response loop detected before any mutating tool; starting one clean Turn (${attempt + 1}/${maxAttempts})`,
|
|
6954
|
+
});
|
|
6955
|
+
continue;
|
|
6956
|
+
}
|
|
6760
6957
|
if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
|
|
6761
6958
|
emit({ type: "status", nodeId, line: `Workspace node retry ${attempt + 1}/${maxAttempts} after network error` });
|
|
6762
6959
|
await sleepMs(Math.min(1500 * attempt, 5000), signal);
|