@openeryc/pi-coding-agent 0.75.45 → 0.75.47

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 (35) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +6 -2
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/auth-storage.d.ts +28 -1
  6. package/dist/core/auth-storage.d.ts.map +1 -1
  7. package/dist/core/auth-storage.js +21 -0
  8. package/dist/core/auth-storage.js.map +1 -1
  9. package/dist/core/http-dispatcher.d.ts +2 -0
  10. package/dist/core/http-dispatcher.d.ts.map +1 -1
  11. package/dist/core/http-dispatcher.js +10 -1
  12. package/dist/core/http-dispatcher.js.map +1 -1
  13. package/dist/core/model-registry.d.ts +20 -1
  14. package/dist/core/model-registry.d.ts.map +1 -1
  15. package/dist/core/model-registry.js +59 -0
  16. package/dist/core/model-registry.js.map +1 -1
  17. package/dist/core/sdk.d.ts.map +1 -1
  18. package/dist/core/sdk.js +3 -3
  19. package/dist/core/sdk.js.map +1 -1
  20. package/dist/modes/interactive/components/oauth-selector.d.ts +1 -1
  21. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  23. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  24. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.js +197 -2
  26. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  27. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  28. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  29. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  30. package/examples/extensions/sandbox/package-lock.json +2 -2
  31. package/examples/extensions/sandbox/package.json +1 -1
  32. package/examples/extensions/with-deps/package-lock.json +2 -2
  33. package/examples/extensions/with-deps/package.json +1 -1
  34. package/npm-shrinkwrap.json +12 -12
  35. package/package.json +4 -4
@@ -30,6 +30,7 @@ import { parseGitUrl } from "../../utils/git.js";
30
30
  import { getCwdRelativePath } from "../../utils/paths.js";
31
31
  import { getPiUserAgent } from "../../utils/pi-user-agent.js";
32
32
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
33
+ import { sleep } from "../../utils/sleep.js";
33
34
  import { ensureTool } from "../../utils/tools-manager.js";
34
35
  import { checkForNewPiVersion } from "../../utils/version-check.js";
35
36
  import { ArminComponent } from "./components/armin.js";
@@ -172,6 +173,15 @@ export class InteractiveMode {
172
173
  compactionQueuedMessages = [];
173
174
  // Shutdown state
174
175
  shutdownRequested = false;
176
+ // Goal runner state: auto-continues working on a session goal across turns,
177
+ // retries on errors, and survives process restarts via session persistence.
178
+ goalRunnerEnabled = true;
179
+ goalRunnerAbortController = undefined;
180
+ goalRunnerRetryAttempt = 0;
181
+ goalRunnerStatusDisplayed = false;
182
+ GOAL_RUNNER_MAX_RETRIES = 10;
183
+ GOAL_RUNNER_BASE_DELAY_MS = 3000;
184
+ GOAL_RUNNER_TURN_DELAY_MS = 800;
175
185
  // Extension UI state
176
186
  extensionSelector = undefined;
177
187
  extensionInput = undefined;
@@ -507,6 +517,16 @@ export class InteractiveMode {
507
517
  */
508
518
  async run() {
509
519
  await this.init();
520
+ // If resuming a session that has a goal, auto-continue from where it left off.
521
+ // This handles crash recovery and manual resume scenarios.
522
+ const resumedGoal = this.session.sessionGoal;
523
+ if (resumedGoal && this.session.state.messages.length > 0) {
524
+ this.goalRunnerEnabled = true;
525
+ this.chatContainer.addChild(new Spacer(1));
526
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Resuming goal: "${resumedGoal}" (${keyText("app.interrupt")} to pause)`), 1, 0));
527
+ this.ui.requestRender();
528
+ await this.goalRunnerContinuationLoop(resumedGoal);
529
+ }
510
530
  // Start version check asynchronously
511
531
  checkForNewPiVersion(this.version).then((newRelease) => {
512
532
  if (newRelease) {
@@ -562,6 +582,7 @@ export class InteractiveMode {
562
582
  // Main interactive loop
563
583
  while (true) {
564
584
  const userInput = await this.getUserInput();
585
+ this.goalRunnerEnabled = true; // Re-enable on any user input
565
586
  try {
566
587
  await this.session.prompt(userInput);
567
588
  }
@@ -569,6 +590,13 @@ export class InteractiveMode {
569
590
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
570
591
  this.showError(errorMessage);
571
592
  }
593
+ // After the prompt completes (or errors), check if goal runner should auto-continue.
594
+ if (this.goalRunnerEnabled) {
595
+ const goal = this.session.sessionGoal;
596
+ if (goal) {
597
+ await this.goalRunnerContinuationLoop(goal);
598
+ }
599
+ }
572
600
  }
573
601
  }
574
602
  async checkForPackageUpdates() {
@@ -1884,6 +1912,14 @@ export class InteractiveMode {
1884
1912
  if (this.session.isStreaming) {
1885
1913
  this.restoreQueuedMessagesToEditor({ abort: true });
1886
1914
  }
1915
+ else if (this.goalRunnerAbortController) {
1916
+ // Pause goal runner without clearing the goal
1917
+ this.goalRunnerEnabled = false;
1918
+ this.goalRunnerAbortController.abort();
1919
+ this.chatContainer.addChild(new Spacer(1));
1920
+ this.chatContainer.addChild(new Text(theme.fg("dim", "Goal runner paused. Use /goal on to resume."), 1, 0));
1921
+ this.ui.requestRender();
1922
+ }
1887
1923
  else if (this.session.isBashRunning) {
1888
1924
  this.session.abortBash();
1889
1925
  }
@@ -3774,6 +3810,14 @@ export class InteractiveMode {
3774
3810
  authType: "api_key",
3775
3811
  });
3776
3812
  }
3813
+ // Add synthetic entry for OpenAI-compatible custom endpoints (always available)
3814
+ if (authType !== "oauth") {
3815
+ options.push({
3816
+ id: "__openai_compatible__",
3817
+ name: "OpenAI Compatible",
3818
+ authType: "api_key",
3819
+ });
3820
+ }
3777
3821
  const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
3778
3822
  return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
3779
3823
  }
@@ -3785,9 +3829,13 @@ export class InteractiveMode {
3785
3829
  if (!credential) {
3786
3830
  continue;
3787
3831
  }
3832
+ // For OpenAI-compatible providers, show the base URL as the display name
3833
+ const name = credential.type === "openai_compatible"
3834
+ ? `OpenAI Compatible (${credential.baseUrl})`
3835
+ : this.session.modelRegistry.getProviderDisplayName(providerId);
3788
3836
  options.push({
3789
3837
  id: providerId,
3790
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
3838
+ name,
3791
3839
  authType: credential.type,
3792
3840
  });
3793
3841
  }
@@ -3824,6 +3872,9 @@ export class InteractiveMode {
3824
3872
  if (providerOption.authType === "oauth") {
3825
3873
  await this.showLoginDialog(providerOption.id, providerOption.name);
3826
3874
  }
3875
+ else if (providerOption.id === "__openai_compatible__") {
3876
+ await this.showOpenAICompatibleLoginDialog();
3877
+ }
3827
3878
  else if (providerOption.id === BEDROCK_PROVIDER_ID) {
3828
3879
  this.showBedrockSetupDialog(providerOption.id, providerOption.name);
3829
3880
  }
@@ -3974,6 +4025,66 @@ export class InteractiveMode {
3974
4025
  }
3975
4026
  }
3976
4027
  }
4028
+ async showOpenAICompatibleLoginDialog() {
4029
+ const previousModel = this.session.model;
4030
+ const providerId = `openai-compatible-${crypto.randomUUID().slice(0, 8)}`;
4031
+ const providerName = "OpenAI Compatible";
4032
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4033
+ // Completion handled below
4034
+ }, providerName, "Configure OpenAI Compatible Provider");
4035
+ this.editorContainer.clear();
4036
+ this.editorContainer.addChild(dialog);
4037
+ this.ui.setFocus(dialog);
4038
+ this.ui.requestRender();
4039
+ const restoreEditor = () => {
4040
+ this.editorContainer.clear();
4041
+ this.editorContainer.addChild(this.editor);
4042
+ this.ui.setFocus(this.editor);
4043
+ this.ui.requestRender();
4044
+ };
4045
+ try {
4046
+ const baseUrl = (await dialog.showPrompt("Enter base URL:", "http://localhost:11434/v1")).trim();
4047
+ if (!baseUrl) {
4048
+ throw new Error("Base URL cannot be empty.");
4049
+ }
4050
+ const apiKey = (await dialog.showPrompt("Enter API key (or leave blank for local endpoints):")).trim();
4051
+ const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", "llama3.1")).trim();
4052
+ if (!modelNames) {
4053
+ throw new Error("At least one model name is required.");
4054
+ }
4055
+ const models = modelNames
4056
+ .split(",")
4057
+ .map((s) => s.trim())
4058
+ .filter((s) => s.length > 0)
4059
+ .map((id) => ({
4060
+ id,
4061
+ name: id,
4062
+ }));
4063
+ const effectiveApiKey = apiKey || "not-needed";
4064
+ // Immediately register the provider so models are available
4065
+ this.session.modelRegistry.configureOpenAICompatibleProvider(providerId, {
4066
+ baseUrl,
4067
+ apiKey: effectiveApiKey,
4068
+ models,
4069
+ });
4070
+ // Persist to auth.json so it survives restarts
4071
+ this.session.modelRegistry.authStorage.set(providerId, {
4072
+ type: "openai_compatible",
4073
+ key: effectiveApiKey,
4074
+ baseUrl,
4075
+ models,
4076
+ });
4077
+ restoreEditor();
4078
+ await this.completeProviderAuthentication(providerId, `${baseUrl}`, "api_key", previousModel);
4079
+ }
4080
+ catch (error) {
4081
+ restoreEditor();
4082
+ const errorMsg = error instanceof Error ? error.message : String(error);
4083
+ if (errorMsg !== "Login cancelled") {
4084
+ this.showError(`Failed to configure OpenAI Compatible provider: ${errorMsg}`);
4085
+ }
4086
+ }
4087
+ }
3977
4088
  showOAuthLoginSelect(dialog, prompt) {
3978
4089
  return new Promise((resolve) => {
3979
4090
  const restoreDialog = () => {
@@ -4365,9 +4476,12 @@ export class InteractiveMode {
4365
4476
  const goal = text.replace(/^\/goal\s*/, "").trim();
4366
4477
  if (!goal) {
4367
4478
  const currentGoal = this.session.sessionGoal;
4479
+ const runnerStatus = this.goalRunnerEnabled ? "enabled" : "paused";
4368
4480
  if (currentGoal) {
4369
4481
  this.chatContainer.addChild(new Spacer(1));
4370
- this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal: ${currentGoal}`), 1, 0));
4482
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal: ${currentGoal} (goal runner: ${runnerStatus})`), 1, 0));
4483
+ this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal on - enable goal runner`), 1, 0));
4484
+ this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal off - pause goal runner`), 1, 0));
4371
4485
  }
4372
4486
  else {
4373
4487
  this.showWarning("Usage: /goal <goal description>");
@@ -4375,11 +4489,92 @@ export class InteractiveMode {
4375
4489
  this.ui.requestRender();
4376
4490
  return;
4377
4491
  }
4492
+ const lower = goal.toLowerCase();
4493
+ if (lower === "on" || lower === "off") {
4494
+ this.goalRunnerEnabled = lower === "on";
4495
+ const currentGoal = this.session.sessionGoal;
4496
+ if (currentGoal) {
4497
+ const label = lower === "on" ? theme.fg("success", "enabled") : theme.fg("warning", "paused");
4498
+ this.chatContainer.addChild(new Spacer(1));
4499
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Goal runner ${label} for goal: "${currentGoal}"`), 1, 0));
4500
+ }
4501
+ else {
4502
+ this.showWarning("No goal is set. Use /goal <description> to set one first.");
4503
+ }
4504
+ this.ui.requestRender();
4505
+ return;
4506
+ }
4378
4507
  this.session.setSessionGoal(goal);
4508
+ this.goalRunnerEnabled = true;
4379
4509
  this.chatContainer.addChild(new Spacer(1));
4380
4510
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal set: ${goal}`), 1, 0));
4381
4511
  this.ui.requestRender();
4382
4512
  }
4513
+ /**
4514
+ * Goal runner continuation loop.
4515
+ * After each turn completes, auto-prompts the agent to continue pursuing the goal.
4516
+ * Handles errors with exponential backoff and respects user interruption.
4517
+ * Survives crashes: on next startup, the persisted session goal triggers auto-resume.
4518
+ */
4519
+ async goalRunnerContinuationLoop(goal) {
4520
+ // Abort any previous goal runner controller
4521
+ this.goalRunnerAbortController?.abort();
4522
+ this.goalRunnerAbortController = new AbortController();
4523
+ const signal = this.goalRunnerAbortController.signal;
4524
+ try {
4525
+ while (this.goalRunnerEnabled && this.session.sessionGoal) {
4526
+ if (signal.aborted)
4527
+ break;
4528
+ // Small delay between turns so the user can see what happened
4529
+ try {
4530
+ await sleep(this.GOAL_RUNNER_TURN_DELAY_MS, signal);
4531
+ }
4532
+ catch {
4533
+ break;
4534
+ }
4535
+ if (signal.aborted)
4536
+ break;
4537
+ // Skip if the agent is already streaming or compacting
4538
+ if (this.session.isStreaming || this.session.isCompacting) {
4539
+ continue;
4540
+ }
4541
+ // Show status on first iteration
4542
+ if (!this.goalRunnerStatusDisplayed) {
4543
+ this.goalRunnerStatusDisplayed = true;
4544
+ this.showStatus(`Pursuing goal: "${goal}" (${keyText("app.interrupt")} to pause)`);
4545
+ }
4546
+ try {
4547
+ 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.`);
4548
+ // Successful turn completed, reset retry counter
4549
+ this.goalRunnerRetryAttempt = 0;
4550
+ }
4551
+ catch (error) {
4552
+ this.goalRunnerRetryAttempt++;
4553
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4554
+ if (this.goalRunnerRetryAttempt >= this.GOAL_RUNNER_MAX_RETRIES) {
4555
+ this.goalRunnerEnabled = false;
4556
+ this.showError(`Goal runner paused after ${this.GOAL_RUNNER_MAX_RETRIES} consecutive errors. ` +
4557
+ `Last error: ${errorMessage}. Use /goal on to resume.`);
4558
+ break;
4559
+ }
4560
+ // Exponential backoff
4561
+ const delay = this.GOAL_RUNNER_BASE_DELAY_MS * 2 ** (this.goalRunnerRetryAttempt - 1);
4562
+ this.showStatus(`Goal runner error, retrying in ${Math.ceil(delay / 1000)}s ` +
4563
+ `(${this.goalRunnerRetryAttempt}/${this.GOAL_RUNNER_MAX_RETRIES})... (${keyText("app.interrupt")} to pause)`);
4564
+ try {
4565
+ await sleep(delay, signal);
4566
+ }
4567
+ catch {
4568
+ break;
4569
+ }
4570
+ }
4571
+ }
4572
+ }
4573
+ finally {
4574
+ this.goalRunnerAbortController = undefined;
4575
+ this.goalRunnerStatusDisplayed = false;
4576
+ }
4577
+ }
4383
4578
  handleSessionCommand() {
4384
4579
  const stats = this.session.getSessionStats();
4385
4580
  const sessionName = this.sessionManager.getSessionName();