@manny-est/node-red-flowpilot 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -26
- package/README.md +10 -1
- package/USER-GUIDE.md +5 -1
- package/flowpilot-core.css +53 -10
- package/flowpilot-node-entry.js +15 -0
- package/flowpilot.js +1207 -187
- package/lib/agent-contract.js +50 -0
- package/lib/build-core-script.js +1 -0
- package/lib/chat-data.js +106 -0
- package/lib/core/apply-review.js +34 -11
- package/lib/core/graph-truth.js +63 -0
- package/lib/core/history.js +157 -6
- package/lib/core/init.js +223 -21
- package/lib/core/main.js +780 -38
- package/lib/core/modes.js +1098 -112
- package/lib/core/selection-context.js +44 -24
- package/lib/default-system-prompt.js +4 -3
- package/lib/document-system-prompt.js +7 -4
- package/lib/envelope.js +13 -7
- package/lib/generation-system-prompt.js +5 -4
- package/lib/modify-system-prompt.js +64 -12
- package/lib/persona-prompt.js +81 -54
- package/lib/prompt-fragments.js +17 -2
- package/lib/provider-anthropic.js +23 -10
- package/lib/provider-openai-compatible.js +51 -11
- package/lib/provider-shape-check.js +34 -0
- package/lib/storage.js +134 -21
- package/package.json +3 -2
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) {
|
|
@@ -271,6 +273,65 @@
|
|
|
271
273
|
dispatch();
|
|
272
274
|
}
|
|
273
275
|
|
|
276
|
+
function summarizeConversationHistory() {
|
|
277
|
+
if (isPopoutContext) {
|
|
278
|
+
addMessage("assistant", "`/summarize` only runs in the main FlowPilot panel.");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
var totalMessages = conversationHistory.length;
|
|
283
|
+
if (totalMessages <= 4) {
|
|
284
|
+
addMessage("assistant", "Nothing much to summarize yet — only " + totalMessages +
|
|
285
|
+
" message(s) so far.");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
var recentTail = conversationHistory.slice(-4);
|
|
290
|
+
var olderSlice = conversationHistory.slice(0, -4);
|
|
291
|
+
var beforeTokens = estimateTokens(olderSlice);
|
|
292
|
+
var prompt = "Summarize the earlier conversation history provided here. " +
|
|
293
|
+
"Keep it concise and factual. Preserve decisions, constraints, important context, " +
|
|
294
|
+
"and unresolved questions. Return plain text only.";
|
|
295
|
+
var payload = {
|
|
296
|
+
prompt: prompt,
|
|
297
|
+
context: null,
|
|
298
|
+
history: olderSlice.slice(),
|
|
299
|
+
historyTruncated: false,
|
|
300
|
+
conversationId: conversationId,
|
|
301
|
+
strategy: "classic",
|
|
302
|
+
entry: "chat"
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
setBusy(true);
|
|
306
|
+
showPending(false);
|
|
307
|
+
|
|
308
|
+
ajaxJson("POST", "flowpilot/chat", payload, function (data) {
|
|
309
|
+
hidePending();
|
|
310
|
+
var summaryText = String(data && data.message ? data.message : "").trim();
|
|
311
|
+
if (!summaryText) {
|
|
312
|
+
addMessage("error", "Summarize failed: no summary text returned.");
|
|
313
|
+
setBusy(false);
|
|
314
|
+
updateSelectionStatus();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
var summaryMessage = {
|
|
319
|
+
role: "assistant",
|
|
320
|
+
content: "[Summary of earlier conversation]\n" + summaryText
|
|
321
|
+
};
|
|
322
|
+
conversationHistory = [summaryMessage].concat(recentTail);
|
|
323
|
+
addMessage("assistant", "Compacted " + olderSlice.length + " earlier message(s) into a summary (~" +
|
|
324
|
+
beforeTokens.toLocaleString() + " → ~" + estimateTokens(summaryMessage).toLocaleString() + " tokens).");
|
|
325
|
+
setBusy(false);
|
|
326
|
+
updateSelectionStatus();
|
|
327
|
+
}, function (msg) {
|
|
328
|
+
hidePending();
|
|
329
|
+
addMessage("error", "Summarize failed: " + msg);
|
|
330
|
+
setBusy(false);
|
|
331
|
+
updateSelectionStatus();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
274
335
|
// ---------------------------------------------------------------------
|
|
275
336
|
// Bounded read-tool loop, shared by chat and
|
|
276
337
|
// generate/document/modify ("explore-then-propose"). Sends the
|
|
@@ -299,14 +360,150 @@
|
|
|
299
360
|
// than erroring out.
|
|
300
361
|
// ---------------------------------------------------------------------
|
|
301
362
|
var AGENT_LOOP_MAX_STEPS = 8;
|
|
302
|
-
|
|
363
|
+
// W7 §16 point 5: ask_user round-trips get their own small budget,
|
|
364
|
+
// separate from AGENT_LOOP_MAX_STEPS, so a couple of clarifying
|
|
365
|
+
// questions don't eat the real step budget.
|
|
366
|
+
var AGENT_ASK_USER_MAX_ROUNDTRIPS = 3;
|
|
303
367
|
|
|
304
368
|
var fpAgentStopRequested = false;
|
|
305
|
-
|
|
306
|
-
|
|
369
|
+
// CLAUDE-022: the in-flight jqXHR for this run's current ajaxJson call
|
|
370
|
+
// (first request / postNextStep / fallbackToPlain), so the Stop button
|
|
371
|
+
// can actually abort it instead of only setting the flag above. Cleared
|
|
372
|
+
// back to null the moment that call's own success/error callback runs,
|
|
373
|
+
// so a stale reference never lingers into the next step.
|
|
374
|
+
var fpCurrentAgentRequest = null;
|
|
375
|
+
|
|
376
|
+
// P10-D1 (ADR-001 R5): monotonic suffix so two runs minted in the same
|
|
377
|
+
// millisecond (Date.now() collision) still get distinct runIds.
|
|
378
|
+
var runIdCounter = 0;
|
|
379
|
+
|
|
380
|
+
function runAgentLoop(firstEndpoint, payload, stepExtra, realOnDone, realOnError) {
|
|
381
|
+
if (!payload || !payload.strategy || !payload.entry) {
|
|
382
|
+
throw new Error("runAgentLoop requires strategy and entry in the initial payload.");
|
|
383
|
+
}
|
|
384
|
+
var AGENT_LOOP_TOKEN_CEILING = (Number(currentSettings.agentLoopTokenCeiling) > 0)
|
|
385
|
+
? Number(currentSettings.agentLoopTokenCeiling) : 50000;
|
|
386
|
+
var runStrategy = payload.strategy;
|
|
387
|
+
var runEntry = payload.entry;
|
|
388
|
+
// P10-D1 follow-up (sr-dev review): captured once, like
|
|
389
|
+
// runStrategy/runEntry/runId — using the free-variable
|
|
390
|
+
// conversationId instead would let a stale run's dedup lookup land
|
|
391
|
+
// in the wrong conversation's bucket if the user switches
|
|
392
|
+
// conversations mid-run.
|
|
393
|
+
var runConversationId = payload.conversationId;
|
|
394
|
+
// P10-D1: minted once per run, included in every step payload so
|
|
395
|
+
// the server can echo it into logs beside strategy/entry. Also the
|
|
396
|
+
// namespace for this run's WRITE tool opIds (opId = runId + ":" +
|
|
397
|
+
// call.id, ADR-001 R5).
|
|
398
|
+
var runId = "run-" + Date.now().toString(36) + "-" + (++runIdCounter);
|
|
399
|
+
// CLAUDE-013: accumulates placeholder->real-id mappings across every
|
|
400
|
+
// WRITE-tool call resolved THIS run, so a later call in the same run
|
|
401
|
+
// (e.g. group_nodes) can resolve a placeholder id (e.g. "fp-new-2")
|
|
402
|
+
// that an earlier call (e.g. apply_step) minted via applyInsertions —
|
|
403
|
+
// each WRITE executor's call-local idMap only covers ids it created
|
|
404
|
+
// itself, not ids from a prior call in the same agent loop.
|
|
405
|
+
var runIdMap = {};
|
|
406
|
+
// CLAUDE-027: accumulates the individual RED.history event(s) each
|
|
407
|
+
// WRITE-tool call this run would otherwise have pushed on its own
|
|
408
|
+
// (apply_step/remove_step/rename_node/group_nodes — see
|
|
409
|
+
// applyInsertions/applyModifications in apply-review.js and
|
|
410
|
+
// executeGroupNodesTool in main.js, all of which push into this
|
|
411
|
+
// array instead of RED.history directly whenever it's passed
|
|
412
|
+
// through). Flushed as ONE RED.history entry — via RED.history's own
|
|
413
|
+
// t:"multi" wrapper, confirmed against @node-red/editor-client's
|
|
414
|
+
// red.js (e.g. its deleteSelection(), which collapses a mixed
|
|
415
|
+
// delete+move into one push the exact same way) when there's more
|
|
416
|
+
// than one, or pushed unwrapped when there's exactly one, matching
|
|
417
|
+
// that same core convention — the moment this run actually ends, by
|
|
418
|
+
// flushRunHistory()/onDone/onError below. A run that never calls a
|
|
419
|
+
// WRITE tool (e.g. a pure ask_user round-trip, or read-only chat)
|
|
420
|
+
// leaves this empty, so flush is a no-op and no spurious entry is
|
|
421
|
+
// pushed.
|
|
422
|
+
var runHistoryEvents = [];
|
|
307
423
|
var step = 0;
|
|
308
424
|
var totalTokens = 0;
|
|
425
|
+
var askUserRounds = 0;
|
|
309
426
|
fpAgentStopRequested = false;
|
|
427
|
+
fpCurrentAgentRequest = null;
|
|
428
|
+
// W7: one entry per WRITE tool call resolved THIS runAgentLoop
|
|
429
|
+
// invocation, in order (proceed-and-pass / proceed-and-fail /
|
|
430
|
+
// user-declined all count, read-tool calls don't) — gives
|
|
431
|
+
// handleModifyResult a real 1:1 todo-item correlation instead of
|
|
432
|
+
// the CLAUDE-005 all-together fallback. Attached to the final
|
|
433
|
+
// data as _agentWriteResults right before onDone(data); untouched
|
|
434
|
+
// (empty) whenever no WRITE tool call was made this turn, which
|
|
435
|
+
// keeps the existing all-together path completely unchanged.
|
|
436
|
+
var agentWriteResults = [];
|
|
437
|
+
// CLAUDE-014: plain-language note for the decision that triggered the
|
|
438
|
+
// NEXT agent-step round trip (consent-gate Proceed/Skip, ask_user
|
|
439
|
+
// answer) — set right before the decision resumes the loop, read and
|
|
440
|
+
// cleared by postNextStep so CODEX-012's debug.log can show what the
|
|
441
|
+
// user actually decided instead of only raw tool-result JSON. Only
|
|
442
|
+
// populated when settings.debugLogging is on.
|
|
443
|
+
var pendingDebugNote = null;
|
|
444
|
+
|
|
445
|
+
// CLAUDE-027: pushes this run's accumulated WRITE-tool history
|
|
446
|
+
// event(s) (runHistoryEvents above) to RED.history as ONE entry —
|
|
447
|
+
// coalesced via t:"multi" when more than one WRITE-tool call
|
|
448
|
+
// mutated this run, unwrapped when exactly one did, a no-op when
|
|
449
|
+
// none did. MUST run at every single point this run can end, not
|
|
450
|
+
// just the "clean" success path — a run that errors out (step
|
|
451
|
+
// budget/token ceiling exceeded, user Stop, a failed request) after
|
|
452
|
+
// ALREADY applying one or more WRITE tool calls still needs its
|
|
453
|
+
// partial progress to land in undo history, or Ctrl+Z would be
|
|
454
|
+
// unable to remove mutations that are visibly sitting on the
|
|
455
|
+
// canvas. Rather than call this at each of those call sites
|
|
456
|
+
// individually (easy to miss one), onDone/onError below shadow the
|
|
457
|
+
// real callback params so EVERY exit from this closure flushes
|
|
458
|
+
// first automatically.
|
|
459
|
+
function flushRunHistory() {
|
|
460
|
+
if (!runHistoryEvents.length) { return; }
|
|
461
|
+
if (runHistoryEvents.length === 1) {
|
|
462
|
+
RED.history.push(runHistoryEvents[0]);
|
|
463
|
+
} else {
|
|
464
|
+
RED.history.push({ t: "multi", events: runHistoryEvents.slice() });
|
|
465
|
+
}
|
|
466
|
+
runHistoryEvents = [];
|
|
467
|
+
}
|
|
468
|
+
function onDone(data) { flushRunHistory(); realOnDone(data); }
|
|
469
|
+
function onError(err, xhr) { flushRunHistory(); realOnError(err, xhr); }
|
|
470
|
+
|
|
471
|
+
// P10-D2 (ADR-001 R5): run events on the record store. runRec is
|
|
472
|
+
// created LAZILY, the first time something WRITE-tool/ask_user-
|
|
473
|
+
// worthy happens — never for a plain chat/read-only turn, so this
|
|
474
|
+
// is a no-op for every mode/strategy that never offers WRITE tools
|
|
475
|
+
// (chat, document, build, classic Modify — agentToolsFor only
|
|
476
|
+
// offers WRITE_TOOLS for strategy:"agent" + mode:"modify"). Once
|
|
477
|
+
// created, it's a "todo" record (the same W4 machinery that
|
|
478
|
+
// already survives /refresh via rerenderRecord) so a mid-run
|
|
479
|
+
// refresh finds it; handleModifyResult upgrades it in place with
|
|
480
|
+
// real Plan: items via data._agentRunRecord instead of creating a
|
|
481
|
+
// second record, once the final turn's explanation arrives.
|
|
482
|
+
var runEvents = [];
|
|
483
|
+
var runRec = null;
|
|
484
|
+
|
|
485
|
+
function syncRunMarker(reason) {
|
|
486
|
+
if (!runRec) { return; }
|
|
487
|
+
writeRunMarker({
|
|
488
|
+
runId: runId,
|
|
489
|
+
action: runEntry,
|
|
490
|
+
appliedCount: runEvents.filter(function (e) { return e.t === "applied"; }).length,
|
|
491
|
+
conversationId: runConversationId
|
|
492
|
+
}, reason || "syncRunMarker");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function recordRunEvent(t, extra) {
|
|
496
|
+
if (!runRec) {
|
|
497
|
+
runRec = addRecord("todo", { action: runEntry, items: [], events: runEvents });
|
|
498
|
+
}
|
|
499
|
+
runEvents.push(Object.assign({ t: t, at: Date.now() }, extra || {}));
|
|
500
|
+
runRec.events = runEvents;
|
|
501
|
+
if (t === "done") {
|
|
502
|
+
clearRunMarker("run done: " + runId);
|
|
503
|
+
} else {
|
|
504
|
+
syncRunMarker("run event: " + t);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
310
507
|
|
|
311
508
|
function addUsage(usage) {
|
|
312
509
|
if (usage && typeof usage.total_tokens === "number") {
|
|
@@ -316,14 +513,184 @@
|
|
|
316
513
|
|
|
317
514
|
function fallbackToPlain() {
|
|
318
515
|
setAgentNarration("Continuing without tools…");
|
|
319
|
-
ajaxJson("POST", firstEndpoint, payload,
|
|
516
|
+
fpCurrentAgentRequest = ajaxJson("POST", firstEndpoint, payload, function (data) {
|
|
517
|
+
fpCurrentAgentRequest = null;
|
|
518
|
+
if (runRec) { recordRunEvent("done", {}); data._agentRunRecord = runRec; }
|
|
519
|
+
onDone(data);
|
|
520
|
+
}, function (err, xhr) {
|
|
521
|
+
fpCurrentAgentRequest = null;
|
|
522
|
+
if (fpAgentStopRequested) {
|
|
523
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "stopped by user" }); rerenderTodoRecord(runRec); }
|
|
524
|
+
onError("Stopped after " + Math.max(0, step - 1) + " tool call step(s) at your request.");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "fallback request failed" }); rerenderTodoRecord(runRec); }
|
|
528
|
+
// CLAUDE-023: forward xhr so a genuine parse error's raw
|
|
529
|
+
// response survives to handleExecuteError (via onModifyError)
|
|
530
|
+
// instead of being dropped here — ajaxJson's error callback
|
|
531
|
+
// is (msg, xhr), and onError expects the same shape.
|
|
532
|
+
onError(err, xhr);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function postNextStep(nextMessages) {
|
|
537
|
+
setAgentNarration("Thinking… (step " + step + "/" + AGENT_LOOP_MAX_STEPS + ")");
|
|
538
|
+
var stepPayload = Object.assign({
|
|
539
|
+
messages: nextMessages,
|
|
540
|
+
conversationId: payload.conversationId
|
|
541
|
+
}, stepExtra, {
|
|
542
|
+
strategy: runStrategy,
|
|
543
|
+
entry: runEntry,
|
|
544
|
+
runId: runId,
|
|
545
|
+
events: runEvents.slice()
|
|
546
|
+
});
|
|
547
|
+
if (pendingDebugNote) {
|
|
548
|
+
stepPayload.debugNote = pendingDebugNote;
|
|
549
|
+
pendingDebugNote = null;
|
|
550
|
+
}
|
|
551
|
+
fpCurrentAgentRequest = ajaxJson("POST", "flowpilot/agent-step", stepPayload,
|
|
552
|
+
function (stepData) { fpCurrentAgentRequest = null; handleStep(stepData, nextMessages); },
|
|
553
|
+
function (err, xhr) {
|
|
554
|
+
fpCurrentAgentRequest = null;
|
|
555
|
+
if (fpAgentStopRequested) {
|
|
556
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "stopped by user" }); rerenderTodoRecord(runRec); }
|
|
557
|
+
onError("Stopped after " + Math.max(0, step - 1) + " tool call step(s) at your request.");
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "agent-step request failed" }); rerenderTodoRecord(runRec); }
|
|
561
|
+
// CLAUDE-023: forward xhr — see fallbackToPlain's error
|
|
562
|
+
// callback above for why this must not be dropped.
|
|
563
|
+
onError(err, xhr);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Processes calls[idx..] one at a time. A WRITE-gated call (per
|
|
568
|
+
// toolTiers, classified against SAFE_NODE_TYPES) or an ask_user
|
|
569
|
+
// call PAUSES here — rendering a chip/question and waiting for a
|
|
570
|
+
// user action — instead of executing synchronously like the
|
|
571
|
+
// existing 6 READ tools still do. Once the whole batch is
|
|
572
|
+
// resolved, posts the next step exactly as before.
|
|
573
|
+
function processToolCallsFrom(calls, idx, nextMessages, toolTiers) {
|
|
574
|
+
if (idx >= calls.length) { postNextStep(nextMessages); return; }
|
|
575
|
+
|
|
576
|
+
var call = calls[idx];
|
|
577
|
+
var name = call.function.name;
|
|
578
|
+
var args = parseToolCallArgs(call);
|
|
579
|
+
if (name === "redirect_mode") {
|
|
580
|
+
var redirectResult = executeAgentToolCall(call, runIdMap, runHistoryEvents);
|
|
581
|
+
nextMessages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(redirectResult) });
|
|
582
|
+
if (runRec) {
|
|
583
|
+
recordRunEvent("redirect", { detail: (redirectResult && redirectResult.suggestedAction && redirectResult.suggestedAction.mode) || args.mode || "unknown" });
|
|
584
|
+
recordRunEvent("done", {});
|
|
585
|
+
}
|
|
586
|
+
onDone({
|
|
587
|
+
explanation: (redirectResult && redirectResult.explanation) || "",
|
|
588
|
+
prose: true,
|
|
589
|
+
flow: null,
|
|
590
|
+
suggestedAction: redirectResult && redirectResult.suggestedAction ? redirectResult.suggestedAction : null
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
var isWriteTool = name === "apply_step" || name === "remove_step" || name === "rename_node" || name === "group_nodes";
|
|
595
|
+
// P10-D1: only WRITE tool calls get an opId — idempotency is
|
|
596
|
+
// about preventing double MUTATION, not about read tools.
|
|
597
|
+
var opId = isWriteTool ? (runId + ":" + call.id) : null;
|
|
598
|
+
|
|
599
|
+
// P10-D1: a repeat opId (duplicate delivery, a retry, or the
|
|
600
|
+
// model repeating a call) returns the SAME result without
|
|
601
|
+
// re-invoking the executor — the graph is mutated at most once
|
|
602
|
+
// per opId, checked against the per-conversation applied-ops
|
|
603
|
+
// map before mutating (main.js).
|
|
604
|
+
function runExecutorIdempotent() {
|
|
605
|
+
if (opId) {
|
|
606
|
+
var recorded = getAppliedOp(runConversationId, opId);
|
|
607
|
+
if (recorded) { return recorded; }
|
|
608
|
+
}
|
|
609
|
+
var result = executeAgentToolCall(call, runIdMap, runHistoryEvents);
|
|
610
|
+
// CLAUDE-013: merge this call's own new placeholder->real-id
|
|
611
|
+
// mappings (e.g. apply_step's newNodes) into the run-scoped
|
|
612
|
+
// map so a LATER call this run can resolve the same ids.
|
|
613
|
+
if (result && result.idMap) { Object.assign(runIdMap, result.idMap); }
|
|
614
|
+
if (opId) { recordAppliedOp(runConversationId, opId, result); }
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function continueWithResult(resultObj) {
|
|
619
|
+
var _reason = null;
|
|
620
|
+
if (resultObj && !resultObj.allPass) {
|
|
621
|
+
_reason = resultObj.error || resultObj.reason || null;
|
|
622
|
+
}
|
|
623
|
+
if (isWriteTool) { agentWriteResults.push({ allPass: !!(resultObj && resultObj.allPass), reason: _reason }); }
|
|
624
|
+
nextMessages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(resultObj) });
|
|
625
|
+
processToolCallsFrom(calls, idx + 1, nextMessages, toolTiers);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (name === "ask_user") {
|
|
629
|
+
if (askUserRounds >= AGENT_ASK_USER_MAX_ROUNDTRIPS) {
|
|
630
|
+
continueWithResult({ error: "ask_user budget (" + AGENT_ASK_USER_MAX_ROUNDTRIPS +
|
|
631
|
+
") exhausted for this turn — proceed with your best judgment or give a final answer." });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
askUserRounds++;
|
|
635
|
+
setAgentNarration(describeAgentToolCall(name, args));
|
|
636
|
+
recordRunEvent("asked", { detail: args.question });
|
|
637
|
+
renderAskUserQuestion({ question: args.question, options: args.options,
|
|
638
|
+
onAnswer: function (answerText) {
|
|
639
|
+
recordRunEvent("answered", { detail: answerText });
|
|
640
|
+
if (currentSettings.debugLogging) { pendingDebugNote = "user answered: " + answerText; }
|
|
641
|
+
continueWithResult({ answer: answerText });
|
|
642
|
+
} });
|
|
643
|
+
return; // PAUSES here until the question is answered
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (isWriteTool) { recordRunEvent("step", { opId: opId, detail: name }); }
|
|
647
|
+
|
|
648
|
+
var tier = toolTiers && toolTiers[call.id];
|
|
649
|
+
if (writeToolCallNeedsConsent(tier, name, args)) {
|
|
650
|
+
if (isWriteTool) { recordRunEvent("consent", { opId: opId, detail: name }); }
|
|
651
|
+
renderAgentToolConsentGate({
|
|
652
|
+
name: name, args: args,
|
|
653
|
+
onResume: function (granted) {
|
|
654
|
+
if (currentSettings.debugLogging) {
|
|
655
|
+
pendingDebugNote = granted ? "user clicked Proceed" : "user clicked Skip this step";
|
|
656
|
+
}
|
|
657
|
+
if (!granted) {
|
|
658
|
+
continueWithResult({ skipped: true, reason: "user declined — this call was not applied" });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
setAgentNarration(describeAgentToolCall(name, args) + " (step " + step + "/" + AGENT_LOOP_MAX_STEPS + ")");
|
|
662
|
+
var result = runExecutorIdempotent();
|
|
663
|
+
if (isWriteTool) {
|
|
664
|
+
recordRunEvent("applied", { opId: opId });
|
|
665
|
+
recordRunEvent("verified", { opId: opId, detail: !!(result && result.allPass) });
|
|
666
|
+
}
|
|
667
|
+
continueWithResult(result);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
return; // PAUSES here until Proceed/Skip is clicked
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
setAgentNarration(describeAgentToolCall(name, args) + " (step " + step + "/" + AGENT_LOOP_MAX_STEPS + ")");
|
|
674
|
+
var autoResult = runExecutorIdempotent();
|
|
675
|
+
if (isWriteTool) {
|
|
676
|
+
recordRunEvent("applied", { opId: opId });
|
|
677
|
+
recordRunEvent("verified", { opId: opId, detail: !!(autoResult && autoResult.allPass) });
|
|
678
|
+
}
|
|
679
|
+
continueWithResult(autoResult);
|
|
320
680
|
}
|
|
321
681
|
|
|
322
682
|
function handleStep(data, messages) {
|
|
323
683
|
addUsage(data.usage);
|
|
324
684
|
|
|
685
|
+
if (data.fallbackToClassic) {
|
|
686
|
+
fallbackToPlain();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
325
690
|
if (!data.toolCalls || !data.toolCalls.length) {
|
|
326
691
|
if (step > 0) { addAgentStatsNote(step, totalTokens); }
|
|
692
|
+
if (agentWriteResults.length) { data._agentWriteResults = agentWriteResults.slice(); }
|
|
693
|
+
if (runRec) { recordRunEvent("done", {}); data._agentRunRecord = runRec; }
|
|
327
694
|
onDone(data);
|
|
328
695
|
return;
|
|
329
696
|
}
|
|
@@ -336,50 +703,76 @@
|
|
|
336
703
|
return;
|
|
337
704
|
}
|
|
338
705
|
|
|
339
|
-
step
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
706
|
+
// An ask_user-ONLY batch doesn't consume the real step budget —
|
|
707
|
+
// see AGENT_ASK_USER_MAX_ROUNDTRIPS above. A batch mixing
|
|
708
|
+
// ask_user with any other tool call counts normally, since real
|
|
709
|
+
// work happened in it too.
|
|
710
|
+
var isAskUserOnlyBatch = data.toolCalls.every(function (c) { return c.function.name === "ask_user"; });
|
|
711
|
+
if (!isAskUserOnlyBatch) {
|
|
712
|
+
step++;
|
|
713
|
+
if (step > AGENT_LOOP_MAX_STEPS) {
|
|
714
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "step budget exceeded" }); rerenderTodoRecord(runRec); }
|
|
715
|
+
onError("FlowPilot stopped after " + AGENT_LOOP_MAX_STEPS +
|
|
716
|
+
" tool call(s) without a final answer. Try breaking your " +
|
|
717
|
+
"request into smaller steps, or be more specific about " +
|
|
718
|
+
"which node(s) or flow you mean.");
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
346
721
|
}
|
|
347
722
|
if (totalTokens > AGENT_LOOP_TOKEN_CEILING) {
|
|
723
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "token ceiling exceeded" }); rerenderTodoRecord(runRec); }
|
|
348
724
|
onError("FlowPilot stopped after using " + totalTokens +
|
|
349
|
-
" tokens on this turn
|
|
350
|
-
"selecting fewer nodes,
|
|
351
|
-
"question
|
|
725
|
+
" tokens on this turn (limit: " + AGENT_LOOP_TOKEN_CEILING +
|
|
726
|
+
") without a final answer. Try selecting fewer nodes, asking a " +
|
|
727
|
+
"more specific question, or raising \"Max total tokens per " +
|
|
728
|
+
"agent turn\" in Settings → Behavior.");
|
|
352
729
|
return;
|
|
353
730
|
}
|
|
354
731
|
if (fpAgentStopRequested) {
|
|
732
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "stopped by user" }); rerenderTodoRecord(runRec); }
|
|
355
733
|
onError("Stopped after " + (step - 1) + " tool call step(s) at your request.");
|
|
356
734
|
return;
|
|
357
735
|
}
|
|
358
736
|
|
|
359
737
|
var nextMessages = (messages || data.messages || []).slice();
|
|
360
738
|
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);
|
|
739
|
+
processToolCallsFrom(data.toolCalls, 0, nextMessages, data.toolTiers);
|
|
377
740
|
}
|
|
378
741
|
|
|
379
|
-
var firstPayload = Object.assign({}, payload, { tools: true });
|
|
380
|
-
ajaxJson("POST", firstEndpoint, firstPayload, function (data) {
|
|
742
|
+
var firstPayload = Object.assign({}, payload, { tools: true, runId: runId });
|
|
743
|
+
fpCurrentAgentRequest = ajaxJson("POST", firstEndpoint, firstPayload, function (data) {
|
|
744
|
+
fpCurrentAgentRequest = null;
|
|
381
745
|
handleStep(data, null);
|
|
382
|
-
}, function () {
|
|
746
|
+
}, function (err, xhr) {
|
|
747
|
+
fpCurrentAgentRequest = null;
|
|
748
|
+
if (fpAgentStopRequested) {
|
|
749
|
+
// CLAUDE-022: an abort of the FIRST request must actually
|
|
750
|
+
// stop, not fallbackToPlain() — falling back would ignore
|
|
751
|
+
// the stop and keep going without tools instead.
|
|
752
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "stopped by user" }); rerenderTodoRecord(runRec); }
|
|
753
|
+
onError("Stopped after " + Math.max(0, step - 1) + " tool call step(s) at your request.");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
// CLAUDE-023: fallbackToPlain() must only fire for failures that
|
|
757
|
+
// actually look like "the provider doesn't support tools" — a
|
|
758
|
+
// genuine parse/validation failure from processGenerationContent
|
|
759
|
+
// (flowpilot.js) always comes back as HTTP 422 with a .raw
|
|
760
|
+
// payload attached (see err.status = 422 / err.raw = content
|
|
761
|
+
// there). A provider-level failure of the tools:true call itself
|
|
762
|
+
// (unrecognized "tools" field, model swapped since the last
|
|
763
|
+
// probe, etc.) never goes through that parser, so it always
|
|
764
|
+
// reaches here as some other status with no .raw. Only the
|
|
765
|
+
// latter should be silently retried without tools; the former is
|
|
766
|
+
// a real failure and must surface as one so handleExecuteError
|
|
767
|
+
// (via onModifyError) gets a chance to show it — including its
|
|
768
|
+
// raw JSON in Debug mode.
|
|
769
|
+
var raw = xhr && xhr.responseJSON && xhr.responseJSON.raw;
|
|
770
|
+
var looksLikeParseFailure = !!raw || (xhr && xhr.status === 422);
|
|
771
|
+
if (looksLikeParseFailure) {
|
|
772
|
+
if (runRec) { recordRunEvent("interrupted", { detail: "first agentic request failed" }); rerenderTodoRecord(runRec); }
|
|
773
|
+
onError(err, xhr);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
383
776
|
// The provider was probed as
|
|
384
777
|
// supportsTools, but the very first tools:true request failed
|
|
385
778
|
// outright — e.g. the model was swapped since the last probe, or
|
|
@@ -622,8 +1015,14 @@
|
|
|
622
1015
|
// flow; show the raw so the user can see what happened.
|
|
623
1016
|
function handleExecuteError(msg, raw) {
|
|
624
1017
|
popDanglingUserHistory();
|
|
625
|
-
|
|
626
|
-
|
|
1018
|
+
if (raw && currentSettings.debugLogging) {
|
|
1019
|
+
addMessage("error", msg);
|
|
1020
|
+
addGeneratedJson(raw, true);
|
|
1021
|
+
} else if (raw) {
|
|
1022
|
+
addMessage("error", msg + " Enable Debug mode in Settings to see the full response next time.");
|
|
1023
|
+
} else {
|
|
1024
|
+
addMessage("error", msg);
|
|
1025
|
+
}
|
|
627
1026
|
setBusy(false);
|
|
628
1027
|
}
|
|
629
1028
|
|
|
@@ -632,6 +1031,28 @@
|
|
|
632
1031
|
function applySuggestedAction(suggestedAction) {
|
|
633
1032
|
if (!suggestedAction || !suggestedAction.mode || !suggestedAction.prompt) { return; }
|
|
634
1033
|
armExecuteAction(suggestedAction.mode);
|
|
1034
|
+
// CLAUDE-015: the model already identified which nodes this action
|
|
1035
|
+
// targets (including "the whole flow" as a valid identification,
|
|
1036
|
+
// not an exemption) — override whatever armExecuteAction's own
|
|
1037
|
+
// pinCurrentSelection() pinned from the live canvas selection, the
|
|
1038
|
+
// same pinnedSelectionIds every downstream consumer (Document,
|
|
1039
|
+
// Modify, their guards, their context builders) already reads via
|
|
1040
|
+
// activeSelectionIds(). Absent targetNodeIds: unchanged, today's
|
|
1041
|
+
// behavior. Mirrors pinCurrentSelection()'s own rule of only
|
|
1042
|
+
// overwriting on a non-empty result, so a bad/empty resolution
|
|
1043
|
+
// doesn't blow away a legitimate live-selection pin.
|
|
1044
|
+
if (suggestedAction.targetNodeIds === "all") {
|
|
1045
|
+
var allTabIds = allActiveTabNodeIds();
|
|
1046
|
+
if (allTabIds.length) { pinnedSelectionIds = allTabIds; }
|
|
1047
|
+
} else if (suggestedAction.targetNodeIds === "instance") {
|
|
1048
|
+
var allInstanceIds = allInstanceNodeIds();
|
|
1049
|
+
if (allInstanceIds.length) { pinnedSelectionIds = allInstanceIds; }
|
|
1050
|
+
} else if (Array.isArray(suggestedAction.targetNodeIds)) {
|
|
1051
|
+
var resolvedIds = suggestedAction.targetNodeIds.filter(function (id) {
|
|
1052
|
+
return !!findLiveNode(id);
|
|
1053
|
+
});
|
|
1054
|
+
if (resolvedIds.length) { pinnedSelectionIds = resolvedIds; }
|
|
1055
|
+
}
|
|
635
1056
|
var $promptBox = el("#fp-prompt");
|
|
636
1057
|
if ($promptBox.length) {
|
|
637
1058
|
$promptBox.val(suggestedAction.prompt);
|
|
@@ -660,11 +1081,23 @@
|
|
|
660
1081
|
var isChatMode = suggestedAction.mode === "chat";
|
|
661
1082
|
var titleText = suggestedAction.customTitle || (isChatMode ? "Switch to Chat" : "Cleared for takeoff — " + modeLabel);
|
|
662
1083
|
|
|
1084
|
+
// CLAUDE-028: the record must exist before the button does, so the
|
|
1085
|
+
// button can carry data-fp-record-id/-action — the same relay
|
|
1086
|
+
// markers renderAskUserQuestion's quick-reply buttons use to
|
|
1087
|
+
// survive the pop-out's innerHTML clone (bindRecordActionButtons
|
|
1088
|
+
// in init.js rebinds by these attributes and relays the click back
|
|
1089
|
+
// to this window via resolveRecordAction; this window is the only
|
|
1090
|
+
// one with live suggestedAction data to act on). The direct click
|
|
1091
|
+
// handler below still fires normally for the un-popped-out sidebar.
|
|
1092
|
+
var rec = addRecord("chip", { chipType: "suggestedAction", suggestedAction: suggestedAction });
|
|
1093
|
+
|
|
663
1094
|
var $row = $("<div>").addClass("fp-chip-row");
|
|
664
1095
|
var $card = $("<button>")
|
|
665
1096
|
.addClass("fp-chip fp-chip-card")
|
|
666
1097
|
.attr("type", "button")
|
|
667
1098
|
.attr("title", suggestedAction.prompt)
|
|
1099
|
+
.attr("data-fp-record-id", rec.id)
|
|
1100
|
+
.attr("data-fp-record-action", "apply-suggested-action")
|
|
668
1101
|
.on("click", function () { applySuggestedAction(suggestedAction); });
|
|
669
1102
|
$("<span>").addClass("fp-chip-icon")
|
|
670
1103
|
.append($("<i>").addClass(isChatMode ? "fa fa-comment" : "fa fa-paper-plane"))
|
|
@@ -675,12 +1108,28 @@
|
|
|
675
1108
|
$("<span>").addClass("fp-chip-go").html("›").appendTo($card);
|
|
676
1109
|
$card.appendTo($row);
|
|
677
1110
|
|
|
678
|
-
|
|
679
|
-
|
|
1111
|
+
// CLAUDE-030: Document mode always requires a real, non-empty
|
|
1112
|
+
// selection at Send time (server-side: describeSelectionContext
|
|
1113
|
+
// returns null and /flowpilot/document 400s on "Select the node(s)
|
|
1114
|
+
// you want documented first" whenever nothing is actually selected)
|
|
1115
|
+
// — but the model has occasionally generated a selectionHint
|
|
1116
|
+
// implying otherwise (e.g. "leave nothing selected to document...
|
|
1117
|
+
// general Node-RED info") when it had no concrete node to name.
|
|
1118
|
+
// Rather than re-word the model's already-fragile mode-mismatch
|
|
1119
|
+
// prompt guidance under time pressure, override deterministically
|
|
1120
|
+
// here: if this chip targets Document and doesn't carry a real
|
|
1121
|
+
// resolved target, always show the one hint that's actually true.
|
|
1122
|
+
var targetResolved = suggestedAction.targetNodeIds === "all" ||
|
|
1123
|
+
suggestedAction.targetNodeIds === "instance" ||
|
|
1124
|
+
(Array.isArray(suggestedAction.targetNodeIds) && suggestedAction.targetNodeIds.length > 0);
|
|
1125
|
+
var hintText = (suggestedAction.mode === "document" && !targetResolved)
|
|
1126
|
+
? "Select the node(s) you want documented first."
|
|
1127
|
+
: suggestedAction.selectionHint;
|
|
1128
|
+
if (hintText) {
|
|
1129
|
+
$("<div>").addClass("fp-chip-hint").text("Tip: " + hintText).appendTo($row);
|
|
680
1130
|
}
|
|
681
1131
|
|
|
682
1132
|
$box.append($row);
|
|
683
|
-
addRecord("chip", { chipType: "suggestedAction", suggestedAction: suggestedAction });
|
|
684
1133
|
scrollMessagesToBottom();
|
|
685
1134
|
}
|
|
686
1135
|
|
|
@@ -772,6 +1221,28 @@
|
|
|
772
1221
|
scrollMessagesToBottom();
|
|
773
1222
|
}
|
|
774
1223
|
|
|
1224
|
+
// Detects the model's own tool-call envelope leaking through as
|
|
1225
|
+
// "prose" — e.g. a vague first-turn Modify reply the server marked
|
|
1226
|
+
// prose:true but whose explanation is actually the raw
|
|
1227
|
+
// {"explanation":...,"changes":[...]} JSON as literal text. Starting
|
|
1228
|
+
// with "{" alone doesn't make text suspect (real prose can open with
|
|
1229
|
+
// a brace), so this also requires it to parse as an object carrying
|
|
1230
|
+
// one of FlowPilot's own envelope keys.
|
|
1231
|
+
function looksLikeToolEnvelope(text) {
|
|
1232
|
+
if (typeof text !== "string") { return false; }
|
|
1233
|
+
var trimmed = text.trim();
|
|
1234
|
+
if (trimmed.charAt(0) !== "{") { return false; }
|
|
1235
|
+
var parsed;
|
|
1236
|
+
try {
|
|
1237
|
+
parsed = JSON.parse(trimmed);
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return false; }
|
|
1242
|
+
return ["changes", "newNodes", "newWires", "removeNodes", "newGroups", "explanation"]
|
|
1243
|
+
.some(function (key) { return Object.prototype.hasOwnProperty.call(parsed, key); });
|
|
1244
|
+
}
|
|
1245
|
+
|
|
775
1246
|
// Shared by Generate/Document/Modify result handlers: renders the model's
|
|
776
1247
|
// clarifying-question or prose-only envelope as a normal assistant
|
|
777
1248
|
// message and leaves the action armed for a follow-up. Returns true if it
|
|
@@ -789,6 +1260,11 @@
|
|
|
789
1260
|
return true;
|
|
790
1261
|
}
|
|
791
1262
|
if (data.prose) {
|
|
1263
|
+
if (looksLikeToolEnvelope(data.explanation)) {
|
|
1264
|
+
handleExecuteError("FlowPilot's reply didn't come through as expected.", data.explanation);
|
|
1265
|
+
updateSelectionStatus();
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
792
1268
|
addMessage("assistant", data.explanation || "(no content returned)");
|
|
793
1269
|
pushHistory("assistant", data.explanation || "");
|
|
794
1270
|
renderActionChip(data.suggestedAction);
|
|
@@ -810,7 +1286,34 @@
|
|
|
810
1286
|
|
|
811
1287
|
// Lay nodes out before review/import — see layoutGeneratedFlow for why.
|
|
812
1288
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
813
|
-
|
|
1289
|
+
var planItems = parseTodoPlan(data.explanation || "");
|
|
1290
|
+
var todoRec = null;
|
|
1291
|
+
var writeResults = Array.isArray(data._agentWriteResults) ? data._agentWriteResults : [];
|
|
1292
|
+
if (writeResults.length) {
|
|
1293
|
+
if (!planItems.length) {
|
|
1294
|
+
planItems = [{ text: goalPrompt || "Generate flow", status: "pending" }];
|
|
1295
|
+
}
|
|
1296
|
+
planItems.forEach(function (item, i) {
|
|
1297
|
+
item.status = (i < writeResults.length)
|
|
1298
|
+
? (writeResults[i].allPass ? "done" : "failed")
|
|
1299
|
+
: "active";
|
|
1300
|
+
});
|
|
1301
|
+
if (data._agentRunRecord) {
|
|
1302
|
+
todoRec = data._agentRunRecord;
|
|
1303
|
+
todoRec.action = "generate";
|
|
1304
|
+
todoRec.items = planItems;
|
|
1305
|
+
} else {
|
|
1306
|
+
todoRec = addRecord("todo", { action: "generate", items: planItems });
|
|
1307
|
+
}
|
|
1308
|
+
rerenderTodoRecord(todoRec);
|
|
1309
|
+
var deterministicSummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
1310
|
+
addMessage("assistant", deterministicSummary || "(no explanation returned)");
|
|
1311
|
+
if (shouldShowSecondaryExplanation(deterministicSummary, data.explanation)) {
|
|
1312
|
+
addMessage("fp-notice", data.explanation);
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1316
|
+
}
|
|
814
1317
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
815
1318
|
// B1: bake the deploy-verify option into the review panel as the
|
|
816
1319
|
// primary chip rather than a separate chip below it. Only for
|
|
@@ -821,7 +1324,9 @@
|
|
|
821
1324
|
var _buildOnImported = (goalPrompt && !activeBuildLoop && _hasDeployable)
|
|
822
1325
|
? function (importResult) { startBuildLoop(goalPrompt, flow, importResult); }
|
|
823
1326
|
: null;
|
|
824
|
-
|
|
1327
|
+
if (!writeResults.length) {
|
|
1328
|
+
addGeneratedReview(flow, _buildOnImported, _buildOnImported ? goalPrompt : null);
|
|
1329
|
+
}
|
|
825
1330
|
// Suppress a server-suggested build chip when deploy-verify is already
|
|
826
1331
|
// the primary action inside the review panel — it would be a duplicate.
|
|
827
1332
|
renderActionChip(_buildOnImported && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
@@ -839,17 +1344,69 @@
|
|
|
839
1344
|
// W4: parse and surface the Plan: block if present.
|
|
840
1345
|
var planItems = parseTodoPlan(data.explanation || "");
|
|
841
1346
|
var todoRec = null;
|
|
1347
|
+
// W7: real per-item correlation, and (CLAUDE-011) the signal that
|
|
1348
|
+
// real mutations already happened this turn via WRITE tool calls
|
|
1349
|
+
// (apply_step/remove_step/rename_node/group_nodes) — each already
|
|
1350
|
+
// consent-gated (or, for group_nodes' write-safe tier, always
|
|
1351
|
+
// auto-applied) and applied through its own review chip. Hoisted
|
|
1352
|
+
// above the planItems.length check (rather than declared inside
|
|
1353
|
+
// it) so it's always defined below even on a turn with no Plan:
|
|
1354
|
+
// block, e.g. a wrap-up-only final response after tool calls.
|
|
1355
|
+
var writeResults = Array.isArray(data._agentWriteResults) ? data._agentWriteResults : [];
|
|
842
1356
|
if (planItems.length) {
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
|
|
1357
|
+
// Each WRITE tool call this turn resolves ONE plan item, in
|
|
1358
|
+
// order (§16 point 2 — "one WRITE tool call = one plan/todo
|
|
1359
|
+
// item"). This is the deferred half of CLAUDE-005 (c6c2e84),
|
|
1360
|
+
// now unblocked: that fix could only mark the WHOLE plan
|
|
1361
|
+
// active/resolved together because there was no way to know
|
|
1362
|
+
// which check proved which numbered line — a per-call tool
|
|
1363
|
+
// result finally gives that mapping for free. Items beyond the
|
|
1364
|
+
// resolved-call count (or ALL items, when no WRITE tool call
|
|
1365
|
+
// was made this turn — the default, non-agentic-write path)
|
|
1366
|
+
// fall back to CLAUDE-005's original all-together marking
|
|
1367
|
+
// below, resolved via the ordinary aggregate verifySteps
|
|
1368
|
+
// envelope path exactly as before — confirmed unchanged when
|
|
1369
|
+
// _agentWriteResults is empty/absent.
|
|
1370
|
+
if (writeResults.length) {
|
|
1371
|
+
planItems.forEach(function (item, i) {
|
|
1372
|
+
item.status = (i < writeResults.length)
|
|
1373
|
+
? (writeResults[i].allPass ? "done" : "failed")
|
|
1374
|
+
: "active";
|
|
1375
|
+
});
|
|
1376
|
+
} else {
|
|
1377
|
+
// Verification only produces one aggregate pass/fail result
|
|
1378
|
+
// for the whole Modify response — mark every item active up
|
|
1379
|
+
// front so they all resolve together instead of leaving
|
|
1380
|
+
// items 2+ stuck at "pending" forever (only item 1 would
|
|
1381
|
+
// ever flip otherwise).
|
|
1382
|
+
planItems.forEach(function (item) { item.status = "active"; });
|
|
1383
|
+
}
|
|
1384
|
+
// P10-D2: if this run already has a live "todo" record from
|
|
1385
|
+
// WRITE-tool events (runAgentLoop, modes.js), upgrade it in
|
|
1386
|
+
// place with the real Plan: items instead of creating a
|
|
1387
|
+
// second record for the same run — so a run that paused
|
|
1388
|
+
// mid-flight (refresh, interrupted) and one that completed
|
|
1389
|
+
// normally both end up as ONE record with both events and
|
|
1390
|
+
// items.
|
|
1391
|
+
if (data._agentRunRecord) {
|
|
1392
|
+
todoRec = data._agentRunRecord;
|
|
1393
|
+
todoRec.action = "modify";
|
|
1394
|
+
todoRec.items = planItems;
|
|
1395
|
+
} else {
|
|
1396
|
+
todoRec = addRecord("todo", { action: "modify", items: planItems });
|
|
1397
|
+
}
|
|
849
1398
|
rerenderTodoRecord(todoRec);
|
|
850
1399
|
}
|
|
851
1400
|
|
|
852
|
-
|
|
1401
|
+
if (writeResults.length) {
|
|
1402
|
+
var deterministicModifySummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
1403
|
+
addMessage("assistant", deterministicModifySummary || "(no explanation returned)");
|
|
1404
|
+
if (shouldShowSecondaryExplanation(deterministicModifySummary, data.explanation)) {
|
|
1405
|
+
addMessage("fp-notice", data.explanation);
|
|
1406
|
+
}
|
|
1407
|
+
} else {
|
|
1408
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
1409
|
+
}
|
|
853
1410
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
854
1411
|
if (data.skippedNote) { addMessage("assistant", "⚠ " + data.skippedNote); }
|
|
855
1412
|
|
|
@@ -870,7 +1427,16 @@
|
|
|
870
1427
|
}
|
|
871
1428
|
} : applyModifications;
|
|
872
1429
|
|
|
873
|
-
|
|
1430
|
+
// CLAUDE-011: agentWriteRules tells the model to omit changes/
|
|
1431
|
+
// newNodes/newWires/removeNodes/newGroups on this final response
|
|
1432
|
+
// when writeResults is non-empty, so already-applied work isn't
|
|
1433
|
+
// proposed a second time — but the model doesn't always comply.
|
|
1434
|
+
// Don't rely on the prompt alone: skip rendering a second review/
|
|
1435
|
+
// apply flow whenever real WRITE-tool work happened this turn,
|
|
1436
|
+
// regardless of what the final envelope contains.
|
|
1437
|
+
if (!writeResults.length) {
|
|
1438
|
+
addModifyReview(data.flow, data.newNodes || [], data.newWires || [], data.removeNodes || [], applyCallback, null, data.newGroups || []);
|
|
1439
|
+
}
|
|
874
1440
|
renderActionChip(data.suggestedAction);
|
|
875
1441
|
setBusy(false);
|
|
876
1442
|
updateSelectionStatus();
|
|
@@ -1041,13 +1607,17 @@
|
|
|
1041
1607
|
|
|
1042
1608
|
var ap = activeProvider();
|
|
1043
1609
|
var isAgentLoop = ap && ap.supportsTools;
|
|
1610
|
+
var useAgentStrategy = isAgentLoop && endpointName === "generate" &&
|
|
1611
|
+
currentSettings.enableAgentWrite === true;
|
|
1044
1612
|
|
|
1045
1613
|
setBusy(true);
|
|
1046
1614
|
showPending(isAgentLoop);
|
|
1047
1615
|
var payload = {
|
|
1048
1616
|
prompt: prompt, context: context,
|
|
1049
1617
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1050
|
-
conversationId: conversationId
|
|
1618
|
+
conversationId: conversationId,
|
|
1619
|
+
strategy: useAgentStrategy ? "agent" : "classic",
|
|
1620
|
+
entry: endpointName
|
|
1051
1621
|
};
|
|
1052
1622
|
|
|
1053
1623
|
function onError(msg, xhr) {
|
|
@@ -1111,6 +1681,40 @@
|
|
|
1111
1681
|
return items;
|
|
1112
1682
|
}
|
|
1113
1683
|
|
|
1684
|
+
function buildDeterministicRunSummary(planItems, writeResults) {
|
|
1685
|
+
var lines = [];
|
|
1686
|
+
var unreached = 0;
|
|
1687
|
+
var items = planItems || [];
|
|
1688
|
+
var counted = Math.max(items.length, (writeResults || []).length);
|
|
1689
|
+
for (var i = 0; i < counted; i++) {
|
|
1690
|
+
var item = items[i];
|
|
1691
|
+
if (item && (i >= writeResults.length || item.status === "active" || item.status === "pending")) {
|
|
1692
|
+
unreached++;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
var itemText = (item && item.text) ? item.text : ("Step " + (i + 1));
|
|
1696
|
+
if (writeResults[i] && writeResults[i].allPass) {
|
|
1697
|
+
lines.push("✓ " + itemText);
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
var reason = (writeResults[i] && writeResults[i].reason) || "failed verification";
|
|
1701
|
+
lines.push("✗ " + itemText + " — " + reason);
|
|
1702
|
+
}
|
|
1703
|
+
if (unreached) {
|
|
1704
|
+
lines.push("▶ " + unreached + " step(s) not reached");
|
|
1705
|
+
}
|
|
1706
|
+
return lines.join("\n");
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function shouldShowSecondaryExplanation(summaryText, explanationText) {
|
|
1710
|
+
if (!explanationText || typeof explanationText !== "string") { return false; }
|
|
1711
|
+
if (!summaryText || typeof summaryText !== "string") { return true; }
|
|
1712
|
+
function normalize(text) {
|
|
1713
|
+
return String(text || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1714
|
+
}
|
|
1715
|
+
return normalize(summaryText) !== normalize(explanationText);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1114
1718
|
// W4: render or re-render a "todo" record. For a 1-item plan, renders
|
|
1115
1719
|
// as a compact status line (one chip). For N>1 items, renders as a
|
|
1116
1720
|
// checklist card. Updates in place when the record already has a
|
|
@@ -1147,6 +1751,25 @@
|
|
|
1147
1751
|
$wrap.append($ul);
|
|
1148
1752
|
}
|
|
1149
1753
|
|
|
1754
|
+
// P10-D2 (ADR-001 R5): a run whose last recorded event isn't "done"
|
|
1755
|
+
// was abandoned mid-flight — an explicit interruption (stop button,
|
|
1756
|
+
// step/token budget) or an orphaned request that never returned.
|
|
1757
|
+
// Freeze whatever checklist state exists (possibly none yet, if
|
|
1758
|
+
// interrupted before any Plan: text arrived) and say so plainly,
|
|
1759
|
+
// replacing the old silent-death behavior ("Continuing without
|
|
1760
|
+
// tools…" then nothing). "step N" counts completed (applied) WRITE
|
|
1761
|
+
// tool calls, not raw round-trips.
|
|
1762
|
+
if (Array.isArray(rec.events) && rec.events.length) {
|
|
1763
|
+
var lastEvent = rec.events[rec.events.length - 1];
|
|
1764
|
+
if (lastEvent.t !== "done") {
|
|
1765
|
+
var appliedCount = rec.events.filter(function (e) { return e.t === "applied"; }).length;
|
|
1766
|
+
$("<div>").addClass("fp-todo-interrupted").text(
|
|
1767
|
+
"⚠ This run was interrupted after step " + appliedCount +
|
|
1768
|
+
" — completed steps are applied (Ctrl+Z to undo). Re-send to continue from here."
|
|
1769
|
+
).appendTo($wrap);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1150
1773
|
if ($existing.length) {
|
|
1151
1774
|
$existing.replaceWith($wrap);
|
|
1152
1775
|
} else {
|
|
@@ -1193,11 +1816,11 @@
|
|
|
1193
1816
|
if (liveNode.type === "comment" || liveNode.type === "group") { return; }
|
|
1194
1817
|
if (liveNode._def && liveNode._def.category === "config") {
|
|
1195
1818
|
configTotal++;
|
|
1196
|
-
if (
|
|
1819
|
+
if (nodeExists(liveNode.id)) { configFound++; }
|
|
1197
1820
|
return;
|
|
1198
1821
|
}
|
|
1199
1822
|
total++;
|
|
1200
|
-
if (
|
|
1823
|
+
if (nodeExists(liveNode.id)) {
|
|
1201
1824
|
found++;
|
|
1202
1825
|
} else {
|
|
1203
1826
|
missing.push(liveNode.type || pid);
|
|
@@ -1240,58 +1863,77 @@
|
|
|
1240
1863
|
// assigns it and returns idMap). Property/absent checks already use
|
|
1241
1864
|
// real existing-node ids, so idMap[id] simply misses and falls through
|
|
1242
1865
|
// to the id unchanged.
|
|
1243
|
-
|
|
1244
|
-
|
|
1866
|
+
// One check-vocabulary evaluation (property/exists/absent/wire), shared
|
|
1867
|
+
// by verifyModifySteps (aggregate Modify verification) and W7's
|
|
1868
|
+
// per-tool-call check results (runChecksForToolResult, main.js) — same
|
|
1869
|
+
// approach, applied either to the whole verifySteps batch or scoped to
|
|
1870
|
+
// one WRITE tool call's own touched id(s). Returns null for an
|
|
1871
|
+
// unrecognized check type (caller should skip it, not count it either
|
|
1872
|
+
// way) or { ok, label } where label is the human-readable failure
|
|
1873
|
+
// description used in the aggregate "did not land as expected" message.
|
|
1874
|
+
// P10-E: each case delegates to graph-truth.js (same closure) — the
|
|
1875
|
+
// one implementation of graph truth, per ADR-005. Wire checks in
|
|
1876
|
+
// particular must never fall back to reading node.wires: RED.nodes.
|
|
1877
|
+
// addLink/removeLink never re-sync a live node's own .wires array
|
|
1878
|
+
// mid-session, so it's stale for anything added/removed this session.
|
|
1879
|
+
function runSingleVerifyCheck(step, idMap) {
|
|
1245
1880
|
idMap = idMap || {};
|
|
1246
1881
|
function resolve(id) { return (idMap && idMap[id]) || id; }
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
var
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
break;
|
|
1257
|
-
}
|
|
1258
|
-
case "exists": {
|
|
1259
|
-
ok = !!RED.nodes.node(resolve(step.nodeId));
|
|
1260
|
-
if (!ok) { failures.push(step.nodeId + " missing"); }
|
|
1261
|
-
break;
|
|
1262
|
-
}
|
|
1263
|
-
case "absent": {
|
|
1264
|
-
ok = !RED.nodes.node(resolve(step.nodeId));
|
|
1265
|
-
if (!ok) { failures.push(step.nodeId + " still present"); }
|
|
1266
|
-
break;
|
|
1267
|
-
}
|
|
1268
|
-
case "wire": {
|
|
1269
|
-
// Read from the live link registry (RED.nodes.eachLink), not
|
|
1270
|
-
// node.wires — RED.nodes.addLink/removeLink never re-sync a
|
|
1271
|
-
// live node's own .wires array mid-session (it's only set at
|
|
1272
|
-
// import and recomputed at export), so fromNode.wires[port] is
|
|
1273
|
-
// stale for any wire added/removed during the current editing
|
|
1274
|
-
// session. Mirrors computeWireDiff (apply-review.js).
|
|
1275
|
-
var fromId = resolve(step.fromId);
|
|
1276
|
-
var toId = resolve(step.toId);
|
|
1277
|
-
var port = step.fromPort || 0;
|
|
1278
|
-
ok = false;
|
|
1279
|
-
RED.nodes.eachLink(function (l) {
|
|
1280
|
-
if (ok) { return; }
|
|
1281
|
-
if (l.source && l.source.id === fromId &&
|
|
1282
|
-
(l.sourcePort || 0) === port &&
|
|
1283
|
-
l.target && l.target.id === toId) {
|
|
1284
|
-
ok = true;
|
|
1285
|
-
}
|
|
1286
|
-
});
|
|
1287
|
-
if (!ok) { failures.push("wire " + step.fromId + " → " + step.toId); }
|
|
1288
|
-
break;
|
|
1882
|
+
switch (step.check) {
|
|
1883
|
+
case "property": {
|
|
1884
|
+
var okP = propertyEquals(resolve(step.nodeId), step.prop, step.expected);
|
|
1885
|
+
var labelP = (step.prop || "property") + " on " + step.nodeId;
|
|
1886
|
+
if (!okP) {
|
|
1887
|
+
var readP = readProperty(resolve(step.nodeId), step.prop);
|
|
1888
|
+
labelP += readP.exists ?
|
|
1889
|
+
" (expected " + JSON.stringify(step.expected) + ", found " + JSON.stringify(readP.value) + ")" :
|
|
1890
|
+
" (node does not exist)";
|
|
1289
1891
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1892
|
+
return { ok: okP, label: labelP };
|
|
1893
|
+
}
|
|
1894
|
+
case "exists": {
|
|
1895
|
+
var okE = nodeExists(resolve(step.nodeId));
|
|
1896
|
+
return { ok: okE, label: step.nodeId + " missing" };
|
|
1897
|
+
}
|
|
1898
|
+
case "absent": {
|
|
1899
|
+
var okA = nodeAbsent(resolve(step.nodeId));
|
|
1900
|
+
return { ok: okA, label: step.nodeId + " still present" };
|
|
1901
|
+
}
|
|
1902
|
+
case "wire": {
|
|
1903
|
+
var fromId = resolve(step.fromId);
|
|
1904
|
+
var toId = resolve(step.toId);
|
|
1905
|
+
var port = step.fromPort || 0;
|
|
1906
|
+
var okW = wireExists(fromId, port, toId);
|
|
1907
|
+
var labelW = "wire " + step.fromId + " → " + step.toId;
|
|
1908
|
+
if (!okW) {
|
|
1909
|
+
var fromExists = nodeExists(fromId);
|
|
1910
|
+
var toExists = nodeExists(toId);
|
|
1911
|
+
if (!fromExists && !toExists) {
|
|
1912
|
+
labelW += " (neither node was ever created)";
|
|
1913
|
+
} else if (!fromExists) {
|
|
1914
|
+
labelW += " (" + step.fromId + " was never created)";
|
|
1915
|
+
} else if (!toExists) {
|
|
1916
|
+
labelW += " (" + step.toId + " was never created)";
|
|
1917
|
+
} else {
|
|
1918
|
+
labelW += " (both nodes exist but aren't connected)";
|
|
1919
|
+
}
|
|
1292
1920
|
}
|
|
1921
|
+
return { ok: okW, label: labelW };
|
|
1922
|
+
}
|
|
1923
|
+
default:
|
|
1924
|
+
return null; // unrecognized check type — don't count it either way
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function verifyModifySteps(verifySteps, idMap, todoRec) {
|
|
1929
|
+
if (!Array.isArray(verifySteps) || !verifySteps.length) { return; }
|
|
1930
|
+
|
|
1931
|
+
var total = 0, passed = 0, failures = [], failedSteps = [];
|
|
1932
|
+
verifySteps.forEach(function (step) {
|
|
1933
|
+
var result = runSingleVerifyCheck(step, idMap);
|
|
1934
|
+
if (!result) { return; }
|
|
1293
1935
|
total++;
|
|
1294
|
-
if (ok) { passed++; }
|
|
1936
|
+
if (result.ok) { passed++; } else { failures.push(result.label); failedSteps.push(step); }
|
|
1295
1937
|
});
|
|
1296
1938
|
|
|
1297
1939
|
if (total === 0) { return; }
|
|
@@ -1301,6 +1943,25 @@
|
|
|
1301
1943
|
} else {
|
|
1302
1944
|
addMessage("fp-notice", "⚠ Verification: " + passed + "/" + total + " change(s) confirmed — " +
|
|
1303
1945
|
failures.length + " did not land as expected (" + failures.join(", ") + ").");
|
|
1946
|
+
// CLAUDE-016: offer a "Fix this" chip naming exactly the nodes
|
|
1947
|
+
// involved in the failed checks, reusing CLAUDE-015/CODEX-014's
|
|
1948
|
+
// targetNodeIds auto-select mechanism rather than a new
|
|
1949
|
+
// retry/loop. User-initiated only — the chip pre-fills a Modify
|
|
1950
|
+
// prompt but still requires the user to review and click Send,
|
|
1951
|
+
// same as every other suggestedAction chip.
|
|
1952
|
+
function resolve(id) { return (idMap && idMap[id]) || id; }
|
|
1953
|
+
var targetNodeIds = [];
|
|
1954
|
+
failedSteps.forEach(function (step) {
|
|
1955
|
+
var ids = step.check === "wire" ? [resolve(step.fromId), resolve(step.toId)] : [resolve(step.nodeId)];
|
|
1956
|
+
ids.forEach(function (id) {
|
|
1957
|
+
if (id && targetNodeIds.indexOf(id) === -1) { targetNodeIds.push(id); }
|
|
1958
|
+
});
|
|
1959
|
+
});
|
|
1960
|
+
renderActionChip({
|
|
1961
|
+
mode: "modify",
|
|
1962
|
+
prompt: "Fix the following change(s) that didn't land as expected: " + failures.join(", "),
|
|
1963
|
+
targetNodeIds: targetNodeIds
|
|
1964
|
+
});
|
|
1304
1965
|
}
|
|
1305
1966
|
if (todoRec && todoRec.items) {
|
|
1306
1967
|
todoRec.items.forEach(function (item) {
|
|
@@ -1314,6 +1975,7 @@
|
|
|
1314
1975
|
hidePending();
|
|
1315
1976
|
if (renderQuestionOrProse(data)) { return; }
|
|
1316
1977
|
var flow = Array.isArray(data.flow) ? layoutGeneratedFlow(data.flow) : data.flow;
|
|
1978
|
+
var writeResults = Array.isArray(data._agentWriteResults) ? data._agentWriteResults : [];
|
|
1317
1979
|
|
|
1318
1980
|
// Build the todo plan. Parse "Plan:" from explanation if present;
|
|
1319
1981
|
// fall back to an implicit single item from the goal prompt.
|
|
@@ -1323,11 +1985,34 @@
|
|
|
1323
1985
|
}
|
|
1324
1986
|
// Same aggregate-verification reasoning as the Modify path: mark
|
|
1325
1987
|
// every item active up front so a multi-item plan resolves together.
|
|
1326
|
-
|
|
1327
|
-
|
|
1988
|
+
if (writeResults.length) {
|
|
1989
|
+
planItems.forEach(function (item, i) {
|
|
1990
|
+
item.status = (i < writeResults.length)
|
|
1991
|
+
? (writeResults[i].allPass ? "done" : "failed")
|
|
1992
|
+
: "active";
|
|
1993
|
+
});
|
|
1994
|
+
} else {
|
|
1995
|
+
planItems.forEach(function (item) { item.status = "active"; });
|
|
1996
|
+
}
|
|
1997
|
+
var todoRec;
|
|
1998
|
+
if (data._agentRunRecord) {
|
|
1999
|
+
todoRec = data._agentRunRecord;
|
|
2000
|
+
todoRec.action = "generate";
|
|
2001
|
+
todoRec.items = planItems;
|
|
2002
|
+
} else {
|
|
2003
|
+
todoRec = addRecord("todo", { action: "generate", items: planItems });
|
|
2004
|
+
}
|
|
1328
2005
|
rerenderTodoRecord(todoRec);
|
|
1329
2006
|
|
|
1330
|
-
|
|
2007
|
+
if (writeResults.length) {
|
|
2008
|
+
var deterministicQueueSummary = buildDeterministicRunSummary(planItems, writeResults);
|
|
2009
|
+
addMessage("assistant", deterministicQueueSummary || "(no explanation returned)");
|
|
2010
|
+
if (shouldShowSecondaryExplanation(deterministicQueueSummary, data.explanation)) {
|
|
2011
|
+
addMessage("fp-notice", data.explanation);
|
|
2012
|
+
}
|
|
2013
|
+
} else {
|
|
2014
|
+
addMessage("assistant", data.explanation || "(no explanation returned)");
|
|
2015
|
+
}
|
|
1331
2016
|
pushHistory("assistant", data.explanation || "(no explanation returned)");
|
|
1332
2017
|
// B1: when a build loop is appropriate, bake deploy-verify into the
|
|
1333
2018
|
// primary chip (same as handleSimpleGenerationResult). The callback
|
|
@@ -1342,7 +2027,9 @@
|
|
|
1342
2027
|
startBuildLoop(goalPrompt, flow, importResult);
|
|
1343
2028
|
}
|
|
1344
2029
|
: function (importResult) { verifyImportedNodes(importResult, todoRec); };
|
|
1345
|
-
|
|
2030
|
+
if (!writeResults.length) {
|
|
2031
|
+
addGeneratedReview(flow, _onImported, _wantLoop ? goalPrompt : null);
|
|
2032
|
+
}
|
|
1346
2033
|
// Suppress a server-suggested build chip when deploy-verify is already
|
|
1347
2034
|
// the primary action inside the review panel.
|
|
1348
2035
|
renderActionChip(_wantLoop && data.suggestedAction && data.suggestedAction.mode === "build"
|
|
@@ -1410,7 +2097,9 @@
|
|
|
1410
2097
|
var payload = {
|
|
1411
2098
|
prompt: instruction, context: context,
|
|
1412
2099
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1413
|
-
conversationId: conversationId
|
|
2100
|
+
conversationId: conversationId,
|
|
2101
|
+
strategy: "classic",
|
|
2102
|
+
entry: "build-existing"
|
|
1414
2103
|
};
|
|
1415
2104
|
|
|
1416
2105
|
function onBuildExistingError(msg, xhr) {
|
|
@@ -1543,12 +2232,16 @@
|
|
|
1543
2232
|
$("<button>")
|
|
1544
2233
|
.addClass("fp-consent-chip fp-consent-chip-primary")
|
|
1545
2234
|
.attr("type", "button")
|
|
2235
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2236
|
+
.attr("data-fp-record-action", "proceed")
|
|
1546
2237
|
.text("Auto-verify")
|
|
1547
2238
|
.on("click", function () { decide(true); })
|
|
1548
2239
|
.appendTo($row);
|
|
1549
2240
|
$("<button>")
|
|
1550
2241
|
.addClass("fp-consent-chip fp-consent-chip-alt")
|
|
1551
2242
|
.attr("type", "button")
|
|
2243
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2244
|
+
.attr("data-fp-record-action", "skip")
|
|
1552
2245
|
.text("I'll check myself")
|
|
1553
2246
|
.on("click", function () { decide(false); })
|
|
1554
2247
|
.appendTo($row);
|
|
@@ -1586,6 +2279,209 @@
|
|
|
1586
2279
|
renderActionChip(src.suggestedAction);
|
|
1587
2280
|
}
|
|
1588
2281
|
|
|
2282
|
+
// W7 — per-call consent gate for a write-gated agent tool call
|
|
2283
|
+
// (apply_step/remove_step/rename_node touching a node type outside
|
|
2284
|
+
// WRITE_GATE_SAFE_NODE_TYPES). Mirrors renderBuildConsentGate's shape
|
|
2285
|
+
// and CLAUDE-008's fp-consent-chip styling, generalized to hold an
|
|
2286
|
+
// arbitrary pending tool call instead of only a build-loop's node set.
|
|
2287
|
+
//
|
|
2288
|
+
// src.onResume is a live function reference, not plain-data-only —
|
|
2289
|
+
// this is a DELIBERATE, reported deviation from renderBuildConsentGate/
|
|
2290
|
+
// runBuildConsentDecision's full data-reconstruction pattern. Reason:
|
|
2291
|
+
// the actual continuation (resuming the SAME in-flight
|
|
2292
|
+
// runAgentLoop/handleStep batch — remaining tool calls, accumulated
|
|
2293
|
+
// messages, then POSTing the next step and continuing the loop) lives
|
|
2294
|
+
// in per-invocation closures that aren't all JSON-serializable
|
|
2295
|
+
// (onDone/onError are themselves ad-hoc closures at each Modify call
|
|
2296
|
+
// site, e.g. onModifyResult/onModifyError capturing goalPrompt/
|
|
2297
|
+
// existingNodeIds). This mirrors the established, already-shipped
|
|
2298
|
+
// rerenderGeneratedReview "onImported is a live function ref, valid
|
|
2299
|
+
// within the same session" pattern rather than the stricter one.
|
|
2300
|
+
// Confirmed the stricter pattern's actual reason — a genuinely separate
|
|
2301
|
+
// pop-out window JS realm — does NOT apply here: the agent loop only
|
|
2302
|
+
// ever runs in the main window (dispatchSend's pop-out branch relays
|
|
2303
|
+
// via postMessage back to the opener rather than running its own loop,
|
|
2304
|
+
// and runBuildConsentDecision/renderBuildConsentGate itself has no
|
|
2305
|
+
// pop-out relay path either — confirmed via grep, so this is no
|
|
2306
|
+
// weaker than the existing shipped precedent). What DOES carry over
|
|
2307
|
+
// from CLAUDE-004-fix is the actual bug class it fixed: rerenderRecord
|
|
2308
|
+
// must find a dispatch branch and re-render from the SAME stored
|
|
2309
|
+
// record on refresh, never silently fall through to a generic path —
|
|
2310
|
+
// that guarantee is fully delivered below.
|
|
2311
|
+
function renderAgentToolConsentGate(src) {
|
|
2312
|
+
var $box = el("#fp-messages");
|
|
2313
|
+
if (!$box.length) {
|
|
2314
|
+
if (!src.decision && typeof src.onResume === "function") { src.onResume(true); }
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
// The pending "typing" indicator is only cleared automatically when
|
|
2318
|
+
// a turn fully completes or errors — a pause here otherwise leaves
|
|
2319
|
+
// it stuck showing stale narration ("Applying step: …") permanently
|
|
2320
|
+
// above the real gate that renders below it.
|
|
2321
|
+
hidePending();
|
|
2322
|
+
|
|
2323
|
+
var _rec = addRecord("question", {
|
|
2324
|
+
agentToolConsent: true,
|
|
2325
|
+
options: ["Proceed", "Skip this step"],
|
|
2326
|
+
name: src.name,
|
|
2327
|
+
args: src.args,
|
|
2328
|
+
label: describeAgentToolCall(src.name, src.args),
|
|
2329
|
+
onResume: src.onResume,
|
|
2330
|
+
decision: src.decision
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
if (_rec.decision) {
|
|
2334
|
+
// Already resolved before this render (e.g. resolved earlier in
|
|
2335
|
+
// the session, now showing again after a refresh) — settled
|
|
2336
|
+
// state, not an interactive choice. Mirrors
|
|
2337
|
+
// renderBuildConsentGate's settled branch exactly.
|
|
2338
|
+
addMessage("assistant", "Consent requested for: " + _rec.label);
|
|
2339
|
+
var $settledRow = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2340
|
+
$("<button>")
|
|
2341
|
+
.addClass("fp-consent-chip")
|
|
2342
|
+
.addClass(_rec.decision === "proceed" ? "fp-consent-chip-primary" : "fp-consent-chip-alt")
|
|
2343
|
+
.attr("type", "button").prop("disabled", true)
|
|
2344
|
+
.text(_rec.decision === "proceed" ? "Proceeded ✓" : "Skipped ✓")
|
|
2345
|
+
.appendTo($settledRow);
|
|
2346
|
+
$box.append($settledRow);
|
|
2347
|
+
scrollMessagesToBottom();
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
addMessage("assistant", "FlowPilot wants to do this: " + _rec.label +
|
|
2352
|
+
". Let it proceed, or skip just this one step and continue?");
|
|
2353
|
+
|
|
2354
|
+
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2355
|
+
|
|
2356
|
+
function decide(proceed) {
|
|
2357
|
+
$row.find("button").prop("disabled", true);
|
|
2358
|
+
_rec.decision = proceed ? "proceed" : "skip";
|
|
2359
|
+
if (typeof _rec.onResume === "function") { _rec.onResume(proceed); }
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
$("<button>")
|
|
2363
|
+
.addClass("fp-consent-chip fp-consent-chip-primary")
|
|
2364
|
+
.attr("type", "button")
|
|
2365
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2366
|
+
.attr("data-fp-record-action", "proceed")
|
|
2367
|
+
.text("Proceed")
|
|
2368
|
+
.on("click", function () { decide(true); })
|
|
2369
|
+
.appendTo($row);
|
|
2370
|
+
$("<button>")
|
|
2371
|
+
.addClass("fp-consent-chip fp-consent-chip-alt")
|
|
2372
|
+
.attr("type", "button")
|
|
2373
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2374
|
+
.attr("data-fp-record-action", "skip")
|
|
2375
|
+
.text("Skip this step")
|
|
2376
|
+
.on("click", function () { decide(false); })
|
|
2377
|
+
.appendTo($row);
|
|
2378
|
+
|
|
2379
|
+
$box.append($row);
|
|
2380
|
+
scrollMessagesToBottom();
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// W7 — ask_user tool UI. Reuses renderClarifyingQuestion's button +
|
|
2384
|
+
// free-text presentation, but resumes the SAME in-flight agent-loop
|
|
2385
|
+
// tool-call batch with the answer as a tool result via src.onAnswer —
|
|
2386
|
+
// NOT dispatchSend() (which sends a brand-new user Send, a different
|
|
2387
|
+
// flow entirely from continuing an already-in-progress tool-call turn).
|
|
2388
|
+
// Same refresh-survival shape as renderAgentToolConsentGate (record +
|
|
2389
|
+
// settled state), for the same reason: a mid-answer /refresh must not
|
|
2390
|
+
// strand the loop or silently re-ask a resolved question.
|
|
2391
|
+
function renderAskUserQuestion(src) {
|
|
2392
|
+
var $box = el("#fp-messages");
|
|
2393
|
+
if (!$box.length) {
|
|
2394
|
+
if (src.decision !== "answered" && typeof src.onAnswer === "function") { src.onAnswer(""); }
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
// Same reasoning as renderAgentToolConsentGate: a pause here must
|
|
2398
|
+
// clear the pending indicator itself, since nothing else will.
|
|
2399
|
+
hidePending();
|
|
2400
|
+
|
|
2401
|
+
var _rec = addRecord("question", {
|
|
2402
|
+
askUserTool: true,
|
|
2403
|
+
question: src.question || "FlowPilot has a question.",
|
|
2404
|
+
options: Array.isArray(src.options) ? src.options : [],
|
|
2405
|
+
onAnswer: src.onAnswer,
|
|
2406
|
+
decision: src.decision,
|
|
2407
|
+
answerText: src.answerText
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
if (_rec.decision === "answered") {
|
|
2411
|
+
addMessage("assistant", _rec.question);
|
|
2412
|
+
var $settled = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2413
|
+
$("<button>")
|
|
2414
|
+
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
2415
|
+
.attr("type", "button").prop("disabled", true)
|
|
2416
|
+
.text((_rec.answerText || "") + " ✓")
|
|
2417
|
+
.appendTo($settled);
|
|
2418
|
+
$box.append($settled);
|
|
2419
|
+
scrollMessagesToBottom();
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
addMessage("assistant", _rec.question);
|
|
2424
|
+
|
|
2425
|
+
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
2426
|
+
var $otherRow;
|
|
2427
|
+
|
|
2428
|
+
function answer(text) {
|
|
2429
|
+
$row.find("button, input").prop("disabled", true);
|
|
2430
|
+
if ($otherRow) { $otherRow.find("button, input").prop("disabled", true); }
|
|
2431
|
+
_rec.decision = "answered";
|
|
2432
|
+
_rec.answerText = text;
|
|
2433
|
+
if (typeof _rec.onAnswer === "function") { _rec.onAnswer(text); }
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
_rec.options.forEach(function (opt) {
|
|
2437
|
+
$("<button>")
|
|
2438
|
+
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
2439
|
+
.attr("type", "button")
|
|
2440
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2441
|
+
.attr("data-fp-record-action", "answer")
|
|
2442
|
+
.attr("data-fp-record-value", opt)
|
|
2443
|
+
.text(opt)
|
|
2444
|
+
.on("click", function () { answer(opt); })
|
|
2445
|
+
.appendTo($row);
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
$otherRow = $("<div>").addClass("fp-question-other-row fp-hidden");
|
|
2449
|
+
var $otherInput = $("<input>")
|
|
2450
|
+
.attr("type", "text")
|
|
2451
|
+
.attr("placeholder", "Type your answer…")
|
|
2452
|
+
.addClass("fp-question-other-input");
|
|
2453
|
+
var $otherSend = $("<button>")
|
|
2454
|
+
.addClass("red-ui-button red-ui-button-small")
|
|
2455
|
+
.attr("type", "button")
|
|
2456
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2457
|
+
.attr("data-fp-record-action", "answer-other")
|
|
2458
|
+
.append($("<i>").addClass("fa fa-paper-plane"));
|
|
2459
|
+
|
|
2460
|
+
function submitOther() {
|
|
2461
|
+
var val = $otherInput.val().trim();
|
|
2462
|
+
if (!val) { return; }
|
|
2463
|
+
answer(val);
|
|
2464
|
+
}
|
|
2465
|
+
$otherSend.on("click", submitOther);
|
|
2466
|
+
$otherInput.on("keydown", function (e) { if (e.key === "Enter") { submitOther(); } });
|
|
2467
|
+
$otherRow.append($otherInput).append($otherSend);
|
|
2468
|
+
|
|
2469
|
+
$("<button>")
|
|
2470
|
+
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-other")
|
|
2471
|
+
.attr("type", "button")
|
|
2472
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2473
|
+
.attr("data-fp-record-action", "show-other")
|
|
2474
|
+
.text("Other…")
|
|
2475
|
+
.on("click", function () {
|
|
2476
|
+
$otherRow.removeClass("fp-hidden");
|
|
2477
|
+
$otherInput.focus();
|
|
2478
|
+
})
|
|
2479
|
+
.appendTo($row);
|
|
2480
|
+
|
|
2481
|
+
$box.append($row).append($otherRow);
|
|
2482
|
+
scrollMessagesToBottom();
|
|
2483
|
+
}
|
|
2484
|
+
|
|
1589
2485
|
function handleBuildResult(data, goalPrompt) {
|
|
1590
2486
|
hidePending();
|
|
1591
2487
|
if (renderQuestionOrProse(data)) { return; }
|
|
@@ -1622,12 +2518,34 @@
|
|
|
1622
2518
|
// Generate — a comment node is just a regular flow-JSON node, so there's
|
|
1623
2519
|
// nothing import-mechanism-specific to build here. The prompt box holds
|
|
1624
2520
|
// OPTIONAL notes to steer the explanation; the selection is the real input.
|
|
2521
|
+
// Nothing selected/pinned for a Document send: rather than hard-erroring
|
|
2522
|
+
// (the old behavior — Document previously only ever meant "the
|
|
2523
|
+
// selection"), offer a deterministic one-click scope choice. This is a
|
|
2524
|
+
// pure client-side UX decision, not something worth routing through the
|
|
2525
|
+
// model — a weak/local provider's suggestedAction may omit
|
|
2526
|
+
// targetNodeIds even when the user's intent was clear (see the "all"/
|
|
2527
|
+
// "instance" vocabulary in the system prompts), so this is the backstop
|
|
2528
|
+
// that always works regardless of model reliability.
|
|
2529
|
+
function offerDocumentScopeClarification() {
|
|
2530
|
+
addMessage("info", "Nothing is selected. What would you like documented?");
|
|
2531
|
+
renderChip("This flow", "fa fa-sitemap", function () {
|
|
2532
|
+
var ids = allActiveTabNodeIds();
|
|
2533
|
+
if (ids.length) { pinnedSelectionIds = ids; }
|
|
2534
|
+
documentFlow();
|
|
2535
|
+
});
|
|
2536
|
+
renderChip("Entire instance", "fa fa-server", function () {
|
|
2537
|
+
var ids = allInstanceNodeIds();
|
|
2538
|
+
if (ids.length) { pinnedSelectionIds = ids; }
|
|
2539
|
+
documentFlow();
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
|
|
1625
2543
|
function documentFlow() {
|
|
1626
2544
|
// Falls back to the pinned selection if nothing is currently
|
|
1627
2545
|
// selected, so follow-up turns need no reselection.
|
|
1628
2546
|
var context = collectSelectionContext(activeSelectionIds());
|
|
1629
2547
|
if (!context || !Array.isArray(context.nodes) || context.nodes.length === 0) {
|
|
1630
|
-
|
|
2548
|
+
offerDocumentScopeClarification();
|
|
1631
2549
|
return;
|
|
1632
2550
|
}
|
|
1633
2551
|
context = attachDebugContext(context);
|
|
@@ -1649,7 +2567,9 @@
|
|
|
1649
2567
|
var payload = {
|
|
1650
2568
|
prompt: notes, context: context,
|
|
1651
2569
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1652
|
-
conversationId: conversationId
|
|
2570
|
+
conversationId: conversationId,
|
|
2571
|
+
strategy: "classic",
|
|
2572
|
+
entry: "document"
|
|
1653
2573
|
};
|
|
1654
2574
|
|
|
1655
2575
|
function onDocumentError(msg, xhr) {
|
|
@@ -1712,14 +2632,17 @@
|
|
|
1712
2632
|
$promptBox.val("");
|
|
1713
2633
|
|
|
1714
2634
|
var ap = activeProvider();
|
|
1715
|
-
var isAgentLoop = ap && ap.supportsTools
|
|
2635
|
+
var isAgentLoop = ap && ap.supportsTools &&
|
|
2636
|
+
currentSettings.enableAgentWrite === true;
|
|
1716
2637
|
|
|
1717
2638
|
setBusy(true);
|
|
1718
2639
|
showPending(isAgentLoop);
|
|
1719
2640
|
var payload = {
|
|
1720
2641
|
prompt: instruction, context: context,
|
|
1721
2642
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1722
|
-
conversationId: conversationId
|
|
2643
|
+
conversationId: conversationId,
|
|
2644
|
+
strategy: isAgentLoop ? "agent" : "classic",
|
|
2645
|
+
entry: "modify"
|
|
1723
2646
|
};
|
|
1724
2647
|
|
|
1725
2648
|
function onModifyError(msg, xhr) {
|
|
@@ -1795,6 +2718,14 @@
|
|
|
1795
2718
|
// held in these states.
|
|
1796
2719
|
var activeBuildLoop = null;
|
|
1797
2720
|
|
|
2721
|
+
// CLAUDE-014: plain-language note for a loop-checkpoint "Continue" click,
|
|
2722
|
+
// read and cleared by runBuildReview's payload build. Mirrors
|
|
2723
|
+
// runAgentLoop's pendingDebugNote, but module-scoped since
|
|
2724
|
+
// renderLoopCheckpoint/runBuildReview aren't nested inside runAgentLoop.
|
|
2725
|
+
// No equivalent exists for "Stop loop" — stopBuildLoop makes no server
|
|
2726
|
+
// round trip to attach a note to.
|
|
2727
|
+
var pendingLoopDebugNote = null;
|
|
2728
|
+
|
|
1798
2729
|
// How long onDebugMessage's auto-attach waits, after each matching
|
|
1799
2730
|
// message, for another one to arrive before locking in and running
|
|
1800
2731
|
// the review — see onDebugMessage for why (a forked/split flow can
|
|
@@ -1829,9 +2760,25 @@
|
|
|
1829
2760
|
|
|
1830
2761
|
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
1831
2762
|
|
|
2763
|
+
var _rec = addRecord("question", {
|
|
2764
|
+
options: ["Continue → AI review", "Stop loop"],
|
|
2765
|
+
loopCheckpoint: true,
|
|
2766
|
+
onResume: function (action) {
|
|
2767
|
+
if (action === "continue") {
|
|
2768
|
+
if (activeBuildLoop) {
|
|
2769
|
+
if (currentSettings.debugLogging) { pendingLoopDebugNote = "user clicked Continue"; }
|
|
2770
|
+
runBuildReview(activeBuildLoop);
|
|
2771
|
+
}
|
|
2772
|
+
} else if (action === "stop") {
|
|
2773
|
+
stopBuildLoop("Build loop stopped — applied nodes remain as-is.");
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
});
|
|
2777
|
+
|
|
1832
2778
|
function onContinue() {
|
|
1833
2779
|
$row.find("button").prop("disabled", true);
|
|
1834
2780
|
if (!activeBuildLoop) { return; }
|
|
2781
|
+
if (currentSettings.debugLogging) { pendingLoopDebugNote = "user clicked Continue"; }
|
|
1835
2782
|
runBuildReview(activeBuildLoop);
|
|
1836
2783
|
}
|
|
1837
2784
|
function onStop() {
|
|
@@ -1842,18 +2789,21 @@
|
|
|
1842
2789
|
$("<button>")
|
|
1843
2790
|
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
1844
2791
|
.attr("type", "button")
|
|
2792
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2793
|
+
.attr("data-fp-record-action", "continue")
|
|
1845
2794
|
.text("Continue → AI review")
|
|
1846
2795
|
.on("click", onContinue)
|
|
1847
2796
|
.appendTo($row);
|
|
1848
2797
|
$("<button>")
|
|
1849
2798
|
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
1850
2799
|
.attr("type", "button")
|
|
2800
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2801
|
+
.attr("data-fp-record-action", "stop")
|
|
1851
2802
|
.text("Stop loop")
|
|
1852
2803
|
.on("click", onStop)
|
|
1853
2804
|
.appendTo($row);
|
|
1854
2805
|
|
|
1855
2806
|
$box.append($row);
|
|
1856
|
-
addRecord("question", { options: ["Continue → AI review", "Stop loop"], loopCheckpoint: true });
|
|
1857
2807
|
scrollMessagesToBottom();
|
|
1858
2808
|
}
|
|
1859
2809
|
|
|
@@ -1960,6 +2910,10 @@
|
|
|
1960
2910
|
} else {
|
|
1961
2911
|
loop.iteration++;
|
|
1962
2912
|
loop.waypoint = "apply";
|
|
2913
|
+
// CLAUDE-025: this fix still needs its OWN fresh deploy before
|
|
2914
|
+
// any evidence counts — the next "deploy" event re-stamps this
|
|
2915
|
+
// once the new apply->attach transition actually fires.
|
|
2916
|
+
loop.deployedAt = null;
|
|
1963
2917
|
renderLoopStepper(loop);
|
|
1964
2918
|
}
|
|
1965
2919
|
}
|
|
@@ -2118,7 +3072,12 @@
|
|
|
2118
3072
|
bailCount: 0,
|
|
2119
3073
|
httpEndpoints: httpEndpoints,
|
|
2120
3074
|
skipCheckpointNodeIds: skipCheckpointNodeIds,
|
|
2121
|
-
skipCheckpointTapIds: skipCheckpointTapIds
|
|
3075
|
+
skipCheckpointTapIds: skipCheckpointTapIds,
|
|
3076
|
+
// CLAUDE-025: stamped by the RED "deploy" listener (init.js) the
|
|
3077
|
+
// moment THIS attempt's own apply->attach transition fires — see
|
|
3078
|
+
// freshBuildLoopEvidence. Starts null: no deploy has happened for
|
|
3079
|
+
// this attempt yet, so nothing can count as evidence.
|
|
3080
|
+
deployedAt: null
|
|
2122
3081
|
};
|
|
2123
3082
|
renderLoopStepper(activeBuildLoop);
|
|
2124
3083
|
}
|
|
@@ -2133,7 +3092,21 @@
|
|
|
2133
3092
|
// node ids instead of the live/pinned canvas selection) are synthetic.
|
|
2134
3093
|
function runBuildReview(loop) {
|
|
2135
3094
|
var context = collectSelectionContext(loop.nodeIds);
|
|
2136
|
-
|
|
3095
|
+
// CLAUDE-025: deliberately NOT attachDebugContext() here — that pulls
|
|
3096
|
+
// in the full sticky attachedDebugMessages buffer, which can still
|
|
3097
|
+
// hold evidence from an earlier Build attempt (or a manual attach)
|
|
3098
|
+
// that has nothing to do with THIS attempt's own deploy. Only
|
|
3099
|
+
// messages that arrived at/after this attempt's own deploy (see
|
|
3100
|
+
// freshBuildLoopEvidence) count as evidence for its review.
|
|
3101
|
+
var freshEvidence = freshBuildLoopEvidence(loop);
|
|
3102
|
+
if (freshEvidence.length) {
|
|
3103
|
+
context = context || { nodes: [], connections: {} };
|
|
3104
|
+
context = Object.assign({}, context, {
|
|
3105
|
+
debugMessages: freshEvidence.map(function (m) {
|
|
3106
|
+
return { id: m.id, timestamp: m.timestamp, sourceKind: m.sourceKind, name: m.name, topic: m.topic, value: m.value };
|
|
3107
|
+
})
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
2137
3110
|
var reviewEvidence = context && Array.isArray(context.debugMessages)
|
|
2138
3111
|
? context.debugMessages : [];
|
|
2139
3112
|
var statusOnlyEvidence = reviewEvidence.length > 0 &&
|
|
@@ -2221,8 +3194,14 @@
|
|
|
2221
3194
|
var payload = {
|
|
2222
3195
|
prompt: instruction, context: context,
|
|
2223
3196
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
2224
|
-
conversationId: loop.conversationId
|
|
3197
|
+
conversationId: loop.conversationId,
|
|
3198
|
+
strategy: "classic",
|
|
3199
|
+
entry: "build-review"
|
|
2225
3200
|
};
|
|
3201
|
+
if (pendingLoopDebugNote) {
|
|
3202
|
+
payload.debugNote = pendingLoopDebugNote;
|
|
3203
|
+
pendingLoopDebugNote = null;
|
|
3204
|
+
}
|
|
2226
3205
|
|
|
2227
3206
|
function onReviewError(msg, xhr) {
|
|
2228
3207
|
hidePending();
|
|
@@ -2298,6 +3277,13 @@
|
|
|
2298
3277
|
if (data.prose) {
|
|
2299
3278
|
var explanation = data.explanation || "(no content returned)";
|
|
2300
3279
|
|
|
3280
|
+
if (looksLikeToolEnvelope(data.explanation)) {
|
|
3281
|
+
handleExecuteError("FlowPilot's reply didn't come through as expected.", data.explanation);
|
|
3282
|
+
stopBuildLoop("Build loop stopped — the review response wasn't in the expected format. Continue manually with Modify, or start a fresh /build.", false);
|
|
3283
|
+
updateSelectionStatus();
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
2301
3287
|
// W0.3: bail detection — a prose reply with a mode-redirect
|
|
2302
3288
|
// suggestedAction means the model tried to exit the loop
|
|
2303
3289
|
// context via the Modify escape hatch. Count it and retry or
|