@manny-est/node-red-flowpilot 0.5.2 → 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 +53 -74
- package/USER-GUIDE.md +5 -1
- package/flowpilot-core.css +38 -10
- package/flowpilot.js +1007 -181
- package/lib/agent-contract.js +45 -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 +10 -1
- package/lib/core/init.js +138 -5
- package/lib/core/main.js +664 -16
- package/lib/core/modes.js +882 -100
- package/lib/core/selection-context.js +27 -24
- package/lib/default-system-prompt.js +4 -3
- package/lib/document-system-prompt.js +3 -2
- 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 +13 -2
- package/lib/provider-anthropic.js +11 -8
- package/lib/provider-openai-compatible.js +37 -9
- package/lib/provider-shape-check.js +34 -0
- package/lib/storage.js +128 -21
- 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);
|
|
@@ -839,13 +1232,57 @@
|
|
|
839
1232
|
// W4: parse and surface the Plan: block if present.
|
|
840
1233
|
var planItems = parseTodoPlan(data.explanation || "");
|
|
841
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 : [];
|
|
842
1244
|
if (planItems.length) {
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
|
|
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
|
+
}
|
|
849
1286
|
rerenderTodoRecord(todoRec);
|
|
850
1287
|
}
|
|
851
1288
|
|
|
@@ -870,7 +1307,16 @@
|
|
|
870
1307
|
}
|
|
871
1308
|
} : applyModifications;
|
|
872
1309
|
|
|
873
|
-
|
|
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
|
+
}
|
|
874
1320
|
renderActionChip(data.suggestedAction);
|
|
875
1321
|
setBusy(false);
|
|
876
1322
|
updateSelectionStatus();
|
|
@@ -1047,7 +1493,9 @@
|
|
|
1047
1493
|
var payload = {
|
|
1048
1494
|
prompt: prompt, context: context,
|
|
1049
1495
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1050
|
-
conversationId: conversationId
|
|
1496
|
+
conversationId: conversationId,
|
|
1497
|
+
strategy: "classic",
|
|
1498
|
+
entry: endpointName
|
|
1051
1499
|
};
|
|
1052
1500
|
|
|
1053
1501
|
function onError(msg, xhr) {
|
|
@@ -1147,6 +1595,25 @@
|
|
|
1147
1595
|
$wrap.append($ul);
|
|
1148
1596
|
}
|
|
1149
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
|
+
|
|
1150
1617
|
if ($existing.length) {
|
|
1151
1618
|
$existing.replaceWith($wrap);
|
|
1152
1619
|
} else {
|
|
@@ -1193,11 +1660,11 @@
|
|
|
1193
1660
|
if (liveNode.type === "comment" || liveNode.type === "group") { return; }
|
|
1194
1661
|
if (liveNode._def && liveNode._def.category === "config") {
|
|
1195
1662
|
configTotal++;
|
|
1196
|
-
if (
|
|
1663
|
+
if (nodeExists(liveNode.id)) { configFound++; }
|
|
1197
1664
|
return;
|
|
1198
1665
|
}
|
|
1199
1666
|
total++;
|
|
1200
|
-
if (
|
|
1667
|
+
if (nodeExists(liveNode.id)) {
|
|
1201
1668
|
found++;
|
|
1202
1669
|
} else {
|
|
1203
1670
|
missing.push(liveNode.type || pid);
|
|
@@ -1240,58 +1707,77 @@
|
|
|
1240
1707
|
// assigns it and returns idMap). Property/absent checks already use
|
|
1241
1708
|
// real existing-node ids, so idMap[id] simply misses and falls through
|
|
1242
1709
|
// to the id unchanged.
|
|
1243
|
-
|
|
1244
|
-
|
|
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) {
|
|
1245
1724
|
idMap = idMap || {};
|
|
1246
1725
|
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;
|
|
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)";
|
|
1289
1735
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
+
}
|
|
1292
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; }
|
|
1293
1779
|
total++;
|
|
1294
|
-
if (ok) { passed++; }
|
|
1780
|
+
if (result.ok) { passed++; } else { failures.push(result.label); failedSteps.push(step); }
|
|
1295
1781
|
});
|
|
1296
1782
|
|
|
1297
1783
|
if (total === 0) { return; }
|
|
@@ -1301,6 +1787,25 @@
|
|
|
1301
1787
|
} else {
|
|
1302
1788
|
addMessage("fp-notice", "⚠ Verification: " + passed + "/" + total + " change(s) confirmed — " +
|
|
1303
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
|
+
});
|
|
1304
1809
|
}
|
|
1305
1810
|
if (todoRec && todoRec.items) {
|
|
1306
1811
|
todoRec.items.forEach(function (item) {
|
|
@@ -1410,7 +1915,9 @@
|
|
|
1410
1915
|
var payload = {
|
|
1411
1916
|
prompt: instruction, context: context,
|
|
1412
1917
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1413
|
-
conversationId: conversationId
|
|
1918
|
+
conversationId: conversationId,
|
|
1919
|
+
strategy: "classic",
|
|
1920
|
+
entry: "build-existing"
|
|
1414
1921
|
};
|
|
1415
1922
|
|
|
1416
1923
|
function onBuildExistingError(msg, xhr) {
|
|
@@ -1543,12 +2050,16 @@
|
|
|
1543
2050
|
$("<button>")
|
|
1544
2051
|
.addClass("fp-consent-chip fp-consent-chip-primary")
|
|
1545
2052
|
.attr("type", "button")
|
|
2053
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2054
|
+
.attr("data-fp-record-action", "proceed")
|
|
1546
2055
|
.text("Auto-verify")
|
|
1547
2056
|
.on("click", function () { decide(true); })
|
|
1548
2057
|
.appendTo($row);
|
|
1549
2058
|
$("<button>")
|
|
1550
2059
|
.addClass("fp-consent-chip fp-consent-chip-alt")
|
|
1551
2060
|
.attr("type", "button")
|
|
2061
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2062
|
+
.attr("data-fp-record-action", "skip")
|
|
1552
2063
|
.text("I'll check myself")
|
|
1553
2064
|
.on("click", function () { decide(false); })
|
|
1554
2065
|
.appendTo($row);
|
|
@@ -1586,6 +2097,209 @@
|
|
|
1586
2097
|
renderActionChip(src.suggestedAction);
|
|
1587
2098
|
}
|
|
1588
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
|
+
|
|
1589
2303
|
function handleBuildResult(data, goalPrompt) {
|
|
1590
2304
|
hidePending();
|
|
1591
2305
|
if (renderQuestionOrProse(data)) { return; }
|
|
@@ -1649,7 +2363,9 @@
|
|
|
1649
2363
|
var payload = {
|
|
1650
2364
|
prompt: notes, context: context,
|
|
1651
2365
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1652
|
-
conversationId: conversationId
|
|
2366
|
+
conversationId: conversationId,
|
|
2367
|
+
strategy: "classic",
|
|
2368
|
+
entry: "document"
|
|
1653
2369
|
};
|
|
1654
2370
|
|
|
1655
2371
|
function onDocumentError(msg, xhr) {
|
|
@@ -1712,14 +2428,17 @@
|
|
|
1712
2428
|
$promptBox.val("");
|
|
1713
2429
|
|
|
1714
2430
|
var ap = activeProvider();
|
|
1715
|
-
var isAgentLoop = ap && ap.supportsTools
|
|
2431
|
+
var isAgentLoop = ap && ap.supportsTools &&
|
|
2432
|
+
currentSettings.enableAgentWrite === true;
|
|
1716
2433
|
|
|
1717
2434
|
setBusy(true);
|
|
1718
2435
|
showPending(isAgentLoop);
|
|
1719
2436
|
var payload = {
|
|
1720
2437
|
prompt: instruction, context: context,
|
|
1721
2438
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
1722
|
-
conversationId: conversationId
|
|
2439
|
+
conversationId: conversationId,
|
|
2440
|
+
strategy: isAgentLoop ? "agent" : "classic",
|
|
2441
|
+
entry: "modify"
|
|
1723
2442
|
};
|
|
1724
2443
|
|
|
1725
2444
|
function onModifyError(msg, xhr) {
|
|
@@ -1795,6 +2514,14 @@
|
|
|
1795
2514
|
// held in these states.
|
|
1796
2515
|
var activeBuildLoop = null;
|
|
1797
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
|
+
|
|
1798
2525
|
// How long onDebugMessage's auto-attach waits, after each matching
|
|
1799
2526
|
// message, for another one to arrive before locking in and running
|
|
1800
2527
|
// the review — see onDebugMessage for why (a forked/split flow can
|
|
@@ -1829,9 +2556,25 @@
|
|
|
1829
2556
|
|
|
1830
2557
|
var $row = $("<div>").addClass("fp-chip-row fp-question-row");
|
|
1831
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
|
+
|
|
1832
2574
|
function onContinue() {
|
|
1833
2575
|
$row.find("button").prop("disabled", true);
|
|
1834
2576
|
if (!activeBuildLoop) { return; }
|
|
2577
|
+
if (currentSettings.debugLogging) { pendingLoopDebugNote = "user clicked Continue"; }
|
|
1835
2578
|
runBuildReview(activeBuildLoop);
|
|
1836
2579
|
}
|
|
1837
2580
|
function onStop() {
|
|
@@ -1842,18 +2585,21 @@
|
|
|
1842
2585
|
$("<button>")
|
|
1843
2586
|
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
1844
2587
|
.attr("type", "button")
|
|
2588
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2589
|
+
.attr("data-fp-record-action", "continue")
|
|
1845
2590
|
.text("Continue → AI review")
|
|
1846
2591
|
.on("click", onContinue)
|
|
1847
2592
|
.appendTo($row);
|
|
1848
2593
|
$("<button>")
|
|
1849
2594
|
.addClass("red-ui-button red-ui-button-small fp-chip fp-question-option")
|
|
1850
2595
|
.attr("type", "button")
|
|
2596
|
+
.attr("data-fp-record-id", _rec.id)
|
|
2597
|
+
.attr("data-fp-record-action", "stop")
|
|
1851
2598
|
.text("Stop loop")
|
|
1852
2599
|
.on("click", onStop)
|
|
1853
2600
|
.appendTo($row);
|
|
1854
2601
|
|
|
1855
2602
|
$box.append($row);
|
|
1856
|
-
addRecord("question", { options: ["Continue → AI review", "Stop loop"], loopCheckpoint: true });
|
|
1857
2603
|
scrollMessagesToBottom();
|
|
1858
2604
|
}
|
|
1859
2605
|
|
|
@@ -1960,6 +2706,10 @@
|
|
|
1960
2706
|
} else {
|
|
1961
2707
|
loop.iteration++;
|
|
1962
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;
|
|
1963
2713
|
renderLoopStepper(loop);
|
|
1964
2714
|
}
|
|
1965
2715
|
}
|
|
@@ -2118,7 +2868,12 @@
|
|
|
2118
2868
|
bailCount: 0,
|
|
2119
2869
|
httpEndpoints: httpEndpoints,
|
|
2120
2870
|
skipCheckpointNodeIds: skipCheckpointNodeIds,
|
|
2121
|
-
skipCheckpointTapIds: skipCheckpointTapIds
|
|
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
|
|
2122
2877
|
};
|
|
2123
2878
|
renderLoopStepper(activeBuildLoop);
|
|
2124
2879
|
}
|
|
@@ -2133,7 +2888,21 @@
|
|
|
2133
2888
|
// node ids instead of the live/pinned canvas selection) are synthetic.
|
|
2134
2889
|
function runBuildReview(loop) {
|
|
2135
2890
|
var context = collectSelectionContext(loop.nodeIds);
|
|
2136
|
-
|
|
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
|
+
}
|
|
2137
2906
|
var reviewEvidence = context && Array.isArray(context.debugMessages)
|
|
2138
2907
|
? context.debugMessages : [];
|
|
2139
2908
|
var statusOnlyEvidence = reviewEvidence.length > 0 &&
|
|
@@ -2221,8 +2990,14 @@
|
|
|
2221
2990
|
var payload = {
|
|
2222
2991
|
prompt: instruction, context: context,
|
|
2223
2992
|
history: historyPayload.messages, historyTruncated: historyPayload.truncated,
|
|
2224
|
-
conversationId: loop.conversationId
|
|
2993
|
+
conversationId: loop.conversationId,
|
|
2994
|
+
strategy: "classic",
|
|
2995
|
+
entry: "build-review"
|
|
2225
2996
|
};
|
|
2997
|
+
if (pendingLoopDebugNote) {
|
|
2998
|
+
payload.debugNote = pendingLoopDebugNote;
|
|
2999
|
+
pendingLoopDebugNote = null;
|
|
3000
|
+
}
|
|
2226
3001
|
|
|
2227
3002
|
function onReviewError(msg, xhr) {
|
|
2228
3003
|
hidePending();
|
|
@@ -2298,6 +3073,13 @@
|
|
|
2298
3073
|
if (data.prose) {
|
|
2299
3074
|
var explanation = data.explanation || "(no content returned)";
|
|
2300
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
|
+
|
|
2301
3083
|
// W0.3: bail detection — a prose reply with a mode-redirect
|
|
2302
3084
|
// suggestedAction means the model tried to exit the loop
|
|
2303
3085
|
// context via the Modify escape hatch. Count it and retry or
|