@oh-my-pi/pi-coding-agent 3.21.0 → 3.24.0

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 (66) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/docs/sdk.md +47 -50
  3. package/examples/custom-tools/README.md +0 -15
  4. package/examples/hooks/custom-compaction.ts +1 -3
  5. package/examples/sdk/README.md +6 -10
  6. package/package.json +5 -5
  7. package/src/cli/args.ts +9 -6
  8. package/src/core/agent-session.ts +3 -3
  9. package/src/core/custom-tools/wrapper.ts +0 -1
  10. package/src/core/extensions/index.ts +1 -6
  11. package/src/core/extensions/wrapper.ts +0 -7
  12. package/src/core/file-mentions.ts +5 -8
  13. package/src/core/sdk.ts +41 -111
  14. package/src/core/session-manager.ts +7 -0
  15. package/src/core/system-prompt.ts +22 -33
  16. package/src/core/tools/ask.ts +14 -7
  17. package/src/core/tools/bash-interceptor.ts +4 -4
  18. package/src/core/tools/bash.ts +19 -9
  19. package/src/core/tools/context.ts +7 -0
  20. package/src/core/tools/edit.ts +8 -15
  21. package/src/core/tools/exa/render.ts +4 -16
  22. package/src/core/tools/find.ts +7 -18
  23. package/src/core/tools/git.ts +13 -3
  24. package/src/core/tools/grep.ts +7 -18
  25. package/src/core/tools/index.test.ts +180 -0
  26. package/src/core/tools/index.ts +94 -237
  27. package/src/core/tools/ls.ts +4 -9
  28. package/src/core/tools/lsp/index.ts +32 -29
  29. package/src/core/tools/lsp/render.ts +7 -28
  30. package/src/core/tools/notebook.ts +3 -5
  31. package/src/core/tools/output.ts +5 -17
  32. package/src/core/tools/read.ts +8 -19
  33. package/src/core/tools/review.ts +0 -18
  34. package/src/core/tools/rulebook.ts +8 -2
  35. package/src/core/tools/task/agents.ts +28 -7
  36. package/src/core/tools/task/discovery.ts +0 -6
  37. package/src/core/tools/task/executor.ts +264 -254
  38. package/src/core/tools/task/index.ts +45 -220
  39. package/src/core/tools/task/render.ts +21 -11
  40. package/src/core/tools/task/types.ts +6 -11
  41. package/src/core/tools/task/worker-protocol.ts +17 -0
  42. package/src/core/tools/task/worker.ts +238 -0
  43. package/src/core/tools/web-fetch.ts +4 -36
  44. package/src/core/tools/web-search/index.ts +2 -1
  45. package/src/core/tools/web-search/render.ts +1 -4
  46. package/src/core/tools/write.ts +7 -15
  47. package/src/discovery/helpers.test.ts +1 -1
  48. package/src/index.ts +5 -16
  49. package/src/main.ts +4 -4
  50. package/src/modes/interactive/theme/theme.ts +4 -4
  51. package/src/prompts/task.md +0 -7
  52. package/src/prompts/tools/output.md +2 -2
  53. package/src/prompts/tools/task.md +68 -0
  54. package/examples/custom-tools/question/index.ts +0 -84
  55. package/examples/custom-tools/subagent/README.md +0 -172
  56. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  57. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  58. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  59. package/examples/custom-tools/subagent/agents.ts +0 -156
  60. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  61. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  62. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  63. package/examples/custom-tools/subagent/index.ts +0 -1002
  64. package/examples/sdk/05-tools.ts +0 -94
  65. package/examples/sdk/12-full-control.ts +0 -95
  66. package/src/prompts/browser.md +0 -71
@@ -237,10 +237,7 @@ function renderDiagnostics(
237
237
  }
238
238
  const severityColor = severityToColor(item.severity);
239
239
  const location = formatDiagnosticLocation(item.file, item.line, item.col, theme);
240
- output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)} ${theme.fg(
241
- "dim",
242
- `[${item.severity}]`,
243
- )}`;
240
+ output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)} ${theme.fg("dim", `[${item.severity}]`)}`;
244
241
  if (item.message) {
245
242
  output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg(
246
243
  "muted",
@@ -274,10 +271,7 @@ function renderDiagnostics(
274
271
  output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)}${message}`;
275
272
  }
276
273
  if (remaining > 0) {
277
- output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
278
- "muted",
279
- `${theme.format.ellipsis} ${remaining} more`,
280
- )}`;
274
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", `${theme.format.ellipsis} ${remaining} more`)}`;
281
275
  }
282
276
 
283
277
  return new Text(output, 0, 0);
@@ -332,10 +326,7 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
332
326
  const isLastLoc = li === locsToShow.length - 1 && locs.length <= maxLocsPerFile;
333
327
  const locBranch = isLastLoc ? theme.tree.last : theme.tree.branch;
334
328
  const locCont = isLastLoc ? " " : `${theme.tree.vertical} `;
335
- output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locBranch)} ${theme.fg(
336
- "muted",
337
- `line ${line}, col ${col}`,
338
- )}`;
329
+ output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locBranch)} ${theme.fg("muted", `line ${line}, col ${col}`)}`;
339
330
  if (expanded) {
340
331
  const context = `at ${file}:${line}:${col}`;
341
332
  output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locCont)}${theme.fg(
@@ -354,10 +345,7 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
354
345
  }
355
346
 
356
347
  if (files.length > maxFiles) {
357
- output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
358
- "muted",
359
- formatMoreItems(files.length - maxFiles, "file", theme),
360
- )}`;
348
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", formatMoreItems(files.length - maxFiles, "file", theme))}`;
361
349
  }
362
350
 
363
351
  return output;
@@ -463,10 +451,7 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
463
451
  )}`;
464
452
  }
465
453
  if (topLevelCount > 3) {
466
- output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
467
- "muted",
468
- `${theme.format.ellipsis} ${topLevelCount - 3} more`,
469
- )}`;
454
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", `${theme.format.ellipsis} ${topLevelCount - 3} more`)}`;
470
455
  }
471
456
 
472
457
  return new Text(output, 0, 0);
@@ -502,10 +487,7 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
502
487
 
503
488
  const firstLine = lines[0] || "No output";
504
489
  const expandHint = formatExpandHint(false, lines.length > 1, theme);
505
- let output = `${icon} ${theme.fg(
506
- "dim",
507
- truncate(firstLine, TRUNCATE_LENGTHS.TITLE, theme.format.ellipsis),
508
- )}${expandHint}`;
490
+ let output = `${icon} ${theme.fg("dim", truncate(firstLine, TRUNCATE_LENGTHS.TITLE, theme.format.ellipsis))}${expandHint}`;
509
491
 
510
492
  if (lines.length > 1) {
511
493
  const previewLines = lines.slice(1, 4);
@@ -518,10 +500,7 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
518
500
  )}`;
519
501
  }
520
502
  if (lines.length > 4) {
521
- output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
522
- "muted",
523
- formatMoreItems(lines.length - 4, "line", theme),
524
- )}`;
503
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", formatMoreItems(lines.length - 4, "line", theme))}`;
525
504
  }
526
505
  }
527
506
 
@@ -4,6 +4,7 @@ import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import type { Theme } from "../../modes/interactive/theme/theme";
6
6
  import type { RenderResultOptions } from "../custom-tools/types";
7
+ import type { ToolSession } from "../sdk";
7
8
  import { untilAborted } from "../utils";
8
9
  import { resolveToCwd } from "./path-utils";
9
10
  import {
@@ -61,7 +62,7 @@ function splitIntoLines(content: string): string[] {
61
62
  return content.split("\n").map((line, i, arr) => (i < arr.length - 1 ? `${line}\n` : line));
62
63
  }
63
64
 
64
- export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema> {
65
+ export function createNotebookTool(session: ToolSession): AgentTool<typeof notebookSchema> {
65
66
  return {
66
67
  name: "notebook",
67
68
  label: "Notebook",
@@ -79,7 +80,7 @@ export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema
79
80
  }: { action: string; notebook_path: string; cell_index: number; content?: string; cell_type?: string },
80
81
  signal?: AbortSignal,
81
82
  ) => {
82
- const absolutePath = resolveToCwd(notebook_path, cwd);
83
+ const absolutePath = resolveToCwd(notebook_path, session.cwd);
83
84
 
84
85
  return untilAborted(signal, async () => {
85
86
  // Check if file exists
@@ -190,9 +191,6 @@ export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema
190
191
  };
191
192
  }
192
193
 
193
- /** Default notebook tool using process.cwd() */
194
- export const notebookTool = createNotebookTool(process.cwd());
195
-
196
194
  // =============================================================================
197
195
  // TUI Renderer
198
196
  // =============================================================================
@@ -14,7 +14,7 @@ import { Type } from "@sinclair/typebox";
14
14
  import type { Theme } from "../../modes/interactive/theme/theme";
15
15
  import outputDescription from "../../prompts/tools/output.md" with { type: "text" };
16
16
  import type { RenderResultOptions } from "../custom-tools/types";
17
- import type { SessionContext } from "./index";
17
+ import type { ToolSession } from "./index";
18
18
  import {
19
19
  formatCount,
20
20
  formatEmptyMessage,
@@ -120,10 +120,7 @@ function extractPreviewLines(content: string, maxLines: number): string[] {
120
120
  return preview;
121
121
  }
122
122
 
123
- export function createOutputTool(
124
- _cwd: string,
125
- sessionContext?: SessionContext,
126
- ): AgentTool<typeof outputSchema, OutputToolDetails> {
123
+ export function createOutputTool(session: ToolSession): AgentTool<typeof outputSchema, OutputToolDetails> {
127
124
  return {
128
125
  name: "output",
129
126
  label: "Output",
@@ -133,7 +130,7 @@ export function createOutputTool(
133
130
  _toolCallId: string,
134
131
  params: { ids: string[]; format?: "raw" | "json" | "stripped"; offset?: number; limit?: number },
135
132
  ): Promise<{ content: TextContent[]; details: OutputToolDetails }> => {
136
- const sessionFile = sessionContext?.getSessionFile();
133
+ const sessionFile = session.getSessionFile();
137
134
 
138
135
  if (!sessionFile) {
139
136
  return {
@@ -253,9 +250,6 @@ export function createOutputTool(
253
250
  };
254
251
  }
255
252
 
256
- /** Default output tool using process.cwd() - for backwards compatibility */
257
- export const outputTool = createOutputTool(process.cwd());
258
-
259
253
  // =============================================================================
260
254
  // TUI Renderer
261
255
  // =============================================================================
@@ -309,15 +303,9 @@ export const outputToolRenderer = {
309
303
  const icon = uiTheme.styledSymbol("status.error", "error");
310
304
  let text = `${icon} ${uiTheme.fg("error", `Error: Not found: ${details.notFound.join(", ")}`)}`;
311
305
  if (details.availableIds?.length) {
312
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
313
- "muted",
314
- `Available: ${details.availableIds.join(", ")}`,
315
- )}`;
306
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", `Available: ${details.availableIds.join(", ")}`)}`;
316
307
  } else {
317
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
318
- "muted",
319
- "No outputs available in current session",
320
- )}`;
308
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No outputs available in current session")}`;
321
309
  }
322
310
  return new Text(text, 0, 0);
323
311
  }
@@ -12,6 +12,7 @@ import { formatDimensionNote, resizeImage } from "../../utils/image-resize";
12
12
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
13
13
  import { ensureTool } from "../../utils/tools-manager";
14
14
  import type { RenderResultOptions } from "../custom-tools/types";
15
+ import type { ToolSession } from "../sdk";
15
16
  import { untilAborted } from "../utils";
16
17
  import { createLsTool } from "./ls";
17
18
  import { resolveReadPath, resolveToCwd } from "./path-utils";
@@ -340,14 +341,9 @@ export interface ReadToolDetails {
340
341
  redirectedTo?: "ls";
341
342
  }
342
343
 
343
- export interface ReadToolOptions {
344
- /** Whether to auto-resize images to 2000x2000 max. Default: true */
345
- autoResizeImages?: boolean;
346
- }
347
-
348
- export function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {
349
- const autoResizeImages = options?.autoResizeImages ?? true;
350
- const lsTool = createLsTool(cwd);
344
+ export function createReadTool(session: ToolSession): AgentTool<typeof readSchema> {
345
+ const autoResizeImages = session.settings?.getImageAutoResize() ?? true;
346
+ const lsTool = createLsTool(session);
351
347
  return {
352
348
  name: "read",
353
349
  label: "Read",
@@ -358,7 +354,7 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
358
354
  { path: readPath, offset, limit }: { path: string; offset?: number; limit?: number },
359
355
  signal?: AbortSignal,
360
356
  ) => {
361
- const absolutePath = resolveReadPath(readPath, cwd);
357
+ const absolutePath = resolveReadPath(readPath, session.cwd);
362
358
 
363
359
  return untilAborted(signal, async () => {
364
360
  let isDirectory = false;
@@ -378,14 +374,12 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
378
374
  }
379
375
  } catch (error) {
380
376
  if (isNotFoundError(error)) {
381
- const suggestions = await findReadPathSuggestions(readPath, cwd);
377
+ const suggestions = await findReadPathSuggestions(readPath, session.cwd);
382
378
  let message = `File not found: ${readPath}`;
383
379
 
384
380
  if (suggestions?.suggestions.length) {
385
381
  const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
386
- message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions
387
- .map((match) => `- ${match}`)
388
- .join("\n")}`;
382
+ message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions.map((match) => `- ${match}`).join("\n")}`;
389
383
  if (suggestions.truncated) {
390
384
  message += `\n[Search truncated to first ${MAX_FUZZY_CANDIDATES} paths. Refine the path if the match isn't listed.]`;
391
385
  }
@@ -462,9 +456,7 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
462
456
  let outputText = truncation.content;
463
457
 
464
458
  if (truncation.truncated) {
465
- outputText += `\n\n[Document converted via markitdown. Output truncated to ${formatSize(
466
- DEFAULT_MAX_BYTES,
467
- )}]`;
459
+ outputText += `\n\n[Document converted via markitdown. Output truncated to ${formatSize(DEFAULT_MAX_BYTES)}]`;
468
460
  details = { truncation };
469
461
  }
470
462
 
@@ -562,9 +554,6 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
562
554
  };
563
555
  }
564
556
 
565
- /** Default read tool using process.cwd() - for backwards compatibility */
566
- export const readTool = createReadTool(process.cwd());
567
-
568
557
  // =============================================================================
569
558
  // TUI Renderer
570
559
  // =============================================================================
@@ -19,13 +19,6 @@ const PRIORITY_LABELS: Record<number, string> = {
19
19
  3: "P3",
20
20
  };
21
21
 
22
- const _PRIORITY_DESCRIPTIONS: Record<number, string> = {
23
- 0: "Drop everything to fix. Blocking release, operations, or major usage.",
24
- 1: "Urgent. Should be addressed in the next cycle.",
25
- 2: "Normal. To be fixed eventually.",
26
- 3: "Low. Nice to have.",
27
- };
28
-
29
22
  // report_finding schema
30
23
  const ReportFindingParams = Type.Object({
31
24
  title: Type.String({
@@ -62,8 +55,6 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
62
55
  label: "Report Finding",
63
56
  description: "Report a code review finding. Use this for each issue found. Call submit_review when done.",
64
57
  parameters: ReportFindingParams,
65
- hidden: true,
66
-
67
58
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
68
59
  const { title, body, priority, confidence, file_path, line_start, line_end } = params;
69
60
  const location = `${file_path}:${line_start}${line_end !== line_start ? `-${line_end}` : ""}`;
@@ -142,7 +133,6 @@ export const submitReviewTool: AgentTool<typeof SubmitReviewParams, SubmitReview
142
133
  label: "Submit Review",
143
134
  description: "Submit the final review verdict. Call this after all findings have been reported.",
144
135
  parameters: SubmitReviewParams,
145
- hidden: true,
146
136
 
147
137
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
148
138
  const { overall_correctness, explanation, confidence } = params;
@@ -206,14 +196,6 @@ export const submitReviewTool: AgentTool<typeof SubmitReviewParams, SubmitReview
206
196
  },
207
197
  };
208
198
 
209
- export function createReportFindingTool(): AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> {
210
- return reportFindingTool;
211
- }
212
-
213
- export function createSubmitReviewTool(): AgentTool<typeof SubmitReviewParams, SubmitReviewDetails, Theme> {
214
- return submitReviewTool;
215
- }
216
-
217
199
  // Re-export types for external use
218
200
  export type { ReportFindingDetails, SubmitReviewDetails };
219
201
 
@@ -9,6 +9,7 @@
9
9
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
10
10
  import { Type } from "@sinclair/typebox";
11
11
  import type { Rule } from "../../capability/rule";
12
+ import type { ToolSession } from "./index";
12
13
 
13
14
  export interface RulebookToolDetails {
14
15
  type: "rulebook";
@@ -23,9 +24,14 @@ const rulebookSchema = Type.Object({
23
24
 
24
25
  /**
25
26
  * Create a rulebook tool with access to discovered rules.
26
- * @param rules - Array of discovered rules (non-TTSR rules with descriptions)
27
+ * Returns null if no rules available.
27
28
  */
28
- export function createRulebookTool(rules: Rule[]): AgentTool<typeof rulebookSchema> {
29
+ export function createRulebookTool(session: ToolSession): AgentTool<typeof rulebookSchema> | null {
30
+ const rules = session.rulebookRules;
31
+ if (!rules || rules.length === 0) {
32
+ return null;
33
+ }
34
+
29
35
  // Build lookup map for O(1) access
30
36
  const ruleMap = new Map<string, Rule>();
31
37
  for (const rule of rules) {
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  // Embed agent markdown files at build time
8
- import browserMd from "../../../prompts/browser.md" with { type: "text" };
9
8
  import exploreMd from "../../../prompts/explore.md" with { type: "text" };
10
9
  import planMd from "../../../prompts/plan.md" with { type: "text" };
11
10
  import reviewerMd from "../../../prompts/reviewer.md" with { type: "text" };
@@ -13,11 +12,37 @@ import taskMd from "../../../prompts/task.md" with { type: "text" };
13
12
  import type { AgentDefinition, AgentSource } from "./types";
14
13
 
15
14
  const EMBEDDED_AGENTS: { name: string; content: string }[] = [
16
- { name: "browser.md", content: browserMd },
17
15
  { name: "explore.md", content: exploreMd },
18
16
  { name: "plan.md", content: planMd },
19
17
  { name: "reviewer.md", content: reviewerMd },
20
- { name: "task.md", content: taskMd },
18
+ {
19
+ name: "task.md",
20
+ content: `---
21
+ name: task
22
+ description: General-purpose subagent with full capabilities for delegated multi-step tasks
23
+ spawns: explore
24
+ model: default
25
+ ---
26
+ ${taskMd}`,
27
+ },
28
+ {
29
+ name: "quick_task.md",
30
+ content: `---
31
+ name: quick_task
32
+ description: Quick task for fast execution
33
+ model: pi/smol
34
+ ---
35
+ ${taskMd}`,
36
+ },
37
+ {
38
+ name: "deep_task.md",
39
+ content: `---
40
+ name: deep_task
41
+ description: Deep task for comprehensive reasoning
42
+ model: pi/slow
43
+ ---
44
+ ${taskMd}`,
45
+ },
21
46
  ];
22
47
 
23
48
  /**
@@ -88,16 +113,12 @@ function parseAgent(fileName: string, content: string, source: AgentSource): Age
88
113
  spawns = "*";
89
114
  }
90
115
 
91
- const recursive =
92
- frontmatter.recursive === undefined ? false : frontmatter.recursive === "true" || frontmatter.recursive === "1";
93
-
94
116
  return {
95
117
  name: frontmatter.name,
96
118
  description: frontmatter.description,
97
119
  tools: tools && tools.length > 0 ? tools : undefined,
98
120
  spawns,
99
121
  model: frontmatter.model,
100
- recursive,
101
122
  systemPrompt: body,
102
123
  source,
103
124
  filePath: `embedded:${fileName}`,
@@ -126,18 +126,12 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
126
126
  spawns = "*";
127
127
  }
128
128
 
129
- const recursive =
130
- frontmatter.recursive === undefined
131
- ? undefined
132
- : frontmatter.recursive === "true" || frontmatter.recursive === "1";
133
-
134
129
  agents.push({
135
130
  name: frontmatter.name,
136
131
  description: frontmatter.description,
137
132
  tools: tools && tools.length > 0 ? tools : undefined,
138
133
  spawns,
139
134
  model: frontmatter.model,
140
- recursive,
141
135
  systemPrompt: body,
142
136
  source,
143
137
  filePath,