@aexol/spectral 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/relay/dispatcher.js
CHANGED
|
@@ -465,11 +465,13 @@ export function handleClientMessage(frame, deps) {
|
|
|
465
465
|
}
|
|
466
466
|
const content = message.content;
|
|
467
467
|
const isLoop = message.loop === true;
|
|
468
|
+
const loopMaxIterations = message.loopMaxIterations;
|
|
469
|
+
const loopGoal = message.loopGoal;
|
|
468
470
|
const validImages = coerceImages(message.images);
|
|
469
471
|
// Set autonomous iterative loop state before firing the prompt.
|
|
470
472
|
// loop:true → start/renew loop with the current content as original prompt;
|
|
471
473
|
// loop:false → stop any active loop.
|
|
472
|
-
manager.setLoopActive(sessionId, isLoop, content);
|
|
474
|
+
manager.setLoopActive(sessionId, isLoop, content, loopMaxIterations, loopGoal);
|
|
473
475
|
// 2. Attach (idempotent). On first attach we capture the replay payload
|
|
474
476
|
// and synthesize a `session_ready` ws_event so the browser sees the
|
|
475
477
|
// same first frame it would have on a direct WS connection.
|
|
@@ -642,6 +642,7 @@ export class SessionStreamManager {
|
|
|
642
642
|
// the next agent_end would otherwise trigger another prompt.
|
|
643
643
|
stream.loopActive = false;
|
|
644
644
|
stream.loopOriginalPrompt = null;
|
|
645
|
+
stream.loopGoal = null;
|
|
645
646
|
stream.loopIterationCount = 0;
|
|
646
647
|
// Dispose the pi bridge immediately — this tears down pi's session and
|
|
647
648
|
// unsubscribe. The bridge's own event handler is detached; no further
|
|
@@ -731,13 +732,22 @@ export class SessionStreamManager {
|
|
|
731
732
|
* The loop stops when the agent emits `<LOOP_DONE>` in its response or the
|
|
732
733
|
* safety iteration limit is reached.
|
|
733
734
|
*/
|
|
734
|
-
setLoopActive(sessionId, active, originalPrompt) {
|
|
735
|
+
setLoopActive(sessionId, active, originalPrompt, maxIterations, goal) {
|
|
735
736
|
const stream = this.streams.get(sessionId);
|
|
736
737
|
if (stream) {
|
|
737
738
|
stream.loopActive = active;
|
|
738
739
|
stream.loopOriginalPrompt = active ? (originalPrompt ?? null) : null;
|
|
739
|
-
if (
|
|
740
|
+
if (active) {
|
|
741
|
+
if (maxIterations !== undefined && maxIterations > 0) {
|
|
742
|
+
stream.loopMaxIterations = Math.min(maxIterations, MAX_LOOP_ITERATIONS);
|
|
743
|
+
}
|
|
744
|
+
stream.loopGoal = goal?.trim() || null;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
740
747
|
stream.loopIterationCount = 0;
|
|
748
|
+
stream.loopMaxIterations = MAX_LOOP_ITERATIONS;
|
|
749
|
+
stream.loopGoal = null;
|
|
750
|
+
}
|
|
741
751
|
}
|
|
742
752
|
}
|
|
743
753
|
/**
|
|
@@ -825,6 +835,29 @@ export class SessionStreamManager {
|
|
|
825
835
|
* entry → prepareCompaction() returns undefined → "Already compacted"
|
|
826
836
|
* → the trigger's onError handler catches it harmlessly.
|
|
827
837
|
*/
|
|
838
|
+
buildLoopPrompt(stream) {
|
|
839
|
+
const parts = [];
|
|
840
|
+
if (stream.loopGoal) {
|
|
841
|
+
parts.push(`[GOAL]: ${stream.loopGoal}`);
|
|
842
|
+
}
|
|
843
|
+
parts.push(stream.loopOriginalPrompt);
|
|
844
|
+
parts.push("");
|
|
845
|
+
parts.push("--- LOOP INSTRUCTIONS ---\n" +
|
|
846
|
+
"You are running in an iterative loop. Each iteration you see your previous changes\n" +
|
|
847
|
+
"and can improve them further.\n" +
|
|
848
|
+
(stream.loopGoal
|
|
849
|
+
? "The [GOAL] above defines what you need to achieve.\n"
|
|
850
|
+
: "\n") +
|
|
851
|
+
"CRITICAL: After EVERY response you MUST self-evaluate:\n" +
|
|
852
|
+
"- If the task IS FULLY COMPLETE (nothing meaningful left to do), " +
|
|
853
|
+
"respond with <LOOP_DONE> as the VERY FIRST text in your reply,\n" +
|
|
854
|
+
" followed by a brief summary of what was accomplished.\n" +
|
|
855
|
+
"- If more work remains, continue working WITHOUT <LOOP_DONE>.\n" +
|
|
856
|
+
"- <LOOP_DONE> is the ONLY way to stop the loop. If you do not emit it,\n" +
|
|
857
|
+
" you WILL be prompted again to keep working.\n" +
|
|
858
|
+
"Be decisive: if the task is done, stop. Do not keep polishing.");
|
|
859
|
+
return parts.join("\n");
|
|
860
|
+
}
|
|
828
861
|
async sendNextLoopIteration(stream) {
|
|
829
862
|
const shouldCompact = stream.bridge.compact &&
|
|
830
863
|
typeof stream.contextWindowUsed === "number" &&
|
|
@@ -846,7 +879,7 @@ export class SessionStreamManager {
|
|
|
846
879
|
}
|
|
847
880
|
}
|
|
848
881
|
}
|
|
849
|
-
await this.prompt(stream.sessionId, stream
|
|
882
|
+
await this.prompt(stream.sessionId, this.buildLoopPrompt(stream), undefined);
|
|
850
883
|
}
|
|
851
884
|
// --- internals ----------------------------------------------------------
|
|
852
885
|
createStream(sessionId, history) {
|
|
@@ -877,6 +910,8 @@ export class SessionStreamManager {
|
|
|
877
910
|
loopActive: false,
|
|
878
911
|
loopIterationCount: 0,
|
|
879
912
|
loopOriginalPrompt: null,
|
|
913
|
+
loopMaxIterations: MAX_LOOP_ITERATIONS,
|
|
914
|
+
loopGoal: null,
|
|
880
915
|
forkCompactSourceId: forkSourceId ?? null,
|
|
881
916
|
compacting: false,
|
|
882
917
|
contextWindowUsed: null,
|
|
@@ -1064,21 +1099,26 @@ export class SessionStreamManager {
|
|
|
1064
1099
|
// iteratively improves the solution.
|
|
1065
1100
|
if (stream.loopActive && stream.loopOriginalPrompt) {
|
|
1066
1101
|
const finishedAssistantText = finishedTurn?.assistantText ?? "";
|
|
1067
|
-
|
|
1102
|
+
// Check for the loop-done marker in the first portion of the
|
|
1103
|
+
// response (the prompt instructs the agent to emit it first).
|
|
1104
|
+
const head = finishedAssistantText.slice(0, 500);
|
|
1105
|
+
if (head.includes(LOOP_DONE_MARKER)) {
|
|
1068
1106
|
console.log(`[loop] completion marker detected after ${stream.loopIterationCount} iteration(s), stopping`);
|
|
1069
1107
|
const completedIterations = stream.loopIterationCount;
|
|
1070
1108
|
stream.loopActive = false;
|
|
1071
1109
|
stream.loopIterationCount = 0;
|
|
1072
1110
|
stream.loopOriginalPrompt = null;
|
|
1111
|
+
stream.loopGoal = null;
|
|
1073
1112
|
this.broadcast(stream, {
|
|
1074
1113
|
type: "loop_complete",
|
|
1075
1114
|
iterations: completedIterations,
|
|
1076
1115
|
});
|
|
1077
1116
|
}
|
|
1078
|
-
else if (stream.loopIterationCount >=
|
|
1079
|
-
console.log(`[loop] max iterations (${
|
|
1117
|
+
else if (stream.loopIterationCount >= stream.loopMaxIterations) {
|
|
1118
|
+
console.log(`[loop] max iterations (${stream.loopMaxIterations}) reached, stopping`);
|
|
1080
1119
|
stream.loopActive = false;
|
|
1081
1120
|
stream.loopOriginalPrompt = null;
|
|
1121
|
+
stream.loopGoal = null;
|
|
1082
1122
|
this.broadcast(stream, {
|
|
1083
1123
|
type: "loop_max_iterations",
|
|
1084
1124
|
iterations: stream.loopIterationCount,
|
|
@@ -1086,17 +1126,18 @@ export class SessionStreamManager {
|
|
|
1086
1126
|
}
|
|
1087
1127
|
else {
|
|
1088
1128
|
stream.loopIterationCount++;
|
|
1089
|
-
console.log(`[loop] iteration ${stream.loopIterationCount}/${
|
|
1129
|
+
console.log(`[loop] iteration ${stream.loopIterationCount}/${stream.loopMaxIterations}`);
|
|
1090
1130
|
this.broadcast(stream, {
|
|
1091
1131
|
type: "loop_iteration",
|
|
1092
1132
|
iteration: stream.loopIterationCount,
|
|
1093
|
-
maxIterations:
|
|
1133
|
+
maxIterations: stream.loopMaxIterations,
|
|
1094
1134
|
prompt: stream.loopOriginalPrompt,
|
|
1095
1135
|
});
|
|
1096
1136
|
void this.sendNextLoopIteration(stream).catch((err) => {
|
|
1097
1137
|
console.error(`[loop] iteration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1098
1138
|
stream.loopActive = false;
|
|
1099
1139
|
stream.loopOriginalPrompt = null;
|
|
1140
|
+
stream.loopGoal = null;
|
|
1100
1141
|
});
|
|
1101
1142
|
}
|
|
1102
1143
|
}
|