@oh-my-pi/pi-coding-agent 15.13.1 → 15.13.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 (71) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli.js +957 -214
  3. package/dist/types/config/model-registry.d.ts +1 -0
  4. package/dist/types/config/models-config-schema.d.ts +3 -0
  5. package/dist/types/config/models-config.d.ts +3 -0
  6. package/dist/types/config/settings-schema.d.ts +66 -0
  7. package/dist/types/edit/hashline/block-resolver.d.ts +1 -1
  8. package/dist/types/edit/index.d.ts +2 -0
  9. package/dist/types/modes/components/welcome.d.ts +1 -0
  10. package/dist/types/modes/controllers/input-controller.d.ts +4 -4
  11. package/dist/types/modes/rpc/rpc-types.d.ts +2 -1
  12. package/dist/types/sdk.d.ts +3 -0
  13. package/dist/types/session/session-dump-format.d.ts +2 -1
  14. package/dist/types/system-prompt.d.ts +11 -0
  15. package/dist/types/tools/ask.d.ts +2 -0
  16. package/dist/types/tools/ast-edit.d.ts +2 -0
  17. package/dist/types/tools/ast-grep.d.ts +2 -0
  18. package/dist/types/tools/browser.d.ts +2 -0
  19. package/dist/types/tools/debug.d.ts +2 -0
  20. package/dist/types/tools/eval.d.ts +2 -0
  21. package/dist/types/tools/find.d.ts +2 -0
  22. package/dist/types/tools/inspect-image.d.ts +2 -1
  23. package/dist/types/tools/irc.d.ts +2 -0
  24. package/dist/types/tools/ssh.d.ts +2 -0
  25. package/dist/types/tools/todo.d.ts +2 -0
  26. package/dist/types/tui/tree-list.d.ts +1 -0
  27. package/package.json +12 -12
  28. package/src/config/model-registry.ts +10 -0
  29. package/src/config/models-config-schema.ts +2 -0
  30. package/src/config/models-config.ts +1 -0
  31. package/src/config/settings-schema.ts +53 -0
  32. package/src/edit/hashline/block-resolver.ts +1 -1
  33. package/src/edit/hashline/execute.ts +1 -6
  34. package/src/edit/index.ts +48 -0
  35. package/src/eval/__tests__/js-context-manager.test.ts +41 -1
  36. package/src/eval/js/context-manager.ts +92 -26
  37. package/src/eval/js/worker-core.ts +1 -1
  38. package/src/internal-urls/docs-index.generated.ts +9 -2
  39. package/src/modes/components/welcome.ts +14 -4
  40. package/src/modes/controllers/input-controller.ts +21 -38
  41. package/src/modes/rpc/rpc-mode.ts +1 -0
  42. package/src/modes/rpc/rpc-types.ts +2 -2
  43. package/src/prompts/system/system-prompt.md +17 -21
  44. package/src/prompts/tools/ask.md +0 -8
  45. package/src/prompts/tools/ast-edit.md +0 -15
  46. package/src/prompts/tools/ast-grep.md +0 -13
  47. package/src/prompts/tools/browser.md +0 -21
  48. package/src/prompts/tools/debug.md +0 -13
  49. package/src/prompts/tools/eval.md +0 -9
  50. package/src/prompts/tools/find.md +0 -13
  51. package/src/prompts/tools/inspect-image.md +0 -9
  52. package/src/prompts/tools/irc.md +0 -15
  53. package/src/prompts/tools/patch.md +0 -13
  54. package/src/prompts/tools/ssh.md +0 -9
  55. package/src/prompts/tools/todo.md +1 -19
  56. package/src/sdk.ts +19 -0
  57. package/src/session/agent-session.ts +125 -19
  58. package/src/session/session-dump-format.ts +10 -31
  59. package/src/system-prompt.ts +31 -0
  60. package/src/tools/ask.ts +41 -0
  61. package/src/tools/ast-edit.ts +46 -0
  62. package/src/tools/ast-grep.ts +24 -0
  63. package/src/tools/browser.ts +52 -0
  64. package/src/tools/debug.ts +17 -0
  65. package/src/tools/eval.ts +20 -1
  66. package/src/tools/find.ts +24 -0
  67. package/src/tools/inspect-image.ts +27 -1
  68. package/src/tools/irc.ts +41 -0
  69. package/src/tools/ssh.ts +16 -0
  70. package/src/tools/todo.ts +82 -3
  71. package/src/tui/tree-list.ts +68 -19
@@ -46,7 +46,9 @@ import {
46
46
  collectEntriesForBranchSummary,
47
47
  collectShakeRegions,
48
48
  compact,
49
+ createCompactionSummaryMessage,
49
50
  DEFAULT_SHAKE_CONFIG,
51
+ effectiveReserveTokens,
50
52
  estimateTokens,
51
53
  generateBranchSummary,
52
54
  generateHandoff,
@@ -6423,6 +6425,37 @@ export class AgentSession {
6423
6425
  let tokensBefore: number;
6424
6426
  let details: unknown;
6425
6427
 
6428
+ // Snapcompact runs locally first; if its frame archive plus the kept
6429
+ // history still overflows the model window, fall back to an LLM summary
6430
+ // (far cheaper than ~FRAME_TOKEN_ESTIMATE per frame).
6431
+ let snapcompactResult: snapcompact.CompactionResult | undefined;
6432
+ if (snapcompactReady) {
6433
+ snapcompactResult = await snapcompact.compact(preparation, {
6434
+ convertToLlm,
6435
+ model: this.model,
6436
+ shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
6437
+ // Providers with hard image caps (OpenRouter: 8) silently drop
6438
+ // frames past the cap — keep the archive within budget.
6439
+ maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
6440
+ });
6441
+ const ctxWindow = this.model?.contextWindow ?? 0;
6442
+ const budget =
6443
+ ctxWindow > 0
6444
+ ? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
6445
+ : Number.POSITIVE_INFINITY;
6446
+ if (this.#projectSnapcompactContextTokens(preparation, snapcompactResult) > budget) {
6447
+ logger.warn("Snapcompact still overflows the window; falling back to an LLM summary", {
6448
+ model: this.model?.id,
6449
+ });
6450
+ this.emitNotice(
6451
+ "warning",
6452
+ "snapcompact could not bring the context under the limit — using an LLM summary instead",
6453
+ "compaction",
6454
+ );
6455
+ snapcompactResult = undefined;
6456
+ }
6457
+ }
6458
+
6426
6459
  if (compactionPrep.kind === "fromHook") {
6427
6460
  summary = compactionPrep.summary;
6428
6461
  shortSummary = compactionPrep.shortSummary;
@@ -6430,15 +6463,7 @@ export class AgentSession {
6430
6463
  tokensBefore = compactionPrep.tokensBefore;
6431
6464
  details = compactionPrep.details;
6432
6465
  preserveData = compactionPrep.preserveData;
6433
- } else if (snapcompactReady) {
6434
- const snapcompactResult = await snapcompact.compact(preparation, {
6435
- convertToLlm,
6436
- model: this.model,
6437
- shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
6438
- // Providers with hard image caps (OpenRouter: 8) silently drop
6439
- // frames past the cap — keep the archive within budget.
6440
- maxFrames: snapcompact.providerFrameBudget(this.model.provider),
6441
- });
6466
+ } else if (snapcompactResult) {
6442
6467
  summary = snapcompactResult.summary;
6443
6468
  shortSummary = snapcompactResult.shortSummary;
6444
6469
  firstKeptEntryId = snapcompactResult.firstKeptEntryId;
@@ -6755,6 +6780,19 @@ export class AgentSession {
6755
6780
  const contextTokens = this.#estimatePendingPromptTokens(messages);
6756
6781
  if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
6757
6782
 
6783
+ // Auto-promote first: switching to a larger-context model avoids compacting
6784
+ // the history at all. The post-turn threshold path already promotes before
6785
+ // compacting; without this, the pre-prompt path would pre-empt promotion and
6786
+ // compact (snapcompact/summary) a session that should have just been promoted.
6787
+ if (await this.#promoteContextModel()) {
6788
+ logger.debug("Pre-prompt context promotion avoided compaction", {
6789
+ contextTokens,
6790
+ contextWindow,
6791
+ model: `${model.provider}/${model.id}`,
6792
+ });
6793
+ return;
6794
+ }
6795
+
6758
6796
  logger.debug("Pre-prompt context maintenance triggered by pending prompt size", {
6759
6797
  contextTokens,
6760
6798
  contextWindow,
@@ -7324,12 +7362,28 @@ export class AgentSession {
7324
7362
  * Returns true if promotion succeeded (caller should retry without compacting).
7325
7363
  */
7326
7364
  async #tryContextPromotion(assistantMessage: AssistantMessage): Promise<boolean> {
7327
- const promotionSettings = this.settings.getGroup("contextPromotion");
7328
- if (!promotionSettings.enabled) return false;
7329
7365
  const currentModel = this.model;
7330
7366
  if (!currentModel) return false;
7367
+ // The overflow/length error may have come from a model the user already
7368
+ // switched away from; only promote when the failing turn was this model.
7331
7369
  if (assistantMessage.provider !== currentModel.provider || assistantMessage.model !== currentModel.id)
7332
7370
  return false;
7371
+ return this.#promoteContextModel();
7372
+ }
7373
+
7374
+ /**
7375
+ * Switch to a larger-context sibling when context promotion is enabled and a
7376
+ * target with a strictly larger window (and a usable key) exists. Returns true
7377
+ * when the model was switched, so the caller can retry without compacting.
7378
+ * Message-independent core shared by the post-turn overflow path
7379
+ * ({@link #tryContextPromotion}) and the pre-prompt threshold path
7380
+ * ({@link #runPrePromptCompactionIfNeeded}).
7381
+ */
7382
+ async #promoteContextModel(): Promise<boolean> {
7383
+ const promotionSettings = this.settings.getGroup("contextPromotion");
7384
+ if (!promotionSettings.enabled) return false;
7385
+ const currentModel = this.model;
7386
+ if (!currentModel) return false;
7333
7387
  const contextWindow = currentModel.contextWindow ?? 0;
7334
7388
  if (contextWindow <= 0) return false;
7335
7389
  const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
@@ -7806,6 +7860,32 @@ export class AgentSession {
7806
7860
  return { kind: "needsLlm", hookContext, hookPrompt, preserveData };
7807
7861
  }
7808
7862
 
7863
+ /**
7864
+ * Project the post-compaction context size of a snapcompact result: kept
7865
+ * recent messages + the summary message with its re-attached frames + the
7866
+ * fixed non-message overhead (system prompt + tools). Mirrors how the
7867
+ * compacted context is rebuilt, so the estimate matches the wire shape, and
7868
+ * lets the caller decide whether snapcompact brought the context under the
7869
+ * window or should fall back to an LLM summary.
7870
+ */
7871
+ #projectSnapcompactContextTokens(preparation: CompactionPreparation, result: snapcompact.CompactionResult): number {
7872
+ const archive = snapcompact.getPreservedArchive(result.preserveData);
7873
+ const frames = archive ? snapcompact.images(archive) : undefined;
7874
+ const summaryMessage = createCompactionSummaryMessage(
7875
+ result.summary,
7876
+ result.tokensBefore,
7877
+ new Date().toISOString(),
7878
+ result.shortSummary,
7879
+ undefined,
7880
+ frames,
7881
+ );
7882
+ let tokens = computeNonMessageTokens(this) + estimateTokens(summaryMessage);
7883
+ for (const message of preparation.recentMessages) {
7884
+ tokens += estimateTokens(message);
7885
+ }
7886
+ return tokens;
7887
+ }
7888
+
7809
7889
  /**
7810
7890
  * Internal: Run auto-compaction with events.
7811
7891
  *
@@ -8018,6 +8098,39 @@ export class AgentSession {
8018
8098
  let tokensBefore: number;
8019
8099
  let details: unknown;
8020
8100
 
8101
+ // Snapcompact runs locally first; if its frame archive plus the kept
8102
+ // history still overflows the model window (frames are capped by the
8103
+ // image budget and cost ~FRAME_TOKEN_ESTIMATE each), an LLM summary is
8104
+ // far cheaper — downgrade to context-full and take the summarizer path.
8105
+ let snapcompactResult: snapcompact.CompactionResult | undefined;
8106
+ if (action === "snapcompact" && compactionPrep.kind !== "fromHook") {
8107
+ snapcompactResult = await snapcompact.compact(preparation, {
8108
+ convertToLlm,
8109
+ model: this.model,
8110
+ maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
8111
+ });
8112
+ const ctxWindow = this.model?.contextWindow ?? 0;
8113
+ const budget =
8114
+ ctxWindow > 0
8115
+ ? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
8116
+ : Number.POSITIVE_INFINITY;
8117
+ const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
8118
+ if (projected > budget) {
8119
+ logger.warn("Snapcompact still overflows the window; falling back to an LLM summary", {
8120
+ model: this.model?.id,
8121
+ projected,
8122
+ budget,
8123
+ });
8124
+ this.emitNotice(
8125
+ "warning",
8126
+ "snapcompact could not bring the context under the limit — using an LLM summary instead",
8127
+ "compaction",
8128
+ );
8129
+ action = "context-full";
8130
+ snapcompactResult = undefined;
8131
+ }
8132
+ }
8133
+
8021
8134
  if (compactionPrep.kind === "fromHook") {
8022
8135
  summary = compactionPrep.summary;
8023
8136
  shortSummary = compactionPrep.shortSummary;
@@ -8025,14 +8138,7 @@ export class AgentSession {
8025
8138
  tokensBefore = compactionPrep.tokensBefore;
8026
8139
  details = compactionPrep.details;
8027
8140
  preserveData = compactionPrep.preserveData;
8028
- } else if (action === "snapcompact") {
8029
- // Local, deterministic: render discarded history onto PNG frames.
8030
- // No model candidates, no API key, no retry loop.
8031
- const snapcompactResult = await snapcompact.compact(preparation, {
8032
- convertToLlm,
8033
- model: this.model,
8034
- maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
8035
- });
8141
+ } else if (snapcompactResult) {
8036
8142
  summary = snapcompactResult.summary;
8037
8143
  shortSummary = snapcompactResult.shortSummary;
8038
8144
  firstKeptEntryId = snapcompactResult.firstKeptEntryId;
@@ -3,8 +3,8 @@
3
3
  */
4
4
  import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
5
  import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
6
- import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
7
- import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
6
+ import type { AssistantMessage, Model, ToolExample, TSchema } from "@oh-my-pi/pi-ai";
7
+ import { renderToolInventory } from "@oh-my-pi/pi-ai/grammar";
8
8
  import { getVisibleThinkingText } from "../utils/thinking-display";
9
9
  import {
10
10
  type BashExecutionMessage,
@@ -23,6 +23,7 @@ export interface SessionDumpToolInfo {
23
23
  name: string;
24
24
  description: string;
25
25
  parameters: unknown;
26
+ examples?: readonly ToolExample[];
26
27
  }
27
28
 
28
29
  export interface FormatSessionDumpTextOptions {
@@ -33,28 +34,6 @@ export interface FormatSessionDumpTextOptions {
33
34
  tools?: readonly SessionDumpToolInfo[];
34
35
  }
35
36
 
36
- function stripTypeBoxFields(obj: unknown): unknown {
37
- if (Array.isArray(obj)) {
38
- return obj.map(stripTypeBoxFields);
39
- }
40
- if (obj && typeof obj === "object") {
41
- const result: Record<string, unknown> = {};
42
- for (const [k, v] of Object.entries(obj)) {
43
- if (!k.startsWith("TypeBox.")) {
44
- result[k] = stripTypeBoxFields(v);
45
- }
46
- }
47
- return result;
48
- }
49
- return obj;
50
- }
51
-
52
- /** Resolve tool parameters to a plain JSON Schema object for dump output. */
53
- function toolParametersToJsonSchema(parameters: unknown): unknown {
54
- if (isZodSchema(parameters)) return zodToWireSchema(parameters);
55
- return stripTypeBoxFields(parameters);
56
- }
57
-
58
37
  /** Serialize an object as XML parameter elements, one per key. */
59
38
  function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
60
39
  const parts: string[] = [];
@@ -94,13 +73,13 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
94
73
  const tools = options.tools ?? [];
95
74
  if (tools.length > 0) {
96
75
  lines.push("## Available Tools\n");
97
- for (const tool of tools) {
98
- lines.push(`<tool name="${tool.name}">`);
99
- lines.push(tool.description);
100
- const parametersClean = toolParametersToJsonSchema(tool.parameters);
101
- lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
102
- lines.push("<" + "/tool>\n");
103
- }
76
+ const inventoryTools = tools.map(tool => ({
77
+ name: tool.name,
78
+ description: tool.description,
79
+ parameters: tool.parameters as TSchema,
80
+ examples: tool.examples,
81
+ }));
82
+ lines.push(renderToolInventory(inventoryTools, options.model?.id ?? ""));
104
83
  lines.push("\n");
105
84
  }
106
85
 
@@ -4,6 +4,8 @@
4
4
 
5
5
  import * as os from "node:os";
6
6
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
7
+ import type { ToolExample, TSchema } from "@oh-my-pi/pi-ai";
8
+ import { renderToolInventory } from "@oh-my-pi/pi-ai/grammar";
7
9
  import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
8
10
  import { $ } from "bun";
9
11
  import { contextFileCapability } from "./capability/context-file";
@@ -330,6 +332,10 @@ export interface SystemPromptToolMetadata {
330
332
  description: string;
331
333
  /** Tool name the model sees on the provider wire. Defaults to the internal tool name. */
332
334
  wireName?: string;
335
+ /** Tool parameters schema (Zod or JSON Schema), fed to the verbose inventory renderer. */
336
+ parameters?: TSchema;
337
+ /** Illustrative examples rendered into the verbose inventory. */
338
+ examples?: readonly ToolExample[];
333
339
  }
334
340
 
335
341
  export function buildSystemPromptToolMetadata(
@@ -349,6 +355,8 @@ export function buildSystemPromptToolMetadata(
349
355
  label: override?.label ?? (typeof toolRecord.label === "string" ? toolRecord.label : ""),
350
356
  description:
351
357
  override?.description ?? (typeof toolRecord.description === "string" ? toolRecord.description : ""),
358
+ parameters: toolRecord.parameters,
359
+ examples: toolRecord.examples,
352
360
  wireName,
353
361
  },
354
362
  ] as const;
@@ -367,6 +375,12 @@ export interface BuildSystemPromptOptions {
367
375
  appendSystemPrompt?: string;
368
376
  /** Repeat full tool descriptions in system prompt. Default: false */
369
377
  repeatToolDescriptions?: boolean;
378
+ /**
379
+ * Whether provider-native tool calling is active (no owned/in-band syntax).
380
+ * When true and `repeatToolDescriptions` is false, the inventory renders as a
381
+ * compact tool-name list; otherwise it renders full `# Tool:` sections. Default: true
382
+ */
383
+ nativeTools?: boolean;
370
384
  /** Skills settings for discovery. */
371
385
  skillsSettings?: SkillsSettings;
372
386
  /** Working directory. Default: getProjectDir() */
@@ -420,6 +434,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
420
434
  tools,
421
435
  appendSystemPrompt,
422
436
  repeatToolDescriptions = false,
437
+ nativeTools = true,
423
438
  skillsSettings,
424
439
  toolNames: providedToolNames,
425
440
  cwd,
@@ -575,6 +590,20 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
575
590
  label: tools?.get(name)?.label ?? "",
576
591
  description: tools?.get(name)?.description ?? "",
577
592
  }));
593
+ const inventoryTools = toolNames.map(name => {
594
+ const meta = tools?.get(name);
595
+ return {
596
+ name: toolPromptNames.get(name) ?? name,
597
+ description: meta?.description ?? "",
598
+ parameters: meta?.parameters ?? ({ type: "object" } as TSchema),
599
+ examples: meta?.examples,
600
+ };
601
+ });
602
+ // List mode shows a compact tool-name list; it only applies when descriptions
603
+ // are not repeated AND native tool calling is active (the model already has the
604
+ // schemas). Otherwise render full `# Tool:` sections.
605
+ const toolListMode = !repeatToolDescriptions && nativeTools;
606
+ const toolInventory = toolListMode ? "" : renderToolInventory(inventoryTools, model ?? "");
578
607
 
579
608
  // Filter skills for the rendered system prompt:
580
609
  // - require the `read` tool so the model can actually fetch skill content;
@@ -596,7 +625,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
596
625
  appendPrompt: resolvedAppendPrompt ?? "",
597
626
  tools: toolNames,
598
627
  toolInfo,
628
+ toolInventory,
599
629
  repeatToolDescriptions,
630
+ toolListMode,
600
631
  toolRefs,
601
632
  environment,
602
633
  contextFiles,
package/src/tools/ask.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
19
20
  import { type Component, Markdown, type MarkdownTheme, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
20
21
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
21
22
  import { z } from "zod/v4";
@@ -422,6 +423,46 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
422
423
  readonly description: string;
423
424
  readonly parameters = askSchema;
424
425
  readonly strict = true;
426
+
427
+ readonly examples: readonly ToolExample<z.input<typeof askSchema>>[] = [
428
+ {
429
+ caption: "Single question",
430
+ call: {
431
+ questions: [
432
+ {
433
+ id: "auth_method",
434
+ question: "Which authentication method should this API use?",
435
+ options: [
436
+ { label: "JWT", description: "Bearer tokens for stateless API clients." },
437
+ { label: "OAuth2", description: "Delegated authorization with external identity providers." },
438
+ {
439
+ label: "Session cookies",
440
+ description: "Browser-first authentication backed by server-side sessions.",
441
+ },
442
+ ],
443
+ recommended: 0,
444
+ },
445
+ ],
446
+ },
447
+ },
448
+ {
449
+ caption: "Multiple questions",
450
+ call: {
451
+ questions: [
452
+ {
453
+ id: "storage_type",
454
+ question: "Which storage backend?",
455
+ options: [{ label: "SQLite" }, { label: "PostgreSQL" }],
456
+ },
457
+ {
458
+ id: "auth_method",
459
+ question: "Which auth method?",
460
+ options: [{ label: "JWT" }, { label: "Session cookies" }],
461
+ },
462
+ ],
463
+ },
464
+ },
465
+ ];
425
466
  // Run alone in its tool batch. The interactive selector/editor is a single
426
467
  // shared UI surface (`ExtensionUiController.showHookSelector` has no queue and
427
468
  // overwrites `ctx.hookSelector` on each call), so two concurrent `ask` calls
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { formatHashlineHeader } from "@oh-my-pi/hashline";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
4
5
  import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
5
6
  import type { Component } from "@oh-my-pi/pi-tui";
6
7
  import { replaceTabs, Text } from "@oh-my-pi/pi-tui";
@@ -194,6 +195,51 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
194
195
  readonly description: string;
195
196
  readonly parameters = astEditSchema;
196
197
  readonly strict = true;
198
+
199
+ readonly examples: readonly ToolExample<z.input<typeof astEditSchema>>[] = [
200
+ {
201
+ caption: "Rename a call site across TypeScript files",
202
+ call: {
203
+ ops: [{ pat: "oldApi($$$ARGS)", out: "newApi($$$ARGS)" }],
204
+ paths: ["src/**/*.ts"],
205
+ },
206
+ },
207
+ {
208
+ caption: "Delete matching calls",
209
+ call: {
210
+ ops: [{ pat: "console.log($$$ARGS)", out: "" }],
211
+ paths: ["src/**/*.ts"],
212
+ },
213
+ },
214
+ {
215
+ caption: "Rewrite import source path",
216
+ call: {
217
+ ops: [{ pat: 'import { $$$IMPORTS } from "old-package"', out: 'import { $$$IMPORTS } from "new-package"' }],
218
+ paths: ["src/**/*.ts"],
219
+ },
220
+ },
221
+ {
222
+ caption: "Modernize to optional chaining (same metavariable enforces identity)",
223
+ call: {
224
+ ops: [{ pat: "$A && $A()", out: "$A?.()" }],
225
+ paths: ["src/**/*.ts"],
226
+ },
227
+ },
228
+ {
229
+ caption: "Swap two arguments using captures",
230
+ call: {
231
+ ops: [{ pat: "assertEqual($A, $B)", out: "assertEqual($B, $A)" }],
232
+ paths: ["tests/**/*.ts"],
233
+ },
234
+ },
235
+ {
236
+ caption: "Python — convert print calls to logging",
237
+ call: {
238
+ ops: [{ pat: "print($$$ARGS)", out: "logger.info($$$ARGS)" }],
239
+ paths: ["src/**/*.py"],
240
+ },
241
+ },
242
+ ];
197
243
  readonly deferrable = true;
198
244
  readonly loadMode = "discoverable";
199
245
  constructor(private readonly session: ToolSession) {
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { formatHashlineHeader } from "@oh-my-pi/hashline";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
4
5
  import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
5
6
  import type { Component } from "@oh-my-pi/pi-tui";
6
7
  import { Text } from "@oh-my-pi/pi-tui";
@@ -130,6 +131,29 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
130
131
  readonly description: string;
131
132
  readonly parameters = astGrepSchema;
132
133
  readonly strict = true;
134
+
135
+ readonly examples: readonly ToolExample<z.input<typeof astGrepSchema>>[] = [
136
+ {
137
+ caption: "Search TypeScript files under src",
138
+ call: { pat: "console.log($$$)", paths: ["src/**/*.ts"] },
139
+ },
140
+ {
141
+ caption: "Named imports from a specific package",
142
+ call: { pat: 'import { $$$IMPORTS } from "react"', paths: ["src/**/*.ts"] },
143
+ },
144
+ {
145
+ caption: "Arrow functions assigned to a const",
146
+ call: { pat: "const $NAME = ($$$ARGS) => $BODY", paths: ["src/utils/**/*.ts"] },
147
+ },
148
+ {
149
+ caption: "Method call on any object, ignoring method name with `$_`",
150
+ call: { pat: "logger.$_($$$ARGS)", paths: ["src/**/*.ts"] },
151
+ },
152
+ {
153
+ caption: "Loosest existence check for a symbol in one file",
154
+ call: { pat: "processItems", paths: ["src/worker.ts"] },
155
+ },
156
+ ];
133
157
  readonly loadMode = "discoverable";
134
158
 
135
159
  constructor(private readonly session: ToolSession) {
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
2
3
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
3
4
  import { z } from "zod/v4";
4
5
  import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
@@ -118,6 +119,57 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
118
119
  readonly parameters = browserSchema;
119
120
  readonly strict = true;
120
121
 
122
+ readonly examples: readonly ToolExample<z.input<typeof browserSchema>>[] = [
123
+ {
124
+ caption: "Open a tab",
125
+ call: { action: "open", name: "docs", url: "https://example.com" },
126
+ },
127
+ {
128
+ caption: "Read structured page data in the opened tab",
129
+ call: {
130
+ action: "run",
131
+ name: "docs",
132
+ code: "const obs = await tab.observe(); display(obs); return obs.elements.length;",
133
+ },
134
+ },
135
+ {
136
+ caption: "Click an observed element by id",
137
+ call: {
138
+ action: "run",
139
+ name: "docs",
140
+ code: "const obs = await tab.observe(); const link = obs.elements.find(e => e.role === 'link' && e.name === 'Sign in'); assert(link, 'Sign in link missing'); await (await tab.id(link.id)).click();",
141
+ },
142
+ },
143
+ {
144
+ caption: "Fill and submit a form via selectors",
145
+ call: {
146
+ action: "run",
147
+ name: "docs",
148
+ code: "await tab.fill('input[name=email]', 'me@example.com'); await tab.click('text/Continue');",
149
+ },
150
+ },
151
+ {
152
+ caption: "Screenshot to look at the page — no save path",
153
+ call: {
154
+ action: "run",
155
+ name: "docs",
156
+ code: "await tab.screenshot();",
157
+ },
158
+ },
159
+ {
160
+ caption: "Attach to an existing Electron app",
161
+ call: {
162
+ action: "open",
163
+ name: "cursor",
164
+ app: { path: "/Applications/Cursor.app/Contents/MacOS/Cursor" },
165
+ },
166
+ },
167
+ {
168
+ caption: "Close every tab and kill spawned-app processes",
169
+ call: { action: "close", all: true, kill: true },
170
+ },
171
+ ];
172
+
121
173
  constructor(private readonly session: ToolSession) {}
122
174
  #description?: string;
123
175
  get description(): string {
@@ -7,6 +7,7 @@ import type {
7
7
  RenderResultOptions,
8
8
  ToolApprovalDecision,
9
9
  } from "@oh-my-pi/pi-agent-core";
10
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
10
11
  import { type Component, Text } from "@oh-my-pi/pi-tui";
11
12
  import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
12
13
  import { z } from "zod/v4";
@@ -659,6 +660,22 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
659
660
  readonly description: string;
660
661
  readonly parameters = debugSchema;
661
662
  readonly strict = true;
663
+
664
+ readonly examples: readonly ToolExample<z.input<typeof debugSchema>>[] = [
665
+ {
666
+ caption: "Launch and inspect hang",
667
+ note: '1. debug(action: "launch", program: "./my_app")\n2. debug(action: "set_breakpoint", file: "src/main.c", line: 42)\n3. debug(action: "continue")\n4. If the program appears hung: debug(action: "pause")\n5. Inspect state with `threads`, `stack_trace`, `scopes`, and `variables`',
668
+ },
669
+ {
670
+ caption: "Launch a Python script with debugpy",
671
+ call: { action: "launch", adapter: "debugpy", program: "scripts/job.py", args: ["--flag"] },
672
+ },
673
+ {
674
+ caption: "Raw debugger command through repl",
675
+ call: { action: "evaluate", expression: "info registers", context: "repl" },
676
+ },
677
+ ];
678
+
662
679
  readonly concurrency = "exclusive";
663
680
  readonly loadMode = "discoverable";
664
681
 
package/src/tools/eval.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import type { ImageContent } from "@oh-my-pi/pi-ai";
2
+ import type { ImageContent, ToolExample } from "@oh-my-pi/pi-ai";
3
3
  import { prompt } from "@oh-my-pi/pi-utils";
4
4
  import { z } from "zod/v4";
5
5
  import { jsBackend, pythonBackend } from "../eval";
@@ -183,6 +183,25 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
183
183
  const spawnsAllowed = sessionSpawns !== "" && sessionSpawns !== null;
184
184
  return getEvalToolDescription({ py: backends.python, js: backends.js, spawns: spawnsAllowed });
185
185
  }
186
+ readonly examples: readonly ToolExample<z.input<typeof evalSchema>>[] = [
187
+ {
188
+ call: {
189
+ cells: [
190
+ {
191
+ language: "py",
192
+ title: "imports",
193
+ timeout: 10,
194
+ code: "import json\nfrom pathlib import Path",
195
+ },
196
+ {
197
+ language: "py",
198
+ title: "load config",
199
+ code: "data = json.loads(read('package.json'))\ndisplay(data)",
200
+ },
201
+ ],
202
+ },
203
+ },
204
+ ];
186
205
  readonly parameters = evalSchema;
187
206
  readonly concurrency = "exclusive";
188
207
  readonly strict = true;
package/src/tools/find.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
4
5
  import * as natives from "@oh-my-pi/pi-natives";
5
6
  import type { Component } from "@oh-my-pi/pi-tui";
6
7
  import { Text } from "@oh-my-pi/pi-tui";
@@ -106,6 +107,29 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
106
107
  readonly label = "Find";
107
108
  readonly description: string;
108
109
  readonly parameters = findSchema;
110
+
111
+ readonly examples: readonly ToolExample<z.input<typeof findSchema>>[] = [
112
+ {
113
+ caption: "Find files",
114
+ call: { paths: ["src/**/*.ts"] },
115
+ },
116
+ {
117
+ caption: "Multiple targets — separate array elements",
118
+ call: { paths: ["src/**/*.ts", "test/**/*.ts"] },
119
+ },
120
+ {
121
+ caption: "Find gitignored files like .env",
122
+ call: { paths: [".env*"], gitignore: false },
123
+ },
124
+ {
125
+ caption: "Find directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
126
+ call: { paths: ["**/tests"] },
127
+ },
128
+ {
129
+ caption: "Long-running search on a slow volume",
130
+ call: { paths: ["/Volumes/Storage/**/*.py"], timeout: 30 },
131
+ },
132
+ ];
109
133
  readonly strict = true;
110
134
 
111
135
  readonly #customOps?: FindOperations;