@oh-my-pi/pi-coding-agent 15.5.6 → 15.5.8
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 +72 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +60 -12
- package/dist/types/edit/file-snapshot-store.d.ts +9 -6
- package/dist/types/edit/hashline/diff.d.ts +4 -5
- package/dist/types/edit/streaming.d.ts +2 -1
- package/dist/types/eval/py/index.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/vault-protocol.d.ts +93 -0
- package/dist/types/lib/xai-http.d.ts +40 -0
- package/dist/types/mcp/transports/http.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +4 -1
- package/dist/types/tools/fetch.d.ts +16 -0
- package/dist/types/tools/image-gen.d.ts +6 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/match-line-format.d.ts +2 -2
- package/dist/types/tools/plan-mode-guard.d.ts +5 -6
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/tts.d.ts +18 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +2 -0
- package/package.json +8 -8
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-broker-cli.ts +2 -1
- package/src/cli/auth-gateway-cli.ts +210 -9
- package/src/commands/auth-gateway.ts +7 -1
- package/src/config/model-registry.ts +41 -9
- package/src/config/settings-schema.ts +55 -13
- package/src/edit/file-snapshot-store.ts +9 -6
- package/src/edit/hashline/diff.ts +26 -13
- package/src/edit/hashline/execute.ts +13 -9
- package/src/edit/renderer.ts +9 -9
- package/src/edit/streaming.ts +4 -6
- package/src/eval/py/index.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/shared-events.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +2 -0
- package/src/internal-urls/vault-protocol.ts +936 -0
- package/src/lib/xai-http.ts +124 -0
- package/src/main.ts +1 -2
- package/src/mcp/transports/http.ts +29 -2
- package/src/modes/components/tool-execution.ts +6 -4
- package/src/modes/controllers/event-controller.ts +10 -3
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +11 -3
- package/src/modes/utils/ui-helpers.ts +2 -1
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +3 -3
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +41 -10
- package/src/session/agent-session.ts +112 -14
- package/src/system-prompt.ts +2 -0
- package/src/tools/ast-edit.ts +10 -7
- package/src/tools/ast-grep.ts +12 -11
- package/src/tools/eval.ts +28 -3
- package/src/tools/fetch.ts +52 -24
- package/src/tools/image-gen.ts +205 -7
- package/src/tools/index.ts +1 -0
- package/src/tools/match-line-format.ts +2 -2
- package/src/tools/path-utils.ts +2 -0
- package/src/tools/plan-mode-guard.ts +20 -7
- package/src/tools/read.ts +70 -55
- package/src/tools/render-utils.ts +15 -0
- package/src/tools/search.ts +14 -14
- package/src/tools/tts.ts +133 -0
- package/src/tools/write.ts +61 -6
- package/src/utils/file-mentions.ts +11 -5
- package/src/web/search/providers/codex.ts +2 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Ported from NousResearch/hermes-agent (MIT) — tools/xai_http.py.
|
|
2
|
+
|
|
3
|
+
import { getBundledModels } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BASE_URL = "https://api.x.ai/v1";
|
|
8
|
+
|
|
9
|
+
interface XAICredentials {
|
|
10
|
+
provider: "xai-oauth" | "xai";
|
|
11
|
+
apiKey: string;
|
|
12
|
+
baseURL: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ohMyPiXAIUserAgent(): string {
|
|
16
|
+
return "oh-my-pi/xai";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type XAIProvider = "xai-oauth" | "xai";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the HTTP base URL for an xAI tool call.
|
|
23
|
+
*
|
|
24
|
+
* Precedence:
|
|
25
|
+
* 1. `model.baseUrl` from the registry IF the user pinned a per-model
|
|
26
|
+
* override — i.e. `merged.baseUrl` differs from the seeded/bundled
|
|
27
|
+
* default for the (provider, id) pair. Mirrors the chat path's per-model
|
|
28
|
+
* contract (`openai-responses.ts: model.baseUrl`).
|
|
29
|
+
* 2. `ModelRegistry.getProviderBaseUrl(provider)` — provider-level override
|
|
30
|
+
* (e.g. `providers.xai-oauth.baseUrl` from models.yml). Reached when the
|
|
31
|
+
* modelId does not appear in the registry under this provider, which
|
|
32
|
+
* happens for tool-only ids like `grok-imagine-image` that
|
|
33
|
+
* `applyXAIOAuthCuration` filters out via `XAI_NON_CHAT_PREFIXES`.
|
|
34
|
+
* Without this leg, a registry-configured proxy is silently bypassed for
|
|
35
|
+
* image/TTS traffic.
|
|
36
|
+
* 3. `XAI_BASE_URL` env var (legacy global override, preserved).
|
|
37
|
+
* 4. `DEFAULT_BASE_URL = "https://api.x.ai/v1"`.
|
|
38
|
+
*
|
|
39
|
+
* The override gate at step 1 uses `bundled?.baseUrl ?? DEFAULT_BASE_URL` as
|
|
40
|
+
* the canonical default sentinel. For xai (which has bundled entries) this
|
|
41
|
+
* compares against the bundled value; for xai-oauth (no bundled entries —
|
|
42
|
+
* models.json carries no xai-oauth records when the seed is absent, the
|
|
43
|
+
* picker is seeded statically from `xaiOAuthModelManagerOptions` with
|
|
44
|
+
* `baseUrl: DEFAULT_BASE_URL`) the sentinel falls back to DEFAULT_BASE_URL
|
|
45
|
+
* so the env leg remains reachable. Without that fallback, every xai-oauth
|
|
46
|
+
* model id forces `!bundled === true` and short-circuits XAI_BASE_URL
|
|
47
|
+
* silently. Lookup is scoped to (provider, id); matching by id alone would
|
|
48
|
+
* let xai-oauth entries hijack a xai tool call (or vice versa) when the
|
|
49
|
+
* same model id ships under both descriptors.
|
|
50
|
+
*/
|
|
51
|
+
function resolveXAIBaseURL(modelRegistry: ModelRegistry, provider: XAIProvider, modelId: string | undefined): string {
|
|
52
|
+
if (modelId) {
|
|
53
|
+
const merged = modelRegistry.getAll().find(m => m.id === modelId && m.provider === provider);
|
|
54
|
+
if (merged?.baseUrl) {
|
|
55
|
+
const bundled = getBundledModels(provider as Parameters<typeof getBundledModels>[0]).find(
|
|
56
|
+
m => m.id === modelId,
|
|
57
|
+
);
|
|
58
|
+
const providerDefault = bundled?.baseUrl ?? DEFAULT_BASE_URL;
|
|
59
|
+
if (merged.baseUrl !== providerDefault) {
|
|
60
|
+
return merged.baseUrl.replace(/\/$/, "");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const providerBaseUrl = modelRegistry.getProviderBaseUrl(provider);
|
|
65
|
+
if (providerBaseUrl) {
|
|
66
|
+
const normalized = providerBaseUrl.replace(/\/$/, "");
|
|
67
|
+
if (normalized !== DEFAULT_BASE_URL) return normalized;
|
|
68
|
+
}
|
|
69
|
+
return ($env.XAI_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve xAI credentials for HTTP tool calls.
|
|
74
|
+
*
|
|
75
|
+
* Credential priority:
|
|
76
|
+
* 1. xai-oauth — only when a *dedicated* xai-oauth source exists. Composed
|
|
77
|
+
* of two checks against the registry layer:
|
|
78
|
+
* a. `authStorage.hasNonEnvCredential("xai-oauth")` covers stored
|
|
79
|
+
* credentials (OAuth or api_key), runtime overrides (CLI
|
|
80
|
+
* `--api-key` for xai-oauth), config overrides (models.yml
|
|
81
|
+
* `providers.xai-oauth.apiKey`), and fallback resolvers.
|
|
82
|
+
* b. `$env.XAI_OAUTH_TOKEN` covers the xai-oauth-specific env var.
|
|
83
|
+
* `XAI_API_KEY` is intentionally NOT a signal here, even though the
|
|
84
|
+
* env-fallback map (`stream.ts: "xai-oauth"`) lets xai-oauth borrow it
|
|
85
|
+
* as a back-compat convenience: the borrow lets API-key-only setups
|
|
86
|
+
* satisfy the xai-oauth branch and then resolve baseUrl under
|
|
87
|
+
* xai-oauth instead of xai, silently bypassing `providers.xai.baseUrl`
|
|
88
|
+
* overrides for image/TTS traffic. The gate routes the borrow case to
|
|
89
|
+
* step 2 while preserving every dedicated xai-oauth path.
|
|
90
|
+
* 2. xai (plain API key). Delegates to ModelRegistry.getApiKeyForProvider
|
|
91
|
+
* which runs AuthStorage.getApiKey's full cascade: runtime override →
|
|
92
|
+
* models.yml config override → stored api_key credential → OAuth
|
|
93
|
+
* resolution → XAI_API_KEY env var → custom fallback resolver.
|
|
94
|
+
*
|
|
95
|
+
* baseURL: see `resolveXAIBaseURL` above. Resolved AFTER the credential
|
|
96
|
+
* decision so the scoped (provider, id) lookup is unambiguous. `modelId`
|
|
97
|
+
* is optional; probes / tool-availability checks pass `undefined` and fall
|
|
98
|
+
* through to env/default.
|
|
99
|
+
*
|
|
100
|
+
* Returns null when neither credential is available. Caller is responsible
|
|
101
|
+
* for surfacing an actionable error message in that case.
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveXAIHttpCredentials(
|
|
104
|
+
modelRegistry: ModelRegistry,
|
|
105
|
+
modelId?: string,
|
|
106
|
+
): Promise<XAICredentials | null> {
|
|
107
|
+
const hasDedicatedXaiOAuth =
|
|
108
|
+
modelRegistry.authStorage.hasNonEnvCredential("xai-oauth") || Boolean($env.XAI_OAUTH_TOKEN);
|
|
109
|
+
if (hasDedicatedXaiOAuth) {
|
|
110
|
+
const oauthKey = await modelRegistry.getApiKeyForProvider("xai-oauth");
|
|
111
|
+
if (oauthKey) {
|
|
112
|
+
const baseURL = resolveXAIBaseURL(modelRegistry, "xai-oauth", modelId);
|
|
113
|
+
return { provider: "xai-oauth", apiKey: oauthKey, baseURL };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const apiKey = await modelRegistry.getApiKeyForProvider("xai");
|
|
118
|
+
if (apiKey) {
|
|
119
|
+
const baseURL = resolveXAIBaseURL(modelRegistry, "xai", modelId);
|
|
120
|
+
return { provider: "xai", apiKey, baseURL };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -9,7 +9,6 @@ import * as fs from "node:fs/promises";
|
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { createInterface } from "node:readline/promises";
|
|
12
|
-
import { keepaliveWhile } from "@oh-my-pi/pi-agent-core";
|
|
13
12
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
14
13
|
import {
|
|
15
14
|
$env,
|
|
@@ -316,7 +315,7 @@ async function runInteractiveMode(
|
|
|
316
315
|
}
|
|
317
316
|
|
|
318
317
|
while (true) {
|
|
319
|
-
const input = await
|
|
318
|
+
const input = await mode.getUserInput();
|
|
320
319
|
await submitInteractiveInput(mode, session, input);
|
|
321
320
|
}
|
|
322
321
|
}
|
|
@@ -16,8 +16,23 @@ import type {
|
|
|
16
16
|
MCPTransport,
|
|
17
17
|
} from "../../mcp/types";
|
|
18
18
|
import { toJsonRpcError } from "../../mcp/types";
|
|
19
|
-
import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
|
|
19
|
+
import { createMCPTimeout, getNeverAbortSignal, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
|
|
20
20
|
|
|
21
|
+
const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort startup deadline for the optional Streamable HTTP GET SSE listener.
|
|
24
|
+
*
|
|
25
|
+
* Returns `0` (disabled) when the operator has explicitly disabled MCP client-side
|
|
26
|
+
* timeouts via `timeout: 0` or `OMP_MCP_TIMEOUT_MS=0`, mirroring the rest of the
|
|
27
|
+
* MCP timeout surface. Otherwise caps the wait at one second and scales below
|
|
28
|
+
* short request timeouts so connect-time never exceeds the request budget.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveSSEConnectTimeoutMs(configTimeout?: number): number {
|
|
31
|
+
const requestTimeout = resolveMCPTimeoutMs(configTimeout);
|
|
32
|
+
if (!isMCPTimeoutEnabled(requestTimeout)) return 0;
|
|
33
|
+
const boundedTimeout = Math.min(HTTP_SSE_CONNECT_TIMEOUT_MS, Math.floor(requestTimeout / 4));
|
|
34
|
+
return Math.max(1, boundedTimeout);
|
|
35
|
+
}
|
|
21
36
|
/**
|
|
22
37
|
* HTTP transport for MCP servers.
|
|
23
38
|
* Uses POST for requests, supports SSE responses.
|
|
@@ -73,6 +88,15 @@ export class HttpTransport implements MCPTransport {
|
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
let response: Response;
|
|
91
|
+
let timedOut = false;
|
|
92
|
+
const startupTimeoutMs = resolveSSEConnectTimeoutMs(this.config.timeout);
|
|
93
|
+
const timeoutId =
|
|
94
|
+
startupTimeoutMs > 0
|
|
95
|
+
? setTimeout(() => {
|
|
96
|
+
timedOut = true;
|
|
97
|
+
this.#sseConnection?.abort();
|
|
98
|
+
}, startupTimeoutMs)
|
|
99
|
+
: null;
|
|
76
100
|
try {
|
|
77
101
|
response = await fetch(this.config.url, {
|
|
78
102
|
method: "GET",
|
|
@@ -81,13 +105,16 @@ export class HttpTransport implements MCPTransport {
|
|
|
81
105
|
});
|
|
82
106
|
} catch (error) {
|
|
83
107
|
this.#sseConnection = null;
|
|
84
|
-
if (error instanceof Error && error.name !== "AbortError") {
|
|
108
|
+
if (error instanceof Error && error.name !== "AbortError" && !timedOut) {
|
|
85
109
|
this.onError?.(error);
|
|
86
110
|
}
|
|
87
111
|
return;
|
|
112
|
+
} finally {
|
|
113
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
if (response.status === 405 || !response.ok || !response.body) {
|
|
117
|
+
await response.body?.cancel();
|
|
91
118
|
this.#sseConnection = null;
|
|
92
119
|
return;
|
|
93
120
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SnapshotStore } from "@oh-my-pi/hashline";
|
|
1
2
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
@@ -105,10 +106,10 @@ function resolveEditModeForTool(toolName: string, tool: AgentTool | undefined):
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export interface ToolExecutionOptions {
|
|
109
|
+
snapshots?: SnapshotStore;
|
|
108
110
|
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
109
111
|
editFuzzyThreshold?: number;
|
|
110
112
|
editAllowFuzzy?: boolean;
|
|
111
|
-
hashlineAutoDropPureInsertDuplicates?: boolean;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
export interface ToolExecutionHandle {
|
|
@@ -142,7 +143,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
142
143
|
#showImages: boolean;
|
|
143
144
|
#editFuzzyThreshold: number | undefined;
|
|
144
145
|
#editAllowFuzzy: boolean | undefined;
|
|
145
|
-
#
|
|
146
|
+
#snapshots?: SnapshotStore;
|
|
146
147
|
#isPartial = true;
|
|
147
148
|
#tool?: AgentTool;
|
|
148
149
|
#ui: TUI;
|
|
@@ -189,7 +190,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
189
190
|
this.#showImages = options.showImages ?? true;
|
|
190
191
|
this.#editFuzzyThreshold = options.editFuzzyThreshold;
|
|
191
192
|
this.#editAllowFuzzy = options.editAllowFuzzy;
|
|
192
|
-
this.#
|
|
193
|
+
this.#snapshots = options.snapshots;
|
|
193
194
|
this.#tool = tool;
|
|
194
195
|
this.#ui = ui;
|
|
195
196
|
this.#cwd = cwd;
|
|
@@ -266,12 +267,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
266
267
|
|
|
267
268
|
try {
|
|
268
269
|
const isStreaming = !this.#argsComplete;
|
|
270
|
+
if (editMode === "hashline" && !this.#snapshots) return;
|
|
269
271
|
const previews = await strategy.computeDiffPreview(effectiveArgs, {
|
|
270
272
|
cwd: this.#cwd,
|
|
271
273
|
signal: controller.signal,
|
|
274
|
+
snapshots: this.#snapshots!,
|
|
272
275
|
fuzzyThreshold: this.#editFuzzyThreshold,
|
|
273
276
|
allowFuzzy: this.#editAllowFuzzy,
|
|
274
|
-
hashlineAutoDropPureInsertDuplicates: this.#hashlineAutoDropPureInsertDuplicates,
|
|
275
277
|
isStreaming,
|
|
276
278
|
});
|
|
277
279
|
if (controller.signal.aborted) return;
|
|
@@ -3,6 +3,7 @@ import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compac
|
|
|
3
3
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { type Component, Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { settings } from "../../config/settings";
|
|
6
|
+
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
6
7
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
7
8
|
import {
|
|
8
9
|
ReadToolGroupComponent,
|
|
@@ -329,10 +330,10 @@ export class EventController {
|
|
|
329
330
|
content.name,
|
|
330
331
|
renderArgs,
|
|
331
332
|
{
|
|
333
|
+
snapshots: getFileSnapshotStore(this.ctx.session),
|
|
332
334
|
showImages: settings.get("terminal.showImages"),
|
|
333
335
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
334
336
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
335
|
-
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
336
337
|
},
|
|
337
338
|
tool,
|
|
338
339
|
this.ctx.ui,
|
|
@@ -444,10 +445,10 @@ export class EventController {
|
|
|
444
445
|
event.toolName,
|
|
445
446
|
event.args,
|
|
446
447
|
{
|
|
448
|
+
snapshots: getFileSnapshotStore(this.ctx.session),
|
|
447
449
|
showImages: settings.get("terminal.showImages"),
|
|
448
450
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
449
451
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
450
|
-
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
451
452
|
},
|
|
452
453
|
tool,
|
|
453
454
|
this.ctx.ui,
|
|
@@ -598,7 +599,13 @@ export class EventController {
|
|
|
598
599
|
};
|
|
599
600
|
this.ctx.statusContainer.clear();
|
|
600
601
|
const reasonText =
|
|
601
|
-
event.reason === "overflow"
|
|
602
|
+
event.reason === "overflow"
|
|
603
|
+
? "Context overflow detected, "
|
|
604
|
+
: event.reason === "incomplete"
|
|
605
|
+
? "Response incomplete, "
|
|
606
|
+
: event.reason === "idle"
|
|
607
|
+
? "Idle "
|
|
608
|
+
: "";
|
|
602
609
|
const actionLabel = event.action === "handoff" ? "Auto-handoff" : "Auto context-full maintenance";
|
|
603
610
|
this.ctx.autoCompactionLoader = new Loader(
|
|
604
611
|
this.ctx.ui,
|
|
@@ -29,7 +29,12 @@ import {
|
|
|
29
29
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
30
30
|
import { type SessionInfo, SessionManager } from "../../session/session-manager";
|
|
31
31
|
import { FileSessionStorage } from "../../session/session-storage";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
isImageProviderPreference,
|
|
34
|
+
isSearchProviderPreference,
|
|
35
|
+
setPreferredImageProvider,
|
|
36
|
+
setPreferredSearchProvider,
|
|
37
|
+
} from "../../tools";
|
|
33
38
|
import { setSessionTerminalTitle } from "../../utils/title-generator";
|
|
34
39
|
import { AgentDashboard } from "../components/agent-dashboard";
|
|
35
40
|
import { AssistantMessageComponent } from "../components/assistant-message";
|
|
@@ -374,7 +379,7 @@ export class SelectorController {
|
|
|
374
379
|
}
|
|
375
380
|
break;
|
|
376
381
|
case "providers.image":
|
|
377
|
-
if (value
|
|
382
|
+
if (isImageProviderPreference(value)) {
|
|
378
383
|
setPreferredImageProvider(value);
|
|
379
384
|
}
|
|
380
385
|
break;
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from "node:fs/promises";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type Agent,
|
|
9
|
+
type AgentMessage,
|
|
10
|
+
type AgentToolResult,
|
|
11
|
+
EventLoopKeepalive,
|
|
12
|
+
ThinkingLevel,
|
|
13
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
8
14
|
import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
|
9
15
|
import {
|
|
10
16
|
type AssistantMessage,
|
|
@@ -619,7 +625,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
619
625
|
};
|
|
620
626
|
this.#scheduleLoopAutoSubmit();
|
|
621
627
|
this.#scheduleGoalContinuation();
|
|
622
|
-
|
|
628
|
+
|
|
629
|
+
using _ = new EventLoopKeepalive();
|
|
630
|
+
return await promise;
|
|
623
631
|
}
|
|
624
632
|
|
|
625
633
|
#scheduleLoopAutoSubmit(): void {
|
|
@@ -1012,7 +1020,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1012
1020
|
}
|
|
1013
1021
|
|
|
1014
1022
|
async #getPlanFilePath(): Promise<string> {
|
|
1015
|
-
return "local://PLAN.md";
|
|
1023
|
+
return this.session.getPlanReferencePath() || "local://PLAN.md";
|
|
1016
1024
|
}
|
|
1017
1025
|
|
|
1018
1026
|
#resolvePlanFilePath(planFilePath: string): string {
|
|
@@ -2,6 +2,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { type Component, Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
|
+
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
5
6
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
6
7
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
7
8
|
import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
|
|
@@ -377,10 +378,10 @@ export class UiHelpers {
|
|
|
377
378
|
content.name,
|
|
378
379
|
renderArgs,
|
|
379
380
|
{
|
|
381
|
+
snapshots: getFileSnapshotStore(this.ctx.session),
|
|
380
382
|
showImages: settings.get("terminal.showImages"),
|
|
381
383
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
382
384
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
383
|
-
hashlineAutoDropPureInsertDuplicates: settings.get("edit.hashlineAutoDropPureInsertDuplicates"),
|
|
384
385
|
},
|
|
385
386
|
tool,
|
|
386
387
|
this.ctx.ui,
|
|
@@ -59,6 +59,9 @@ With most FS/bash-like tools, static references to them will automatically resol
|
|
|
59
59
|
- `/<path>`: JSON field extraction
|
|
60
60
|
- `artifact://<id>`: Artifact content
|
|
61
61
|
- `local://<name>.md`: Plan artifacts and shared content with subagents
|
|
62
|
+
{{#if hasObsidian}}
|
|
63
|
+
- `vault://<vault>/<path>`: Obsidian vault content (read/edit). `vault://` lists vaults; `vault://_/…` targets the active vault. File-scoped `?op=outline|backlinks|links|tags|properties|tasks|base|…`; vault-scoped `?op=search&q=…|daily|tasks|orphans|unresolved|bases|…`.
|
|
64
|
+
{{/if}}
|
|
62
65
|
- `mcp://<uri>`: MCP resource
|
|
63
66
|
- `issue://<N>` (or `issue://<owner>/<repo>/<N>`): GitHub issue view; cached on disk so re-reads are free. Bare `issue://` (or `issue://<owner>/<repo>`) lists recent issues; supports `?state=open|closed|all&limit=&author=&label=`.
|
|
64
67
|
- `pr://<N>` (or `pr://<owner>/<repo>/<N>`): GitHub PR view; same cache. Append `?comments=0` to drop the comments section. Bare `pr://` (or `pr://<owner>/<repo>`) lists recent PRs; supports `?state=open|closed|merged|all&limit=&author=&label=`.
|
|
@@ -14,7 +14,7 @@ Performs structural AST-aware rewrites via native ast-grep.
|
|
|
14
14
|
</instruction>
|
|
15
15
|
|
|
16
16
|
<output>
|
|
17
|
-
- Replacement summary, per-file replacement counts, and change diffs as `¶src/foo.ts#
|
|
17
|
+
- Replacement summary, per-file replacement counts, and change diffs as `¶src/foo.ts#0a`, `-12:before`, `+12:after` lines in hashline mode
|
|
18
18
|
- Parse issues when files cannot be processed
|
|
19
19
|
</output>
|
|
20
20
|
|
|
@@ -18,7 +18,7 @@ Performs structural code search using AST matching via native ast-grep.
|
|
|
18
18
|
|
|
19
19
|
<output>
|
|
20
20
|
- Grouped matches with file path, byte range, line/column ranges, metavariable captures
|
|
21
|
-
- Match lines are numbered under a file
|
|
21
|
+
- Match lines are numbered under a file snapshot tag header in hashline mode: `¶src/foo.ts#0a`, `*42:content` for the matched line, ` 43:content` for context
|
|
22
22
|
- Summary counts (`totalMatches`, `filesWithMatches`, `filesSearched`) and parse issues when present
|
|
23
23
|
</output>
|
|
24
24
|
|
|
@@ -8,7 +8,7 @@ Read files, directories, archives, SQLite databases, images, documents, internal
|
|
|
8
8
|
|
|
9
9
|
## Parameters
|
|
10
10
|
|
|
11
|
-
- `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `mcp://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
|
|
11
|
+
- `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
|
|
12
12
|
|
|
13
13
|
## Selectors
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ Append `:<sel>` to `path`. The bare path falls back to the default mode.
|
|
|
28
28
|
|
|
29
29
|
- Reading a directory path returns a depth-limited dirent listing.
|
|
30
30
|
{{#if IS_HL_MODE}}
|
|
31
|
-
- Reading a file with an explicit selector emits a file
|
|
31
|
+
- Reading a file with an explicit selector emits a file snapshot tag header and numbered lines: `¶src/foo.ts#0a` then `41:def alpha():`. Copy the `¶PATH#TAG` header for anchored edits; ops use bare line numbers. NEVER fabricate the tag.
|
|
32
32
|
{{else}}
|
|
33
33
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
34
34
|
- Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`.
|
|
@@ -70,7 +70,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
70
70
|
|
|
71
71
|
# Internal URIs
|
|
72
72
|
|
|
73
|
-
`skill://<name>`, `agent://<id>`, `artifact://<id>`, `memory://root`, `rule://<name>`, `local://<name>.md`, `mcp://<uri>` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
|
|
73
|
+
`skill://<name>`, `agent://<id>`, `artifact://<id>`, `memory://root`, `rule://<name>`, `local://<name>.md`, `vault://<vault>/<path>`, `mcp://<uri>` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
|
|
74
74
|
|
|
75
75
|
<critical>
|
|
76
76
|
- You MUST use `read` for every file, directory, archive, and URL inspection. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, `wget` are FORBIDDEN — any such bash call is a bug, regardless of how short or convenient it looks.
|
|
@@ -9,7 +9,7 @@ Searches files using powerful regex matching.
|
|
|
9
9
|
|
|
10
10
|
<output>
|
|
11
11
|
{{#if IS_HL_MODE}}
|
|
12
|
-
- Text output emits a file
|
|
12
|
+
- Text output emits a file snapshot tag header per matched file plus numbered lines: `¶src/login.ts#1f`, `*42:if (user.id) {` (match), ` 43:return user;` (context). Copy the header for anchored edits; ops use bare line numbers.
|
|
13
13
|
{{else}}
|
|
14
14
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
15
15
|
- Text output is line-number-prefixed
|
package/src/sdk.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import {
|
|
12
12
|
type CredentialDisabledEvent,
|
|
13
|
+
isUsageLimitError,
|
|
13
14
|
type Message,
|
|
14
15
|
type Model,
|
|
15
16
|
type SimpleStreamOptions,
|
|
@@ -23,6 +24,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
23
24
|
import {
|
|
24
25
|
$env,
|
|
25
26
|
$flag,
|
|
27
|
+
extractRetryHint,
|
|
26
28
|
getAgentDbPath,
|
|
27
29
|
getAgentDir,
|
|
28
30
|
getProjectDir,
|
|
@@ -129,6 +131,7 @@ import {
|
|
|
129
131
|
FindTool,
|
|
130
132
|
getSearchTools,
|
|
131
133
|
HIDDEN_TOOLS,
|
|
134
|
+
isImageProviderPreference,
|
|
132
135
|
isSearchProviderPreference,
|
|
133
136
|
type LspStartupServerInfo,
|
|
134
137
|
loadSshTool,
|
|
@@ -148,6 +151,7 @@ import { ToolContextStore } from "./tools/context";
|
|
|
148
151
|
import { getImageGenTools } from "./tools/image-gen";
|
|
149
152
|
import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
150
153
|
import { queueResolveHandler } from "./tools/resolve";
|
|
154
|
+
import { ttsTool } from "./tools/tts";
|
|
151
155
|
import { EventBus } from "./utils/event-bus";
|
|
152
156
|
import { buildNamedToolChoice } from "./utils/tool-choice";
|
|
153
157
|
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
@@ -893,12 +897,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
893
897
|
}
|
|
894
898
|
|
|
895
899
|
const imageProvider = settings.get("providers.image");
|
|
896
|
-
if (
|
|
897
|
-
imageProvider === "auto" ||
|
|
898
|
-
imageProvider === "openai" ||
|
|
899
|
-
imageProvider === "gemini" ||
|
|
900
|
-
imageProvider === "openrouter"
|
|
901
|
-
) {
|
|
900
|
+
if (isImageProviderPreference(imageProvider)) {
|
|
902
901
|
setPreferredImageProvider(imageProvider);
|
|
903
902
|
}
|
|
904
903
|
|
|
@@ -1319,6 +1318,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1319
1318
|
customTools.push(...(imageGenTools as unknown as CustomTool[]));
|
|
1320
1319
|
}
|
|
1321
1320
|
|
|
1321
|
+
if (settings.get("tts.enabled")) {
|
|
1322
|
+
customTools.push(ttsTool as unknown as CustomTool);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1322
1325
|
// Add web search tools
|
|
1323
1326
|
if (options.toolNames?.includes("web_search")) {
|
|
1324
1327
|
customTools.push(...getSearchTools());
|
|
@@ -1876,21 +1879,49 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1876
1879
|
}
|
|
1877
1880
|
return key;
|
|
1878
1881
|
},
|
|
1879
|
-
streamFn: (streamModel, context, streamOptions) =>
|
|
1880
|
-
|
|
1882
|
+
streamFn: (streamModel, context, streamOptions) => {
|
|
1883
|
+
const openrouterRoutingPreset = settings.get("providers.openrouterVariant");
|
|
1884
|
+
const openrouterVariant =
|
|
1885
|
+
openrouterRoutingPreset && openrouterRoutingPreset !== "default" ? openrouterRoutingPreset : undefined;
|
|
1886
|
+
return streamSimple(streamModel, context, {
|
|
1881
1887
|
...streamOptions,
|
|
1888
|
+
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
|
|
1882
1889
|
onAuthError: async (provider, oldKey, error) => {
|
|
1890
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1891
|
+
// streamSimple invokes this for both 401 auth failures AND
|
|
1892
|
+
// rotatable usage-limit errors (Codex usage_limit_reached,
|
|
1893
|
+
// Anthropic usage_limit_reached, etc.). The two need
|
|
1894
|
+
// different storage actions: a real 401 means the credential
|
|
1895
|
+
// is bad and should be marked suspect; a usage limit just
|
|
1896
|
+
// means this account is parked until reset and should be
|
|
1897
|
+
// temporarily blocked so a sibling can pick the request up.
|
|
1898
|
+
if (isUsageLimitError(message)) {
|
|
1899
|
+
const retryAfterMs = extractRetryHint(undefined, message);
|
|
1900
|
+
const switched = await modelRegistry.authStorage.markUsageLimitReached(provider, agent.sessionId, {
|
|
1901
|
+
retryAfterMs,
|
|
1902
|
+
signal: streamOptions?.signal,
|
|
1903
|
+
});
|
|
1904
|
+
logger.debug("Retrying provider request after usage-limit block", {
|
|
1905
|
+
provider,
|
|
1906
|
+
switched,
|
|
1907
|
+
retryAfterMs,
|
|
1908
|
+
error: message,
|
|
1909
|
+
});
|
|
1910
|
+
if (!switched) return undefined;
|
|
1911
|
+
return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
|
|
1912
|
+
}
|
|
1883
1913
|
await modelRegistry.authStorage.invalidateCredentialMatching(provider, oldKey, {
|
|
1884
1914
|
signal: streamOptions?.signal,
|
|
1885
1915
|
sessionId: agent.sessionId,
|
|
1886
1916
|
});
|
|
1887
1917
|
logger.debug("Retrying provider request after credential invalidation", {
|
|
1888
1918
|
provider,
|
|
1889
|
-
error:
|
|
1919
|
+
error: message,
|
|
1890
1920
|
});
|
|
1891
1921
|
return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
|
|
1892
1922
|
},
|
|
1893
|
-
})
|
|
1923
|
+
});
|
|
1924
|
+
},
|
|
1894
1925
|
cursorExecHandlers,
|
|
1895
1926
|
transformToolCallArguments: (args, _toolName) => {
|
|
1896
1927
|
let result = args;
|