pi-adaptive-thinking 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright 2026 Ian Pascoe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -6,7 +6,9 @@ Bring adaptive reasoning-effort control to Pi agents.
6
6
 
7
7
  </div>
8
8
 
9
- `pi-adaptive-thinking` is a Pi extension that lets the agent change Pi's thinking level through a tool named `set_thinking_level`.
9
+ `pi-adaptive-thinking` is a Pi extension that lets the agent inspect and change Pi's thinking level through tools named `get_thinking_level` and `set_thinking_level`.
10
+
11
+ Requires Node `>=22.19.0` and Pi `>=0.84.1`.
10
12
 
11
13
  It mirrors the user-facing behavior of `opencode-adaptive-thinking` while using Pi-native APIs: `pi.getThinkingLevel()`, `pi.setThinkingLevel()`, and `thinking_level_select`.
12
14
 
@@ -20,13 +22,14 @@ For local development:
20
22
 
21
23
  ```bash
22
24
  pnpm install
23
- pnpm build
24
- pi -e ./dist/index.js
25
+ pi -e ./src/index.ts
25
26
  ```
26
27
 
27
28
  ## Behavior
28
29
 
29
- The extension registers a tool with these parameters:
30
+ The extension registers two tools. `get_thinking_level` returns the current native Pi thinking level and the levels supported by the selected model. The agent should inspect status only when the level is uncertain, not poll it every turn.
31
+
32
+ `set_thinking_level` accepts these parameters:
30
33
 
31
34
  - `level`: one of the valid Pi thinking levels for the current model.
32
35
  - `persist`: optional boolean, default `false`.
@@ -47,6 +50,8 @@ Persistent changes:
47
50
 
48
51
  This changes the session baseline until another persistent change is made or the user changes thinking level manually.
49
52
 
53
+ The extension contributes only static tool guidance to Pi's base prompt. It never replaces the per-turn system prompt, so current level, supported levels, model, and session state cannot invalidate the system-prompt cache prefix.
54
+
50
55
  ## Configuration
51
56
 
52
57
  Configuration files are loaded in this order:
@@ -63,10 +68,15 @@ Project configuration takes precedence over global configuration.
63
68
  "quiet": false,
64
69
  "toolName": "set_thinking_level",
65
70
  "toolDescription": "Set your thinking level",
66
- "systemPrompt": "You MUST manage thinking level actively. Lower it before trivial or routine turns; raise it for ambiguity, debugging, risky changes, or multi-step synthesis. Reassess at turn start, after meaningful new evidence, and when the task shifts. NEVER leave the current level unchanged by inertia, and NEVER reply to a trivial turn before considering a downshift."
71
+ "statusToolName": "get_thinking_level",
72
+ "guidance": "You MUST manage thinking level actively. Lower it before trivial or routine turns; raise it for ambiguity, debugging, risky changes, or multi-step synthesis. Reassess at turn start, after meaningful new evidence, and when the task shifts. NEVER leave the current level unchanged by inertia, and NEVER reply to a trivial turn before considering a downshift."
67
73
  }
68
74
  ```
69
75
 
76
+ `guidance`, tool names, and tool descriptions are loaded once per session and remain static. Do not put runtime state in `guidance`.
77
+
78
+ The former `systemPrompt` field remains a deprecated alias for `guidance` during the `0.x` release line. Existing configurations continue to work and produce one UI warning at session start unless `quiet` is enabled. A configuration containing both fields is invalid.
79
+
70
80
  ## Development
71
81
 
72
82
  ```bash
@@ -75,7 +85,6 @@ pnpm format:check
75
85
  pnpm lint
76
86
  pnpm typecheck
77
87
  pnpm test
78
- pnpm build
79
88
  ```
80
89
 
81
90
  Run all checks:
package/package.json CHANGED
@@ -1,38 +1,32 @@
1
1
  {
2
2
  "name": "pi-adaptive-thinking",
3
- "version": "0.1.1",
4
- "description": "Pi Adaptive Thinking extension",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "Pi extension for adaptive reasoning-effort control",
5
6
  "keywords": [
6
7
  "adaptive-thinking",
7
8
  "pi",
9
+ "pi-extension",
8
10
  "pi-package",
9
- "reasoning",
10
11
  "thinking"
11
12
  ],
12
- "homepage": "https://github.com/ian-pascoe/pi-adaptive-thinking#readme",
13
+ "homepage": "https://github.com/ian-pascoe/pi-extensions#readme",
13
14
  "bugs": {
14
- "url": "https://github.com/ian-pascoe/pi-adaptive-thinking/issues"
15
+ "url": "https://github.com/ian-pascoe/pi-extensions/issues"
15
16
  },
16
17
  "license": "MIT",
17
- "author": "Ian Pascoe",
18
+ "author": "Ian Pascoe <ian.g.pascoe@gmail.com>",
18
19
  "repository": {
19
20
  "type": "git",
20
- "url": "git+https://github.com/ian-pascoe/pi-adaptive-thinking.git"
21
+ "url": "git+https://github.com/ian-pascoe/pi-extensions.git",
22
+ "directory": "packages/pi-adaptive-thinking"
21
23
  },
22
24
  "files": [
23
- "dist",
24
- "README.md"
25
+ "src",
26
+ "README.md",
27
+ "LICENSE"
25
28
  ],
26
29
  "type": "module",
27
- "main": "dist/index.js",
28
- "types": "dist/index.d.ts",
29
- "exports": {
30
- ".": {
31
- "types": "./dist/index.d.ts",
32
- "import": "./dist/index.js",
33
- "default": "./dist/index.js"
34
- }
35
- },
36
30
  "publishConfig": {
37
31
  "access": "public",
38
32
  "provenance": true
@@ -41,43 +35,23 @@
41
35
  "proper-lockfile": "^4.1.2"
42
36
  },
43
37
  "devDependencies": {
44
- "@changesets/changelog-github": "^0.7.0",
45
- "@changesets/cli": "^2.31.0",
46
- "@types/node": "^25.9.1",
47
- "@types/proper-lockfile": "^4.1.4",
48
- "@typescript/native-preview": "7.0.0-dev.20260526.1",
49
- "husky": "^9.1.7",
50
- "oxfmt": "^0.52.0",
51
- "oxlint": "^1.67.0",
52
- "rolldown": "1.0.2",
53
- "typescript": "^6.0.3",
54
- "vitest": "^4.1.7"
38
+ "@types/proper-lockfile": "4.1.4"
55
39
  },
56
40
  "peerDependencies": {
57
41
  "@earendil-works/pi-ai": "*",
58
42
  "@earendil-works/pi-coding-agent": "*",
59
43
  "typebox": "*"
60
44
  },
45
+ "engines": {
46
+ "node": ">=22.19.0"
47
+ },
61
48
  "pi": {
62
49
  "extensions": [
63
- "./dist/index.js"
50
+ "./src/index.ts"
64
51
  ]
65
52
  },
66
53
  "scripts": {
67
- "build": "pnpm run clean && pnpm run build:js && pnpm run build:types",
68
- "build:js": "rolldown -c",
69
- "build:types": "tsgo --project tsconfig.build.json --emitDeclarationOnly",
70
- "build:watch": "rolldown -c -w",
71
- "changeset": "changeset",
72
- "clean": "rm -rf dist",
73
- "format": "oxfmt",
74
- "format:check": "oxfmt --check",
75
- "lint": "oxlint",
76
- "lint:fix": "oxlint --fix",
77
- "release": "pnpm run build && changeset publish",
78
- "test": "vitest run",
79
- "typecheck": "tsgo --noEmit",
80
- "verify": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build",
81
- "version-packages": "changeset version"
54
+ "test": "vitest run --config ../../vitest.config.ts --root .",
55
+ "typecheck": "tsc --noEmit -p tsconfig.json"
82
56
  }
83
57
  }
@@ -0,0 +1,66 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { parseConfig, usesDeprecatedSystemPrompt, type AdaptiveThinkingConfig } from "./config.js";
5
+
6
+ export type { AdaptiveThinkingConfig } from "./config.js";
7
+
8
+ export type LoadConfigOptions = {
9
+ cwd: string;
10
+ homeDir?: string;
11
+ };
12
+
13
+ export type LoadConfigResult =
14
+ | {
15
+ success: true;
16
+ config: AdaptiveThinkingConfig;
17
+ source?: string;
18
+ usedDeprecatedSystemPrompt?: true;
19
+ }
20
+ | { success: false; source: string; error: Error };
21
+
22
+ const hasCode = (error: unknown, code: string) => {
23
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
24
+ };
25
+
26
+ const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error));
27
+
28
+ const invalidConfig = (source: string, error: unknown): LoadConfigResult => ({
29
+ success: false,
30
+ source,
31
+ error: new Error(`Invalid Adaptive Thinking configuration in ${source}: ${errorMessage(error)}`),
32
+ });
33
+
34
+ export const loadConfig = async ({
35
+ cwd,
36
+ homeDir = homedir(),
37
+ }: LoadConfigOptions): Promise<LoadConfigResult> => {
38
+ const candidates = [
39
+ join(cwd, ".pi", "adaptive-thinking.json"),
40
+ join(homeDir, ".pi", "agent", "adaptive-thinking.json"),
41
+ ];
42
+
43
+ for (const source of candidates) {
44
+ let raw: string;
45
+ try {
46
+ raw = await readFile(source, "utf8");
47
+ } catch (error) {
48
+ if (hasCode(error, "ENOENT")) continue;
49
+ return invalidConfig(source, error);
50
+ }
51
+
52
+ try {
53
+ const input: unknown = JSON.parse(raw);
54
+ return {
55
+ success: true,
56
+ source,
57
+ config: parseConfig(input),
58
+ ...(usesDeprecatedSystemPrompt(input) ? { usedDeprecatedSystemPrompt: true as const } : {}),
59
+ };
60
+ } catch (error) {
61
+ return invalidConfig(source, error);
62
+ }
63
+ }
64
+
65
+ return { success: true, config: parseConfig(undefined) };
66
+ };
package/src/config.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { Type } from "typebox";
2
+ import { Parse } from "typebox/value";
3
+
4
+ export const defaultGuidance =
5
+ "You MUST manage thinking level actively. " +
6
+ "Lower it before trivial or routine turns; raise it for ambiguity, debugging, risky changes, or multi-step synthesis. " +
7
+ "Reassess at turn start, after meaningful new evidence, and when the task shifts. " +
8
+ "NEVER leave the current level unchanged by inertia, and NEVER reply to a trivial turn before considering a downshift.";
9
+
10
+ export type AdaptiveThinkingConfig = {
11
+ enabled: boolean;
12
+ quiet: boolean;
13
+ toolName: string;
14
+ toolDescription: string;
15
+ statusToolName: string;
16
+ guidance: string;
17
+ };
18
+
19
+ export const configDefaults: AdaptiveThinkingConfig = {
20
+ enabled: true,
21
+ quiet: false,
22
+ toolName: "set_thinking_level",
23
+ toolDescription: "Set your thinking level",
24
+ statusToolName: "get_thinking_level",
25
+ guidance: defaultGuidance,
26
+ };
27
+
28
+ const ConfigInputSchema = Type.Object(
29
+ {
30
+ enabled: Type.Boolean(),
31
+ quiet: Type.Boolean(),
32
+ toolName: Type.String({ minLength: 1 }),
33
+ toolDescription: Type.String({ minLength: 1 }),
34
+ statusToolName: Type.String({ minLength: 1 }),
35
+ guidance: Type.Optional(Type.String({ minLength: 1 })),
36
+ systemPrompt: Type.Optional(Type.String({ minLength: 1 })),
37
+ },
38
+ { additionalProperties: false },
39
+ );
40
+
41
+ const hasOwnProperty = (input: unknown, property: string) =>
42
+ typeof input === "object" &&
43
+ input !== null &&
44
+ Object.prototype.hasOwnProperty.call(input, property);
45
+
46
+ /** Returns whether raw configuration uses the deprecated system prompt alias. */
47
+ export const usesDeprecatedSystemPrompt = (input: unknown) => hasOwnProperty(input, "systemPrompt");
48
+
49
+ /** Parses configuration and normalizes legacy system prompt guidance. */
50
+ export const parseConfig = (input: unknown): AdaptiveThinkingConfig => {
51
+ const usesGuidance = hasOwnProperty(input, "guidance");
52
+ const usesSystemPrompt = usesDeprecatedSystemPrompt(input);
53
+ if (usesGuidance && usesSystemPrompt) {
54
+ throw new Error(
55
+ "Adaptive Thinking configuration cannot contain both guidance and systemPrompt",
56
+ );
57
+ }
58
+
59
+ const rawInput = input as Record<string, unknown> | undefined;
60
+ const merged = {
61
+ ...configDefaults,
62
+ ...rawInput,
63
+ guidance: usesSystemPrompt ? rawInput?.systemPrompt : (rawInput?.guidance ?? defaultGuidance),
64
+ };
65
+ delete (merged as Record<string, unknown>).systemPrompt;
66
+
67
+ const config = Parse(ConfigInputSchema, merged) as AdaptiveThinkingConfig;
68
+ if (config.toolName === config.statusToolName) {
69
+ throw new Error("Adaptive Thinking toolName and statusToolName must be different");
70
+ }
71
+ return config;
72
+ };
package/src/index.ts ADDED
@@ -0,0 +1,329 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import lockfile from "proper-lockfile";
5
+ import type {
6
+ AgentToolResult,
7
+ ExtensionAPI,
8
+ ExtensionContext,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import { type Static, Type } from "typebox";
11
+ import { loadConfig, type AdaptiveThinkingConfig } from "./config-loader.js";
12
+ import {
13
+ isThinkingLevel,
14
+ resolveSupportedThinkingLevels,
15
+ type PiThinkingLevel,
16
+ } from "./thinking-levels.js";
17
+
18
+ type NotifyType = "info" | "warning" | "error";
19
+
20
+ type RuntimeState = {
21
+ config: AdaptiveThinkingConfig;
22
+ persistedLevel?: PiThinkingLevel;
23
+ temporaryResetLevel?: PiThinkingLevel;
24
+ lastToolCallWasReasoningTool?: boolean;
25
+ reasoningToolCallBackToBackById: Map<string, boolean>;
26
+ };
27
+
28
+ const ToolParameters = Type.Object(
29
+ {
30
+ level: Type.String({
31
+ minLength: 1,
32
+ description:
33
+ "The Pi thinking level to apply. Higher levels may improve hard-task quality but may take more time and resources.",
34
+ }),
35
+ persist: Type.Optional(
36
+ Type.Boolean({
37
+ default: false,
38
+ description:
39
+ "Whether to persist the setting for this session; otherwise it applies only for the current turn.",
40
+ }),
41
+ ),
42
+ },
43
+ { additionalProperties: false },
44
+ );
45
+
46
+ type ToolParameters = Static<typeof ToolParameters>;
47
+
48
+ const StatusToolParameters = Type.Object({}, { additionalProperties: false });
49
+
50
+ type ThinkingLevelStatus = {
51
+ currentLevel: PiThinkingLevel | "unknown";
52
+ supportedLevels: PiThinkingLevel[];
53
+ };
54
+
55
+ const textResult = (text: string): AgentToolResult<undefined> => ({
56
+ content: [{ type: "text", text }],
57
+ details: undefined,
58
+ });
59
+
60
+ const thinkingLevelStatusResult = (
61
+ currentLevel: PiThinkingLevel | "unknown",
62
+ supportedLevels: PiThinkingLevel[],
63
+ ): AgentToolResult<ThinkingLevelStatus> => ({
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: `Current thinking level: ${currentLevel}. Supported thinking levels: ${supportedLevels.join(", ")}.`,
68
+ },
69
+ ],
70
+ details: { currentLevel, supportedLevels },
71
+ });
72
+
73
+ const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error));
74
+
75
+ const agentDir = () => process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
76
+
77
+ const globalSettingsPath = () => join(agentDir(), "settings.json");
78
+
79
+ const sleepSync = (milliseconds: number) => {
80
+ const end = Date.now() + milliseconds;
81
+ while (Date.now() < end) {
82
+ // Synchronous ExtensionAPI methods require a synchronous retry loop.
83
+ }
84
+ };
85
+
86
+ const acquireSettingsLock = (lockPath: string) => {
87
+ const maxAttempts = 100;
88
+ const delayMs = 20;
89
+
90
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
91
+ try {
92
+ return lockfile.lockSync(lockPath, { realpath: false });
93
+ } catch (error) {
94
+ const code =
95
+ typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
96
+ if (code !== "ELOCKED" || attempt === maxAttempts) throw error;
97
+ sleepSync(delayMs);
98
+ }
99
+ }
100
+
101
+ throw new Error(`Failed to acquire settings lock: ${lockPath}`);
102
+ };
103
+
104
+ const withSettingsLock = <T>(settingsPath: string, fn: () => T): T => {
105
+ mkdirSync(join(settingsPath, ".."), { recursive: true });
106
+ const lockPath = `${settingsPath}.adaptive-thinking`;
107
+ if (!existsSync(lockPath)) writeFileSync(lockPath, "");
108
+
109
+ const release = acquireSettingsLock(lockPath);
110
+
111
+ try {
112
+ return fn();
113
+ } finally {
114
+ release();
115
+ }
116
+ };
117
+
118
+ const readDefaultThinkingLevel = (settingsPath: string): PiThinkingLevel | undefined => {
119
+ if (!existsSync(settingsPath)) return undefined;
120
+
121
+ try {
122
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf-8")) as {
123
+ defaultThinkingLevel?: unknown;
124
+ };
125
+ return typeof parsed.defaultThinkingLevel === "string" &&
126
+ isThinkingLevel(parsed.defaultThinkingLevel)
127
+ ? parsed.defaultThinkingLevel
128
+ : undefined;
129
+ } catch {
130
+ return undefined;
131
+ }
132
+ };
133
+
134
+ const restoreDefaultThinkingLevel = (
135
+ settingsPath: string,
136
+ previousDefaultThinkingLevel: PiThinkingLevel | undefined,
137
+ ) => {
138
+ if (!existsSync(settingsPath)) return;
139
+
140
+ try {
141
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record<string, unknown>;
142
+ if (previousDefaultThinkingLevel === undefined) {
143
+ delete settings.defaultThinkingLevel;
144
+ } else {
145
+ settings.defaultThinkingLevel = previousDefaultThinkingLevel;
146
+ }
147
+ writeFileSync(settingsPath, JSON.stringify(settings, undefined, 2) + "\n");
148
+ } catch {
149
+ return;
150
+ }
151
+ };
152
+
153
+ const withSessionOnlyThinkingLevelChange = (changeThinkingLevel: () => void) => {
154
+ const settingsPath = globalSettingsPath();
155
+
156
+ return withSettingsLock(settingsPath, () => {
157
+ const previousDefaultThinkingLevel = readDefaultThinkingLevel(settingsPath);
158
+
159
+ changeThinkingLevel();
160
+
161
+ restoreDefaultThinkingLevel(settingsPath, previousDefaultThinkingLevel);
162
+ });
163
+ };
164
+
165
+ const notify = (
166
+ ctx: ExtensionContext,
167
+ type: NotifyType,
168
+ message: string,
169
+ config?: Pick<AdaptiveThinkingConfig, "quiet">,
170
+ ) => {
171
+ if (config?.quiet) return;
172
+ if (!ctx.hasUI) return;
173
+ ctx.ui.notify(message, type);
174
+ };
175
+
176
+ export default function adaptiveThinking(pi: ExtensionAPI) {
177
+ let runtime: RuntimeState | undefined;
178
+ let runtimeHandlersRegistered = false;
179
+
180
+ const registerRuntimeHandlers = () => {
181
+ if (runtimeHandlersRegistered) return;
182
+ runtimeHandlersRegistered = true;
183
+
184
+ pi.on("tool_call", async (event) => {
185
+ const state = runtime;
186
+ if (!state) return;
187
+
188
+ if (event.toolName === state.config.toolName) {
189
+ state.reasoningToolCallBackToBackById.set(
190
+ event.toolCallId,
191
+ state.lastToolCallWasReasoningTool ?? false,
192
+ );
193
+ state.lastToolCallWasReasoningTool = true;
194
+ } else {
195
+ state.lastToolCallWasReasoningTool = false;
196
+ }
197
+ });
198
+
199
+ pi.on("agent_end", async (_event, ctx) => {
200
+ await resetTemporaryLevel(ctx);
201
+ if (!runtime) return;
202
+ runtime.lastToolCallWasReasoningTool = false;
203
+ runtime.reasoningToolCallBackToBackById.clear();
204
+ });
205
+ };
206
+
207
+ const resetTemporaryLevel = async (ctx: ExtensionContext) => {
208
+ const state = runtime;
209
+ const resetLevel = state?.temporaryResetLevel;
210
+ if (!state || !resetLevel) return;
211
+
212
+ try {
213
+ withSessionOnlyThinkingLevelChange(() => pi.setThinkingLevel(resetLevel));
214
+ delete state.temporaryResetLevel;
215
+ } catch (error) {
216
+ notify(ctx, "error", `Failed to reset thinking level: ${errorMessage(error)}`, state.config);
217
+ }
218
+ };
219
+
220
+ pi.on("session_start", async (_event, ctx) => {
221
+ const configResult = await loadConfig({ cwd: ctx.cwd });
222
+ if (!configResult.success) {
223
+ runtime = undefined;
224
+ notify(ctx, "error", configResult.error.message);
225
+ return;
226
+ }
227
+
228
+ const { config } = configResult;
229
+ if (!config.enabled) {
230
+ runtime = undefined;
231
+ return;
232
+ }
233
+
234
+ runtime = { config, reasoningToolCallBackToBackById: new Map() };
235
+
236
+ if (configResult.usedDeprecatedSystemPrompt) {
237
+ notify(
238
+ ctx,
239
+ "warning",
240
+ "Adaptive Thinking configuration: systemPrompt is deprecated; rename it to guidance.",
241
+ config,
242
+ );
243
+ }
244
+
245
+ pi.registerTool({
246
+ name: config.toolName,
247
+ label: "Set Thinking Level",
248
+ description: config.toolDescription,
249
+ promptSnippet: "Set the current Pi thinking level.",
250
+ promptGuidelines: [
251
+ config.guidance,
252
+ `Use ${config.toolName} to change the thinking level when task complexity justifies a different level.`,
253
+ `Use ${config.statusToolName} only when the current or supported thinking levels are uncertain; do not poll it routinely.`,
254
+ `Do not call ${config.toolName} twice in a row; reassess only after new evidence from other tool calls or user input.`,
255
+ ],
256
+ parameters: ToolParameters,
257
+ execute: async (toolCallId, params: ToolParameters, _signal, _onUpdate, ctx) => {
258
+ const state = runtime;
259
+ if (!state) return textResult("Adaptive Thinking is not enabled for this session.");
260
+
261
+ const level = params.level.trim();
262
+ const validLevels = resolveSupportedThinkingLevels(ctx.model);
263
+ if (!isThinkingLevel(level) || !validLevels.includes(level)) {
264
+ return textResult(
265
+ `Invalid thinking level: ${level}. Valid levels: ${validLevels.join(", ")}.`,
266
+ );
267
+ }
268
+
269
+ const persist = params.persist ?? false;
270
+ const currentLevel = pi.getThinkingLevel();
271
+ if (currentLevel === level) {
272
+ return textResult(`Thinking level is already ${level}; no change made.`);
273
+ }
274
+
275
+ if (state.reasoningToolCallBackToBackById.get(toolCallId) ?? false) {
276
+ return textResult(
277
+ `Thinking level change skipped because the previous tool call was also ${state.config.toolName}. Reassess after another tool call or new user input.`,
278
+ );
279
+ }
280
+
281
+ const resetLevel =
282
+ state.persistedLevel ?? (isThinkingLevel(currentLevel) ? currentLevel : undefined);
283
+
284
+ if (!persist && !resetLevel) {
285
+ return textResult(
286
+ "Cannot apply a temporary thinking level because the Session Baseline is unknown.",
287
+ );
288
+ }
289
+
290
+ try {
291
+ withSessionOnlyThinkingLevelChange(() => pi.setThinkingLevel(level));
292
+ } catch (error) {
293
+ return textResult(`Failed to set thinking level: ${errorMessage(error)}`);
294
+ }
295
+
296
+ if (persist) {
297
+ state.persistedLevel = level;
298
+ delete state.temporaryResetLevel;
299
+ } else if (resetLevel && resetLevel !== level) {
300
+ state.temporaryResetLevel = resetLevel;
301
+ } else {
302
+ delete state.temporaryResetLevel;
303
+ }
304
+
305
+ return textResult(`Thinking level set to ${level}`);
306
+ },
307
+ });
308
+
309
+ pi.registerTool({
310
+ name: config.statusToolName,
311
+ label: "Get Thinking Level",
312
+ description: "Get the current and supported Pi thinking levels",
313
+ promptSnippet: "Inspect the current and supported Pi thinking levels.",
314
+ promptGuidelines: [
315
+ `Use ${config.statusToolName} only when thinking-level state is uncertain; do not poll it routinely.`,
316
+ ],
317
+ parameters: StatusToolParameters,
318
+ execute: async (_toolCallId, _params, _signal, _onUpdate, ctx) => {
319
+ const currentLevel = pi.getThinkingLevel();
320
+ return thinkingLevelStatusResult(
321
+ isThinkingLevel(currentLevel) ? currentLevel : "unknown",
322
+ resolveSupportedThinkingLevels(ctx.model),
323
+ );
324
+ },
325
+ });
326
+
327
+ registerRuntimeHandlers();
328
+ });
329
+ }
@@ -0,0 +1,28 @@
1
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
2
+ import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
3
+
4
+ export const fallbackThinkingLevels = [
5
+ "off",
6
+ "minimal",
7
+ "low",
8
+ "medium",
9
+ "high",
10
+ "xhigh",
11
+ "max",
12
+ ] as const;
13
+
14
+ export type PiThinkingLevel = (typeof fallbackThinkingLevels)[number];
15
+
16
+ const levelSet = new Set<string>(fallbackThinkingLevels);
17
+
18
+ export const isThinkingLevel = (level: string): level is PiThinkingLevel => levelSet.has(level);
19
+
20
+ export const resolveSupportedThinkingLevels = (
21
+ model: Model<Api> | undefined,
22
+ ): PiThinkingLevel[] => {
23
+ if (!model) return [...fallbackThinkingLevels];
24
+
25
+ return getSupportedThinkingLevels(model).filter(
26
+ (level: ModelThinkingLevel): level is PiThinkingLevel => isThinkingLevel(level),
27
+ );
28
+ };