@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.
@@ -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 truncateText(value);
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 && stable.designRevision !== draftRevision),
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
- "outParams:",
3137
- ...slots.slice(0, 3).map((slot) => {
3138
- const kind = displayByField.get(`outParams.${slot.name}`) || "";
3139
- const fileLike = ["file", "image", "audio", "video", "binary"].includes(slot.type);
3140
- return kind || fileLike
3141
- ? ` ${slot.name}: |\n <完整${kind ? ` ${kind}` : ""}正文,每行缩进四个空格>`
3142
- : ` ${slot.name}: <${slot.name} 的短值>`;
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 finalInstructions = slots.length
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
- "- 主文本结果和内联额外输出由 AgentFlow 在任务结束后自动写入、发布和清理,无需自行创建结果文件。",
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
- emit?.({ type: "status", line: `Run script: ${scriptRef || command.slice(0, 120)}` });
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(() => reject(error)));
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(() => reject(outputLimitError));
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(() => reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`)));
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?.({ type: "status", line: `Timing script: ${elapsedMs}ms`, timing: { label: "script", elapsedMs } });
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);