@aexol/spectral 0.5.1 → 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.
@@ -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,20 @@ 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
+ parts.push("");
843
+ }
844
+ parts.push(stream.loopOriginalPrompt);
845
+ if (stream.loopGoal) {
846
+ parts.push("");
847
+ parts.push("After completing your work, evaluate whether the goal has been FULLY achieved. " +
848
+ 'Include <LOOP_DONE> in your response ONLY if you are confident the goal is completely met.');
849
+ }
850
+ return parts.join("\n");
851
+ }
828
852
  async sendNextLoopIteration(stream) {
829
853
  const shouldCompact = stream.bridge.compact &&
830
854
  typeof stream.contextWindowUsed === "number" &&
@@ -846,7 +870,7 @@ export class SessionStreamManager {
846
870
  }
847
871
  }
848
872
  }
849
- await this.prompt(stream.sessionId, stream.loopOriginalPrompt, undefined);
873
+ await this.prompt(stream.sessionId, this.buildLoopPrompt(stream), undefined);
850
874
  }
851
875
  // --- internals ----------------------------------------------------------
852
876
  createStream(sessionId, history) {
@@ -877,6 +901,8 @@ export class SessionStreamManager {
877
901
  loopActive: false,
878
902
  loopIterationCount: 0,
879
903
  loopOriginalPrompt: null,
904
+ loopMaxIterations: MAX_LOOP_ITERATIONS,
905
+ loopGoal: null,
880
906
  forkCompactSourceId: forkSourceId ?? null,
881
907
  compacting: false,
882
908
  contextWindowUsed: null,
@@ -1070,15 +1096,17 @@ export class SessionStreamManager {
1070
1096
  stream.loopActive = false;
1071
1097
  stream.loopIterationCount = 0;
1072
1098
  stream.loopOriginalPrompt = null;
1099
+ stream.loopGoal = null;
1073
1100
  this.broadcast(stream, {
1074
1101
  type: "loop_complete",
1075
1102
  iterations: completedIterations,
1076
1103
  });
1077
1104
  }
1078
- else if (stream.loopIterationCount >= MAX_LOOP_ITERATIONS) {
1079
- console.log(`[loop] max iterations (${MAX_LOOP_ITERATIONS}) reached, stopping`);
1105
+ else if (stream.loopIterationCount >= stream.loopMaxIterations) {
1106
+ console.log(`[loop] max iterations (${stream.loopMaxIterations}) reached, stopping`);
1080
1107
  stream.loopActive = false;
1081
1108
  stream.loopOriginalPrompt = null;
1109
+ stream.loopGoal = null;
1082
1110
  this.broadcast(stream, {
1083
1111
  type: "loop_max_iterations",
1084
1112
  iterations: stream.loopIterationCount,
@@ -1086,17 +1114,18 @@ export class SessionStreamManager {
1086
1114
  }
1087
1115
  else {
1088
1116
  stream.loopIterationCount++;
1089
- console.log(`[loop] iteration ${stream.loopIterationCount}/${MAX_LOOP_ITERATIONS}`);
1117
+ console.log(`[loop] iteration ${stream.loopIterationCount}/${stream.loopMaxIterations}`);
1090
1118
  this.broadcast(stream, {
1091
1119
  type: "loop_iteration",
1092
1120
  iteration: stream.loopIterationCount,
1093
- maxIterations: MAX_LOOP_ITERATIONS,
1121
+ maxIterations: stream.loopMaxIterations,
1094
1122
  prompt: stream.loopOriginalPrompt,
1095
1123
  });
1096
1124
  void this.sendNextLoopIteration(stream).catch((err) => {
1097
1125
  console.error(`[loop] iteration failed: ${err instanceof Error ? err.message : String(err)}`);
1098
1126
  stream.loopActive = false;
1099
1127
  stream.loopOriginalPrompt = null;
1128
+ stream.loopGoal = null;
1100
1129
  });
1101
1130
  }
1102
1131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
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,