@openeryc/pi-coding-agent 0.75.57 → 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 (38) hide show
  1. package/CHANGELOG.md +28 -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/components/login-dialog.d.ts +7 -1
  15. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  16. package/dist/modes/interactive/components/login-dialog.js +14 -4
  17. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  18. package/dist/modes/interactive/goal-evaluator.d.ts +23 -0
  19. package/dist/modes/interactive/goal-evaluator.d.ts.map +1 -0
  20. package/dist/modes/interactive/goal-evaluator.js +68 -0
  21. package/dist/modes/interactive/goal-evaluator.js.map +1 -0
  22. package/dist/modes/interactive/goal-runner-utils.d.ts +11 -0
  23. package/dist/modes/interactive/goal-runner-utils.d.ts.map +1 -0
  24. package/dist/modes/interactive/goal-runner-utils.js +39 -0
  25. package/dist/modes/interactive/goal-runner-utils.js.map +1 -0
  26. package/dist/modes/interactive/interactive-mode.d.ts +6 -2
  27. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  28. package/dist/modes/interactive/interactive-mode.js +147 -47
  29. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  30. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  31. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  32. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  33. package/examples/extensions/sandbox/package-lock.json +2 -2
  34. package/examples/extensions/sandbox/package.json +1 -1
  35. package/examples/extensions/with-deps/package-lock.json +2 -2
  36. package/examples/extensions/with-deps/package.json +1 -1
  37. package/npm-shrinkwrap.json +12 -12
  38. package/package.json +4 -4
@@ -62,6 +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 { evaluateGoalCondition } from "./goal-evaluator.js";
65
66
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
66
67
  function isExpandable(obj) {
67
68
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
@@ -181,9 +182,12 @@ export class InteractiveMode {
181
182
  goalRunnerAbortController = undefined;
182
183
  goalRunnerRetryAttempt = 0;
183
184
  goalRunnerStatusDisplayed = false;
184
- GOAL_RUNNER_MAX_RETRIES = 10;
185
- GOAL_RUNNER_BASE_DELAY_MS = 3000;
186
- GOAL_RUNNER_TURN_DELAY_MS = 800;
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;
187
191
  // Extension UI state
188
192
  extensionSelector = undefined;
189
193
  extensionInput = undefined;
@@ -2072,8 +2076,10 @@ export class InteractiveMode {
2072
2076
  // Handle commands (with special case for /goal which continues to submit)
2073
2077
  if (text.startsWith("/goal")) {
2074
2078
  const goal = text.replace(/^\/goal\s*/, "").trim();
2075
- this.handleGoalCommand(text);
2079
+ const shouldSubmit = await this.handleGoalCommand(text);
2076
2080
  this.editor.setText("");
2081
+ if (!shouldSubmit)
2082
+ return;
2077
2083
  if (goal)
2078
2084
  text = goal;
2079
2085
  }
@@ -4031,7 +4037,9 @@ export class InteractiveMode {
4031
4037
  this.ui.requestRender();
4032
4038
  };
4033
4039
  try {
4034
- const rawName = (await dialog.showPrompt(isEdit ? "Provider name/id:" : "Provider name/id (e.g. ollama, agnes):", existing?.providerId ?? "ollama")).trim();
4040
+ const rawName = (await dialog.showPrompt(isEdit ? "Provider name/id:" : "Provider name/id (e.g. ollama, agnes):", isEdit
4041
+ ? { value: existing?.providerId ?? "" }
4042
+ : { placeholder: "ollama", value: existing?.providerId ?? "ollama" })).trim();
4035
4043
  const providerId = slugifyProviderId(rawName);
4036
4044
  if (!providerId) {
4037
4045
  throw new Error("Provider name cannot be empty.");
@@ -4039,7 +4047,9 @@ export class InteractiveMode {
4039
4047
  if (BUILT_IN_MODEL_PROVIDERS.has(providerId) && options.target === "openai_compatible") {
4040
4048
  throw new Error(`"${providerId}" is a built-in provider. Use a different name, or add it via Manage → models.json override.`);
4041
4049
  }
4042
- const baseUrl = (await dialog.showPrompt("Enter base URL:", existing?.baseUrl || "http://localhost:11434/v1")).trim();
4050
+ const baseUrl = (await dialog.showPrompt("Enter base URL:", isEdit || existing?.baseUrl
4051
+ ? { value: existing?.baseUrl || "http://localhost:11434/v1" }
4052
+ : { placeholder: "http://localhost:11434/v1", value: "http://localhost:11434/v1" })).trim();
4043
4053
  if (!baseUrl) {
4044
4054
  throw new Error("Base URL cannot be empty.");
4045
4055
  }
@@ -4050,7 +4060,8 @@ export class InteractiveMode {
4050
4060
  : isEdit
4051
4061
  ? "API key (blank keeps current; type not-needed for local):"
4052
4062
  : "Enter API key (or leave blank for local endpoints):";
4053
- const apiKey = (await dialog.showPrompt(apiKeyPrompt, existing?.apiKey === "not-needed" ? "" : (existing?.apiKey ?? ""))).trim();
4063
+ const apiKeyCurrent = existing?.apiKey === "not-needed" ? "" : (existing?.apiKey ?? "");
4064
+ const apiKey = (await dialog.showPrompt(apiKeyPrompt, isEdit || apiKeyCurrent ? { value: apiKeyCurrent } : { placeholder: "sk-... or leave blank" })).trim();
4054
4065
  // Blank on edit keeps the existing apiKey (models.json and openai_compatible).
4055
4066
  const effectiveApiKey = apiKey ? apiKey : isEdit ? undefined : "not-needed";
4056
4067
  dialog.showInfo([theme.fg("muted", "Discovering models from /models ...")]);
@@ -4119,7 +4130,7 @@ export class InteractiveMode {
4119
4130
  ]);
4120
4131
  this.ui.requestRender();
4121
4132
  const defaultIds = existing?.models.map((m) => m.id).join(", ") || "llama3.1";
4122
- const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", defaultIds)).trim();
4133
+ const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", { value: defaultIds })).trim();
4123
4134
  if (!modelNames) {
4124
4135
  throw new Error("At least one model name is required.");
4125
4136
  }
@@ -4583,60 +4594,115 @@ export class InteractiveMode {
4583
4594
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
4584
4595
  this.ui.requestRender();
4585
4596
  }
4586
- handleGoalCommand(text) {
4597
+ async handleGoalCommand(text) {
4587
4598
  const goal = text.replace(/^\/goal\s*/, "").trim();
4588
4599
  if (!goal) {
4600
+ // Show goal status (like Claude Code's /goal with no args)
4589
4601
  const currentGoal = this.session.sessionGoal;
4590
- const runnerStatus = this.goalRunnerEnabled ? "enabled" : "paused";
4591
- 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();
4592
4639
  this.chatContainer.addChild(new Spacer(1));
4593
- this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal: ${currentGoal} (goal runner: ${runnerStatus})`), 1, 0));
4594
- this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal on - enable goal runner`), 1, 0));
4595
- 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));
4596
4641
  }
4597
4642
  else {
4598
- this.showWarning("Usage: /goal <goal description>");
4643
+ this.showWarning("No active goal to clear.");
4599
4644
  }
4600
4645
  this.ui.requestRender();
4601
- return;
4646
+ return false;
4602
4647
  }
4603
- const lower = goal.toLowerCase();
4604
- if (lower === "on" || lower === "off") {
4605
- this.goalRunnerEnabled = lower === "on";
4648
+ if (lower === "on") {
4649
+ this.goalRunnerEnabled = true;
4606
4650
  const currentGoal = this.session.sessionGoal;
4607
4651
  if (currentGoal) {
4608
- const label = lower === "on" ? theme.fg("success", "enabled") : theme.fg("warning", "paused");
4609
4652
  this.chatContainer.addChild(new Spacer(1));
4610
- 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));
4611
4654
  }
4612
4655
  else {
4613
- 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.");
4614
4657
  }
4615
4658
  this.ui.requestRender();
4616
- return;
4659
+ return false;
4617
4660
  }
4661
+ // Set new goal — like Claude Code, the goal text is submitted as the next user message
4618
4662
  this.session.setSessionGoal(goal);
4619
4663
  this.goalRunnerEnabled = true;
4664
+ this.goalRunnerTurnCount = 0;
4665
+ this.goalRunnerStartTime = Date.now();
4666
+ this.goalRunnerTokenSpend = 0;
4667
+ this.goalRunnerLastEvalReason = "";
4620
4668
  this.chatContainer.addChild(new Spacer(1));
4621
- 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));
4622
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`;
4623
4684
  }
4624
4685
  /**
4625
- * Goal runner continuation loop.
4626
- * After each turn completes, auto-prompts the agent to continue pursuing the goal.
4627
- * 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.
4628
4695
  * Survives crashes: on next startup, the persisted session goal triggers auto-resume.
4629
4696
  */
4630
4697
  async goalRunnerContinuationLoop(goal) {
4631
- // Abort any previous goal runner controller
4632
4698
  this.goalRunnerAbortController?.abort();
4633
4699
  this.goalRunnerAbortController = new AbortController();
4634
4700
  const signal = this.goalRunnerAbortController.signal;
4701
+ let nextDirection;
4635
4702
  try {
4636
4703
  while (this.goalRunnerEnabled && this.session.sessionGoal) {
4637
4704
  if (signal.aborted)
4638
4705
  break;
4639
- // Small delay between turns so the user can see what happened
4640
4706
  try {
4641
4707
  await sleep(this.GOAL_RUNNER_TURN_DELAY_MS, signal);
4642
4708
  }
@@ -4645,35 +4711,69 @@ export class InteractiveMode {
4645
4711
  }
4646
4712
  if (signal.aborted)
4647
4713
  break;
4648
- // Skip if the agent is already streaming or compacting
4649
- if (this.session.isStreaming || this.session.isCompacting) {
4714
+ if (this.session.isStreaming || this.session.isCompacting)
4650
4715
  continue;
4651
- }
4652
- // Show status on first iteration
4653
4716
  if (!this.goalRunnerStatusDisplayed) {
4654
4717
  this.goalRunnerStatusDisplayed = true;
4655
- this.showStatus(`Pursuing goal: "${goal}" (${keyText("app.interrupt")} to pause)`);
4718
+ this.showStatus(`Goal: "${goal}" (${keyText("app.interrupt")} to pause, /goal clear to clear)`);
4656
4719
  }
4657
4720
  try {
4658
- 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.`);
4659
- // 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);
4660
4727
  this.goalRunnerRetryAttempt = 0;
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) {
4747
+ this.goalRunnerEnabled = false;
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();
4753
+ break;
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)`);
4763
+ }
4764
+ }
4765
+ else {
4766
+ nextDirection = undefined;
4767
+ this.showStatus(`Goal: "${goal}" — continuing (${this.goalRunnerTurnCount} turn(s), ${keyText("app.interrupt")} to pause)`);
4768
+ }
4661
4769
  }
4662
4770
  catch (error) {
4771
+ // Error occurred — retry indefinitely, never stop
4663
4772
  this.goalRunnerRetryAttempt++;
4664
4773
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
4665
- if (this.goalRunnerRetryAttempt >= this.GOAL_RUNNER_MAX_RETRIES) {
4666
- this.goalRunnerEnabled = false;
4667
- this.showError(`Goal runner paused after ${this.GOAL_RUNNER_MAX_RETRIES} consecutive errors. ` +
4668
- `Last error: ${errorMessage}. Use /goal on to resume.`);
4669
- break;
4670
- }
4671
- // Exponential backoff
4672
- const delay = this.GOAL_RUNNER_BASE_DELAY_MS * 2 ** (this.goalRunnerRetryAttempt - 1);
4673
- this.showStatus(`Goal runner error, retrying in ${Math.ceil(delay / 1000)}s ` +
4674
- `(${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)`);
4675
4775
  try {
4676
- await sleep(delay, signal);
4776
+ await sleep(this.GOAL_RUNNER_RETRY_DELAY_MS, signal);
4677
4777
  }
4678
4778
  catch {
4679
4779
  break;