@openeryc/pi-coding-agent 0.75.58 → 0.75.59

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/core/agent-session.d.ts +11 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +32 -0
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/session-manager.d.ts +4 -0
  7. package/dist/core/session-manager.d.ts.map +1 -1
  8. package/dist/core/session-manager.js +19 -2
  9. package/dist/core/session-manager.js.map +1 -1
  10. package/dist/core/system-prompt.d.ts +2 -0
  11. package/dist/core/system-prompt.d.ts.map +1 -1
  12. package/dist/core/system-prompt.js +8 -1
  13. package/dist/core/system-prompt.js.map +1 -1
  14. package/dist/modes/interactive/goal-evaluator.d.ts +23 -0
  15. package/dist/modes/interactive/goal-evaluator.d.ts.map +1 -0
  16. package/dist/modes/interactive/goal-evaluator.js +68 -0
  17. package/dist/modes/interactive/goal-evaluator.js.map +1 -0
  18. package/dist/modes/interactive/interactive-mode.d.ts +6 -6
  19. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  20. package/dist/modes/interactive/interactive-mode.js +133 -79
  21. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  22. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  23. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  24. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  25. package/examples/extensions/sandbox/package-lock.json +2 -2
  26. package/examples/extensions/sandbox/package.json +1 -1
  27. package/examples/extensions/with-deps/package-lock.json +2 -2
  28. package/examples/extensions/with-deps/package.json +1 -1
  29. package/npm-shrinkwrap.json +12 -12
  30. package/package.json +4 -4
@@ -62,7 +62,7 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
62
62
  import { TreeSelectorComponent } from "./components/tree-selector.js";
63
63
  import { UserMessageComponent } from "./components/user-message.js";
64
64
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
65
- import { isNearDuplicateGoalOutput, normalizeGoalOutput } from "./goal-runner-utils.js";
65
+ import { evaluateGoalCondition } from "./goal-evaluator.js";
66
66
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
67
67
  function isExpandable(obj) {
68
68
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
@@ -182,13 +182,12 @@ export class InteractiveMode {
182
182
  goalRunnerAbortController = undefined;
183
183
  goalRunnerRetryAttempt = 0;
184
184
  goalRunnerStatusDisplayed = false;
185
- /** Normalized assistant texts from recent goal turns (for duplicate detection). */
186
- goalRunnerRecentOutputs = [];
187
- GOAL_RUNNER_MAX_RETRIES = 10;
188
- GOAL_RUNNER_BASE_DELAY_MS = 3000;
189
- GOAL_RUNNER_TURN_DELAY_MS = 800;
190
- /** Pause when this many consecutive turns look like near-duplicates. */
191
- GOAL_RUNNER_MAX_DUPLICATE_STREAK = 3;
185
+ goalRunnerTurnCount = 0;
186
+ goalRunnerStartTime = 0;
187
+ goalRunnerTokenSpend = 0;
188
+ goalRunnerLastEvalReason = "";
189
+ GOAL_RUNNER_TURN_DELAY_MS = 1200;
190
+ GOAL_RUNNER_RETRY_DELAY_MS = 5000;
192
191
  // Extension UI state
193
192
  extensionSelector = undefined;
194
193
  extensionInput = undefined;
@@ -2077,8 +2076,10 @@ export class InteractiveMode {
2077
2076
  // Handle commands (with special case for /goal which continues to submit)
2078
2077
  if (text.startsWith("/goal")) {
2079
2078
  const goal = text.replace(/^\/goal\s*/, "").trim();
2080
- this.handleGoalCommand(text);
2079
+ const shouldSubmit = await this.handleGoalCommand(text);
2081
2080
  this.editor.setText("");
2081
+ if (!shouldSubmit)
2082
+ return;
2082
2083
  if (goal)
2083
2084
  text = goal;
2084
2085
  }
@@ -4593,64 +4594,115 @@ export class InteractiveMode {
4593
4594
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
4594
4595
  this.ui.requestRender();
4595
4596
  }
4596
- handleGoalCommand(text) {
4597
+ async handleGoalCommand(text) {
4597
4598
  const goal = text.replace(/^\/goal\s*/, "").trim();
4598
4599
  if (!goal) {
4600
+ // Show goal status (like Claude Code's /goal with no args)
4599
4601
  const currentGoal = this.session.sessionGoal;
4600
- const runnerStatus = this.goalRunnerEnabled ? "enabled" : "paused";
4601
- if (currentGoal) {
4602
+ if (!currentGoal) {
4603
+ this.showWarning("Usage: /goal <condition>\n /goal clear|stop|off|reset|cancel - clear goal");
4604
+ this.ui.requestRender();
4605
+ return false;
4606
+ }
4607
+ const elapsed = this.goalRunnerStartTime > 0 ? this.formatDuration(Date.now() - this.goalRunnerStartTime) : "-";
4608
+ const turns = this.goalRunnerTurnCount > 0 ? String(this.goalRunnerTurnCount) : "-";
4609
+ const tokens = this.goalRunnerTokenSpend > 0 ? `${this.goalRunnerTokenSpend.toLocaleString()} tokens` : "-";
4610
+ let info = `${theme.bold(theme.fg("accent", "Session Goal"))}\n\n`;
4611
+ info += `${theme.fg("dim", "Condition:")} ${currentGoal}\n`;
4612
+ info += `${theme.fg("dim", "Running:")} ${elapsed}\n`;
4613
+ info += `${theme.fg("dim", "Turns:")} ${turns}\n`;
4614
+ info += `${theme.fg("dim", "Tokens:")} ${tokens}\n`;
4615
+ if (this.goalRunnerLastEvalReason) {
4616
+ info += `\n${theme.fg("dim", "Last evaluation:")} ${this.goalRunnerLastEvalReason}\n`;
4617
+ }
4618
+ this.chatContainer.addChild(new Spacer(1));
4619
+ this.chatContainer.addChild(new Markdown(info, 1, 0, this.getMarkdownThemeWithSettings()));
4620
+ this.chatContainer.addChild(new Spacer(1));
4621
+ this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal clear - clear goal`), 1, 0));
4622
+ this.ui.requestRender();
4623
+ return false;
4624
+ }
4625
+ // Handle clear / stop / off / reset / cancel aliases (like Claude Code)
4626
+ const lower = goal.toLowerCase();
4627
+ const clearAliases = new Set(["clear", "stop", "off", "reset", "none", "cancel"]);
4628
+ if (clearAliases.has(lower)) {
4629
+ const prevGoal = this.session.sessionGoal;
4630
+ if (prevGoal) {
4631
+ this.goalRunnerEnabled = false;
4632
+ this.goalRunnerAbortController?.abort();
4633
+ this.goalRunnerTurnCount = 0;
4634
+ this.goalRunnerStartTime = 0;
4635
+ this.goalRunnerTokenSpend = 0;
4636
+ this.goalRunnerLastEvalReason = "";
4637
+ // Clear the goal from session and rebuild system prompt
4638
+ this.session.clearSessionGoal();
4602
4639
  this.chatContainer.addChild(new Spacer(1));
4603
- this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal: ${currentGoal} (goal runner: ${runnerStatus})`), 1, 0));
4604
- this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal on - enable goal runner`), 1, 0));
4605
- this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal off - pause goal runner`), 1, 0));
4640
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Goal cleared: "${prevGoal}"`), 1, 0));
4606
4641
  }
4607
4642
  else {
4608
- this.showWarning("Usage: /goal <goal description>");
4643
+ this.showWarning("No active goal to clear.");
4609
4644
  }
4610
4645
  this.ui.requestRender();
4611
- return;
4646
+ return false;
4612
4647
  }
4613
- const lower = goal.toLowerCase();
4614
- if (lower === "on" || lower === "off") {
4615
- this.goalRunnerEnabled = lower === "on";
4616
- if (lower === "on") {
4617
- this.goalRunnerRecentOutputs = [];
4618
- }
4648
+ if (lower === "on") {
4649
+ this.goalRunnerEnabled = true;
4619
4650
  const currentGoal = this.session.sessionGoal;
4620
4651
  if (currentGoal) {
4621
- const label = lower === "on" ? theme.fg("success", "enabled") : theme.fg("warning", "paused");
4622
4652
  this.chatContainer.addChild(new Spacer(1));
4623
- this.chatContainer.addChild(new Text(theme.fg("dim", `Goal runner ${label} for goal: "${currentGoal}"`), 1, 0));
4653
+ this.chatContainer.addChild(new Text(theme.fg("success", `Goal runner enabled for: "${currentGoal}"`), 1, 0));
4624
4654
  }
4625
4655
  else {
4626
- this.showWarning("No goal is set. Use /goal <description> to set one first.");
4656
+ this.showWarning("No goal is set. Use /goal <condition> to set one first.");
4627
4657
  }
4628
4658
  this.ui.requestRender();
4629
- return;
4659
+ return false;
4630
4660
  }
4661
+ // Set new goal — like Claude Code, the goal text is submitted as the next user message
4631
4662
  this.session.setSessionGoal(goal);
4632
4663
  this.goalRunnerEnabled = true;
4633
- this.goalRunnerRecentOutputs = [];
4664
+ this.goalRunnerTurnCount = 0;
4665
+ this.goalRunnerStartTime = Date.now();
4666
+ this.goalRunnerTokenSpend = 0;
4667
+ this.goalRunnerLastEvalReason = "";
4634
4668
  this.chatContainer.addChild(new Spacer(1));
4635
- this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal set: ${goal}`), 1, 0));
4669
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal set: "${goal}" — working toward it...`), 1, 0));
4636
4670
  this.ui.requestRender();
4671
+ return true;
4672
+ }
4673
+ formatDuration(ms) {
4674
+ const seconds = Math.floor(ms / 1000);
4675
+ if (seconds < 60)
4676
+ return `${seconds}s`;
4677
+ const minutes = Math.floor(seconds / 60);
4678
+ const secs = seconds % 60;
4679
+ if (minutes < 60)
4680
+ return `${minutes}m ${secs}s`;
4681
+ const hours = Math.floor(minutes / 60);
4682
+ const mins = minutes % 60;
4683
+ return `${hours}h ${mins}m ${secs}s`;
4637
4684
  }
4638
4685
  /**
4639
- * Goal runner continuation loop.
4640
- * After each turn completes, auto-prompts the agent to continue pursuing the goal.
4641
- * Handles errors with exponential backoff and respects user interruption.
4686
+ * Goal runner continuation loop — Claude Code-style evaluator loop.
4687
+ *
4688
+ * After each turn:
4689
+ * 1. Runs the evaluator to check if the condition is met
4690
+ * 2. If met: shows achievement and stops
4691
+ * 3. If not met: continues with next turn
4692
+ *
4693
+ * Handles errors with exponential backoff.
4694
+ * User can interrupt with the interrupt key.
4642
4695
  * Survives crashes: on next startup, the persisted session goal triggers auto-resume.
4643
4696
  */
4644
4697
  async goalRunnerContinuationLoop(goal) {
4645
- // Abort any previous goal runner controller
4646
4698
  this.goalRunnerAbortController?.abort();
4647
4699
  this.goalRunnerAbortController = new AbortController();
4648
4700
  const signal = this.goalRunnerAbortController.signal;
4701
+ let nextDirection;
4649
4702
  try {
4650
4703
  while (this.goalRunnerEnabled && this.session.sessionGoal) {
4651
4704
  if (signal.aborted)
4652
4705
  break;
4653
- // Small delay between turns so the user can see what happened
4654
4706
  try {
4655
4707
  await sleep(this.GOAL_RUNNER_TURN_DELAY_MS, signal);
4656
4708
  }
@@ -4659,67 +4711,69 @@ export class InteractiveMode {
4659
4711
  }
4660
4712
  if (signal.aborted)
4661
4713
  break;
4662
- // Skip if the agent is already streaming or compacting
4663
- if (this.session.isStreaming || this.session.isCompacting) {
4714
+ if (this.session.isStreaming || this.session.isCompacting)
4664
4715
  continue;
4665
- }
4666
- // Show status on first iteration
4667
4716
  if (!this.goalRunnerStatusDisplayed) {
4668
4717
  this.goalRunnerStatusDisplayed = true;
4669
- this.showStatus(`Pursuing goal: "${goal}" (${keyText("app.interrupt")} to pause)`);
4718
+ this.showStatus(`Goal: "${goal}" (${keyText("app.interrupt")} to pause, /goal clear to clear)`);
4670
4719
  }
4671
4720
  try {
4672
- await this.session.prompt(`Continue pursuing the goal: "${goal}". If you have fully completed this goal, report what was accomplished. If you cannot make further progress, explain what blocked you. Do not repeat the same status/report as previous turns — either make concrete progress or clearly state that the goal is done/blocked.`);
4673
- // Successful turn completed, reset retry counter
4721
+ // Use evaluator's nextDirection, or fall back to generic prompt
4722
+ const promptText = nextDirection
4723
+ ? nextDirection
4724
+ : `Work toward the goal: "${goal}". This goal must be completed. Explore, plan, and execute. Do not stop until it is done.`;
4725
+ nextDirection = undefined;
4726
+ await this.session.prompt(promptText);
4674
4727
  this.goalRunnerRetryAttempt = 0;
4675
- // Auto-pause when the model keeps emitting near-duplicate content.
4676
- const lastText = this.session.getLastAssistantText();
4677
- if (lastText) {
4678
- const normalized = normalizeGoalOutput(lastText);
4679
- const prev = this.goalRunnerRecentOutputs[this.goalRunnerRecentOutputs.length - 1];
4680
- if (prev && isNearDuplicateGoalOutput(prev, normalized)) {
4681
- this.goalRunnerRecentOutputs.push(normalized);
4682
- }
4683
- else {
4684
- this.goalRunnerRecentOutputs = [normalized];
4685
- }
4686
- // Keep a small window
4687
- if (this.goalRunnerRecentOutputs.length > this.GOAL_RUNNER_MAX_DUPLICATE_STREAK) {
4688
- this.goalRunnerRecentOutputs = this.goalRunnerRecentOutputs.slice(-this.GOAL_RUNNER_MAX_DUPLICATE_STREAK);
4689
- }
4690
- if (this.goalRunnerRecentOutputs.length >= this.GOAL_RUNNER_MAX_DUPLICATE_STREAK) {
4691
- const window = this.goalRunnerRecentOutputs;
4692
- let allDup = true;
4693
- for (let i = 1; i < window.length; i++) {
4694
- if (!isNearDuplicateGoalOutput(window[i - 1], window[i])) {
4695
- allDup = false;
4696
- break;
4697
- }
4698
- }
4699
- if (allDup) {
4728
+ this.goalRunnerTurnCount++;
4729
+ // Track token spend from last assistant message if available
4730
+ const lastMsg = this.session.getLastAssistantMessage();
4731
+ if (lastMsg?.usage) {
4732
+ this.goalRunnerTokenSpend += lastMsg.usage.input + lastMsg.usage.output;
4733
+ }
4734
+ // Run evaluator to check if condition is met
4735
+ const model = this.session.model;
4736
+ if (model) {
4737
+ const auth = await this.session.modelRegistry.getApiKeyAndHeaders(model);
4738
+ if (auth.ok) {
4739
+ const lastText = this.session.getLastAssistantText();
4740
+ const evaluation = await evaluateGoalCondition(goal, lastText, model, {
4741
+ apiKey: auth.apiKey,
4742
+ headers: auth.headers,
4743
+ signal,
4744
+ });
4745
+ this.goalRunnerLastEvalReason = evaluation.reason;
4746
+ if (evaluation.met) {
4700
4747
  this.goalRunnerEnabled = false;
4701
- this.goalRunnerRecentOutputs = [];
4702
- this.showStatus(`Goal runner paused: model repeated similar output ${this.GOAL_RUNNER_MAX_DUPLICATE_STREAK} turns in a row. Use /goal on to resume, or refine the goal.`);
4748
+ const elapsed = this.formatDuration(Date.now() - this.goalRunnerStartTime);
4749
+ this.chatContainer.addChild(new Spacer(1));
4750
+ this.chatContainer.addChild(new Text(theme.fg("success", `Goal achieved! (${this.goalRunnerTurnCount} turns, ${elapsed})`), 1, 0));
4751
+ this.chatContainer.addChild(new Text(theme.fg("dim", ` ${evaluation.reason}`), 1, 0));
4752
+ this.ui.requestRender();
4703
4753
  break;
4704
4754
  }
4755
+ // Capture next direction from evaluator
4756
+ nextDirection = evaluation.nextDirection;
4757
+ // Update status with evaluator reason
4758
+ this.showStatus(`Goal: "${goal}" — ${evaluation.reason} (${this.goalRunnerTurnCount} turn(s), ${keyText("app.interrupt")} to pause)`);
4759
+ }
4760
+ else {
4761
+ nextDirection = undefined;
4762
+ this.showStatus(`Goal: "${goal}" — continuing (${this.goalRunnerTurnCount} turn(s), ${keyText("app.interrupt")} to pause)`);
4705
4763
  }
4706
4764
  }
4765
+ else {
4766
+ nextDirection = undefined;
4767
+ this.showStatus(`Goal: "${goal}" — continuing (${this.goalRunnerTurnCount} turn(s), ${keyText("app.interrupt")} to pause)`);
4768
+ }
4707
4769
  }
4708
4770
  catch (error) {
4771
+ // Error occurred — retry indefinitely, never stop
4709
4772
  this.goalRunnerRetryAttempt++;
4710
4773
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
4711
- if (this.goalRunnerRetryAttempt >= this.GOAL_RUNNER_MAX_RETRIES) {
4712
- this.goalRunnerEnabled = false;
4713
- this.showError(`Goal runner paused after ${this.GOAL_RUNNER_MAX_RETRIES} consecutive errors. ` +
4714
- `Last error: ${errorMessage}. Use /goal on to resume.`);
4715
- break;
4716
- }
4717
- // Exponential backoff
4718
- const delay = this.GOAL_RUNNER_BASE_DELAY_MS * 2 ** (this.goalRunnerRetryAttempt - 1);
4719
- this.showStatus(`Goal runner error, retrying in ${Math.ceil(delay / 1000)}s ` +
4720
- `(${this.goalRunnerRetryAttempt}/${this.GOAL_RUNNER_MAX_RETRIES})... (${keyText("app.interrupt")} to pause)`);
4774
+ this.showStatus(`Goal runner error (attempt ${this.goalRunnerRetryAttempt}), retrying in ${Math.ceil(this.GOAL_RUNNER_RETRY_DELAY_MS / 1000)}s: ${errorMessage} — (${keyText("app.interrupt")} to pause)`);
4721
4775
  try {
4722
- await sleep(delay, signal);
4776
+ await sleep(this.GOAL_RUNNER_RETRY_DELAY_MS, signal);
4723
4777
  }
4724
4778
  catch {
4725
4779
  break;