pi-adaptive-thinking 0.1.2 → 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/README.md CHANGED
@@ -6,7 +6,7 @@ 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
10
 
11
11
  Requires Node `>=22.19.0` and Pi `>=0.84.1`.
12
12
 
@@ -27,7 +27,9 @@ pi -e ./src/index.ts
27
27
 
28
28
  ## Behavior
29
29
 
30
- 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:
31
33
 
32
34
  - `level`: one of the valid Pi thinking levels for the current model.
33
35
  - `persist`: optional boolean, default `false`.
@@ -48,6 +50,8 @@ Persistent changes:
48
50
 
49
51
  This changes the session baseline until another persistent change is made or the user changes thinking level manually.
50
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
+
51
55
  ## Configuration
52
56
 
53
57
  Configuration files are loaded in this order:
@@ -64,10 +68,15 @@ Project configuration takes precedence over global configuration.
64
68
  "quiet": false,
65
69
  "toolName": "set_thinking_level",
66
70
  "toolDescription": "Set your thinking level",
67
- "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."
68
73
  }
69
74
  ```
70
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
+
71
80
  ## Development
72
81
 
73
82
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-adaptive-thinking",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Pi extension for adaptive reasoning-effort control",
6
6
  "keywords": [
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { parseConfig, type AdaptiveThinkingConfig } from "./config.js";
4
+ import { parseConfig, usesDeprecatedSystemPrompt, type AdaptiveThinkingConfig } from "./config.js";
5
5
 
6
6
  export type { AdaptiveThinkingConfig } from "./config.js";
7
7
 
@@ -11,7 +11,12 @@ export type LoadConfigOptions = {
11
11
  };
12
12
 
13
13
  export type LoadConfigResult =
14
- | { success: true; config: AdaptiveThinkingConfig; source?: string }
14
+ | {
15
+ success: true;
16
+ config: AdaptiveThinkingConfig;
17
+ source?: string;
18
+ usedDeprecatedSystemPrompt?: true;
19
+ }
15
20
  | { success: false; source: string; error: Error };
16
21
 
17
22
  const hasCode = (error: unknown, code: string) => {
@@ -45,7 +50,13 @@ export const loadConfig = async ({
45
50
  }
46
51
 
47
52
  try {
48
- return { success: true, source, config: parseConfig(JSON.parse(raw)) };
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
+ };
49
60
  } catch (error) {
50
61
  return invalidConfig(source, error);
51
62
  }
package/src/config.ts CHANGED
@@ -1,34 +1,72 @@
1
- import { Type, type Static } from "typebox";
1
+ import { Type } from "typebox";
2
2
  import { Parse } from "typebox/value";
3
3
 
4
- export const defaultSystemPrompt =
4
+ export const defaultGuidance =
5
5
  "You MUST manage thinking level actively. " +
6
6
  "Lower it before trivial or routine turns; raise it for ambiguity, debugging, risky changes, or multi-step synthesis. " +
7
7
  "Reassess at turn start, after meaningful new evidence, and when the task shifts. " +
8
8
  "NEVER leave the current level unchanged by inertia, and NEVER reply to a trivial turn before considering a downshift.";
9
9
 
10
- export const configDefaults = {
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 = {
11
20
  enabled: true,
12
21
  quiet: false,
13
22
  toolName: "set_thinking_level",
14
23
  toolDescription: "Set your thinking level",
15
- systemPrompt: defaultSystemPrompt,
24
+ statusToolName: "get_thinking_level",
25
+ guidance: defaultGuidance,
16
26
  };
17
27
 
18
- export const ConfigSchema = Type.Object(
28
+ const ConfigInputSchema = Type.Object(
19
29
  {
20
30
  enabled: Type.Boolean(),
21
31
  quiet: Type.Boolean(),
22
32
  toolName: Type.String({ minLength: 1 }),
23
33
  toolDescription: Type.String({ minLength: 1 }),
24
- systemPrompt: 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 })),
25
37
  },
26
38
  { additionalProperties: false },
27
39
  );
28
40
 
29
- export type AdaptiveThinkingConfig = Static<typeof ConfigSchema>;
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");
30
48
 
49
+ /** Parses configuration and normalizes legacy system prompt guidance. */
31
50
  export const parseConfig = (input: unknown): AdaptiveThinkingConfig => {
32
- const merged = input === undefined ? configDefaults : { ...configDefaults, ...input };
33
- return Parse(ConfigSchema, merged);
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;
34
72
  };
package/src/index.ts CHANGED
@@ -4,8 +4,6 @@ import { join } from "node:path";
4
4
  import lockfile from "proper-lockfile";
5
5
  import type {
6
6
  AgentToolResult,
7
- BeforeAgentStartEvent,
8
- BeforeAgentStartEventResult,
9
7
  ExtensionAPI,
10
8
  ExtensionContext,
11
9
  } from "@earendil-works/pi-coding-agent";
@@ -21,7 +19,6 @@ type NotifyType = "info" | "warning" | "error";
21
19
 
22
20
  type RuntimeState = {
23
21
  config: AdaptiveThinkingConfig;
24
- currentLevel?: PiThinkingLevel;
25
22
  persistedLevel?: PiThinkingLevel;
26
23
  temporaryResetLevel?: PiThinkingLevel;
27
24
  lastToolCallWasReasoningTool?: boolean;
@@ -48,11 +45,31 @@ const ToolParameters = Type.Object(
48
45
 
49
46
  type ToolParameters = Static<typeof ToolParameters>;
50
47
 
48
+ const StatusToolParameters = Type.Object({}, { additionalProperties: false });
49
+
50
+ type ThinkingLevelStatus = {
51
+ currentLevel: PiThinkingLevel | "unknown";
52
+ supportedLevels: PiThinkingLevel[];
53
+ };
54
+
51
55
  const textResult = (text: string): AgentToolResult<undefined> => ({
52
56
  content: [{ type: "text", text }],
53
57
  details: undefined,
54
58
  });
55
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
+
56
73
  const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error));
57
74
 
58
75
  const agentDir = () => process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
@@ -156,30 +173,6 @@ const notify = (
156
173
  ctx.ui.notify(message, type);
157
174
  };
158
175
 
159
- const appendSystemPromptBlock = (systemPrompt: string, block: string) => {
160
- const trimmedBlock = block.trim();
161
- if (!trimmedBlock) return systemPrompt;
162
- if (!systemPrompt.trim()) return trimmedBlock;
163
- return `${systemPrompt.trimEnd()}\n\n${trimmedBlock}`;
164
- };
165
-
166
- const formatGuidance = (
167
- config: AdaptiveThinkingConfig,
168
- currentLevel: string | undefined,
169
- validLevels: string[],
170
- ) => {
171
- return (
172
- config.systemPrompt.trim() +
173
- " " +
174
- (currentLevel ? `Current thinking level: ${currentLevel}. ` : "") +
175
- `Valid thinking levels for this session: ${validLevels.join(", ")}. ` +
176
- `To change the thinking level, use the \`${config.toolName}\` tool with one of the valid levels. ` +
177
- "Only call it when the task complexity justifies changing levels. " +
178
- `Do not call ${config.toolName} if the current thinking level already matches the target level. ` +
179
- `Do not call ${config.toolName} twice in a row; reassess only after new evidence from other tool calls or user input.`
180
- );
181
- };
182
-
183
176
  export default function adaptiveThinking(pi: ExtensionAPI) {
184
177
  let runtime: RuntimeState | undefined;
185
178
  let runtimeHandlersRegistered = false;
@@ -188,11 +181,6 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
188
181
  if (runtimeHandlersRegistered) return;
189
182
  runtimeHandlersRegistered = true;
190
183
 
191
- pi.on("thinking_level_select", async (event) => {
192
- if (!runtime) return;
193
- if (isThinkingLevel(event.level)) runtime.currentLevel = event.level;
194
- });
195
-
196
184
  pi.on("tool_call", async (event) => {
197
185
  const state = runtime;
198
186
  if (!state) return;
@@ -208,8 +196,6 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
208
196
  }
209
197
  });
210
198
 
211
- pi.on("before_agent_start", async (event, ctx) => beforeAgentStart(event, ctx));
212
-
213
199
  pi.on("agent_end", async (_event, ctx) => {
214
200
  await resetTemporaryLevel(ctx);
215
201
  if (!runtime) return;
@@ -218,28 +204,6 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
218
204
  });
219
205
  };
220
206
 
221
- const beforeAgentStart = async (
222
- event: BeforeAgentStartEvent,
223
- ctx: ExtensionContext,
224
- ): Promise<BeforeAgentStartEventResult> => {
225
- const state = runtime;
226
- if (!state) return { systemPrompt: event.systemPrompt };
227
-
228
- state.lastToolCallWasReasoningTool = false;
229
- state.reasoningToolCallBackToBackById.clear();
230
-
231
- const currentLevel = state.currentLevel ?? pi.getThinkingLevel();
232
- if (isThinkingLevel(currentLevel)) state.currentLevel = currentLevel;
233
-
234
- const validLevels = resolveSupportedThinkingLevels(ctx.model);
235
- return {
236
- systemPrompt: appendSystemPromptBlock(
237
- event.systemPrompt,
238
- formatGuidance(state.config, state.currentLevel, validLevels),
239
- ),
240
- };
241
- };
242
-
243
207
  const resetTemporaryLevel = async (ctx: ExtensionContext) => {
244
208
  const state = runtime;
245
209
  const resetLevel = state?.temporaryResetLevel;
@@ -247,7 +211,6 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
247
211
 
248
212
  try {
249
213
  withSessionOnlyThinkingLevelChange(() => pi.setThinkingLevel(resetLevel));
250
- state.currentLevel = resetLevel;
251
214
  delete state.temporaryResetLevel;
252
215
  } catch (error) {
253
216
  notify(ctx, "error", `Failed to reset thinking level: ${errorMessage(error)}`, state.config);
@@ -268,9 +231,16 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
268
231
  return;
269
232
  }
270
233
 
271
- const initialLevel = pi.getThinkingLevel();
272
234
  runtime = { config, reasoningToolCallBackToBackById: new Map() };
273
- if (isThinkingLevel(initialLevel)) runtime.currentLevel = initialLevel;
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
+ }
274
244
 
275
245
  pi.registerTool({
276
246
  name: config.toolName,
@@ -278,7 +248,10 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
278
248
  description: config.toolDescription,
279
249
  promptSnippet: "Set the current Pi thinking level.",
280
250
  promptGuidelines: [
251
+ config.guidance,
281
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.`,
282
255
  ],
283
256
  parameters: ToolParameters,
284
257
  execute: async (toolCallId, params: ToolParameters, _signal, _onUpdate, ctx) => {
@@ -294,7 +267,7 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
294
267
  }
295
268
 
296
269
  const persist = params.persist ?? false;
297
- const currentLevel = state.currentLevel ?? pi.getThinkingLevel();
270
+ const currentLevel = pi.getThinkingLevel();
298
271
  if (currentLevel === level) {
299
272
  return textResult(`Thinking level is already ${level}; no change made.`);
300
273
  }
@@ -308,13 +281,18 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
308
281
  const resetLevel =
309
282
  state.persistedLevel ?? (isThinkingLevel(currentLevel) ? currentLevel : undefined);
310
283
 
284
+ if (!persist && !resetLevel) {
285
+ return textResult(
286
+ "Cannot apply a temporary thinking level because the Session Baseline is unknown.",
287
+ );
288
+ }
289
+
311
290
  try {
312
291
  withSessionOnlyThinkingLevelChange(() => pi.setThinkingLevel(level));
313
292
  } catch (error) {
314
293
  return textResult(`Failed to set thinking level: ${errorMessage(error)}`);
315
294
  }
316
295
 
317
- state.currentLevel = level;
318
296
  if (persist) {
319
297
  state.persistedLevel = level;
320
298
  delete state.temporaryResetLevel;
@@ -328,6 +306,24 @@ export default function adaptiveThinking(pi: ExtensionAPI) {
328
306
  },
329
307
  });
330
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
+
331
327
  registerRuntimeHandlers();
332
328
  });
333
329
  }