@manny-est/node-red-flowpilot 0.6.0-beta.1 → 0.6.1
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 +119 -13
- package/README.md +10 -1
- package/flowpilot-core.css +15 -0
- package/flowpilot-node-entry.js +15 -0
- package/flowpilot.js +296 -63
- package/lib/agent-contract.js +8 -3
- package/lib/core/history.js +151 -9
- package/lib/core/init.js +90 -21
- package/lib/core/main.js +120 -26
- package/lib/core/modes.js +237 -21
- package/lib/core/selection-context.js +17 -0
- package/lib/default-system-prompt.js +2 -2
- package/lib/document-system-prompt.js +4 -2
- package/lib/generation-system-prompt.js +2 -2
- package/lib/modify-system-prompt.js +1 -1
- package/lib/prompt-fragments.js +10 -6
- package/lib/provider-anthropic.js +12 -2
- package/lib/provider-openai-compatible.js +14 -2
- package/lib/provider-shape-check.js +1 -1
- package/lib/storage.js +6 -0
- package/package.json +3 -2
package/lib/core/modes.js
CHANGED
|
@@ -273,6 +273,65 @@
|
|
|
273
273
|
dispatch();
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function summarizeConversationHistory() {
|
|
277
|
+
if (isPopoutContext) {
|
|
278
|
+
addMessage("assistant", "`/summarize` only runs in the main FlowPilot panel.");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
var totalMessages = conversationHistory.length;
|
|
283
|
+
if (totalMessages <= 4) {
|
|
284
|
+
addMessage("assistant", "Nothing much to summarize yet — only " + totalMessages +
|
|
285
|
+
" message(s) so far.");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
var recentTail = conversationHistory.slice(-4);
|
|
290
|
+
var olderSlice = conversationHistory.slice(0, -4);
|
|
291
|
+
var beforeTokens = estimateTokens(olderSlice);
|
|
292
|
+
var prompt = "Summarize the earlier conversation history provided here. " +
|
|
293
|
+
"Keep it concise and factual. Preserve decisions, constraints, important context, " +
|
|
294
|
+
"and unresolved questions. Return plain text only.";
|
|
295
|
+
var payload = {
|
|
296
|
+
prompt: prompt,
|
|
297
|
+
context: null,
|
|
298
|
+
history: olderSlice.slice(),
|
|
299
|
+
historyTruncated: false,
|
|
300
|
+
conversationId: conversationId,
|
|
301
|
+
strategy: "classic",
|
|
302
|
+
entry: "chat"
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
setBusy(true);
|
|
306
|
+
showPending(false);
|
|
307
|
+
|
|
308
|
+
ajaxJson("POST", "flowpilot/chat", payload, function (data) {
|
|
309
|
+
hidePending();
|
|
310
|
+
var summaryText = String(data && data.message ? data.message : "").trim();
|
|
311
|
+
if (!summaryText) {
|
|
312
|
+
addMessage("error", "Summarize failed: no summary text returned.");
|
|
313
|
+
setBusy(false);
|
|
314
|
+
updateSelectionStatus();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
var summaryMessage = {
|
|
319
|
+
role: "assistant",
|
|
320
|
+
content: "[Summary of earlier conversation]\n" + summaryText
|
|
321
|
+
};
|
|
322
|
+
conversationHistory = [summaryMessage].concat(recentTail);
|
|
323
|
+
addMessage("assistant", "Compacted " + olderSlice.length + " earlier message(s) into a summary (~" +
|
|
324
|
+
beforeTokens.toLocaleString() + " → ~" + estimateTokens(summaryMessage).toLocaleString() + " tokens).");
|
|
325
|
+
setBusy(false);
|
|
326
|
+
updateSelectionStatus();
|
|
327
|
+
}, function (msg) {
|
|
328
|
+
hidePending();
|
|
329
|
+
addMessage("error", "Summarize failed: " + msg);
|
|
330
|
+
setBusy(false);
|
|
331
|
+
updateSelectionStatus();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
276
335
|
// ---------------------------------------------------------------------
|
|
277
336
|
// Bounded read-tool loop, shared by chat and
|
|
278
337
|
// generate/document/modify ("explore-then-propose"). Sends the
|
|
@@ -301,7 +360,6 @@
|
|
|
301
360
|
// than erroring out.
|
|
302
361
|
// ---------------------------------------------------------------------
|
|
303
362
|
var AGENT_LOOP_MAX_STEPS = 8;
|
|
304
|
-
var AGENT_LOOP_TOKEN_CEILING = 50000;
|
|
305
363
|
// W7 §16 point 5: ask_user round-trips get their own small budget,
|
|
306
364
|
// separate from AGENT_LOOP_MAX_STEPS, so a couple of clarifying
|
|
307
365
|
// questions don't eat the real step budget.
|
|
@@ -323,6 +381,8 @@
|
|
|
323
381
|
if (!payload || !payload.strategy || !payload.entry) {
|
|
324
382
|
throw new Error("runAgentLoop requires strategy and entry in the initial payload.");
|
|
325
383
|
}
|
|
384
|
+
var AGENT_LOOP_TOKEN_CEILING = (Number(currentSettings.agentLoopTokenCeiling) > 0)
|
|
385
|
+
? Number(currentSettings.agentLoopTokenCeiling) : 50000;
|
|
326
386
|
var runStrategy = payload.strategy;
|
|
327
387
|
var runEntry = payload.entry;
|
|
328
388
|
// P10-D1 follow-up (sr-dev review): captured once, like
|
|
@@ -422,12 +482,27 @@
|
|
|
422
482
|
var runEvents = [];
|
|
423
483
|
var runRec = null;
|
|
424
484
|
|
|
485
|
+
function syncRunMarker(reason) {
|
|
486
|
+
if (!runRec) { return; }
|
|
487
|
+
writeRunMarker({
|
|
488
|
+
runId: runId,
|
|
489
|
+
action: runEntry,
|
|
490
|
+
appliedCount: runEvents.filter(function (e) { return e.t === "applied"; }).length,
|
|
491
|
+
conversationId: runConversationId
|
|
492
|
+
}, reason || "syncRunMarker");
|
|
493
|
+
}
|
|
494
|
+
|
|
425
495
|
function recordRunEvent(t, extra) {
|
|
426
496
|
if (!runRec) {
|
|
427
497
|
runRec = addRecord("todo", { action: runEntry, items: [], events: runEvents });
|
|
428
498
|
}
|
|
429
499
|
runEvents.push(Object.assign({ t: t, at: Date.now() }, extra || {}));
|
|
430
500
|
runRec.events = runEvents;
|
|
501
|
+
if (t === "done") {
|
|
502
|
+
clearRunMarker("run done: " + runId);
|
|
503
|
+
} else {
|
|
504
|
+
syncRunMarker("run event: " + t);
|
|
505
|
+
}
|
|
431
506
|
}
|
|
432
507
|
|
|
433
508
|
function addUsage(usage) {
|
|
@@ -466,7 +541,8 @@
|
|
|
466
541
|
}, stepExtra, {
|
|
467
542
|
strategy: runStrategy,
|
|
468
543
|
entry: runEntry,
|
|
469
|
-
runId: runId
|
|
544
|
+
runId: runId,
|
|
545
|
+
events: runEvents.slice()
|
|
470
546
|
});
|
|
471
547
|
if (pendingDebugNote) {
|
|
472
548
|
stepPayload.debugNote = pendingDebugNote;
|
|
@@ -540,7 +616,11 @@
|
|
|
540
616
|
}
|
|
541
617
|
|
|
542
618
|
function continueWithResult(resultObj) {
|
|
543
|
-
|
|
619
|
+
var _reason = null;
|
|
620
|
+
if (resultObj && !resultObj.allPass) {
|
|
621
|
+
_reason = resultObj.error || resultObj.reason || null;
|
|
622
|
+
}
|
|
623
|
+
if (isWriteTool) { agentWriteResults.push({ allPass: !!(resultObj && resultObj.allPass), reason: _reason }); }
|
|
544
624
|
nextMessages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(resultObj) });
|
|
545
625
|
processToolCallsFrom(calls, idx + 1, nextMessages, toolTiers);
|
|
546
626
|
}
|
|
@@ -642,9 +722,10 @@
|
|
|
642
722
|
if (totalTokens > AGENT_LOOP_TOKEN_CEILING) {
|
|
643
723
|
if (runRec) { recordRunEvent("interrupted", { detail: "token ceiling exceeded" }); rerenderTodoRecord(runRec); }
|
|
644
724
|
onError("FlowPilot stopped after using " + totalTokens +
|
|
645
|
-
" tokens on this turn
|
|
646
|
-
"selecting fewer nodes,
|
|
647
|
-
"question
|
|
725
|
+
" tokens on this turn (limit: " + AGENT_LOOP_TOKEN_CEILING +
|
|
726
|
+
") without a final answer. Try selecting fewer nodes, asking a " +
|
|
727
|
+
"more specific question, or raising \"Max total tokens per " +
|
|
728
|
+
"agent turn\" in Settings → Behavior.");
|
|
648
729
|
return;
|
|
649
730
|
}
|
|
650
731
|
if (fpAgentStopRequested) {
|
|
@@ -961,10 +1042,11 @@
|
|
|
961
1042
|
// overwriting on a non-empty result, so a bad/empty resolution
|
|
962
1043
|
// doesn't blow away a legitimate live-selection pin.
|
|
963
1044
|
if (suggestedAction.targetNodeIds === "all") {
|
|
964
|
-
var
|
|
965
|
-
var allTabIds = [];
|
|
966
|
-
RED.nodes.eachNode(function (n) { if (n.z === activeTabId) { allTabIds.push(n.id); } });
|
|
1045
|
+
var allTabIds = allActiveTabNodeIds();
|
|
967
1046
|
if (allTabIds.length) { pinnedSelectionIds = allTabIds; }
|
|
1047
|
+
} else if (suggestedAction.targetNodeIds === "instance") {
|
|
1048
|
+
var allInstanceIds = allInstanceNodeIds();
|
|
1049
|
+
if (allInstanceIds.length) { pinnedSelectionIds = allInstanceIds; }
|
|
968
1050
|
} else if (Array.isArray(suggestedAction.targetNodeIds)) {
|
|
969
1051
|
var resolvedIds = suggestedAction.targetNodeIds.filter(function (id) {
|
|
970
1052
|
return !!findLiveNode(id);
|
|
@@ -1038,6 +1120,7 @@
|
|
|
1038
1120
|
// here: if this chip targets Document and doesn't carry a real
|
|
1039
1121
|
// resolved target, always show the one hint that's actually true.
|
|
1040
1122
|
var targetResolved = suggestedAction.targetNodeIds === "all" ||
|
|
1123
|
+
suggestedAction.targetNodeIds === "instance" ||
|
|
1041
1124
|
(Array.isArray(suggestedAction.targetNodeIds) && suggestedAction.targetNodeIds.length > 0);
|
|
1042
1125
|
var hintText = (suggestedAction.mode === "document" && !targetResolved)
|
|
1043
1126
|
? "Select the node(s) you want documented first."
|
|
@@ -1176,7 +1259,19 @@
|
|
|
1176
1259
|
updateSelectionStatus();
|
|
1177
1260
|
return true;
|
|
1178
1261
|
}
|
|
1179
|
-
|
|
1262
|
+
// finalizeSimpleGeneration/finalizeModifyResult set prose:true on
|
|
1263
|
+
// EVERY agent-strategy final response, whether or not real
|
|
1264
|
+
// WRITE-tool work happened this run — it's the server's generic
|
|
1265
|
+
// "no classic flow/changes to review" signal, not a claim that
|
|
1266
|
+
// nothing happened. When real write results are attached
|
|
1267
|
+
// (data._agentWriteResults, added client-side in handleStep before
|
|
1268
|
+
// onDone fires), the caller's own deterministic-summary rendering
|
|
1269
|
+
// needs to run instead of this generic prose bubble — bug found
|
|
1270
|
+
// live during the 0.6.0 go-live smoke test: this unconditional
|
|
1271
|
+
// early-return made C1's entire deterministic-summary/prose-
|
|
1272
|
+
// demotion feature unreachable for every real agent-strategy run.
|
|
1273
|
+
var hasRealWriteResults = Array.isArray(data._agentWriteResults) && data._agentWriteResults.length > 0;
|
|
1274
|
+
if (data.prose && !hasRealWriteResults) {
|
|
1180
1275
|
if (looksLikeToolEnvelope(data.explanation)) {
|
|
1181
1276
|
handleExecuteError("FlowPilot's reply didn't come through as expected.", data.explanation);
|
|
1182
1277
|
updateSelectionStatus();
|
|
@@ -1203,18 +1298,47 @@
|
|
|
1203
1298
|
|
|
1204
1299
|
// Lay nodes out before review/import — see layoutGeneratedFlow for why.
|
|
1205
1300
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1206
|
-
|
|
1301
|
+
var planItems = parseTodoPlan(data.explanation || "");
|
|
1302
|
+
var todoRec = null;
|
|
1303
|
+
var writeResults = Array.isArray(data._agentWriteResults) ? data._agentWriteResults : [];
|
|
1304
|
+
if (writeResults.length) {
|
|
1305
|
+
if (!planItems.length) {
|
|
1306
|
+
planItems = [{ text: goalPrompt || "Generate flow", status: "pending" }];
|
|
1307
|
+
}
|
|
1308
|
+
planItems.forEach(function (item, i) {
|
|
1309
|
+
item.status = (i < writeResults.length)
|
|
1310
|
+
? (writeResults[i].allPass ? "done" : "failed")
|
|
1311
|
+
: "active";
|
|
1312
|
+
});
|
|
1313
|
+
if (data._agentRunRecord) {
|
|
1314
|
+
todoRec = data._agentRunRecord;
|
|
1315
|
+
todoRec.action = "generate";
|
|
1316
|
+
todoRec.items = planItems;
|
|
1317
|
+
} else {
|
|
1318
|
+
todoRec = addRecord("todo", { action: "generate", items: planItems });
|
|
1319
|
+
}
|
|
1320
|
+
rerenderTodoRecord(todoRec);
|
|
1321
|
+
var deterministicSummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
1322
|
+
addMessage("assistant", deterministicSummary || "(no explanation returned)");
|
|
1323
|
+
if (shouldShowSecondaryExplanation(deterministicSummary, data.explanation)) {
|
|
1324
|
+
addMessage("fp-notice", data.explanation);
|
|
1325
|
+
}
|
|
1326
|
+
} else {
|
|
1327
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1328
|
+
}
|
|
1207
1329
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1208
1330
|
// B1: bake the deploy-verify option into the review panel as the
|
|
1209
1331
|
// primary chip rather than a separate chip below it. Only for
|
|
1210
1332
|
// executable flows (not documentation-only comment nodes) and when no
|
|
1211
1333
|
// loop is already active. The secondary "Just add to canvas" button is
|
|
1212
1334
|
// always shown alongside it as an escape hatch.
|
|
1213
|
-
var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
1335
|
+
var _hasDeployable = (flow || []).some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
1214
1336
|
var _buildOnImported = (goalPrompt && !activeBuildLoop && _hasDeployable)
|
|
1215
1337
|
? function (importResult) { startBuildLoop(goalPrompt, flow, importResult); }
|
|
1216
1338
|
: null;
|
|
1217
|
-
|
|
1339
|
+
if (!writeResults.length) {
|
|
1340
|
+
addGeneratedReview(flow, _buildOnImported, _buildOnImported ? goalPrompt : null);
|
|
1341
|
+
}
|
|
1218
1342
|
// Suppress a server-suggested build chip when deploy-verify is already
|
|
1219
1343
|
// the primary action inside the review panel — it would be a duplicate.
|
|
1220
1344
|
renderActionChip(_buildOnImported && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
@@ -1286,7 +1410,15 @@
|
|
|
1286
1410
|
rerenderTodoRecord(todoRec);
|
|
1287
1411
|
}
|
|
1288
1412
|
|
|
1289
|
-
|
|
1413
|
+
if (writeResults.length) {
|
|
1414
|
+
var deterministicModifySummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
1415
|
+
addMessage("assistant", deterministicModifySummary || "(no explanation returned)");
|
|
1416
|
+
if (shouldShowSecondaryExplanation(deterministicModifySummary, data.explanation)) {
|
|
1417
|
+
addMessage("fp-notice", data.explanation);
|
|
1418
|
+
}
|
|
1419
|
+
} else {
|
|
1420
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1421
|
+
}
|
|
1290
1422
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1291
1423
|
if (data.skippedNote) { addMessage("assistant", "⚠ " + data.skippedNote); }
|
|
1292
1424
|
|
|
@@ -1487,6 +1619,8 @@
|
|
|
1487
1619
|
|
|
1488
1620
|
var ap = activeProvider();
|
|
1489
1621
|
var isAgentLoop = ap && ap.supportsTools;
|
|
1622
|
+
var useAgentStrategy = isAgentLoop && endpointName === "generate" &&
|
|
1623
|
+
currentSettings.enableAgentWrite === true;
|
|
1490
1624
|
|
|
1491
1625
|
setBusy(true);
|
|
1492
1626
|
showPending(isAgentLoop);
|
|
@@ -1494,7 +1628,7 @@
|
|
|
1494
1628
|
prompt: prompt, context: context,
|
|
1495
1629
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1496
1630
|
conversationId: conversationId,
|
|
1497
|
-
strategy: "classic",
|
|
1631
|
+
strategy: useAgentStrategy ? "agent" : "classic",
|
|
1498
1632
|
entry: endpointName
|
|
1499
1633
|
};
|
|
1500
1634
|
|
|
@@ -1559,6 +1693,40 @@
|
|
|
1559
1693
|
return items;
|
|
1560
1694
|
}
|
|
1561
1695
|
|
|
1696
|
+
function buildDeterministicRunSummary(planItems, writeResults) {
|
|
1697
|
+
var lines = [];
|
|
1698
|
+
var unreached = 0;
|
|
1699
|
+
var items = planItems || [];
|
|
1700
|
+
var counted = Math.max(items.length, (writeResults || []).length);
|
|
1701
|
+
for (var i = 0; i < counted; i++) {
|
|
1702
|
+
var item = items[i];
|
|
1703
|
+
if (item && (i >= writeResults.length || item.status === "active" || item.status === "pending")) {
|
|
1704
|
+
unreached++;
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
var itemText = (item && item.text) ? item.text : ("Step " + (i + 1));
|
|
1708
|
+
if (writeResults[i] && writeResults[i].allPass) {
|
|
1709
|
+
lines.push("✓ " + itemText);
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
var reason = (writeResults[i] && writeResults[i].reason) || "failed verification";
|
|
1713
|
+
lines.push("✗ " + itemText + " — " + reason);
|
|
1714
|
+
}
|
|
1715
|
+
if (unreached) {
|
|
1716
|
+
lines.push("▶ " + unreached + " step(s) not reached");
|
|
1717
|
+
}
|
|
1718
|
+
return lines.join("\n");
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function shouldShowSecondaryExplanation(summaryText, explanationText) {
|
|
1722
|
+
if (!explanationText || typeof explanationText !== "string") { return false; }
|
|
1723
|
+
if (!summaryText || typeof summaryText !== "string") { return true; }
|
|
1724
|
+
function normalize(text) {
|
|
1725
|
+
return String(text || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1726
|
+
}
|
|
1727
|
+
return normalize(summaryText) !== normalize(explanationText);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1562
1730
|
// W4: render or re-render a "todo" record. For a 1-item plan, renders
|
|
1563
1731
|
// as a compact status line (one chip). For N>1 items, renders as a
|
|
1564
1732
|
// checklist card. Updates in place when the record already has a
|
|
@@ -1819,6 +1987,7 @@
|
|
|
1819
1987
|
hidePending();
|
|
1820
1988
|
if (renderQuestionOrProse(data)) { return; }
|
|
1821
1989
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1990
|
+
var writeResults = Array.isArray(data._agentWriteResults) ? data._agentWriteResults : [];
|
|
1822
1991
|
|
|
1823
1992
|
// Build the todo plan. Parse "Plan:" from explanation if present;
|
|
1824
1993
|
// fall back to an implicit single item from the goal prompt.
|
|
@@ -1828,18 +1997,41 @@
|
|
|
1828
1997
|
}
|
|
1829
1998
|
// Same aggregate-verification reasoning as the Modify path: mark
|
|
1830
1999
|
// every item active up front so a multi-item plan resolves together.
|
|
1831
|
-
|
|
1832
|
-
|
|
2000
|
+
if (writeResults.length) {
|
|
2001
|
+
planItems.forEach(function (item, i) {
|
|
2002
|
+
item.status = (i < writeResults.length)
|
|
2003
|
+
? (writeResults[i].allPass ? "done" : "failed")
|
|
2004
|
+
: "active";
|
|
2005
|
+
});
|
|
2006
|
+
} else {
|
|
2007
|
+
planItems.forEach(function (item) { item.status = "active"; });
|
|
2008
|
+
}
|
|
2009
|
+
var todoRec;
|
|
2010
|
+
if (data._agentRunRecord) {
|
|
2011
|
+
todoRec = data._agentRunRecord;
|
|
2012
|
+
todoRec.action = "generate";
|
|
2013
|
+
todoRec.items = planItems;
|
|
2014
|
+
} else {
|
|
2015
|
+
todoRec = addRecord("todo", { action: "generate", items: planItems });
|
|
2016
|
+
}
|
|
1833
2017
|
rerenderTodoRecord(todoRec);
|
|
1834
2018
|
|
|
1835
|
-
|
|
2019
|
+
if (writeResults.length) {
|
|
2020
|
+
var deterministicQueueSummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
2021
|
+
addMessage("assistant", deterministicQueueSummary || "(no explanation returned)");
|
|
2022
|
+
if (shouldShowSecondaryExplanation(deterministicQueueSummary, data.explanation)) {
|
|
2023
|
+
addMessage("fp-notice", data.explanation);
|
|
2024
|
+
}
|
|
2025
|
+
} else {
|
|
2026
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
2027
|
+
}
|
|
1836
2028
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1837
2029
|
// B1: when a build loop is appropriate, bake deploy-verify into the
|
|
1838
2030
|
// primary chip (same as handleSimpleGenerationResult). The callback
|
|
1839
2031
|
// also runs verifyImportedNodes so the todo record still gets checked
|
|
1840
2032
|
// off. Without a build loop, fall back to a plain "Add to canvas"
|
|
1841
2033
|
// button that still fires the verify callback.
|
|
1842
|
-
var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
2034
|
+
var _hasDeployable = (flow || []).some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
1843
2035
|
var _wantLoop = goalPrompt && !activeBuildLoop && _hasDeployable;
|
|
1844
2036
|
var _onImported = _wantLoop
|
|
1845
2037
|
? function (importResult) {
|
|
@@ -1847,7 +2039,9 @@
|
|
|
1847
2039
|
startBuildLoop(goalPrompt, flow, importResult);
|
|
1848
2040
|
}
|
|
1849
2041
|
: function (importResult) { verifyImportedNodes(importResult, todoRec); };
|
|
1850
|
-
|
|
2042
|
+
if (!writeResults.length) {
|
|
2043
|
+
addGeneratedReview(flow, _onImported, _wantLoop ? goalPrompt : null);
|
|
2044
|
+
}
|
|
1851
2045
|
// Suppress a server-suggested build chip when deploy-verify is already
|
|
1852
2046
|
// the primary action inside the review panel.
|
|
1853
2047
|
renderActionChip(_wantLoop && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
@@ -2336,12 +2530,34 @@
|
|
|
2336
2530
|
// Generate — a comment node is just a regular flow-JSON node, so there's
|
|
2337
2531
|
// nothing import-mechanism-specific to build here. The prompt box holds
|
|
2338
2532
|
// OPTIONAL notes to steer the explanation; the selection is the real input.
|
|
2533
|
+
// Nothing selected/pinned for a Document send: rather than hard-erroring
|
|
2534
|
+
// (the old behavior — Document previously only ever meant "the
|
|
2535
|
+
// selection"), offer a deterministic one-click scope choice. This is a
|
|
2536
|
+
// pure client-side UX decision, not something worth routing through the
|
|
2537
|
+
// model — a weak/local provider's suggestedAction may omit
|
|
2538
|
+
// targetNodeIds even when the user's intent was clear (see the "all"/
|
|
2539
|
+
// "instance" vocabulary in the system prompts), so this is the backstop
|
|
2540
|
+
// that always works regardless of model reliability.
|
|
2541
|
+
function offerDocumentScopeClarification() {
|
|
2542
|
+
addMessage("info", "Nothing is selected. What would you like documented?");
|
|
2543
|
+
renderChip("This flow", "fa fa-sitemap", function () {
|
|
2544
|
+
var ids = allActiveTabNodeIds();
|
|
2545
|
+
if (ids.length) { pinnedSelectionIds = ids; }
|
|
2546
|
+
documentFlow();
|
|
2547
|
+
});
|
|
2548
|
+
renderChip("Entire instance", "fa fa-server", function () {
|
|
2549
|
+
var ids = allInstanceNodeIds();
|
|
2550
|
+
if (ids.length) { pinnedSelectionIds = ids; }
|
|
2551
|
+
documentFlow();
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2339
2555
|
function documentFlow() {
|
|
2340
2556
|
// Falls back to the pinned selection if nothing is currently
|
|
2341
2557
|
// selected, so follow-up turns need no reselection.
|
|
2342
2558
|
var context = collectSelectionContext(activeSelectionIds());
|
|
2343
2559
|
if (!context || !Array.isArray(context.nodes) || context.nodes.length === 0) {
|
|
2344
|
-
|
|
2560
|
+
offerDocumentScopeClarification();
|
|
2345
2561
|
return;
|
|
2346
2562
|
}
|
|
2347
2563
|
context = attachDebugContext(context);
|
|
@@ -46,6 +46,23 @@
|
|
|
46
46
|
return { nodes: expanded, groupCount: groupCount };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// The "all"/"instance" targetNodeIds scopes (suggestedAction chips,
|
|
50
|
+
// redirect_mode tool calls): every node in the active flow tab, or
|
|
51
|
+
// every node in the whole instance (every tab, enabled or disabled —
|
|
52
|
+
// RED.nodes.eachNode iterates all of them regardless of tab state).
|
|
53
|
+
function allActiveTabNodeIds() {
|
|
54
|
+
var activeTabId = RED.workspaces && RED.workspaces.active ? RED.workspaces.active() : null;
|
|
55
|
+
var ids = [];
|
|
56
|
+
RED.nodes.eachNode(function (n) { if (n.z === activeTabId) { ids.push(n.id); } });
|
|
57
|
+
return ids;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function allInstanceNodeIds() {
|
|
61
|
+
var ids = [];
|
|
62
|
+
RED.nodes.eachNode(function (n) { ids.push(n.id); });
|
|
63
|
+
return ids;
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
function pinCurrentSelection() {
|
|
50
67
|
var sel = (RED.view && RED.view.selection) ? RED.view.selection() : null;
|
|
51
68
|
var ids = expandGroupSelection((sel && sel.nodes) ? sel.nodes : []).nodes
|
|
@@ -33,12 +33,12 @@ Whenever the user describes something they want built, changed, fixed, wired up,
|
|
|
33
33
|
To do this, end your response with a hidden data block: on its own, after all visible reply text, with nothing after it and not inside a code fence, write the literal marker on its own line followed immediately by a single JSON object:
|
|
34
34
|
|
|
35
35
|
<<<FLOWPILOT_DATA>>>
|
|
36
|
-
{"suggestedAction": {"mode": "generate" | "document" | "modify", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."]}}
|
|
36
|
+
{"suggestedAction": {"mode": "generate" | "document" | "modify", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | "instance" | ["real-node-id", "..."]}}
|
|
37
37
|
|
|
38
38
|
- "mode": which FlowPilot action the chip switches to.
|
|
39
39
|
- "prompt": the exact instruction text to pre-fill in the user's compose box — written as a ready-to-send request to FlowPilot, in the user's voice. Keep it as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements, implementation choices, or assumptions the user did not state.
|
|
40
40
|
- "selectionHint" (optional): plain-language description of which node(s) the user should select before sending (only useful for "modify"/"document", which act on a selection).
|
|
41
|
-
- "targetNodeIds" (only for "modify"/"document"): REQUIRED whenever the current selection, active flow/tab, or other provided context makes the target resolvable. Use "all" when the entire active flow/tab is the resolved target, or a non-empty array of real node ids when a specific subset is the resolved target. Omit it only when you genuinely cannot resolve the target from the current context. Modify and Document require a resolved node set: never say or imply in "selectionHint" that no selection is needed unless "targetNodeIds" supplies that target, and never emit an empty array.
|
|
41
|
+
- "targetNodeIds" (only for "modify"/"document"): REQUIRED whenever the current selection, active flow/tab, whole instance, or other provided context makes the target resolvable. Use "all" when the entire active flow/tab is the resolved target, "instance" when the user means every flow tab in the whole Node-RED instance (only ever valid for "document" — Modify always acts on one flow), or a non-empty array of real node ids when a specific subset is the resolved target. Omit it only when you genuinely cannot resolve the target from the current context — don't guess between "all" and "instance" when the wording doesn't make it clear. Modify and Document require a resolved node set: never say or imply in "selectionHint" that no selection is needed unless "targetNodeIds" supplies that target, and never emit an empty array.
|
|
42
42
|
|
|
43
43
|
The user reviews the prepared prompt and clicks the chip themselves — nothing is sent automatically. Skip the data block only when there's truly no actionable follow-up (e.g. plain factual Q&A, status checks).
|
|
44
44
|
|
|
@@ -3,7 +3,7 @@ const {
|
|
|
3
3
|
buildSuggestedActionFragment
|
|
4
4
|
} = require("./prompt-fragments");
|
|
5
5
|
|
|
6
|
-
const identity = `You are FlowPilot's documentation generator.
|
|
6
|
+
const identity = `You are FlowPilot's documentation generator. You are given a set of existing Node-RED nodes as context (sanitized configuration plus how they're wired together) — this may be a hand-picked selection, an entire flow tab, or every flow tab in the whole instance, depending on what the user asked for and what got resolved before you were called. Your job is to explain what that context does, in detail, and package the explanation as a single Node-RED comment node the user can drop onto their canvas as a "read me". If the context spans multiple flow tabs, structure the explanation and diagram around that (e.g. group by tab) rather than writing as if it's all one flow.`;
|
|
7
7
|
|
|
8
8
|
const modeRouting = `Before documenting — check this is actually a "document" request:
|
|
9
9
|
|
|
@@ -23,6 +23,8 @@ When one of these applies, do NOT produce the {"explanation", "flow"} JSON envel
|
|
|
23
23
|
- "selectionHint" (optional): for "modify", which node(s) to select first (Generate needs no selection).
|
|
24
24
|
- "targetNodeIds" (only for "modify"): REQUIRED whenever the current selection, active flow/tab, or other provided context makes the target resolvable. Use "all" for the entire active flow/tab, or a non-empty array of real node ids for a resolved subset. Omit it only when you genuinely cannot resolve the target from the current context. Never imply no selection is needed for Modify unless this field supplies the target, and never emit an empty array.
|
|
25
25
|
|
|
26
|
+
This escape hatch is for redirecting AWAY from Document (to Generate/Modify/Chat) — it never applies to a request that legitimately wants documentation, including "document my whole flow" or "document this entire Node-RED instance". Those stay in Document; see below for how their scope gets resolved.
|
|
27
|
+
|
|
26
28
|
The data block (marker and JSON) is never shown to the user — keep your visible reply complete on its own. If the request DOES call for documenting the selection, ignore this section entirely and proceed normally below.
|
|
27
29
|
|
|
28
30
|
IMPORTANT: this escape hatch is ONLY for requests that belong to a different
|
|
@@ -53,7 +55,7 @@ What "info" should contain:
|
|
|
53
55
|
- A Mermaid diagram of the flow using a fenced code block: \`\`\`mermaid ... \`\`\` (e.g. a \`graph LR\` or \`flowchart LR\` showing each node as a labeled box and arrows for the wiring). Use the node names/types from the context, not raw ids.
|
|
54
56
|
- If the user added their own notes alongside the selection, treat those as instructions for emphasis or audience (e.g. "explain like I'm new to Node-RED") — fold them into how you write the explanation, not as a separate section.
|
|
55
57
|
|
|
56
|
-
Base everything on the actual
|
|
58
|
+
Base everything on the actual nodes given as context and their wiring — never invent nodes that aren't in the context. If you were given nothing useful to document, say so plainly in "explanation" and still return a single comment node whose "info" explains that nothing could be documented.`;
|
|
57
59
|
|
|
58
60
|
const suggestedAction = buildSuggestedActionFragment({
|
|
59
61
|
responseContext: "e.g. you noticed something worth fixing while documenting",
|
|
@@ -16,12 +16,12 @@ The user is currently in Generate mode, which always produces a NEW, disconnecte
|
|
|
16
16
|
When one of these applies, do NOT produce the {"explanation", "flow"} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked — answer the question, or explain that this looks like a Modify/Document/Chat request — and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
|
|
17
17
|
|
|
18
18
|
<<<FLOWPILOT_DATA>>>
|
|
19
|
-
{"suggestedAction": {"mode": "modify" | "document" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."]}}
|
|
19
|
+
{"suggestedAction": {"mode": "modify" | "document" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | "instance" | ["real-node-id", "..."]}}
|
|
20
20
|
|
|
21
21
|
- "mode": "modify" or "document" if their request matches one of those actions instead; "chat" if it's a question, explanation, or remark with no further action needed — including questions about the existing flow, since answering IS the action (don't pick "document" just because the question mentions the flow).
|
|
22
22
|
- "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice.
|
|
23
23
|
- "selectionHint" (optional): for "modify"/"document", plain-language description of which node(s) to select first (Generate needs no selection, so this never applies to a "generate" suggestion here).
|
|
24
|
-
- "targetNodeIds" (optional, only for "modify"/"document"): "all" for the entire active flow/tab, or a non-empty array of real node ids for a resolved subset. Omit when no resolved target is known. Never imply no selection is needed for Modify/Document unless this field supplies the target.
|
|
24
|
+
- "targetNodeIds" (optional, only for "modify"/"document"): "all" for the entire active flow/tab, "instance" for the whole Node-RED instance across every flow tab (only ever valid for "document"), or a non-empty array of real node ids for a resolved subset. Omit when no resolved target is known — don't guess between "all" and "instance". Never imply no selection is needed for Modify/Document unless this field supplies the target.
|
|
25
25
|
|
|
26
26
|
The data block (marker and JSON) is never shown to the user — keep your visible reply complete on its own. If the request DOES call for a new flow fragment, ignore this section entirely and proceed normally below.
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ const agentWriteRules = `IMPORTANT — WRITE-tool execution overrides the other
|
|
|
31
31
|
- Never use \`ask_user\` to triage a mode mismatch. \`ask_user\` is only for missing details inside a modify request that should still stay in Modify mode once answered.
|
|
32
32
|
- A tool result shaped as \`{"unsupported":true,"operation":"...","reason":"...","available":[...]}\` means that operation is outside the current WRITE surface. Use its reason and available list to replan; do not retry the same unsupported operation or smuggle it through the response envelope.
|
|
33
33
|
- If part of the request cannot be achieved with the available WRITE tools, complete every part that can be achieved and state the remainder plainly in the final explanation; do not abandon the whole request or redirect modes. Never emit flow JSON in your final message — it will be discarded.
|
|
34
|
-
- Only after every plan item has been executed, return the normal single JSON response with a concise explanation of what was completed. Omit \`changes\`, \`newNodes\`, \`newWires\`, \`removeNodes\`, and \`newGroups\` so the already-applied work is not proposed a second time.`;
|
|
34
|
+
- Only after every plan item has been executed, return the normal single JSON response with a concise explanation of what was completed. Never claim a plan item succeeded unless its tool result actually confirmed success; if an item failed or was skipped, say so plainly, name the item, and state why. Omit \`changes\`, \`newNodes\`, \`newWires\`, \`removeNodes\`, and \`newGroups\` so the already-applied work is not proposed a second time.`;
|
|
35
35
|
|
|
36
36
|
const modeRouting = `Before modifying — check this is actually a "modify" request:
|
|
37
37
|
|
package/lib/prompt-fragments.js
CHANGED
|
@@ -22,7 +22,7 @@ If there's an obvious, single one-click follow-up the user would want after this
|
|
|
22
22
|
response${responseContext} include an${optionalPrefix} "suggestedAction" key alongside ${responseTarget}:
|
|
23
23
|
|
|
24
24
|
{
|
|
25
|
-
"suggestedAction": { "mode": "generate" | "document" | "modify" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."] }
|
|
25
|
+
"suggestedAction": { "mode": "generate" | "document" | "modify" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | "instance" | ["real-node-id", "..."] }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
- "mode": which FlowPilot action the chip switches to ("chat" for a follow-up
|
|
@@ -36,11 +36,15 @@ response${responseContext} include an${optionalPrefix} "suggestedAction" key alo
|
|
|
36
36
|
should select before sending (only useful for "modify"/"document", which act on a
|
|
37
37
|
selection).
|
|
38
38
|
- "targetNodeIds" (only for "modify"/"document"): REQUIRED whenever the current
|
|
39
|
-
selection, active flow/tab, or other provided context makes
|
|
40
|
-
resolvable. Use "all" when the entire active flow/tab is the
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
selection, active flow/tab, whole instance, or other provided context makes
|
|
40
|
+
the target resolvable. Use "all" when the entire active flow/tab is the
|
|
41
|
+
resolved target, "instance" when the user means every flow tab in this
|
|
42
|
+
Node-RED instance (only ever valid for "document" — Modify always acts on
|
|
43
|
+
one flow), or a non-empty array of real node ids when a specific subset is
|
|
44
|
+
the resolved target. Omit it only when you genuinely cannot resolve the
|
|
45
|
+
target from the current context — don't guess between "all" and "instance"
|
|
46
|
+
when the user's wording doesn't make it clear; omitting it is correct there.
|
|
47
|
+
Modify and Document require a resolved node set: never imply
|
|
44
48
|
in "selectionHint" that no selection is needed unless "targetNodeIds"
|
|
45
49
|
supplies it, and never emit an empty array.
|
|
46
50
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const https = require("https");
|
|
2
2
|
const http = require("http");
|
|
3
|
+
const { isModelsListShaped } = require("./provider-shape-check");
|
|
3
4
|
|
|
4
5
|
const ANTHROPIC_API_BASE = "https://api.anthropic.com";
|
|
5
6
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
@@ -327,12 +328,21 @@ async function chatStream(settings, messages, onDelta, onReasoningDelta) {
|
|
|
327
328
|
// ---- listModels ----
|
|
328
329
|
// Tries GET /v1/models; falls back to a hardcoded list if that endpoint
|
|
329
330
|
// is unavailable (non-standard proxy) or returns an error.
|
|
331
|
+
//
|
|
332
|
+
// Allowed to run against an UNCONFIRMED provider (ADR-007), same as the
|
|
333
|
+
// OpenAI-compatible provider's listModels — see its comment for the full
|
|
334
|
+
// rationale. isModelsListShaped gates the success path here too: a
|
|
335
|
+
// non-provider target's response never gets its ids reflected back, it
|
|
336
|
+
// just falls through to the safe hardcoded fallback below like any other
|
|
337
|
+
// failure.
|
|
330
338
|
async function listModels(settings) {
|
|
331
339
|
const baseUrl = resolveBaseUrl(settings);
|
|
332
340
|
try {
|
|
333
341
|
const response = await getJson(baseUrl + "/v1/models", anthropicHeaders(settings), settings.requestTimeoutMs || 30000);
|
|
334
|
-
|
|
335
|
-
|
|
342
|
+
if (!isModelsListShaped(response)) {
|
|
343
|
+
throw new Error("Not a valid provider endpoint (no FlowPilot-compatible response).");
|
|
344
|
+
}
|
|
345
|
+
const models = response.data.map(function (m) { return m && m.id; }).filter(function (id) { return typeof id === "string" && id; });
|
|
336
346
|
if (models.length) { return { models: models }; }
|
|
337
347
|
throw new Error("Empty model list from provider");
|
|
338
348
|
} catch (err) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const http = require("http");
|
|
2
2
|
const https = require("https");
|
|
3
|
+
const { isModelsListShaped } = require("./provider-shape-check");
|
|
3
4
|
|
|
4
5
|
function postJson(urlString, headers, body, timeoutMs) {
|
|
5
6
|
return new Promise((resolve, reject) => {
|
|
@@ -125,6 +126,15 @@ function getJson(urlString, headers, timeoutMs) {
|
|
|
125
126
|
// the user picks one. Never throws: a provider without /v1/models (or any
|
|
126
127
|
// other failure) just means an empty list with an explanatory error, which
|
|
127
128
|
// the UI shows as a hint while leaving the model field free-text.
|
|
129
|
+
//
|
|
130
|
+
// Allowed to run against an UNCONFIRMED provider (ADR-007) — same blind
|
|
131
|
+
// treatment as /flowpilot/test and /flowpilot/probe: getJson's error path
|
|
132
|
+
// is already generic (never reflects the upstream body), and a "successful"
|
|
133
|
+
// response is only trusted if it's genuinely models-list-shaped
|
|
134
|
+
// (isModelsListShaped) — a non-provider target returning some unrelated
|
|
135
|
+
// JSON blob with data[].id-shaped entries doesn't get those ids reflected
|
|
136
|
+
// back to the client. This route never writes confirmedBaseUrl/confirmedAt;
|
|
137
|
+
// only /flowpilot/test performs the deliberate confirming action.
|
|
128
138
|
// ---------------------------------------------------------------------
|
|
129
139
|
async function listModels(settings) {
|
|
130
140
|
const baseUrl = String(settings.baseUrl || "").replace(/\/+$/, "");
|
|
@@ -137,8 +147,10 @@ async function listModels(settings) {
|
|
|
137
147
|
|
|
138
148
|
try {
|
|
139
149
|
const response = await getJson(`${baseUrl}/v1/models`, headers, settings.requestTimeoutMs || 30000);
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
if (!isModelsListShaped(response)) {
|
|
151
|
+
return { models: [], error: "Not a valid provider endpoint (no FlowPilot-compatible response)." };
|
|
152
|
+
}
|
|
153
|
+
const models = response.data
|
|
142
154
|
.map(function (m) { return m && m.id; })
|
|
143
155
|
.filter(function (id) { return typeof id === "string" && id; });
|
|
144
156
|
return { models: models };
|
package/lib/storage.js
CHANGED
|
@@ -135,6 +135,12 @@ function createStorage(userDir) {
|
|
|
135
135
|
// Hard output bound for each tool-capable agent turn. Classic completions
|
|
136
136
|
// remain uncapped so ordinary flow envelopes are never silently clipped.
|
|
137
137
|
agentTurnMaxTokens: 4096,
|
|
138
|
+
// Cumulative token budget across one read/write tool-calling turn
|
|
139
|
+
// before stopping with an honest error instead of continuing forever.
|
|
140
|
+
// User-configurable since "reasonable" varies by provider context
|
|
141
|
+
// window. Unrelated to agentTurnMaxTokens above, which caps one
|
|
142
|
+
// individual model response's output length, not the running total.
|
|
143
|
+
agentLoopTokenCeiling: 50000,
|
|
138
144
|
// Max build->deploy->test->fix cycles the /build agentic loop will run
|
|
139
145
|
// before stopping with an honest "couldn't fully verify" instead of
|
|
140
146
|
// proposing another fix. Bounds against a non-converging loop burning
|