@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.
- package/CHANGELOG.md +11 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +6 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +28 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +21 -0
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts +2 -0
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +10 -1
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/model-registry.d.ts +20 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +59 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +197 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- 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
|
|
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();
|