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 +12 -3
- package/package.json +1 -1
- package/src/config-loader.ts +14 -3
- package/src/config.ts +47 -9
- package/src/index.ts +57 -61
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
|
|
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
|
|
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
|
-
"
|
|
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
package/src/config-loader.ts
CHANGED
|
@@ -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
|
-
| {
|
|
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
|
-
|
|
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
|
|
1
|
+
import { Type } from "typebox";
|
|
2
2
|
import { Parse } from "typebox/value";
|
|
3
3
|
|
|
4
|
-
export const
|
|
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
|
|
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
|
-
|
|
24
|
+
statusToolName: "get_thinking_level",
|
|
25
|
+
guidance: defaultGuidance,
|
|
16
26
|
};
|
|
17
27
|
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
}
|