@oh-my-pi/pi-coding-agent 15.13.1 → 15.13.3

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 (109) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/cli.js +1057 -289
  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 +97 -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/eval/js/context-manager.d.ts +15 -0
  10. package/dist/types/modes/components/welcome.d.ts +1 -0
  11. package/dist/types/modes/controllers/input-controller.d.ts +4 -4
  12. package/dist/types/modes/interactive-mode.d.ts +1 -0
  13. package/dist/types/modes/rpc/rpc-types.d.ts +2 -1
  14. package/dist/types/modes/types.d.ts +6 -0
  15. package/dist/types/sdk.d.ts +3 -0
  16. package/dist/types/session/session-dump-format.d.ts +2 -1
  17. package/dist/types/session/unexpected-stop-classifier.d.ts +13 -0
  18. package/dist/types/stt/asr-client.d.ts +1 -1
  19. package/dist/types/system-prompt.d.ts +11 -0
  20. package/dist/types/tiny/title-client.d.ts +1 -1
  21. package/dist/types/tools/ask.d.ts +2 -0
  22. package/dist/types/tools/ast-edit.d.ts +2 -0
  23. package/dist/types/tools/ast-grep.d.ts +2 -0
  24. package/dist/types/tools/browser.d.ts +2 -0
  25. package/dist/types/tools/debug.d.ts +2 -0
  26. package/dist/types/tools/eval.d.ts +2 -0
  27. package/dist/types/tools/find.d.ts +2 -0
  28. package/dist/types/tools/inspect-image.d.ts +2 -1
  29. package/dist/types/tools/irc.d.ts +2 -0
  30. package/dist/types/tools/job.d.ts +1 -0
  31. package/dist/types/tools/ssh.d.ts +2 -0
  32. package/dist/types/tools/todo.d.ts +2 -0
  33. package/dist/types/tts/tts-client.d.ts +1 -1
  34. package/dist/types/tui/tree-list.d.ts +1 -0
  35. package/dist/types/utils/thinking-display.d.ts +1 -17
  36. package/package.json +12 -12
  37. package/src/cli.ts +25 -12
  38. package/src/config/model-registry.ts +16 -2
  39. package/src/config/models-config-schema.ts +2 -0
  40. package/src/config/models-config.ts +1 -0
  41. package/src/config/settings-schema.ts +78 -0
  42. package/src/edit/hashline/block-resolver.ts +1 -1
  43. package/src/edit/hashline/execute.ts +1 -6
  44. package/src/edit/index.ts +48 -0
  45. package/src/eval/__tests__/agent-bridge.test.ts +106 -46
  46. package/src/eval/__tests__/js-context-manager.test.ts +53 -3
  47. package/src/eval/js/context-manager.ts +132 -29
  48. package/src/eval/js/worker-core.ts +1 -1
  49. package/src/eval/js/worker-entry.ts +7 -0
  50. package/src/export/html/template.js +18 -22
  51. package/src/internal-urls/docs-index.generated.ts +12 -3
  52. package/src/main.ts +15 -5
  53. package/src/modes/acp/acp-agent.ts +2 -2
  54. package/src/modes/acp/acp-event-mapper.ts +2 -2
  55. package/src/modes/components/agent-hub.ts +31 -7
  56. package/src/modes/components/assistant-message.ts +24 -15
  57. package/src/modes/components/snapcompact-shape-preview-doc.md +2 -2
  58. package/src/modes/components/snapcompact-shape-preview.ts +2 -2
  59. package/src/modes/components/tree-selector.ts +3 -2
  60. package/src/modes/components/welcome.ts +14 -4
  61. package/src/modes/controllers/event-controller.ts +3 -3
  62. package/src/modes/controllers/input-controller.ts +28 -39
  63. package/src/modes/controllers/streaming-reveal.ts +4 -4
  64. package/src/modes/interactive-mode.ts +2 -0
  65. package/src/modes/rpc/rpc-mode.ts +1 -0
  66. package/src/modes/rpc/rpc-types.ts +2 -2
  67. package/src/modes/types.ts +6 -0
  68. package/src/modes/utils/ui-helpers.ts +3 -3
  69. package/src/prompts/agents/oracle.md +0 -1
  70. package/src/prompts/agents/reviewer.md +0 -1
  71. package/src/prompts/system/system-prompt.md +17 -21
  72. package/src/prompts/system/unexpected-stop-classifier.md +17 -0
  73. package/src/prompts/system/unexpected-stop-retry.md +4 -0
  74. package/src/prompts/tools/ask.md +0 -8
  75. package/src/prompts/tools/ast-edit.md +0 -15
  76. package/src/prompts/tools/ast-grep.md +0 -13
  77. package/src/prompts/tools/browser.md +0 -21
  78. package/src/prompts/tools/debug.md +0 -13
  79. package/src/prompts/tools/eval.md +0 -9
  80. package/src/prompts/tools/find.md +0 -13
  81. package/src/prompts/tools/inspect-image.md +0 -9
  82. package/src/prompts/tools/irc.md +0 -15
  83. package/src/prompts/tools/patch.md +0 -13
  84. package/src/prompts/tools/ssh.md +0 -9
  85. package/src/prompts/tools/todo.md +1 -19
  86. package/src/sdk.ts +19 -0
  87. package/src/session/agent-session.ts +289 -29
  88. package/src/session/session-dump-format.ts +17 -49
  89. package/src/session/unexpected-stop-classifier.ts +129 -0
  90. package/src/stt/asr-client.ts +1 -1
  91. package/src/system-prompt.ts +31 -0
  92. package/src/tiny/title-client.ts +1 -1
  93. package/src/tools/ask.ts +41 -0
  94. package/src/tools/ast-edit.ts +46 -0
  95. package/src/tools/ast-grep.ts +24 -0
  96. package/src/tools/browser/tab-supervisor.ts +1 -1
  97. package/src/tools/browser/tab-worker-entry.ts +12 -4
  98. package/src/tools/browser.ts +52 -0
  99. package/src/tools/debug.ts +17 -0
  100. package/src/tools/eval.ts +20 -1
  101. package/src/tools/find.ts +24 -0
  102. package/src/tools/inspect-image.ts +27 -1
  103. package/src/tools/irc.ts +41 -0
  104. package/src/tools/job.ts +1 -0
  105. package/src/tools/ssh.ts +16 -0
  106. package/src/tools/todo.ts +82 -3
  107. package/src/tts/tts-client.ts +1 -1
  108. package/src/tui/tree-list.ts +68 -19
  109. package/src/utils/thinking-display.ts +8 -34
@@ -0,0 +1,129 @@
1
+ import { type AssistantMessage, completeSimple } from "@oh-my-pi/pi-ai";
2
+ import { logger, prompt } from "@oh-my-pi/pi-utils";
3
+
4
+ import type { ModelRegistry } from "../config/model-registry";
5
+ import { resolveRoleSelection } from "../config/model-resolver";
6
+ import type { Settings } from "../config/settings";
7
+ import unexpectedStopClassifierPrompt from "../prompts/system/unexpected-stop-classifier.md" with { type: "text" };
8
+ import { isTinyMemoryLocalModelKey, ONLINE_MEMORY_MODEL_KEY } from "../tiny/models";
9
+ import { tinyModelClient } from "../tiny/title-client";
10
+
11
+ const CLASSIFIER_SYSTEM_PROMPT = prompt.render(unexpectedStopClassifierPrompt);
12
+
13
+ /**
14
+ * The answer is a single word. OpenAI-compatible endpoints reject values below
15
+ * 16, so 16 is the smallest portable budget for this classifier.
16
+ */
17
+ const ANSWER_MAX_TOKENS = 16;
18
+ /**
19
+ * Reasoning backends ignore `disableReasoning` on some providers, so reserve
20
+ * enough output room for the keyword to still land after unavoidable thinking.
21
+ */
22
+ const REASONING_SAFE_MAX_TOKENS = 1024;
23
+
24
+ export interface ClassifyUnexpectedStopDeps {
25
+ settings: Settings;
26
+ registry: ModelRegistry;
27
+ sessionId: string;
28
+ metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
29
+ signal?: AbortSignal;
30
+ }
31
+
32
+ export function isUnexpectedStopCandidate(message: AssistantMessage): boolean {
33
+ if (message.stopReason !== "stop") return false;
34
+ let hasText = false;
35
+ for (const content of message.content) {
36
+ if (content.type === "toolCall") return false;
37
+ if (content.type === "text" && /\S/.test(content.text)) {
38
+ hasText = true;
39
+ }
40
+ }
41
+ return hasText;
42
+ }
43
+
44
+ export async function classifyUnexpectedStop(
45
+ text: string,
46
+ deps: ClassifyUnexpectedStopDeps,
47
+ ): Promise<boolean | undefined> {
48
+ const backend = deps.settings.get("providers.unexpectedStopModel");
49
+ try {
50
+ if (backend === ONLINE_MEMORY_MODEL_KEY) {
51
+ return await classifyOnline(text, deps);
52
+ }
53
+ if (isTinyMemoryLocalModelKey(backend)) {
54
+ return await classifyLocal(text, backend, deps);
55
+ }
56
+ return undefined;
57
+ } catch (error) {
58
+ logger.debug("unexpected-stop: classification failed", {
59
+ error: error instanceof Error ? error.message : String(error),
60
+ backend,
61
+ });
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ async function classifyOnline(text: string, deps: ClassifyUnexpectedStopDeps): Promise<boolean | undefined> {
67
+ const resolved = resolveRoleSelection(["smol"], deps.settings, deps.registry.getAvailable(), deps.registry);
68
+ const model = resolved?.model;
69
+ if (!model) {
70
+ throw new Error("unexpected-stop: no smol model available for classification");
71
+ }
72
+ const apiKey = await deps.registry.getApiKey(model, deps.sessionId);
73
+ if (!apiKey) {
74
+ throw new Error(`unexpected-stop: no API key for ${model.provider}/${model.id}`);
75
+ }
76
+ const metadata = deps.metadataResolver?.(model.provider);
77
+ const maxTokens = model.reasoning ? Math.max(ANSWER_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS) : ANSWER_MAX_TOKENS;
78
+
79
+ const response = await completeSimple(
80
+ model,
81
+ {
82
+ systemPrompt: [CLASSIFIER_SYSTEM_PROMPT],
83
+ messages: [{ role: "user", content: text, timestamp: Date.now() }],
84
+ },
85
+ {
86
+ apiKey: deps.registry.resolver(model, deps.sessionId),
87
+ maxTokens,
88
+ disableReasoning: true,
89
+ metadata,
90
+ signal: deps.signal,
91
+ },
92
+ );
93
+
94
+ if (response.stopReason === "error") {
95
+ throw new Error(`unexpected-stop: online classification failed: ${response.errorMessage ?? "unknown error"}`);
96
+ }
97
+
98
+ const outputText = response.content
99
+ .filter((part): part is { type: "text"; text: string } => part.type === "text")
100
+ .map(part => part.text)
101
+ .join("\n");
102
+ return parseUnexpectedStopClassification(outputText);
103
+ }
104
+
105
+ async function classifyLocal(
106
+ text: string,
107
+ modelKey: string,
108
+ deps: ClassifyUnexpectedStopDeps,
109
+ ): Promise<boolean | undefined> {
110
+ if (!isTinyMemoryLocalModelKey(modelKey)) {
111
+ throw new Error(`unexpected-stop: unsupported local classifier model: ${modelKey}`);
112
+ }
113
+ const builtPrompt = prompt.render(unexpectedStopClassifierPrompt, { message: text });
114
+ const output = await tinyModelClient.complete(modelKey, builtPrompt, {
115
+ maxTokens: ANSWER_MAX_TOKENS,
116
+ signal: deps.signal,
117
+ });
118
+ if (!output) {
119
+ return undefined;
120
+ }
121
+ return parseUnexpectedStopClassification(output);
122
+ }
123
+
124
+ export function parseUnexpectedStopClassification(text: string): boolean | undefined {
125
+ const trimmed = text.trim().toLowerCase();
126
+ if (trimmed.startsWith("yes")) return true;
127
+ if (trimmed.startsWith("no")) return false;
128
+ return undefined;
129
+ }
@@ -72,7 +72,7 @@ const SMOKE_TEST_TIMEOUT_MS = 30_000;
72
72
  * Hidden subcommand on the main CLI that boots the speech-recognition worker in
73
73
  * the spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
74
74
  */
75
- export const STT_WORKER_ARG = "__omp_stt_worker";
75
+ export const STT_WORKER_ARG = "__omp_worker_stt";
76
76
 
77
77
  function readTinyModelSetting(key: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
78
78
  try {
@@ -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,
@@ -69,7 +69,7 @@ function normalizeTinyTitleGenerateOptions(
69
69
  * Hidden subcommand on the main CLI that boots the tiny-model worker in the
70
70
  * spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
71
71
  */
72
- export const TINY_WORKER_ARG = "--tiny-worker";
72
+ export const TINY_WORKER_ARG = "__omp_worker_tiny_inference";
73
73
 
74
74
  function readTinyModelSetting(path: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
75
75
  try {
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) {
@@ -685,7 +685,7 @@ async function spawnTabWorker(): Promise<WorkerHandle> {
685
685
  try {
686
686
  const hostEntry = workerHostEntry();
687
687
  const worker = hostEntry
688
- ? new Worker(hostEntry, { type: "module", argv: ["__omp_tab_worker"] })
688
+ ? new Worker(hostEntry, { type: "module", argv: ["__omp_worker_tab"] })
689
689
  : new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" });
690
690
  return wrapBunWorker(worker);
691
691
  } catch (err) {
@@ -1,20 +1,28 @@
1
1
  import { parentPort } from "node:worker_threads";
2
+ import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
2
3
  import type { Transport, WorkerInbound, WorkerOutbound } from "./tab-protocol";
3
4
  import { WorkerCore } from "./tab-worker";
4
5
 
5
6
  if (!parentPort) throw new Error("tab-worker-entry: missing parentPort");
6
7
 
8
+ const port = parentPort;
9
+ // When the CLI host pre-buffered messages (it imports this module dynamically),
10
+ // bind that inbox so the parent's already-delivered `init` is replayed. Loaded
11
+ // directly (test/SDK fallback), this module's top-level runs synchronously at
12
+ // worker start, so the direct `parentPort.on` below wins the flush on its own.
13
+ const inbox = consumeWorkerInbox();
7
14
  const transport: Transport = {
8
15
  send(msg, transferList) {
9
- parentPort!.postMessage(msg, transferList ?? []);
16
+ port.postMessage(msg, transferList ?? []);
10
17
  },
11
18
  onMessage(handler) {
19
+ if (inbox) return inbox.bind(data => handler(data as WorkerOutbound | WorkerInbound));
12
20
  const wrap = (message: unknown): void => handler(message as WorkerOutbound | WorkerInbound);
13
- parentPort!.on("message", wrap);
14
- return () => parentPort!.off("message", wrap);
21
+ port.on("message", wrap);
22
+ return () => port.off("message", wrap);
15
23
  },
16
24
  close() {
17
- parentPort!.close();
25
+ port.close();
18
26
  },
19
27
  };
20
28
 
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
3
- import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
3
+ import { type Api, completeSimple, type Model, type ToolExample } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import { z } from "zod/v4";
6
6
  import { extractTextContent } from "../commit/utils";
@@ -43,6 +43,32 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
43
43
  readonly parameters = inspectImageSchema;
44
44
  readonly strict = false;
45
45
 
46
+ readonly examples: readonly ToolExample<z.input<typeof inspectImageSchema>>[] = [
47
+ {
48
+ caption: "OCR with strict formatting",
49
+ call: {
50
+ path: "screenshots/error.png",
51
+ question: "Extract all visible text verbatim. Return as bullet list in reading order.",
52
+ },
53
+ },
54
+ {
55
+ caption: "Screenshot debugging",
56
+ call: {
57
+ path: "screenshots/settings.png",
58
+ question:
59
+ "Identify the likely cause of the disabled Save button. Return: (1) observations, (2) likely cause, (3) confidence.",
60
+ },
61
+ },
62
+ {
63
+ caption: "Scene/object question",
64
+ call: {
65
+ path: "photos/shelf.jpg",
66
+ question:
67
+ "List all clearly visible product labels and their shelf positions (top/middle/bottom). If unreadable, say unreadable.",
68
+ },
69
+ },
70
+ ];
71
+
46
72
  constructor(
47
73
  private readonly session: ToolSession,
48
74
  private readonly completeImageRequest: typeof completeSimple = completeSimple,