@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.12
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 +49 -0
- package/dist/cli.js +3119 -3028
- package/dist/types/advisor/runtime.d.ts +11 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/discovery/helpers.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
- package/dist/types/modes/components/model-selector.d.ts +2 -1
- package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/modes/utils/context-usage.d.ts +0 -12
- package/dist/types/modes/workflow.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/bash-interactive.d.ts +1 -1
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +2 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +24 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/utils/local-date.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +145 -0
- package/src/advisor/runtime.ts +19 -0
- package/src/config/api-key-resolver.ts +7 -2
- package/src/config/model-registry.ts +9 -0
- package/src/config/settings-schema.ts +11 -1
- package/src/config/settings.ts +11 -0
- package/src/discovery/builtin.ts +2 -1
- package/src/discovery/claude-plugins.ts +167 -46
- package/src/discovery/helpers.ts +16 -1
- package/src/edit/renderer.ts +20 -6
- package/src/eval/js/worker-core.ts +163 -6
- package/src/exec/bash-executor.ts +14 -9
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/registry-helpers.ts +9 -6
- package/src/modes/components/model-selector.ts +30 -6
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line/component.ts +14 -2
- package/src/modes/controllers/command-controller.ts +13 -23
- package/src/modes/controllers/event-controller.ts +12 -12
- package/src/modes/controllers/extension-ui-controller.ts +6 -35
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/mcp-command-controller.ts +10 -9
- package/src/modes/controllers/selector-controller.ts +16 -5
- package/src/modes/github-ref-autocomplete.ts +75 -0
- package/src/modes/interactive-mode.ts +54 -10
- package/src/modes/prompt-action-autocomplete.ts +35 -0
- package/src/modes/utils/context-usage.ts +58 -5
- package/src/modes/utils/hotkeys-markdown.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/modes/workflow.ts +14 -8
- package/src/prompts/system/plan-mode-active.md +5 -2
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/prompts/system/workflow-notice.md +69 -50
- package/src/prompts/tools/bash.md +18 -7
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/read.md +4 -3
- package/src/sdk.ts +11 -0
- package/src/session/agent-session.ts +128 -14
- package/src/system-prompt.ts +3 -2
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +39 -7
- package/src/tools/bash.ts +69 -39
- package/src/tools/browser/launch.ts +31 -4
- package/src/tools/grep.ts +53 -14
- package/src/tools/index.ts +11 -0
- package/src/tools/path-utils.ts +46 -1
- package/src/tools/read.ts +108 -50
- package/src/utils/local-date.ts +7 -0
- package/src/utils/open.ts +36 -10
|
@@ -45,6 +45,17 @@ export interface AdvisorRuntimeHost {
|
|
|
45
45
|
* one that routes `advise()` results back to the primary.
|
|
46
46
|
*/
|
|
47
47
|
beginAdvisorUpdate?(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Called with the error of every failed advisor turn, before the retry sleep
|
|
50
|
+
* or the dropped-after-3 path. Lets the host apply credential-level remedies
|
|
51
|
+
* the advisor loop lacks: the in-stream a/b/c auth retry rotates through
|
|
52
|
+
* sibling credentials within one request but never blocks the LAST failing
|
|
53
|
+
* one — the primary agent's retry pipeline does that via
|
|
54
|
+
* `markUsageLimitReached`, so without this hook the advisor re-picks the
|
|
55
|
+
* same usage-limited account on every retry. Errors thrown here are logged
|
|
56
|
+
* and swallowed.
|
|
57
|
+
*/
|
|
58
|
+
onTurnError?(error: unknown): Promise<void> | void;
|
|
48
59
|
/** Surface a non-recovering advisor failure to the host UI without adding model-visible context. */
|
|
49
60
|
notifyFailure?(error: unknown): void;
|
|
50
61
|
}
|
|
@@ -189,6 +189,10 @@ export declare class ModelRegistry {
|
|
|
189
189
|
* Check if a model selector is currently suppressed due to rate limits.
|
|
190
190
|
*/
|
|
191
191
|
isSelectorSuppressed(selector: string): boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Clear the cooldown suppression for one selector after an explicit user selection.
|
|
194
|
+
*/
|
|
195
|
+
clearSuppressedSelector(selector: string): void;
|
|
192
196
|
/**
|
|
193
197
|
* Clear all cooldown suppressions recorded via {@link suppressSelector}.
|
|
194
198
|
* Used to reset retry-fallback cooldown state without a full {@link refresh}.
|
|
@@ -1380,6 +1380,12 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
1380
1380
|
readonly "retry.fallbackChains": {
|
|
1381
1381
|
readonly type: "record";
|
|
1382
1382
|
readonly default: Record<string, string[]>;
|
|
1383
|
+
readonly ui: {
|
|
1384
|
+
readonly tab: "model";
|
|
1385
|
+
readonly group: "Retry & Fallback";
|
|
1386
|
+
readonly label: "Retry Fallback Chains";
|
|
1387
|
+
readonly description: 'JSON object mapping model roles to ordered fallback model selectors, e.g. {"default":["openai/gpt-4o-mini"]}.';
|
|
1388
|
+
};
|
|
1383
1389
|
};
|
|
1384
1390
|
readonly "retry.fallbackRevertPolicy": {
|
|
1385
1391
|
readonly type: "enum";
|
|
@@ -152,6 +152,8 @@ export declare class Settings {
|
|
|
152
152
|
* can register independently without overwriting each other.
|
|
153
153
|
*/
|
|
154
154
|
export declare const onAppendOnlyModeChanged: (cb: (value: string) => void) => () => void;
|
|
155
|
+
/** Subscribe to model role changes. Returns an unsubscribe function. */
|
|
156
|
+
export declare const onModelRolesChanged: (cb: () => void) => () => void;
|
|
155
157
|
/**
|
|
156
158
|
* Subscribe to session-accent setting changes.
|
|
157
159
|
* Returns an unsubscribe function. Callers should re-read settings in the callback.
|
|
@@ -122,6 +122,15 @@ export interface ScanSkillsFromDirOptions {
|
|
|
122
122
|
providerId: string;
|
|
123
123
|
level: "user" | "project";
|
|
124
124
|
requireDescription?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* When true, treat a `SKILL.md` sitting directly under `dir` as a single skill in addition to
|
|
127
|
+
* scanning `<dir>/<name>/SKILL.md` children. Matches the Claude plugin manifest convention
|
|
128
|
+
* that lets a skill path point at a directory containing `SKILL.md` directly (e.g.
|
|
129
|
+
* `"skills": ["./"]`), where the frontmatter `name` determines the invocation name and the
|
|
130
|
+
* directory basename is the fallback. Default `false` preserves the strict child-scan
|
|
131
|
+
* semantic every non-Claude provider relies on.
|
|
132
|
+
*/
|
|
133
|
+
includeSelf?: boolean;
|
|
125
134
|
}
|
|
126
135
|
export declare function compareSkillOrder(aName: string, aPath: string, bName: string, bPath: string): number;
|
|
127
136
|
export declare function scanSkillsFromDir(_ctx: LoadContext, options: ScanSkillsFromDirOptions): Promise<LoadResult<Skill>>;
|
|
@@ -2,6 +2,7 @@ import { type MinimizerOptions } from "@oh-my-pi/pi-natives";
|
|
|
2
2
|
import { type ShellMinimizerSettings } from "../config/settings.js";
|
|
3
3
|
export interface BashExecutorOptions {
|
|
4
4
|
cwd?: string;
|
|
5
|
+
/** Milliseconds before aborting the command; 0 disables the executor deadline. */
|
|
5
6
|
timeout?: number;
|
|
6
7
|
onChunk?: (chunk: string) => void;
|
|
7
8
|
chunkThrottleMs?: number;
|
|
@@ -27,7 +27,7 @@ export interface SessionStartEvent {
|
|
|
27
27
|
export interface SessionBeforeSwitchEvent {
|
|
28
28
|
type: "session_before_switch";
|
|
29
29
|
/** Reason for the switch */
|
|
30
|
-
reason: "new" | "resume" | "fork";
|
|
30
|
+
reason: "new" | "resume" | "fork" | "handoff";
|
|
31
31
|
/** Session file we're switching to (only for "resume") */
|
|
32
32
|
targetSessionFile?: string;
|
|
33
33
|
}
|
|
@@ -35,7 +35,7 @@ export interface SessionBeforeSwitchEvent {
|
|
|
35
35
|
export interface SessionSwitchEvent {
|
|
36
36
|
type: "session_switch";
|
|
37
37
|
/** Reason for the switch */
|
|
38
|
-
reason: "new" | "resume" | "fork";
|
|
38
|
+
reason: "new" | "resume" | "fork" | "handoff";
|
|
39
39
|
/** Session file we came from */
|
|
40
40
|
previousSessionFile: string | undefined;
|
|
41
41
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,10 +3,12 @@ export declare function resetRegisteredArtifactDirsForTests(): void;
|
|
|
3
3
|
/**
|
|
4
4
|
* Snapshot of artifacts dirs for every registered session, deduped.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Collects TWO candidate dirs per ref, because a subagent reads from its
|
|
7
|
+
* adopted (root-wide) `ArtifactManager.dir` but its own children are written
|
|
8
|
+
* one level deeper, under `sessionFile.slice(0, -6)` (`task/index.ts`). A
|
|
9
|
+
* depth-2+ subagent's output therefore lives in the write-time dir, not the
|
|
10
|
+
* adopted one, so `agent://` must scan both or it 404s a live nested peer.
|
|
11
|
+
* `addDir` dedup collapses the depth-0 case (both formulas agree) back to a
|
|
12
|
+
* single entry.
|
|
11
13
|
*/
|
|
12
14
|
export declare function artifactsDirsFromRegistry(): string[];
|
|
@@ -7,7 +7,8 @@ interface ScopedModelItem {
|
|
|
7
7
|
model: Model;
|
|
8
8
|
thinkingLevel?: string;
|
|
9
9
|
}
|
|
10
|
-
type
|
|
10
|
+
type ModelSelectorAction = "modelRole" | "retryFallback";
|
|
11
|
+
type RoleSelectCallback = (model: Model, role: string | null, thinkingLevel?: ConfiguredThinkingLevel, selector?: string, action?: ModelSelectorAction) => void;
|
|
11
12
|
/**
|
|
12
13
|
* Component that renders a model selector with provider tabs and context menu.
|
|
13
14
|
* - Tab/Arrow Left/Right: Switch between provider tabs
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autocomplete for GitHub issue/PR references typed as `#<number>` (e.g. `#3164`).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the `@` file-reference and `scheme://` internal-url conventions: the
|
|
5
|
+
* token is rewritten to an internal URL (`pr://3164` or `issue://3164`) plus a
|
|
6
|
+
* trailing space, and the existing tool-mediated pipeline (the `read` tool →
|
|
7
|
+
* InternalUrlRouter → `gh`) resolves it from the session cwd's git remote.
|
|
8
|
+
*
|
|
9
|
+
* No network at suggestion time — candidates are generated locally. GitHub
|
|
10
|
+
* shares the issue/PR number space and there is no cheap way to tell which a
|
|
11
|
+
* given number is while typing, so both a PR and an Issue candidate are offered
|
|
12
|
+
* by default. Naming the type first (`pr #3164` / `issue #3164`) constrains the
|
|
13
|
+
* candidates to that kind. Anything that is not a standalone `#<number>` token
|
|
14
|
+
* keeps falling through to the existing prompt-action menu.
|
|
15
|
+
*/
|
|
16
|
+
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
|
|
17
|
+
export interface GithubRefContext {
|
|
18
|
+
/** Text to replace on accept: `#3164`, or `pr #3164` when a qualifier precedes it. */
|
|
19
|
+
prefix: string;
|
|
20
|
+
/** Type the user named (`pr`/`pull` → `pr`, `issue` → `issue`), or null to offer both. */
|
|
21
|
+
qualifier: "pr" | "issue" | null;
|
|
22
|
+
/** The numeric reference, e.g. `3164`. */
|
|
23
|
+
number: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function getGithubRefContext(textBeforeCursor: string): GithubRefContext | null;
|
|
26
|
+
/**
|
|
27
|
+
* Suggestions for a `#<number>` token. Both kinds are offered unless the user
|
|
28
|
+
* named a type (`pr #3164` / `issue #3164`), in which case only that kind is
|
|
29
|
+
* offered. Returns `null` when the text before the cursor is not a standalone
|
|
30
|
+
* `#<number>` token.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getGithubRefSuggestions(textBeforeCursor: string): {
|
|
33
|
+
items: AutocompleteItem[];
|
|
34
|
+
prefix: string;
|
|
35
|
+
} | null;
|
|
@@ -24,18 +24,6 @@ export interface ContextBreakdown {
|
|
|
24
24
|
}
|
|
25
25
|
export declare function estimateSkillsTokens(skills: readonly Skill[]): number;
|
|
26
26
|
export declare function estimateToolSchemaTokens(tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>): number;
|
|
27
|
-
/**
|
|
28
|
-
* Compute just the NON-MESSAGE token total: system prompt (with its skills
|
|
29
|
-
* section subtracted, since skills are tokenized separately) + system context
|
|
30
|
-
* (the rest of the system-prompt array) + tools + skills.
|
|
31
|
-
*
|
|
32
|
-
* Exposed so callers like `StatusLineComponent` can cache the non-message
|
|
33
|
-
* total separately from the message total. Non-message inputs (skills,
|
|
34
|
-
* tools, system prompt) change rarely; the message list grows on every
|
|
35
|
-
* streaming turn. Splitting the two lets the caller refresh each on its own
|
|
36
|
-
* cadence — non-message recomputed only when the inputs identity changes,
|
|
37
|
-
* messages walked incrementally as new entries append.
|
|
38
|
-
*/
|
|
39
27
|
export declare function computeNonMessageTokens(session: AgentSession): number;
|
|
40
28
|
/**
|
|
41
29
|
* Shared helper for the four non-message token totals used by
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type KeywordHighlighter } from "./gradient-highlight.js";
|
|
2
|
-
/**
|
|
2
|
+
/** WORKFLOW_NOTICE is the default hidden notice for sessions with batched task calls enabled. */
|
|
3
3
|
export declare const WORKFLOW_NOTICE: string;
|
|
4
|
+
/** renderWorkflowNotice renders the workflow notice for the active task schema. */
|
|
5
|
+
export declare function renderWorkflowNotice({ taskBatch }: {
|
|
6
|
+
taskBatch: boolean;
|
|
7
|
+
}): string;
|
|
4
8
|
/**
|
|
5
9
|
* Whether `text` contains the standalone keyword "workflowz"
|
|
6
10
|
* (lowercase, whitespace-delimited) in prose — never inside a code block, inline
|
|
@@ -184,6 +184,8 @@ export interface AgentSessionConfig {
|
|
|
184
184
|
toolRegistry?: Map<string, AgentTool>;
|
|
185
185
|
/** Tool names whose current registry entry is still the built-in implementation. */
|
|
186
186
|
builtInToolNames?: Iterable<string>;
|
|
187
|
+
/** Update tool-session predicates that render guidance from the live active tool set. */
|
|
188
|
+
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
187
189
|
/** Current session pre-LLM message transform pipeline */
|
|
188
190
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
189
191
|
/**
|
|
@@ -224,6 +226,8 @@ export interface AgentSessionConfig {
|
|
|
224
226
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{
|
|
225
227
|
systemPrompt: string[];
|
|
226
228
|
}>;
|
|
229
|
+
/** Local calendar date provider used by prompt-cache invalidation. Defaults to the host local date. */
|
|
230
|
+
getLocalCalendarDate?: () => string;
|
|
227
231
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
228
232
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
229
233
|
requestedToolNames?: ReadonlySet<string>;
|
|
@@ -366,6 +370,7 @@ export interface HandoffResult {
|
|
|
366
370
|
export interface SessionHandoffOptions {
|
|
367
371
|
autoTriggered?: boolean;
|
|
368
372
|
signal?: AbortSignal;
|
|
373
|
+
onSwitchCancelled?: () => void;
|
|
369
374
|
}
|
|
370
375
|
/** Result from cycleModel() */
|
|
371
376
|
export interface ModelCycleResult {
|
|
@@ -35,7 +35,7 @@ export declare function loadProjectContextFiles(options?: LoadContextFilesOption
|
|
|
35
35
|
* Project-level SYSTEM.md overrides user-level SYSTEM.md.
|
|
36
36
|
*/
|
|
37
37
|
export declare function loadSystemPromptFiles(options?: LoadContextFilesOptions): Promise<string | null>;
|
|
38
|
-
export declare const DEFAULT_SYSTEM_PROMPT_TOOL_NAMES: readonly ["read", "bash", "
|
|
38
|
+
export declare const DEFAULT_SYSTEM_PROMPT_TOOL_NAMES: readonly ["read", "bash", "edit", "write"];
|
|
39
39
|
export interface SystemPromptToolMetadata {
|
|
40
40
|
label: string;
|
|
41
41
|
description: string;
|
|
@@ -8,7 +8,7 @@ export interface BashInteractiveResult extends OutputSummary {
|
|
|
8
8
|
export declare function runInteractiveBashPty(ui: NonNullable<AgentToolContext["ui"]>, options: {
|
|
9
9
|
command: string;
|
|
10
10
|
cwd: string;
|
|
11
|
-
timeoutMs
|
|
11
|
+
timeoutMs?: number;
|
|
12
12
|
signal?: AbortSignal;
|
|
13
13
|
env?: Record<string, string>;
|
|
14
14
|
artifactPath?: string;
|
|
@@ -25,6 +25,7 @@ export declare function resolveSkillUrlToPath(url: string, skills: readonly Skil
|
|
|
25
25
|
export declare function expandSkillUrls(command: string, skills: readonly Skill[]): string;
|
|
26
26
|
/**
|
|
27
27
|
* Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
|
|
28
|
+
* Unresolvable URLs and literal mentions inside larger quoted text are left unchanged.
|
|
28
29
|
* Supported schemes: skill://, agent://, artifact://, memory://, rule://, local://
|
|
29
30
|
*/
|
|
30
31
|
export declare function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string>;
|
|
@@ -72,6 +72,7 @@ export interface BashToolDetails {
|
|
|
72
72
|
meta?: OutputMeta;
|
|
73
73
|
timeoutSeconds?: number;
|
|
74
74
|
requestedTimeoutSeconds?: number;
|
|
75
|
+
timeoutDisabled?: boolean;
|
|
75
76
|
wallTimeMs?: number;
|
|
76
77
|
/** Exit code of a command that ran to completion but failed (non-zero). */
|
|
77
78
|
exitCode?: number;
|
|
@@ -97,7 +98,7 @@ export declare class BashTool implements AgentTool<typeof bashSchemaBase | typeo
|
|
|
97
98
|
readonly formatApprovalDetails: (args: unknown) => string[];
|
|
98
99
|
readonly label = "Bash";
|
|
99
100
|
readonly loadMode = "essential";
|
|
100
|
-
|
|
101
|
+
get description(): string;
|
|
101
102
|
readonly parameters: BashToolSchema;
|
|
102
103
|
readonly concurrency: (args: Partial<BashToolInput>) => "shared" | "exclusive";
|
|
103
104
|
readonly strict = true;
|
|
@@ -60,6 +60,7 @@ export declare function applyStealthPatches(browser: Browser, page: Page, state:
|
|
|
60
60
|
browserSession: CDPSession | null;
|
|
61
61
|
override: UserAgentOverride | null;
|
|
62
62
|
}): Promise<void>;
|
|
63
|
+
export declare function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[];
|
|
63
64
|
export declare function targetSupportsUserAgentOverrideForTest(target: Target): boolean;
|
|
64
65
|
export declare function configureUserAgentTargetsForTest(browser: Browser, state: {
|
|
65
66
|
browserSession: CDPSession | null;
|
|
@@ -8,6 +8,7 @@ import type { OutputMeta } from "./output-meta.js";
|
|
|
8
8
|
declare const searchSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
9
9
|
pattern: string;
|
|
10
10
|
path?: string | undefined;
|
|
11
|
+
selector?: string | undefined;
|
|
11
12
|
case?: boolean | undefined;
|
|
12
13
|
gitignore?: boolean | undefined;
|
|
13
14
|
skip?: number | null | undefined;
|
|
@@ -66,6 +67,7 @@ export declare class GrepTool implements AgentTool<typeof searchSchema, GrepTool
|
|
|
66
67
|
readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
67
68
|
pattern: string;
|
|
68
69
|
path?: string | undefined;
|
|
70
|
+
selector?: string | undefined;
|
|
69
71
|
case?: boolean | undefined;
|
|
70
72
|
gitignore?: boolean | undefined;
|
|
71
73
|
skip?: number | null | undefined;
|
|
@@ -172,6 +172,10 @@ export interface ToolSession {
|
|
|
172
172
|
getAgentId?: () => string | null;
|
|
173
173
|
/** Look up a registered tool by name (used by the eval js backend's tool bridge). */
|
|
174
174
|
getToolByName?: (name: string) => AgentTool | undefined;
|
|
175
|
+
/** Return whether a built-in tool is active in this turn's tool set. */
|
|
176
|
+
isToolActive?: (name: string) => boolean;
|
|
177
|
+
/** Update the active built-in tool predicate when a session changes tools mid-run. */
|
|
178
|
+
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
175
179
|
/** Agent registry for IRC routing across live sessions. */
|
|
176
180
|
agentRegistry?: AgentRegistry;
|
|
177
181
|
/** Get artifacts directory for artifact:// URLs */
|
|
@@ -36,6 +36,30 @@ export declare function splitPathAndSel(rawPath: string): {
|
|
|
36
36
|
path: string;
|
|
37
37
|
sel?: string;
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Three-way probe for whether the exact filesystem entry named by `filePath`
|
|
41
|
+
* exists. `stat` (used earlier) failed for reasons other than "no such file"
|
|
42
|
+
* (dangling symlink, `EACCES` on a parent, transient I/O), and each of those
|
|
43
|
+
* silently reinterpreted a real literal path such as `test:1-2` as `test`
|
|
44
|
+
* plus selector `1-2` (issue #4618). `lstat` inspects the entry itself, so a
|
|
45
|
+
* dangling symlink is still detected as present; ambiguous errors resolve to
|
|
46
|
+
* `"unknown"` so callers keep the raw path instead of guessing.
|
|
47
|
+
*/
|
|
48
|
+
export declare function probeLiteralPathExists(filePath: string, cwd: string): Promise<"exists" | "missing" | "unknown">;
|
|
49
|
+
/**
|
|
50
|
+
* Async sibling of {@link splitPathAndSel} that prefers a literal filesystem
|
|
51
|
+
* path over selector interpretation. Filenames whose tail matches the selector
|
|
52
|
+
* grammar (e.g. `test:1-2`, `log:raw`) are legal on POSIX; without this the
|
|
53
|
+
* strict splitter peels the tail and both `read` and `grep` refuse to open the
|
|
54
|
+
* real file (issue #4618). The literal wins on a confirmed `lstat`, and also
|
|
55
|
+
* on `"unknown"` (`EACCES` on a parent, transient I/O), so an unreachable
|
|
56
|
+
* literal is never silently reinterpreted as `path + selector`. Only a
|
|
57
|
+
* definitive `ENOENT`/`ENOTDIR` falls back to the strict split.
|
|
58
|
+
*/
|
|
59
|
+
export declare function splitPathAndSelPreferringLiteral(rawPath: string, cwd: string): Promise<{
|
|
60
|
+
path: string;
|
|
61
|
+
sel?: string;
|
|
62
|
+
}>;
|
|
39
63
|
/**
|
|
40
64
|
* Variant of {@link splitPathAndSel} for internal URLs (`scheme://...`).
|
|
41
65
|
*
|
|
@@ -7,6 +7,7 @@ import { type TruncationResult } from "../session/streaming-output.js";
|
|
|
7
7
|
import { type OutputMeta } from "./output-meta.js";
|
|
8
8
|
declare const readSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
9
9
|
path: string;
|
|
10
|
+
selector?: string | undefined;
|
|
10
11
|
}, {}>;
|
|
11
12
|
export type ReadToolInput = typeof readSchema.infer;
|
|
12
13
|
export interface ReadToolDetails {
|
|
@@ -59,6 +60,7 @@ export declare class ReadTool implements AgentTool<typeof readSchema, ReadToolDe
|
|
|
59
60
|
readonly description: string;
|
|
60
61
|
readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
61
62
|
path: string;
|
|
63
|
+
selector?: string | undefined;
|
|
62
64
|
}, {}>;
|
|
63
65
|
readonly strict = true;
|
|
64
66
|
constructor(session: ToolSession);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.3.
|
|
4
|
+
"version": "16.3.12",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -56,17 +56,17 @@
|
|
|
56
56
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
57
57
|
"@babel/parser": "^7.29.7",
|
|
58
58
|
"@mozilla/readability": "^0.6.0",
|
|
59
|
-
"@oh-my-pi/hashline": "16.3.
|
|
60
|
-
"@oh-my-pi/omp-stats": "16.3.
|
|
61
|
-
"@oh-my-pi/pi-agent-core": "16.3.
|
|
62
|
-
"@oh-my-pi/pi-ai": "16.3.
|
|
63
|
-
"@oh-my-pi/pi-catalog": "16.3.
|
|
64
|
-
"@oh-my-pi/pi-mnemopi": "16.3.
|
|
65
|
-
"@oh-my-pi/pi-natives": "16.3.
|
|
66
|
-
"@oh-my-pi/pi-tui": "16.3.
|
|
67
|
-
"@oh-my-pi/pi-utils": "16.3.
|
|
68
|
-
"@oh-my-pi/pi-wire": "16.3.
|
|
69
|
-
"@oh-my-pi/snapcompact": "16.3.
|
|
59
|
+
"@oh-my-pi/hashline": "16.3.12",
|
|
60
|
+
"@oh-my-pi/omp-stats": "16.3.12",
|
|
61
|
+
"@oh-my-pi/pi-agent-core": "16.3.12",
|
|
62
|
+
"@oh-my-pi/pi-ai": "16.3.12",
|
|
63
|
+
"@oh-my-pi/pi-catalog": "16.3.12",
|
|
64
|
+
"@oh-my-pi/pi-mnemopi": "16.3.12",
|
|
65
|
+
"@oh-my-pi/pi-natives": "16.3.12",
|
|
66
|
+
"@oh-my-pi/pi-tui": "16.3.12",
|
|
67
|
+
"@oh-my-pi/pi-utils": "16.3.12",
|
|
68
|
+
"@oh-my-pi/pi-wire": "16.3.12",
|
|
69
|
+
"@oh-my-pi/snapcompact": "16.3.12",
|
|
70
70
|
"@opentelemetry/api": "^1.9.1",
|
|
71
71
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
72
72
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -1294,6 +1294,151 @@ describe("advisor", () => {
|
|
|
1294
1294
|
expect(failures).toHaveLength(2);
|
|
1295
1295
|
});
|
|
1296
1296
|
|
|
1297
|
+
it("calls onTurnError with state.error before retrying the batch", async () => {
|
|
1298
|
+
const promptInputs: string[] = [];
|
|
1299
|
+
const turnErrors: unknown[] = [];
|
|
1300
|
+
const events: string[] = [];
|
|
1301
|
+
const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
|
|
1302
|
+
let promptCalls = 0;
|
|
1303
|
+
const agent: AdvisorAgent = {
|
|
1304
|
+
prompt: async input => {
|
|
1305
|
+
promptCalls++;
|
|
1306
|
+
promptInputs.push(input);
|
|
1307
|
+
events.push(`prompt:${promptCalls}`);
|
|
1308
|
+
state.error = promptCalls === 1 ? "provider failed" : undefined;
|
|
1309
|
+
},
|
|
1310
|
+
abort: () => {},
|
|
1311
|
+
reset: () => {
|
|
1312
|
+
state.error = undefined;
|
|
1313
|
+
},
|
|
1314
|
+
state,
|
|
1315
|
+
};
|
|
1316
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1317
|
+
const host: AdvisorRuntimeHost = {
|
|
1318
|
+
snapshotMessages: () => messages,
|
|
1319
|
+
enqueueAdvice: () => {},
|
|
1320
|
+
onTurnError: error => {
|
|
1321
|
+
turnErrors.push(error);
|
|
1322
|
+
events.push(`hook:${error instanceof Error ? error.message : String(error)}`);
|
|
1323
|
+
},
|
|
1324
|
+
};
|
|
1325
|
+
const runtime = new AdvisorRuntime(agent, host, 1);
|
|
1326
|
+
|
|
1327
|
+
runtime.onTurnEnd(messages);
|
|
1328
|
+
await runtime.waitForCatchup(1000, 1);
|
|
1329
|
+
|
|
1330
|
+
expect(promptInputs).toHaveLength(2);
|
|
1331
|
+
expect(turnErrors).toHaveLength(1);
|
|
1332
|
+
const error = turnErrors[0];
|
|
1333
|
+
if (!(error instanceof Error)) throw new Error("expected advisor turn error");
|
|
1334
|
+
expect(error.message).toBe("provider failed");
|
|
1335
|
+
expect(events).toEqual(["prompt:1", "hook:provider failed", "prompt:2"]);
|
|
1336
|
+
expect(runtime.backlog).toBe(0);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
it("calls onTurnError for each consecutive failure including the dropped third turn", async () => {
|
|
1340
|
+
const promptInputs: string[] = [];
|
|
1341
|
+
const turnErrors: unknown[] = [];
|
|
1342
|
+
const failures: unknown[] = [];
|
|
1343
|
+
const events: string[] = [];
|
|
1344
|
+
const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
|
|
1345
|
+
let promptCalls = 0;
|
|
1346
|
+
const agent: AdvisorAgent = {
|
|
1347
|
+
prompt: async input => {
|
|
1348
|
+
promptCalls++;
|
|
1349
|
+
promptInputs.push(input);
|
|
1350
|
+
events.push(`prompt:${promptCalls}`);
|
|
1351
|
+
state.error = `provider failed ${promptCalls}`;
|
|
1352
|
+
},
|
|
1353
|
+
abort: () => {},
|
|
1354
|
+
reset: () => {
|
|
1355
|
+
state.error = undefined;
|
|
1356
|
+
},
|
|
1357
|
+
state,
|
|
1358
|
+
};
|
|
1359
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1360
|
+
const host: AdvisorRuntimeHost = {
|
|
1361
|
+
snapshotMessages: () => messages,
|
|
1362
|
+
enqueueAdvice: () => {},
|
|
1363
|
+
onTurnError: error => {
|
|
1364
|
+
turnErrors.push(error);
|
|
1365
|
+
events.push(`hook:${error instanceof Error ? error.message : String(error)}`);
|
|
1366
|
+
},
|
|
1367
|
+
notifyFailure: error => {
|
|
1368
|
+
failures.push(error);
|
|
1369
|
+
events.push(`notify:${error instanceof Error ? error.message : String(error)}`);
|
|
1370
|
+
},
|
|
1371
|
+
};
|
|
1372
|
+
const runtime = new AdvisorRuntime(agent, host, 1);
|
|
1373
|
+
|
|
1374
|
+
runtime.onTurnEnd(messages);
|
|
1375
|
+
await runtime.waitForCatchup(1000, 1);
|
|
1376
|
+
|
|
1377
|
+
expect(promptInputs).toHaveLength(3);
|
|
1378
|
+
expect(turnErrors.map(error => (error instanceof Error ? error.message : String(error)))).toEqual([
|
|
1379
|
+
"provider failed 1",
|
|
1380
|
+
"provider failed 2",
|
|
1381
|
+
"provider failed 3",
|
|
1382
|
+
]);
|
|
1383
|
+
expect(failures).toHaveLength(1);
|
|
1384
|
+
const failure = failures[0];
|
|
1385
|
+
if (!(failure instanceof Error)) throw new Error("expected advisor failure error");
|
|
1386
|
+
expect(failure.message).toBe("provider failed 3");
|
|
1387
|
+
expect(events).toEqual([
|
|
1388
|
+
"prompt:1",
|
|
1389
|
+
"hook:provider failed 1",
|
|
1390
|
+
"prompt:2",
|
|
1391
|
+
"hook:provider failed 2",
|
|
1392
|
+
"prompt:3",
|
|
1393
|
+
"hook:provider failed 3",
|
|
1394
|
+
"notify:provider failed 3",
|
|
1395
|
+
]);
|
|
1396
|
+
expect(runtime.backlog).toBe(0);
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
it("continues retrying when onTurnError rejects", async () => {
|
|
1400
|
+
const promptInputs: string[] = [];
|
|
1401
|
+
const turnErrors: unknown[] = [];
|
|
1402
|
+
const events: string[] = [];
|
|
1403
|
+
const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
|
|
1404
|
+
let promptCalls = 0;
|
|
1405
|
+
const agent: AdvisorAgent = {
|
|
1406
|
+
prompt: async input => {
|
|
1407
|
+
promptCalls++;
|
|
1408
|
+
promptInputs.push(input);
|
|
1409
|
+
events.push(`prompt:${promptCalls}`);
|
|
1410
|
+
state.error = promptCalls === 1 ? "provider failed" : undefined;
|
|
1411
|
+
},
|
|
1412
|
+
abort: () => {},
|
|
1413
|
+
reset: () => {
|
|
1414
|
+
state.error = undefined;
|
|
1415
|
+
},
|
|
1416
|
+
state,
|
|
1417
|
+
};
|
|
1418
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1419
|
+
const host: AdvisorRuntimeHost = {
|
|
1420
|
+
snapshotMessages: () => messages,
|
|
1421
|
+
enqueueAdvice: () => {},
|
|
1422
|
+
onTurnError: async error => {
|
|
1423
|
+
turnErrors.push(error);
|
|
1424
|
+
events.push(`hook:${error instanceof Error ? error.message : String(error)}`);
|
|
1425
|
+
throw new Error("hook failed");
|
|
1426
|
+
},
|
|
1427
|
+
};
|
|
1428
|
+
const runtime = new AdvisorRuntime(agent, host, 1);
|
|
1429
|
+
|
|
1430
|
+
runtime.onTurnEnd(messages);
|
|
1431
|
+
await runtime.waitForCatchup(1000, 1);
|
|
1432
|
+
|
|
1433
|
+
expect(promptInputs).toHaveLength(2);
|
|
1434
|
+
expect(turnErrors).toHaveLength(1);
|
|
1435
|
+
const error = turnErrors[0];
|
|
1436
|
+
if (!(error instanceof Error)) throw new Error("expected advisor turn error");
|
|
1437
|
+
expect(error.message).toBe("provider failed");
|
|
1438
|
+
expect(events).toEqual(["prompt:1", "hook:provider failed", "prompt:2"]);
|
|
1439
|
+
expect(runtime.backlog).toBe(0);
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1297
1442
|
it("rolls advisor state back after each failed prompt so retries don't replay duplicate turns", async () => {
|
|
1298
1443
|
// The real `Agent` appends the user batch + a synthetic `stopReason: "error"`
|
|
1299
1444
|
// assistant turn before `state.error` is read. Without rollback, the runtime's
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -48,6 +48,17 @@ export interface AdvisorRuntimeHost {
|
|
|
48
48
|
* one that routes `advise()` results back to the primary.
|
|
49
49
|
*/
|
|
50
50
|
beginAdvisorUpdate?(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Called with the error of every failed advisor turn, before the retry sleep
|
|
53
|
+
* or the dropped-after-3 path. Lets the host apply credential-level remedies
|
|
54
|
+
* the advisor loop lacks: the in-stream a/b/c auth retry rotates through
|
|
55
|
+
* sibling credentials within one request but never blocks the LAST failing
|
|
56
|
+
* one — the primary agent's retry pipeline does that via
|
|
57
|
+
* `markUsageLimitReached`, so without this hook the advisor re-picks the
|
|
58
|
+
* same usage-limited account on every retry. Errors thrown here are logged
|
|
59
|
+
* and swallowed.
|
|
60
|
+
*/
|
|
61
|
+
onTurnError?(error: unknown): Promise<void> | void;
|
|
51
62
|
/** Surface a non-recovering advisor failure to the host UI without adding model-visible context. */
|
|
52
63
|
notifyFailure?(error: unknown): void;
|
|
53
64
|
}
|
|
@@ -352,6 +363,14 @@ export class AdvisorRuntime {
|
|
|
352
363
|
if (this.#epoch !== epoch) continue;
|
|
353
364
|
this.#rollbackFailedTurn(messageSnapshot);
|
|
354
365
|
logger.debug("advisor turn failed", { err: String(err) });
|
|
366
|
+
try {
|
|
367
|
+
await this.host.onTurnError?.(err);
|
|
368
|
+
} catch (hookErr) {
|
|
369
|
+
logger.debug("advisor onTurnError hook failed", { err: String(hookErr) });
|
|
370
|
+
}
|
|
371
|
+
// The hook awaits; a reset during it invalidates this batch like the
|
|
372
|
+
// prompt await above — drop it instead of requeueing stale content.
|
|
373
|
+
if (this.#epoch !== epoch) continue;
|
|
355
374
|
this.#consecutiveFailures++;
|
|
356
375
|
if (this.#consecutiveFailures >= 3) {
|
|
357
376
|
logger.warn("advisor failed consecutively 3 times; dropping backlog to prevent stall");
|
|
@@ -49,7 +49,7 @@ export function createApiKeyResolver(
|
|
|
49
49
|
options: ApiKeyResolverOptions = {},
|
|
50
50
|
): ApiKeyResolver {
|
|
51
51
|
const { sessionId, baseUrl, modelId } = options;
|
|
52
|
-
return async ({ lastChance, error, signal }) => {
|
|
52
|
+
return async ({ lastChance, error, signal, previousKey }) => {
|
|
53
53
|
if (error === undefined) {
|
|
54
54
|
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId });
|
|
55
55
|
}
|
|
@@ -59,7 +59,12 @@ export function createApiKeyResolver(
|
|
|
59
59
|
// sibling exists we switch immediately; the precise no-sibling backoff
|
|
60
60
|
// is owned by `markUsageLimitReached` (default + server usage-report
|
|
61
61
|
// reset) and the outer whole-turn retry layer.
|
|
62
|
-
await registry.authStorage.rotateSessionCredential(provider, sessionId, {
|
|
62
|
+
await registry.authStorage.rotateSessionCredential(provider, sessionId, {
|
|
63
|
+
error,
|
|
64
|
+
modelId,
|
|
65
|
+
signal,
|
|
66
|
+
apiKey: previousKey,
|
|
67
|
+
});
|
|
63
68
|
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId });
|
|
64
69
|
}
|
|
65
70
|
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId, forceRefresh: true, signal });
|
|
@@ -2266,6 +2266,15 @@ export class ModelRegistry {
|
|
|
2266
2266
|
return true;
|
|
2267
2267
|
}
|
|
2268
2268
|
|
|
2269
|
+
/**
|
|
2270
|
+
* Clear the cooldown suppression for one selector after an explicit user selection.
|
|
2271
|
+
*/
|
|
2272
|
+
clearSuppressedSelector(selector: string): void {
|
|
2273
|
+
this.#suppressedSelectors.delete(
|
|
2274
|
+
normalizeSuppressedSelector(selector, (provider, id) => this.find(provider, id) !== undefined),
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2269
2278
|
/**
|
|
2270
2279
|
* Clear all cooldown suppressions recorded via {@link suppressSelector}.
|
|
2271
2280
|
* Used to reset retry-fallback cooldown state without a full {@link refresh}.
|