@manny-est/node-red-flowpilot 0.6.0-beta.1 → 0.6.0
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 +93 -13
- package/README.md +10 -1
- package/flowpilot-core.css +15 -0
- package/flowpilot-node-entry.js +15 -0
- package/flowpilot.js +215 -21
- 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 +222 -18
- 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."
|
|
@@ -1203,7 +1286,34 @@
|
|
|
1203
1286
|
|
|
1204
1287
|
// Lay nodes out before review/import — see layoutGeneratedFlow for why.
|
|
1205
1288
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1206
|
-
|
|
1289
|
+
var planItems = parseTodoPlan(data.explanation || "");
|
|
1290
|
+
var todoRec = null;
|
|
1291
|
+
var writeResults = Array.isArray(data._agentWriteResults) ? data._agentWriteResults : [];
|
|
1292
|
+
if (writeResults.length) {
|
|
1293
|
+
if (!planItems.length) {
|
|
1294
|
+
planItems = [{ text: goalPrompt || "Generate flow", status: "pending" }];
|
|
1295
|
+
}
|
|
1296
|
+
planItems.forEach(function (item, i) {
|
|
1297
|
+
item.status = (i < writeResults.length)
|
|
1298
|
+
? (writeResults[i].allPass ? "done" : "failed")
|
|
1299
|
+
: "active";
|
|
1300
|
+
});
|
|
1301
|
+
if (data._agentRunRecord) {
|
|
1302
|
+
todoRec = data._agentRunRecord;
|
|
1303
|
+
todoRec.action = "generate";
|
|
1304
|
+
todoRec.items = planItems;
|
|
1305
|
+
} else {
|
|
1306
|
+
todoRec = addRecord("todo", { action: "generate", items: planItems });
|
|
1307
|
+
}
|
|
1308
|
+
rerenderTodoRecord(todoRec);
|
|
1309
|
+
var deterministicSummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
1310
|
+
addMessage("assistant", deterministicSummary || "(no explanation returned)");
|
|
1311
|
+
if (shouldShowSecondaryExplanation(deterministicSummary, data.explanation)) {
|
|
1312
|
+
addMessage("fp-notice", data.explanation);
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1316
|
+
}
|
|
1207
1317
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1208
1318
|
// B1: bake the deploy-verify option into the review panel as the
|
|
1209
1319
|
// primary chip rather than a separate chip below it. Only for
|
|
@@ -1214,7 +1324,9 @@
|
|
|
1214
1324
|
var _buildOnImported = (goalPrompt && !activeBuildLoop && _hasDeployable)
|
|
1215
1325
|
? function (importResult) { startBuildLoop(goalPrompt, flow, importResult); }
|
|
1216
1326
|
: null;
|
|
1217
|
-
|
|
1327
|
+
if (!writeResults.length) {
|
|
1328
|
+
addGeneratedReview(flow, _buildOnImported, _buildOnImported ? goalPrompt : null);
|
|
1329
|
+
}
|
|
1218
1330
|
// Suppress a server-suggested build chip when deploy-verify is already
|
|
1219
1331
|
// the primary action inside the review panel — it would be a duplicate.
|
|
1220
1332
|
renderActionChip(_buildOnImported && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
@@ -1286,7 +1398,15 @@
|
|
|
1286
1398
|
rerenderTodoRecord(todoRec);
|
|
1287
1399
|
}
|
|
1288
1400
|
|
|
1289
|
-
|
|
1401
|
+
if (writeResults.length) {
|
|
1402
|
+
var deterministicModifySummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
1403
|
+
addMessage("assistant", deterministicModifySummary || "(no explanation returned)");
|
|
1404
|
+
if (shouldShowSecondaryExplanation(deterministicModifySummary, data.explanation)) {
|
|
1405
|
+
addMessage("fp-notice", data.explanation);
|
|
1406
|
+
}
|
|
1407
|
+
} else {
|
|
1408
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1409
|
+
}
|
|
1290
1410
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1291
1411
|
if (data.skippedNote) { addMessage("assistant", "⚠ " + data.skippedNote); }
|
|
1292
1412
|
|
|
@@ -1487,6 +1607,8 @@
|
|
|
1487
1607
|
|
|
1488
1608
|
var ap = activeProvider();
|
|
1489
1609
|
var isAgentLoop = ap && ap.supportsTools;
|
|
1610
|
+
var useAgentStrategy = isAgentLoop && endpointName === "generate" &&
|
|
1611
|
+
currentSettings.enableAgentWrite === true;
|
|
1490
1612
|
|
|
1491
1613
|
setBusy(true);
|
|
1492
1614
|
showPending(isAgentLoop);
|
|
@@ -1494,7 +1616,7 @@
|
|
|
1494
1616
|
prompt: prompt, context: context,
|
|
1495
1617
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1496
1618
|
conversationId: conversationId,
|
|
1497
|
-
strategy: "classic",
|
|
1619
|
+
strategy: useAgentStrategy ? "agent" : "classic",
|
|
1498
1620
|
entry: endpointName
|
|
1499
1621
|
};
|
|
1500
1622
|
|
|
@@ -1559,6 +1681,40 @@
|
|
|
1559
1681
|
return items;
|
|
1560
1682
|
}
|
|
1561
1683
|
|
|
1684
|
+
function buildDeterministicRunSummary(planItems, writeResults) {
|
|
1685
|
+
var lines = [];
|
|
1686
|
+
var unreached = 0;
|
|
1687
|
+
var items = planItems || [];
|
|
1688
|
+
var counted = Math.max(items.length, (writeResults || []).length);
|
|
1689
|
+
for (var i = 0; i < counted; i++) {
|
|
1690
|
+
var item = items[i];
|
|
1691
|
+
if (item && (i >= writeResults.length || item.status === "active" || item.status === "pending")) {
|
|
1692
|
+
unreached++;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
var itemText = (item && item.text) ? item.text : ("Step " + (i + 1));
|
|
1696
|
+
if (writeResults[i] && writeResults[i].allPass) {
|
|
1697
|
+
lines.push("✓ " + itemText);
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
var reason = (writeResults[i] && writeResults[i].reason) || "failed verification";
|
|
1701
|
+
lines.push("✗ " + itemText + " — " + reason);
|
|
1702
|
+
}
|
|
1703
|
+
if (unreached) {
|
|
1704
|
+
lines.push("▶ " + unreached + " step(s) not reached");
|
|
1705
|
+
}
|
|
1706
|
+
return lines.join("\n");
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function shouldShowSecondaryExplanation(summaryText, explanationText) {
|
|
1710
|
+
if (!explanationText || typeof explanationText !== "string") { return false; }
|
|
1711
|
+
if (!summaryText || typeof summaryText !== "string") { return true; }
|
|
1712
|
+
function normalize(text) {
|
|
1713
|
+
return String(text || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1714
|
+
}
|
|
1715
|
+
return normalize(summaryText) !== normalize(explanationText);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1562
1718
|
// W4: render or re-render a "todo" record. For a 1-item plan, renders
|
|
1563
1719
|
// as a compact status line (one chip). For N>1 items, renders as a
|
|
1564
1720
|
// checklist card. Updates in place when the record already has a
|
|
@@ -1819,6 +1975,7 @@
|
|
|
1819
1975
|
hidePending();
|
|
1820
1976
|
if (renderQuestionOrProse(data)) { return; }
|
|
1821
1977
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1978
|
+
var writeResults = Array.isArray(data._agentWriteResults) ? data._agentWriteResults : [];
|
|
1822
1979
|
|
|
1823
1980
|
// Build the todo plan. Parse "Plan:" from explanation if present;
|
|
1824
1981
|
// fall back to an implicit single item from the goal prompt.
|
|
@@ -1828,11 +1985,34 @@
|
|
|
1828
1985
|
}
|
|
1829
1986
|
// Same aggregate-verification reasoning as the Modify path: mark
|
|
1830
1987
|
// every item active up front so a multi-item plan resolves together.
|
|
1831
|
-
|
|
1832
|
-
|
|
1988
|
+
if (writeResults.length) {
|
|
1989
|
+
planItems.forEach(function (item, i) {
|
|
1990
|
+
item.status = (i < writeResults.length)
|
|
1991
|
+
? (writeResults[i].allPass ? "done" : "failed")
|
|
1992
|
+
: "active";
|
|
1993
|
+
});
|
|
1994
|
+
} else {
|
|
1995
|
+
planItems.forEach(function (item) { item.status = "active"; });
|
|
1996
|
+
}
|
|
1997
|
+
var todoRec;
|
|
1998
|
+
if (data._agentRunRecord) {
|
|
1999
|
+
todoRec = data._agentRunRecord;
|
|
2000
|
+
todoRec.action = "generate";
|
|
2001
|
+
todoRec.items = planItems;
|
|
2002
|
+
} else {
|
|
2003
|
+
todoRec = addRecord("todo", { action: "generate", items: planItems });
|
|
2004
|
+
}
|
|
1833
2005
|
rerenderTodoRecord(todoRec);
|
|
1834
2006
|
|
|
1835
|
-
|
|
2007
|
+
if (writeResults.length) {
|
|
2008
|
+
var deterministicQueueSummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
2009
|
+
addMessage("assistant", deterministicQueueSummary || "(no explanation returned)");
|
|
2010
|
+
if (shouldShowSecondaryExplanation(deterministicQueueSummary, data.explanation)) {
|
|
2011
|
+
addMessage("fp-notice", data.explanation);
|
|
2012
|
+
}
|
|
2013
|
+
} else {
|
|
2014
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
2015
|
+
}
|
|
1836
2016
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1837
2017
|
// B1: when a build loop is appropriate, bake deploy-verify into the
|
|
1838
2018
|
// primary chip (same as handleSimpleGenerationResult). The callback
|
|
@@ -1847,7 +2027,9 @@
|
|
|
1847
2027
|
startBuildLoop(goalPrompt, flow, importResult);
|
|
1848
2028
|
}
|
|
1849
2029
|
: function (importResult) { verifyImportedNodes(importResult, todoRec); };
|
|
1850
|
-
|
|
2030
|
+
if (!writeResults.length) {
|
|
2031
|
+
addGeneratedReview(flow, _onImported, _wantLoop ? goalPrompt : null);
|
|
2032
|
+
}
|
|
1851
2033
|
// Suppress a server-suggested build chip when deploy-verify is already
|
|
1852
2034
|
// the primary action inside the review panel.
|
|
1853
2035
|
renderActionChip(_wantLoop && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
@@ -2336,12 +2518,34 @@
|
|
|
2336
2518
|
// Generate — a comment node is just a regular flow-JSON node, so there's
|
|
2337
2519
|
// nothing import-mechanism-specific to build here. The prompt box holds
|
|
2338
2520
|
// OPTIONAL notes to steer the explanation; the selection is the real input.
|
|
2521
|
+
// Nothing selected/pinned for a Document send: rather than hard-erroring
|
|
2522
|
+
// (the old behavior — Document previously only ever meant "the
|
|
2523
|
+
// selection"), offer a deterministic one-click scope choice. This is a
|
|
2524
|
+
// pure client-side UX decision, not something worth routing through the
|
|
2525
|
+
// model — a weak/local provider's suggestedAction may omit
|
|
2526
|
+
// targetNodeIds even when the user's intent was clear (see the "all"/
|
|
2527
|
+
// "instance" vocabulary in the system prompts), so this is the backstop
|
|
2528
|
+
// that always works regardless of model reliability.
|
|
2529
|
+
function offerDocumentScopeClarification() {
|
|
2530
|
+
addMessage("info", "Nothing is selected. What would you like documented?");
|
|
2531
|
+
renderChip("This flow", "fa fa-sitemap", function () {
|
|
2532
|
+
var ids = allActiveTabNodeIds();
|
|
2533
|
+
if (ids.length) { pinnedSelectionIds = ids; }
|
|
2534
|
+
documentFlow();
|
|
2535
|
+
});
|
|
2536
|
+
renderChip("Entire instance", "fa fa-server", function () {
|
|
2537
|
+
var ids = allInstanceNodeIds();
|
|
2538
|
+
if (ids.length) { pinnedSelectionIds = ids; }
|
|
2539
|
+
documentFlow();
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2339
2543
|
function documentFlow() {
|
|
2340
2544
|
// Falls back to the pinned selection if nothing is currently
|
|
2341
2545
|
// selected, so follow-up turns need no reselection.
|
|
2342
2546
|
var context = collectSelectionContext(activeSelectionIds());
|
|
2343
2547
|
if (!context || !Array.isArray(context.nodes) || context.nodes.length === 0) {
|
|
2344
|
-
|
|
2548
|
+
offerDocumentScopeClarification();
|
|
2345
2549
|
return;
|
|
2346
2550
|
}
|
|
2347
2551
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manny-est/node-red-flowpilot",
|
|
3
|
-
"version": "0.6.0
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "FlowPilot for Node-RED - an AI-powered development assistant sidebar",
|
|
5
5
|
"main": "flowpilot.js",
|
|
6
6
|
"keywords": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"node-red": {
|
|
27
27
|
"version": ">=4.0.0",
|
|
28
28
|
"nodes": {
|
|
29
|
-
"flowpilot": "flowpilot.js"
|
|
29
|
+
"flowpilot": "flowpilot-node-entry.js"
|
|
30
30
|
},
|
|
31
31
|
"plugins": {
|
|
32
32
|
"flowpilot": "flowpilot.html"
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"flowpilot.js",
|
|
40
|
+
"flowpilot-node-entry.js",
|
|
40
41
|
"flowpilot.html",
|
|
41
42
|
"flowpilot-core.css",
|
|
42
43
|
"lib",
|