@oh-my-pi/pi-coding-agent 16.0.1 → 16.0.2
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 +25 -0
- package/dist/cli.js +135 -131
- package/dist/types/config/model-resolver.d.ts +14 -0
- package/dist/types/exec/non-interactive-env.d.ts +2 -0
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/utils/markit.d.ts +8 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +44 -0
- package/src/cli/args.ts +1 -0
- package/src/config/model-resolver.ts +35 -1
- package/src/discovery/github.ts +89 -1
- package/src/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +154 -21
- package/src/extensibility/plugins/manager.ts +40 -33
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/session/agent-session.ts +135 -32
- package/src/session/messages.ts +1 -1
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +100 -4
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
|
@@ -139,9 +139,11 @@ import {
|
|
|
139
139
|
filterAvailableModelsByEnabledPatterns,
|
|
140
140
|
formatModelSelectorValue,
|
|
141
141
|
formatModelString,
|
|
142
|
+
formatModelStringWithRouting,
|
|
142
143
|
getModelMatchPreferences,
|
|
143
144
|
parseModelString,
|
|
144
145
|
type ResolvedModelRoleValue,
|
|
146
|
+
resolveModelOverride,
|
|
145
147
|
resolveModelRoleValue,
|
|
146
148
|
resolveRoleSelection,
|
|
147
149
|
} from "../config/model-resolver";
|
|
@@ -273,6 +275,7 @@ import {
|
|
|
273
275
|
type BashExecutionMessage,
|
|
274
276
|
type CustomMessage,
|
|
275
277
|
convertToLlm,
|
|
278
|
+
GENERIC_ABORT_SENTINEL,
|
|
276
279
|
type PythonExecutionMessage,
|
|
277
280
|
readQueueChipText,
|
|
278
281
|
SILENT_ABORT_MARKER,
|
|
@@ -640,8 +643,7 @@ function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | u
|
|
|
640
643
|
}
|
|
641
644
|
|
|
642
645
|
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
643
|
-
|
|
644
|
-
return thinkingLevel ? `${selector}:${thinkingLevel}` : selector;
|
|
646
|
+
return formatModelSelectorValue(formatModelStringWithRouting(model), thinkingLevel);
|
|
645
647
|
}
|
|
646
648
|
|
|
647
649
|
function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): string {
|
|
@@ -2402,6 +2404,11 @@ export class AgentSession {
|
|
|
2402
2404
|
return;
|
|
2403
2405
|
}
|
|
2404
2406
|
|
|
2407
|
+
if (this.#isRetryableReasonlessAbort(msg)) {
|
|
2408
|
+
const didRetry = await this.#handleRetryableError(msg, { allowModelFallback: false });
|
|
2409
|
+
if (didRetry) return;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2405
2412
|
// A deliberate abort should settle the current turn, not trigger queued continuations.
|
|
2406
2413
|
if (msg.stopReason === "aborted") {
|
|
2407
2414
|
this.#resolveRetry();
|
|
@@ -2566,6 +2573,11 @@ export class AgentSession {
|
|
|
2566
2573
|
|
|
2567
2574
|
#scheduleAutoContinuePrompt(generation: number): void {
|
|
2568
2575
|
const continuePrompt = async () => {
|
|
2576
|
+
// Compaction summarizes away the first-message eager preludes, so re-assert the
|
|
2577
|
+
// delegate-via-tasks / phased-todo reminders on this auto-resumed turn. This runs
|
|
2578
|
+
// at invocation (past the abort check below), so an aborted continuation queues
|
|
2579
|
+
// nothing; scoped to this request via prependMessages, never the shared queue.
|
|
2580
|
+
const eagerNudges = this.#buildPostCompactionEagerNudges();
|
|
2569
2581
|
await this.#promptWithMessage(
|
|
2570
2582
|
{
|
|
2571
2583
|
role: "developer",
|
|
@@ -2574,7 +2586,10 @@ export class AgentSession {
|
|
|
2574
2586
|
timestamp: Date.now(),
|
|
2575
2587
|
},
|
|
2576
2588
|
autoContinuePrompt,
|
|
2577
|
-
{
|
|
2589
|
+
{
|
|
2590
|
+
skipPostPromptRecoveryWait: true,
|
|
2591
|
+
prependMessages: eagerNudges.length > 0 ? eagerNudges : undefined,
|
|
2592
|
+
},
|
|
2578
2593
|
);
|
|
2579
2594
|
};
|
|
2580
2595
|
this.#schedulePostPromptTask(
|
|
@@ -7774,7 +7789,9 @@ export class AgentSession {
|
|
|
7774
7789
|
};
|
|
7775
7790
|
}
|
|
7776
7791
|
|
|
7777
|
-
#createEagerTodoPrelude(
|
|
7792
|
+
#createEagerTodoPrelude(
|
|
7793
|
+
promptText: string | undefined,
|
|
7794
|
+
): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
|
|
7778
7795
|
const mode = this.settings.get("todo.eager");
|
|
7779
7796
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
7780
7797
|
if (mode === "default" || !todosEnabled) {
|
|
@@ -7791,14 +7808,18 @@ export class AgentSession {
|
|
|
7791
7808
|
// Only inject on the first user message of the conversation. Subsequent user
|
|
7792
7809
|
// turns must not receive the eager todo reminder — they often correct, clarify,
|
|
7793
7810
|
// or redirect the prior task, and forcing a brand-new todo list there is wrong.
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7811
|
+
// When `promptText` is undefined (post-compaction re-injection) there is no fresh
|
|
7812
|
+
// user message to gate on, so skip the first-message and prompt-suffix checks.
|
|
7813
|
+
if (promptText !== undefined) {
|
|
7814
|
+
const hasPriorUserMessage = this.agent.state.messages.some(m => m.role === "user");
|
|
7815
|
+
if (hasPriorUserMessage) {
|
|
7816
|
+
return undefined;
|
|
7817
|
+
}
|
|
7798
7818
|
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7819
|
+
const trimmedPromptText = promptText.trimEnd();
|
|
7820
|
+
if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
|
|
7821
|
+
return undefined;
|
|
7822
|
+
}
|
|
7802
7823
|
}
|
|
7803
7824
|
|
|
7804
7825
|
// Must check the active tool set, not just the registry: tool discovery
|
|
@@ -7821,8 +7842,10 @@ export class AgentSession {
|
|
|
7821
7842
|
timestamp: Date.now(),
|
|
7822
7843
|
};
|
|
7823
7844
|
// `preferred` suggests a todo list (reminder only); `always` also forces the
|
|
7824
|
-
// `todo` tool on the first turn — the previous boolean-on behavior.
|
|
7825
|
-
|
|
7845
|
+
// `todo` tool on the first turn — the previous boolean-on behavior. Post-compaction
|
|
7846
|
+
// re-injection (`promptText === undefined`) is always reminder-only: forcing a tool
|
|
7847
|
+
// onto the auto-resumed turn would override the agent's in-flight action.
|
|
7848
|
+
if (promptText === undefined || mode === "preferred") {
|
|
7826
7849
|
return { message };
|
|
7827
7850
|
}
|
|
7828
7851
|
const todoToolChoice = buildNamedToolChoice("todo", this.model);
|
|
@@ -7840,7 +7863,7 @@ export class AgentSession {
|
|
|
7840
7863
|
return { message, toolChoice: todoToolChoice };
|
|
7841
7864
|
}
|
|
7842
7865
|
|
|
7843
|
-
#createEagerTaskPrelude(promptText: string): AgentMessage | undefined {
|
|
7866
|
+
#createEagerTaskPrelude(promptText: string | undefined): AgentMessage | undefined {
|
|
7844
7867
|
if (this.settings.get("task.eager") !== "always") return undefined;
|
|
7845
7868
|
// Main agent only: subagents keep `task` active (the parent only filters `todo`),
|
|
7846
7869
|
// so a salient delegate-reminder there would amplify nested fan-out. Gate on the
|
|
@@ -7848,9 +7871,13 @@ export class AgentSession {
|
|
|
7848
7871
|
// still gets the reminder.
|
|
7849
7872
|
if (this.#agentKind === "sub") return undefined;
|
|
7850
7873
|
if (this.#planModeState?.enabled) return undefined;
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
if (
|
|
7874
|
+
// First-message-only gates are skipped post-compaction (`promptText === undefined`),
|
|
7875
|
+
// where there is no fresh user message to suppress the reminder for.
|
|
7876
|
+
if (promptText !== undefined) {
|
|
7877
|
+
if (this.agent.state.messages.some(m => m.role === "user")) return undefined;
|
|
7878
|
+
const trimmed = promptText.trimEnd();
|
|
7879
|
+
if (trimmed.endsWith("?") || trimmed.endsWith("!")) return undefined;
|
|
7880
|
+
}
|
|
7854
7881
|
if (!this.getActiveToolNames().includes("task")) return undefined;
|
|
7855
7882
|
return {
|
|
7856
7883
|
role: "custom",
|
|
@@ -7861,6 +7888,24 @@ export class AgentSession {
|
|
|
7861
7888
|
timestamp: Date.now(),
|
|
7862
7889
|
};
|
|
7863
7890
|
}
|
|
7891
|
+
|
|
7892
|
+
/**
|
|
7893
|
+
* Build the eager task/todo reminders to re-inject on the auto-continuation turn that
|
|
7894
|
+
* follows a compaction. The first-message preludes are the oldest messages, so
|
|
7895
|
+
* compaction summarizes them away and the agent silently loses the delegate-via-tasks
|
|
7896
|
+
* and phased-todo guidance mid-work; this re-asserts them, reminder-only (the todo
|
|
7897
|
+
* builder drops its forced tool_choice when `promptText` is undefined). Each builder
|
|
7898
|
+
* still applies its own mode / agent-kind / plan-mode / tool-active / surviving-todo
|
|
7899
|
+
* gates, so an empty array means nothing currently warrants a nudge.
|
|
7900
|
+
*/
|
|
7901
|
+
#buildPostCompactionEagerNudges(): AgentMessage[] {
|
|
7902
|
+
const nudges: AgentMessage[] = [];
|
|
7903
|
+
const todo = this.#createEagerTodoPrelude(undefined);
|
|
7904
|
+
if (todo) nudges.push(todo.message);
|
|
7905
|
+
const task = this.#createEagerTaskPrelude(undefined);
|
|
7906
|
+
if (task) nudges.push(task);
|
|
7907
|
+
return nudges;
|
|
7908
|
+
}
|
|
7864
7909
|
/**
|
|
7865
7910
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
7866
7911
|
*/
|
|
@@ -9136,9 +9181,31 @@ export class AgentSession {
|
|
|
9136
9181
|
// Auto-Retry
|
|
9137
9182
|
// =========================================================================
|
|
9138
9183
|
|
|
9184
|
+
/**
|
|
9185
|
+
* Retry an empty, reason-less provider abort: a turn that ended `aborted`
|
|
9186
|
+
* with no content and the generic sentinel (bare `abort()`), but only while
|
|
9187
|
+
* the session is neither aborting nor tearing down. A user/lifecycle abort
|
|
9188
|
+
* (`#abortInProgress`), a dispose-driven abort (`#isDisposed`), or a
|
|
9189
|
+
* session-induced streaming-edit guard abort (`#streamingEditAbortTriggered` —
|
|
9190
|
+
* auto-generated-file guard or failed-patch preview) is deliberate and MUST
|
|
9191
|
+
* settle the turn instead: routing it through retry would orphan
|
|
9192
|
+
* `#retryPromise` on a continuation the guard skips (hanging the in-flight
|
|
9193
|
+
* `prompt()`) or silently undo the guard's intended abort.
|
|
9194
|
+
*/
|
|
9195
|
+
#isRetryableReasonlessAbort(message: AssistantMessage): boolean {
|
|
9196
|
+
return (
|
|
9197
|
+
message.stopReason === "aborted" &&
|
|
9198
|
+
message.content.length === 0 &&
|
|
9199
|
+
message.errorMessage === GENERIC_ABORT_SENTINEL &&
|
|
9200
|
+
!this.#abortInProgress &&
|
|
9201
|
+
!this.#isDisposed &&
|
|
9202
|
+
!this.#streamingEditAbortTriggered
|
|
9203
|
+
);
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9139
9206
|
/**
|
|
9140
9207
|
* Check if an error is retryable (transient errors or usage limits).
|
|
9141
|
-
* Context overflow
|
|
9208
|
+
* Context overflow is NOT retryable (handled by compaction instead).
|
|
9142
9209
|
* Usage-limit errors are retryable because the retry handler performs credential switching.
|
|
9143
9210
|
*/
|
|
9144
9211
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
@@ -9295,11 +9362,25 @@ export class AgentSession {
|
|
|
9295
9362
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9296
9363
|
if (!parsedCurrent) return undefined;
|
|
9297
9364
|
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
9365
|
+
const currentPlainSelector = this.model
|
|
9366
|
+
? formatModelSelectorValue(formatModelString(this.model), parsedCurrent.thinkingLevel)
|
|
9367
|
+
: undefined;
|
|
9368
|
+
const currentPlainBaseSelector =
|
|
9369
|
+
currentPlainSelector && currentPlainSelector !== currentSelector
|
|
9370
|
+
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
9371
|
+
: undefined;
|
|
9372
|
+
|
|
9373
|
+
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
9374
|
+
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
9375
|
+
if (primarySelector?.raw === currentSelector) return role;
|
|
9376
|
+
}
|
|
9298
9377
|
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
9299
9378
|
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
9300
9379
|
if (!primarySelector) continue;
|
|
9301
|
-
if (primarySelector.raw ===
|
|
9302
|
-
|
|
9380
|
+
if (currentPlainSelector && primarySelector.raw === currentPlainSelector) return role;
|
|
9381
|
+
const primaryBaseSelector = formatRetryFallbackBaseSelector(primarySelector);
|
|
9382
|
+
if (primaryBaseSelector === currentBaseSelector) return role;
|
|
9383
|
+
if (currentPlainBaseSelector && primaryBaseSelector === currentPlainBaseSelector) return role;
|
|
9303
9384
|
}
|
|
9304
9385
|
return undefined;
|
|
9305
9386
|
}
|
|
@@ -9323,10 +9404,23 @@ export class AgentSession {
|
|
|
9323
9404
|
if (chain.length <= 1) return [];
|
|
9324
9405
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9325
9406
|
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
9326
|
-
const
|
|
9407
|
+
const currentPlainSelector =
|
|
9408
|
+
this.model && parsedCurrent
|
|
9409
|
+
? formatModelSelectorValue(formatModelString(this.model), parsedCurrent.thinkingLevel)
|
|
9410
|
+
: undefined;
|
|
9411
|
+
const currentPlainBaseSelector =
|
|
9412
|
+
parsedCurrent && currentPlainSelector && currentPlainSelector !== currentSelector
|
|
9413
|
+
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
9414
|
+
: undefined;
|
|
9415
|
+
const exactIndex = chain.findIndex(
|
|
9416
|
+
selector => selector.raw === currentSelector || selector.raw === currentPlainSelector,
|
|
9417
|
+
);
|
|
9327
9418
|
if (exactIndex >= 0) return chain.slice(exactIndex + 1);
|
|
9328
9419
|
const baseIndex = currentBaseSelector
|
|
9329
|
-
? chain.findIndex(selector =>
|
|
9420
|
+
? chain.findIndex(selector => {
|
|
9421
|
+
const selectorBase = formatRetryFallbackBaseSelector(selector);
|
|
9422
|
+
return selectorBase === currentBaseSelector || selectorBase === currentPlainBaseSelector;
|
|
9423
|
+
})
|
|
9330
9424
|
: -1;
|
|
9331
9425
|
if (baseIndex >= 0) return chain.slice(baseIndex + 1);
|
|
9332
9426
|
return chain.slice(1);
|
|
@@ -9338,7 +9432,8 @@ export class AgentSession {
|
|
|
9338
9432
|
currentSelector: string,
|
|
9339
9433
|
options?: { pinFallback?: boolean },
|
|
9340
9434
|
): Promise<void> {
|
|
9341
|
-
const
|
|
9435
|
+
const resolved = resolveModelOverride([selector.raw], this.#modelRegistry, this.settings);
|
|
9436
|
+
const candidate = resolved.model ?? this.#modelRegistry.find(selector.provider, selector.id);
|
|
9342
9437
|
if (!candidate) {
|
|
9343
9438
|
throw new Error(`Retry fallback model not found: ${selector.raw}`);
|
|
9344
9439
|
}
|
|
@@ -9351,10 +9446,10 @@ export class AgentSession {
|
|
|
9351
9446
|
// `auto` instead of collapsing it to the level it resolved to this turn.
|
|
9352
9447
|
const currentThinkingLevel = this.configuredThinkingLevel();
|
|
9353
9448
|
const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
|
|
9354
|
-
|
|
9449
|
+
const candidateSelector = formatModelStringWithRouting(candidate);
|
|
9355
9450
|
this.#setModelWithProviderSessionReset(candidate);
|
|
9356
|
-
this.sessionManager.appendModelChange(
|
|
9357
|
-
this.settings.getStorage()?.recordModelUsage(
|
|
9451
|
+
this.sessionManager.appendModelChange(candidateSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
9452
|
+
this.settings.getStorage()?.recordModelUsage(candidateSelector);
|
|
9358
9453
|
this.setThinkingLevel(nextThinkingLevel);
|
|
9359
9454
|
if (!this.#activeRetryFallback) {
|
|
9360
9455
|
this.#activeRetryFallback = {
|
|
@@ -9382,7 +9477,8 @@ export class AgentSession {
|
|
|
9382
9477
|
|
|
9383
9478
|
for (const selector of this.#findRetryFallbackCandidates(role, currentSelector)) {
|
|
9384
9479
|
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
9385
|
-
const
|
|
9480
|
+
const resolved = resolveModelOverride([selector.raw], this.#modelRegistry, this.settings);
|
|
9481
|
+
const candidate = resolved.model ?? this.#modelRegistry.find(selector.provider, selector.id);
|
|
9386
9482
|
if (!candidate) continue;
|
|
9387
9483
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
9388
9484
|
if (!apiKey) continue;
|
|
@@ -9420,7 +9516,9 @@ export class AgentSession {
|
|
|
9420
9516
|
}
|
|
9421
9517
|
if (this.#isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
9422
9518
|
|
|
9423
|
-
const
|
|
9519
|
+
const resolvedPrimary = resolveModelOverride([originalSelector.raw], this.#modelRegistry, this.settings);
|
|
9520
|
+
const primaryModel =
|
|
9521
|
+
resolvedPrimary.model ?? this.#modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
9424
9522
|
if (!primaryModel) return;
|
|
9425
9523
|
const apiKey = await this.#modelRegistry.getApiKey(primaryModel, this.sessionId);
|
|
9426
9524
|
if (!apiKey) return;
|
|
@@ -9428,9 +9526,10 @@ export class AgentSession {
|
|
|
9428
9526
|
const currentThinkingLevel = this.configuredThinkingLevel();
|
|
9429
9527
|
const thinkingToApply =
|
|
9430
9528
|
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
9529
|
+
const primarySelector = formatModelStringWithRouting(primaryModel);
|
|
9431
9530
|
this.#setModelWithProviderSessionReset(primaryModel);
|
|
9432
|
-
this.sessionManager.appendModelChange(
|
|
9433
|
-
this.settings.getStorage()?.recordModelUsage(
|
|
9531
|
+
this.sessionManager.appendModelChange(primarySelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
9532
|
+
this.settings.getStorage()?.recordModelUsage(primarySelector);
|
|
9434
9533
|
this.setThinkingLevel(thinkingToApply);
|
|
9435
9534
|
this.#clearActiveRetryFallback();
|
|
9436
9535
|
}
|
|
@@ -9490,7 +9589,10 @@ export class AgentSession {
|
|
|
9490
9589
|
* Handle retryable errors with exponential backoff.
|
|
9491
9590
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
9492
9591
|
*/
|
|
9493
|
-
async #handleRetryableError(
|
|
9592
|
+
async #handleRetryableError(
|
|
9593
|
+
message: AssistantMessage,
|
|
9594
|
+
options?: { allowModelFallback?: boolean },
|
|
9595
|
+
): Promise<boolean> {
|
|
9494
9596
|
const retrySettings = this.settings.getGroup("retry");
|
|
9495
9597
|
if (!retrySettings.enabled) return false;
|
|
9496
9598
|
const classifierRefusal = this.#isClassifierRefusal(message);
|
|
@@ -9578,9 +9680,10 @@ export class AgentSession {
|
|
|
9578
9680
|
}
|
|
9579
9681
|
}
|
|
9580
9682
|
|
|
9683
|
+
const allowModelFallback = options?.allowModelFallback !== false;
|
|
9581
9684
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
9582
9685
|
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
9583
|
-
if (retrySettings.modelFallback) {
|
|
9686
|
+
if (allowModelFallback && retrySettings.modelFallback) {
|
|
9584
9687
|
if (!classifierRefusal) {
|
|
9585
9688
|
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
9586
9689
|
}
|
package/src/session/messages.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function shouldRenderAbortReason(errorMessage: string | undefined): boole
|
|
|
94
94
|
|
|
95
95
|
/** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
|
|
96
96
|
* reason (bare `abort()`). Renderers treat it as "no specific reason given". */
|
|
97
|
-
const GENERIC_ABORT_SENTINEL = "Request was aborted";
|
|
97
|
+
export const GENERIC_ABORT_SENTINEL = "Request was aborted";
|
|
98
98
|
|
|
99
99
|
/** Resolve the operator-facing label for an aborted assistant turn. A custom
|
|
100
100
|
* abort reason threaded onto `errorMessage` is returned verbatim; aborts with
|
package/src/system-prompt.ts
CHANGED
|
@@ -615,7 +615,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
615
615
|
resolvedCustomPrompt,
|
|
616
616
|
resolvedAppendPrompt,
|
|
617
617
|
]);
|
|
618
|
-
const
|
|
618
|
+
const contextPromptSources = contextFiles.map(file => file.content);
|
|
619
|
+
const promptSources = [
|
|
620
|
+
effectiveSystemPromptCustomization,
|
|
621
|
+
resolvedCustomPrompt,
|
|
622
|
+
resolvedAppendPrompt,
|
|
623
|
+
...contextPromptSources,
|
|
624
|
+
];
|
|
619
625
|
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
620
626
|
|
|
621
627
|
const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
|
package/src/task/executor.ts
CHANGED
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import type { Api, Model, Usage } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import type { Rule } from "../capability/rule";
|
|
13
13
|
import { ModelRegistry } from "../config/model-registry";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
formatModelSelectorValue,
|
|
16
|
+
formatModelStringWithRouting,
|
|
17
|
+
resolveModelOverride,
|
|
18
|
+
resolveModelOverrideWithAuthFallback,
|
|
19
|
+
} from "../config/model-resolver";
|
|
15
20
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
16
21
|
import { Settings } from "../config/settings";
|
|
17
22
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
@@ -120,6 +125,74 @@ function normalizeModelPatterns(value: string | string[] | undefined): string[]
|
|
|
120
125
|
.filter(Boolean);
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
const SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX = "subagent:";
|
|
129
|
+
|
|
130
|
+
interface SubagentRetryFallbackCandidate {
|
|
131
|
+
model: Model<Api>;
|
|
132
|
+
selector: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveSubagentRetryFallbackCandidates(
|
|
136
|
+
modelPatterns: string[],
|
|
137
|
+
modelRegistry: ModelRegistry,
|
|
138
|
+
settings: Settings,
|
|
139
|
+
): SubagentRetryFallbackCandidate[] {
|
|
140
|
+
const candidates: SubagentRetryFallbackCandidate[] = [];
|
|
141
|
+
const seen = new Set<string>();
|
|
142
|
+
for (const pattern of modelPatterns) {
|
|
143
|
+
const resolved = resolveModelOverride([pattern], modelRegistry, settings);
|
|
144
|
+
if (!resolved.model) continue;
|
|
145
|
+
const selector = resolved.explicitThinkingLevel
|
|
146
|
+
? formatModelSelectorValue(formatModelStringWithRouting(resolved.model), resolved.thinkingLevel)
|
|
147
|
+
: formatModelStringWithRouting(resolved.model);
|
|
148
|
+
if (seen.has(selector)) continue;
|
|
149
|
+
seen.add(selector);
|
|
150
|
+
candidates.push({ model: resolved.model, selector });
|
|
151
|
+
}
|
|
152
|
+
return candidates;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function installSubagentRetryFallbackChain(args: {
|
|
156
|
+
settings: Settings;
|
|
157
|
+
id: string;
|
|
158
|
+
candidates: SubagentRetryFallbackCandidate[];
|
|
159
|
+
model: Model<Api> | undefined;
|
|
160
|
+
authFallbackUsed: boolean;
|
|
161
|
+
}): string | undefined {
|
|
162
|
+
const { settings, id, candidates, model, authFallbackUsed } = args;
|
|
163
|
+
if (!model || authFallbackUsed || candidates.length <= 1) return undefined;
|
|
164
|
+
|
|
165
|
+
const selectedIndex = candidates.findIndex(
|
|
166
|
+
candidate => candidate.model.provider === model.provider && candidate.model.id === model.id,
|
|
167
|
+
);
|
|
168
|
+
if (selectedIndex < 0) return undefined;
|
|
169
|
+
const fallbackSelectors = candidates.slice(selectedIndex + 1).map(candidate => candidate.selector);
|
|
170
|
+
if (fallbackSelectors.length === 0) return undefined;
|
|
171
|
+
|
|
172
|
+
const role = `${SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX}${id}`;
|
|
173
|
+
const modelRoles: Record<string, string> = {};
|
|
174
|
+
const existingRoles = settings.getModelRoles();
|
|
175
|
+
for (const existingRole in existingRoles) {
|
|
176
|
+
const selector = existingRoles[existingRole];
|
|
177
|
+
if (selector) {
|
|
178
|
+
modelRoles[existingRole] = selector;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
modelRoles[role] = candidates[selectedIndex].selector;
|
|
182
|
+
settings.override("modelRoles", modelRoles);
|
|
183
|
+
const fallbackChains: Record<string, string[]> = {
|
|
184
|
+
[role]: fallbackSelectors,
|
|
185
|
+
};
|
|
186
|
+
const existingFallbackChains = settings.get("retry.fallbackChains");
|
|
187
|
+
for (const existingRole in existingFallbackChains) {
|
|
188
|
+
if (existingRole !== role) {
|
|
189
|
+
fallbackChains[existingRole] = existingFallbackChains[existingRole];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
settings.override("retry.fallbackChains", fallbackChains);
|
|
193
|
+
return role;
|
|
194
|
+
}
|
|
195
|
+
|
|
123
196
|
function renderIrcPeerRoster(selfId: string): string {
|
|
124
197
|
const peers = AgentRegistry.global()
|
|
125
198
|
.list()
|
|
@@ -1222,6 +1295,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1222
1295
|
popLoopPhase();
|
|
1223
1296
|
}
|
|
1224
1297
|
}
|
|
1298
|
+
if (event.type === "retry_fallback_applied") {
|
|
1299
|
+
progress.resolvedModel = event.to;
|
|
1300
|
+
scheduleProgress(true);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (event.type === "retry_fallback_succeeded") {
|
|
1304
|
+
progress.resolvedModel = event.model;
|
|
1305
|
+
scheduleProgress(true);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1225
1308
|
});
|
|
1226
1309
|
|
|
1227
1310
|
const captureSalvage = (session: AgentSession): void => {
|
|
@@ -1817,13 +1900,26 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1817
1900
|
resolvedModel: model.id,
|
|
1818
1901
|
});
|
|
1819
1902
|
}
|
|
1903
|
+
const retryFallbackRole = installSubagentRetryFallbackChain({
|
|
1904
|
+
settings: subagentSettings,
|
|
1905
|
+
id,
|
|
1906
|
+
candidates: resolveSubagentRetryFallbackCandidates(modelPatterns, modelRegistry, settings),
|
|
1907
|
+
model,
|
|
1908
|
+
authFallbackUsed,
|
|
1909
|
+
});
|
|
1910
|
+
if (retryFallbackRole) {
|
|
1911
|
+
logger.debug("Configured subagent runtime model fallback chain", {
|
|
1912
|
+
role: retryFallbackRole,
|
|
1913
|
+
requested: modelPatterns,
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1820
1916
|
if (model?.contextWindow && model.contextWindow > 0) {
|
|
1821
1917
|
progress.contextWindow = model.contextWindow;
|
|
1822
1918
|
}
|
|
1823
1919
|
if (model) {
|
|
1824
1920
|
progress.resolvedModel = explicitThinkingLevel
|
|
1825
|
-
?
|
|
1826
|
-
:
|
|
1921
|
+
? formatModelSelectorValue(formatModelStringWithRouting(model), resolvedThinkingLevel)
|
|
1922
|
+
: formatModelStringWithRouting(model);
|
|
1827
1923
|
}
|
|
1828
1924
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
1829
1925
|
? resolvedThinkingLevel
|
|
@@ -66,6 +66,7 @@ const EXTENSION_LANG: Record<string, readonly [string, string]> = {
|
|
|
66
66
|
cljc: ["clojure", "clojure"],
|
|
67
67
|
cljs: ["clojure", "clojure"],
|
|
68
68
|
edn: ["clojure", "clojure"],
|
|
69
|
+
el: ["emacs-lisp", "emacs-lisp"],
|
|
69
70
|
|
|
70
71
|
// .NET
|
|
71
72
|
cs: ["csharp", "csharp"],
|
|
@@ -209,6 +210,7 @@ export function getLanguageFromPath(filePath: string): string | undefined {
|
|
|
209
210
|
if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
|
|
210
211
|
return "dockerfile";
|
|
211
212
|
}
|
|
213
|
+
if (baseName === ".emacs") return "emacs-lisp";
|
|
212
214
|
if (baseName === "justfile") return "just";
|
|
213
215
|
if (baseName === "cmakelists.txt") return "cmake";
|
|
214
216
|
|
|
@@ -223,6 +225,9 @@ export function detectLanguageId(filePath: string): string {
|
|
|
223
225
|
if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
|
|
224
226
|
return "dockerfile";
|
|
225
227
|
}
|
|
228
|
+
if (baseName === ".emacs") {
|
|
229
|
+
return "emacs-lisp";
|
|
230
|
+
}
|
|
226
231
|
if (baseName === "makefile" || baseName === "gnumakefile") {
|
|
227
232
|
return "makefile";
|
|
228
233
|
}
|
package/src/utils/markit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Markit, StreamInfo } from "markit-ai";
|
|
3
3
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
4
4
|
|
|
@@ -8,6 +8,29 @@ export interface MarkitConversionResult {
|
|
|
8
8
|
error?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface MuPdfWasmModuleConfig {
|
|
12
|
+
print?: (...values: unknown[]) => void;
|
|
13
|
+
printErr?: (...values: unknown[]) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
var $libmupdf_wasm_Module: MuPdfWasmModuleConfig | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function logMuPdfWasmOutput(stream: "stdout" | "stderr", values: unknown[]): void {
|
|
21
|
+
const message = values.length === 1 && typeof values[0] === "string" ? values[0] : values.map(String).join(" ");
|
|
22
|
+
logger.debug("mupdf wasm output", { stream, message });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function installMuPdfWasmLogger(): void {
|
|
26
|
+
const moduleConfig = globalThis.$libmupdf_wasm_Module ?? {};
|
|
27
|
+
moduleConfig.print = (...values) => logMuPdfWasmOutput("stdout", values);
|
|
28
|
+
moduleConfig.printErr = (...values) => logMuPdfWasmOutput("stderr", values);
|
|
29
|
+
globalThis.$libmupdf_wasm_Module = moduleConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
installMuPdfWasmLogger();
|
|
33
|
+
|
|
11
34
|
let markit: () => Markit | Promise<Markit> = async () => {
|
|
12
35
|
const promise = import("markit-ai").then(({ Markit }) => {
|
|
13
36
|
const instance = new Markit();
|