@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.
@@ -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 (!active)
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.loopOriginalPrompt, undefined);
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
- if (finishedAssistantText.includes(LOOP_DONE_MARKER)) {
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 >= MAX_LOOP_ITERATIONS) {
1079
- console.log(`[loop] max iterations (${MAX_LOOP_ITERATIONS}) reached, stopping`);
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}/${MAX_LOOP_ITERATIONS}`);
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: MAX_LOOP_ITERATIONS,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,