@manny-est/node-red-flowpilot 0.5.1 → 0.6.0-beta.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 +58 -0
- package/README.md +21 -7
- package/USER-GUIDE.md +27 -14
- package/flowpilot-core.css +159 -5
- package/flowpilot.js +1353 -172
- package/lib/agent-contract.js +45 -0
- package/lib/build-core-script.js +1 -0
- package/lib/build-system-prompt.js +26 -4
- package/lib/chat-data.js +106 -0
- package/lib/core/apply-review.js +268 -64
- package/lib/core/graph-truth.js +63 -0
- package/lib/core/history.js +10 -1
- package/lib/core/init.js +148 -5
- package/lib/core/main.js +755 -21
- package/lib/core/modes.js +1523 -67
- package/lib/core/selection-context.js +31 -1
- package/lib/default-system-prompt.js +17 -12
- package/lib/document-system-prompt.js +22 -32
- package/lib/envelope.js +13 -7
- package/lib/generation-system-prompt.js +29 -44
- package/lib/modify-system-prompt.js +152 -72
- package/lib/persona-prompt.js +81 -54
- package/lib/prompt-fragments.js +56 -0
- package/lib/provider-anthropic.js +388 -0
- package/lib/provider-openai-compatible.js +49 -12
- package/lib/provider-shape-check.js +34 -0
- package/lib/storage.js +160 -12
- package/lib/validator.js +238 -0
- package/package.json +1 -1
package/lib/core/modes.js
CHANGED
|
@@ -155,7 +155,9 @@
|
|
|
155
155
|
context: context,
|
|
156
156
|
history: historyPayload.messages,
|
|
157
157
|
historyTruncated: historyPayload.truncated,
|
|
158
|
-
conversationId: conversationId
|
|
158
|
+
conversationId: conversationId,
|
|
159
|
+
strategy: "classic",
|
|
160
|
+
entry: "chat"
|
|
159
161
|
};
|
|
160
162
|
|
|
161
163
|
function handleSendResult(data) {
|
|
@@ -300,13 +302,133 @@
|
|
|
300
302
|
// ---------------------------------------------------------------------
|
|
301
303
|
var AGENT_LOOP_MAX_STEPS = 8;
|
|
302
304
|
var AGENT_LOOP_TOKEN_CEILING = 50000;
|
|
305
|
+
// W7 §16 point 5: ask_user round-trips get their own small budget,
|
|
306
|
+
// separate from AGENT_LOOP_MAX_STEPS, so a couple of clarifying
|
|
307
|
+
// questions don't eat the real step budget.
|
|
308
|
+
var AGENT_ASK_USER_MAX_ROUNDTRIPS = 3;
|
|
303
309
|
|
|
304
310
|
var fpAgentStopRequested = false;
|
|
305
|
-
|
|
306
|
-
|
|
311
|
+
// CLAUDE-022: the in-flight jqXHR for this run's current ajaxJson call
|
|
312
|
+
// (first request / postNextStep / fallbackToPlain), so the Stop button
|
|
313
|
+
// can actually abort it instead of only setting the flag above. Cleared
|
|
314
|
+
// back to null the moment that call's own success/error callback runs,
|
|
315
|
+
// so a stale reference never lingers into the next step.
|
|
316
|
+
var fpCurrentAgentRequest = null;
|
|
317
|
+
|
|
318
|
+
// P10-D1 (ADR-001 R5): monotonic suffix so two runs minted in the same
|
|
319
|
+
// millisecond (Date.now() collision) still get distinct runIds.
|
|
320
|
+
var runIdCounter = 0;
|
|
321
|
+
|
|
322
|
+
function runAgentLoop(firstEndpoint, payload, stepExtra, realOnDone, realOnError) {
|
|
323
|
+
if (!payload || !payload.strategy || !payload.entry) {
|
|
324
|
+
throw new Error("runAgentLoop requires strategy and entry in the initial payload.");
|
|
325
|
+
}
|
|
326
|
+
var runStrategy = payload.strategy;
|
|
327
|
+
var runEntry = payload.entry;
|
|
328
|
+
// P10-D1 follow-up (sr-dev review): captured once, like
|
|
329
|
+
// runStrategy/runEntry/runId — using the free-variable
|
|
330
|
+
// conversationId instead would let a stale run's dedup lookup land
|
|
331
|
+
// in the wrong conversation's bucket if the user switches
|
|
332
|
+
// conversations mid-run.
|
|
333
|
+
var runConversationId = payload.conversationId;
|
|
334
|
+
// P10-D1: minted once per run, included in every step payload so
|
|
335
|
+
// the server can echo it into logs beside strategy/entry. Also the
|
|
336
|
+
// namespace for this run's WRITE tool opIds (opId = runId + ":" +
|
|
337
|
+
// call.id, ADR-001 R5).
|
|
338
|
+
var runId = "run-" + Date.now().toString(36) + "-" + (++runIdCounter);
|
|
339
|
+
// CLAUDE-013: accumulates placeholder->real-id mappings across every
|
|
340
|
+
// WRITE-tool call resolved THIS run, so a later call in the same run
|
|
341
|
+
// (e.g. group_nodes) can resolve a placeholder id (e.g. "fp-new-2")
|
|
342
|
+
// that an earlier call (e.g. apply_step) minted via applyInsertions —
|
|
343
|
+
// each WRITE executor's call-local idMap only covers ids it created
|
|
344
|
+
// itself, not ids from a prior call in the same agent loop.
|
|
345
|
+
var runIdMap = {};
|
|
346
|
+
// CLAUDE-027: accumulates the individual RED.history event(s) each
|
|
347
|
+
// WRITE-tool call this run would otherwise have pushed on its own
|
|
348
|
+
// (apply_step/remove_step/rename_node/group_nodes — see
|
|
349
|
+
// applyInsertions/applyModifications in apply-review.js and
|
|
350
|
+
// executeGroupNodesTool in main.js, all of which push into this
|
|
351
|
+
// array instead of RED.history directly whenever it's passed
|
|
352
|
+
// through). Flushed as ONE RED.history entry — via RED.history's own
|
|
353
|
+
// t:"multi" wrapper, confirmed against @node-red/editor-client's
|
|
354
|
+
// red.js (e.g. its deleteSelection(), which collapses a mixed
|
|
355
|
+
// delete+move into one push the exact same way) when there's more
|
|
356
|
+
// than one, or pushed unwrapped when there's exactly one, matching
|
|
357
|
+
// that same core convention — the moment this run actually ends, by
|
|
358
|
+
// flushRunHistory()/onDone/onError below. A run that never calls a
|
|
359
|
+
// WRITE tool (e.g. a pure ask_user round-trip, or read-only chat)
|
|
360
|
+
// leaves this empty, so flush is a no-op and no spurious entry is
|
|
361
|
+
// pushed.
|
|
362
|
+
var runHistoryEvents = [];
|
|
307
363
|
var step = 0;
|
|
308
364
|
var totalTokens = 0;
|
|
365
|
+
var askUserRounds = 0;
|
|
309
366
|
fpAgentStopRequested = false;
|
|
367
|
+
fpCurrentAgentRequest = null;
|
|
368
|
+
// W7: one entry per WRITE tool call resolved THIS runAgentLoop
|
|
369
|
+
// invocation, in order (proceed-and-pass / proceed-and-fail /
|
|
370
|
+
// user-declined all count, read-tool calls don't) — gives
|
|
371
|
+
// handleModifyResult a real 1:1 todo-item correlation instead of
|
|
372
|
+
// the CLAUDE-005 all-together fallback. Attached to the final
|
|
373
|
+
// data as _agentWriteResults right before onDone(data); untouched
|
|
374
|
+
// (empty) whenever no WRITE tool call was made this turn, which
|
|
375
|
+
// keeps the existing all-together path completely unchanged.
|
|
376
|
+
var agentWriteResults = [];
|
|
377
|
+
// CLAUDE-014: plain-language note for the decision that triggered the
|
|
378
|
+
// NEXT agent-step round trip (consent-gate Proceed/Skip, ask_user
|
|
379
|
+
// answer) — set right before the decision resumes the loop, read and
|
|
380
|
+
// cleared by postNextStep so CODEX-012's debug.log can show what the
|
|
381
|
+
// user actually decided instead of only raw tool-result JSON. Only
|
|
382
|
+
// populated when settings.debugLogging is on.
|
|
383
|
+
var pendingDebugNote = null;
|
|
384
|
+
|
|
385
|
+
// CLAUDE-027: pushes this run's accumulated WRITE-tool history
|
|
386
|
+
// event(s) (runHistoryEvents above) to RED.history as ONE entry —
|
|
387
|
+
// coalesced via t:"multi" when more than one WRITE-tool call
|
|
388
|
+
// mutated this run, unwrapped when exactly one did, a no-op when
|
|
389
|
+
// none did. MUST run at every single point this run can end, not
|
|
390
|
+
// just the "clean" success path — a run that errors out (step
|
|
391
|
+
// budget/token ceiling exceeded, user Stop, a failed request) after
|
|
392
|
+
// ALREADY applying one or more WRITE tool calls still needs its
|
|
393
|
+
// partial progress to land in undo history, or Ctrl+Z would be
|
|
394
|
+
// unable to remove mutations that are visibly sitting on the
|
|
395
|
+
// canvas. Rather than call this at each of those call sites
|
|
396
|
+
// individually (easy to miss one), onDone/onError below shadow the
|
|
397
|
+
// real callback params so EVERY exit from this closure flushes
|
|
398
|
+
// first automatically.
|
|
399
|
+
function flushRunHistory() {
|
|
400
|
+
if (!runHistoryEvents.length) { return; }
|
|
401
|
+
if (runHistoryEvents.length === 1) {
|
|
402
|
+
RED.history.push(runHistoryEvents[0]);
|
|
403
|
+
} else {
|
|
404
|
+
RED.history.push({ t: "multi", events: runHistoryEvents.slice() });
|
|
405
|
+
}
|
|
406
|
+
runHistoryEvents = [];
|
|
407
|
+
}
|
|
408
|
+
function onDone(data) { flushRunHistory(); realOnDone(data); }
|
|
409
|
+
function onError(err, xhr) { flushRunHistory(); realOnError(err, xhr); }
|
|
410
|
+
|
|
411
|
+
// P10-D2 (ADR-001 R5): run events on the record store. runRec is
|
|
412
|
+
// created LAZILY, the first time something WRITE-tool/ask_user-
|
|
413
|
+
// worthy happens — never for a plain chat/read-only turn, so this
|
|
414
|
+
// is a no-op for every mode/strategy that never offers WRITE tools
|
|
415
|
+
// (chat, document, build, classic Modify — agentToolsFor only
|
|
416
|
+
// offers WRITE_TOOLS for strategy:"agent" + mode:"modify"). Once
|
|
417
|
+
// created, it's a "todo" record (the same W4 machinery that
|
|
418
|
+
// already survives /refresh via rerenderRecord) so a mid-run
|
|
419
|
+
// refresh finds it; handleModifyResult upgrades it in place with
|
|
420
|
+
// real Plan: items via data._agentRunRecord instead of creating a
|
|
421
|
+
// second record, once the final turn's explanation arrives.
|
|
422
|
+
var runEvents = [];
|
|
423
|
+
var runRec = null;
|
|
424
|
+
|
|
425
|
+
function recordRunEvent(t, extra) {
|
|
426
|
+
if (!runRec) {
|
|
427
|
+
runRec = addRecord("todo", { action: runEntry, items: [], events: runEvents });
|
|
428
|
+
}
|
|
429
|
+
runEvents.push(Object.assign({ t: t, at: Date.now() }, extra || {}));
|
|
430
|
+
runRec.events = runEvents;
|
|
431
|
+
}
|
|
310
432
|
|
|
311
433
|
function addUsage(usage) {
|
|
312
434
|
if (usage && typeof usage.total_tokens === "number") {
|
|
@@ -316,14 +438,179 @@
|
|
|
316
438
|
|
|
317
439
|
function fallbackToPlain() {
|
|
318
440
|
setAgentNarration("Continuing without tools…");
|
|
319
|
-
ajaxJson("POST", firstEndpoint, payload,
|
|
441
|
+
fpCurrentAgentRequest = ajaxJson("POST", firstEndpoint, payload, function (data) {
|
|
442
|
+
fpCurrentAgentRequest = null;
|
|
443
|
+
if (runRec) { recordRunEvent("done", {}); data._agentRunRecord = runRec; }
|
|
444
|
+
onDone(data);
|
|
445
|
+
}, function (err, xhr) {
|
|
446
|
+
fpCurrentAgentRequest = null;
|
|
447
|
+
if (fpAgentStopRequested) {
|
|
448
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "stopped by user" }); rerenderTodoRecord(runRec); }
|
|
449
|
+
onError("Stopped after " + Math.max(0, step - 1) + " tool call step(s) at your request.");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "fallback request failed" }); rerenderTodoRecord(runRec); }
|
|
453
|
+
// CLAUDE-023: forward xhr so a genuine parse error's raw
|
|
454
|
+
// response survives to handleExecuteError (via onModifyError)
|
|
455
|
+
// instead of being dropped here — ajaxJson's error callback
|
|
456
|
+
// is (msg, xhr), and onError expects the same shape.
|
|
457
|
+
onError(err, xhr);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function postNextStep(nextMessages) {
|
|
462
|
+
setAgentNarration("Thinking… (step " + step + "/" + AGENT_LOOP_MAX_STEPS + ")");
|
|
463
|
+
var stepPayload = Object.assign({
|
|
464
|
+
messages: nextMessages,
|
|
465
|
+
conversationId: payload.conversationId
|
|
466
|
+
}, stepExtra, {
|
|
467
|
+
strategy: runStrategy,
|
|
468
|
+
entry: runEntry,
|
|
469
|
+
runId: runId
|
|
470
|
+
});
|
|
471
|
+
if (pendingDebugNote) {
|
|
472
|
+
stepPayload.debugNote = pendingDebugNote;
|
|
473
|
+
pendingDebugNote = null;
|
|
474
|
+
}
|
|
475
|
+
fpCurrentAgentRequest = ajaxJson("POST", "flowpilot/agent-step", stepPayload,
|
|
476
|
+
function (stepData) { fpCurrentAgentRequest = null; handleStep(stepData, nextMessages); },
|
|
477
|
+
function (err, xhr) {
|
|
478
|
+
fpCurrentAgentRequest = null;
|
|
479
|
+
if (fpAgentStopRequested) {
|
|
480
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "stopped by user" }); rerenderTodoRecord(runRec); }
|
|
481
|
+
onError("Stopped after " + Math.max(0, step - 1) + " tool call step(s) at your request.");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "agent-step request failed" }); rerenderTodoRecord(runRec); }
|
|
485
|
+
// CLAUDE-023: forward xhr — see fallbackToPlain's error
|
|
486
|
+
// callback above for why this must not be dropped.
|
|
487
|
+
onError(err, xhr);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Processes calls[idx..] one at a time. A WRITE-gated call (per
|
|
492
|
+
// toolTiers, classified against SAFE_NODE_TYPES) or an ask_user
|
|
493
|
+
// call PAUSES here — rendering a chip/question and waiting for a
|
|
494
|
+
// user action — instead of executing synchronously like the
|
|
495
|
+
// existing 6 READ tools still do. Once the whole batch is
|
|
496
|
+
// resolved, posts the next step exactly as before.
|
|
497
|
+
function processToolCallsFrom(calls, idx, nextMessages, toolTiers) {
|
|
498
|
+
if (idx >= calls.length) { postNextStep(nextMessages); return; }
|
|
499
|
+
|
|
500
|
+
var call = calls[idx];
|
|
501
|
+
var name = call.function.name;
|
|
502
|
+
var args = parseToolCallArgs(call);
|
|
503
|
+
if (name === "redirect_mode") {
|
|
504
|
+
var redirectResult = executeAgentToolCall(call, runIdMap, runHistoryEvents);
|
|
505
|
+
nextMessages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(redirectResult) });
|
|
506
|
+
if (runRec) {
|
|
507
|
+
recordRunEvent("redirect", { detail: (redirectResult && redirectResult.suggestedAction && redirectResult.suggestedAction.mode) || args.mode || "unknown" });
|
|
508
|
+
recordRunEvent("done", {});
|
|
509
|
+
}
|
|
510
|
+
onDone({
|
|
511
|
+
explanation: (redirectResult && redirectResult.explanation) || "",
|
|
512
|
+
prose: true,
|
|
513
|
+
flow: null,
|
|
514
|
+
suggestedAction: redirectResult && redirectResult.suggestedAction ? redirectResult.suggestedAction : null
|
|
515
|
+
});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
var isWriteTool = name === "apply_step" || name === "remove_step" || name === "rename_node" || name === "group_nodes";
|
|
519
|
+
// P10-D1: only WRITE tool calls get an opId — idempotency is
|
|
520
|
+
// about preventing double MUTATION, not about read tools.
|
|
521
|
+
var opId = isWriteTool ? (runId + ":" + call.id) : null;
|
|
522
|
+
|
|
523
|
+
// P10-D1: a repeat opId (duplicate delivery, a retry, or the
|
|
524
|
+
// model repeating a call) returns the SAME result without
|
|
525
|
+
// re-invoking the executor — the graph is mutated at most once
|
|
526
|
+
// per opId, checked against the per-conversation applied-ops
|
|
527
|
+
// map before mutating (main.js).
|
|
528
|
+
function runExecutorIdempotent() {
|
|
529
|
+
if (opId) {
|
|
530
|
+
var recorded = getAppliedOp(runConversationId, opId);
|
|
531
|
+
if (recorded) { return recorded; }
|
|
532
|
+
}
|
|
533
|
+
var result = executeAgentToolCall(call, runIdMap, runHistoryEvents);
|
|
534
|
+
// CLAUDE-013: merge this call's own new placeholder->real-id
|
|
535
|
+
// mappings (e.g. apply_step's newNodes) into the run-scoped
|
|
536
|
+
// map so a LATER call this run can resolve the same ids.
|
|
537
|
+
if (result && result.idMap) { Object.assign(runIdMap, result.idMap); }
|
|
538
|
+
if (opId) { recordAppliedOp(runConversationId, opId, result); }
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function continueWithResult(resultObj) {
|
|
543
|
+
if (isWriteTool) { agentWriteResults.push({ allPass: !!(resultObj && resultObj.allPass) }); }
|
|
544
|
+
nextMessages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(resultObj) });
|
|
545
|
+
processToolCallsFrom(calls, idx + 1, nextMessages, toolTiers);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (name === "ask_user") {
|
|
549
|
+
if (askUserRounds >= AGENT_ASK_USER_MAX_ROUNDTRIPS) {
|
|
550
|
+
continueWithResult({ error: "ask_user budget (" + AGENT_ASK_USER_MAX_ROUNDTRIPS +
|
|
551
|
+
") exhausted for this turn — proceed with your best judgment or give a final answer." });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
askUserRounds++;
|
|
555
|
+
setAgentNarration(describeAgentToolCall(name, args));
|
|
556
|
+
recordRunEvent("asked", { detail: args.question });
|
|
557
|
+
renderAskUserQuestion({ question: args.question, options: args.options,
|
|
558
|
+
onAnswer: function (answerText) {
|
|
559
|
+
recordRunEvent("answered", { detail: answerText });
|
|
560
|
+
if (currentSettings.debugLogging) { pendingDebugNote = "user answered: " + answerText; }
|
|
561
|
+
continueWithResult({ answer: answerText });
|
|
562
|
+
} });
|
|
563
|
+
return; // PAUSES here until the question is answered
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (isWriteTool) { recordRunEvent("step", { opId: opId, detail: name }); }
|
|
567
|
+
|
|
568
|
+
var tier = toolTiers && toolTiers[call.id];
|
|
569
|
+
if (writeToolCallNeedsConsent(tier, name, args)) {
|
|
570
|
+
if (isWriteTool) { recordRunEvent("consent", { opId: opId, detail: name }); }
|
|
571
|
+
renderAgentToolConsentGate({
|
|
572
|
+
name: name, args: args,
|
|
573
|
+
onResume: function (granted) {
|
|
574
|
+
if (currentSettings.debugLogging) {
|
|
575
|
+
pendingDebugNote = granted ? "user clicked Proceed" : "user clicked Skip this step";
|
|
576
|
+
}
|
|
577
|
+
if (!granted) {
|
|
578
|
+
continueWithResult({ skipped: true, reason: "user declined — this call was not applied" });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
setAgentNarration(describeAgentToolCall(name, args) + " (step " + step + "/" + AGENT_LOOP_MAX_STEPS + ")");
|
|
582
|
+
var result = runExecutorIdempotent();
|
|
583
|
+
if (isWriteTool) {
|
|
584
|
+
recordRunEvent("applied", { opId: opId });
|
|
585
|
+
recordRunEvent("verified", { opId: opId, detail: !!(result && result.allPass) });
|
|
586
|
+
}
|
|
587
|
+
continueWithResult(result);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
return; // PAUSES here until Proceed/Skip is clicked
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
setAgentNarration(describeAgentToolCall(name, args) + " (step " + step + "/" + AGENT_LOOP_MAX_STEPS + ")");
|
|
594
|
+
var autoResult = runExecutorIdempotent();
|
|
595
|
+
if (isWriteTool) {
|
|
596
|
+
recordRunEvent("applied", { opId: opId });
|
|
597
|
+
recordRunEvent("verified", { opId: opId, detail: !!(autoResult && autoResult.allPass) });
|
|
598
|
+
}
|
|
599
|
+
continueWithResult(autoResult);
|
|
320
600
|
}
|
|
321
601
|
|
|
322
602
|
function handleStep(data, messages) {
|
|
323
603
|
addUsage(data.usage);
|
|
324
604
|
|
|
605
|
+
if (data.fallbackToClassic) {
|
|
606
|
+
fallbackToPlain();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
325
610
|
if (!data.toolCalls || !data.toolCalls.length) {
|
|
326
611
|
if (step > 0) { addAgentStatsNote(step, totalTokens); }
|
|
612
|
+
if (agentWriteResults.length) { data._agentWriteResults = agentWriteResults.slice(); }
|
|
613
|
+
if (runRec) { recordRunEvent("done", {}); data._agentRunRecord = runRec; }
|
|
327
614
|
onDone(data);
|
|
328
615
|
return;
|
|
329
616
|
}
|
|
@@ -336,15 +623,24 @@
|
|
|
336
623
|
return;
|
|
337
624
|
}
|
|
338
625
|
|
|
339
|
-
step
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
626
|
+
// An ask_user-ONLY batch doesn't consume the real step budget —
|
|
627
|
+
// see AGENT_ASK_USER_MAX_ROUNDTRIPS above. A batch mixing
|
|
628
|
+
// ask_user with any other tool call counts normally, since real
|
|
629
|
+
// work happened in it too.
|
|
630
|
+
var isAskUserOnlyBatch = data.toolCalls.every(function (c) { return c.function.name === "ask_user"; });
|
|
631
|
+
if (!isAskUserOnlyBatch) {
|
|
632
|
+
step++;
|
|
633
|
+
if (step > AGENT_LOOP_MAX_STEPS) {
|
|
634
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "step budget exceeded" }); rerenderTodoRecord(runRec); }
|
|
635
|
+
onError("FlowPilot stopped after " + AGENT_LOOP_MAX_STEPS +
|
|
636
|
+
" tool call(s) without a final answer. Try breaking your " +
|
|
637
|
+
"request into smaller steps, or be more specific about " +
|
|
638
|
+
"which node(s) or flow you mean.");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
346
641
|
}
|
|
347
642
|
if (totalTokens > AGENT_LOOP_TOKEN_CEILING) {
|
|
643
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "token ceiling exceeded" }); rerenderTodoRecord(runRec); }
|
|
348
644
|
onError("FlowPilot stopped after using " + totalTokens +
|
|
349
645
|
" tokens on this turn without a final answer. Try " +
|
|
350
646
|
"selecting fewer nodes, or asking a more specific " +
|
|
@@ -352,34 +648,50 @@
|
|
|
352
648
|
return;
|
|
353
649
|
}
|
|
354
650
|
if (fpAgentStopRequested) {
|
|
651
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "stopped by user" }); rerenderTodoRecord(runRec); }
|
|
355
652
|
onError("Stopped after " + (step - 1) + " tool call step(s) at your request.");
|
|
356
653
|
return;
|
|
357
654
|
}
|
|
358
655
|
|
|
359
656
|
var nextMessages = (messages || data.messages || []).slice();
|
|
360
657
|
nextMessages.push({ role: "assistant", content: data.content || null, tool_calls: data.toolCalls });
|
|
361
|
-
data.toolCalls.
|
|
362
|
-
setAgentNarration(describeAgentToolCall(call.function.name, parseToolCallArgs(call)) +
|
|
363
|
-
" (step " + step + "/" + AGENT_LOOP_MAX_STEPS + ")");
|
|
364
|
-
nextMessages.push({
|
|
365
|
-
role: "tool",
|
|
366
|
-
tool_call_id: call.id,
|
|
367
|
-
content: JSON.stringify(executeAgentToolCall(call))
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
setAgentNarration("Thinking… (step " + step + "/" + AGENT_LOOP_MAX_STEPS + ")");
|
|
371
|
-
var stepPayload = Object.assign({
|
|
372
|
-
messages: nextMessages,
|
|
373
|
-
conversationId: payload.conversationId
|
|
374
|
-
}, stepExtra);
|
|
375
|
-
ajaxJson("POST", "flowpilot/agent-step", stepPayload,
|
|
376
|
-
function (stepData) { handleStep(stepData, nextMessages); }, onError);
|
|
658
|
+
processToolCallsFrom(data.toolCalls, 0, nextMessages, data.toolTiers);
|
|
377
659
|
}
|
|
378
660
|
|
|
379
|
-
var firstPayload = Object.assign({}, payload, { tools: true });
|
|
380
|
-
ajaxJson("POST", firstEndpoint, firstPayload, function (data) {
|
|
661
|
+
var firstPayload = Object.assign({}, payload, { tools: true, runId: runId });
|
|
662
|
+
fpCurrentAgentRequest = ajaxJson("POST", firstEndpoint, firstPayload, function (data) {
|
|
663
|
+
fpCurrentAgentRequest = null;
|
|
381
664
|
handleStep(data, null);
|
|
382
|
-
}, function () {
|
|
665
|
+
}, function (err, xhr) {
|
|
666
|
+
fpCurrentAgentRequest = null;
|
|
667
|
+
if (fpAgentStopRequested) {
|
|
668
|
+
// CLAUDE-022: an abort of the FIRST request must actually
|
|
669
|
+
// stop, not fallbackToPlain() — falling back would ignore
|
|
670
|
+
// the stop and keep going without tools instead.
|
|
671
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "stopped by user" }); rerenderTodoRecord(runRec); }
|
|
672
|
+
onError("Stopped after " + Math.max(0, step - 1) + " tool call step(s) at your request.");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
// CLAUDE-023: fallbackToPlain() must only fire for failures that
|
|
676
|
+
// actually look like "the provider doesn't support tools" — a
|
|
677
|
+
// genuine parse/validation failure from processGenerationContent
|
|
678
|
+
// (flowpilot.js) always comes back as HTTP 422 with a .raw
|
|
679
|
+
// payload attached (see err.status = 422 / err.raw = content
|
|
680
|
+
// there). A provider-level failure of the tools:true call itself
|
|
681
|
+
// (unrecognized "tools" field, model swapped since the last
|
|
682
|
+
// probe, etc.) never goes through that parser, so it always
|
|
683
|
+
// reaches here as some other status with no .raw. Only the
|
|
684
|
+
// latter should be silently retried without tools; the former is
|
|
685
|
+
// a real failure and must surface as one so handleExecuteError
|
|
686
|
+
// (via onModifyError) gets a chance to show it — including its
|
|
687
|
+
// raw JSON in Debug mode.
|
|
688
|
+
var raw = xhr && xhr.responseJSON && xhr.responseJSON.raw;
|
|
689
|
+
var looksLikeParseFailure = !!raw || (xhr && xhr.status === 422);
|
|
690
|
+
if (looksLikeParseFailure) {
|
|
691
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "first agentic request failed" }); rerenderTodoRecord(runRec); }
|
|
692
|
+
onError(err, xhr);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
383
695
|
// The provider was probed as
|
|
384
696
|
// supportsTools, but the very first tools:true request failed
|
|
385
697
|
// outright — e.g. the model was swapped since the last probe, or
|
|
@@ -622,8 +934,14 @@
|
|
|
622
934
|
// flow; show the raw so the user can see what happened.
|
|
623
935
|
function handleExecuteError(msg, raw) {
|
|
624
936
|
popDanglingUserHistory();
|
|
625
|
-
|
|
626
|
-
|
|
937
|
+
if (raw && currentSettings.debugLogging) {
|
|
938
|
+
addMessage("error", msg);
|
|
939
|
+
addGeneratedJson(raw, true);
|
|
940
|
+
} else if (raw) {
|
|
941
|
+
addMessage("error", msg + " Enable Debug mode in Settings to see the full response next time.");
|
|
942
|
+
} else {
|
|
943
|
+
addMessage("error", msg);
|
|
944
|
+
}
|
|
627
945
|
setBusy(false);
|
|
628
946
|
}
|
|
629
947
|
|
|
@@ -632,6 +950,27 @@
|
|
|
632
950
|
function applySuggestedAction(suggestedAction) {
|
|
633
951
|
if (!suggestedAction || !suggestedAction.mode || !suggestedAction.prompt) { return; }
|
|
634
952
|
armExecuteAction(suggestedAction.mode);
|
|
953
|
+
// CLAUDE-015: the model already identified which nodes this action
|
|
954
|
+
// targets (including "the whole flow" as a valid identification,
|
|
955
|
+
// not an exemption) — override whatever armExecuteAction's own
|
|
956
|
+
// pinCurrentSelection() pinned from the live canvas selection, the
|
|
957
|
+
// same pinnedSelectionIds every downstream consumer (Document,
|
|
958
|
+
// Modify, their guards, their context builders) already reads via
|
|
959
|
+
// activeSelectionIds(). Absent targetNodeIds: unchanged, today's
|
|
960
|
+
// behavior. Mirrors pinCurrentSelection()'s own rule of only
|
|
961
|
+
// overwriting on a non-empty result, so a bad/empty resolution
|
|
962
|
+
// doesn't blow away a legitimate live-selection pin.
|
|
963
|
+
if (suggestedAction.targetNodeIds === "all") {
|
|
964
|
+
var activeTabId = RED.workspaces && RED.workspaces.active ? RED.workspaces.active() : null;
|
|
965
|
+
var allTabIds = [];
|
|
966
|
+
RED.nodes.eachNode(function (n) { if (n.z === activeTabId) { allTabIds.push(n.id); } });
|
|
967
|
+
if (allTabIds.length) { pinnedSelectionIds = allTabIds; }
|
|
968
|
+
} else if (Array.isArray(suggestedAction.targetNodeIds)) {
|
|
969
|
+
var resolvedIds = suggestedAction.targetNodeIds.filter(function (id) {
|
|
970
|
+
return !!findLiveNode(id);
|
|
971
|
+
});
|
|
972
|
+
if (resolvedIds.length) { pinnedSelectionIds = resolvedIds; }
|
|
973
|
+
}
|
|
635
974
|
var $promptBox = el("#fp-prompt");
|
|
636
975
|
if ($promptBox.length) {
|
|
637
976
|
$promptBox.val(suggestedAction.prompt);
|
|
@@ -660,11 +999,23 @@
|
|
|
660
999
|
var isChatMode = suggestedAction.mode === "chat";
|
|
661
1000
|
var titleText = suggestedAction.customTitle || (isChatMode ? "Switch to Chat" : "Cleared for takeoff — " + modeLabel);
|
|
662
1001
|
|
|
1002
|
+
// CLAUDE-028: the record must exist before the button does, so the
|
|
1003
|
+
// button can carry data-fp-record-id/-action — the same relay
|
|
1004
|
+
// markers renderAskUserQuestion's quick-reply buttons use to
|
|
1005
|
+
// survive the pop-out's innerHTML clone (bindRecordActionButtons
|
|
1006
|
+
// in init.js rebinds by these attributes and relays the click back
|
|
1007
|
+
// to this window via resolveRecordAction; this window is the only
|
|
1008
|
+
// one with live suggestedAction data to act on). The direct click
|
|
1009
|
+
// handler below still fires normally for the un-popped-out sidebar.
|
|
1010
|
+
var rec = addRecord("chip", { chipType: "suggestedAction", suggestedAction: suggestedAction });
|
|
1011
|
+
|
|
663
1012
|
var $row = $("<div>").addClass("fp-chip-row");
|
|
664
1013
|
var $card = $("<button>")
|
|
665
1014
|
.addClass("fp-chip fp-chip-card")
|
|
666
1015
|
.attr("type", "button")
|
|
667
1016
|
.attr("title", suggestedAction.prompt)
|
|
1017
|
+
.attr("data-fp-record-id", rec.id)
|
|
1018
|
+
.attr("data-fp-record-action", "apply-suggested-action")
|
|
668
1019
|
.on("click", function () { applySuggestedAction(suggestedAction); });
|
|
669
1020
|
$("<span>").addClass("fp-chip-icon")
|
|
670
1021
|
.append($("<i>").addClass(isChatMode ? "fa fa-comment" : "fa fa-paper-plane"))
|
|
@@ -675,12 +1026,27 @@
|
|
|
675
1026
|
$("<span>").addClass("fp-chip-go").html("›").appendTo($card);
|
|
676
1027
|
$card.appendTo($row);
|
|
677
1028
|
|
|
678
|
-
|
|
679
|
-
|
|
1029
|
+
// CLAUDE-030: Document mode always requires a real, non-empty
|
|
1030
|
+
// selection at Send time (server-side: describeSelectionContext
|
|
1031
|
+
// returns null and /flowpilot/document 400s on "Select the node(s)
|
|
1032
|
+
// you want documented first" whenever nothing is actually selected)
|
|
1033
|
+
// — but the model has occasionally generated a selectionHint
|
|
1034
|
+
// implying otherwise (e.g. "leave nothing selected to document...
|
|
1035
|
+
// general Node-RED info") when it had no concrete node to name.
|
|
1036
|
+
// Rather than re-word the model's already-fragile mode-mismatch
|
|
1037
|
+
// prompt guidance under time pressure, override deterministically
|
|
1038
|
+
// here: if this chip targets Document and doesn't carry a real
|
|
1039
|
+
// resolved target, always show the one hint that's actually true.
|
|
1040
|
+
var targetResolved = suggestedAction.targetNodeIds === "all" ||
|
|
1041
|
+
(Array.isArray(suggestedAction.targetNodeIds) && suggestedAction.targetNodeIds.length > 0);
|
|
1042
|
+
var hintText = (suggestedAction.mode === "document" && !targetResolved)
|
|
1043
|
+
? "Select the node(s) you want documented first."
|
|
1044
|
+
: suggestedAction.selectionHint;
|
|
1045
|
+
if (hintText) {
|
|
1046
|
+
$("<div>").addClass("fp-chip-hint").text("Tip: " + hintText).appendTo($row);
|
|
680
1047
|
}
|
|
681
1048
|
|
|
682
1049
|
$box.append($row);
|
|
683
|
-
addRecord("chip", { chipType: "suggestedAction", suggestedAction: suggestedAction });
|
|
684
1050
|
scrollMessagesToBottom();
|
|
685
1051
|
}
|
|
686
1052
|
|
|
@@ -772,6 +1138,28 @@
|
|
|
772
1138
|
scrollMessagesToBottom();
|
|
773
1139
|
}
|
|
774
1140
|
|
|
1141
|
+
// Detects the model's own tool-call envelope leaking through as
|
|
1142
|
+
// "prose" — e.g. a vague first-turn Modify reply the server marked
|
|
1143
|
+
// prose:true but whose explanation is actually the raw
|
|
1144
|
+
// {"explanation":...,"changes":[...]} JSON as literal text. Starting
|
|
1145
|
+
// with "{" alone doesn't make text suspect (real prose can open with
|
|
1146
|
+
// a brace), so this also requires it to parse as an object carrying
|
|
1147
|
+
// one of FlowPilot's own envelope keys.
|
|
1148
|
+
function looksLikeToolEnvelope(text) {
|
|
1149
|
+
if (typeof text !== "string") { return false; }
|
|
1150
|
+
var trimmed = text.trim();
|
|
1151
|
+
if (trimmed.charAt(0) !== "{") { return false; }
|
|
1152
|
+
var parsed;
|
|
1153
|
+
try {
|
|
1154
|
+
parsed = JSON.parse(trimmed);
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return false; }
|
|
1159
|
+
return ["changes", "newNodes", "newWires", "removeNodes", "newGroups", "explanation"]
|
|
1160
|
+
.some(function (key) { return Object.prototype.hasOwnProperty.call(parsed, key); });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
775
1163
|
// Shared by Generate/Document/Modify result handlers: renders the model's
|
|
776
1164
|
// clarifying-question or prose-only envelope as a normal assistant
|
|
777
1165
|
// message and leaves the action armed for a follow-up. Returns true if it
|
|
@@ -789,6 +1177,11 @@
|
|
|
789
1177
|
return true;
|
|
790
1178
|
}
|
|
791
1179
|
if (data.prose) {
|
|
1180
|
+
if (looksLikeToolEnvelope(data.explanation)) {
|
|
1181
|
+
handleExecuteError("FlowPilot's reply didn't come through as expected.", data.explanation);
|
|
1182
|
+
updateSelectionStatus();
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
792
1185
|
addMessage("assistant", data.explanation || "(no content returned)");
|
|
793
1186
|
pushHistory("assistant", data.explanation || "");
|
|
794
1187
|
renderActionChip(data.suggestedAction);
|
|
@@ -812,15 +1205,20 @@
|
|
|
812
1205
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
813
1206
|
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
814
1207
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
815
|
-
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1208
|
+
// B1: bake the deploy-verify option into the review panel as the
|
|
1209
|
+
// primary chip rather than a separate chip below it. Only for
|
|
1210
|
+
// executable flows (not documentation-only comment nodes) and when no
|
|
1211
|
+
// loop is already active. The secondary "Just add to canvas" button is
|
|
1212
|
+
// always shown alongside it as an escape hatch.
|
|
1213
|
+
var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
1214
|
+
var _buildOnImported = (goalPrompt && !activeBuildLoop && _hasDeployable)
|
|
1215
|
+
? function (importResult) { startBuildLoop(goalPrompt, flow, importResult); }
|
|
1216
|
+
: null;
|
|
1217
|
+
addGeneratedReview(flow, _buildOnImported, _buildOnImported ? goalPrompt : null);
|
|
1218
|
+
// Suppress a server-suggested build chip when deploy-verify is already
|
|
1219
|
+
// the primary action inside the review panel — it would be a duplicate.
|
|
1220
|
+
renderActionChip(_buildOnImported && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
1221
|
+
? null : data.suggestedAction);
|
|
824
1222
|
setBusy(false);
|
|
825
1223
|
updateSelectionStatus();
|
|
826
1224
|
}
|
|
@@ -831,10 +1229,94 @@
|
|
|
831
1229
|
hidePending();
|
|
832
1230
|
if (renderQuestionOrProse(data)) { return; }
|
|
833
1231
|
|
|
1232
|
+
// W4: parse and surface the Plan: block if present.
|
|
1233
|
+
var planItems = parseTodoPlan(data.explanation || "");
|
|
1234
|
+
var todoRec = null;
|
|
1235
|
+
// W7: real per-item correlation, and (CLAUDE-011) the signal that
|
|
1236
|
+
// real mutations already happened this turn via WRITE tool calls
|
|
1237
|
+
// (apply_step/remove_step/rename_node/group_nodes) — each already
|
|
1238
|
+
// consent-gated (or, for group_nodes' write-safe tier, always
|
|
1239
|
+
// auto-applied) and applied through its own review chip. Hoisted
|
|
1240
|
+
// above the planItems.length check (rather than declared inside
|
|
1241
|
+
// it) so it's always defined below even on a turn with no Plan:
|
|
1242
|
+
// block, e.g. a wrap-up-only final response after tool calls.
|
|
1243
|
+
var writeResults = Array.isArray(data._agentWriteResults) ? data._agentWriteResults : [];
|
|
1244
|
+
if (planItems.length) {
|
|
1245
|
+
// Each WRITE tool call this turn resolves ONE plan item, in
|
|
1246
|
+
// order (§16 point 2 — "one WRITE tool call = one plan/todo
|
|
1247
|
+
// item"). This is the deferred half of CLAUDE-005 (c6c2e84),
|
|
1248
|
+
// now unblocked: that fix could only mark the WHOLE plan
|
|
1249
|
+
// active/resolved together because there was no way to know
|
|
1250
|
+
// which check proved which numbered line — a per-call tool
|
|
1251
|
+
// result finally gives that mapping for free. Items beyond the
|
|
1252
|
+
// resolved-call count (or ALL items, when no WRITE tool call
|
|
1253
|
+
// was made this turn — the default, non-agentic-write path)
|
|
1254
|
+
// fall back to CLAUDE-005's original all-together marking
|
|
1255
|
+
// below, resolved via the ordinary aggregate verifySteps
|
|
1256
|
+
// envelope path exactly as before — confirmed unchanged when
|
|
1257
|
+
// _agentWriteResults is empty/absent.
|
|
1258
|
+
if (writeResults.length) {
|
|
1259
|
+
planItems.forEach(function (item, i) {
|
|
1260
|
+
item.status = (i < writeResults.length)
|
|
1261
|
+
? (writeResults[i].allPass ? "done" : "failed")
|
|
1262
|
+
: "active";
|
|
1263
|
+
});
|
|
1264
|
+
} else {
|
|
1265
|
+
// Verification only produces one aggregate pass/fail result
|
|
1266
|
+
// for the whole Modify response — mark every item active up
|
|
1267
|
+
// front so they all resolve together instead of leaving
|
|
1268
|
+
// items 2+ stuck at "pending" forever (only item 1 would
|
|
1269
|
+
// ever flip otherwise).
|
|
1270
|
+
planItems.forEach(function (item) { item.status = "active"; });
|
|
1271
|
+
}
|
|
1272
|
+
// P10-D2: if this run already has a live "todo" record from
|
|
1273
|
+
// WRITE-tool events (runAgentLoop, modes.js), upgrade it in
|
|
1274
|
+
// place with the real Plan: items instead of creating a
|
|
1275
|
+
// second record for the same run — so a run that paused
|
|
1276
|
+
// mid-flight (refresh, interrupted) and one that completed
|
|
1277
|
+
// normally both end up as ONE record with both events and
|
|
1278
|
+
// items.
|
|
1279
|
+
if (data._agentRunRecord) {
|
|
1280
|
+
todoRec = data._agentRunRecord;
|
|
1281
|
+
todoRec.action = "modify";
|
|
1282
|
+
todoRec.items = planItems;
|
|
1283
|
+
} else {
|
|
1284
|
+
todoRec = addRecord("todo", { action: "modify", items: planItems });
|
|
1285
|
+
}
|
|
1286
|
+
rerenderTodoRecord(todoRec);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
834
1289
|
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
835
1290
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
836
1291
|
if (data.skippedNote) { addMessage("assistant", "⚠ " + data.skippedNote); }
|
|
837
|
-
|
|
1292
|
+
|
|
1293
|
+
// W4 Phase 2: wrap apply to run a real graph read-back via
|
|
1294
|
+
// verifySteps (Track A, server) instead of unconditionally marking
|
|
1295
|
+
// the todo done. Falls back to the Phase 1 behavior (check off with
|
|
1296
|
+
// no verification) when the server sent no verifySteps.
|
|
1297
|
+
var verifySteps = Array.isArray(data.verifySteps) ? data.verifySteps : [];
|
|
1298
|
+
var applyCallback = todoRec ? function(nodeDiffs, removeNodes, $btn, idMap) {
|
|
1299
|
+
applyModifications(nodeDiffs, removeNodes, $btn, idMap);
|
|
1300
|
+
if (verifySteps.length) {
|
|
1301
|
+
verifyModifySteps(verifySteps, idMap, todoRec);
|
|
1302
|
+
} else {
|
|
1303
|
+
todoRec.items.forEach(function(item) {
|
|
1304
|
+
if (item.status === "active") { item.status = "done"; }
|
|
1305
|
+
});
|
|
1306
|
+
rerenderTodoRecord(todoRec);
|
|
1307
|
+
}
|
|
1308
|
+
} : applyModifications;
|
|
1309
|
+
|
|
1310
|
+
// CLAUDE-011: agentWriteRules tells the model to omit changes/
|
|
1311
|
+
// newNodes/newWires/removeNodes/newGroups on this final response
|
|
1312
|
+
// when writeResults is non-empty, so already-applied work isn't
|
|
1313
|
+
// proposed a second time — but the model doesn't always comply.
|
|
1314
|
+
// Don't rely on the prompt alone: skip rendering a second review/
|
|
1315
|
+
// apply flow whenever real WRITE-tool work happened this turn,
|
|
1316
|
+
// regardless of what the final envelope contains.
|
|
1317
|
+
if (!writeResults.length) {
|
|
1318
|
+
addModifyReview(data.flow, data.newNodes || [], data.newWires || [], data.removeNodes || [], applyCallback, null, data.newGroups || []);
|
|
1319
|
+
}
|
|
838
1320
|
renderActionChip(data.suggestedAction);
|
|
839
1321
|
setBusy(false);
|
|
840
1322
|
updateSelectionStatus();
|
|
@@ -1011,7 +1493,9 @@
|
|
|
1011
1493
|
var payload = {
|
|
1012
1494
|
prompt: prompt, context: context,
|
|
1013
1495
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1014
|
-
conversationId: conversationId
|
|
1496
|
+
conversationId: conversationId,
|
|
1497
|
+
strategy: "classic",
|
|
1498
|
+
entry: endpointName
|
|
1015
1499
|
};
|
|
1016
1500
|
|
|
1017
1501
|
function onError(msg, xhr) {
|
|
@@ -1055,8 +1539,329 @@
|
|
|
1055
1539
|
ajaxJson("POST", fullEndpoint, payload, wrappedOnResult, onError);
|
|
1056
1540
|
}
|
|
1057
1541
|
|
|
1542
|
+
// W4: parse a "Plan:" block from the model's explanation field.
|
|
1543
|
+
// Returns an array of { text, status } items, or [] if none found.
|
|
1544
|
+
// Each numbered/bulleted line under "Plan:" up to the first blank line
|
|
1545
|
+
// becomes one item. Status starts as "pending" for all items — the
|
|
1546
|
+
// caller sets the first to "active" before rendering.
|
|
1547
|
+
function parseTodoPlan(explanation) {
|
|
1548
|
+
if (!explanation || typeof explanation !== "string") { return []; }
|
|
1549
|
+
var planStart = explanation.indexOf("Plan:");
|
|
1550
|
+
if (planStart === -1) { return []; }
|
|
1551
|
+
var afterPlan = explanation.slice(planStart + 5);
|
|
1552
|
+
var planBlock = afterPlan.split(/\n\n/)[0];
|
|
1553
|
+
var lines = planBlock.split("\n");
|
|
1554
|
+
var items = [];
|
|
1555
|
+
lines.forEach(function (line) {
|
|
1556
|
+
var stripped = line.replace(/^\s*\d+[.):\s]+/, "").replace(/^\s*[-*]\s+/, "").trim();
|
|
1557
|
+
if (stripped) { items.push({ text: stripped, status: "pending" }); }
|
|
1558
|
+
});
|
|
1559
|
+
return items;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// W4: render or re-render a "todo" record. For a 1-item plan, renders
|
|
1563
|
+
// as a compact status line (one chip). For N>1 items, renders as a
|
|
1564
|
+
// checklist card. Updates in place when the record already has a
|
|
1565
|
+
// data-fp-todo-id element in the message box (e.g. on verify check-off).
|
|
1566
|
+
function rerenderTodoRecord(rec) {
|
|
1567
|
+
if (!rec || !rec.items) { return; }
|
|
1568
|
+
var $box = el("#fp-messages");
|
|
1569
|
+
if (!$box.length) { return; }
|
|
1570
|
+
var items = rec.items;
|
|
1571
|
+
var $existing = $box.find("[data-fp-todo-id='" + rec.id + "']");
|
|
1572
|
+
|
|
1573
|
+
var $wrap;
|
|
1574
|
+
if (items.length === 1) {
|
|
1575
|
+
var item = items[0];
|
|
1576
|
+
var icon = item.status === "done" ? "✓" : item.status === "failed" ? "✗" : "▶";
|
|
1577
|
+
$wrap = $("<div>")
|
|
1578
|
+
.addClass("fp-todo-status fp-todo-" + item.status)
|
|
1579
|
+
.attr("data-fp-todo-id", rec.id)
|
|
1580
|
+
.text(icon + " " + item.text);
|
|
1581
|
+
} else {
|
|
1582
|
+
$wrap = $("<div>")
|
|
1583
|
+
.addClass("fp-todo-card")
|
|
1584
|
+
.attr("data-fp-todo-id", rec.id);
|
|
1585
|
+
var $ul = $("<ul>").addClass("fp-todo-list");
|
|
1586
|
+
items.forEach(function (item) {
|
|
1587
|
+
var icon = item.status === "done" ? "✓" :
|
|
1588
|
+
item.status === "failed" ? "✗" :
|
|
1589
|
+
item.status === "active" ? "▶" : "○";
|
|
1590
|
+
$("<li>")
|
|
1591
|
+
.addClass("fp-todo-item fp-todo-item-" + item.status)
|
|
1592
|
+
.text(icon + " " + item.text)
|
|
1593
|
+
.appendTo($ul);
|
|
1594
|
+
});
|
|
1595
|
+
$wrap.append($ul);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// P10-D2 (ADR-001 R5): a run whose last recorded event isn't "done"
|
|
1599
|
+
// was abandoned mid-flight — an explicit interruption (stop button,
|
|
1600
|
+
// step/token budget) or an orphaned request that never returned.
|
|
1601
|
+
// Freeze whatever checklist state exists (possibly none yet, if
|
|
1602
|
+
// interrupted before any Plan: text arrived) and say so plainly,
|
|
1603
|
+
// replacing the old silent-death behavior ("Continuing without
|
|
1604
|
+
// tools…" then nothing). "step N" counts completed (applied) WRITE
|
|
1605
|
+
// tool calls, not raw round-trips.
|
|
1606
|
+
if (Array.isArray(rec.events) && rec.events.length) {
|
|
1607
|
+
var lastEvent = rec.events[rec.events.length - 1];
|
|
1608
|
+
if (lastEvent.t !== "done") {
|
|
1609
|
+
var appliedCount = rec.events.filter(function (e) { return e.t === "applied"; }).length;
|
|
1610
|
+
$("<div>").addClass("fp-todo-interrupted").text(
|
|
1611
|
+
"⚠ This run was interrupted after step " + appliedCount +
|
|
1612
|
+
" — completed steps are applied (Ctrl+Z to undo). Re-send to continue from here."
|
|
1613
|
+
).appendTo($wrap);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if ($existing.length) {
|
|
1618
|
+
$existing.replaceWith($wrap);
|
|
1619
|
+
} else {
|
|
1620
|
+
$box.append($wrap);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Step-queue path for Generate (opt-in via enableStepQueue setting).
|
|
1625
|
+
// After the user clicks "Add to workspace", performs a synchronous graph
|
|
1626
|
+
// read-back — calls RED.nodes.node(id) for every node that landed on the
|
|
1627
|
+
// canvas — and surfaces the result as a verification notice. This is the
|
|
1628
|
+
// structural verification step for Generate: "did the import actually land?"
|
|
1629
|
+
// not "does it do the thing?" (that's the semantic loop, Build's domain).
|
|
1630
|
+
// Skips comment and group nodes — those aren't addressable via RED.nodes.node.
|
|
1631
|
+
// todoRec: optional todo record to check off (or fail) after verification.
|
|
1632
|
+
function verifyImportedNodes(importResult, todoRec) {
|
|
1633
|
+
if (!importResult || !importResult.nodeMap) { return; }
|
|
1634
|
+
var nodeMap = importResult.nodeMap;
|
|
1635
|
+
var total = 0, found = 0, missing = [];
|
|
1636
|
+
// Config nodes (e.g. an http-request's TLS config, an mqtt broker
|
|
1637
|
+
// config) ride along in nodeMap whenever the model's own `flow`
|
|
1638
|
+
// array included them, but they aren't part of what the user asked
|
|
1639
|
+
// for — RED.nodes.node() resolves them same as regular nodes (it
|
|
1640
|
+
// checks configNodes[id] before falling back), so left uncounted
|
|
1641
|
+
// they'd silently inflate the headline total (e.g. "8" instead of
|
|
1642
|
+
// "5"). Track and verify them separately instead.
|
|
1643
|
+
var configTotal = 0, configFound = 0;
|
|
1644
|
+
// Node-RED's own RED.nodes.import (the generateIds:true path used
|
|
1645
|
+
// for every Generate/Build import) keys nodeMap TWICE per imported
|
|
1646
|
+
// node: once under the model's own placeholder id (assigned while
|
|
1647
|
+
// constructing the node, before it's added to the live registry)
|
|
1648
|
+
// and again under the freshly-generated real editor id (assigned in
|
|
1649
|
+
// the final addNode/addGroup/addJunction registration loop) — both
|
|
1650
|
+
// entries point to the same live node object. Left undeduped this
|
|
1651
|
+
// doubles every count here (visible AND config alike), independent
|
|
1652
|
+
// of the config/visible split above. Confirmed by reading
|
|
1653
|
+
// @node-red/editor-client/public/red/red.js's importNodes directly.
|
|
1654
|
+
var seenLiveIds = {};
|
|
1655
|
+
Object.keys(nodeMap).forEach(function (pid) {
|
|
1656
|
+
var liveNode = nodeMap[pid];
|
|
1657
|
+
if (!liveNode || !liveNode.id) { return; }
|
|
1658
|
+
if (seenLiveIds[liveNode.id]) { return; }
|
|
1659
|
+
seenLiveIds[liveNode.id] = true;
|
|
1660
|
+
if (liveNode.type === "comment" || liveNode.type === "group") { return; }
|
|
1661
|
+
if (liveNode._def && liveNode._def.category === "config") {
|
|
1662
|
+
configTotal++;
|
|
1663
|
+
if (nodeExists(liveNode.id)) { configFound++; }
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
total++;
|
|
1667
|
+
if (nodeExists(liveNode.id)) {
|
|
1668
|
+
found++;
|
|
1669
|
+
} else {
|
|
1670
|
+
missing.push(liveNode.type || pid);
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
var configMissing = configTotal - configFound;
|
|
1674
|
+
var allGood = total > 0 && missing.length === 0 && configMissing === 0;
|
|
1675
|
+
if (total === 0) {
|
|
1676
|
+
// Nothing user-visible to verify (only comments/groups/config
|
|
1677
|
+
// nodes) — skip notice.
|
|
1678
|
+
} else if (allGood) {
|
|
1679
|
+
var configSuffix = configTotal > 0
|
|
1680
|
+
? " (+" + configTotal + " supporting config node(s))"
|
|
1681
|
+
: "";
|
|
1682
|
+
addMessage("fp-notice", "✓ Verified: all " + found + " node(s) confirmed on canvas" + configSuffix + ".");
|
|
1683
|
+
} else {
|
|
1684
|
+
var allMissing = missing.slice();
|
|
1685
|
+
if (configMissing > 0) { allMissing.push(configMissing + " config node(s)"); }
|
|
1686
|
+
addMessage("fp-notice", "⚠ Verification: " + found + "/" + total + " node(s) on canvas — " +
|
|
1687
|
+
allMissing.length + " not found after import (" + allMissing.join(", ") + "). " +
|
|
1688
|
+
"These may be uninstalled node types that were silently dropped.");
|
|
1689
|
+
}
|
|
1690
|
+
// Check off (or fail) the active todo item.
|
|
1691
|
+
if (todoRec && todoRec.items) {
|
|
1692
|
+
todoRec.items.forEach(function (item) {
|
|
1693
|
+
if (item.status === "active") { item.status = allGood ? "done" : "failed"; }
|
|
1694
|
+
});
|
|
1695
|
+
rerenderTodoRecord(todoRec);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// W4 Phase 2: real graph read-back verification for Modify, using the
|
|
1700
|
+
// server-derived verifySteps (Track A — property/exists/absent/wire
|
|
1701
|
+
// checks; see finalizeModifyResult in flowpilot.js). Mirrors
|
|
1702
|
+
// verifyImportedNodes's design for Generate: aggregate pass/fail across
|
|
1703
|
+
// all steps, surface one notice, and check off (or fail) the active
|
|
1704
|
+
// todo item. idMap resolves the response-time placeholder ids that
|
|
1705
|
+
// existence/wire checks on newly-inserted nodes carry (the server can't
|
|
1706
|
+
// know the browser-assigned id at response time — applyInsertions
|
|
1707
|
+
// assigns it and returns idMap). Property/absent checks already use
|
|
1708
|
+
// real existing-node ids, so idMap[id] simply misses and falls through
|
|
1709
|
+
// to the id unchanged.
|
|
1710
|
+
// One check-vocabulary evaluation (property/exists/absent/wire), shared
|
|
1711
|
+
// by verifyModifySteps (aggregate Modify verification) and W7's
|
|
1712
|
+
// per-tool-call check results (runChecksForToolResult, main.js) — same
|
|
1713
|
+
// approach, applied either to the whole verifySteps batch or scoped to
|
|
1714
|
+
// one WRITE tool call's own touched id(s). Returns null for an
|
|
1715
|
+
// unrecognized check type (caller should skip it, not count it either
|
|
1716
|
+
// way) or { ok, label } where label is the human-readable failure
|
|
1717
|
+
// description used in the aggregate "did not land as expected" message.
|
|
1718
|
+
// P10-E: each case delegates to graph-truth.js (same closure) — the
|
|
1719
|
+
// one implementation of graph truth, per ADR-005. Wire checks in
|
|
1720
|
+
// particular must never fall back to reading node.wires: RED.nodes.
|
|
1721
|
+
// addLink/removeLink never re-sync a live node's own .wires array
|
|
1722
|
+
// mid-session, so it's stale for anything added/removed this session.
|
|
1723
|
+
function runSingleVerifyCheck(step, idMap) {
|
|
1724
|
+
idMap = idMap || {};
|
|
1725
|
+
function resolve(id) { return (idMap && idMap[id]) || id; }
|
|
1726
|
+
switch (step.check) {
|
|
1727
|
+
case "property": {
|
|
1728
|
+
var okP = propertyEquals(resolve(step.nodeId), step.prop, step.expected);
|
|
1729
|
+
var labelP = (step.prop || "property") + " on " + step.nodeId;
|
|
1730
|
+
if (!okP) {
|
|
1731
|
+
var readP = readProperty(resolve(step.nodeId), step.prop);
|
|
1732
|
+
labelP += readP.exists ?
|
|
1733
|
+
" (expected " + JSON.stringify(step.expected) + ", found " + JSON.stringify(readP.value) + ")" :
|
|
1734
|
+
" (node does not exist)";
|
|
1735
|
+
}
|
|
1736
|
+
return { ok: okP, label: labelP };
|
|
1737
|
+
}
|
|
1738
|
+
case "exists": {
|
|
1739
|
+
var okE = nodeExists(resolve(step.nodeId));
|
|
1740
|
+
return { ok: okE, label: step.nodeId + " missing" };
|
|
1741
|
+
}
|
|
1742
|
+
case "absent": {
|
|
1743
|
+
var okA = nodeAbsent(resolve(step.nodeId));
|
|
1744
|
+
return { ok: okA, label: step.nodeId + " still present" };
|
|
1745
|
+
}
|
|
1746
|
+
case "wire": {
|
|
1747
|
+
var fromId = resolve(step.fromId);
|
|
1748
|
+
var toId = resolve(step.toId);
|
|
1749
|
+
var port = step.fromPort || 0;
|
|
1750
|
+
var okW = wireExists(fromId, port, toId);
|
|
1751
|
+
var labelW = "wire " + step.fromId + " → " + step.toId;
|
|
1752
|
+
if (!okW) {
|
|
1753
|
+
var fromExists = nodeExists(fromId);
|
|
1754
|
+
var toExists = nodeExists(toId);
|
|
1755
|
+
if (!fromExists && !toExists) {
|
|
1756
|
+
labelW += " (neither node was ever created)";
|
|
1757
|
+
} else if (!fromExists) {
|
|
1758
|
+
labelW += " (" + step.fromId + " was never created)";
|
|
1759
|
+
} else if (!toExists) {
|
|
1760
|
+
labelW += " (" + step.toId + " was never created)";
|
|
1761
|
+
} else {
|
|
1762
|
+
labelW += " (both nodes exist but aren't connected)";
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return { ok: okW, label: labelW };
|
|
1766
|
+
}
|
|
1767
|
+
default:
|
|
1768
|
+
return null; // unrecognized check type — don't count it either way
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function verifyModifySteps(verifySteps, idMap, todoRec) {
|
|
1773
|
+
if (!Array.isArray(verifySteps) || !verifySteps.length) { return; }
|
|
1774
|
+
|
|
1775
|
+
var total = 0, passed = 0, failures = [], failedSteps = [];
|
|
1776
|
+
verifySteps.forEach(function (step) {
|
|
1777
|
+
var result = runSingleVerifyCheck(step, idMap);
|
|
1778
|
+
if (!result) { return; }
|
|
1779
|
+
total++;
|
|
1780
|
+
if (result.ok) { passed++; } else { failures.push(result.label); failedSteps.push(step); }
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
if (total === 0) { return; }
|
|
1784
|
+
var allGood = failures.length === 0;
|
|
1785
|
+
if (allGood) {
|
|
1786
|
+
addMessage("fp-notice", "✓ Verified: all " + passed + " change(s) confirmed on canvas.");
|
|
1787
|
+
} else {
|
|
1788
|
+
addMessage("fp-notice", "⚠ Verification: " + passed + "/" + total + " change(s) confirmed — " +
|
|
1789
|
+
failures.length + " did not land as expected (" + failures.join(", ") + ").");
|
|
1790
|
+
// CLAUDE-016: offer a "Fix this" chip naming exactly the nodes
|
|
1791
|
+
// involved in the failed checks, reusing CLAUDE-015/CODEX-014's
|
|
1792
|
+
// targetNodeIds auto-select mechanism rather than a new
|
|
1793
|
+
// retry/loop. User-initiated only — the chip pre-fills a Modify
|
|
1794
|
+
// prompt but still requires the user to review and click Send,
|
|
1795
|
+
// same as every other suggestedAction chip.
|
|
1796
|
+
function resolve(id) { return (idMap && idMap[id]) || id; }
|
|
1797
|
+
var targetNodeIds = [];
|
|
1798
|
+
failedSteps.forEach(function (step) {
|
|
1799
|
+
var ids = step.check === "wire" ? [resolve(step.fromId), resolve(step.toId)] : [resolve(step.nodeId)];
|
|
1800
|
+
ids.forEach(function (id) {
|
|
1801
|
+
if (id && targetNodeIds.indexOf(id) === -1) { targetNodeIds.push(id); }
|
|
1802
|
+
});
|
|
1803
|
+
});
|
|
1804
|
+
renderActionChip({
|
|
1805
|
+
mode: "modify",
|
|
1806
|
+
prompt: "Fix the following change(s) that didn't land as expected: " + failures.join(", "),
|
|
1807
|
+
targetNodeIds: targetNodeIds
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
if (todoRec && todoRec.items) {
|
|
1811
|
+
todoRec.items.forEach(function (item) {
|
|
1812
|
+
if (item.status === "active") { item.status = allGood ? "done" : "failed"; }
|
|
1813
|
+
});
|
|
1814
|
+
rerenderTodoRecord(todoRec);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function handleStepQueueGenerateResult(data, goalPrompt) {
|
|
1819
|
+
hidePending();
|
|
1820
|
+
if (renderQuestionOrProse(data)) { return; }
|
|
1821
|
+
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1822
|
+
|
|
1823
|
+
// Build the todo plan. Parse "Plan:" from explanation if present;
|
|
1824
|
+
// fall back to an implicit single item from the goal prompt.
|
|
1825
|
+
var planItems = parseTodoPlan(data.explanation || "");
|
|
1826
|
+
if (!planItems.length) {
|
|
1827
|
+
planItems = [{ text: goalPrompt || "Generate flow", status: "pending" }];
|
|
1828
|
+
}
|
|
1829
|
+
// Same aggregate-verification reasoning as the Modify path: mark
|
|
1830
|
+
// every item active up front so a multi-item plan resolves together.
|
|
1831
|
+
planItems.forEach(function (item) { item.status = "active"; });
|
|
1832
|
+
var todoRec = addRecord("todo", { action: "generate", items: planItems });
|
|
1833
|
+
rerenderTodoRecord(todoRec);
|
|
1834
|
+
|
|
1835
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1836
|
+
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1837
|
+
// B1: when a build loop is appropriate, bake deploy-verify into the
|
|
1838
|
+
// primary chip (same as handleSimpleGenerationResult). The callback
|
|
1839
|
+
// also runs verifyImportedNodes so the todo record still gets checked
|
|
1840
|
+
// off. Without a build loop, fall back to a plain "Add to canvas"
|
|
1841
|
+
// button that still fires the verify callback.
|
|
1842
|
+
var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
|
|
1843
|
+
var _wantLoop = goalPrompt && !activeBuildLoop && _hasDeployable;
|
|
1844
|
+
var _onImported = _wantLoop
|
|
1845
|
+
? function (importResult) {
|
|
1846
|
+
verifyImportedNodes(importResult, todoRec);
|
|
1847
|
+
startBuildLoop(goalPrompt, flow, importResult);
|
|
1848
|
+
}
|
|
1849
|
+
: function (importResult) { verifyImportedNodes(importResult, todoRec); };
|
|
1850
|
+
addGeneratedReview(flow, _onImported, _wantLoop ? goalPrompt : null);
|
|
1851
|
+
// Suppress a server-suggested build chip when deploy-verify is already
|
|
1852
|
+
// the primary action inside the review panel.
|
|
1853
|
+
renderActionChip(_wantLoop && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
1854
|
+
? null : data.suggestedAction);
|
|
1855
|
+
setBusy(false);
|
|
1856
|
+
updateSelectionStatus();
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1058
1859
|
function generate() {
|
|
1059
|
-
|
|
1860
|
+
if (currentSettings.enableStepQueue) {
|
|
1861
|
+
runGenerateLikeAction("generate", "generate", "Generate: ", handleStepQueueGenerateResult);
|
|
1862
|
+
} else {
|
|
1863
|
+
runGenerateLikeAction("generate", "generate", "Generate: ", handleSimpleGenerationResult);
|
|
1864
|
+
}
|
|
1060
1865
|
}
|
|
1061
1866
|
|
|
1062
1867
|
// /build's first step. Reuses Generate's pipeline wholesale for the
|
|
@@ -1110,7 +1915,9 @@
|
|
|
1110
1915
|
var payload = {
|
|
1111
1916
|
prompt: instruction, context: context,
|
|
1112
1917
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1113
|
-
conversationId: conversationId
|
|
1918
|
+
conversationId: conversationId,
|
|
1919
|
+
strategy: "classic",
|
|
1920
|
+
entry: "build-existing"
|
|
1114
1921
|
};
|
|
1115
1922
|
|
|
1116
1923
|
function onBuildExistingError(msg, xhr) {
|
|
@@ -1165,6 +1972,334 @@
|
|
|
1165
1972
|
updateSelectionStatus();
|
|
1166
1973
|
}
|
|
1167
1974
|
|
|
1975
|
+
// WS4: real consent gate for side-effecting build steps. Renders one
|
|
1976
|
+
// combined chip covering every side-effecting node this step's
|
|
1977
|
+
// classification found (classifyFlowNodes in flowpilot.js, server-side)
|
|
1978
|
+
// — a single decision rather than per-node chips, which is sufficient
|
|
1979
|
+
// because only the FIRST /flowpilot/build response carries
|
|
1980
|
+
// stepNodeClasses today (fix iterations via /flowpilot/modify don't, so
|
|
1981
|
+
// there's exactly one consent point per loop lifetime under the current
|
|
1982
|
+
// limitation — see the handleBuildResult call site).
|
|
1983
|
+
//
|
|
1984
|
+
// Reconstructed entirely from `src` (plain data, never a live closure)
|
|
1985
|
+
// on every call — the initial render from handleBuildResult AND every
|
|
1986
|
+
// later rerender (refresh, pop-out reopen, the W0A idle/focus
|
|
1987
|
+
// auto-refresh) via rerenderRecord's buildConsentGate branch below.
|
|
1988
|
+
// Mirrors rerenderReviewRecord's pattern in apply-review.js: a fresh
|
|
1989
|
+
// record is added each time from the source's stored fields (including
|
|
1990
|
+
// `decision`, once made), rather than relying on an in-memory callback
|
|
1991
|
+
// surviving a refresh. That in-memory-callback version is exactly what
|
|
1992
|
+
// broke before this fix — a refresh mid-decision fell through to
|
|
1993
|
+
// renderClarifyingQuestion's generic path, whose buttons send the
|
|
1994
|
+
// clicked label as a new chat message instead of resolving Proceed/Skip,
|
|
1995
|
+
// permanently stranding the loop.
|
|
1996
|
+
//
|
|
1997
|
+
// src fields: sideEffecting, flow, goalPrompt, fpUidManifest,
|
|
1998
|
+
// suggestedAction — everything runBuildConsentDecision needs — plus,
|
|
1999
|
+
// once resolved, `decision` ("proceed"|"skip") so a later rerender shows
|
|
2000
|
+
// a settled state instead of re-offering an already-made choice.
|
|
2001
|
+
function renderBuildConsentGate(src) {
|
|
2002
|
+
var $box = el("#fp-messages");
|
|
2003
|
+
if (!$box.length) {
|
|
2004
|
+
if (!src.decision) { runBuildConsentDecision(src, true); }
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
var _rec = addRecord("question", {
|
|
2009
|
+
buildConsentGate: true,
|
|
2010
|
+
options: ["Auto-verify", "I'll check myself"],
|
|
2011
|
+
sideEffecting: src.sideEffecting,
|
|
2012
|
+
flow: src.flow,
|
|
2013
|
+
goalPrompt: src.goalPrompt,
|
|
2014
|
+
fpUidManifest: src.fpUidManifest,
|
|
2015
|
+
suggestedAction: src.suggestedAction,
|
|
2016
|
+
decision: src.decision
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
var sideEffecting = Array.isArray(_rec.sideEffecting) ? _rec.sideEffecting : [];
|
|
2020
|
+
var labels = sideEffecting.map(function (n) { return n.name || n.type; }).join(", ");
|
|
2021
|
+
|
|
2022
|
+
if (_rec.decision) {
|
|
2023
|
+
// Already resolved before this render (e.g. resolved earlier in
|
|
2024
|
+
// the session, now showing again after a refresh) — settled
|
|
2025
|
+
// state, not an interactive choice.
|
|
2026
|
+
addMessage("assistant", "This step calls an external service: " + labels + ".");
|
|
2027
|
+
var $settledRow = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2028
|
+
$("<button>")
|
|
2029
|
+
.addClass("fp-consent-chip")
|
|
2030
|
+
.addClass(_rec.decision === "proceed" ? "fp-consent-chip-primary" : "fp-consent-chip-alt")
|
|
2031
|
+
.attr("type", "button").prop("disabled", true)
|
|
2032
|
+
.text(_rec.decision === "proceed" ? "Auto-verify ✓" : "Checking myself ✓")
|
|
2033
|
+
.appendTo($settledRow);
|
|
2034
|
+
$box.append($settledRow);
|
|
2035
|
+
scrollMessagesToBottom();
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
addMessage("assistant", "This step calls an external service: " + labels +
|
|
2040
|
+
". Want it verified automatically once triggered, or would you rather check it yourself?");
|
|
2041
|
+
|
|
2042
|
+
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2043
|
+
|
|
2044
|
+
function decide(proceed) {
|
|
2045
|
+
$row.find("button").prop("disabled", true);
|
|
2046
|
+
_rec.decision = proceed ? "proceed" : "skip";
|
|
2047
|
+
runBuildConsentDecision(_rec, proceed);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
$("<button>")
|
|
2051
|
+
.addClass("fp-consent-chip fp-consent-chip-primary")
|
|
2052
|
+
.attr("type", "button")
|
|
2053
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2054
|
+
.attr("data-fp-record-action", "proceed")
|
|
2055
|
+
.text("Auto-verify")
|
|
2056
|
+
.on("click", function () { decide(true); })
|
|
2057
|
+
.appendTo($row);
|
|
2058
|
+
$("<button>")
|
|
2059
|
+
.addClass("fp-consent-chip fp-consent-chip-alt")
|
|
2060
|
+
.attr("type", "button")
|
|
2061
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2062
|
+
.attr("data-fp-record-action", "skip")
|
|
2063
|
+
.text("I'll check myself")
|
|
2064
|
+
.on("click", function () { decide(false); })
|
|
2065
|
+
.appendTo($row);
|
|
2066
|
+
|
|
2067
|
+
$box.append($row);
|
|
2068
|
+
scrollMessagesToBottom();
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// The actual "proceed with review + loop" action, factored out so both
|
|
2072
|
+
// the fresh (no side-effecting nodes) and gated (decision made) paths in
|
|
2073
|
+
// handleBuildResult, and a rerendered consent-gate record's decide(),
|
|
2074
|
+
// all run identical logic sourced from plain data — never a captured
|
|
2075
|
+
// closure. consentGranted=false builds the Skip consent object
|
|
2076
|
+
// (skippedNodeIds/fpUidManifest) that startBuildLoop resolves into real
|
|
2077
|
+
// ids via importResult.nodeMap — see startBuildLoop's own comment.
|
|
2078
|
+
function runBuildConsentDecision(src, consentGranted) {
|
|
2079
|
+
var sideEffecting = Array.isArray(src.sideEffecting) ? src.sideEffecting : [];
|
|
2080
|
+
var flow = src.flow;
|
|
2081
|
+
var goalPrompt = src.goalPrompt;
|
|
2082
|
+
var fpUidManifest = Array.isArray(src.fpUidManifest) ? src.fpUidManifest : [];
|
|
2083
|
+
|
|
2084
|
+
if (sideEffecting.length > 0) {
|
|
2085
|
+
var sideLabels = sideEffecting.map(function (n) { return n.name || n.type; }).join(", ");
|
|
2086
|
+
addMessage("fp-notice", consentGranted
|
|
2087
|
+
? "⚠ External calls: " + sideLabels + " — the deploy-test loop will auto-verify these once triggered."
|
|
2088
|
+
: "⚠ External calls: " + sideLabels + " — auto-verify skipped for these node(s); confirm the result yourself.");
|
|
2089
|
+
}
|
|
2090
|
+
var consent = consentGranted ? null : {
|
|
2091
|
+
skippedNodeIds: sideEffecting.map(function (n) { return n.id; }),
|
|
2092
|
+
fpUidManifest: fpUidManifest
|
|
2093
|
+
};
|
|
2094
|
+
addGeneratedReview(flow, function (importResult) {
|
|
2095
|
+
startBuildLoop(goalPrompt, flow, importResult, consent);
|
|
2096
|
+
}, goalPrompt);
|
|
2097
|
+
renderActionChip(src.suggestedAction);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// W7 — per-call consent gate for a write-gated agent tool call
|
|
2101
|
+
// (apply_step/remove_step/rename_node touching a node type outside
|
|
2102
|
+
// WRITE_GATE_SAFE_NODE_TYPES). Mirrors renderBuildConsentGate's shape
|
|
2103
|
+
// and CLAUDE-008's fp-consent-chip styling, generalized to hold an
|
|
2104
|
+
// arbitrary pending tool call instead of only a build-loop's node set.
|
|
2105
|
+
//
|
|
2106
|
+
// src.onResume is a live function reference, not plain-data-only —
|
|
2107
|
+
// this is a DELIBERATE, reported deviation from renderBuildConsentGate/
|
|
2108
|
+
// runBuildConsentDecision's full data-reconstruction pattern. Reason:
|
|
2109
|
+
// the actual continuation (resuming the SAME in-flight
|
|
2110
|
+
// runAgentLoop/handleStep batch — remaining tool calls, accumulated
|
|
2111
|
+
// messages, then POSTing the next step and continuing the loop) lives
|
|
2112
|
+
// in per-invocation closures that aren't all JSON-serializable
|
|
2113
|
+
// (onDone/onError are themselves ad-hoc closures at each Modify call
|
|
2114
|
+
// site, e.g. onModifyResult/onModifyError capturing goalPrompt/
|
|
2115
|
+
// existingNodeIds). This mirrors the established, already-shipped
|
|
2116
|
+
// rerenderGeneratedReview "onImported is a live function ref, valid
|
|
2117
|
+
// within the same session" pattern rather than the stricter one.
|
|
2118
|
+
// Confirmed the stricter pattern's actual reason — a genuinely separate
|
|
2119
|
+
// pop-out window JS realm — does NOT apply here: the agent loop only
|
|
2120
|
+
// ever runs in the main window (dispatchSend's pop-out branch relays
|
|
2121
|
+
// via postMessage back to the opener rather than running its own loop,
|
|
2122
|
+
// and runBuildConsentDecision/renderBuildConsentGate itself has no
|
|
2123
|
+
// pop-out relay path either — confirmed via grep, so this is no
|
|
2124
|
+
// weaker than the existing shipped precedent). What DOES carry over
|
|
2125
|
+
// from CLAUDE-004-fix is the actual bug class it fixed: rerenderRecord
|
|
2126
|
+
// must find a dispatch branch and re-render from the SAME stored
|
|
2127
|
+
// record on refresh, never silently fall through to a generic path —
|
|
2128
|
+
// that guarantee is fully delivered below.
|
|
2129
|
+
function renderAgentToolConsentGate(src) {
|
|
2130
|
+
var $box = el("#fp-messages");
|
|
2131
|
+
if (!$box.length) {
|
|
2132
|
+
if (!src.decision && typeof src.onResume === "function") { src.onResume(true); }
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
// The pending "typing" indicator is only cleared automatically when
|
|
2136
|
+
// a turn fully completes or errors — a pause here otherwise leaves
|
|
2137
|
+
// it stuck showing stale narration ("Applying step: …") permanently
|
|
2138
|
+
// above the real gate that renders below it.
|
|
2139
|
+
hidePending();
|
|
2140
|
+
|
|
2141
|
+
var _rec = addRecord("question", {
|
|
2142
|
+
agentToolConsent: true,
|
|
2143
|
+
options: ["Proceed", "Skip this step"],
|
|
2144
|
+
name: src.name,
|
|
2145
|
+
args: src.args,
|
|
2146
|
+
label: describeAgentToolCall(src.name, src.args),
|
|
2147
|
+
onResume: src.onResume,
|
|
2148
|
+
decision: src.decision
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2151
|
+
if (_rec.decision) {
|
|
2152
|
+
// Already resolved before this render (e.g. resolved earlier in
|
|
2153
|
+
// the session, now showing again after a refresh) — settled
|
|
2154
|
+
// state, not an interactive choice. Mirrors
|
|
2155
|
+
// renderBuildConsentGate's settled branch exactly.
|
|
2156
|
+
addMessage("assistant", "Consent requested for: " + _rec.label);
|
|
2157
|
+
var $settledRow = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2158
|
+
$("<button>")
|
|
2159
|
+
.addClass("fp-consent-chip")
|
|
2160
|
+
.addClass(_rec.decision === "proceed" ? "fp-consent-chip-primary" : "fp-consent-chip-alt")
|
|
2161
|
+
.attr("type", "button").prop("disabled", true)
|
|
2162
|
+
.text(_rec.decision === "proceed" ? "Proceeded ✓" : "Skipped ✓")
|
|
2163
|
+
.appendTo($settledRow);
|
|
2164
|
+
$box.append($settledRow);
|
|
2165
|
+
scrollMessagesToBottom();
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
addMessage("assistant", "FlowPilot wants to do this: " + _rec.label +
|
|
2170
|
+
". Let it proceed, or skip just this one step and continue?");
|
|
2171
|
+
|
|
2172
|
+
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2173
|
+
|
|
2174
|
+
function decide(proceed) {
|
|
2175
|
+
$row.find("button").prop("disabled", true);
|
|
2176
|
+
_rec.decision = proceed ? "proceed" : "skip";
|
|
2177
|
+
if (typeof _rec.onResume === "function") { _rec.onResume(proceed); }
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
$("<button>")
|
|
2181
|
+
.addClass("fp-consent-chip fp-consent-chip-primary")
|
|
2182
|
+
.attr("type", "button")
|
|
2183
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2184
|
+
.attr("data-fp-record-action", "proceed")
|
|
2185
|
+
.text("Proceed")
|
|
2186
|
+
.on("click", function () { decide(true); })
|
|
2187
|
+
.appendTo($row);
|
|
2188
|
+
$("<button>")
|
|
2189
|
+
.addClass("fp-consent-chip fp-consent-chip-alt")
|
|
2190
|
+
.attr("type", "button")
|
|
2191
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2192
|
+
.attr("data-fp-record-action", "skip")
|
|
2193
|
+
.text("Skip this step")
|
|
2194
|
+
.on("click", function () { decide(false); })
|
|
2195
|
+
.appendTo($row);
|
|
2196
|
+
|
|
2197
|
+
$box.append($row);
|
|
2198
|
+
scrollMessagesToBottom();
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// W7 — ask_user tool UI. Reuses renderClarifyingQuestion's button +
|
|
2202
|
+
// free-text presentation, but resumes the SAME in-flight agent-loop
|
|
2203
|
+
// tool-call batch with the answer as a tool result via src.onAnswer —
|
|
2204
|
+
// NOT dispatchSend() (which sends a brand-new user Send, a different
|
|
2205
|
+
// flow entirely from continuing an already-in-progress tool-call turn).
|
|
2206
|
+
// Same refresh-survival shape as renderAgentToolConsentGate (record +
|
|
2207
|
+
// settled state), for the same reason: a mid-answer /refresh must not
|
|
2208
|
+
// strand the loop or silently re-ask a resolved question.
|
|
2209
|
+
function renderAskUserQuestion(src) {
|
|
2210
|
+
var $box = el("#fp-messages");
|
|
2211
|
+
if (!$box.length) {
|
|
2212
|
+
if (src.decision !== "answered" && typeof src.onAnswer === "function") { src.onAnswer(""); }
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
// Same reasoning as renderAgentToolConsentGate: a pause here must
|
|
2216
|
+
// clear the pending indicator itself, since nothing else will.
|
|
2217
|
+
hidePending();
|
|
2218
|
+
|
|
2219
|
+
var _rec = addRecord("question", {
|
|
2220
|
+
askUserTool: true,
|
|
2221
|
+
question: src.question || "FlowPilot has a question.",
|
|
2222
|
+
options: Array.isArray(src.options) ? src.options : [],
|
|
2223
|
+
onAnswer: src.onAnswer,
|
|
2224
|
+
decision: src.decision,
|
|
2225
|
+
answerText: src.answerText
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
if (_rec.decision === "answered") {
|
|
2229
|
+
addMessage("assistant", _rec.question);
|
|
2230
|
+
var $settled = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2231
|
+
$("<button>")
|
|
2232
|
+
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
2233
|
+
.attr("type", "button").prop("disabled", true)
|
|
2234
|
+
.text((_rec.answerText || "") + " ✓")
|
|
2235
|
+
.appendTo($settled);
|
|
2236
|
+
$box.append($settled);
|
|
2237
|
+
scrollMessagesToBottom();
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
addMessage("assistant", _rec.question);
|
|
2242
|
+
|
|
2243
|
+
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2244
|
+
var $otherRow;
|
|
2245
|
+
|
|
2246
|
+
function answer(text) {
|
|
2247
|
+
$row.find("button, input").prop("disabled", true);
|
|
2248
|
+
if ($otherRow) { $otherRow.find("button, input").prop("disabled", true); }
|
|
2249
|
+
_rec.decision = "answered";
|
|
2250
|
+
_rec.answerText = text;
|
|
2251
|
+
if (typeof _rec.onAnswer === "function") { _rec.onAnswer(text); }
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
_rec.options.forEach(function (opt) {
|
|
2255
|
+
$("<button>")
|
|
2256
|
+
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
2257
|
+
.attr("type", "button")
|
|
2258
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2259
|
+
.attr("data-fp-record-action", "answer")
|
|
2260
|
+
.attr("data-fp-record-value", opt)
|
|
2261
|
+
.text(opt)
|
|
2262
|
+
.on("click", function () { answer(opt); })
|
|
2263
|
+
.appendTo($row);
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
$otherRow = $("<div>").addClass("fp-question-other-row fp-hidden");
|
|
2267
|
+
var $otherInput = $("<input>")
|
|
2268
|
+
.attr("type", "text")
|
|
2269
|
+
.attr("placeholder", "Type your answer…")
|
|
2270
|
+
.addClass("fp-question-other-input");
|
|
2271
|
+
var $otherSend = $("<button>")
|
|
2272
|
+
.addClass("red-ui-button red-ui-button-small")
|
|
2273
|
+
.attr("type", "button")
|
|
2274
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2275
|
+
.attr("data-fp-record-action", "answer-other")
|
|
2276
|
+
.append($("<i>").addClass("fa fa-paper-plane"));
|
|
2277
|
+
|
|
2278
|
+
function submitOther() {
|
|
2279
|
+
var val = $otherInput.val().trim();
|
|
2280
|
+
if (!val) { return; }
|
|
2281
|
+
answer(val);
|
|
2282
|
+
}
|
|
2283
|
+
$otherSend.on("click", submitOther);
|
|
2284
|
+
$otherInput.on("keydown", function (e) { if (e.key === "Enter") { submitOther(); } });
|
|
2285
|
+
$otherRow.append($otherInput).append($otherSend);
|
|
2286
|
+
|
|
2287
|
+
$("<button>")
|
|
2288
|
+
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-other")
|
|
2289
|
+
.attr("type", "button")
|
|
2290
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2291
|
+
.attr("data-fp-record-action", "show-other")
|
|
2292
|
+
.text("Other…")
|
|
2293
|
+
.on("click", function () {
|
|
2294
|
+
$otherRow.removeClass("fp-hidden");
|
|
2295
|
+
$otherInput.focus();
|
|
2296
|
+
})
|
|
2297
|
+
.appendTo($row);
|
|
2298
|
+
|
|
2299
|
+
$box.append($row).append($otherRow);
|
|
2300
|
+
scrollMessagesToBottom();
|
|
2301
|
+
}
|
|
2302
|
+
|
|
1168
2303
|
function handleBuildResult(data, goalPrompt) {
|
|
1169
2304
|
hidePending();
|
|
1170
2305
|
if (renderQuestionOrProse(data)) { return; }
|
|
@@ -1173,8 +2308,24 @@
|
|
|
1173
2308
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1174
2309
|
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1175
2310
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1176
|
-
|
|
1177
|
-
|
|
2311
|
+
|
|
2312
|
+
var nodeClasses = data.stepNodeClasses;
|
|
2313
|
+
var sideEffecting = (nodeClasses && Array.isArray(nodeClasses.sideEffecting))
|
|
2314
|
+
? nodeClasses.sideEffecting : [];
|
|
2315
|
+
var fpUidManifest = Array.isArray(data.fpUidManifest) ? data.fpUidManifest : [];
|
|
2316
|
+
var consentSrc = {
|
|
2317
|
+
sideEffecting: sideEffecting,
|
|
2318
|
+
flow: flow,
|
|
2319
|
+
goalPrompt: goalPrompt,
|
|
2320
|
+
fpUidManifest: fpUidManifest,
|
|
2321
|
+
suggestedAction: data.suggestedAction
|
|
2322
|
+
};
|
|
2323
|
+
|
|
2324
|
+
if (sideEffecting.length > 0) {
|
|
2325
|
+
renderBuildConsentGate(consentSrc);
|
|
2326
|
+
} else {
|
|
2327
|
+
runBuildConsentDecision(consentSrc, true);
|
|
2328
|
+
}
|
|
1178
2329
|
setBusy(false);
|
|
1179
2330
|
updateSelectionStatus();
|
|
1180
2331
|
}
|
|
@@ -1212,7 +2363,9 @@
|
|
|
1212
2363
|
var payload = {
|
|
1213
2364
|
prompt: notes, context: context,
|
|
1214
2365
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1215
|
-
conversationId: conversationId
|
|
2366
|
+
conversationId: conversationId,
|
|
2367
|
+
strategy: "classic",
|
|
2368
|
+
entry: "document"
|
|
1216
2369
|
};
|
|
1217
2370
|
|
|
1218
2371
|
function onDocumentError(msg, xhr) {
|
|
@@ -1265,6 +2418,8 @@
|
|
|
1265
2418
|
addMessage("error", "Describe what you want to change.");
|
|
1266
2419
|
return;
|
|
1267
2420
|
}
|
|
2421
|
+
var existingNodeIds = context.nodes.map(function (n) { return n.id; });
|
|
2422
|
+
|
|
1268
2423
|
var label = "Modify: " + instruction + contextAttachmentNote(context);
|
|
1269
2424
|
addMessage("user", label);
|
|
1270
2425
|
// Snapshot history before pushing this turn (see send()).
|
|
@@ -1273,14 +2428,17 @@
|
|
|
1273
2428
|
$promptBox.val("");
|
|
1274
2429
|
|
|
1275
2430
|
var ap = activeProvider();
|
|
1276
|
-
var isAgentLoop = ap && ap.supportsTools
|
|
2431
|
+
var isAgentLoop = ap && ap.supportsTools &&
|
|
2432
|
+
currentSettings.enableAgentWrite === true;
|
|
1277
2433
|
|
|
1278
2434
|
setBusy(true);
|
|
1279
2435
|
showPending(isAgentLoop);
|
|
1280
2436
|
var payload = {
|
|
1281
2437
|
prompt: instruction, context: context,
|
|
1282
2438
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1283
|
-
conversationId: conversationId
|
|
2439
|
+
conversationId: conversationId,
|
|
2440
|
+
strategy: isAgentLoop ? "agent" : "classic",
|
|
2441
|
+
entry: "modify"
|
|
1284
2442
|
};
|
|
1285
2443
|
|
|
1286
2444
|
function onModifyError(msg, xhr) {
|
|
@@ -1289,6 +2447,10 @@
|
|
|
1289
2447
|
handleExecuteError(msg, raw);
|
|
1290
2448
|
}
|
|
1291
2449
|
|
|
2450
|
+
function onModifyResult(data) {
|
|
2451
|
+
handleModifyResult(data);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
1292
2454
|
// Explore-then-propose, same as generate(). The
|
|
1293
2455
|
// model may call read tools (e.g. to re-check the selected node's
|
|
1294
2456
|
// current config) before producing the modify envelope; the final
|
|
@@ -1296,7 +2458,7 @@
|
|
|
1296
2458
|
if (isAgentLoop) {
|
|
1297
2459
|
runAgentLoop("flowpilot/modify", payload,
|
|
1298
2460
|
{ mode: "modify", context: context, prompt: instruction },
|
|
1299
|
-
|
|
2461
|
+
onModifyResult, onModifyError);
|
|
1300
2462
|
return;
|
|
1301
2463
|
}
|
|
1302
2464
|
|
|
@@ -1304,11 +2466,11 @@
|
|
|
1304
2466
|
// generate() for details.
|
|
1305
2467
|
if (currentSettings.streamingEnabled) {
|
|
1306
2468
|
payload.stream = true;
|
|
1307
|
-
sendExecuteStream("modify", payload,
|
|
2469
|
+
sendExecuteStream("modify", payload, onModifyResult);
|
|
1308
2470
|
return;
|
|
1309
2471
|
}
|
|
1310
2472
|
|
|
1311
|
-
ajaxJson("POST", "flowpilot/modify", payload,
|
|
2473
|
+
ajaxJson("POST", "flowpilot/modify", payload, onModifyResult, onModifyError);
|
|
1312
2474
|
}
|
|
1313
2475
|
|
|
1314
2476
|
// Render generated flow JSON in a preformatted, copyable block. Used for
|
|
@@ -1352,11 +2514,23 @@
|
|
|
1352
2514
|
// held in these states.
|
|
1353
2515
|
var activeBuildLoop = null;
|
|
1354
2516
|
|
|
2517
|
+
// CLAUDE-014: plain-language note for a loop-checkpoint "Continue" click,
|
|
2518
|
+
// read and cleared by runBuildReview's payload build. Mirrors
|
|
2519
|
+
// runAgentLoop's pendingDebugNote, but module-scoped since
|
|
2520
|
+
// renderLoopCheckpoint/runBuildReview aren't nested inside runAgentLoop.
|
|
2521
|
+
// No equivalent exists for "Stop loop" — stopBuildLoop makes no server
|
|
2522
|
+
// round trip to attach a note to.
|
|
2523
|
+
var pendingLoopDebugNote = null;
|
|
2524
|
+
|
|
1355
2525
|
// How long onDebugMessage's auto-attach waits, after each matching
|
|
1356
2526
|
// message, for another one to arrive before locking in and running
|
|
1357
2527
|
// the review — see onDebugMessage for why (a forked/split flow can
|
|
1358
2528
|
// fire its debug node more than once per trigger).
|
|
1359
2529
|
var BUILD_LOOP_ATTACH_DEBOUNCE_MS = 1200;
|
|
2530
|
+
// W0.3: how many times the model can bail (emit a prose reply with a
|
|
2531
|
+
// suggestedAction mode-redirect) before the loop gives up with an
|
|
2532
|
+
// honest-timeout instead of silently treating the bail as success.
|
|
2533
|
+
var BUILD_LOOP_MAX_BAILS = 2;
|
|
1360
2534
|
var buildLoopAttachTimer = null;
|
|
1361
2535
|
// Fires when "attach" waits too long with no debug — surfaces a prompt
|
|
1362
2536
|
// for flows that don't produce automatic debug output (HTTP endpoints, etc).
|
|
@@ -1382,9 +2556,25 @@
|
|
|
1382
2556
|
|
|
1383
2557
|
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
1384
2558
|
|
|
2559
|
+
var _rec = addRecord("question", {
|
|
2560
|
+
options: ["Continue → AI review", "Stop loop"],
|
|
2561
|
+
loopCheckpoint: true,
|
|
2562
|
+
onResume: function (action) {
|
|
2563
|
+
if (action === "continue") {
|
|
2564
|
+
if (activeBuildLoop) {
|
|
2565
|
+
if (currentSettings.debugLogging) { pendingLoopDebugNote = "user clicked Continue"; }
|
|
2566
|
+
runBuildReview(activeBuildLoop);
|
|
2567
|
+
}
|
|
2568
|
+
} else if (action === "stop") {
|
|
2569
|
+
stopBuildLoop("Build loop stopped — applied nodes remain as-is.");
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
|
|
1385
2574
|
function onContinue() {
|
|
1386
2575
|
$row.find("button").prop("disabled", true);
|
|
1387
2576
|
if (!activeBuildLoop) { return; }
|
|
2577
|
+
if (currentSettings.debugLogging) { pendingLoopDebugNote = "user clicked Continue"; }
|
|
1388
2578
|
runBuildReview(activeBuildLoop);
|
|
1389
2579
|
}
|
|
1390
2580
|
function onStop() {
|
|
@@ -1395,21 +2585,73 @@
|
|
|
1395
2585
|
$("<button>")
|
|
1396
2586
|
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
1397
2587
|
.attr("type", "button")
|
|
2588
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2589
|
+
.attr("data-fp-record-action", "continue")
|
|
1398
2590
|
.text("Continue → AI review")
|
|
1399
2591
|
.on("click", onContinue)
|
|
1400
2592
|
.appendTo($row);
|
|
1401
2593
|
$("<button>")
|
|
1402
2594
|
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
1403
2595
|
.attr("type", "button")
|
|
2596
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2597
|
+
.attr("data-fp-record-action", "stop")
|
|
1404
2598
|
.text("Stop loop")
|
|
1405
2599
|
.on("click", onStop)
|
|
1406
2600
|
.appendTo($row);
|
|
1407
2601
|
|
|
1408
2602
|
$box.append($row);
|
|
1409
|
-
addRecord("question", { options: ["Continue → AI review", "Stop loop"], loopCheckpoint: true });
|
|
1410
2603
|
scrollMessagesToBottom();
|
|
1411
2604
|
}
|
|
1412
2605
|
|
|
2606
|
+
// WS3: remove FP-UID checkpoint tap nodes that the build prompt placed
|
|
2607
|
+
// on the canvas as FlowPilot scaffolding. These are debug nodes named
|
|
2608
|
+
// FP-UID001, FP-UID002, etc. — wired in parallel to external-call nodes
|
|
2609
|
+
// so the loop can attribute debug messages to specific checkpoints.
|
|
2610
|
+
// Called at loop end (any outcome) to clean up before returning control
|
|
2611
|
+
// to the user. Replicates the remove-and-history pattern from
|
|
2612
|
+
// applyModifications in apply-review.js (confirmed group-cleanup bookkeeping).
|
|
2613
|
+
function removeFpUidTaps(loop) {
|
|
2614
|
+
if (!loop || !Array.isArray(loop.nodeIds) || !loop.nodeIds.length) { return 0; }
|
|
2615
|
+
var FP_UID_RE = /^FP-UID\d+$/;
|
|
2616
|
+
var removed = 0;
|
|
2617
|
+
loop.nodeIds.forEach(function (id) {
|
|
2618
|
+
var liveNode = RED.nodes.node(id);
|
|
2619
|
+
if (!liveNode || !FP_UID_RE.test(liveNode.name)) { return; }
|
|
2620
|
+
var connectedLinks = [];
|
|
2621
|
+
RED.nodes.eachLink(function (l) {
|
|
2622
|
+
if ((l.source && l.source.id === id) || (l.target && l.target.id === id)) {
|
|
2623
|
+
connectedLinks.push(l);
|
|
2624
|
+
}
|
|
2625
|
+
});
|
|
2626
|
+
try { RED.nodes.remove(liveNode.id); } catch (e) { return; }
|
|
2627
|
+
if (liveNode.g && RED.nodes.group) {
|
|
2628
|
+
var ownerGroup = RED.nodes.group(liveNode.g);
|
|
2629
|
+
if (ownerGroup) {
|
|
2630
|
+
var idx = ownerGroup.nodes.indexOf(liveNode);
|
|
2631
|
+
if (idx !== -1) { ownerGroup.nodes.splice(idx, 1); }
|
|
2632
|
+
RED.group.markDirty(ownerGroup);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
RED.history.push({
|
|
2636
|
+
t: "delete",
|
|
2637
|
+
nodes: [liveNode],
|
|
2638
|
+
links: connectedLinks,
|
|
2639
|
+
groups: [],
|
|
2640
|
+
junctions: [],
|
|
2641
|
+
subflow: { id: undefined, instances: [] },
|
|
2642
|
+
subflowInputs: [],
|
|
2643
|
+
subflowOutputs: [],
|
|
2644
|
+
dirty: RED.nodes.dirty()
|
|
2645
|
+
});
|
|
2646
|
+
removed++;
|
|
2647
|
+
});
|
|
2648
|
+
if (removed) {
|
|
2649
|
+
RED.nodes.dirty(true);
|
|
2650
|
+
RED.view.redraw(true);
|
|
2651
|
+
}
|
|
2652
|
+
return removed;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
1413
2655
|
// The single exit point for every way a build loop ends — Touchdown,
|
|
1414
2656
|
// the cap being reached, pausing on a clarifying question, or the user
|
|
1415
2657
|
// clicking Stop. Releases Build mode and its pinned selection too: once
|
|
@@ -1420,6 +2662,7 @@
|
|
|
1420
2662
|
// visible as a completion badge. success=false (default): remove the stepper
|
|
1421
2663
|
// (user stop, cap reached, paused for question).
|
|
1422
2664
|
function stopBuildLoop(note, success) {
|
|
2665
|
+
var tapCount = activeBuildLoop ? removeFpUidTaps(activeBuildLoop) : 0;
|
|
1423
2666
|
if (success && activeBuildLoop) {
|
|
1424
2667
|
activeBuildLoop.waypoint = "done";
|
|
1425
2668
|
renderLoopStepper(activeBuildLoop);
|
|
@@ -1430,6 +2673,7 @@
|
|
|
1430
2673
|
if (!success) { el("#fp-loop-stepper").remove(); }
|
|
1431
2674
|
disarmExecuteAction();
|
|
1432
2675
|
if (note) { addMessage("assistant", note); }
|
|
2676
|
+
if (tapCount) { addMessage("assistant", "Removed " + tapCount + " FP-UID checkpoint tap(s) from the canvas."); }
|
|
1433
2677
|
}
|
|
1434
2678
|
|
|
1435
2679
|
// Applies a build-loop review's fix envelope, then keeps the loop's
|
|
@@ -1462,6 +2706,10 @@
|
|
|
1462
2706
|
} else {
|
|
1463
2707
|
loop.iteration++;
|
|
1464
2708
|
loop.waypoint = "apply";
|
|
2709
|
+
// CLAUDE-025: this fix still needs its OWN fresh deploy before
|
|
2710
|
+
// any evidence counts — the next "deploy" event re-stamps this
|
|
2711
|
+
// once the new apply->attach transition actually fires.
|
|
2712
|
+
loop.deployedAt = null;
|
|
1465
2713
|
renderLoopStepper(loop);
|
|
1466
2714
|
}
|
|
1467
2715
|
}
|
|
@@ -1492,7 +2740,21 @@
|
|
|
1492
2740
|
if (loop.waypoint === "apply") {
|
|
1493
2741
|
hint = "Click the canvas to place the new node(s), then Deploy — I'll move on automatically once you deploy.";
|
|
1494
2742
|
} else if (loop.waypoint === "attach") {
|
|
1495
|
-
|
|
2743
|
+
var eps = loop.httpEndpoints;
|
|
2744
|
+
if (eps && eps.length > 0) {
|
|
2745
|
+
var ep = eps[0];
|
|
2746
|
+
var baseUrl = (typeof window !== "undefined" && window.location)
|
|
2747
|
+
? window.location.origin : "";
|
|
2748
|
+
var curlMethod = ep.method === "GET" ? "" : " -X " + ep.method;
|
|
2749
|
+
hint = "Send " + ep.method + " " + ep.url + " to trigger the flow — " +
|
|
2750
|
+
"e.g. curl" + curlMethod + " " + baseUrl + ep.url +
|
|
2751
|
+
". I’ll attach the debug output automatically.";
|
|
2752
|
+
if (eps.length > 1) {
|
|
2753
|
+
hint += " (" + (eps.length - 1) + " more endpoint(s) in this flow.)";
|
|
2754
|
+
}
|
|
2755
|
+
} else {
|
|
2756
|
+
hint = "Trigger the flow, then check the Debug sidebar — I'll attach the next debug message automatically.";
|
|
2757
|
+
}
|
|
1496
2758
|
} else if (loop.waypoint === "review") {
|
|
1497
2759
|
hint = "Debug output attached — reviewing against the goal…";
|
|
1498
2760
|
}
|
|
@@ -1513,7 +2775,8 @@
|
|
|
1513
2775
|
iteration: loop.iteration,
|
|
1514
2776
|
maxIterations: loop.maxIterations,
|
|
1515
2777
|
goal: loop.goal,
|
|
1516
|
-
nodeIds: Array.isArray(loop.nodeIds) ? loop.nodeIds.slice() : []
|
|
2778
|
+
nodeIds: Array.isArray(loop.nodeIds) ? loop.nodeIds.slice() : [],
|
|
2779
|
+
httpEndpoints: Array.isArray(loop.httpEndpoints) ? loop.httpEndpoints.slice() : []
|
|
1517
2780
|
});
|
|
1518
2781
|
|
|
1519
2782
|
$box.append($msg);
|
|
@@ -1526,7 +2789,8 @@
|
|
|
1526
2789
|
iteration: rec.iteration || 1,
|
|
1527
2790
|
maxIterations: rec.maxIterations || 5,
|
|
1528
2791
|
goal: rec.goal || "",
|
|
1529
|
-
nodeIds: Array.isArray(rec.nodeIds) ? rec.nodeIds : []
|
|
2792
|
+
nodeIds: Array.isArray(rec.nodeIds) ? rec.nodeIds : [],
|
|
2793
|
+
httpEndpoints: Array.isArray(rec.httpEndpoints) ? rec.httpEndpoints : []
|
|
1530
2794
|
});
|
|
1531
2795
|
}
|
|
1532
2796
|
|
|
@@ -1542,12 +2806,22 @@
|
|
|
1542
2806
|
// end up on the canvas — importResult.nodeMap maps each placeholder id
|
|
1543
2807
|
// to the real live node object, which is the only way later review/fix
|
|
1544
2808
|
// requests can target the right nodes via collectSelectionContext.
|
|
1545
|
-
|
|
2809
|
+
//
|
|
2810
|
+
// consent (WS4, optional): { skippedNodeIds, fpUidManifest } from a
|
|
2811
|
+
// Skip decision at the build consent gate (see handleBuildResult /
|
|
2812
|
+
// renderBuildConsentGate) — skippedNodeIds are the side-effecting
|
|
2813
|
+
// nodes' own PLACEHOLDER ids, fpUidManifest maps each FP-UID debug tap's
|
|
2814
|
+
// placeholder id to the placeholder id of the node it's wired from
|
|
2815
|
+
// (wiredFrom). Resolved here into two REAL-id sets: the skipped nodes
|
|
2816
|
+
// themselves (onNodeStatus's status/<nodeId> path checks against these)
|
|
2817
|
+
// and the taps wired to them (onDebugMessage's msg.id check does) — so
|
|
2818
|
+
// both auto-verify evidence paths honor the same Skip decision.
|
|
2819
|
+
function startBuildLoop(goal, nodeIdsOrNodes, importResult, consent) {
|
|
1546
2820
|
var nodeIds = [];
|
|
2821
|
+
var nodeMap = importResult && importResult.nodeMap;
|
|
1547
2822
|
if (importResult) {
|
|
1548
2823
|
// Fresh build: map placeholder ids from the proposal to the real
|
|
1549
2824
|
// ids importNodes assigned on the canvas.
|
|
1550
|
-
var nodeMap = importResult.nodeMap;
|
|
1551
2825
|
if (nodeMap && Array.isArray(nodeIdsOrNodes)) {
|
|
1552
2826
|
nodeIdsOrNodes.forEach(function (n) {
|
|
1553
2827
|
var real = n && n.id && nodeMap[n.id];
|
|
@@ -1558,13 +2832,48 @@
|
|
|
1558
2832
|
// Existing-flow build: ids are already resolved real canvas ids.
|
|
1559
2833
|
nodeIds = nodeIdsOrNodes.filter(function (id) { return typeof id === "string" && id; });
|
|
1560
2834
|
}
|
|
2835
|
+
// Detect HTTP-in endpoints so the "attach" step can show a specific
|
|
2836
|
+
// trigger hint instead of the generic "trigger the flow" message.
|
|
2837
|
+
var httpEndpoints = [];
|
|
2838
|
+
nodeIds.forEach(function (id) {
|
|
2839
|
+
var n = RED.nodes.node(id);
|
|
2840
|
+
if (n && n.type === "http in" && n.url) {
|
|
2841
|
+
httpEndpoints.push({ method: (n.method || "get").toUpperCase(), url: n.url });
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2844
|
+
|
|
2845
|
+
var skipCheckpointNodeIds = [];
|
|
2846
|
+
var skipCheckpointTapIds = [];
|
|
2847
|
+
var skippedPlaceholderIds = consent && Array.isArray(consent.skippedNodeIds) ? consent.skippedNodeIds : [];
|
|
2848
|
+
if (skippedPlaceholderIds.length && nodeMap) {
|
|
2849
|
+
skippedPlaceholderIds.forEach(function (placeholderId) {
|
|
2850
|
+
var real = nodeMap[placeholderId];
|
|
2851
|
+
if (real && real.id) { skipCheckpointNodeIds.push(real.id); }
|
|
2852
|
+
});
|
|
2853
|
+
var manifest = Array.isArray(consent.fpUidManifest) ? consent.fpUidManifest : [];
|
|
2854
|
+
manifest.forEach(function (tap) {
|
|
2855
|
+
if (!tap || skippedPlaceholderIds.indexOf(tap.wiredFrom) === -1) { return; }
|
|
2856
|
+
var realTap = nodeMap[tap.id];
|
|
2857
|
+
if (realTap && realTap.id) { skipCheckpointTapIds.push(realTap.id); }
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
|
|
1561
2861
|
activeBuildLoop = {
|
|
1562
2862
|
goal: goal,
|
|
1563
2863
|
nodeIds: nodeIds,
|
|
1564
2864
|
iteration: 1,
|
|
1565
2865
|
maxIterations: getAgentLoopMaxIterations(),
|
|
1566
2866
|
waypoint: "apply",
|
|
1567
|
-
conversationId: conversationId
|
|
2867
|
+
conversationId: conversationId,
|
|
2868
|
+
bailCount: 0,
|
|
2869
|
+
httpEndpoints: httpEndpoints,
|
|
2870
|
+
skipCheckpointNodeIds: skipCheckpointNodeIds,
|
|
2871
|
+
skipCheckpointTapIds: skipCheckpointTapIds,
|
|
2872
|
+
// CLAUDE-025: stamped by the RED "deploy" listener (init.js) the
|
|
2873
|
+
// moment THIS attempt's own apply->attach transition fires — see
|
|
2874
|
+
// freshBuildLoopEvidence. Starts null: no deploy has happened for
|
|
2875
|
+
// this attempt yet, so nothing can count as evidence.
|
|
2876
|
+
deployedAt: null
|
|
1568
2877
|
};
|
|
1569
2878
|
renderLoopStepper(activeBuildLoop);
|
|
1570
2879
|
}
|
|
@@ -1579,8 +2888,62 @@
|
|
|
1579
2888
|
// node ids instead of the live/pinned canvas selection) are synthetic.
|
|
1580
2889
|
function runBuildReview(loop) {
|
|
1581
2890
|
var context = collectSelectionContext(loop.nodeIds);
|
|
1582
|
-
|
|
1583
|
-
|
|
2891
|
+
// CLAUDE-025: deliberately NOT attachDebugContext() here — that pulls
|
|
2892
|
+
// in the full sticky attachedDebugMessages buffer, which can still
|
|
2893
|
+
// hold evidence from an earlier Build attempt (or a manual attach)
|
|
2894
|
+
// that has nothing to do with THIS attempt's own deploy. Only
|
|
2895
|
+
// messages that arrived at/after this attempt's own deploy (see
|
|
2896
|
+
// freshBuildLoopEvidence) count as evidence for its review.
|
|
2897
|
+
var freshEvidence = freshBuildLoopEvidence(loop);
|
|
2898
|
+
if (freshEvidence.length) {
|
|
2899
|
+
context = context || { nodes: [], connections: {} };
|
|
2900
|
+
context = Object.assign({}, context, {
|
|
2901
|
+
debugMessages: freshEvidence.map(function (m) {
|
|
2902
|
+
return { id: m.id, timestamp: m.timestamp, sourceKind: m.sourceKind, name: m.name, topic: m.topic, value: m.value };
|
|
2903
|
+
})
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
var reviewEvidence = context && Array.isArray(context.debugMessages)
|
|
2907
|
+
? context.debugMessages : [];
|
|
2908
|
+
var statusOnlyEvidence = reviewEvidence.length > 0 &&
|
|
2909
|
+
reviewEvidence.every(function (entry) {
|
|
2910
|
+
return entry && entry.sourceKind === "status";
|
|
2911
|
+
});
|
|
2912
|
+
// W0.3: framing block — suppresses the Modify escape hatch
|
|
2913
|
+
// (suggestedAction mode-redirect) inside the build loop context.
|
|
2914
|
+
// The code-side handler (handleBuildReviewResult) also detects and
|
|
2915
|
+
// counts bail attempts so N bails trigger an honest-timeout instead
|
|
2916
|
+
// of silently treating a redirect as success.
|
|
2917
|
+
var instruction = "CONTEXT: You are the fix engine inside a build-test-fix loop. " +
|
|
2918
|
+
"Your only valid responses are: (1) plain text when the goal is fully " +
|
|
2919
|
+
"satisfied, or (2) a {\"explanation\", \"changes\", ...} fix envelope " +
|
|
2920
|
+
"when something needs patching." +
|
|
2921
|
+
(statusOnlyEvidence
|
|
2922
|
+
? " (3) Because the attached evidence contains ONLY coarse node-status " +
|
|
2923
|
+
"lines, you may instead return the atomic {\"question\", " +
|
|
2924
|
+
"\"questionOptions\"} envelope described below."
|
|
2925
|
+
: "") +
|
|
2926
|
+
" Do NOT use the <<<FLOWPILOT_DATA>>> " +
|
|
2927
|
+
"block or suggest switching to chat/generate/document — you are already " +
|
|
2928
|
+
"in the right context and any mode-redirect will be ignored. If you are " +
|
|
2929
|
+
"genuinely uncertain what to fix, " +
|
|
2930
|
+
(statusOnlyEvidence
|
|
2931
|
+
? "use the status-only confirmation question below."
|
|
2932
|
+
: "describe the uncertainty inside \"explanation\" in a fix envelope.") +
|
|
2933
|
+
"\n\nEach attached evidence object has sourceKind. sourceKind:\"debug\" " +
|
|
2934
|
+
"is real message content emitted by a debug node. sourceKind:\"status\" " +
|
|
2935
|
+
"is only a coarse connection/status line synthesized from node status; " +
|
|
2936
|
+
"never treat it as proof of message payload content or successful " +
|
|
2937
|
+
"end-to-end behavior. " +
|
|
2938
|
+
(statusOnlyEvidence
|
|
2939
|
+
? "STATUS-ONLY FALLBACK: if the coarse status does not prove whether " +
|
|
2940
|
+
"the deployed node is actually connected/working, do not guess or " +
|
|
2941
|
+
"assert failure. Ask one concrete yes/no confirmation such as " +
|
|
2942
|
+
"\"Does the node show connected after deploy?\" by returning ONLY " +
|
|
2943
|
+
"{\"question\":\"...\",\"questionOptions\":[\"Yes\",\"No\"]}. "
|
|
2944
|
+
: "") +
|
|
2945
|
+
"\n\n" +
|
|
2946
|
+
"Review the attached debug output against this build goal: \"" +
|
|
1584
2947
|
loop.goal + "\". Before concluding anything, list out every distinct " +
|
|
1585
2948
|
"piece of data or behavior the goal actually requires, then check the " +
|
|
1586
2949
|
"attached debug payload(s) contain EACH one — a payload that's merely " +
|
|
@@ -1588,7 +2951,22 @@
|
|
|
1588
2951
|
"goal asked to combine two things but the payload only shows one), " +
|
|
1589
2952
|
"does NOT fully satisfy it. If more than one debug message is " +
|
|
1590
2953
|
"attached, treat them together as the full picture from one trigger, " +
|
|
1591
|
-
"not as separate independent attempts.
|
|
2954
|
+
"not as separate independent attempts. " +
|
|
2955
|
+
"SPECIAL CASE — network errors: if the debug output shows ONLY a " +
|
|
2956
|
+
"network-level error (EHOSTUNREACH, ECONNREFUSED, ETIMEDOUT, " +
|
|
2957
|
+
"ENOTFOUND, getaddrinfo ENOTFOUND, EAI_AGAIN, EAI_NODATA), the " +
|
|
2958
|
+
"flow MIGHT be correctly built — BUT you MUST first check the node " +
|
|
2959
|
+
"context: if any http-request node has an empty url field, a " +
|
|
2960
|
+
"placeholder, or a clearly malformed url (no hostname, no protocol, " +
|
|
2961
|
+
"etc.), the DNS or connection error is a CONFIGURATION problem — " +
|
|
2962
|
+
"fix the url field, do NOT declare it an infrastructure issue. Only " +
|
|
2963
|
+
"apply this special case when the url is a real, non-empty, " +
|
|
2964
|
+
"well-formed URL and the external service is simply unreachable. In " +
|
|
2965
|
+
"that case reply in plain text acknowledging the flow is structurally " +
|
|
2966
|
+
"correct and the network error is an infrastructure issue outside the " +
|
|
2967
|
+
"flow. Do NOT propose any changes; this error cannot be resolved by " +
|
|
2968
|
+
"modifying the flow. " +
|
|
2969
|
+
"If it fully satisfies the goal, " +
|
|
1592
2970
|
"say so in plain text — no changes needed. If something's wrong " +
|
|
1593
2971
|
"(including a node that never fired, or a value that's missing/empty " +
|
|
1594
2972
|
"when the goal needed it), propose the fix directly as a patch in " +
|
|
@@ -1612,8 +2990,14 @@
|
|
|
1612
2990
|
var payload = {
|
|
1613
2991
|
prompt: instruction, context: context,
|
|
1614
2992
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1615
|
-
conversationId: loop.conversationId
|
|
2993
|
+
conversationId: loop.conversationId,
|
|
2994
|
+
strategy: "classic",
|
|
2995
|
+
entry: "build-review"
|
|
1616
2996
|
};
|
|
2997
|
+
if (pendingLoopDebugNote) {
|
|
2998
|
+
payload.debugNote = pendingLoopDebugNote;
|
|
2999
|
+
pendingLoopDebugNote = null;
|
|
3000
|
+
}
|
|
1617
3001
|
|
|
1618
3002
|
function onReviewError(msg, xhr) {
|
|
1619
3003
|
hidePending();
|
|
@@ -1644,6 +3028,26 @@
|
|
|
1644
3028
|
// diff-then-Apply pipeline as a manual Modify, then the loop advances
|
|
1645
3029
|
// back to "apply" for the next deploy/test cycle, or stops if the
|
|
1646
3030
|
// iteration cap is reached).
|
|
3031
|
+
// Returns false when all of data's proposed changes are sentinel-echoed
|
|
3032
|
+
// with no insertions, removals, or wire changes. Used by
|
|
3033
|
+
// handleBuildReviewResult to avoid showing an all-blocked review panel
|
|
3034
|
+
// when the model said "no changes needed" but still emitted a modify
|
|
3035
|
+
// envelope (a common model behavior after a build-loop review).
|
|
3036
|
+
function reviewHasRealDiffs(data) {
|
|
3037
|
+
if ((data.newNodes && data.newNodes.length) ||
|
|
3038
|
+
(data.removeNodes && data.removeNodes.length) ||
|
|
3039
|
+
(data.newWires && data.newWires.length) ||
|
|
3040
|
+
(data.newGroups && data.newGroups.length)) { return true; }
|
|
3041
|
+
var nodes = Array.isArray(data.flow) ? data.flow : [];
|
|
3042
|
+
return nodes.some(function (modNode) {
|
|
3043
|
+
if (!modNode || !modNode.id) { return false; }
|
|
3044
|
+
var liveNode = findLiveNode(modNode.id);
|
|
3045
|
+
if (!liveNode) { return false; }
|
|
3046
|
+
var diff = computeNodeDiff(liveNode, modNode);
|
|
3047
|
+
return diff.propertyChanges.length > 0 || diff.wiresChanged;
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
|
|
1647
3051
|
function handleBuildReviewResult(data) {
|
|
1648
3052
|
hidePending();
|
|
1649
3053
|
var loop = activeBuildLoop;
|
|
@@ -1667,6 +3071,58 @@
|
|
|
1667
3071
|
}
|
|
1668
3072
|
|
|
1669
3073
|
if (data.prose) {
|
|
3074
|
+
var explanation = data.explanation || "(no content returned)";
|
|
3075
|
+
|
|
3076
|
+
if (looksLikeToolEnvelope(data.explanation)) {
|
|
3077
|
+
handleExecuteError("FlowPilot's reply didn't come through as expected.", data.explanation);
|
|
3078
|
+
stopBuildLoop("Build loop stopped — the review response wasn't in the expected format. Continue manually with Modify, or start a fresh /build.", false);
|
|
3079
|
+
updateSelectionStatus();
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
// W0.3: bail detection — a prose reply with a mode-redirect
|
|
3084
|
+
// suggestedAction means the model tried to exit the loop
|
|
3085
|
+
// context via the Modify escape hatch. Count it and retry or
|
|
3086
|
+
// honest-timeout rather than treating it as success.
|
|
3087
|
+
var sa = data.suggestedAction;
|
|
3088
|
+
var isBail = sa && (sa.mode === "chat" || sa.mode === "generate" || sa.mode === "document");
|
|
3089
|
+
if (isBail) {
|
|
3090
|
+
loop.bailCount = (loop.bailCount || 0) + 1;
|
|
3091
|
+
console.warn("[FlowPilot] build-loop bail #" + loop.bailCount +
|
|
3092
|
+
" mode=" + sa.mode + ": " + explanation);
|
|
3093
|
+
addMessage("assistant", explanation);
|
|
3094
|
+
pushHistory("assistant", explanation);
|
|
3095
|
+
if (loop.bailCount >= BUILD_LOOP_MAX_BAILS) {
|
|
3096
|
+
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);
|
|
3097
|
+
setBusy(false);
|
|
3098
|
+
updateSelectionStatus();
|
|
3099
|
+
} else {
|
|
3100
|
+
addMessage("fp-notice", "Build-loop: review redirected to " + sa.mode +
|
|
3101
|
+
" — staying in build context and retrying (bail " + loop.bailCount +
|
|
3102
|
+
"/" + BUILD_LOOP_MAX_BAILS + ").");
|
|
3103
|
+
runBuildReview(loop);
|
|
3104
|
+
}
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
addMessage("assistant", explanation);
|
|
3109
|
+
pushHistory("assistant", explanation);
|
|
3110
|
+
renderActionChip(data.suggestedAction);
|
|
3111
|
+
var stopMsg = /EHOSTUNREACH|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EAI_NODATA|getaddrinfo|unreachable|infrastructure/i
|
|
3112
|
+
.test(explanation)
|
|
3113
|
+
? "Build complete — the flow is correctly structured, but the external endpoint was unreachable during testing (infrastructure issue, not a flow problem)."
|
|
3114
|
+
: "Touchdown — the debug output matches the goal.";
|
|
3115
|
+
stopBuildLoop(stopMsg, true);
|
|
3116
|
+
setBusy(false);
|
|
3117
|
+
updateSelectionStatus();
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
// Modify envelope where every proposed change is a sentinel echo —
|
|
3122
|
+
// the model emitted a changes object but all fields are redacted
|
|
3123
|
+
// placeholders with no insertions, removals, or wire changes. Treat
|
|
3124
|
+
// it as "no changes needed" rather than showing an all-blocked panel.
|
|
3125
|
+
if (!reviewHasRealDiffs(data)) {
|
|
1670
3126
|
addMessage("assistant", data.explanation || "(no content returned)");
|
|
1671
3127
|
pushHistory("assistant", data.explanation || "");
|
|
1672
3128
|
renderActionChip(data.suggestedAction);
|