@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.13.1",
4
+ "version": "15.13.3",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,17 +47,17 @@
47
47
  "@agentclientprotocol/sdk": "0.25.0",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.13.1",
51
- "@oh-my-pi/omp-stats": "15.13.1",
52
- "@oh-my-pi/pi-agent-core": "15.13.1",
53
- "@oh-my-pi/pi-ai": "15.13.1",
54
- "@oh-my-pi/pi-catalog": "15.13.1",
55
- "@oh-my-pi/pi-mnemopi": "15.13.1",
56
- "@oh-my-pi/pi-natives": "15.13.1",
57
- "@oh-my-pi/pi-tui": "15.13.1",
58
- "@oh-my-pi/pi-utils": "15.13.1",
59
- "@oh-my-pi/pi-wire": "15.13.1",
60
- "@oh-my-pi/snapcompact": "15.13.1",
50
+ "@oh-my-pi/hashline": "15.13.3",
51
+ "@oh-my-pi/omp-stats": "15.13.3",
52
+ "@oh-my-pi/pi-agent-core": "15.13.3",
53
+ "@oh-my-pi/pi-ai": "15.13.3",
54
+ "@oh-my-pi/pi-catalog": "15.13.3",
55
+ "@oh-my-pi/pi-mnemopi": "15.13.3",
56
+ "@oh-my-pi/pi-natives": "15.13.3",
57
+ "@oh-my-pi/pi-tui": "15.13.3",
58
+ "@oh-my-pi/pi-utils": "15.13.3",
59
+ "@oh-my-pi/pi-wire": "15.13.3",
60
+ "@oh-my-pi/snapcompact": "15.13.3",
61
61
  "@opentelemetry/api": "^1.9.1",
62
62
  "@opentelemetry/context-async-hooks": "^2.7.1",
63
63
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
package/src/cli.ts CHANGED
@@ -14,6 +14,7 @@ try {
14
14
  * CLI entry point — registers all commands explicitly and delegates to the
15
15
  * lightweight CLI runner from pi-utils.
16
16
  */
17
+ import { parentPort } from "node:worker_threads";
17
18
  import type { CliConfig } from "@oh-my-pi/pi-utils/cli";
18
19
  import {
19
20
  APP_NAME,
@@ -23,7 +24,7 @@ import {
23
24
  setProfile,
24
25
  VERSION,
25
26
  } from "@oh-my-pi/pi-utils/dirs";
26
- import { declareWorkerHostEntry } from "@oh-my-pi/pi-utils/worker-host";
27
+ import { declareWorkerHostEntry, installWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
27
28
  import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias";
28
29
  import { extractProfileFlags } from "./cli/profile-bootstrap";
29
30
 
@@ -67,6 +68,7 @@ async function runSmokeTest(): Promise<void> {
67
68
  const { smokeTestTinyTitleWorker } = await import("./tiny/title-client");
68
69
  const { smokeTestSttWorker } = await import("./stt/asr-client");
69
70
  const { smokeTestTtsWorker } = await import("./tts/tts-client");
71
+ const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
70
72
  await smokeTestSyncWorker();
71
73
 
72
74
  const statsServer = await startServer(0);
@@ -83,18 +85,23 @@ async function runSmokeTest(): Promise<void> {
83
85
 
84
86
  await smokeTestTinyTitleWorker();
85
87
  await smokeTestSttWorker();
88
+ await smokeTestJsEvalWorker();
86
89
  await smokeTestTtsWorker();
87
90
  process.stdout.write("smoke-test: ok\n");
88
91
  }
89
92
 
90
- const TINY_WORKER_ARGS = new Set(["--tiny-worker", "__tiny_worker"]);
91
- const STATS_SYNC_WORKER_ARG = "__omp_stats_sync_worker";
92
- const TAB_WORKER_ARG = "__omp_tab_worker";
93
- const JS_EVAL_WORKER_ARG = "__omp_js_eval_worker";
94
- const STT_WORKER_ARG = "__omp_stt_worker";
95
- const TTS_WORKER_ARG = "__omp_tts_worker";
93
+ const TINY_WORKER_ARG = "__omp_worker_tiny_inference";
94
+ const STATS_SYNC_WORKER_ARG = "__omp_worker_stats_sync";
95
+ const TAB_WORKER_ARG = "__omp_worker_tab";
96
+ const JS_EVAL_WORKER_ARG = "__omp_worker_js_eval";
97
+ const STT_WORKER_ARG = "__omp_worker_stt";
98
+ const TTS_WORKER_ARG = "__omp_worker_tts";
96
99
 
97
100
  async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
101
+ if (arg === TINY_WORKER_ARG) {
102
+ await runTinyWorker();
103
+ return true;
104
+ }
98
105
  if (arg === STATS_SYNC_WORKER_ARG) {
99
106
  // The sync worker handles messages via `self.onmessage`, assigned during
100
107
  // this *async* dynamic import. Bun flushes the worker's initial message
@@ -117,11 +124,20 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
117
124
  }
118
125
  return true;
119
126
  }
127
+ // Bun flushes messages the parent posted before spawn once this entry's
128
+ // top-level evaluation completes, delivering them only to listeners present
129
+ // at that moment. These worker modules are imported dynamically below, so
130
+ // their own `parentPort.on("message")` lands after the flush and the parent's
131
+ // synchronous `init` is dropped. Install a buffering inbox synchronously here
132
+ // (still inside the entry's sync prefix) so the handshake survives; the worker
133
+ // module binds the real handler once loaded.
120
134
  if (arg === TAB_WORKER_ARG) {
135
+ if (parentPort) installWorkerInbox(parentPort);
121
136
  await import("./tools/browser/tab-worker-entry");
122
137
  return true;
123
138
  }
124
139
  if (arg === JS_EVAL_WORKER_ARG) {
140
+ if (parentPort) installWorkerInbox(parentPort);
125
141
  await import("./eval/js/worker-entry");
126
142
  return true;
127
143
  }
@@ -251,11 +267,8 @@ export async function runCli(argv: string[]): Promise<void> {
251
267
  // synchronous prefix of `runWorkerEntrypoint`, and Bun flushes the
252
268
  // worker's parked initial messages as soon as the entry module's
253
269
  // top-level evaluation finishes.
254
- if (TINY_WORKER_ARGS.has(resolvedArgv[0] ?? "")) {
255
- await runTinyWorker();
256
- return;
257
- }
258
- if (await runWorkerEntrypoint(resolvedArgv[0])) {
270
+ if (resolvedArgv[0]?.startsWith("__omp_worker_")) {
271
+ await runWorkerEntrypoint(resolvedArgv[0]);
259
272
  return;
260
273
  }
261
274
 
@@ -59,7 +59,7 @@ import {
59
59
  resolveCanonicalVariant,
60
60
  resolveModelReference,
61
61
  } from "@oh-my-pi/pi-catalog/identity";
62
- import { isRecord, logger } from "@oh-my-pi/pi-utils";
62
+ import { isBunTestRuntime, isRecord, logger } from "@oh-my-pi/pi-utils";
63
63
  import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
64
64
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
65
65
  import { type ApiKeyResolverModel, type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
@@ -130,11 +130,13 @@ export function mergeDiscoveredModel<TApi extends Api>(
130
130
  providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
131
131
  ): Model<TApi> {
132
132
  if (existing) {
133
+ const supportsTools = model.supportsTools ?? existing.supportsTools;
133
134
  return buildModel({
134
135
  ...model,
135
136
  baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
136
137
  headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
137
138
  transport: providerOverride?.transport ?? existing.transport ?? model.transport,
139
+ ...(supportsTools !== undefined ? { supportsTools } : {}),
138
140
  compat: model.compatConfig,
139
141
  } as ModelSpec<TApi>);
140
142
  }
@@ -370,6 +372,7 @@ interface ModelPatch {
370
372
  reasoning?: boolean;
371
373
  thinking?: ThinkingConfig;
372
374
  input?: ("text" | "image")[];
375
+ supportsTools?: boolean;
373
376
  cost?: Partial<Model<Api>["cost"]>;
374
377
  contextWindow?: number;
375
378
  maxTokens?: number;
@@ -395,6 +398,7 @@ function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTr
395
398
  if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
396
399
  if (patch.thinking !== undefined) result.thinking = patch.thinking;
397
400
  if (patch.input !== undefined) result.input = patch.input;
401
+ if (patch.supportsTools !== undefined) result.supportsTools = patch.supportsTools;
398
402
  if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
399
403
  if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
400
404
  if (patch.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = patch.omitMaxOutputTokens;
@@ -506,6 +510,7 @@ function buildCustomModelOverlay(
506
510
  reasoning: modelDef.reasoning,
507
511
  thinking: modelDef.thinking,
508
512
  input: modelDef.input,
513
+ supportsTools: modelDef.supportsTools,
509
514
  cost: modelDef.cost,
510
515
  contextWindow: modelDef.contextWindow,
511
516
  maxTokens: modelDef.maxTokens,
@@ -535,6 +540,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
535
540
  reference?.cost ??
536
541
  (options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
537
542
  const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
543
+ const supportsTools = resolvedModel.supportsTools ?? reference?.supportsTools;
538
544
  return buildModel({
539
545
  id: resolvedModel.id,
540
546
  name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
@@ -544,6 +550,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
544
550
  reasoning: resolvedModel.reasoning ?? reference?.reasoning ?? (options.useDefaults ? false : undefined),
545
551
  thinking: resolvedModel.thinking ?? reference?.thinking,
546
552
  input: input as ("text" | "image")[],
553
+ ...(supportsTools !== undefined ? { supportsTools } : {}),
547
554
  cost,
548
555
  contextWindow: resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : null),
549
556
  maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : null),
@@ -683,7 +690,11 @@ export class ModelRegistry {
683
690
  modelsPath?: string,
684
691
  options?: { fetch?: FetchImpl },
685
692
  ) {
686
- this.#fetch = options?.fetch ?? fetch;
693
+ this.#fetch =
694
+ options?.fetch ??
695
+ (isBunTestRuntime()
696
+ ? () => Promise.reject(new Error("network disabled in model-registry runtime test"))
697
+ : fetch);
687
698
  this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
688
699
  this.#cacheDbPath = modelsPath ? path.join(path.dirname(modelsPath), "models.db") : undefined;
689
700
  // Set up fallback resolver for custom provider API keys
@@ -878,10 +889,12 @@ export class ModelRegistry {
878
889
  #mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
879
890
  return mergeByModelKey(baseModels, replacementModels, (existing, replacementModel) => {
880
891
  if (!existing) return replacementModel;
892
+ const supportsTools = replacementModel.supportsTools ?? existing.supportsTools;
881
893
  return {
882
894
  ...replacementModel,
883
895
  contextWindow: replacementModel.contextWindow ?? existing.contextWindow,
884
896
  maxTokens: replacementModel.maxTokens ?? existing.maxTokens,
897
+ ...(supportsTools !== undefined ? { supportsTools } : {}),
885
898
  };
886
899
  });
887
900
  }
@@ -2205,6 +2218,7 @@ export interface ProviderConfigInput {
2205
2218
  reasoning: boolean;
2206
2219
  thinking?: ThinkingConfig;
2207
2220
  input: ("text" | "image")[];
2221
+ supportsTools?: boolean;
2208
2222
  cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
2209
2223
  contextWindow: number;
2210
2224
  maxTokens: number;
@@ -133,6 +133,7 @@ const ModelDefinitionSchema = z.object({
133
133
  reasoning: z.boolean().optional(),
134
134
  thinking: ModelThinkingSchema.optional(),
135
135
  input: z.array(z.enum(["text", "image"])).optional(),
136
+ supportsTools: z.boolean().optional(),
136
137
  cost: z
137
138
  .object({
138
139
  input: z.number(),
@@ -155,6 +156,7 @@ export const ModelOverrideSchema = z.object({
155
156
  reasoning: z.boolean().optional(),
156
157
  thinking: ModelThinkingSchema.optional(),
157
158
  input: z.array(z.enum(["text", "image"])).optional(),
159
+ supportsTools: z.boolean().optional(),
158
160
  cost: z
159
161
  .object({
160
162
  input: z.number().optional(),
@@ -17,6 +17,7 @@ export interface ProviderValidationModel {
17
17
  id: string;
18
18
  api?: Api;
19
19
  contextWindow?: number;
20
+ supportsTools?: boolean;
20
21
  maxTokens?: number;
21
22
  }
22
23
 
@@ -116,6 +116,7 @@ export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
116
116
  "Magic Keywords",
117
117
  "Startup & Updates",
118
118
  "Power (macOS)",
119
+ "Agent",
119
120
  ],
120
121
  context: ["General", "Compaction", "Rules (TTSR)", "Experimental"],
121
122
  memory: ["General", "Auto-Learn", "Mnemopi", "Hindsight"],
@@ -1719,6 +1720,48 @@ export const SETTINGS_SCHEMA = {
1719
1720
  },
1720
1721
  },
1721
1722
 
1723
+ "tools.format": {
1724
+ type: "enum",
1725
+ values: [
1726
+ "auto",
1727
+ "native",
1728
+ "glm",
1729
+ "hermes",
1730
+ "kimi",
1731
+ "xml",
1732
+ "anthropic",
1733
+ "deepseek",
1734
+ "harmony",
1735
+ "pi",
1736
+ "qwen3",
1737
+ ] as const,
1738
+ default: "auto",
1739
+ ui: {
1740
+ tab: "context",
1741
+ group: "Experimental",
1742
+ label: "Tool Call Format",
1743
+ description:
1744
+ "Controls how tools are exposed to the model. Auto uses native tool calls unless the selected model is marked as not supporting tools, then falls back to GLM-style in-band tool calls. Native forces provider-native tools; the other values force the named in-band syntax. Applies on session start.",
1745
+ options: [
1746
+ {
1747
+ value: "auto",
1748
+ label: "Auto",
1749
+ description: "Use native tool calls unless the model is known not to support them.",
1750
+ },
1751
+ { value: "native", label: "Native", description: "Use provider-native tool calls." },
1752
+ { value: "glm", label: "GLM", description: "Use GLM-style in-band tool calls." },
1753
+ { value: "hermes", label: "Hermes", description: "Use Hermes-style in-band tool calls." },
1754
+ { value: "kimi", label: "Kimi", description: "Use Kimi-style in-band tool calls." },
1755
+ { value: "xml", label: "XML", description: "Use generic XML in-band tool calls." },
1756
+ { value: "anthropic", label: "Anthropic", description: "Use Anthropic-style in-band tool calls." },
1757
+ { value: "deepseek", label: "DeepSeek", description: "Use DeepSeek-style in-band tool calls." },
1758
+ { value: "harmony", label: "Harmony", description: "Use Harmony-style in-band tool calls." },
1759
+ { value: "pi", label: "Pi", description: "Use Pi-style in-band tool calls." },
1760
+ { value: "qwen3", label: "Qwen3", description: "Use Qwen3-style in-band tool calls." },
1761
+ ],
1762
+ },
1763
+ },
1764
+
1722
1765
  "snapcompact.shape": {
1723
1766
  type: "enum",
1724
1767
  values: ["auto", ...SHAPE_VARIANT_NAMES] as const,
@@ -3183,6 +3226,17 @@ export const SETTINGS_SCHEMA = {
3183
3226
  description: "Ask the agent to describe the intent of each tool call before executing it",
3184
3227
  },
3185
3228
  },
3229
+ "tools.abortOnFabricatedResult": {
3230
+ type: "boolean",
3231
+ default: true,
3232
+ ui: {
3233
+ tab: "tools",
3234
+ group: "Execution",
3235
+ label: "Abort On Fabricated Tool Result",
3236
+ description:
3237
+ "With in-band tool calls, stop the model immediately when it starts hallucinating a tool result mid-turn. Disable to let the model finish generating and discard the fabricated continuation instead.",
3238
+ },
3239
+ },
3186
3240
 
3187
3241
  "tools.maxTimeout": {
3188
3242
  type: "number",
@@ -3940,6 +3994,30 @@ export const SETTINGS_SCHEMA = {
3940
3994
  options: AUTO_THINKING_MODEL_OPTIONS,
3941
3995
  },
3942
3996
  },
3997
+ "features.unexpectedStopDetection": {
3998
+ type: "boolean",
3999
+ default: false,
4000
+ ui: {
4001
+ tab: "interaction",
4002
+ group: "Agent",
4003
+ label: "Detect unexpected stops",
4004
+ description:
4005
+ "Use a small model to detect when the assistant says it will continue but stops without tool calls; automatically prompt it to continue.",
4006
+ },
4007
+ },
4008
+ "providers.unexpectedStopModel": {
4009
+ type: "enum",
4010
+ values: TINY_MEMORY_MODEL_VALUES,
4011
+ default: ONLINE_MEMORY_MODEL_KEY,
4012
+ ui: {
4013
+ tab: "providers",
4014
+ group: "Tiny Model",
4015
+ label: "Unexpected Stop Model",
4016
+ description: "Classifier for unexpected-stop detection: online smol by default, or a local on-device model.",
4017
+ condition: "unexpectedStopDetection",
4018
+ options: TINY_MEMORY_MODEL_OPTIONS,
4019
+ },
4020
+ },
3943
4021
 
3944
4022
  "providers.kimiApiFormat": {
3945
4023
  type: "enum",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tree-sitter-backed {@link BlockResolver} for the hashline `replace block N:`
2
+ * Tree-sitter-backed {@link BlockResolver} for the hashline block replace
3
3
  * operator. Bridges the pure hashline seam to the native `blockRangeAt`
4
4
  * primitive in `@oh-my-pi/pi-natives`, which infers the language from the file
5
5
  * path and returns the 1-indexed line span of the syntactic block beginning on
@@ -98,12 +98,7 @@ interface RenderedSection {
98
98
  }
99
99
 
100
100
  function formatBlockResolution(resolution: BlockResolution): string {
101
- const op =
102
- resolution.op === "delete"
103
- ? "delete block"
104
- : resolution.op === "insert_after"
105
- ? "insert after block"
106
- : "replace block";
101
+ const op = resolution.op === "delete" ? "DEL.BLK" : resolution.op === "insert_after" ? "INS.BLK.POST" : "SWAP.BLK";
107
102
  const lines = resolution.end - resolution.start + 1;
108
103
  const span =
109
104
  resolution.start === resolution.end ? `line ${resolution.start}` : `lines ${resolution.start}-${resolution.end}`;
package/src/edit/index.ts CHANGED
@@ -2,7 +2,9 @@ import { MismatchError as HashlineMismatchError } from "@oh-my-pi/hashline";
2
2
  import hashlineGrammar from "@oh-my-pi/hashline/grammar.lark" with { type: "text" };
3
3
  import hashlineDescription from "@oh-my-pi/hashline/prompt.md" with { type: "text" };
4
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
+ import type { ToolExample } from "@oh-my-pi/pi-ai";
5
6
  import { prompt } from "@oh-my-pi/pi-utils";
7
+ import type { z } from "zod/v4";
6
8
  import {
7
9
  createLspWritethrough,
8
10
  type FileDiagnosticsResult,
@@ -50,6 +52,7 @@ type EditParams = ReplaceParams | PatchParams | HashlineParams | ApplyPatchParam
50
52
  type EditModeDefinition = {
51
53
  description: (session: ToolSession) => string;
52
54
  parameters: TInput;
55
+ examples?: readonly ToolExample[];
53
56
  execute: (
54
57
  tool: EditTool,
55
58
  params: EditParams,
@@ -356,6 +359,10 @@ export class EditTool implements AgentTool<TInput> {
356
359
  return this.#getModeDefinition().parameters;
357
360
  }
358
361
 
362
+ get examples(): readonly ToolExample[] | undefined {
363
+ return this.#getModeDefinition().examples;
364
+ }
365
+
359
366
  /**
360
367
  * When in `apply_patch` mode, expose the Codex Lark grammar so providers
361
368
  * that support OpenAI-style custom tools can emit a grammar-constrained
@@ -404,6 +411,39 @@ export class EditTool implements AgentTool<TInput> {
404
411
  patch: {
405
412
  description: () => prompt.render(patchDescription),
406
413
  parameters: patchEditSchema,
414
+ examples: [
415
+ {
416
+ caption: "Create",
417
+ call: { path: "hello.txt", edits: [{ op: "create", diff: "Hello\n" }] },
418
+ },
419
+ {
420
+ caption: "Update",
421
+ call: {
422
+ path: "src/app.py",
423
+ edits: [
424
+ {
425
+ op: "update",
426
+ diff: "@@ def greet():\n def greet():\n-print('Hi')\n+print('Hello')\n",
427
+ },
428
+ ],
429
+ },
430
+ },
431
+ {
432
+ caption: "Rename",
433
+ call: {
434
+ path: "src/app.py",
435
+ edits: [{ op: "update", rename: "src/main.py", diff: "@@\n …\n" }],
436
+ },
437
+ },
438
+ {
439
+ caption: "Delete",
440
+ call: { path: "obsolete.txt", edits: [{ op: "delete" }] },
441
+ },
442
+ {
443
+ caption: "Multiple entries",
444
+ note: "All entries in one call apply to the top-level `path`; use separate calls for different files.",
445
+ },
446
+ ] satisfies readonly ToolExample<z.input<typeof patchEditSchema>>[],
407
447
  execute: (
408
448
  tool: EditTool,
409
449
  params: EditParams,
@@ -432,6 +472,14 @@ export class EditTool implements AgentTool<TInput> {
432
472
  apply_patch: {
433
473
  description: () => prompt.render(applyPatchDescription),
434
474
  parameters: applyPatchSchema,
475
+ examples: [
476
+ {
477
+ caption: "Apply a combined patch file",
478
+ call: {
479
+ input: '*** Begin Patch\n*** Add File: hello.txt\n+Hello world\n*** Update File: src/app.py\n*** Move to: src/main.py\n@@ def greet():\n-print("Hi")\n+print("Hello, world!")\n*** Delete File: obsolete.txt\n*** End Patch\n',
480
+ },
481
+ },
482
+ ] satisfies readonly ToolExample<z.input<typeof applyPatchSchema>>[],
435
483
  execute: (
436
484
  tool: EditTool,
437
485
  params: EditParams,