@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.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.
Files changed (193) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/types/cli/file-processor.d.ts +1 -1
  3. package/dist/types/config/settings-schema.d.ts +45 -3
  4. package/dist/types/config/settings.d.ts +1 -1
  5. package/dist/types/debug/raw-sse.d.ts +2 -0
  6. package/dist/types/edit/file-read-cache.d.ts +15 -4
  7. package/dist/types/edit/index.d.ts +3 -8
  8. package/dist/types/edit/renderer.d.ts +1 -2
  9. package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
  10. package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
  11. package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
  12. package/dist/types/eval/js/shared/runtime.d.ts +14 -8
  13. package/dist/types/eval/py/executor.d.ts +1 -2
  14. package/dist/types/eval/py/kernel.d.ts +6 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -5
  16. package/dist/types/eval/session-id.d.ts +3 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +1 -3
  18. package/dist/types/hashline/anchors.d.ts +15 -9
  19. package/dist/types/hashline/constants.d.ts +0 -2
  20. package/dist/types/hashline/diff.d.ts +1 -2
  21. package/dist/types/hashline/executor.d.ts +52 -0
  22. package/dist/types/hashline/hash.d.ts +44 -93
  23. package/dist/types/hashline/index.d.ts +2 -1
  24. package/dist/types/hashline/input.d.ts +2 -9
  25. package/dist/types/hashline/recovery.d.ts +3 -9
  26. package/dist/types/hashline/tokenizer.d.ts +91 -0
  27. package/dist/types/hashline/types.d.ts +5 -7
  28. package/dist/types/modes/components/extensions/types.d.ts +0 -4
  29. package/dist/types/modes/types.d.ts +1 -0
  30. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  31. package/dist/types/sdk.d.ts +2 -0
  32. package/dist/types/session/agent-session.d.ts +11 -15
  33. package/dist/types/session/agent-storage.d.ts +11 -10
  34. package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
  35. package/dist/types/slash-commands/types.d.ts +0 -5
  36. package/dist/types/task/executor.d.ts +2 -0
  37. package/dist/types/tool-discovery/tool-index.d.ts +0 -50
  38. package/dist/types/tools/index.d.ts +2 -8
  39. package/dist/types/tools/match-line-format.d.ts +4 -4
  40. package/dist/types/tools/output-schema-validator.d.ts +64 -0
  41. package/dist/types/tools/review.d.ts +13 -0
  42. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  43. package/dist/types/tools/search.d.ts +4 -3
  44. package/dist/types/utils/edit-mode.d.ts +1 -1
  45. package/dist/types/web/kagi.d.ts +4 -2
  46. package/dist/types/web/parallel.d.ts +4 -3
  47. package/dist/types/web/scrapers/types.d.ts +2 -1
  48. package/dist/types/web/search/index.d.ts +12 -4
  49. package/dist/types/web/search/provider.d.ts +2 -1
  50. package/dist/types/web/search/providers/anthropic.d.ts +9 -4
  51. package/dist/types/web/search/providers/base.d.ts +34 -2
  52. package/dist/types/web/search/providers/brave.d.ts +8 -1
  53. package/dist/types/web/search/providers/codex.d.ts +13 -9
  54. package/dist/types/web/search/providers/exa.d.ts +10 -1
  55. package/dist/types/web/search/providers/gemini.d.ts +20 -23
  56. package/dist/types/web/search/providers/jina.d.ts +2 -1
  57. package/dist/types/web/search/providers/kagi.d.ts +4 -1
  58. package/dist/types/web/search/providers/kimi.d.ts +10 -1
  59. package/dist/types/web/search/providers/parallel.d.ts +3 -2
  60. package/dist/types/web/search/providers/perplexity.d.ts +5 -2
  61. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  62. package/dist/types/web/search/providers/synthetic.d.ts +5 -8
  63. package/dist/types/web/search/providers/tavily.d.ts +11 -4
  64. package/dist/types/web/search/providers/utils.d.ts +8 -6
  65. package/dist/types/web/search/providers/zai.d.ts +12 -3
  66. package/package.json +7 -7
  67. package/src/cli/file-processor.ts +12 -2
  68. package/src/cli.ts +0 -8
  69. package/src/commands/commit.ts +8 -8
  70. package/src/config/prompt-templates.ts +6 -6
  71. package/src/config/settings-schema.ts +47 -3
  72. package/src/config/settings.ts +5 -5
  73. package/src/debug/raw-sse.ts +68 -3
  74. package/src/edit/file-read-cache.ts +68 -25
  75. package/src/edit/index.ts +6 -37
  76. package/src/edit/renderer.ts +9 -47
  77. package/src/edit/streaming.ts +43 -56
  78. package/src/eval/__tests__/shared-executors.test.ts +520 -0
  79. package/src/eval/js/context-manager.ts +64 -53
  80. package/src/eval/js/shared/local-module-loader.ts +265 -0
  81. package/src/eval/js/shared/prelude.txt +4 -0
  82. package/src/eval/js/shared/rewrite-imports.ts +85 -0
  83. package/src/eval/js/shared/runtime.ts +129 -86
  84. package/src/eval/js/worker-core.ts +23 -38
  85. package/src/eval/py/executor.ts +155 -84
  86. package/src/eval/py/kernel.ts +10 -1
  87. package/src/eval/py/prelude.py +22 -24
  88. package/src/eval/py/runner.py +203 -85
  89. package/src/eval/py/tool-bridge.ts +17 -10
  90. package/src/eval/session-id.ts +8 -0
  91. package/src/exec/bash-executor.ts +27 -16
  92. package/src/extensibility/extensions/runner.ts +0 -1
  93. package/src/extensibility/extensions/types.ts +1 -3
  94. package/src/hashline/anchors.ts +56 -65
  95. package/src/hashline/apply.ts +29 -31
  96. package/src/hashline/constants.ts +0 -3
  97. package/src/hashline/diff-preview.ts +4 -5
  98. package/src/hashline/diff.ts +30 -4
  99. package/src/hashline/execute.ts +91 -26
  100. package/src/hashline/executor.ts +239 -0
  101. package/src/hashline/grammar.lark +12 -10
  102. package/src/hashline/hash.ts +69 -114
  103. package/src/hashline/index.ts +2 -1
  104. package/src/hashline/input.ts +48 -41
  105. package/src/hashline/prefixes.ts +21 -11
  106. package/src/hashline/recovery.ts +63 -71
  107. package/src/hashline/stream.ts +2 -2
  108. package/src/hashline/tokenizer.ts +467 -0
  109. package/src/hashline/types.ts +6 -8
  110. package/src/internal-urls/docs-index.generated.ts +7 -7
  111. package/src/modes/components/extensions/types.ts +0 -5
  112. package/src/modes/components/session-observer-overlay.ts +11 -2
  113. package/src/modes/components/settings-selector.ts +10 -1
  114. package/src/modes/components/tree-selector.ts +10 -2
  115. package/src/modes/controllers/command-controller.ts +1 -3
  116. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  117. package/src/modes/controllers/selector-controller.ts +5 -5
  118. package/src/modes/theme/theme.ts +4 -2
  119. package/src/modes/types.ts +4 -1
  120. package/src/modes/utils/ui-helpers.ts +4 -0
  121. package/src/prompts/agents/explore.md +1 -1
  122. package/src/prompts/tools/ast-edit.md +1 -1
  123. package/src/prompts/tools/ast-grep.md +1 -1
  124. package/src/prompts/tools/eval.md +1 -1
  125. package/src/prompts/tools/hashline.md +73 -94
  126. package/src/prompts/tools/read.md +4 -4
  127. package/src/prompts/tools/search.md +3 -3
  128. package/src/sdk.ts +33 -26
  129. package/src/session/agent-session.ts +59 -66
  130. package/src/session/agent-storage.ts +13 -14
  131. package/src/slash-commands/acp-builtins.ts +3 -3
  132. package/src/slash-commands/types.ts +0 -6
  133. package/src/task/executor.ts +26 -57
  134. package/src/task/index.ts +8 -4
  135. package/src/tool-discovery/tool-index.ts +0 -134
  136. package/src/tools/ast-edit.ts +36 -13
  137. package/src/tools/ast-grep.ts +45 -4
  138. package/src/tools/browser/tab-worker.ts +3 -2
  139. package/src/tools/eval.ts +2 -1
  140. package/src/tools/fetch.ts +23 -14
  141. package/src/tools/index.ts +2 -8
  142. package/src/tools/irc.ts +59 -5
  143. package/src/tools/match-line-format.ts +5 -7
  144. package/src/tools/output-schema-validator.ts +132 -0
  145. package/src/tools/read.ts +142 -31
  146. package/src/tools/review.ts +23 -0
  147. package/src/tools/search-tool-bm25.ts +3 -30
  148. package/src/tools/search.ts +48 -16
  149. package/src/tools/write.ts +3 -3
  150. package/src/tools/yield.ts +32 -41
  151. package/src/utils/edit-mode.ts +1 -2
  152. package/src/utils/file-mentions.ts +2 -2
  153. package/src/web/kagi.ts +15 -6
  154. package/src/web/parallel.ts +9 -6
  155. package/src/web/scrapers/types.ts +7 -1
  156. package/src/web/scrapers/youtube.ts +13 -7
  157. package/src/web/search/index.ts +37 -11
  158. package/src/web/search/provider.ts +5 -3
  159. package/src/web/search/providers/anthropic.ts +30 -21
  160. package/src/web/search/providers/base.ts +35 -2
  161. package/src/web/search/providers/brave.ts +4 -4
  162. package/src/web/search/providers/codex.ts +118 -89
  163. package/src/web/search/providers/exa.ts +3 -2
  164. package/src/web/search/providers/gemini.ts +58 -155
  165. package/src/web/search/providers/jina.ts +4 -4
  166. package/src/web/search/providers/kagi.ts +17 -11
  167. package/src/web/search/providers/kimi.ts +29 -13
  168. package/src/web/search/providers/parallel.ts +171 -23
  169. package/src/web/search/providers/perplexity.ts +38 -37
  170. package/src/web/search/providers/searxng.ts +3 -1
  171. package/src/web/search/providers/synthetic.ts +16 -19
  172. package/src/web/search/providers/tavily.ts +23 -18
  173. package/src/web/search/providers/utils.ts +11 -17
  174. package/src/web/search/providers/zai.ts +16 -8
  175. package/dist/types/hashline/parser.d.ts +0 -7
  176. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  177. package/dist/types/tools/vim.d.ts +0 -58
  178. package/dist/types/vim/buffer.d.ts +0 -41
  179. package/dist/types/vim/commands.d.ts +0 -6
  180. package/dist/types/vim/engine.d.ts +0 -47
  181. package/dist/types/vim/parser.d.ts +0 -3
  182. package/dist/types/vim/render.d.ts +0 -25
  183. package/dist/types/vim/types.d.ts +0 -182
  184. package/src/hashline/parser.ts +0 -246
  185. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  186. package/src/prompts/tools/vim.md +0 -98
  187. package/src/tools/vim.ts +0 -949
  188. package/src/vim/buffer.ts +0 -309
  189. package/src/vim/commands.ts +0 -382
  190. package/src/vim/engine.ts +0 -2409
  191. package/src/vim/parser.ts +0 -134
  192. package/src/vim/render.ts +0 -252
  193. package/src/vim/types.ts +0 -197
@@ -8,15 +8,13 @@ import type { TSchema } from "@oh-my-pi/pi-ai/types";
8
8
  import {
9
9
  dereferenceJsonSchema,
10
10
  isValidJsonSchema,
11
- type JsonSchemaValidationIssue,
12
11
  type JsonSchemaValidationResult,
13
12
  sanitizeSchemaForStrictMode,
14
13
  tryEnforceStrictSchema,
15
- validateJsonSchemaValue,
16
14
  } from "@oh-my-pi/pi-ai/utils/schema";
17
15
  import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
18
16
  import type { ToolSession } from ".";
19
- import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
17
+ import { buildOutputValidator, formatAllValidationIssues } from "./output-schema-validator";
20
18
 
21
19
  export interface YieldDetails {
22
20
  data: unknown;
@@ -34,16 +32,6 @@ function formatSchema(schema: unknown): string {
34
32
  }
35
33
  }
36
34
 
37
- function formatJsonSchemaIssues(issues: ReadonlyArray<JsonSchemaValidationIssue> | undefined): string {
38
- if (!issues || issues.length === 0) return "Unknown schema validation error.";
39
- return issues
40
- .map(issue => {
41
- const path = issue.path.length === 0 ? "" : `${issue.path.map(seg => String(seg)).join("/")}: `;
42
- return `${path}${issue.message}`;
43
- })
44
- .join("; ");
45
- }
46
-
47
35
  function looseRecordSchema(description: string): Record<string, unknown> {
48
36
  return {
49
37
  type: "object",
@@ -100,6 +88,15 @@ function wrapYieldParameters(dataSchema: Record<string, unknown>): Record<string
100
88
  };
101
89
  }
102
90
 
91
+ /**
92
+ * Max consecutive schema-validation failures before the yield tool overrides validation
93
+ * and lets non-conforming data through. The override is a safety net for schemas the
94
+ * JTD→JSON-Schema converter cannot fully express; it should not be reached during normal
95
+ * model retries. Three matches the existing "3 reminders" pattern elsewhere in the agent
96
+ * runtime.
97
+ */
98
+ const MAX_SCHEMA_RETRIES = 3;
99
+
103
100
  export class YieldTool implements AgentTool<TSchema, YieldDetails> {
104
101
  readonly name = "yield";
105
102
  readonly label = "Submit Result";
@@ -120,21 +117,14 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
120
117
  let parameters: TSchema;
121
118
 
122
119
  try {
123
- const schemaResult = normalizeSchema(session.outputSchema);
124
- const normalizedSchema =
125
- schemaResult.normalized !== undefined ? jtdToJsonSchema(schemaResult.normalized) : undefined;
126
- let schemaError = schemaResult.error;
127
-
128
- if (!schemaError && normalizedSchema === false) {
129
- schemaError = "boolean false schema rejects all outputs";
130
- }
131
-
132
- if (normalizedSchema !== undefined && normalizedSchema !== false && !schemaError) {
133
- if (!isValidJsonSchema(normalizedSchema)) {
134
- schemaError = "invalid JSON schema";
135
- } else {
136
- validate = value => validateJsonSchemaValue(normalizedSchema, value);
137
- }
120
+ const {
121
+ validator,
122
+ jsonSchema: normalizedSchema,
123
+ normalized,
124
+ error: schemaError,
125
+ } = buildOutputValidator(session.outputSchema);
126
+ if (validator) {
127
+ validate = value => validator.validate(value);
138
128
  }
139
129
 
140
130
  const schemaHint = formatSchema(normalizedSchema ?? session.outputSchema);
@@ -142,21 +132,15 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
142
132
  ? `Structured JSON output (output schema invalid; accepting unconstrained object): ${schemaError}`
143
133
  : `Structured output matching the schema:\n${schemaHint}`;
144
134
  let sanitizedSchema: Record<string, unknown> | undefined;
145
- if (
146
- !schemaError &&
147
- normalizedSchema != null &&
148
- typeof normalizedSchema === "object" &&
149
- !Array.isArray(normalizedSchema)
150
- ) {
151
- const normalizedRecord = normalizedSchema as Record<string, unknown>;
152
- const strictProbe = tryEnforceStrictSchema(normalizedRecord);
135
+ if (!schemaError && normalizedSchema !== undefined) {
136
+ const strictProbe = tryEnforceStrictSchema(normalizedSchema);
153
137
  if (strictProbe.strict) {
154
- sanitizedSchema = sanitizeSchemaForStrictMode(normalizedRecord);
138
+ sanitizedSchema = sanitizeSchemaForStrictMode(normalizedSchema);
155
139
  } else {
156
- sanitizedSchema = normalizedRecord;
140
+ sanitizedSchema = normalizedSchema;
157
141
  this.strict = false;
158
142
  }
159
- } else if (!schemaError && normalizedSchema === true) {
143
+ } else if (!schemaError && normalized === true) {
160
144
  sanitizedSchema = {};
161
145
  this.strict = false;
162
146
  }
@@ -229,8 +213,15 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
229
213
  const parsed = this.#validate(data);
230
214
  if (!parsed.success) {
231
215
  this.#schemaValidationFailures++;
232
- if (this.#schemaValidationFailures <= 1) {
233
- throw new Error(`Output does not match schema: ${formatJsonSchemaIssues(parsed.issues)}`);
216
+ if (this.#schemaValidationFailures <= MAX_SCHEMA_RETRIES) {
217
+ const remaining = MAX_SCHEMA_RETRIES - this.#schemaValidationFailures;
218
+ const retryHint =
219
+ remaining > 0
220
+ ? ` Call yield again with the corrected shape — ${remaining} retry attempt(s) remain before the schema constraint is dropped.`
221
+ : " Call yield again with the corrected shape — this is the final retry before the schema constraint is dropped.";
222
+ throw new Error(
223
+ `Output does not match schema: ${formatAllValidationIssues(parsed.issues)}.${retryHint}`,
224
+ );
234
225
  }
235
226
  schemaValidationOverridden = true;
236
227
  }
@@ -1,6 +1,6 @@
1
1
  import { $env } from "@oh-my-pi/pi-utils";
2
2
 
3
- export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
3
+ export type EditMode = "replace" | "patch" | "hashline" | "apply_patch";
4
4
 
5
5
  export const DEFAULT_EDIT_MODE: EditMode = "hashline";
6
6
 
@@ -9,7 +9,6 @@ const EDIT_MODE_IDS = {
9
9
  hashline: "hashline",
10
10
  patch: "patch",
11
11
  replace: "replace",
12
- vim: "vim",
13
12
  } as const satisfies Record<string, EditMode>;
14
13
 
15
14
  export const EDIT_MODES = Object.keys(EDIT_MODE_IDS) as EditMode[];
@@ -12,7 +12,7 @@ import type { ImageContent } from "@oh-my-pi/pi-ai";
12
12
  import { glob } from "@oh-my-pi/pi-natives";
13
13
  import { fuzzyMatch } from "@oh-my-pi/pi-tui";
14
14
  import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
15
- import { formatHashLines } from "../hashline/hash";
15
+ import { computeFileHash, formatHashlineHeader, formatNumberedLines } from "../hashline/hash";
16
16
  import type { FileMentionMessage } from "../session/messages";
17
17
  import {
18
18
  DEFAULT_MAX_BYTES,
@@ -356,7 +356,7 @@ export async function generateFileMentionMessages(
356
356
  const content = await Bun.file(absolutePath).text();
357
357
  let { output, lineCount } = buildTextOutput(content);
358
358
  if (options?.useHashLines) {
359
- output = formatHashLines(output);
359
+ output = `${formatHashlineHeader(resolvedPath, computeFileHash(content))}\n${formatNumberedLines(output)}`;
360
360
  }
361
361
  files.push({ path: resolvedPath, content: output, lineCount });
362
362
  } catch {
package/src/web/kagi.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { getEnvApiKey } from "@oh-my-pi/pi-ai";
2
- import { findCredential, withHardTimeout } from "./search/providers/utils";
1
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
2
+ import { withHardTimeout } from "./search/providers/utils";
3
3
 
4
4
  const KAGI_SEARCH_URL = "https://kagi.com/api/v0/search";
5
5
 
@@ -97,6 +97,7 @@ function parseKagiErrorResponse(statusCode: number, responseText: string): KagiA
97
97
 
98
98
  export interface KagiSearchOptions {
99
99
  limit?: number;
100
+ sessionId?: string;
100
101
  signal?: AbortSignal;
101
102
  }
102
103
 
@@ -113,8 +114,12 @@ export interface KagiSearchResult {
113
114
  relatedQuestions: string[];
114
115
  }
115
116
 
116
- export async function findKagiApiKey(): Promise<string | null> {
117
- return findCredential(getEnvApiKey("kagi"), "kagi");
117
+ export async function findKagiApiKey(
118
+ authStorage: AuthStorage,
119
+ sessionId?: string,
120
+ signal?: AbortSignal,
121
+ ): Promise<string | null> {
122
+ return (await authStorage.getApiKey("kagi", sessionId, { signal })) ?? null;
118
123
  }
119
124
 
120
125
  function getAuthHeaders(apiKey: string): Record<string, string> {
@@ -124,8 +129,12 @@ function getAuthHeaders(apiKey: string): Record<string, string> {
124
129
  };
125
130
  }
126
131
 
127
- export async function searchWithKagi(query: string, options: KagiSearchOptions = {}): Promise<KagiSearchResult> {
128
- const apiKey = await findKagiApiKey();
132
+ export async function searchWithKagi(
133
+ query: string,
134
+ options: KagiSearchOptions = {},
135
+ authStorage: AuthStorage,
136
+ ): Promise<KagiSearchResult> {
137
+ const apiKey = await findKagiApiKey(authStorage, options.sessionId, options.signal);
129
138
  if (!apiKey) {
130
139
  throw new KagiApiError("Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.");
131
140
  }
@@ -1,4 +1,5 @@
1
1
  import { getEnvApiKey } from "@oh-my-pi/pi-ai";
2
+ import type { AgentStorage } from "../session/agent-storage";
2
3
  import { findCredential, withHardTimeout } from "./search/providers/utils";
3
4
 
4
5
  const PARALLEL_API_URL = "https://api.parallel.ai";
@@ -73,8 +74,8 @@ export class ParallelApiError extends Error {
73
74
  }
74
75
  }
75
76
 
76
- export async function findParallelApiKey(): Promise<string | null> {
77
- return findCredential(getEnvApiKey("parallel"), "parallel");
77
+ export function findParallelApiKey(storage: AgentStorage | null | undefined): string | null {
78
+ return findCredential(storage, getEnvApiKey("parallel"), "parallel");
78
79
  }
79
80
 
80
81
  export function getParallelExtractContent(document: ParallelExtractDocument): string {
@@ -284,9 +285,10 @@ function parseExtractPayload(payload: unknown): ParallelExtractResult {
284
285
  export async function searchWithParallel(
285
286
  objective: string,
286
287
  queries: string[],
287
- options: ParallelSearchOptions = {},
288
+ options: ParallelSearchOptions,
289
+ storage: AgentStorage | null | undefined,
288
290
  ): Promise<ParallelSearchResult> {
289
- const apiKey = await findParallelApiKey();
291
+ const apiKey = findParallelApiKey(storage);
290
292
  if (!apiKey) {
291
293
  throw new ParallelApiError(
292
294
  "Parallel credentials not found. Set PARALLEL_API_KEY or login with 'omp /login parallel'.",
@@ -316,9 +318,10 @@ export async function searchWithParallel(
316
318
 
317
319
  export async function extractWithParallel(
318
320
  urls: string[],
319
- options: ParallelExtractOptions = {},
321
+ options: ParallelExtractOptions,
322
+ storage: AgentStorage | null | undefined,
320
323
  ): Promise<ParallelExtractResult> {
321
- const apiKey = await findParallelApiKey();
324
+ const apiKey = findParallelApiKey(storage);
322
325
  if (!apiKey) {
323
326
  throw new ParallelApiError(
324
327
  "Parallel credentials not found. Set PARALLEL_API_KEY or login with 'omp /login parallel'.",
@@ -4,6 +4,7 @@
4
4
  import { ptree } from "@oh-my-pi/pi-utils";
5
5
  import type TurndownService from "turndown";
6
6
 
7
+ import type { AgentStorage } from "../../session/agent-storage";
7
8
  import { ToolAbortError } from "../../tools/tool-errors";
8
9
 
9
10
  export { formatNumber } from "@oh-my-pi/pi-utils";
@@ -19,7 +20,12 @@ export interface RenderResult {
19
20
  notes: string[];
20
21
  }
21
22
 
22
- export type SpecialHandler = (url: string, timeout: number, signal?: AbortSignal) => Promise<RenderResult | null>;
23
+ export type SpecialHandler = (
24
+ url: string,
25
+ timeout: number,
26
+ signal?: AbortSignal,
27
+ storage?: AgentStorage | null,
28
+ ) => Promise<RenderResult | null>;
23
29
 
24
30
  export const MAX_OUTPUT_CHARS = 500_000;
25
31
  export const MAX_BYTES = 50 * 1024 * 1024;
@@ -3,6 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { ptree, Snowflake } from "@oh-my-pi/pi-utils";
5
5
  import { settings } from "../../config/settings";
6
+ import type { AgentStorage } from "../../session/agent-storage";
6
7
  import { throwIfAborted } from "../../tools/tool-errors";
7
8
  import { ensureTool } from "../../utils/tools-manager";
8
9
  import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../parallel";
@@ -101,6 +102,7 @@ export const handleYouTube: SpecialHandler = async (
101
102
  url: string,
102
103
  timeout: number,
103
104
  userSignal?: AbortSignal,
105
+ storage?: AgentStorage | null,
104
106
  ): Promise<RenderResult | null> => {
105
107
  throwIfAborted(userSignal);
106
108
  const yt = parseYouTubeUrl(url);
@@ -112,14 +114,18 @@ export const handleYouTube: SpecialHandler = async (
112
114
  const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
113
115
 
114
116
  // Prefer Parallel extract when credentials are available
115
- if (settings.get("providers.parallelFetch") && (await findParallelApiKey())) {
117
+ if (settings.get("providers.parallelFetch") && findParallelApiKey(storage)) {
116
118
  try {
117
- const parallelResult = await extractWithParallel([videoUrl], {
118
- objective: "Extract the main content of this YouTube video page",
119
- excerpts: true,
120
- fullContent: false,
121
- signal,
122
- });
119
+ const parallelResult = await extractWithParallel(
120
+ [videoUrl],
121
+ {
122
+ objective: "Extract the main content of this YouTube video page",
123
+ excerpts: true,
124
+ fullContent: false,
125
+ signal,
126
+ },
127
+ storage,
128
+ );
123
129
  const firstDocument = parallelResult.results[0];
124
130
  if (firstDocument) {
125
131
  const content = getParallelExtractContent(firstDocument);
@@ -3,15 +3,16 @@
3
3
  *
4
4
  * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Tavily, Kagi, Z.AI, SearXNG, and Synthetic
5
5
  * providers with provider-specific parameters exposed conditionally.
6
- *
7
6
  */
8
7
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
8
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
9
9
  import { prompt } from "@oh-my-pi/pi-utils";
10
10
  import * as z from "zod/v4";
11
11
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
12
12
  import type { Theme } from "../../modes/theme/theme";
13
13
  import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { type: "text" };
14
14
  import webSearchDescription from "../../prompts/tools/web-search.md" with { type: "text" };
15
+ import { discoverAuthStorage } from "../../sdk";
15
16
  import type { ToolSession } from "../../tools";
16
17
  import { formatAge } from "../../tools/render-utils";
17
18
  import { throwIfAborted } from "../../tools/tool-errors";
@@ -114,18 +115,25 @@ function formatForLLM(response: SearchResponse): string {
114
115
  return parts.join("\n");
115
116
  }
116
117
 
118
+ interface ExecuteSearchOptions {
119
+ authStorage: AuthStorage;
120
+ sessionId?: string;
121
+ signal?: AbortSignal;
122
+ }
123
+
117
124
  /** Execute web search */
118
125
  async function executeSearch(
119
126
  _toolCallId: string,
120
127
  params: SearchQueryParams,
121
- signal?: AbortSignal,
128
+ options: ExecuteSearchOptions,
122
129
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
130
+ const { authStorage, sessionId, signal } = options;
123
131
  const providers =
124
132
  params.provider && params.provider !== "auto"
125
- ? await getSearchProvider(params.provider).then(provider =>
126
- provider.isAvailable() ? [provider] : resolveProviderChain("auto"),
133
+ ? await getSearchProvider(params.provider).then(async provider =>
134
+ (await provider.isAvailable(authStorage)) ? [provider] : resolveProviderChain(authStorage, "auto"),
127
135
  )
128
- : await resolveProviderChain();
136
+ : await resolveProviderChain(authStorage);
129
137
  if (providers.length === 0) {
130
138
  const message = "No web search provider configured.";
131
139
  return {
@@ -148,6 +156,8 @@ async function executeSearch(
148
156
  numSearchResults: params.num_search_results,
149
157
  temperature: params.temperature,
150
158
  signal,
159
+ authStorage,
160
+ sessionId,
151
161
  });
152
162
 
153
163
  const text = formatForLLM(response);
@@ -190,18 +200,27 @@ async function executeSearch(
190
200
 
191
201
  /**
192
202
  * Execute a web search query for CLI/testing workflows.
203
+ *
204
+ * `authStorage` may be omitted; in that case we discover one via the standard
205
+ * factory (`discoverAuthStorage`), which honours `OMP_AUTH_BROKER_URL` and
206
+ * otherwise opens the local SQLite credential store.
193
207
  */
194
208
  export async function runSearchQuery(
195
209
  params: SearchQueryParams,
210
+ options: { authStorage?: AuthStorage; sessionId?: string; signal?: AbortSignal } = {},
196
211
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
197
- return executeSearch("cli-web-search", params);
212
+ const authStorage = options.authStorage ?? (await discoverAuthStorage());
213
+ return executeSearch("cli-web-search", params, {
214
+ authStorage,
215
+ sessionId: options.sessionId,
216
+ signal: options.signal,
217
+ });
198
218
  }
199
219
 
200
220
  /**
201
221
  * Web search tool implementation.
202
222
  *
203
223
  * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, SearXNG, and Synthetic providers with automatic fallback.
204
- * Session is accepted for interface consistency but not used.
205
224
  */
206
225
  export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
207
226
  readonly name = "web_search";
@@ -212,7 +231,10 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
212
231
  readonly loadMode = "discoverable";
213
232
  readonly summary = "Search the web for up-to-date information";
214
233
 
215
- constructor(_session: ToolSession) {
234
+ #session: ToolSession;
235
+
236
+ constructor(session: ToolSession) {
237
+ this.#session = session;
216
238
  this.description = prompt.render(webSearchDescription);
217
239
  }
218
240
 
@@ -223,7 +245,9 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
223
245
  _onUpdate?: AgentToolUpdateCallback<SearchRenderDetails>,
224
246
  _context?: AgentToolContext,
225
247
  ): Promise<AgentToolResult<SearchRenderDetails>> {
226
- return executeSearch(_toolCallId, params, signal);
248
+ const authStorage = this.#session.authStorage ?? (await discoverAuthStorage());
249
+ const sessionId = this.#session.getSessionId?.() ?? undefined;
250
+ return executeSearch(_toolCallId, params, { authStorage, sessionId, signal });
227
251
  }
228
252
  }
229
253
 
@@ -238,10 +262,12 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
238
262
  toolCallId: string,
239
263
  params: SearchToolParams,
240
264
  _onUpdate,
241
- _ctx: CustomToolContext,
265
+ ctx: CustomToolContext,
242
266
  signal?: AbortSignal,
243
267
  ) {
244
- return executeSearch(toolCallId, params, signal);
268
+ const authStorage = ctx.modelRegistry?.authStorage ?? (await discoverAuthStorage());
269
+ const sessionId = ctx.sessionManager.getSessionId();
270
+ return executeSearch(toolCallId, params, { authStorage, sessionId, signal });
245
271
  },
246
272
 
247
273
  renderCall(args: SearchToolParams, options: RenderResultOptions, theme: Theme) {
@@ -8,6 +8,7 @@
8
8
  // The `label`/`id` metadata is kept inline so callers needing a display name
9
9
  // (error formatting, UI listings) do not force a load.
10
10
 
11
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
11
12
  import type { SearchProvider } from "./providers/base";
12
13
  import type { SearchProviderId } from "./types";
13
14
 
@@ -64,7 +65,7 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
64
65
  },
65
66
  codex: {
66
67
  id: "codex",
67
- label: "Codex",
68
+ label: "OpenAI",
68
69
  load: async () => new (await import("./providers/codex")).CodexProvider(),
69
70
  },
70
71
  tavily: {
@@ -148,13 +149,14 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"):
148
149
  * is walked, so unconfigured providers never pay the load cost.
149
150
  */
150
151
  export async function resolveProviderChain(
152
+ authStorage: AuthStorage,
151
153
  preferredProvider: SearchProviderId | "auto" = preferredProvId,
152
154
  ): Promise<SearchProvider[]> {
153
155
  const providers: SearchProvider[] = [];
154
156
 
155
157
  if (preferredProvider !== "auto") {
156
158
  const provider = await getSearchProvider(preferredProvider);
157
- if (await provider.isAvailable()) {
159
+ if (await provider.isAvailable(authStorage)) {
158
160
  providers.push(provider);
159
161
  }
160
162
  }
@@ -162,7 +164,7 @@ export async function resolveProviderChain(
162
164
  for (const id of SEARCH_PROVIDER_ORDER) {
163
165
  if (id === preferredProvider) continue;
164
166
  const provider = await getSearchProvider(id);
165
- if (await provider.isAvailable()) {
167
+ if (await provider.isAvailable(authStorage)) {
166
168
  providers.push(provider);
167
169
  }
168
170
  }
@@ -7,10 +7,11 @@
7
7
  import {
8
8
  type AnthropicAuthConfig,
9
9
  type AnthropicSystemBlock,
10
+ type AuthStorage,
11
+ buildAnthropicAuthConfig,
10
12
  buildAnthropicSearchHeaders,
11
13
  buildAnthropicSystemBlocks,
12
14
  buildAnthropicUrl,
13
- findAnthropicAuth,
14
15
  stripClaudeToolPrefix,
15
16
  } from "@oh-my-pi/pi-ai";
16
17
  import { $env } from "@oh-my-pi/pi-utils";
@@ -34,9 +35,7 @@ export interface AnthropicSearchParams {
34
35
  query: string;
35
36
  system_prompt?: string;
36
37
  num_results?: number;
37
- /** Maximum output tokens. Defaults to 4096. */
38
38
  max_tokens?: number;
39
- /** Sampling temperature (0–1). Lower = more focused/factual. */
40
39
  temperature?: number;
41
40
  signal?: AbortSignal;
42
41
  }
@@ -242,30 +241,47 @@ function parseResponse(response: AnthropicApiResponse): SearchResponse {
242
241
  * @returns Search response with synthesized answer, sources, and citations
243
242
  * @throws {Error} If no Anthropic credentials are configured
244
243
  */
245
- export async function searchAnthropic(params: AnthropicSearchParams): Promise<SearchResponse> {
246
- const auth = await findAnthropicAuth();
244
+ export async function searchAnthropic(
245
+ params: SearchParams | AnthropicSearchParams,
246
+ _legacyStorage?: unknown,
247
+ ): Promise<SearchResponse> {
248
+ const searchApiKey = $env.ANTHROPIC_SEARCH_API_KEY;
249
+ const searchBaseUrl = $env.ANTHROPIC_SEARCH_BASE_URL;
250
+ let auth: AnthropicAuthConfig | undefined;
251
+
252
+ if (searchApiKey) {
253
+ auth = buildAnthropicAuthConfig(searchApiKey, searchBaseUrl);
254
+ } else if ("authStorage" in params) {
255
+ const apiKey = await params.authStorage.getApiKey("anthropic", params.sessionId, {
256
+ signal: params.signal,
257
+ });
258
+ if (apiKey) auth = buildAnthropicAuthConfig(apiKey);
259
+ }
260
+
247
261
  if (!auth) {
248
262
  throw new Error(
249
- "No Anthropic credentials found. Set ANTHROPIC_API_KEY or configure OAuth in ~/.omp/agent/agent.db",
263
+ "No Anthropic credentials found. Set ANTHROPIC_SEARCH_API_KEY or ANTHROPIC_API_KEY, or configure Anthropic OAuth.",
250
264
  );
251
265
  }
252
266
 
253
267
  const model = getModel();
268
+ const systemPrompt = "authStorage" in params ? params.systemPrompt : params.system_prompt;
269
+ const maxTokens = "authStorage" in params ? params.maxOutputTokens : params.max_tokens;
254
270
  const response = await callSearch(
255
271
  auth,
256
272
  model,
257
273
  params.query,
258
- params.system_prompt,
259
- params.max_tokens,
274
+ systemPrompt,
275
+ maxTokens,
260
276
  params.temperature,
261
277
  params.signal,
262
278
  );
263
279
 
264
280
  const result = parseResponse(response);
265
281
 
266
- // Apply num_results limit if specified
267
- if (params.num_results && result.sources.length > params.num_results) {
268
- result.sources = result.sources.slice(0, params.num_results);
282
+ const numResults = "authStorage" in params ? (params.numSearchResults ?? params.limit) : params.num_results;
283
+ if (numResults && result.sources.length > numResults) {
284
+ result.sources = result.sources.slice(0, numResults);
269
285
  }
270
286
 
271
287
  return result;
@@ -276,18 +292,11 @@ export class AnthropicProvider extends SearchProvider {
276
292
  readonly id = "anthropic";
277
293
  readonly label = "Anthropic";
278
294
 
279
- isAvailable() {
280
- return findAnthropicAuth().then(Boolean);
295
+ isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean {
296
+ return Boolean($env.ANTHROPIC_SEARCH_API_KEY) || authStorage.hasAuth("anthropic");
281
297
  }
282
298
 
283
299
  search(params: SearchParams): Promise<SearchResponse> {
284
- return searchAnthropic({
285
- query: params.query,
286
- system_prompt: params.systemPrompt,
287
- num_results: params.numSearchResults ?? params.limit,
288
- max_tokens: params.maxOutputTokens,
289
- temperature: params.temperature,
290
- signal: params.signal,
291
- });
300
+ return searchAnthropic(params);
292
301
  }
293
302
  }
@@ -1,6 +1,16 @@
1
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
1
2
  import type { SearchProviderId, SearchResponse } from "../types";
2
3
 
3
- /** Shared web search parameters passed to providers. */
4
+ /**
5
+ * Shared web search parameters passed to providers.
6
+ *
7
+ * `authStorage` is the **only** credential source providers may consult.
8
+ * Opening a sibling SQLite handle or calling provider-direct refresh helpers
9
+ * (e.g. `refreshOpenAICodexToken`, `refreshGoogleCloudToken`) is prohibited:
10
+ * it races the broker's per-credential refresh and POSTs the broker sentinel
11
+ * (`REMOTE_REFRESH_SENTINEL`) to the upstream token endpoint, which classifies
12
+ * as `invalid_grant` and disables the row.
13
+ */
4
14
  export interface SearchParams {
5
15
  query: string;
6
16
  limit?: number;
@@ -26,6 +36,20 @@ export interface SearchParams {
26
36
  googleSearch?: Record<string, unknown>;
27
37
  codeExecution?: Record<string, unknown>;
28
38
  urlContext?: Record<string, unknown>;
39
+ /**
40
+ * The single source of truth for credentials. Providers MUST consult this
41
+ * handle exclusively (`getApiKey` for bearer-style auth, `getOAuthAccess`
42
+ * when identity metadata is required). Do not open `AgentStorage` or any
43
+ * `AuthCredentialStore` directly — that bypasses the broker pipeline and
44
+ * the per-credential single-flight refresh.
45
+ */
46
+ authStorage: AuthStorage;
47
+ /**
48
+ * Optional session id used as the round-robin / sticky key when selecting
49
+ * among multiple credentials for the same provider. Pass through from the
50
+ * caller's agent session when available; otherwise omit.
51
+ */
52
+ sessionId?: string;
29
53
  }
30
54
 
31
55
  /** Base class for web search providers. */
@@ -33,6 +57,15 @@ export abstract class SearchProvider {
33
57
  abstract readonly id: SearchProviderId;
34
58
  abstract readonly label: string;
35
59
 
36
- abstract isAvailable(): Promise<boolean> | boolean;
60
+ /**
61
+ * Indicates whether this provider has the credentials/config it needs to
62
+ * service a request right now. Implementations consult the passed
63
+ * {@link AuthStorage} — never a sibling store.
64
+ */
65
+ abstract isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean;
66
+
67
+ /**
68
+ * Execute a search. Credentials MUST be resolved through `params.authStorage`.
69
+ */
37
70
  abstract search(params: SearchParams): Promise<SearchResponse>;
38
71
  }
@@ -4,13 +4,13 @@
4
4
  * Calls Brave's web search REST API and maps results into the unified
5
5
  * SearchResponse shape used by the web search tool.
6
6
  */
7
- import { getEnvApiKey } from "@oh-my-pi/pi-ai";
7
+ import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
8
8
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
9
9
  import { SearchProviderError } from "../../../web/search/types";
10
10
  import { clampNumResults, dateToAgeSeconds } from "../utils";
11
11
  import type { SearchParams } from "./base";
12
12
  import { SearchProvider } from "./base";
13
- import { classifyProviderHttpError, isApiKeyAvailable, withHardTimeout } from "./utils";
13
+ import { classifyProviderHttpError, withHardTimeout } from "./utils";
14
14
 
15
15
  const BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search";
16
16
  const DEFAULT_NUM_RESULTS = 10;
@@ -134,8 +134,8 @@ export class BraveProvider extends SearchProvider {
134
134
  readonly id = "brave";
135
135
  readonly label = "Brave";
136
136
 
137
- isAvailable() {
138
- return isApiKeyAvailable(findApiKey);
137
+ isAvailable(_authStorage: AuthStorage): boolean {
138
+ return !!findApiKey();
139
139
  }
140
140
 
141
141
  search(params: SearchParams): Promise<SearchResponse> {