@manny-est/node-red-flowpilot 0.5.1 → 0.5.2
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/CHANGELOG.md +79 -0
- package/README.md +21 -7
- package/USER-GUIDE.md +22 -13
- package/flowpilot-core.css +128 -2
- package/flowpilot.js +382 -27
- package/lib/build-system-prompt.js +26 -4
- package/lib/core/apply-review.js +234 -53
- package/lib/core/init.js +10 -0
- package/lib/core/main.js +92 -6
- package/lib/core/modes.js +698 -24
- package/lib/core/selection-context.js +28 -1
- package/lib/default-system-prompt.js +13 -9
- package/lib/document-system-prompt.js +19 -30
- package/lib/generation-system-prompt.js +24 -40
- package/lib/modify-system-prompt.js +98 -70
- package/lib/prompt-fragments.js +45 -0
- package/lib/provider-anthropic.js +385 -0
- package/lib/provider-openai-compatible.js +12 -3
- package/lib/storage.js +43 -2
- package/lib/validator.js +238 -0
- package/package.json +1 -1
package/lib/core/modes.js
CHANGED
|
@@ -812,15 +812,20 @@
|
|
|
812
812
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
813
813
|
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
814
814
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
815
|
-
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
815
|
+
// B1: bake the deploy-verify option into the review panel as the
|
|
816
|
+
// primary chip rather than a separate chip below it. Only for
|
|
817
|
+
// executable flows (not documentation-only comment nodes) and when no
|
|
818
|
+
// loop is already active. The secondary "Just add to canvas" button is
|
|
819
|
+
// always shown alongside it as an escape hatch.
|
|
820
|
+
var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
821
|
+
var _buildOnImported = (goalPrompt && !activeBuildLoop && _hasDeployable)
|
|
822
|
+
? function (importResult) { startBuildLoop(goalPrompt, flow, importResult); }
|
|
823
|
+
: null;
|
|
824
|
+
addGeneratedReview(flow, _buildOnImported, _buildOnImported ? goalPrompt : null);
|
|
825
|
+
// Suppress a server-suggested build chip when deploy-verify is already
|
|
826
|
+
// the primary action inside the review panel — it would be a duplicate.
|
|
827
|
+
renderActionChip(_buildOnImported && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
828
|
+
? null : data.suggestedAction);
|
|
824
829
|
setBusy(false);
|
|
825
830
|
updateSelectionStatus();
|
|
826
831
|
}
|
|
@@ -831,10 +836,41 @@
|
|
|
831
836
|
hidePending();
|
|
832
837
|
if (renderQuestionOrProse(data)) { return; }
|
|
833
838
|
|
|
839
|
+
// W4: parse and surface the Plan: block if present.
|
|
840
|
+
var planItems = parseTodoPlan(data.explanation || "");
|
|
841
|
+
var todoRec = null;
|
|
842
|
+
if (planItems.length) {
|
|
843
|
+
// Verification only produces one aggregate pass/fail result for
|
|
844
|
+
// the whole Modify response — mark every item active up front so
|
|
845
|
+
// they all resolve together instead of leaving items 2+ stuck at
|
|
846
|
+
// "pending" forever (only item 1 would ever flip otherwise).
|
|
847
|
+
planItems.forEach(function (item) { item.status = "active"; });
|
|
848
|
+
todoRec = addRecord("todo", { action: "modify", items: planItems });
|
|
849
|
+
rerenderTodoRecord(todoRec);
|
|
850
|
+
}
|
|
851
|
+
|
|
834
852
|
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
835
853
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
836
854
|
if (data.skippedNote) { addMessage("assistant", "⚠ " + data.skippedNote); }
|
|
837
|
-
|
|
855
|
+
|
|
856
|
+
// W4 Phase 2: wrap apply to run a real graph read-back via
|
|
857
|
+
// verifySteps (Track A, server) instead of unconditionally marking
|
|
858
|
+
// the todo done. Falls back to the Phase 1 behavior (check off with
|
|
859
|
+
// no verification) when the server sent no verifySteps.
|
|
860
|
+
var verifySteps = Array.isArray(data.verifySteps) ? data.verifySteps : [];
|
|
861
|
+
var applyCallback = todoRec ? function(nodeDiffs, removeNodes, $btn, idMap) {
|
|
862
|
+
applyModifications(nodeDiffs, removeNodes, $btn, idMap);
|
|
863
|
+
if (verifySteps.length) {
|
|
864
|
+
verifyModifySteps(verifySteps, idMap, todoRec);
|
|
865
|
+
} else {
|
|
866
|
+
todoRec.items.forEach(function(item) {
|
|
867
|
+
if (item.status === "active") { item.status = "done"; }
|
|
868
|
+
});
|
|
869
|
+
rerenderTodoRecord(todoRec);
|
|
870
|
+
}
|
|
871
|
+
} : applyModifications;
|
|
872
|
+
|
|
873
|
+
addModifyReview(data.flow, data.newNodes || [], data.newWires || [], data.removeNodes || [], applyCallback, null, data.newGroups || []);
|
|
838
874
|
renderActionChip(data.suggestedAction);
|
|
839
875
|
setBusy(false);
|
|
840
876
|
updateSelectionStatus();
|
|
@@ -1055,8 +1091,272 @@
|
|
|
1055
1091
|
ajaxJson("POST", fullEndpoint, payload, wrappedOnResult, onError);
|
|
1056
1092
|
}
|
|
1057
1093
|
|
|
1094
|
+
// W4: parse a "Plan:" block from the model's explanation field.
|
|
1095
|
+
// Returns an array of { text, status } items, or [] if none found.
|
|
1096
|
+
// Each numbered/bulleted line under "Plan:" up to the first blank line
|
|
1097
|
+
// becomes one item. Status starts as "pending" for all items — the
|
|
1098
|
+
// caller sets the first to "active" before rendering.
|
|
1099
|
+
function parseTodoPlan(explanation) {
|
|
1100
|
+
if (!explanation || typeof explanation !== "string") { return []; }
|
|
1101
|
+
var planStart = explanation.indexOf("Plan:");
|
|
1102
|
+
if (planStart === -1) { return []; }
|
|
1103
|
+
var afterPlan = explanation.slice(planStart + 5);
|
|
1104
|
+
var planBlock = afterPlan.split(/\n\n/)[0];
|
|
1105
|
+
var lines = planBlock.split("\n");
|
|
1106
|
+
var items = [];
|
|
1107
|
+
lines.forEach(function (line) {
|
|
1108
|
+
var stripped = line.replace(/^\s*\d+[.):\s]+/, "").replace(/^\s*[-*]\s+/, "").trim();
|
|
1109
|
+
if (stripped) { items.push({ text: stripped, status: "pending" }); }
|
|
1110
|
+
});
|
|
1111
|
+
return items;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// W4: render or re-render a "todo" record. For a 1-item plan, renders
|
|
1115
|
+
// as a compact status line (one chip). For N>1 items, renders as a
|
|
1116
|
+
// checklist card. Updates in place when the record already has a
|
|
1117
|
+
// data-fp-todo-id element in the message box (e.g. on verify check-off).
|
|
1118
|
+
function rerenderTodoRecord(rec) {
|
|
1119
|
+
if (!rec || !rec.items) { return; }
|
|
1120
|
+
var $box = el("#fp-messages");
|
|
1121
|
+
if (!$box.length) { return; }
|
|
1122
|
+
var items = rec.items;
|
|
1123
|
+
var $existing = $box.find("[data-fp-todo-id='" + rec.id + "']");
|
|
1124
|
+
|
|
1125
|
+
var $wrap;
|
|
1126
|
+
if (items.length === 1) {
|
|
1127
|
+
var item = items[0];
|
|
1128
|
+
var icon = item.status === "done" ? "✓" : item.status === "failed" ? "✗" : "▶";
|
|
1129
|
+
$wrap = $("<div>")
|
|
1130
|
+
.addClass("fp-todo-status fp-todo-" + item.status)
|
|
1131
|
+
.attr("data-fp-todo-id", rec.id)
|
|
1132
|
+
.text(icon + " " + item.text);
|
|
1133
|
+
} else {
|
|
1134
|
+
$wrap = $("<div>")
|
|
1135
|
+
.addClass("fp-todo-card")
|
|
1136
|
+
.attr("data-fp-todo-id", rec.id);
|
|
1137
|
+
var $ul = $("<ul>").addClass("fp-todo-list");
|
|
1138
|
+
items.forEach(function (item) {
|
|
1139
|
+
var icon = item.status === "done" ? "✓" :
|
|
1140
|
+
item.status === "failed" ? "✗" :
|
|
1141
|
+
item.status === "active" ? "▶" : "○";
|
|
1142
|
+
$("<li>")
|
|
1143
|
+
.addClass("fp-todo-item fp-todo-item-" + item.status)
|
|
1144
|
+
.text(icon + " " + item.text)
|
|
1145
|
+
.appendTo($ul);
|
|
1146
|
+
});
|
|
1147
|
+
$wrap.append($ul);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if ($existing.length) {
|
|
1151
|
+
$existing.replaceWith($wrap);
|
|
1152
|
+
} else {
|
|
1153
|
+
$box.append($wrap);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Step-queue path for Generate (opt-in via enableStepQueue setting).
|
|
1158
|
+
// After the user clicks "Add to workspace", performs a synchronous graph
|
|
1159
|
+
// read-back — calls RED.nodes.node(id) for every node that landed on the
|
|
1160
|
+
// canvas — and surfaces the result as a verification notice. This is the
|
|
1161
|
+
// structural verification step for Generate: "did the import actually land?"
|
|
1162
|
+
// not "does it do the thing?" (that's the semantic loop, Build's domain).
|
|
1163
|
+
// Skips comment and group nodes — those aren't addressable via RED.nodes.node.
|
|
1164
|
+
// todoRec: optional todo record to check off (or fail) after verification.
|
|
1165
|
+
function verifyImportedNodes(importResult, todoRec) {
|
|
1166
|
+
if (!importResult || !importResult.nodeMap) { return; }
|
|
1167
|
+
var nodeMap = importResult.nodeMap;
|
|
1168
|
+
var total = 0, found = 0, missing = [];
|
|
1169
|
+
// Config nodes (e.g. an http-request's TLS config, an mqtt broker
|
|
1170
|
+
// config) ride along in nodeMap whenever the model's own `flow`
|
|
1171
|
+
// array included them, but they aren't part of what the user asked
|
|
1172
|
+
// for — RED.nodes.node() resolves them same as regular nodes (it
|
|
1173
|
+
// checks configNodes[id] before falling back), so left uncounted
|
|
1174
|
+
// they'd silently inflate the headline total (e.g. "8" instead of
|
|
1175
|
+
// "5"). Track and verify them separately instead.
|
|
1176
|
+
var configTotal = 0, configFound = 0;
|
|
1177
|
+
// Node-RED's own RED.nodes.import (the generateIds:true path used
|
|
1178
|
+
// for every Generate/Build import) keys nodeMap TWICE per imported
|
|
1179
|
+
// node: once under the model's own placeholder id (assigned while
|
|
1180
|
+
// constructing the node, before it's added to the live registry)
|
|
1181
|
+
// and again under the freshly-generated real editor id (assigned in
|
|
1182
|
+
// the final addNode/addGroup/addJunction registration loop) — both
|
|
1183
|
+
// entries point to the same live node object. Left undeduped this
|
|
1184
|
+
// doubles every count here (visible AND config alike), independent
|
|
1185
|
+
// of the config/visible split above. Confirmed by reading
|
|
1186
|
+
// @node-red/editor-client/public/red/red.js's importNodes directly.
|
|
1187
|
+
var seenLiveIds = {};
|
|
1188
|
+
Object.keys(nodeMap).forEach(function (pid) {
|
|
1189
|
+
var liveNode = nodeMap[pid];
|
|
1190
|
+
if (!liveNode || !liveNode.id) { return; }
|
|
1191
|
+
if (seenLiveIds[liveNode.id]) { return; }
|
|
1192
|
+
seenLiveIds[liveNode.id] = true;
|
|
1193
|
+
if (liveNode.type === "comment" || liveNode.type === "group") { return; }
|
|
1194
|
+
if (liveNode._def && liveNode._def.category === "config") {
|
|
1195
|
+
configTotal++;
|
|
1196
|
+
if (RED.nodes.node(liveNode.id)) { configFound++; }
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
total++;
|
|
1200
|
+
if (RED.nodes.node(liveNode.id)) {
|
|
1201
|
+
found++;
|
|
1202
|
+
} else {
|
|
1203
|
+
missing.push(liveNode.type || pid);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
var configMissing = configTotal - configFound;
|
|
1207
|
+
var allGood = total > 0 && missing.length === 0 && configMissing === 0;
|
|
1208
|
+
if (total === 0) {
|
|
1209
|
+
// Nothing user-visible to verify (only comments/groups/config
|
|
1210
|
+
// nodes) — skip notice.
|
|
1211
|
+
} else if (allGood) {
|
|
1212
|
+
var configSuffix = configTotal > 0
|
|
1213
|
+
? " (+" + configTotal + " supporting config node(s))"
|
|
1214
|
+
: "";
|
|
1215
|
+
addMessage("fp-notice", "✓ Verified: all " + found + " node(s) confirmed on canvas" + configSuffix + ".");
|
|
1216
|
+
} else {
|
|
1217
|
+
var allMissing = missing.slice();
|
|
1218
|
+
if (configMissing > 0) { allMissing.push(configMissing + " config node(s)"); }
|
|
1219
|
+
addMessage("fp-notice", "⚠ Verification: " + found + "/" + total + " node(s) on canvas — " +
|
|
1220
|
+
allMissing.length + " not found after import (" + allMissing.join(", ") + "). " +
|
|
1221
|
+
"These may be uninstalled node types that were silently dropped.");
|
|
1222
|
+
}
|
|
1223
|
+
// Check off (or fail) the active todo item.
|
|
1224
|
+
if (todoRec && todoRec.items) {
|
|
1225
|
+
todoRec.items.forEach(function (item) {
|
|
1226
|
+
if (item.status === "active") { item.status = allGood ? "done" : "failed"; }
|
|
1227
|
+
});
|
|
1228
|
+
rerenderTodoRecord(todoRec);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// W4 Phase 2: real graph read-back verification for Modify, using the
|
|
1233
|
+
// server-derived verifySteps (Track A — property/exists/absent/wire
|
|
1234
|
+
// checks; see finalizeModifyResult in flowpilot.js). Mirrors
|
|
1235
|
+
// verifyImportedNodes's design for Generate: aggregate pass/fail across
|
|
1236
|
+
// all steps, surface one notice, and check off (or fail) the active
|
|
1237
|
+
// todo item. idMap resolves the response-time placeholder ids that
|
|
1238
|
+
// existence/wire checks on newly-inserted nodes carry (the server can't
|
|
1239
|
+
// know the browser-assigned id at response time — applyInsertions
|
|
1240
|
+
// assigns it and returns idMap). Property/absent checks already use
|
|
1241
|
+
// real existing-node ids, so idMap[id] simply misses and falls through
|
|
1242
|
+
// to the id unchanged.
|
|
1243
|
+
function verifyModifySteps(verifySteps, idMap, todoRec) {
|
|
1244
|
+
if (!Array.isArray(verifySteps) || !verifySteps.length) { return; }
|
|
1245
|
+
idMap = idMap || {};
|
|
1246
|
+
function resolve(id) { return (idMap && idMap[id]) || id; }
|
|
1247
|
+
|
|
1248
|
+
var total = 0, passed = 0, failures = [];
|
|
1249
|
+
verifySteps.forEach(function (step) {
|
|
1250
|
+
var ok = false;
|
|
1251
|
+
switch (step.check) {
|
|
1252
|
+
case "property": {
|
|
1253
|
+
var pNode = RED.nodes.node(resolve(step.nodeId));
|
|
1254
|
+
ok = !!pNode && pNode[step.prop] === step.expected;
|
|
1255
|
+
if (!ok) { failures.push((step.prop || "property") + " on " + step.nodeId); }
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
case "exists": {
|
|
1259
|
+
ok = !!RED.nodes.node(resolve(step.nodeId));
|
|
1260
|
+
if (!ok) { failures.push(step.nodeId + " missing"); }
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1263
|
+
case "absent": {
|
|
1264
|
+
ok = !RED.nodes.node(resolve(step.nodeId));
|
|
1265
|
+
if (!ok) { failures.push(step.nodeId + " still present"); }
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case "wire": {
|
|
1269
|
+
// Read from the live link registry (RED.nodes.eachLink), not
|
|
1270
|
+
// node.wires — RED.nodes.addLink/removeLink never re-sync a
|
|
1271
|
+
// live node's own .wires array mid-session (it's only set at
|
|
1272
|
+
// import and recomputed at export), so fromNode.wires[port] is
|
|
1273
|
+
// stale for any wire added/removed during the current editing
|
|
1274
|
+
// session. Mirrors computeWireDiff (apply-review.js).
|
|
1275
|
+
var fromId = resolve(step.fromId);
|
|
1276
|
+
var toId = resolve(step.toId);
|
|
1277
|
+
var port = step.fromPort || 0;
|
|
1278
|
+
ok = false;
|
|
1279
|
+
RED.nodes.eachLink(function (l) {
|
|
1280
|
+
if (ok) { return; }
|
|
1281
|
+
if (l.source && l.source.id === fromId &&
|
|
1282
|
+
(l.sourcePort || 0) === port &&
|
|
1283
|
+
l.target && l.target.id === toId) {
|
|
1284
|
+
ok = true;
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
if (!ok) { failures.push("wire " + step.fromId + " → " + step.toId); }
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
default:
|
|
1291
|
+
return; // unrecognized check type — don't count it either way
|
|
1292
|
+
}
|
|
1293
|
+
total++;
|
|
1294
|
+
if (ok) { passed++; }
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
if (total === 0) { return; }
|
|
1298
|
+
var allGood = failures.length === 0;
|
|
1299
|
+
if (allGood) {
|
|
1300
|
+
addMessage("fp-notice", "✓ Verified: all " + passed + " change(s) confirmed on canvas.");
|
|
1301
|
+
} else {
|
|
1302
|
+
addMessage("fp-notice", "⚠ Verification: " + passed + "/" + total + " change(s) confirmed — " +
|
|
1303
|
+
failures.length + " did not land as expected (" + failures.join(", ") + ").");
|
|
1304
|
+
}
|
|
1305
|
+
if (todoRec && todoRec.items) {
|
|
1306
|
+
todoRec.items.forEach(function (item) {
|
|
1307
|
+
if (item.status === "active") { item.status = allGood ? "done" : "failed"; }
|
|
1308
|
+
});
|
|
1309
|
+
rerenderTodoRecord(todoRec);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function handleStepQueueGenerateResult(data, goalPrompt) {
|
|
1314
|
+
hidePending();
|
|
1315
|
+
if (renderQuestionOrProse(data)) { return; }
|
|
1316
|
+
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1317
|
+
|
|
1318
|
+
// Build the todo plan. Parse "Plan:" from explanation if present;
|
|
1319
|
+
// fall back to an implicit single item from the goal prompt.
|
|
1320
|
+
var planItems = parseTodoPlan(data.explanation || "");
|
|
1321
|
+
if (!planItems.length) {
|
|
1322
|
+
planItems = [{ text: goalPrompt || "Generate flow", status: "pending" }];
|
|
1323
|
+
}
|
|
1324
|
+
// Same aggregate-verification reasoning as the Modify path: mark
|
|
1325
|
+
// every item active up front so a multi-item plan resolves together.
|
|
1326
|
+
planItems.forEach(function (item) { item.status = "active"; });
|
|
1327
|
+
var todoRec = addRecord("todo", { action: "generate", items: planItems });
|
|
1328
|
+
rerenderTodoRecord(todoRec);
|
|
1329
|
+
|
|
1330
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1331
|
+
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1332
|
+
// B1: when a build loop is appropriate, bake deploy-verify into the
|
|
1333
|
+
// primary chip (same as handleSimpleGenerationResult). The callback
|
|
1334
|
+
// also runs verifyImportedNodes so the todo record still gets checked
|
|
1335
|
+
// off. Without a build loop, fall back to a plain "Add to canvas"
|
|
1336
|
+
// button that still fires the verify callback.
|
|
1337
|
+
var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
1338
|
+
var _wantLoop = goalPrompt && !activeBuildLoop && _hasDeployable;
|
|
1339
|
+
var _onImported = _wantLoop
|
|
1340
|
+
? function (importResult) {
|
|
1341
|
+
verifyImportedNodes(importResult, todoRec);
|
|
1342
|
+
startBuildLoop(goalPrompt, flow, importResult);
|
|
1343
|
+
}
|
|
1344
|
+
: function (importResult) { verifyImportedNodes(importResult, todoRec); };
|
|
1345
|
+
addGeneratedReview(flow, _onImported, _wantLoop ? goalPrompt : null);
|
|
1346
|
+
// Suppress a server-suggested build chip when deploy-verify is already
|
|
1347
|
+
// the primary action inside the review panel.
|
|
1348
|
+
renderActionChip(_wantLoop && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
1349
|
+
? null : data.suggestedAction);
|
|
1350
|
+
setBusy(false);
|
|
1351
|
+
updateSelectionStatus();
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1058
1354
|
function generate() {
|
|
1059
|
-
|
|
1355
|
+
if (currentSettings.enableStepQueue) {
|
|
1356
|
+
runGenerateLikeAction("generate", "generate", "Generate: ", handleStepQueueGenerateResult);
|
|
1357
|
+
} else {
|
|
1358
|
+
runGenerateLikeAction("generate", "generate", "Generate: ", handleSimpleGenerationResult);
|
|
1359
|
+
}
|
|
1060
1360
|
}
|
|
1061
1361
|
|
|
1062
1362
|
// /build's first step. Reuses Generate's pipeline wholesale for the
|
|
@@ -1165,6 +1465,127 @@
|
|
|
1165
1465
|
updateSelectionStatus();
|
|
1166
1466
|
}
|
|
1167
1467
|
|
|
1468
|
+
// WS4: real consent gate for side-effecting build steps. Renders one
|
|
1469
|
+
// combined chip covering every side-effecting node this step's
|
|
1470
|
+
// classification found (classifyFlowNodes in flowpilot.js, server-side)
|
|
1471
|
+
// — a single decision rather than per-node chips, which is sufficient
|
|
1472
|
+
// because only the FIRST /flowpilot/build response carries
|
|
1473
|
+
// stepNodeClasses today (fix iterations via /flowpilot/modify don't, so
|
|
1474
|
+
// there's exactly one consent point per loop lifetime under the current
|
|
1475
|
+
// limitation — see the handleBuildResult call site).
|
|
1476
|
+
//
|
|
1477
|
+
// Reconstructed entirely from `src` (plain data, never a live closure)
|
|
1478
|
+
// on every call — the initial render from handleBuildResult AND every
|
|
1479
|
+
// later rerender (refresh, pop-out reopen, the W0A idle/focus
|
|
1480
|
+
// auto-refresh) via rerenderRecord's buildConsentGate branch below.
|
|
1481
|
+
// Mirrors rerenderReviewRecord's pattern in apply-review.js: a fresh
|
|
1482
|
+
// record is added each time from the source's stored fields (including
|
|
1483
|
+
// `decision`, once made), rather than relying on an in-memory callback
|
|
1484
|
+
// surviving a refresh. That in-memory-callback version is exactly what
|
|
1485
|
+
// broke before this fix — a refresh mid-decision fell through to
|
|
1486
|
+
// renderClarifyingQuestion's generic path, whose buttons send the
|
|
1487
|
+
// clicked label as a new chat message instead of resolving Proceed/Skip,
|
|
1488
|
+
// permanently stranding the loop.
|
|
1489
|
+
//
|
|
1490
|
+
// src fields: sideEffecting, flow, goalPrompt, fpUidManifest,
|
|
1491
|
+
// suggestedAction — everything runBuildConsentDecision needs — plus,
|
|
1492
|
+
// once resolved, `decision` ("proceed"|"skip") so a later rerender shows
|
|
1493
|
+
// a settled state instead of re-offering an already-made choice.
|
|
1494
|
+
function renderBuildConsentGate(src) {
|
|
1495
|
+
var $box = el("#fp-messages");
|
|
1496
|
+
if (!$box.length) {
|
|
1497
|
+
if (!src.decision) { runBuildConsentDecision(src, true); }
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
var _rec = addRecord("question", {
|
|
1502
|
+
buildConsentGate: true,
|
|
1503
|
+
options: ["Auto-verify", "I'll check myself"],
|
|
1504
|
+
sideEffecting: src.sideEffecting,
|
|
1505
|
+
flow: src.flow,
|
|
1506
|
+
goalPrompt: src.goalPrompt,
|
|
1507
|
+
fpUidManifest: src.fpUidManifest,
|
|
1508
|
+
suggestedAction: src.suggestedAction,
|
|
1509
|
+
decision: src.decision
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
var sideEffecting = Array.isArray(_rec.sideEffecting) ? _rec.sideEffecting : [];
|
|
1513
|
+
var labels = sideEffecting.map(function (n) { return n.name || n.type; }).join(", ");
|
|
1514
|
+
|
|
1515
|
+
if (_rec.decision) {
|
|
1516
|
+
// Already resolved before this render (e.g. resolved earlier in
|
|
1517
|
+
// the session, now showing again after a refresh) — settled
|
|
1518
|
+
// state, not an interactive choice.
|
|
1519
|
+
addMessage("assistant", "This step calls an external service: " + labels + ".");
|
|
1520
|
+
var $settledRow = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
1521
|
+
$("<button>")
|
|
1522
|
+
.addClass("fp-consent-chip")
|
|
1523
|
+
.addClass(_rec.decision === "proceed" ? "fp-consent-chip-primary" : "fp-consent-chip-alt")
|
|
1524
|
+
.attr("type", "button").prop("disabled", true)
|
|
1525
|
+
.text(_rec.decision === "proceed" ? "Auto-verify ✓" : "Checking myself ✓")
|
|
1526
|
+
.appendTo($settledRow);
|
|
1527
|
+
$box.append($settledRow);
|
|
1528
|
+
scrollMessagesToBottom();
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
addMessage("assistant", "This step calls an external service: " + labels +
|
|
1533
|
+
". Want it verified automatically once triggered, or would you rather check it yourself?");
|
|
1534
|
+
|
|
1535
|
+
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
1536
|
+
|
|
1537
|
+
function decide(proceed) {
|
|
1538
|
+
$row.find("button").prop("disabled", true);
|
|
1539
|
+
_rec.decision = proceed ? "proceed" : "skip";
|
|
1540
|
+
runBuildConsentDecision(_rec, proceed);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
$("<button>")
|
|
1544
|
+
.addClass("fp-consent-chip fp-consent-chip-primary")
|
|
1545
|
+
.attr("type", "button")
|
|
1546
|
+
.text("Auto-verify")
|
|
1547
|
+
.on("click", function () { decide(true); })
|
|
1548
|
+
.appendTo($row);
|
|
1549
|
+
$("<button>")
|
|
1550
|
+
.addClass("fp-consent-chip fp-consent-chip-alt")
|
|
1551
|
+
.attr("type", "button")
|
|
1552
|
+
.text("I'll check myself")
|
|
1553
|
+
.on("click", function () { decide(false); })
|
|
1554
|
+
.appendTo($row);
|
|
1555
|
+
|
|
1556
|
+
$box.append($row);
|
|
1557
|
+
scrollMessagesToBottom();
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// The actual "proceed with review + loop" action, factored out so both
|
|
1561
|
+
// the fresh (no side-effecting nodes) and gated (decision made) paths in
|
|
1562
|
+
// handleBuildResult, and a rerendered consent-gate record's decide(),
|
|
1563
|
+
// all run identical logic sourced from plain data — never a captured
|
|
1564
|
+
// closure. consentGranted=false builds the Skip consent object
|
|
1565
|
+
// (skippedNodeIds/fpUidManifest) that startBuildLoop resolves into real
|
|
1566
|
+
// ids via importResult.nodeMap — see startBuildLoop's own comment.
|
|
1567
|
+
function runBuildConsentDecision(src, consentGranted) {
|
|
1568
|
+
var sideEffecting = Array.isArray(src.sideEffecting) ? src.sideEffecting : [];
|
|
1569
|
+
var flow = src.flow;
|
|
1570
|
+
var goalPrompt = src.goalPrompt;
|
|
1571
|
+
var fpUidManifest = Array.isArray(src.fpUidManifest) ? src.fpUidManifest : [];
|
|
1572
|
+
|
|
1573
|
+
if (sideEffecting.length > 0) {
|
|
1574
|
+
var sideLabels = sideEffecting.map(function (n) { return n.name || n.type; }).join(", ");
|
|
1575
|
+
addMessage("fp-notice", consentGranted
|
|
1576
|
+
? "⚠ External calls: " + sideLabels + " — the deploy-test loop will auto-verify these once triggered."
|
|
1577
|
+
: "⚠ External calls: " + sideLabels + " — auto-verify skipped for these node(s); confirm the result yourself.");
|
|
1578
|
+
}
|
|
1579
|
+
var consent = consentGranted ? null : {
|
|
1580
|
+
skippedNodeIds: sideEffecting.map(function (n) { return n.id; }),
|
|
1581
|
+
fpUidManifest: fpUidManifest
|
|
1582
|
+
};
|
|
1583
|
+
addGeneratedReview(flow, function (importResult) {
|
|
1584
|
+
startBuildLoop(goalPrompt, flow, importResult, consent);
|
|
1585
|
+
}, goalPrompt);
|
|
1586
|
+
renderActionChip(src.suggestedAction);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1168
1589
|
function handleBuildResult(data, goalPrompt) {
|
|
1169
1590
|
hidePending();
|
|
1170
1591
|
if (renderQuestionOrProse(data)) { return; }
|
|
@@ -1173,8 +1594,24 @@
|
|
|
1173
1594
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1174
1595
|
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1175
1596
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1176
|
-
|
|
1177
|
-
|
|
1597
|
+
|
|
1598
|
+
var nodeClasses = data.stepNodeClasses;
|
|
1599
|
+
var sideEffecting = (nodeClasses && Array.isArray(nodeClasses.sideEffecting))
|
|
1600
|
+
? nodeClasses.sideEffecting : [];
|
|
1601
|
+
var fpUidManifest = Array.isArray(data.fpUidManifest) ? data.fpUidManifest : [];
|
|
1602
|
+
var consentSrc = {
|
|
1603
|
+
sideEffecting: sideEffecting,
|
|
1604
|
+
flow: flow,
|
|
1605
|
+
goalPrompt: goalPrompt,
|
|
1606
|
+
fpUidManifest: fpUidManifest,
|
|
1607
|
+
suggestedAction: data.suggestedAction
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
if (sideEffecting.length > 0) {
|
|
1611
|
+
renderBuildConsentGate(consentSrc);
|
|
1612
|
+
} else {
|
|
1613
|
+
runBuildConsentDecision(consentSrc, true);
|
|
1614
|
+
}
|
|
1178
1615
|
setBusy(false);
|
|
1179
1616
|
updateSelectionStatus();
|
|
1180
1617
|
}
|
|
@@ -1265,6 +1702,8 @@
|
|
|
1265
1702
|
addMessage("error", "Describe what you want to change.");
|
|
1266
1703
|
return;
|
|
1267
1704
|
}
|
|
1705
|
+
var existingNodeIds = context.nodes.map(function (n) { return n.id; });
|
|
1706
|
+
|
|
1268
1707
|
var label = "Modify: " + instruction + contextAttachmentNote(context);
|
|
1269
1708
|
addMessage("user", label);
|
|
1270
1709
|
// Snapshot history before pushing this turn (see send()).
|
|
@@ -1289,6 +1728,10 @@
|
|
|
1289
1728
|
handleExecuteError(msg, raw);
|
|
1290
1729
|
}
|
|
1291
1730
|
|
|
1731
|
+
function onModifyResult(data) {
|
|
1732
|
+
handleModifyResult(data);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1292
1735
|
// Explore-then-propose, same as generate(). The
|
|
1293
1736
|
// model may call read tools (e.g. to re-check the selected node's
|
|
1294
1737
|
// current config) before producing the modify envelope; the final
|
|
@@ -1296,7 +1739,7 @@
|
|
|
1296
1739
|
if (isAgentLoop) {
|
|
1297
1740
|
runAgentLoop("flowpilot/modify", payload,
|
|
1298
1741
|
{ mode: "modify", context: context, prompt: instruction },
|
|
1299
|
-
|
|
1742
|
+
onModifyResult, onModifyError);
|
|
1300
1743
|
return;
|
|
1301
1744
|
}
|
|
1302
1745
|
|
|
@@ -1304,11 +1747,11 @@
|
|
|
1304
1747
|
// generate() for details.
|
|
1305
1748
|
if (currentSettings.streamingEnabled) {
|
|
1306
1749
|
payload.stream = true;
|
|
1307
|
-
sendExecuteStream("modify", payload,
|
|
1750
|
+
sendExecuteStream("modify", payload, onModifyResult);
|
|
1308
1751
|
return;
|
|
1309
1752
|
}
|
|
1310
1753
|
|
|
1311
|
-
ajaxJson("POST", "flowpilot/modify", payload,
|
|
1754
|
+
ajaxJson("POST", "flowpilot/modify", payload, onModifyResult, onModifyError);
|
|
1312
1755
|
}
|
|
1313
1756
|
|
|
1314
1757
|
// Render generated flow JSON in a preformatted, copyable block. Used for
|
|
@@ -1357,6 +1800,10 @@
|
|
|
1357
1800
|
// the review — see onDebugMessage for why (a forked/split flow can
|
|
1358
1801
|
// fire its debug node more than once per trigger).
|
|
1359
1802
|
var BUILD_LOOP_ATTACH_DEBOUNCE_MS = 1200;
|
|
1803
|
+
// W0.3: how many times the model can bail (emit a prose reply with a
|
|
1804
|
+
// suggestedAction mode-redirect) before the loop gives up with an
|
|
1805
|
+
// honest-timeout instead of silently treating the bail as success.
|
|
1806
|
+
var BUILD_LOOP_MAX_BAILS = 2;
|
|
1360
1807
|
var buildLoopAttachTimer = null;
|
|
1361
1808
|
// Fires when "attach" waits too long with no debug — surfaces a prompt
|
|
1362
1809
|
// for flows that don't produce automatic debug output (HTTP endpoints, etc).
|
|
@@ -1410,6 +1857,55 @@
|
|
|
1410
1857
|
scrollMessagesToBottom();
|
|
1411
1858
|
}
|
|
1412
1859
|
|
|
1860
|
+
// WS3: remove FP-UID checkpoint tap nodes that the build prompt placed
|
|
1861
|
+
// on the canvas as FlowPilot scaffolding. These are debug nodes named
|
|
1862
|
+
// FP-UID001, FP-UID002, etc. — wired in parallel to external-call nodes
|
|
1863
|
+
// so the loop can attribute debug messages to specific checkpoints.
|
|
1864
|
+
// Called at loop end (any outcome) to clean up before returning control
|
|
1865
|
+
// to the user. Replicates the remove-and-history pattern from
|
|
1866
|
+
// applyModifications in apply-review.js (confirmed group-cleanup bookkeeping).
|
|
1867
|
+
function removeFpUidTaps(loop) {
|
|
1868
|
+
if (!loop || !Array.isArray(loop.nodeIds) || !loop.nodeIds.length) { return 0; }
|
|
1869
|
+
var FP_UID_RE = /^FP-UID\d+$/;
|
|
1870
|
+
var removed = 0;
|
|
1871
|
+
loop.nodeIds.forEach(function (id) {
|
|
1872
|
+
var liveNode = RED.nodes.node(id);
|
|
1873
|
+
if (!liveNode || !FP_UID_RE.test(liveNode.name)) { return; }
|
|
1874
|
+
var connectedLinks = [];
|
|
1875
|
+
RED.nodes.eachLink(function (l) {
|
|
1876
|
+
if ((l.source && l.source.id === id) || (l.target && l.target.id === id)) {
|
|
1877
|
+
connectedLinks.push(l);
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
try { RED.nodes.remove(liveNode.id); } catch (e) { return; }
|
|
1881
|
+
if (liveNode.g && RED.nodes.group) {
|
|
1882
|
+
var ownerGroup = RED.nodes.group(liveNode.g);
|
|
1883
|
+
if (ownerGroup) {
|
|
1884
|
+
var idx = ownerGroup.nodes.indexOf(liveNode);
|
|
1885
|
+
if (idx !== -1) { ownerGroup.nodes.splice(idx, 1); }
|
|
1886
|
+
RED.group.markDirty(ownerGroup);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
RED.history.push({
|
|
1890
|
+
t: "delete",
|
|
1891
|
+
nodes: [liveNode],
|
|
1892
|
+
links: connectedLinks,
|
|
1893
|
+
groups: [],
|
|
1894
|
+
junctions: [],
|
|
1895
|
+
subflow: { id: undefined, instances: [] },
|
|
1896
|
+
subflowInputs: [],
|
|
1897
|
+
subflowOutputs: [],
|
|
1898
|
+
dirty: RED.nodes.dirty()
|
|
1899
|
+
});
|
|
1900
|
+
removed++;
|
|
1901
|
+
});
|
|
1902
|
+
if (removed) {
|
|
1903
|
+
RED.nodes.dirty(true);
|
|
1904
|
+
RED.view.redraw(true);
|
|
1905
|
+
}
|
|
1906
|
+
return removed;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1413
1909
|
// The single exit point for every way a build loop ends — Touchdown,
|
|
1414
1910
|
// the cap being reached, pausing on a clarifying question, or the user
|
|
1415
1911
|
// clicking Stop. Releases Build mode and its pinned selection too: once
|
|
@@ -1420,6 +1916,7 @@
|
|
|
1420
1916
|
// visible as a completion badge. success=false (default): remove the stepper
|
|
1421
1917
|
// (user stop, cap reached, paused for question).
|
|
1422
1918
|
function stopBuildLoop(note, success) {
|
|
1919
|
+
var tapCount = activeBuildLoop ? removeFpUidTaps(activeBuildLoop) : 0;
|
|
1423
1920
|
if (success && activeBuildLoop) {
|
|
1424
1921
|
activeBuildLoop.waypoint = "done";
|
|
1425
1922
|
renderLoopStepper(activeBuildLoop);
|
|
@@ -1430,6 +1927,7 @@
|
|
|
1430
1927
|
if (!success) { el("#fp-loop-stepper").remove(); }
|
|
1431
1928
|
disarmExecuteAction();
|
|
1432
1929
|
if (note) { addMessage("assistant", note); }
|
|
1930
|
+
if (tapCount) { addMessage("assistant", "Removed " + tapCount + " FP-UID checkpoint tap(s) from the canvas."); }
|
|
1433
1931
|
}
|
|
1434
1932
|
|
|
1435
1933
|
// Applies a build-loop review's fix envelope, then keeps the loop's
|
|
@@ -1492,7 +1990,21 @@
|
|
|
1492
1990
|
if (loop.waypoint === "apply") {
|
|
1493
1991
|
hint = "Click the canvas to place the new node(s), then Deploy — I'll move on automatically once you deploy.";
|
|
1494
1992
|
} else if (loop.waypoint === "attach") {
|
|
1495
|
-
|
|
1993
|
+
var eps = loop.httpEndpoints;
|
|
1994
|
+
if (eps && eps.length > 0) {
|
|
1995
|
+
var ep = eps[0];
|
|
1996
|
+
var baseUrl = (typeof window !== "undefined" && window.location)
|
|
1997
|
+
? window.location.origin : "";
|
|
1998
|
+
var curlMethod = ep.method === "GET" ? "" : " -X " + ep.method;
|
|
1999
|
+
hint = "Send " + ep.method + " " + ep.url + " to trigger the flow — " +
|
|
2000
|
+
"e.g. curl" + curlMethod + " " + baseUrl + ep.url +
|
|
2001
|
+
". I’ll attach the debug output automatically.";
|
|
2002
|
+
if (eps.length > 1) {
|
|
2003
|
+
hint += " (" + (eps.length - 1) + " more endpoint(s) in this flow.)";
|
|
2004
|
+
}
|
|
2005
|
+
} else {
|
|
2006
|
+
hint = "Trigger the flow, then check the Debug sidebar — I'll attach the next debug message automatically.";
|
|
2007
|
+
}
|
|
1496
2008
|
} else if (loop.waypoint === "review") {
|
|
1497
2009
|
hint = "Debug output attached — reviewing against the goal…";
|
|
1498
2010
|
}
|
|
@@ -1513,7 +2025,8 @@
|
|
|
1513
2025
|
iteration: loop.iteration,
|
|
1514
2026
|
maxIterations: loop.maxIterations,
|
|
1515
2027
|
goal: loop.goal,
|
|
1516
|
-
nodeIds: Array.isArray(loop.nodeIds) ? loop.nodeIds.slice() : []
|
|
2028
|
+
nodeIds: Array.isArray(loop.nodeIds) ? loop.nodeIds.slice() : [],
|
|
2029
|
+
httpEndpoints: Array.isArray(loop.httpEndpoints) ? loop.httpEndpoints.slice() : []
|
|
1517
2030
|
});
|
|
1518
2031
|
|
|
1519
2032
|
$box.append($msg);
|
|
@@ -1526,7 +2039,8 @@
|
|
|
1526
2039
|
iteration: rec.iteration || 1,
|
|
1527
2040
|
maxIterations: rec.maxIterations || 5,
|
|
1528
2041
|
goal: rec.goal || "",
|
|
1529
|
-
nodeIds: Array.isArray(rec.nodeIds) ? rec.nodeIds : []
|
|
2042
|
+
nodeIds: Array.isArray(rec.nodeIds) ? rec.nodeIds : [],
|
|
2043
|
+
httpEndpoints: Array.isArray(rec.httpEndpoints) ? rec.httpEndpoints : []
|
|
1530
2044
|
});
|
|
1531
2045
|
}
|
|
1532
2046
|
|
|
@@ -1542,12 +2056,22 @@
|
|
|
1542
2056
|
// end up on the canvas — importResult.nodeMap maps each placeholder id
|
|
1543
2057
|
// to the real live node object, which is the only way later review/fix
|
|
1544
2058
|
// requests can target the right nodes via collectSelectionContext.
|
|
1545
|
-
|
|
2059
|
+
//
|
|
2060
|
+
// consent (WS4, optional): { skippedNodeIds, fpUidManifest } from a
|
|
2061
|
+
// Skip decision at the build consent gate (see handleBuildResult /
|
|
2062
|
+
// renderBuildConsentGate) — skippedNodeIds are the side-effecting
|
|
2063
|
+
// nodes' own PLACEHOLDER ids, fpUidManifest maps each FP-UID debug tap's
|
|
2064
|
+
// placeholder id to the placeholder id of the node it's wired from
|
|
2065
|
+
// (wiredFrom). Resolved here into two REAL-id sets: the skipped nodes
|
|
2066
|
+
// themselves (onNodeStatus's status/<nodeId> path checks against these)
|
|
2067
|
+
// and the taps wired to them (onDebugMessage's msg.id check does) — so
|
|
2068
|
+
// both auto-verify evidence paths honor the same Skip decision.
|
|
2069
|
+
function startBuildLoop(goal, nodeIdsOrNodes, importResult, consent) {
|
|
1546
2070
|
var nodeIds = [];
|
|
2071
|
+
var nodeMap = importResult && importResult.nodeMap;
|
|
1547
2072
|
if (importResult) {
|
|
1548
2073
|
// Fresh build: map placeholder ids from the proposal to the real
|
|
1549
2074
|
// ids importNodes assigned on the canvas.
|
|
1550
|
-
var nodeMap = importResult.nodeMap;
|
|
1551
2075
|
if (nodeMap && Array.isArray(nodeIdsOrNodes)) {
|
|
1552
2076
|
nodeIdsOrNodes.forEach(function (n) {
|
|
1553
2077
|
var real = n && n.id && nodeMap[n.id];
|
|
@@ -1558,13 +2082,43 @@
|
|
|
1558
2082
|
// Existing-flow build: ids are already resolved real canvas ids.
|
|
1559
2083
|
nodeIds = nodeIdsOrNodes.filter(function (id) { return typeof id === "string" && id; });
|
|
1560
2084
|
}
|
|
2085
|
+
// Detect HTTP-in endpoints so the "attach" step can show a specific
|
|
2086
|
+
// trigger hint instead of the generic "trigger the flow" message.
|
|
2087
|
+
var httpEndpoints = [];
|
|
2088
|
+
nodeIds.forEach(function (id) {
|
|
2089
|
+
var n = RED.nodes.node(id);
|
|
2090
|
+
if (n && n.type === "http in" && n.url) {
|
|
2091
|
+
httpEndpoints.push({ method: (n.method || "get").toUpperCase(), url: n.url });
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
var skipCheckpointNodeIds = [];
|
|
2096
|
+
var skipCheckpointTapIds = [];
|
|
2097
|
+
var skippedPlaceholderIds = consent && Array.isArray(consent.skippedNodeIds) ? consent.skippedNodeIds : [];
|
|
2098
|
+
if (skippedPlaceholderIds.length && nodeMap) {
|
|
2099
|
+
skippedPlaceholderIds.forEach(function (placeholderId) {
|
|
2100
|
+
var real = nodeMap[placeholderId];
|
|
2101
|
+
if (real && real.id) { skipCheckpointNodeIds.push(real.id); }
|
|
2102
|
+
});
|
|
2103
|
+
var manifest = Array.isArray(consent.fpUidManifest) ? consent.fpUidManifest : [];
|
|
2104
|
+
manifest.forEach(function (tap) {
|
|
2105
|
+
if (!tap || skippedPlaceholderIds.indexOf(tap.wiredFrom) === -1) { return; }
|
|
2106
|
+
var realTap = nodeMap[tap.id];
|
|
2107
|
+
if (realTap && realTap.id) { skipCheckpointTapIds.push(realTap.id); }
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1561
2111
|
activeBuildLoop = {
|
|
1562
2112
|
goal: goal,
|
|
1563
2113
|
nodeIds: nodeIds,
|
|
1564
2114
|
iteration: 1,
|
|
1565
2115
|
maxIterations: getAgentLoopMaxIterations(),
|
|
1566
2116
|
waypoint: "apply",
|
|
1567
|
-
conversationId: conversationId
|
|
2117
|
+
conversationId: conversationId,
|
|
2118
|
+
bailCount: 0,
|
|
2119
|
+
httpEndpoints: httpEndpoints,
|
|
2120
|
+
skipCheckpointNodeIds: skipCheckpointNodeIds,
|
|
2121
|
+
skipCheckpointTapIds: skipCheckpointTapIds
|
|
1568
2122
|
};
|
|
1569
2123
|
renderLoopStepper(activeBuildLoop);
|
|
1570
2124
|
}
|
|
@@ -1580,7 +2134,47 @@
|
|
|
1580
2134
|
function runBuildReview(loop) {
|
|
1581
2135
|
var context = collectSelectionContext(loop.nodeIds);
|
|
1582
2136
|
context = attachDebugContext(context);
|
|
1583
|
-
var
|
|
2137
|
+
var reviewEvidence = context && Array.isArray(context.debugMessages)
|
|
2138
|
+
? context.debugMessages : [];
|
|
2139
|
+
var statusOnlyEvidence = reviewEvidence.length > 0 &&
|
|
2140
|
+
reviewEvidence.every(function (entry) {
|
|
2141
|
+
return entry && entry.sourceKind === "status";
|
|
2142
|
+
});
|
|
2143
|
+
// W0.3: framing block — suppresses the Modify escape hatch
|
|
2144
|
+
// (suggestedAction mode-redirect) inside the build loop context.
|
|
2145
|
+
// The code-side handler (handleBuildReviewResult) also detects and
|
|
2146
|
+
// counts bail attempts so N bails trigger an honest-timeout instead
|
|
2147
|
+
// of silently treating a redirect as success.
|
|
2148
|
+
var instruction = "CONTEXT: You are the fix engine inside a build-test-fix loop. " +
|
|
2149
|
+
"Your only valid responses are: (1) plain text when the goal is fully " +
|
|
2150
|
+
"satisfied, or (2) a {\"explanation\", \"changes\", ...} fix envelope " +
|
|
2151
|
+
"when something needs patching." +
|
|
2152
|
+
(statusOnlyEvidence
|
|
2153
|
+
? " (3) Because the attached evidence contains ONLY coarse node-status " +
|
|
2154
|
+
"lines, you may instead return the atomic {\"question\", " +
|
|
2155
|
+
"\"questionOptions\"} envelope described below."
|
|
2156
|
+
: "") +
|
|
2157
|
+
" Do NOT use the <<<FLOWPILOT_DATA>>> " +
|
|
2158
|
+
"block or suggest switching to chat/generate/document — you are already " +
|
|
2159
|
+
"in the right context and any mode-redirect will be ignored. If you are " +
|
|
2160
|
+
"genuinely uncertain what to fix, " +
|
|
2161
|
+
(statusOnlyEvidence
|
|
2162
|
+
? "use the status-only confirmation question below."
|
|
2163
|
+
: "describe the uncertainty inside \"explanation\" in a fix envelope.") +
|
|
2164
|
+
"\n\nEach attached evidence object has sourceKind. sourceKind:\"debug\" " +
|
|
2165
|
+
"is real message content emitted by a debug node. sourceKind:\"status\" " +
|
|
2166
|
+
"is only a coarse connection/status line synthesized from node status; " +
|
|
2167
|
+
"never treat it as proof of message payload content or successful " +
|
|
2168
|
+
"end-to-end behavior. " +
|
|
2169
|
+
(statusOnlyEvidence
|
|
2170
|
+
? "STATUS-ONLY FALLBACK: if the coarse status does not prove whether " +
|
|
2171
|
+
"the deployed node is actually connected/working, do not guess or " +
|
|
2172
|
+
"assert failure. Ask one concrete yes/no confirmation such as " +
|
|
2173
|
+
"\"Does the node show connected after deploy?\" by returning ONLY " +
|
|
2174
|
+
"{\"question\":\"...\",\"questionOptions\":[\"Yes\",\"No\"]}. "
|
|
2175
|
+
: "") +
|
|
2176
|
+
"\n\n" +
|
|
2177
|
+
"Review the attached debug output against this build goal: \"" +
|
|
1584
2178
|
loop.goal + "\". Before concluding anything, list out every distinct " +
|
|
1585
2179
|
"piece of data or behavior the goal actually requires, then check the " +
|
|
1586
2180
|
"attached debug payload(s) contain EACH one — a payload that's merely " +
|
|
@@ -1588,7 +2182,22 @@
|
|
|
1588
2182
|
"goal asked to combine two things but the payload only shows one), " +
|
|
1589
2183
|
"does NOT fully satisfy it. If more than one debug message is " +
|
|
1590
2184
|
"attached, treat them together as the full picture from one trigger, " +
|
|
1591
|
-
"not as separate independent attempts.
|
|
2185
|
+
"not as separate independent attempts. " +
|
|
2186
|
+
"SPECIAL CASE — network errors: if the debug output shows ONLY a " +
|
|
2187
|
+
"network-level error (EHOSTUNREACH, ECONNREFUSED, ETIMEDOUT, " +
|
|
2188
|
+
"ENOTFOUND, getaddrinfo ENOTFOUND, EAI_AGAIN, EAI_NODATA), the " +
|
|
2189
|
+
"flow MIGHT be correctly built — BUT you MUST first check the node " +
|
|
2190
|
+
"context: if any http-request node has an empty url field, a " +
|
|
2191
|
+
"placeholder, or a clearly malformed url (no hostname, no protocol, " +
|
|
2192
|
+
"etc.), the DNS or connection error is a CONFIGURATION problem — " +
|
|
2193
|
+
"fix the url field, do NOT declare it an infrastructure issue. Only " +
|
|
2194
|
+
"apply this special case when the url is a real, non-empty, " +
|
|
2195
|
+
"well-formed URL and the external service is simply unreachable. In " +
|
|
2196
|
+
"that case reply in plain text acknowledging the flow is structurally " +
|
|
2197
|
+
"correct and the network error is an infrastructure issue outside the " +
|
|
2198
|
+
"flow. Do NOT propose any changes; this error cannot be resolved by " +
|
|
2199
|
+
"modifying the flow. " +
|
|
2200
|
+
"If it fully satisfies the goal, " +
|
|
1592
2201
|
"say so in plain text — no changes needed. If something's wrong " +
|
|
1593
2202
|
"(including a node that never fired, or a value that's missing/empty " +
|
|
1594
2203
|
"when the goal needed it), propose the fix directly as a patch in " +
|
|
@@ -1644,6 +2253,26 @@
|
|
|
1644
2253
|
// diff-then-Apply pipeline as a manual Modify, then the loop advances
|
|
1645
2254
|
// back to "apply" for the next deploy/test cycle, or stops if the
|
|
1646
2255
|
// iteration cap is reached).
|
|
2256
|
+
// Returns false when all of data's proposed changes are sentinel-echoed
|
|
2257
|
+
// with no insertions, removals, or wire changes. Used by
|
|
2258
|
+
// handleBuildReviewResult to avoid showing an all-blocked review panel
|
|
2259
|
+
// when the model said "no changes needed" but still emitted a modify
|
|
2260
|
+
// envelope (a common model behavior after a build-loop review).
|
|
2261
|
+
function reviewHasRealDiffs(data) {
|
|
2262
|
+
if ((data.newNodes && data.newNodes.length) ||
|
|
2263
|
+
(data.removeNodes && data.removeNodes.length) ||
|
|
2264
|
+
(data.newWires && data.newWires.length) ||
|
|
2265
|
+
(data.newGroups && data.newGroups.length)) { return true; }
|
|
2266
|
+
var nodes = Array.isArray(data.flow) ? data.flow : [];
|
|
2267
|
+
return nodes.some(function (modNode) {
|
|
2268
|
+
if (!modNode || !modNode.id) { return false; }
|
|
2269
|
+
var liveNode = findLiveNode(modNode.id);
|
|
2270
|
+
if (!liveNode) { return false; }
|
|
2271
|
+
var diff = computeNodeDiff(liveNode, modNode);
|
|
2272
|
+
return diff.propertyChanges.length > 0 || diff.wiresChanged;
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
|
|
1647
2276
|
function handleBuildReviewResult(data) {
|
|
1648
2277
|
hidePending();
|
|
1649
2278
|
var loop = activeBuildLoop;
|
|
@@ -1667,6 +2296,51 @@
|
|
|
1667
2296
|
}
|
|
1668
2297
|
|
|
1669
2298
|
if (data.prose) {
|
|
2299
|
+
var explanation = data.explanation || "(no content returned)";
|
|
2300
|
+
|
|
2301
|
+
// W0.3: bail detection — a prose reply with a mode-redirect
|
|
2302
|
+
// suggestedAction means the model tried to exit the loop
|
|
2303
|
+
// context via the Modify escape hatch. Count it and retry or
|
|
2304
|
+
// honest-timeout rather than treating it as success.
|
|
2305
|
+
var sa = data.suggestedAction;
|
|
2306
|
+
var isBail = sa && (sa.mode === "chat" || sa.mode === "generate" || sa.mode === "document");
|
|
2307
|
+
if (isBail) {
|
|
2308
|
+
loop.bailCount = (loop.bailCount || 0) + 1;
|
|
2309
|
+
console.warn("[FlowPilot] build-loop bail #" + loop.bailCount +
|
|
2310
|
+
" mode=" + sa.mode + ": " + explanation);
|
|
2311
|
+
addMessage("assistant", explanation);
|
|
2312
|
+
pushHistory("assistant", explanation);
|
|
2313
|
+
if (loop.bailCount >= BUILD_LOOP_MAX_BAILS) {
|
|
2314
|
+
stopBuildLoop("Build loop could not assess the debug output — the AI kept redirecting instead of reviewing. Try attaching more debug context or continuing manually with Modify.", false);
|
|
2315
|
+
setBusy(false);
|
|
2316
|
+
updateSelectionStatus();
|
|
2317
|
+
} else {
|
|
2318
|
+
addMessage("fp-notice", "Build-loop: review redirected to " + sa.mode +
|
|
2319
|
+
" — staying in build context and retrying (bail " + loop.bailCount +
|
|
2320
|
+
"/" + BUILD_LOOP_MAX_BAILS + ").");
|
|
2321
|
+
runBuildReview(loop);
|
|
2322
|
+
}
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
addMessage("assistant", explanation);
|
|
2327
|
+
pushHistory("assistant", explanation);
|
|
2328
|
+
renderActionChip(data.suggestedAction);
|
|
2329
|
+
var stopMsg = /EHOSTUNREACH|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EAI_NODATA|getaddrinfo|unreachable|infrastructure/i
|
|
2330
|
+
.test(explanation)
|
|
2331
|
+
? "Build complete — the flow is correctly structured, but the external endpoint was unreachable during testing (infrastructure issue, not a flow problem)."
|
|
2332
|
+
: "Touchdown — the debug output matches the goal.";
|
|
2333
|
+
stopBuildLoop(stopMsg, true);
|
|
2334
|
+
setBusy(false);
|
|
2335
|
+
updateSelectionStatus();
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// Modify envelope where every proposed change is a sentinel echo —
|
|
2340
|
+
// the model emitted a changes object but all fields are redacted
|
|
2341
|
+
// placeholders with no insertions, removals, or wire changes. Treat
|
|
2342
|
+
// it as "no changes needed" rather than showing an all-blocked panel.
|
|
2343
|
+
if (!reviewHasRealDiffs(data)) {
|
|
1670
2344
|
addMessage("assistant", data.explanation || "(no content returned)");
|
|
1671
2345
|
pushHistory("assistant", data.explanation || "");
|
|
1672
2346
|
renderActionChip(data.suggestedAction);
|