@oh-my-pi/pi-coding-agent 17.2.13 → 17.2.14

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.
@@ -1026,6 +1026,16 @@ export declare const SETTINGS_SCHEMA: {
1026
1026
  readonly description: "Instruct upstream providers to completely omit thinking summaries from responses (where supported)";
1027
1027
  };
1028
1028
  };
1029
+ readonly externalThinking: {
1030
+ readonly type: "boolean";
1031
+ readonly default: false;
1032
+ readonly ui: {
1033
+ readonly tab: "model";
1034
+ readonly group: "Thinking";
1035
+ readonly label: "External Thinking";
1036
+ readonly description: "Use a private think tool and send reasoning effort off to GPT Responses models";
1037
+ };
1038
+ };
1029
1039
  readonly "model.loopGuard.enabled": {
1030
1040
  readonly type: "boolean";
1031
1041
  readonly default: true;
@@ -138,6 +138,8 @@ export interface AgentSessionConfig {
138
138
  createMemoryTools?: () => Promise<AgentTool[]>;
139
139
  /** Creates the built-in `computer` tool for session-scoped runtime enablement (see {@link AgentSession.setComputerToolEnabled}). */
140
140
  createComputerTool?: () => Promise<AgentTool | null>;
141
+ /** Creates the private `think` scratchpad tool for runtime setting changes. */
142
+ createThinkTool?: () => Promise<AgentTool | null>;
141
143
  /** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link AgentSession.setInspectImageMode}). */
142
144
  createInspectImageTool?: () => Promise<AgentTool | null>;
143
145
  /** Model registry for API key resolution and model discovery. */
@@ -331,6 +331,8 @@ export declare class AgentSession {
331
331
  * tool (e.g. restricted child sessions have no factory).
332
332
  */
333
333
  setComputerToolEnabled(enabled: boolean): Promise<boolean>;
334
+ /** Applies the external-thinking setting to the private scratchpad tool immediately. */
335
+ setThinkToolEnabled(enabled: boolean): Promise<boolean>;
334
336
  /**
335
337
  * Session-scoped inspect_image mode (`/vision`). `auto` clears the override
336
338
  * and returns to the persisted `inspect_image.mode` setting; `on`/`off`
@@ -43,6 +43,8 @@ interface SessionToolsOptions {
43
43
  toolRegistry?: Map<string, AgentTool>;
44
44
  createVibeTools?: () => AgentTool[];
45
45
  createComputerTool?: () => Promise<AgentTool | null>;
46
+ /** Creates the private `think` scratchpad tool for runtime setting changes. */
47
+ createThinkTool?: () => Promise<AgentTool | null>;
46
48
  /** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link SessionTools.setInspectImageMode}). */
47
49
  createInspectImageTool?: () => Promise<AgentTool | null>;
48
50
  builtInToolNames?: Iterable<string>;
@@ -232,6 +234,15 @@ export declare class SessionTools {
232
234
  * tool (e.g. restricted child sessions have no factory).
233
235
  */
234
236
  setComputerToolEnabled(enabled: boolean): Promise<boolean>;
237
+ /**
238
+ * Session-scoped enable/disable for the private `think` scratchpad tool.
239
+ *
240
+ * Enabling constructs the tool once and refreshes the model's tool contract;
241
+ * disabling removes it from the active set while preserving its registry entry.
242
+ *
243
+ * @returns false when enabling was requested but this session cannot build the tool.
244
+ */
245
+ setThinkToolEnabled(enabled: boolean): Promise<boolean>;
235
246
  /** Current effective inspect_image state for `/vision status`. */
236
247
  inspectImageState(): {
237
248
  mode: InspectImageMode;
@@ -1,7 +1,6 @@
1
1
  export declare const BUILTIN_TOOL_NAMES: readonly ["read", "bash", "edit", "ast_grep", "ast_edit", "ask", "debug", "eval", "github", "glob", "grep", "lsp", "inspect_image", "browser", "computer", "checkpoint", "rewind", "security_scan", "task", "hub", "todo", "web_search", "write", "memory_edit", "retain", "recall", "reflect", "learn", "manage_skill"];
2
2
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
3
- /** Hidden built-ins: constructible and `--tools`-addressable, but never part of the default active set. */
4
- export declare const HIDDEN_TOOL_NAMES: readonly ["yield", "goal"];
3
+ export declare const HIDDEN_TOOL_NAMES: readonly ["yield", "goal", "think"];
5
4
  export type HiddenToolName = (typeof HIDDEN_TOOL_NAMES)[number];
6
5
  /** Return the canonical tool name for current and legacy built-in tool IDs. */
7
6
  export declare function normalizeToolName(name: string): string;
@@ -67,6 +67,7 @@ export * from "./report-tool-issue.js";
67
67
  export * from "./resolve.js";
68
68
  export * from "./review.js";
69
69
  export * from "./security-scan.js";
70
+ export * from "./think.js";
70
71
  export * from "./todo.js";
71
72
  export * from "./tts.js";
72
73
  export * from "./vibe.js";
@@ -0,0 +1,38 @@
1
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import { type Component } from "@oh-my-pi/pi-tui";
3
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types.js";
4
+ import { type Theme } from "../modes/theme/theme.js";
5
+ declare const thinkSchema: import("@oh-my-pi/omptype").FluentType<{
6
+ thoughts: string;
7
+ }, {
8
+ thoughts: string;
9
+ }>;
10
+ type ThinkParams = typeof thinkSchema.infer;
11
+ export type ThinkRenderArgs = {
12
+ thoughts?: string;
13
+ };
14
+ export declare const thinkToolRenderer: {
15
+ inline: boolean;
16
+ renderCall(args: ThinkRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component;
17
+ renderResult(): Component;
18
+ };
19
+ interface ThinkToolDetails {
20
+ recorded: true;
21
+ }
22
+ /** Records private intermediate reasoning while native GPT reasoning is disabled. */
23
+ export declare class ThinkTool implements AgentTool<typeof thinkSchema, ThinkToolDetails> {
24
+ readonly name = "think";
25
+ readonly approval: "read";
26
+ readonly label = "Think";
27
+ readonly summary = "Record private intermediate reasoning before answering";
28
+ readonly description = "Use this private scratchpad to plan, derive, or check work before answering. Record only materially new reasoning. The user does not see this tool activity.";
29
+ readonly parameters: import("@oh-my-pi/omptype").FluentType<{
30
+ thoughts: string;
31
+ }, {
32
+ thoughts: string;
33
+ }>;
34
+ readonly strict = true;
35
+ readonly intent: "omit";
36
+ execute(_toolCallId: string, _params: ThinkParams): Promise<AgentToolResult<ThinkToolDetails>>;
37
+ }
38
+ export {};
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": "17.2.13",
4
+ "version": "17.2.14",
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",
@@ -50,18 +50,18 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@babel/parser": "^7.29.7",
53
- "@oh-my-pi/hashline": "17.2.13",
54
- "@oh-my-pi/omp-stats": "17.2.13",
55
- "@oh-my-pi/omptype": "17.2.13",
56
- "@oh-my-pi/pi-agent-core": "17.2.13",
57
- "@oh-my-pi/pi-ai": "17.2.13",
58
- "@oh-my-pi/pi-catalog": "17.2.13",
59
- "@oh-my-pi/pi-mnemopi": "17.2.13",
60
- "@oh-my-pi/pi-natives": "17.2.13",
61
- "@oh-my-pi/pi-tui": "17.2.13",
62
- "@oh-my-pi/pi-utils": "17.2.13",
63
- "@oh-my-pi/pi-wire": "17.2.13",
64
- "@oh-my-pi/snapcompact": "17.2.13",
53
+ "@oh-my-pi/hashline": "17.2.14",
54
+ "@oh-my-pi/omp-stats": "17.2.14",
55
+ "@oh-my-pi/omptype": "17.2.14",
56
+ "@oh-my-pi/pi-agent-core": "17.2.14",
57
+ "@oh-my-pi/pi-ai": "17.2.14",
58
+ "@oh-my-pi/pi-catalog": "17.2.14",
59
+ "@oh-my-pi/pi-mnemopi": "17.2.14",
60
+ "@oh-my-pi/pi-natives": "17.2.14",
61
+ "@oh-my-pi/pi-tui": "17.2.14",
62
+ "@oh-my-pi/pi-utils": "17.2.14",
63
+ "@oh-my-pi/pi-wire": "17.2.14",
64
+ "@oh-my-pi/snapcompact": "17.2.14",
65
65
  "@opentelemetry/api": "^1.9.1",
66
66
  "@opentelemetry/api-logs": "^0.220.0",
67
67
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -361,6 +361,22 @@ export const agenticFixtures: Record<string, GalleryFixture> = {
361
361
  },
362
362
  },
363
363
 
364
+ think: {
365
+ label: "Think",
366
+ // Streaming: scratchpad text still arriving.
367
+ streamingArgs: {
368
+ thoughts: "The retry loop re-reads the config after every failure — that explains the doubled latency.",
369
+ },
370
+ args: {
371
+ thoughts:
372
+ "The retry loop re-reads the config after every failure — that explains the doubled latency. Cache the parsed config outside the loop, then re-check the invalidation path before answering.",
373
+ },
374
+ result: {
375
+ content: [{ type: "text", text: "------" }],
376
+ details: { recorded: true },
377
+ },
378
+ },
379
+
364
380
  hub_jobs: {
365
381
  label: "Hub jobs",
366
382
  renderer: "hub",
@@ -1139,6 +1139,17 @@ export const SETTINGS_SCHEMA = {
1139
1139
  },
1140
1140
  },
1141
1141
 
1142
+ externalThinking: {
1143
+ type: "boolean",
1144
+ default: false,
1145
+ ui: {
1146
+ tab: "model",
1147
+ group: "Thinking",
1148
+ label: "External Thinking",
1149
+ description: "Use a private think tool and send reasoning effort off to GPT Responses models",
1150
+ },
1151
+ },
1152
+
1142
1153
  "model.loopGuard.enabled": {
1143
1154
  type: "boolean",
1144
1155
  default: true,
@@ -479,6 +479,11 @@ export class SelectorController {
479
479
  this.ctx.showError(`Failed to apply vision mode: ${err}`);
480
480
  });
481
481
  break;
482
+ case "externalThinking":
483
+ void this.ctx.session.setThinkToolEnabled(value as boolean).catch(err => {
484
+ this.ctx.showError(`Failed to apply external thinking: ${err}`);
485
+ });
486
+ break;
482
487
 
483
488
  case "autocompleteMaxVisible":
484
489
  this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
@@ -101,6 +101,15 @@ Invalid args return the schema in the error — fix and retry
101
101
  TOOL POLICY
102
102
  ==============
103
103
 
104
+ {{#has tools "think"}}
105
+ # Reasoning
106
+ `{{toolRefs.think}}` is your scratchpad and it is where your reasoning actually happens — whatever you do not write there, you have not worked out. Its content is private; the user never sees it.
107
+ - MUST call `{{toolRefs.think}}` before the first action of a turn, and again before any step that is expensive to undo: an edit, a destructive command, a final answer.
108
+ - Restate what is actually being asked and the constraints given. Split it into ordered sub-problems and solve each explicitly, writing the intermediate result instead of jumping to the conclusion. When a step splits into cases, enumerate them and resolve each.
109
+ - Then check the work: verify each claim against the constraints, test one boundary or degenerate case, and look specifically for the error you would most plausibly have made. If a check fails, redo that step — NEVER patch the conclusion.
110
+ - Call again only for materially new state: a tool result that changes the plan, a failed check, a sub-problem you had not opened. NEVER use it to narrate progress or restate what you already recorded.
111
+ {{/has}}
112
+
104
113
  # General
105
114
  Use tools whenever they improve correctness, completeness, or grounding.
106
115
  - SHOULD resolve prerequisites before acting.
package/src/sdk.ts CHANGED
@@ -3237,7 +3237,12 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
3237
3237
  });
3238
3238
  }
3239
3239
  }
3240
- return settingsAwareStreamFn(streamModel, context, streamOptions);
3240
+ const externalThinking =
3241
+ settings.get("externalThinking") && agent.state.tools.some(tool => tool.name === "think");
3242
+ return settingsAwareStreamFn(streamModel, context, {
3243
+ ...streamOptions,
3244
+ forceReasoningOff: externalThinking || streamOptions?.forceReasoningOff,
3245
+ });
3241
3246
  },
3242
3247
  cursorExecHandlers,
3243
3248
  getCursorTools: () => (toolSession.xdev ? listXdevTools(toolSession.xdev) : []),
@@ -3393,6 +3398,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
3393
3398
  createComputerTool: restrictToolNames
3394
3399
  ? undefined
3395
3400
  : async () => (await BUILTIN_TOOLS.computer(toolSession)) ?? null,
3401
+ createThinkTool: async () => (await HIDDEN_TOOLS.think(toolSession)) ?? null,
3396
3402
  createInspectImageTool: restrictToolNames
3397
3403
  ? undefined
3398
3404
  : async () => (await BUILTIN_TOOLS.inspect_image(toolSession)) ?? null,
@@ -164,6 +164,8 @@ export interface AgentSessionConfig {
164
164
  createMemoryTools?: () => Promise<AgentTool[]>;
165
165
  /** Creates the built-in `computer` tool for session-scoped runtime enablement (see {@link AgentSession.setComputerToolEnabled}). */
166
166
  createComputerTool?: () => Promise<AgentTool | null>;
167
+ /** Creates the private `think` scratchpad tool for runtime setting changes. */
168
+ createThinkTool?: () => Promise<AgentTool | null>;
167
169
  /** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link AgentSession.setInspectImageMode}). */
168
170
  createInspectImageTool?: () => Promise<AgentTool | null>;
169
171
  /** Model registry for API key resolution and model discovery. */
@@ -1241,6 +1241,7 @@ export class AgentSession {
1241
1241
  toolRegistry: config.toolRegistry,
1242
1242
  createVibeTools: config.createVibeTools,
1243
1243
  createComputerTool: config.createComputerTool,
1244
+ createThinkTool: config.createThinkTool,
1244
1245
  createInspectImageTool: config.createInspectImageTool,
1245
1246
  builtInToolNames: config.builtInToolNames,
1246
1247
  mcpManagerToolNames: config.mcpManagerToolNames,
@@ -4419,6 +4420,11 @@ export class AgentSession {
4419
4420
  return this.#tools.setComputerToolEnabled(enabled);
4420
4421
  }
4421
4422
 
4423
+ /** Applies the external-thinking setting to the private scratchpad tool immediately. */
4424
+ setThinkToolEnabled(enabled: boolean): Promise<boolean> {
4425
+ return this.#tools.setThinkToolEnabled(enabled);
4426
+ }
4427
+
4422
4428
  /**
4423
4429
  * Session-scoped inspect_image mode (`/vision`). `auto` clears the override
4424
4430
  * and returns to the persisted `inspect_image.mode` setting; `on`/`off`
@@ -5176,6 +5182,18 @@ export class AgentSession {
5176
5182
 
5177
5183
  // Skip eager preludes when the user has already queued a directive
5178
5184
  const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
5185
+ const activeModel = this.agent.state.model;
5186
+ const externalThinkingToolChoice =
5187
+ !options?.synthetic &&
5188
+ !hasPendingUserDirective &&
5189
+ this.settings.get("externalThinking") &&
5190
+ this.getEnabledToolNames().includes("think") &&
5191
+ activeModel &&
5192
+ (activeModel.api === "openai-responses" ||
5193
+ activeModel.api === "azure-openai-responses" ||
5194
+ activeModel.api === "openai-codex-responses")
5195
+ ? buildNamedToolChoice("think", activeModel)
5196
+ : undefined;
5179
5197
  const eagerTodoPrelude =
5180
5198
  !options?.synthetic && !hasPendingUserDirective ? this.#todo.createEagerTodoPrelude(expandedText) : undefined;
5181
5199
  const eagerTaskPrelude =
@@ -5193,6 +5211,12 @@ export class AgentSession {
5193
5211
  : undefined;
5194
5212
 
5195
5213
  const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
5214
+ if (externalThinkingToolChoice) {
5215
+ this.#toolChoiceQueue.pushOnce(externalThinkingToolChoice, {
5216
+ label: "external-thinking",
5217
+ now: true,
5218
+ });
5219
+ }
5196
5220
  const message = options?.synthetic
5197
5221
  ? { role: "developer" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() }
5198
5222
  : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
@@ -5223,6 +5247,7 @@ export class AgentSession {
5223
5247
  // Clean up residual eager-todo directive if the prompt never consumed it
5224
5248
  // (e.g., compaction aborted, validation failed).
5225
5249
  this.#toolChoiceQueue.removeByLabel("eager-todo");
5250
+ this.#toolChoiceQueue.removeByLabel("external-thinking");
5226
5251
  }
5227
5252
  return true;
5228
5253
  }
@@ -68,6 +68,8 @@ interface SessionToolsOptions {
68
68
  toolRegistry?: Map<string, AgentTool>;
69
69
  createVibeTools?: () => AgentTool[];
70
70
  createComputerTool?: () => Promise<AgentTool | null>;
71
+ /** Creates the private `think` scratchpad tool for runtime setting changes. */
72
+ createThinkTool?: () => Promise<AgentTool | null>;
71
73
  /** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link SessionTools.setInspectImageMode}). */
72
74
  createInspectImageTool?: () => Promise<AgentTool | null>;
73
75
  builtInToolNames?: Iterable<string>;
@@ -184,6 +186,7 @@ export class SessionTools {
184
186
  #toolRegistry: Map<string, AgentTool>;
185
187
  #createVibeTools: (() => AgentTool[]) | undefined;
186
188
  #createComputerTool: SessionToolsOptions["createComputerTool"];
189
+ #createThinkTool: SessionToolsOptions["createThinkTool"];
187
190
  #createInspectImageTool: SessionToolsOptions["createInspectImageTool"];
188
191
  #installedVibeToolNames = new Set<string>();
189
192
  #builtInToolNames: Set<string>;
@@ -241,6 +244,7 @@ export class SessionTools {
241
244
  this.#toolRegistry = options.toolRegistry ?? new Map();
242
245
  this.#createVibeTools = options.createVibeTools;
243
246
  this.#createComputerTool = options.createComputerTool;
247
+ this.#createThinkTool = options.createThinkTool;
244
248
  this.#createInspectImageTool = options.createInspectImageTool;
245
249
  this.#builtInToolNames = new Set(options.builtInToolNames ?? []);
246
250
  this.#mcpManagerToolNames = new Set(options.mcpManagerToolNames ?? []);
@@ -1158,6 +1162,37 @@ export class SessionTools {
1158
1162
  });
1159
1163
  }
1160
1164
 
1165
+ /**
1166
+ * Session-scoped enable/disable for the private `think` scratchpad tool.
1167
+ *
1168
+ * Enabling constructs the tool once and refreshes the model's tool contract;
1169
+ * disabling removes it from the active set while preserving its registry entry.
1170
+ *
1171
+ * @returns false when enabling was requested but this session cannot build the tool.
1172
+ */
1173
+ setThinkToolEnabled(enabled: boolean): Promise<boolean> {
1174
+ return this.runToolRegistryMutation(async () => {
1175
+ const active = this.getEnabledToolNames();
1176
+ if (!enabled) {
1177
+ if (active.includes("think")) {
1178
+ await this.#applyActiveToolsByName(active.filter(name => name !== "think"));
1179
+ }
1180
+ return true;
1181
+ }
1182
+ if (!this.#toolRegistry.has("think")) {
1183
+ const tool = await this.#createThinkTool?.();
1184
+ if (tool?.name !== "think") return false;
1185
+ const wrapped = this.#wrapRuntimeTool(tool);
1186
+ this.#toolRegistry.set(wrapped.name, wrapped);
1187
+ this.#builtInToolNames.add(wrapped.name);
1188
+ }
1189
+ if (!active.includes("think")) {
1190
+ await this.#applyActiveToolsByName([...active, "think"]);
1191
+ }
1192
+ return true;
1193
+ });
1194
+ }
1195
+
1161
1196
  /** Current effective inspect_image state for `/vision status`. */
1162
1197
  inspectImageState(): { mode: InspectImageMode; active: boolean; model: string | undefined } {
1163
1198
  const model = this.#host.model();
@@ -32,8 +32,7 @@ export const BUILTIN_TOOL_NAMES = [
32
32
 
33
33
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
34
34
 
35
- /** Hidden built-ins: constructible and `--tools`-addressable, but never part of the default active set. */
36
- export const HIDDEN_TOOL_NAMES = ["yield", "goal"] as const;
35
+ export const HIDDEN_TOOL_NAMES = ["yield", "goal", "think"] as const;
37
36
 
38
37
  export type HiddenToolName = (typeof HIDDEN_TOOL_NAMES)[number];
39
38
 
@@ -62,6 +62,7 @@ import { wrapToolWithMetaNotice } from "./output-meta";
62
62
  import { ReadTool } from "./read";
63
63
  import type { PlanProposalHandler } from "./resolve";
64
64
  import { SecurityScanTool } from "./security-scan";
65
+ import { ThinkTool } from "./think";
65
66
  import { type TodoPhase, TodoTool } from "./todo";
66
67
  import { WriteTool } from "./write";
67
68
  import { isMountableUnderXdev, type XdevState } from "./xdev";
@@ -102,6 +103,7 @@ export * from "./report-tool-issue";
102
103
  export * from "./resolve";
103
104
  export * from "./review";
104
105
  export * from "./security-scan";
106
+ export * from "./think";
105
107
  export * from "./todo";
106
108
  export * from "./tts";
107
109
  export * from "./vibe";
@@ -444,6 +446,7 @@ export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
444
446
  };
445
447
 
446
448
  export const HIDDEN_TOOLS: Record<HiddenToolName, ToolFactory> = {
449
+ think: () => new ThinkTool(),
447
450
  yield: s => new YieldTool(s),
448
451
  goal: s => new GoalTool(s),
449
452
  };
@@ -562,6 +565,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
562
565
  if (session.settings.get("memory.backend") === "mnemopi" && !requestedTools.includes("memory_edit")) {
563
566
  requestedTools.push("memory_edit");
564
567
  }
568
+ if (session.settings.get("externalThinking") && !requestedTools.includes("think")) {
569
+ requestedTools.push("think");
570
+ }
565
571
  // Auto-learn tools are gated by `autolearn.enabled` but, like the memory
566
572
  // tools above, must also be force-included into an explicit requestedTools
567
573
  // list so a restricted top-level session whose controller/guidance is
@@ -604,6 +610,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
604
610
  if (name === "inspect_image") return isInspectImageToolActive(session);
605
611
  if (name === "web_search") return session.settings.get("web_search.enabled");
606
612
  if (name === "security_scan") return session.settings.get("security.enabled");
613
+ if (name === "think") return session.settings.get("externalThinking");
607
614
  if (name === "ask") return session.settings.get("ask.enabled");
608
615
  if (name === "browser") return session.settings.get("browser.enabled");
609
616
  if (name === "computer") return session.settings.get("computer.enabled");
@@ -650,6 +657,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
650
657
  ...Object.entries(BUILTIN_TOOLS)
651
658
  .filter(([name]) => isToolAllowed(name))
652
659
  .map(([name, factory]) => [name, factory] as const),
660
+ ...(session.settings.get("externalThinking") ? ([["think", HIDDEN_TOOLS.think]] as const) : []),
653
661
  ...(includeYield ? ([["yield", HIDDEN_TOOLS.yield]] as const) : []),
654
662
  ...(goalModeActive ? ([["goal", HIDDEN_TOOLS.goal]] as const) : []),
655
663
  ];
@@ -27,6 +27,7 @@ import { inspectImageToolRenderer } from "./inspect-image-renderer";
27
27
  import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
28
28
  import { readToolRenderer } from "./read";
29
29
  import { resolveRenderer } from "./resolve";
30
+ import { thinkToolRenderer } from "./think";
30
31
  import { todoToolRenderer } from "./todo";
31
32
  import { createVibeToolRenderer } from "./vibe";
32
33
  import { writeToolRenderer } from "./write";
@@ -115,6 +116,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
115
116
  get task(): ToolRenderer {
116
117
  return taskToolRenderer as ToolRenderer;
117
118
  },
119
+ think: thinkToolRenderer as ToolRenderer,
118
120
  todo: todoToolRenderer as ToolRenderer,
119
121
  github: githubToolRenderer as ToolRenderer,
120
122
  goal: goalToolRenderer as ToolRenderer,
@@ -0,0 +1,62 @@
1
+ import { type } from "@oh-my-pi/omptype";
2
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
3
+ import { type Component, Markdown } from "@oh-my-pi/pi-tui";
4
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
5
+ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
6
+
7
+ const thinkSchema = type({
8
+ thoughts: type("string").describe("private scratchpad reasoning to retain before the next response"),
9
+ "+": "reject",
10
+ }).describe("record private intermediate reasoning before answering");
11
+
12
+ type ThinkParams = typeof thinkSchema.infer;
13
+
14
+ export type ThinkRenderArgs = {
15
+ thoughts?: string;
16
+ };
17
+
18
+ export const thinkToolRenderer = {
19
+ inline: true,
20
+ renderCall(args: ThinkRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
21
+ const thoughts =
22
+ typeof args === "object" && args !== null && "thoughts" in args && typeof args.thoughts === "string"
23
+ ? args.thoughts
24
+ : "";
25
+ return new Markdown(thoughts, 1, 0, getMarkdownTheme(), {
26
+ color: (text: string) => uiTheme.fg("thinkingText", text),
27
+ italic: true,
28
+ });
29
+ },
30
+ renderResult(): Component {
31
+ return undefined as unknown as Component;
32
+ },
33
+ };
34
+
35
+ interface ThinkToolDetails {
36
+ recorded: true;
37
+ }
38
+
39
+ /** Records private intermediate reasoning while native GPT reasoning is disabled. */
40
+ export class ThinkTool implements AgentTool<typeof thinkSchema, ThinkToolDetails> {
41
+ readonly name = "think";
42
+ readonly approval = "read" as const;
43
+ readonly label = "Think";
44
+ readonly summary = "Record private intermediate reasoning before answering";
45
+ readonly description =
46
+ "Use this private scratchpad to plan, derive, or check work before answering. Record only materially new reasoning. The user does not see this tool activity.";
47
+ readonly parameters = thinkSchema;
48
+ readonly strict = true;
49
+ readonly intent = "omit" as const;
50
+
51
+ async execute(_toolCallId: string, _params: ThinkParams): Promise<AgentToolResult<ThinkToolDetails>> {
52
+ return {
53
+ content: [
54
+ {
55
+ type: "text",
56
+ text: "------",
57
+ },
58
+ ],
59
+ details: { recorded: true },
60
+ };
61
+ }
62
+ }