@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
@@ -7,7 +7,6 @@
7
7
  import path from "node:path";
8
8
  import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
9
9
  import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
10
- import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@oh-my-pi/pi-ai/utils/schema";
11
10
  import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
12
11
  import { ModelRegistry } from "../config/model-registry";
13
12
  import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
@@ -33,7 +32,10 @@ import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
33
32
  import { SessionManager } from "../session/session-manager";
34
33
  import { truncateTail } from "../session/streaming-output";
35
34
  import type { ContextFileEntry } from "../tools";
36
- import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
35
+ import { normalizeSchema } from "../tools/jtd-to-json-schema";
36
+ import { buildOutputValidator, summarizeValidationFailure } from "../tools/output-schema-validator";
37
+
38
+ import { type ReportFindingDetails, toReviewFinding } from "../tools/review";
37
39
  import { ToolAbortError } from "../tools/tool-errors";
38
40
  import type { EventBus } from "../utils/event-bus";
39
41
  import { buildNamedToolChoice } from "../utils/tool-choice";
@@ -183,6 +185,8 @@ export interface ExecutorOptions {
183
185
  */
184
186
  parentArtifactManager?: ArtifactManager;
185
187
  parentHindsightSessionState?: HindsightSessionState;
188
+ /** Parent agent's eval executor session id. Subagents reuse it so eval state is shared. */
189
+ parentEvalSessionId?: string;
186
190
  /**
187
191
  * Parent agent's OpenTelemetry configuration. When defined, the subagent's
188
192
  * loop is started with the same tracer/hooks but its own agent identity
@@ -208,51 +212,6 @@ function parseStringifiedJson(value: unknown): unknown {
208
212
  }
209
213
  }
210
214
 
211
- interface OutputValidator {
212
- validate: (value: unknown) => { ok: true } | { ok: false; message: string; missingRequired: string[] };
213
- requiredFields: string[];
214
- }
215
-
216
- function buildOutputValidator(schema: unknown): { validator?: OutputValidator; error?: string } {
217
- const { normalized, error } = normalizeSchema(schema);
218
- if (error) return { error };
219
- if (normalized === undefined) return {};
220
- const jsonSchema = jtdToJsonSchema(normalized);
221
- const required = extractRequiredFields(jsonSchema);
222
- return {
223
- validator: {
224
- requiredFields: required,
225
- validate: value => {
226
- const result = validateJsonSchemaValue(jsonSchema, value);
227
- if (result.success) return { ok: true };
228
- const missing = computeMissingRequired(required, value);
229
- const message = formatValidationIssue(result.issues[0]) ?? "schema validation failed";
230
- return { ok: false, message, missingRequired: missing };
231
- },
232
- },
233
- };
234
- }
235
-
236
- function extractRequiredFields(jsonSchema: unknown): string[] {
237
- if (!jsonSchema || typeof jsonSchema !== "object") return [];
238
- const required = (jsonSchema as { required?: unknown }).required;
239
- return Array.isArray(required) ? required.filter((k): k is string => typeof k === "string") : [];
240
- }
241
-
242
- function computeMissingRequired(required: readonly string[], value: unknown): string[] {
243
- if (required.length === 0) return [];
244
- if (value === null || value === undefined) return [...required];
245
- if (typeof value !== "object" || Array.isArray(value)) return [];
246
- const record = value as Record<string, unknown>;
247
- return required.filter(key => !(key in record) || record[key] === undefined);
248
- }
249
-
250
- function formatValidationIssue(issue: JsonSchemaValidationIssue | undefined): string | undefined {
251
- if (!issue) return undefined;
252
- const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
253
- return `${path}: ${issue.message}`;
254
- }
255
-
256
215
  function previewOffendingData(value: unknown, maxLength = 500): string {
257
216
  let serialized: string;
258
217
  try {
@@ -306,7 +265,7 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
306
265
  if (candidate === undefined) return null;
307
266
  const { validator, error } = buildOutputValidator(outputSchema);
308
267
  if (error) return null;
309
- if (validator && !validator.validate(candidate).ok) return null;
268
+ if (validator && !validator.validate(candidate).success) return null;
310
269
  return { data: candidate };
311
270
  }
312
271
 
@@ -393,9 +352,10 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
393
352
  stderr = `schema_violation: invalid output schema: ${schemaError}`;
394
353
  exitCode = 1;
395
354
  } else {
396
- const verdict = validator ? validator.validate(completeData) : { ok: true as const };
397
- if (!verdict.ok) {
398
- const outcome = buildSchemaViolationOutcome(verdict, completeData);
355
+ const result = validator?.validate(completeData) ?? { success: true as const };
356
+ if (!result.success) {
357
+ const summary = summarizeValidationFailure(result, completeData, validator?.requiredFields ?? []);
358
+ const outcome = buildSchemaViolationOutcome(summary, completeData);
399
359
  rawOutput = outcome.rawOutput;
400
360
  stderr = outcome.stderr;
401
361
  exitCode = outcome.exitCode;
@@ -420,9 +380,10 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
420
380
  if (fallback) {
421
381
  const completeData = normalizeCompleteData(fallback.data, reportFindings);
422
382
  const { validator } = buildOutputValidator(outputSchema);
423
- const verdict = validator ? validator.validate(completeData) : { ok: true as const };
424
- if (!verdict.ok) {
425
- const outcome = buildSchemaViolationOutcome(verdict, completeData);
383
+ const result = validator?.validate(completeData) ?? { success: true as const };
384
+ if (!result.success) {
385
+ const summary = summarizeValidationFailure(result, completeData, validator?.requiredFields ?? []);
386
+ const outcome = buildSchemaViolationOutcome(summary, completeData);
426
387
  rawOutput = outcome.rawOutput;
427
388
  stderr = outcome.stderr;
428
389
  exitCode = outcome.exitCode;
@@ -1277,6 +1238,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1277
1238
  customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
1278
1239
  localProtocolOptions: options.localProtocolOptions,
1279
1240
  telemetry: subagentTelemetry,
1241
+ parentEvalSessionId: options.parentEvalSessionId,
1280
1242
  }),
1281
1243
  );
1282
1244
 
@@ -1324,22 +1286,25 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1324
1286
  }
1325
1287
 
1326
1288
  const extensionRunner = session.extensionRunner;
1289
+ const pendingExtensionMessages: Promise<void>[] = [];
1327
1290
  if (extensionRunner) {
1328
1291
  extensionRunner.initialize(
1329
1292
  {
1330
1293
  sendMessage: (message, options) => {
1331
- session.sendCustomMessage(message, options).catch(e => {
1294
+ const sendPromise = session.sendCustomMessage(message, options).catch(e => {
1332
1295
  logger.error("Extension sendMessage failed", {
1333
1296
  error: e instanceof Error ? e.message : String(e),
1334
1297
  });
1335
1298
  });
1299
+ pendingExtensionMessages.push(sendPromise);
1336
1300
  },
1337
1301
  sendUserMessage: (content, options) => {
1338
- session.sendUserMessage(content, options).catch(e => {
1302
+ const sendPromise = session.sendUserMessage(content, options).catch(e => {
1339
1303
  logger.error("Extension sendUserMessage failed", {
1340
1304
  error: e instanceof Error ? e.message : String(e),
1341
1305
  });
1342
1306
  });
1307
+ pendingExtensionMessages.push(sendPromise);
1343
1308
  },
1344
1309
  appendEntry: (customType, data) => {
1345
1310
  session.sessionManager.appendCustomEntry(customType, data);
@@ -1375,6 +1340,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1375
1340
  logger.error("Extension error", { path: err.extensionPath, error: err.error });
1376
1341
  });
1377
1342
  await awaitAbortable(extensionRunner.emit({ type: "session_start" }));
1343
+ while (pendingExtensionMessages.length > 0) {
1344
+ await awaitAbortable(Promise.all(pendingExtensionMessages.splice(0)));
1345
+ }
1378
1346
  }
1379
1347
 
1380
1348
  const MAX_YIELD_RETRIES = 3;
@@ -1547,7 +1515,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1547
1515
  // Use final output if available, otherwise accumulated output
1548
1516
  let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
1549
1517
  const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
1550
- const reportFindings = progress.extractedToolData?.report_finding as ReviewFinding[] | undefined;
1518
+ const reportFindingDetails = progress.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
1519
+ const reportFindings: ReviewFinding[] | undefined = reportFindingDetails?.map(toReviewFinding);
1551
1520
  const finalized = finalizeSubprocessOutput({
1552
1521
  rawOutput,
1553
1522
  exitCode,
package/src/task/index.ts CHANGED
@@ -558,6 +558,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
558
558
  const commitStyle = this.session.settings.get("task.isolation.commits");
559
559
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
560
560
  const taskDepth = this.session.taskDepth ?? 0;
561
+ const subagentLspEnabled = (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp");
561
562
 
562
563
  if (isolationMode === "none" && "isolated" in params) {
563
564
  return {
@@ -843,6 +844,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
843
844
  file => path.basename(file.path).toLowerCase() !== "agents.md",
844
845
  );
845
846
  const promptTemplates = this.session.promptTemplates;
847
+ const parentEvalSessionId = this.session.getEvalSessionId?.() ?? undefined;
846
848
 
847
849
  // Initialize progress for all tasks
848
850
  for (let i = 0; i < tasksWithUniqueIds.length; i++) {
@@ -872,7 +874,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
872
874
  if (!isIsolated) {
873
875
  return runSubprocess({
874
876
  cwd: this.session.cwd,
875
- agent,
877
+ agent: effectiveAgent,
876
878
  task: renderSubagentUserPrompt(task.assignment, simpleMode),
877
879
  assignment: task.assignment.trim(),
878
880
  context: sharedContext,
@@ -888,7 +890,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
888
890
  persistArtifacts: !!artifactsDir,
889
891
  artifactsDir: effectiveArtifactsDir,
890
892
  contextFile: contextFilePath,
891
- enableLsp: false,
893
+ enableLsp: subagentLspEnabled,
892
894
  signal,
893
895
  eventBus: this.session.eventBus,
894
896
  onProgress: progress => {
@@ -910,6 +912,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
910
912
  parentArtifactManager,
911
913
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
912
914
  parentTelemetry: this.session.getTelemetry?.(),
915
+ parentEvalSessionId,
913
916
  });
914
917
  }
915
918
 
@@ -927,7 +930,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
927
930
  const result = await runSubprocess({
928
931
  cwd: this.session.cwd,
929
932
  worktree: isolationDir,
930
- agent,
933
+ agent: effectiveAgent,
931
934
  task: renderSubagentUserPrompt(task.assignment, simpleMode),
932
935
  assignment: task.assignment.trim(),
933
936
  context: sharedContext,
@@ -943,7 +946,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
943
946
  persistArtifacts: !!artifactsDir,
944
947
  artifactsDir: effectiveArtifactsDir,
945
948
  contextFile: contextFilePath,
946
- enableLsp: false,
949
+ enableLsp: subagentLspEnabled,
947
950
  signal,
948
951
  eventBus: this.session.eventBus,
949
952
  onProgress: progress => {
@@ -965,6 +968,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
965
968
  parentArtifactManager,
966
969
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
967
970
  parentTelemetry: this.session.getTelemetry?.(),
971
+ parentEvalSessionId,
968
972
  });
969
973
  if (mergeMode === "branch" && result.exitCode === 0) {
970
974
  try {
@@ -44,47 +44,6 @@ export interface DiscoverableToolSearchResult {
44
44
  score: number;
45
45
  }
46
46
 
47
- // ─── Legacy MCP-typed aliases (back-compat) ──────────────────────────────────
48
-
49
- /** @deprecated Use DiscoverableTool with source === "mcp" */
50
- export type DiscoverableMCPTool = Pick<
51
- DiscoverableTool,
52
- "name" | "label" | "schemaKeys" | "serverName" | "mcpToolName"
53
- > & { description: string };
54
-
55
- /** @deprecated Use DiscoverableToolServerSummary */
56
- export type DiscoverableMCPToolServerSummary = DiscoverableToolServerSummary;
57
-
58
- /** @deprecated Use DiscoverableToolSummary */
59
- export type DiscoverableMCPToolSummary = DiscoverableToolSummary;
60
-
61
- /** Tool object stored on legacy MCP index documents. Carries both legacy `description` and the
62
- * generic `summary`/`source` so the legacy index is structurally assignable to
63
- * DiscoverableToolSearchIndex (search functions read termFrequencies, not the tool fields). */
64
- export type DiscoverableMCPSearchTool = DiscoverableTool & { description: string };
65
-
66
- /** @deprecated Use DiscoverableToolSearchDocument */
67
- export interface DiscoverableMCPSearchDocument {
68
- tool: DiscoverableMCPSearchTool;
69
- termFrequencies: Map<string, number>;
70
- length: number;
71
- }
72
-
73
- /** @deprecated Use DiscoverableToolSearchIndex.
74
- * Documents on this index expose `tool.description` (legacy MCP shape) while still being
75
- * searchable via `searchDiscoverableTools`. */
76
- export interface DiscoverableMCPSearchIndex {
77
- documents: DiscoverableMCPSearchDocument[];
78
- averageLength: number;
79
- documentFrequencies: Map<string, number>;
80
- }
81
-
82
- /** @deprecated Use DiscoverableToolSearchResult */
83
- export interface DiscoverableMCPSearchResult {
84
- tool: DiscoverableMCPSearchTool;
85
- score: number;
86
- }
87
-
88
47
  // ─── BM25 Constants ───────────────────────────────────────────────────────────
89
48
 
90
49
  const BM25_K1 = 1.2;
@@ -295,96 +254,3 @@ export function searchDiscoverableTools(
295
254
  .sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name))
296
255
  .slice(0, limit);
297
256
  }
298
-
299
- // ─── Legacy MCP-specific shims (back-compat wrappers) ────────────────────────
300
-
301
- /** @deprecated Use getDiscoverableTool */
302
- export function getDiscoverableMCPTool(tool: AgentTool): DiscoverableMCPTool | null {
303
- if (!isMCPToolName(tool.name)) return null;
304
- const toolRecord = tool as AgentTool & {
305
- label?: string;
306
- description?: string;
307
- mcpServerName?: string;
308
- mcpToolName?: string;
309
- parameters?: unknown;
310
- };
311
- return {
312
- name: tool.name,
313
- label: typeof toolRecord.label === "string" ? toolRecord.label : tool.name,
314
- description: typeof toolRecord.description === "string" ? toolRecord.description : "",
315
- serverName: typeof toolRecord.mcpServerName === "string" ? toolRecord.mcpServerName : undefined,
316
- mcpToolName: typeof toolRecord.mcpToolName === "string" ? toolRecord.mcpToolName : undefined,
317
- schemaKeys: getSchemaPropertyKeys(toolRecord.parameters),
318
- };
319
- }
320
-
321
- /** @deprecated Use collectDiscoverableTools with source filter */
322
- export function collectDiscoverableMCPTools(tools: Iterable<AgentTool>): DiscoverableMCPTool[] {
323
- const discoverable: DiscoverableMCPTool[] = [];
324
- for (const tool of tools) {
325
- const metadata = getDiscoverableMCPTool(tool);
326
- if (metadata) {
327
- discoverable.push(metadata);
328
- }
329
- }
330
- return discoverable;
331
- }
332
-
333
- /** @deprecated Use selectDiscoverableToolNamesByServer */
334
- export function selectDiscoverableMCPToolNamesByServer(
335
- tools: Iterable<DiscoverableMCPTool>,
336
- serverNames: ReadonlySet<string>,
337
- ): string[] {
338
- if (serverNames.size === 0) return [];
339
- return Array.from(tools)
340
- .filter(tool => tool.serverName !== undefined && serverNames.has(tool.serverName))
341
- .map(tool => tool.name);
342
- }
343
-
344
- /** @deprecated Use summarizeDiscoverableTools */
345
- export function summarizeDiscoverableMCPTools(tools: DiscoverableMCPTool[]): DiscoverableMCPToolSummary {
346
- const serverToolCounts = new Map<string, number>();
347
- for (const tool of tools) {
348
- if (!tool.serverName) continue;
349
- serverToolCounts.set(tool.serverName, (serverToolCounts.get(tool.serverName) ?? 0) + 1);
350
- }
351
- const servers = Array.from(serverToolCounts.entries())
352
- .sort(([left], [right]) => left.localeCompare(right))
353
- .map(([name, toolCount]) => ({ name, toolCount }));
354
- return {
355
- servers,
356
- toolCount: tools.length,
357
- };
358
- }
359
-
360
- /** @deprecated Use buildDiscoverableToolSearchIndex.
361
- * Builds an index whose documents preserve the legacy `description` field on each tool while
362
- * also carrying the generic `summary` (set from `description`) so the index remains usable
363
- * with `searchDiscoverableTools`. */
364
- export function buildDiscoverableMCPSearchIndex(tools: Iterable<DiscoverableMCPTool>): DiscoverableMCPSearchIndex {
365
- const adapted: DiscoverableMCPSearchTool[] = Array.from(tools).map(t => ({
366
- name: t.name,
367
- label: t.label,
368
- description: t.description,
369
- summary: t.description,
370
- source: "mcp" as DiscoverableToolSource,
371
- serverName: t.serverName,
372
- mcpToolName: t.mcpToolName,
373
- schemaKeys: t.schemaKeys,
374
- }));
375
- const generic = buildDiscoverableToolSearchIndex(adapted);
376
- // Documents reference `adapted` tools (with `description`), so the cast is sound.
377
- return generic as unknown as DiscoverableMCPSearchIndex;
378
- }
379
-
380
- /** @deprecated Use searchDiscoverableTools */
381
- export function searchDiscoverableMCPTools(
382
- index: DiscoverableMCPSearchIndex | DiscoverableToolSearchIndex,
383
- query: string,
384
- limit: number,
385
- ): DiscoverableMCPSearchResult[] {
386
- return searchDiscoverableTools(index as DiscoverableToolSearchIndex, query, limit) as DiscoverableMCPSearchResult[];
387
- }
388
-
389
- /** @deprecated Use formatDiscoverableToolServerSummary */
390
- export const formatDiscoverableMCPToolServerSummary = formatDiscoverableToolServerSummary;
@@ -6,7 +6,7 @@ import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import * as z from "zod/v4";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
- import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
9
+ import { computeFileHash, formatHashlineHeader } from "../hashline/hash";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
12
12
  import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
@@ -257,12 +257,26 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
257
257
  }
258
258
 
259
259
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
260
+ const hashContexts = new Map<string, { fileHash: string }>();
261
+ if (useHashLines) {
262
+ for (const relativePath of fileList) {
263
+ const absolutePath = path.resolve(this.session.cwd, relativePath);
264
+ try {
265
+ const fullText = await Bun.file(absolutePath).text();
266
+ const fileHash = computeFileHash(fullText);
267
+ hashContexts.set(relativePath, { fileHash });
268
+ } catch {
269
+ // Best-effort: if a file disappears between ast-edit and rendering, emit plain line output.
270
+ }
271
+ }
272
+ }
260
273
  const outputLines: string[] = [];
261
274
  const displayLines: string[] = [];
262
275
  const renderChangesForFile = (relativePath: string): { model: string[]; display: string[] } => {
263
276
  const modelOut: string[] = [];
264
277
  const displayOut: string[] = [];
265
278
  const fileChanges = changesByFile.get(relativePath) ?? [];
279
+ const hashContext = hashContexts.get(relativePath);
266
280
  const lineNumberWidth = fileChanges.reduce(
267
281
  (width, change) => Math.max(width, String(change.startLine).length),
268
282
  0,
@@ -272,13 +286,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
272
286
  const afterFirstLine = change.after.split("\n", 1)[0] ?? "";
273
287
  const beforeLine = beforeFirstLine.slice(0, 120);
274
288
  const afterLine = afterFirstLine.slice(0, 120);
275
- const beforeRef = useHashLines
276
- ? `${change.startLine}${computeLineHash(change.startLine, beforeFirstLine)}`
277
- : `${change.startLine}:${change.startColumn}`;
278
- const afterRef = useHashLines
279
- ? `${change.startLine}${computeLineHash(change.startLine, afterFirstLine)}`
280
- : `${change.startLine}:${change.startColumn}`;
281
- const lineSeparator = useHashLines ? HL_BODY_SEP : " ";
289
+ const beforeRef = hashContext ? `${change.startLine}` : `${change.startLine}:${change.startColumn}`;
290
+ const afterRef = hashContext ? `${change.startLine}` : `${change.startLine}:${change.startColumn}`;
291
+ const lineSeparator = hashContext ? ":" : " ";
282
292
  modelOut.push(`-${beforeRef}${lineSeparator}${beforeLine}`);
283
293
  modelOut.push(`+${afterRef}${lineSeparator}${afterLine}`);
284
294
  displayOut.push(formatCodeFrameLine("-", change.startLine, beforeLine, lineNumberWidth));
@@ -291,10 +301,13 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
291
301
  const grouped = formatGroupedFiles(fileList, relativePath => {
292
302
  const rendered = renderChangesForFile(relativePath);
293
303
  const count = fileReplacementCounts.get(relativePath) ?? 0;
304
+ const hashContext = hashContexts.get(relativePath);
305
+ const hashSuffix = hashContext ? `#${hashContext.fileHash}` : "";
294
306
  return {
295
- headerSuffix: ` (${formatCount("replacement", count)})`,
307
+ headerSuffix: `${hashSuffix} (${formatCount("replacement", count)})`,
296
308
  modelLines: rendered.model,
297
309
  displayLines: rendered.display,
310
+ skip: rendered.model.length === 0,
298
311
  };
299
312
  });
300
313
  outputLines.push(...grouped.model);
@@ -302,6 +315,15 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
302
315
  } else {
303
316
  for (const relativePath of fileList) {
304
317
  const rendered = renderChangesForFile(relativePath);
318
+ if (rendered.model.length === 0) continue;
319
+ if (outputLines.length > 0) {
320
+ outputLines.push("");
321
+ displayLines.push("");
322
+ }
323
+ const hashContext = hashContexts.get(relativePath);
324
+ if (hashContext) {
325
+ outputLines.push(formatHashlineHeader(relativePath, hashContext.fileHash));
326
+ }
305
327
  outputLines.push(...rendered.model);
306
328
  displayLines.push(...rendered.display);
307
329
  }
@@ -499,11 +521,12 @@ export const astEditToolRenderer = {
499
521
  let contextDir = searchBase ?? "";
500
522
  return group.map(line => {
501
523
  if (line.startsWith("## ")) {
502
- // Strip ` (3 replacements)` suffix attached by formatGroupedFiles.
524
+ // Strip ` (3 replacements)` and `#hash` suffixes from formatGroupedFiles.
503
525
  const fileName = line
504
526
  .slice(3)
505
527
  .trimEnd()
506
- .replace(/\s+\([^)]*\)\s*$/, "");
528
+ .replace(/\s+\([^)]*\)\s*$/, "")
529
+ .replace(/#[0-9a-f]+$/, "");
507
530
  const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
508
531
  const styled = uiTheme.fg("dim", line);
509
532
  return absPath ? fileHyperlink(absPath, styled) : styled;
@@ -514,14 +537,14 @@ export const astEditToolRenderer = {
514
537
  .trimEnd()
515
538
  .replace(/\s+\([^)]*\)\s*$/, "");
516
539
  const isDirectory = raw.endsWith("/");
517
- const name = raw.replace(/\/$/, "");
540
+ const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
518
541
  if (isDirectory) {
519
542
  if (searchBase) {
520
543
  contextDir = name === "." ? searchBase : path.join(searchBase, name);
521
544
  }
522
545
  return uiTheme.fg("accent", line);
523
546
  }
524
- // Root-level file with optional suffix, e.g. `# foo.ts (3 replacements)`.
547
+ // Root-level file with optional `#hash` and ` (3 replacements)` suffixes.
525
548
  const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
526
549
  const styled = uiTheme.fg("accent", line);
527
550
  return absPath ? fileHyperlink(absPath, styled) : styled;
@@ -5,7 +5,9 @@ import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import * as z from "zod/v4";
8
+ import { getFileReadCache } from "../edit/file-read-cache";
8
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
+ import { computeFileHash, formatHashlineHeader } from "../hashline/hash";
9
11
  import type { Theme } from "../modes/theme/theme";
10
12
  import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
11
13
  import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
@@ -216,25 +218,43 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
216
218
  }
217
219
 
218
220
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
221
+ const hashContexts = new Map<string, { absolutePath: string; fileHash: string }>();
222
+ if (useHashLines) {
223
+ for (const relativePath of fileList) {
224
+ const absolutePath = path.resolve(this.session.cwd, relativePath);
225
+ try {
226
+ const fullText = await Bun.file(absolutePath).text();
227
+ const fileHash = computeFileHash(fullText);
228
+ hashContexts.set(relativePath, { absolutePath, fileHash });
229
+ } catch {
230
+ // Best-effort: if a file disappears between ast-grep and rendering, emit plain line output.
231
+ }
232
+ }
233
+ }
219
234
  const outputLines: string[] = [];
220
235
  const displayLines: string[] = [];
221
236
  const renderMatchesForFile = (relativePath: string): { model: string[]; display: string[] } => {
222
237
  const modelOut: string[] = [];
223
238
  const displayOut: string[] = [];
224
239
  const fileMatches = matchesByFile.get(relativePath) ?? [];
240
+ const hashContext = hashContexts.get(relativePath);
225
241
  const lineNumberWidth = fileMatches.reduce((width, match) => {
226
242
  const lineCount = match.text.split("\n").length;
227
243
  const endLine = match.startLine + lineCount - 1;
228
244
  return Math.max(width, String(match.startLine).length, String(endLine).length);
229
245
  }, 0);
246
+ const cacheEntries: Array<readonly [number, string]> = [];
230
247
  for (const match of fileMatches) {
231
248
  const matchLines = match.text.split("\n");
232
249
  for (let index = 0; index < matchLines.length; index++) {
233
250
  const lineNumber = match.startLine + index;
234
251
  const isMatch = index === 0;
235
252
  const line = matchLines[index] ?? "";
236
- modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
253
+ modelOut.push(
254
+ formatMatchLine(lineNumber, line, isMatch, { useHashLines: hashContext !== undefined }),
255
+ );
237
256
  displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
257
+ cacheEntries.push([lineNumber, line] as const);
238
258
  }
239
259
  if (match.metaVariables && Object.keys(match.metaVariables).length > 0) {
240
260
  const serializedMeta = Object.entries(match.metaVariables)
@@ -246,19 +266,39 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
246
266
  }
247
267
  fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
248
268
  }
269
+ if (hashContext && cacheEntries.length > 0) {
270
+ getFileReadCache(this.session).recordSparse(hashContext.absolutePath, cacheEntries, {
271
+ fileHash: hashContext.fileHash,
272
+ });
273
+ }
249
274
  return { model: modelOut, display: displayOut };
250
275
  };
251
276
 
252
277
  if (isDirectory) {
253
278
  const grouped = formatGroupedFiles(fileList, relativePath => {
254
279
  const rendered = renderMatchesForFile(relativePath);
255
- return { modelLines: rendered.model, displayLines: rendered.display };
280
+ const hashContext = hashContexts.get(relativePath);
281
+ return {
282
+ modelLines: rendered.model,
283
+ displayLines: rendered.display,
284
+ headerSuffix: hashContext ? `#${hashContext.fileHash}` : "",
285
+ skip: rendered.model.length === 0,
286
+ };
256
287
  });
257
288
  outputLines.push(...grouped.model);
258
289
  displayLines.push(...grouped.display);
259
290
  } else {
260
291
  for (const relativePath of fileList) {
261
292
  const rendered = renderMatchesForFile(relativePath);
293
+ if (rendered.model.length === 0) continue;
294
+ if (outputLines.length > 0) {
295
+ outputLines.push("");
296
+ displayLines.push("");
297
+ }
298
+ const hashContext = hashContexts.get(relativePath);
299
+ if (hashContext) {
300
+ outputLines.push(formatHashlineHeader(relativePath, hashContext.fileHash));
301
+ }
262
302
  outputLines.push(...rendered.model);
263
303
  displayLines.push(...rendered.display);
264
304
  }
@@ -385,7 +425,8 @@ export const astGrepToolRenderer = {
385
425
  const fileName = line
386
426
  .slice(3)
387
427
  .trimEnd()
388
- .replace(/\s+\([^)]*\)\s*$/, "");
428
+ .replace(/\s+\([^)]*\)\s*$/, "")
429
+ .replace(/#[0-9a-f]+$/, "");
389
430
  const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
390
431
  const styled = uiTheme.fg("dim", line);
391
432
  return absPath ? fileHyperlink(absPath, styled) : styled;
@@ -396,7 +437,7 @@ export const astGrepToolRenderer = {
396
437
  .trimEnd()
397
438
  .replace(/\s+\([^)]*\)\s*$/, "");
398
439
  const isDirectory = raw.endsWith("/");
399
- const name = raw.replace(/\/$/, "");
440
+ const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
400
441
  if (isDirectory) {
401
442
  if (searchBase) {
402
443
  contextDir = name === "." ? searchBase : path.join(searchBase, name);
@@ -575,8 +575,10 @@ export class WorkerCore {
575
575
  if (signal.aborted) onCancel();
576
576
  else signal.addEventListener("abort", onCancel, { once: true });
577
577
  try {
578
+ const hooks = this.#hooksForActiveRun();
579
+ if (!hooks) throw new ToolError("Browser runtime started without an active run");
578
580
  const returnValue = await Promise.race([
579
- runtime.run(msg.code, `browser-run-${msg.id}.js`),
581
+ runtime.run(msg.code, `browser-run-${msg.id}.js`, hooks, { runId: msg.id, cwd: msg.session.cwd }),
580
582
  cancelRejection,
581
583
  ]);
582
584
  await this.#postReadyInfo();
@@ -601,7 +603,6 @@ export class WorkerCore {
601
603
  this.#runtime = new JsRuntime({
602
604
  initialCwd: session.cwd,
603
605
  sessionId: `browser-tab-${this.#targetId ?? "unknown"}`,
604
- getHooks: () => this.#hooksForActiveRun(),
605
606
  });
606
607
  return this.#runtime;
607
608
  }
package/src/tools/eval.ts CHANGED
@@ -6,6 +6,7 @@ import { prompt } from "@oh-my-pi/pi-utils";
6
6
  import * as z from "zod/v4";
7
7
  import { jsBackend, pythonBackend } from "../eval";
8
8
  import type { ExecutorBackend } from "../eval/backend";
9
+ import { defaultEvalSessionId } from "../eval/session-id";
9
10
  import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
10
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
12
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
@@ -347,7 +348,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
347
348
  pushUpdate();
348
349
  },
349
350
  });
350
- const sessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
351
+ const sessionId = session.getEvalSessionId?.() ?? defaultEvalSessionId(session);
351
352
 
352
353
  for (let i = 0; i < cells.length; i++) {
353
354
  const cell = cells[i];