@oh-my-pi/pi-coding-agent 15.0.0 → 15.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 +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commands/commit.ts +10 -0
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +93 -8
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/ssh.ts +3 -2
- package/src/tools/write.ts +64 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives";
|
|
3
2
|
|
|
4
3
|
export interface ImageResizeOptions {
|
|
5
|
-
maxWidth?: number;
|
|
6
|
-
maxHeight?: number;
|
|
7
|
-
maxBytes?: number;
|
|
8
|
-
jpegQuality?: number;
|
|
4
|
+
maxWidth?: number;
|
|
5
|
+
maxHeight?: number;
|
|
6
|
+
maxBytes?: number;
|
|
7
|
+
jpegQuality?: number;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
export interface ResizedImage {
|
|
@@ -24,12 +23,12 @@ export interface ResizedImage {
|
|
|
24
23
|
const DEFAULT_MAX_BYTES = 500 * 1024;
|
|
25
24
|
|
|
26
25
|
const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// Anthropic's "internal recommended size" — Claude internally caps images at
|
|
27
|
+
// 1568px on the longest edge before vision processing.
|
|
29
28
|
maxWidth: 1568,
|
|
30
29
|
maxHeight: 1568,
|
|
31
30
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
32
|
-
jpegQuality:
|
|
31
|
+
jpegQuality: 80,
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
/** Pick the smallest of N encoded buffers. */
|
|
@@ -48,43 +47,34 @@ Buffer.prototype.toBase64 = function (this: Buffer) {
|
|
|
48
47
|
/**
|
|
49
48
|
* Resize and recompress an image to fit within the specified max dimensions and file size.
|
|
50
49
|
*
|
|
51
|
-
* Defaults target Anthropic's internal 1568px downscale threshold and produce small
|
|
52
|
-
* lossy JPEG output suitable for tool-call payloads (~100–500KB typical).
|
|
53
|
-
*
|
|
54
50
|
* Strategy:
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* 3. If still too large, JPEG-only quality ladder (PNG quality is a no-op).
|
|
61
|
-
* 4. If still too large, progressively reduce dimensions and retry the JPEG ladder.
|
|
62
|
-
* 5. Last resort: ship the smallest variant produced.
|
|
51
|
+
* 1. Probe metadata. If already within all limits, return original.
|
|
52
|
+
* 2. Resize to fit max dimensions and encode at high quality across PNG/JPEG/WebP — return smallest.
|
|
53
|
+
* 3. If still too large, walk a lossy JPEG/WebP quality ladder.
|
|
54
|
+
* 4. If still too large, walk a dimension-scale ladder × quality ladder.
|
|
55
|
+
* 5. If still too large, return the smallest variant produced.
|
|
63
56
|
*
|
|
64
|
-
*
|
|
57
|
+
* Backed by `Bun.Image`: a chainable native pipeline that runs decode/transform/encode
|
|
58
|
+
* off the JS thread when the terminal (`.bytes()`) is awaited.
|
|
65
59
|
*/
|
|
66
60
|
export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
|
|
67
61
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
68
62
|
const inputBuffer = Buffer.from(img.data, "base64");
|
|
69
63
|
|
|
70
64
|
try {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const originalWidth = image.width;
|
|
74
|
-
const originalHeight = image.height;
|
|
75
|
-
const format = img.mimeType?.split("/")[1] ?? "png";
|
|
65
|
+
const { width: originalWidth, height: originalHeight, format } = await new Bun.Image(inputBuffer).metadata();
|
|
66
|
+
const sourceMime = img.mimeType ?? `image/${format}`;
|
|
76
67
|
|
|
77
|
-
//
|
|
78
|
-
const originalSize = inputBuffer.length;
|
|
79
|
-
// Fast path: skip if already within dimensions AND well under budget.
|
|
68
|
+
// Fast path: already within dimensions AND well under budget.
|
|
80
69
|
// Threshold is 1/4 of budget — if already that compact, don't re-encode.
|
|
81
70
|
// Avoids wasted work on tiny icons/diagrams while ensuring larger PNGs
|
|
82
71
|
// still get JPEG-compressed.
|
|
72
|
+
const originalSize = inputBuffer.length;
|
|
83
73
|
const comfortableSize = opts.maxBytes / 4;
|
|
84
74
|
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= comfortableSize) {
|
|
85
75
|
return {
|
|
86
76
|
buffer: inputBuffer,
|
|
87
|
-
mimeType:
|
|
77
|
+
mimeType: sourceMime,
|
|
88
78
|
originalWidth,
|
|
89
79
|
originalHeight,
|
|
90
80
|
width: originalWidth,
|
|
@@ -117,14 +107,11 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
117
107
|
height: number,
|
|
118
108
|
quality: number,
|
|
119
109
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
|
120
|
-
const resized = await image.resize(width, height, SamplingFilter.Lanczos3);
|
|
121
|
-
|
|
122
110
|
const [pngBuffer, jpegBuffer, webpBuffer] = await Promise.all([
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
new Bun.Image(inputBuffer).resize(width, height).png().bytes(),
|
|
112
|
+
new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(),
|
|
113
|
+
new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(),
|
|
126
114
|
]);
|
|
127
|
-
|
|
128
115
|
return pickSmallest(
|
|
129
116
|
{ buffer: pngBuffer, mimeType: "image/png" },
|
|
130
117
|
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
@@ -140,10 +127,9 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
140
127
|
height: number,
|
|
141
128
|
quality: number,
|
|
142
129
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
|
143
|
-
const resized = await image.resize(width, height, SamplingFilter.Lanczos3);
|
|
144
130
|
const [jpegBuffer, webpBuffer] = await Promise.all([
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(),
|
|
132
|
+
new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(),
|
|
147
133
|
]);
|
|
148
134
|
return pickSmallest(
|
|
149
135
|
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
@@ -159,7 +145,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
159
145
|
let finalWidth = targetWidth;
|
|
160
146
|
let finalHeight = targetHeight;
|
|
161
147
|
|
|
162
|
-
// First attempt: resize to target, try
|
|
148
|
+
// First attempt: resize to target, try PNG/JPEG/WebP, pick smallest
|
|
163
149
|
best = await encodeSmallest(targetWidth, targetHeight, opts.jpegQuality);
|
|
164
150
|
|
|
165
151
|
if (best.buffer.length <= opts.maxBytes) {
|
|
@@ -264,9 +250,12 @@ export function formatDimensionNote(result: ResizedImage): string | undefined {
|
|
|
264
250
|
if (!result.wasResized) {
|
|
265
251
|
return undefined;
|
|
266
252
|
}
|
|
267
|
-
|
|
253
|
+
if (!result.originalWidth || !result.originalHeight || !result.width || !result.height) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
if (result.width === result.originalWidth && result.height === result.originalHeight) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
268
259
|
const scale = result.originalWidth / result.width;
|
|
269
|
-
return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${
|
|
270
|
-
result.height
|
|
271
|
-
}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;
|
|
260
|
+
return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;
|
|
272
261
|
}
|
package/src/vim/parser.ts
CHANGED
|
@@ -124,10 +124,6 @@ export function parseKeySequences(sequences: string[]): VimKeyToken[] {
|
|
|
124
124
|
return tokens;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
export function tokensToReplay(tokens: readonly VimKeyToken[]): string[] {
|
|
128
|
-
return tokens.map(token => token.value);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
127
|
export function replayTokens(values: readonly string[]): VimKeyToken[] {
|
|
132
128
|
return values.map((value, index) => ({
|
|
133
129
|
value,
|
|
@@ -136,16 +132,3 @@ export function replayTokens(values: readonly string[]): VimKeyToken[] {
|
|
|
136
132
|
offset: index,
|
|
137
133
|
}));
|
|
138
134
|
}
|
|
139
|
-
|
|
140
|
-
export function formatVimError(error: unknown): string {
|
|
141
|
-
if (!(error instanceof VimError)) {
|
|
142
|
-
return error instanceof Error ? error.message : String(error);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const base = error.message;
|
|
146
|
-
if (!error.location) {
|
|
147
|
-
return base;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return `${base} (sequence ${error.location.sequenceIndex + 1}, token ${error.location.offset + 1})`;
|
|
151
|
-
}
|
package/src/vim/render.ts
CHANGED
package/src/vim/types.ts
CHANGED
|
@@ -164,7 +164,7 @@ export function clonePosition(position: Position): Position {
|
|
|
164
164
|
return { line: position.line, col: position.col };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
function comparePositions(left: Position, right: Position): number {
|
|
168
168
|
if (left.line !== right.line) {
|
|
169
169
|
return left.line - right.line;
|
|
170
170
|
}
|
|
@@ -38,6 +38,7 @@ export interface AnthropicSearchParams {
|
|
|
38
38
|
max_tokens?: number;
|
|
39
39
|
/** Sampling temperature (0–1). Lower = more focused/factual. */
|
|
40
40
|
temperature?: number;
|
|
41
|
+
signal?: AbortSignal;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -86,6 +87,7 @@ async function callSearch(
|
|
|
86
87
|
systemPrompt?: string,
|
|
87
88
|
maxTokens?: number,
|
|
88
89
|
temperature?: number,
|
|
90
|
+
signal?: AbortSignal,
|
|
89
91
|
): Promise<AnthropicApiResponse> {
|
|
90
92
|
const url = buildAnthropicUrl(auth);
|
|
91
93
|
const headers = buildAnthropicSearchHeaders(auth);
|
|
@@ -116,6 +118,7 @@ async function callSearch(
|
|
|
116
118
|
method: "POST",
|
|
117
119
|
headers,
|
|
118
120
|
body: JSON.stringify(body),
|
|
121
|
+
signal,
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
if (!response.ok) {
|
|
@@ -253,6 +256,7 @@ export async function searchAnthropic(params: AnthropicSearchParams): Promise<Se
|
|
|
253
256
|
params.system_prompt,
|
|
254
257
|
params.max_tokens,
|
|
255
258
|
params.temperature,
|
|
259
|
+
params.signal,
|
|
256
260
|
);
|
|
257
261
|
|
|
258
262
|
const result = parseResponse(response);
|
|
@@ -281,6 +285,7 @@ export class AnthropicProvider extends SearchProvider {
|
|
|
281
285
|
num_results: params.numSearchResults ?? params.limit,
|
|
282
286
|
max_tokens: params.maxOutputTokens,
|
|
283
287
|
temperature: params.temperature,
|
|
288
|
+
signal: params.signal,
|
|
284
289
|
});
|
|
285
290
|
}
|
|
286
291
|
}
|
|
@@ -29,6 +29,7 @@ export interface ExaSearchParams {
|
|
|
29
29
|
exclude_domains?: string[];
|
|
30
30
|
start_published_date?: string;
|
|
31
31
|
end_published_date?: string;
|
|
32
|
+
signal?: AbortSignal;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
interface ExaSearchResult {
|
|
@@ -179,6 +180,7 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
|
|
|
179
180
|
"x-api-key": apiKey,
|
|
180
181
|
},
|
|
181
182
|
body: JSON.stringify(body),
|
|
183
|
+
signal: params.signal,
|
|
182
184
|
});
|
|
183
185
|
|
|
184
186
|
if (!response.ok) {
|
|
@@ -259,6 +261,7 @@ export class ExaProvider extends SearchProvider {
|
|
|
259
261
|
return searchExa({
|
|
260
262
|
query: params.query,
|
|
261
263
|
num_results: params.numSearchResults ?? params.limit,
|
|
264
|
+
signal: params.signal,
|
|
262
265
|
});
|
|
263
266
|
}
|
|
264
267
|
}
|
|
@@ -5,15 +5,11 @@
|
|
|
5
5
|
* Requires OAuth credentials stored in agent.db for provider "google-gemini-cli" or "google-antigravity".
|
|
6
6
|
* Returns synthesized answers with citations and source metadata from grounding chunks.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
ANTIGRAVITY_SYSTEM_INSTRUCTION,
|
|
10
|
-
extractRetryDelay,
|
|
11
|
-
getAntigravityUserAgent,
|
|
12
|
-
getGeminiCliHeaders,
|
|
13
|
-
} from "@oh-my-pi/pi-ai";
|
|
8
|
+
import { ANTIGRAVITY_SYSTEM_INSTRUCTION, getAntigravityUserAgent, getGeminiCliHeaders } from "@oh-my-pi/pi-ai";
|
|
14
9
|
import { refreshAntigravityToken } from "@oh-my-pi/pi-ai/utils/oauth/google-antigravity";
|
|
15
10
|
import { refreshGoogleCloudToken } from "@oh-my-pi/pi-ai/utils/oauth/google-gemini-cli";
|
|
16
|
-
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { fetchWithRetry, getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
12
|
+
|
|
17
13
|
import { AgentStorage } from "../../../session/agent-storage";
|
|
18
14
|
import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
|
|
19
15
|
import { SearchProviderError } from "../../../web/search/types";
|
|
@@ -43,6 +39,7 @@ export interface GeminiSearchParams extends GeminiToolParams {
|
|
|
43
39
|
max_output_tokens?: number;
|
|
44
40
|
/** Sampling temperature (0–1). Lower = more focused/factual. */
|
|
45
41
|
temperature?: number;
|
|
42
|
+
signal?: AbortSignal;
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
export function buildGeminiRequestTools(params: GeminiToolParams): Array<Record<string, Record<string, unknown>>> {
|
|
@@ -239,6 +236,7 @@ async function callGeminiSearch(
|
|
|
239
236
|
maxOutputTokens?: number,
|
|
240
237
|
temperature?: number,
|
|
241
238
|
toolParams: GeminiToolParams = {},
|
|
239
|
+
signal?: AbortSignal,
|
|
242
240
|
): Promise<{
|
|
243
241
|
answer: string;
|
|
244
242
|
sources: SearchSource[];
|
|
@@ -303,98 +301,43 @@ async function callGeminiSearch(
|
|
|
303
301
|
}
|
|
304
302
|
(requestBody.request as Record<string, unknown>).generationConfig = generationConfig;
|
|
305
303
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (attempt < MAX_RETRIES) {
|
|
327
|
-
await Bun.sleep(BASE_DELAY_MS * 2 ** attempt);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (auth.isAntigravity && endpointIndex < endpoints.length - 1) {
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (response.ok) {
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const errorText = await response.text();
|
|
343
|
-
const canRefreshAuth =
|
|
344
|
-
response.status === 401 ||
|
|
345
|
-
response.status === 403 ||
|
|
346
|
-
(response.status === 400 &&
|
|
347
|
-
/api key not valid|invalid credentials|invalid authentication/i.test(errorText));
|
|
348
|
-
if (canRefreshAuth && attempt === 0 && (await refreshGeminiAuth(auth))) {
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
const isRetryableStatus =
|
|
352
|
-
response.status === 429 ||
|
|
353
|
-
response.status === 500 ||
|
|
354
|
-
response.status === 502 ||
|
|
355
|
-
response.status === 503 ||
|
|
356
|
-
response.status === 504;
|
|
357
|
-
|
|
358
|
-
if (isRetryableStatus && attempt < MAX_RETRIES) {
|
|
359
|
-
const serverDelay = extractRetryDelay(errorText, response);
|
|
360
|
-
if (response.status === 429) {
|
|
361
|
-
if (serverDelay && rateLimitTimeSpent + serverDelay <= RATE_LIMIT_BUDGET_MS) {
|
|
362
|
-
rateLimitTimeSpent += serverDelay;
|
|
363
|
-
await Bun.sleep(serverDelay);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (!serverDelay) {
|
|
367
|
-
await Bun.sleep(BASE_DELAY_MS * 2 ** attempt);
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
} else {
|
|
371
|
-
await Bun.sleep(serverDelay ?? BASE_DELAY_MS * 2 ** attempt);
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
lastError = new SearchProviderError(
|
|
377
|
-
"gemini",
|
|
378
|
-
`Gemini Cloud Code API error (${response.status}): ${errorText}`,
|
|
379
|
-
response.status,
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
if (auth.isAntigravity && isRetryableStatus && endpointIndex < endpoints.length - 1) {
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
throw lastError;
|
|
387
|
-
}
|
|
304
|
+
const buildInit = (): RequestInit => ({
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: {
|
|
307
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
308
|
+
"Content-Type": "application/json",
|
|
309
|
+
Accept: "text/event-stream",
|
|
310
|
+
...headers,
|
|
311
|
+
},
|
|
312
|
+
body: JSON.stringify(requestBody),
|
|
313
|
+
signal,
|
|
314
|
+
});
|
|
315
|
+
const urlFor = (attempt: number) =>
|
|
316
|
+
`${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`;
|
|
317
|
+
|
|
318
|
+
let response = await fetchWithRetry(urlFor, {
|
|
319
|
+
...buildInit(),
|
|
320
|
+
maxAttempts: MAX_RETRIES + 1,
|
|
321
|
+
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
322
|
+
maxDelayMs: RATE_LIMIT_BUDGET_MS,
|
|
323
|
+
});
|
|
388
324
|
|
|
389
|
-
|
|
390
|
-
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
const errorText = await response.clone().text();
|
|
327
|
+
const canRefreshAuth =
|
|
328
|
+
response.status === 401 ||
|
|
329
|
+
response.status === 403 ||
|
|
330
|
+
(response.status === 400 && /api key not valid|invalid credentials|invalid authentication/i.test(errorText));
|
|
331
|
+
if (canRefreshAuth && (await refreshGeminiAuth(auth))) {
|
|
332
|
+
response = await fetchWithRetry(urlFor, {
|
|
333
|
+
...buildInit(),
|
|
334
|
+
maxAttempts: MAX_RETRIES + 1,
|
|
335
|
+
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
336
|
+
maxDelayMs: RATE_LIMIT_BUDGET_MS,
|
|
337
|
+
});
|
|
391
338
|
}
|
|
392
339
|
}
|
|
393
340
|
|
|
394
|
-
if (!response) {
|
|
395
|
-
throw new SearchProviderError("gemini", "Gemini API request failed", 500);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
341
|
if (!response.ok) {
|
|
399
342
|
const errorText = await response.text();
|
|
400
343
|
throw new SearchProviderError(
|
|
@@ -560,6 +503,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
|
|
|
560
503
|
code_execution: params.code_execution,
|
|
561
504
|
url_context: params.url_context,
|
|
562
505
|
},
|
|
506
|
+
params.signal,
|
|
563
507
|
);
|
|
564
508
|
|
|
565
509
|
let sources = result.sources;
|
|
@@ -599,6 +543,7 @@ export class GeminiProvider extends SearchProvider {
|
|
|
599
543
|
google_search: params.googleSearch,
|
|
600
544
|
code_execution: params.codeExecution,
|
|
601
545
|
url_context: params.urlContext,
|
|
546
|
+
signal: params.signal,
|
|
602
547
|
});
|
|
603
548
|
}
|
|
604
549
|
}
|
|
@@ -17,6 +17,7 @@ const JINA_SEARCH_URL = "https://s.jina.ai";
|
|
|
17
17
|
export interface JinaSearchParams {
|
|
18
18
|
query: string;
|
|
19
19
|
num_results?: number;
|
|
20
|
+
signal?: AbortSignal;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
interface JinaSearchResult {
|
|
@@ -33,13 +34,14 @@ export function findApiKey(): string | null {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/** Call Jina Reader search API. */
|
|
36
|
-
async function callJinaSearch(apiKey: string, query: string): Promise<JinaSearchResponse> {
|
|
37
|
+
async function callJinaSearch(apiKey: string, query: string, signal?: AbortSignal): Promise<JinaSearchResponse> {
|
|
37
38
|
const requestUrl = `${JINA_SEARCH_URL}/${encodeURIComponent(query)}`;
|
|
38
39
|
const response = await fetch(requestUrl, {
|
|
39
40
|
headers: {
|
|
40
41
|
Accept: "application/json",
|
|
41
42
|
Authorization: `Bearer ${apiKey}`,
|
|
42
43
|
},
|
|
44
|
+
signal,
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
if (!response.ok) {
|
|
@@ -58,7 +60,7 @@ export async function searchJina(params: JinaSearchParams): Promise<SearchRespon
|
|
|
58
60
|
throw new Error("JINA_API_KEY not found. Set it in environment or .env file.");
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
const response = await callJinaSearch(apiKey, params.query);
|
|
63
|
+
const response = await callJinaSearch(apiKey, params.query, params.signal);
|
|
62
64
|
const sources: SearchSource[] = [];
|
|
63
65
|
|
|
64
66
|
for (const result of response) {
|
|
@@ -91,6 +93,7 @@ export class JinaProvider extends SearchProvider {
|
|
|
91
93
|
return searchJina({
|
|
92
94
|
query: params.query,
|
|
93
95
|
num_results: params.numSearchResults ?? params.limit,
|
|
96
|
+
signal: params.signal,
|
|
94
97
|
});
|
|
95
98
|
}
|
|
96
99
|
}
|
|
@@ -20,6 +20,7 @@ const DEFAULT_NUM_RESULTS = 10;
|
|
|
20
20
|
export interface ZaiSearchParams {
|
|
21
21
|
query: string;
|
|
22
22
|
num_results?: number;
|
|
23
|
+
signal?: AbortSignal;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
interface ZaiSearchResult {
|
|
@@ -55,7 +56,7 @@ export async function findApiKey(): Promise<string | null> {
|
|
|
55
56
|
return findCredential(getEnvApiKey("zai"), "zai");
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
async function callZaiTool(apiKey: string, args: Record<string, unknown
|
|
59
|
+
async function callZaiTool(apiKey: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
|
|
59
60
|
const response = await fetch(ZAI_MCP_URL, {
|
|
60
61
|
method: "POST",
|
|
61
62
|
headers: {
|
|
@@ -72,6 +73,7 @@ async function callZaiTool(apiKey: string, args: Record<string, unknown>): Promi
|
|
|
72
73
|
arguments: args,
|
|
73
74
|
},
|
|
74
75
|
}),
|
|
76
|
+
signal,
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
if (!response.ok) {
|
|
@@ -157,7 +159,7 @@ async function callZaiSearch(apiKey: string, params: ZaiSearchParams): Promise<u
|
|
|
157
159
|
let lastError: unknown;
|
|
158
160
|
for (let i = 0; i < attempts.length; i++) {
|
|
159
161
|
try {
|
|
160
|
-
return await callZaiTool(apiKey, attempts[i]);
|
|
162
|
+
return await callZaiTool(apiKey, attempts[i], params.signal);
|
|
161
163
|
} catch (error) {
|
|
162
164
|
lastError = error;
|
|
163
165
|
const isLastAttempt = i === attempts.length - 1;
|
|
@@ -302,6 +304,7 @@ export class ZaiProvider extends SearchProvider {
|
|
|
302
304
|
return searchZai({
|
|
303
305
|
query: params.query,
|
|
304
306
|
num_results: params.numSearchResults ?? params.limit,
|
|
307
|
+
signal: params.signal,
|
|
305
308
|
});
|
|
306
309
|
}
|
|
307
310
|
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
Submits a finalized implementation plan for user approval.
|
|
2
|
-
|
|
3
|
-
Write the plan to `local://PLAN.md` first, then call this with `title` (e.g. `WP_MIGRATION_PLAN`); on approval the file is renamed to `local://<title>.md` and full tool access is restored.
|
|
4
|
-
- Use only after planning implementation steps; not for pure research.
|
|
5
|
-
- NEVER call before the plan file exists.
|
|
6
|
-
- NEVER use `ask` to request plan approval — this tool does that.
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import { type Static, Type } from "@sinclair/typebox";
|
|
5
|
-
import exitPlanModeDescription from "../prompts/tools/exit-plan-mode.md" with { type: "text" };
|
|
6
|
-
import type { ToolSession } from ".";
|
|
7
|
-
import { resolvePlanPath } from "./plan-mode-guard";
|
|
8
|
-
import { ToolError } from "./tool-errors";
|
|
9
|
-
|
|
10
|
-
const exitPlanModeSchema = Type.Object({
|
|
11
|
-
title: Type.String({ description: "final plan title", examples: ["WP_MIGRATION_PLAN"] }),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
type ExitPlanModeParams = Static<typeof exitPlanModeSchema>;
|
|
15
|
-
|
|
16
|
-
function normalizePlanTitle(title: string): { title: string; fileName: string } {
|
|
17
|
-
const trimmed = title.trim();
|
|
18
|
-
if (!trimmed) {
|
|
19
|
-
throw new ToolError("Title is required and must not be empty.");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) {
|
|
23
|
-
throw new ToolError("Title must not contain path separators or '..'.");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const withExtension = trimmed.toLowerCase().endsWith(".md") ? trimmed : `${trimmed}.md`;
|
|
27
|
-
if (!/^[A-Za-z0-9_-]+\.md$/.test(withExtension)) {
|
|
28
|
-
throw new ToolError("Title may only contain letters, numbers, underscores, or hyphens.");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const normalizedTitle = withExtension.slice(0, -3);
|
|
32
|
-
return { title: normalizedTitle, fileName: withExtension };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ExitPlanModeDetails {
|
|
36
|
-
planFilePath: string;
|
|
37
|
-
planExists: boolean;
|
|
38
|
-
title: string;
|
|
39
|
-
finalPlanFilePath: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, ExitPlanModeDetails> {
|
|
43
|
-
readonly name = "exit_plan_mode";
|
|
44
|
-
readonly label = "ExitPlanMode";
|
|
45
|
-
readonly description: string;
|
|
46
|
-
readonly parameters = exitPlanModeSchema;
|
|
47
|
-
readonly strict = true;
|
|
48
|
-
readonly concurrency = "exclusive";
|
|
49
|
-
readonly intent = (): string => "present plan";
|
|
50
|
-
|
|
51
|
-
constructor(private readonly session: ToolSession) {
|
|
52
|
-
this.description = prompt.render(exitPlanModeDescription);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async execute(
|
|
56
|
-
_toolCallId: string,
|
|
57
|
-
params: ExitPlanModeParams,
|
|
58
|
-
_signal?: AbortSignal,
|
|
59
|
-
_onUpdate?: AgentToolUpdateCallback<ExitPlanModeDetails>,
|
|
60
|
-
_context?: AgentToolContext,
|
|
61
|
-
): Promise<AgentToolResult<ExitPlanModeDetails>> {
|
|
62
|
-
const state = this.session.getPlanModeState?.();
|
|
63
|
-
if (!state?.enabled) {
|
|
64
|
-
throw new ToolError("Plan mode is not active.");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const normalized = normalizePlanTitle(params.title);
|
|
68
|
-
const finalPlanFilePath = `local://${normalized.fileName}`;
|
|
69
|
-
const resolvedPlanPath = resolvePlanPath(this.session, state.planFilePath);
|
|
70
|
-
resolvePlanPath(this.session, finalPlanFilePath);
|
|
71
|
-
let planExists = false;
|
|
72
|
-
try {
|
|
73
|
-
const stat = await fs.stat(resolvedPlanPath);
|
|
74
|
-
planExists = stat.isFile();
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if (!isEnoent(error)) {
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!planExists) {
|
|
82
|
-
throw new ToolError(
|
|
83
|
-
`Plan file not found at ${state.planFilePath}. Write the finalized plan to ${state.planFilePath} before calling exit_plan_mode.`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
content: [{ type: "text", text: "Plan ready for approval." }],
|
|
89
|
-
details: {
|
|
90
|
-
planFilePath: state.planFilePath,
|
|
91
|
-
planExists,
|
|
92
|
-
title: normalized.title,
|
|
93
|
-
finalPlanFilePath,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|