pi-adaptive-thinking 0.2.0 → 0.2.2
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/package.json +1 -7
- package/src/adaptive-thinking-lifecycle.ts +430 -0
- package/src/config-loader.ts +33 -23
- package/src/config.ts +37 -30
- package/src/index.ts +1 -329
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-adaptive-thinking",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Pi extension for adaptive reasoning-effort control",
|
|
6
6
|
"keywords": [
|
|
@@ -31,12 +31,6 @@
|
|
|
31
31
|
"access": "public",
|
|
32
32
|
"provenance": true
|
|
33
33
|
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"proper-lockfile": "^4.1.2"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/proper-lockfile": "4.1.4"
|
|
39
|
-
},
|
|
40
34
|
"peerDependencies": {
|
|
41
35
|
"@earendil-works/pi-ai": "*",
|
|
42
36
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { open, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type {
|
|
6
|
+
AgentEndEvent,
|
|
7
|
+
AgentToolResult,
|
|
8
|
+
ExtensionAPI,
|
|
9
|
+
ExtensionContext,
|
|
10
|
+
SessionStartEvent,
|
|
11
|
+
ToolCallEvent,
|
|
12
|
+
ToolCallEventResult,
|
|
13
|
+
ToolDefinition,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { type Static, Type } from "typebox";
|
|
16
|
+
import { Parse } from "typebox/value";
|
|
17
|
+
import { loadConfig, type AdaptiveThinkingConfig } from "./config-loader.js";
|
|
18
|
+
import {
|
|
19
|
+
isThinkingLevel,
|
|
20
|
+
resolveSupportedThinkingLevels,
|
|
21
|
+
type PiThinkingLevel,
|
|
22
|
+
} from "./thinking-levels.js";
|
|
23
|
+
|
|
24
|
+
type NotifyType = "info" | "warning" | "error";
|
|
25
|
+
|
|
26
|
+
type RuntimeState = {
|
|
27
|
+
config: AdaptiveThinkingConfig;
|
|
28
|
+
persistedLevel?: PiThinkingLevel;
|
|
29
|
+
temporaryResetLevel?: PiThinkingLevel;
|
|
30
|
+
lastToolCallWasReasoningTool?: boolean;
|
|
31
|
+
reasoningToolCallBackToBackById: Map<string, boolean>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ToolParameters = Type.Object(
|
|
35
|
+
{
|
|
36
|
+
level: Type.String({
|
|
37
|
+
minLength: 1,
|
|
38
|
+
description:
|
|
39
|
+
"The Pi thinking level to apply. Higher levels may improve hard-task quality but may take more time and resources.",
|
|
40
|
+
}),
|
|
41
|
+
persist: Type.Optional(
|
|
42
|
+
Type.Boolean({
|
|
43
|
+
default: false,
|
|
44
|
+
description:
|
|
45
|
+
"Whether to persist the setting for this session; otherwise it applies only for the current turn.",
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
{ additionalProperties: false },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
type ToolParameters = Static<typeof ToolParameters>;
|
|
53
|
+
|
|
54
|
+
const StatusToolParameters = Type.Object({}, { additionalProperties: false });
|
|
55
|
+
|
|
56
|
+
type ThinkingLevelStatus = {
|
|
57
|
+
currentLevel: PiThinkingLevel | "unknown";
|
|
58
|
+
supportedLevels: PiThinkingLevel[];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The extension-context fields Adaptive Thinking reads at its lifecycle and tool seams. */
|
|
62
|
+
export type AdaptiveThinkingContext = {
|
|
63
|
+
readonly cwd: string;
|
|
64
|
+
readonly hasUI: boolean;
|
|
65
|
+
readonly model: ExtensionContext["model"];
|
|
66
|
+
readonly ui: Pick<ExtensionContext["ui"], "notify">;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type AdaptiveThinkingHandler<Event, Result = undefined> = (
|
|
70
|
+
event: Event,
|
|
71
|
+
context: AdaptiveThinkingContext,
|
|
72
|
+
) => Promise<Result | void> | Result | void;
|
|
73
|
+
|
|
74
|
+
type AdaptiveThinkingToolExecution<Args, Details> = (
|
|
75
|
+
toolCallId: string,
|
|
76
|
+
parameters: Args,
|
|
77
|
+
signal: AbortSignal | undefined,
|
|
78
|
+
onUpdate: globalThis.Parameters<ToolDefinition<typeof ToolParameters, Details>["execute"]>[3],
|
|
79
|
+
context: AdaptiveThinkingContext,
|
|
80
|
+
) => Promise<AgentToolResult<Details>>;
|
|
81
|
+
|
|
82
|
+
/** Set-thinking-level parameters accepted after Pi has validated the tool schema. */
|
|
83
|
+
export type AdaptiveThinkingSetThinkingLevelParameters = Static<typeof ToolParameters>;
|
|
84
|
+
|
|
85
|
+
/** Current and supported thinking levels returned by the status tool. */
|
|
86
|
+
export type AdaptiveThinkingLevelStatus = ThinkingLevelStatus;
|
|
87
|
+
|
|
88
|
+
type SetThinkingLevelTool = Omit<ToolDefinition<typeof ToolParameters, undefined>, "execute"> & {
|
|
89
|
+
execute: AdaptiveThinkingToolExecution<AdaptiveThinkingSetThinkingLevelParameters, undefined>;
|
|
90
|
+
};
|
|
91
|
+
type GetThinkingLevelTool = Omit<
|
|
92
|
+
ToolDefinition<typeof StatusToolParameters, ThinkingLevelStatus>,
|
|
93
|
+
"execute"
|
|
94
|
+
> & {
|
|
95
|
+
execute: AdaptiveThinkingToolExecution<Record<string, never>, ThinkingLevelStatus>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Tool definitions registered by the Adaptive Thinking extension. */
|
|
99
|
+
export type AdaptiveThinkingToolDefinition = SetThinkingLevelTool | GetThinkingLevelTool;
|
|
100
|
+
|
|
101
|
+
/** Identifies the set-thinking-level tool without relying on its configurable name. */
|
|
102
|
+
export const isAdaptiveThinkingSetThinkingLevelTool = (
|
|
103
|
+
tool: AdaptiveThinkingToolDefinition,
|
|
104
|
+
): tool is SetThinkingLevelTool => tool.parameters === ToolParameters;
|
|
105
|
+
|
|
106
|
+
/** Identifies the status tool without relying on its configurable name. */
|
|
107
|
+
export const isAdaptiveThinkingStatusTool = (
|
|
108
|
+
tool: AdaptiveThinkingToolDefinition,
|
|
109
|
+
): tool is GetThinkingLevelTool => tool.parameters === StatusToolParameters;
|
|
110
|
+
|
|
111
|
+
/** Minimal Pi host capability required by the Adaptive Thinking extension. */
|
|
112
|
+
export type AdaptiveThinkingExtensionHost = {
|
|
113
|
+
onSessionStart(handler: AdaptiveThinkingHandler<SessionStartEvent>): void;
|
|
114
|
+
onToolCall(handler: AdaptiveThinkingHandler<ToolCallEvent, ToolCallEventResult>): void;
|
|
115
|
+
onAgentEnd(handler: AdaptiveThinkingHandler<AgentEndEvent>): void;
|
|
116
|
+
registerTool(tool: AdaptiveThinkingToolDefinition): void;
|
|
117
|
+
getThinkingLevel(): string;
|
|
118
|
+
setThinkingLevel(level: PiThinkingLevel): void;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const textResult = (text: string): AgentToolResult<undefined> => ({
|
|
122
|
+
content: [{ type: "text", text }],
|
|
123
|
+
details: undefined,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const thinkingLevelStatusResult = (
|
|
127
|
+
currentLevel: PiThinkingLevel | "unknown",
|
|
128
|
+
supportedLevels: PiThinkingLevel[],
|
|
129
|
+
): AgentToolResult<ThinkingLevelStatus> => ({
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `Current thinking level: ${currentLevel}. Supported thinking levels: ${supportedLevels.join(", ")}.`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
details: { currentLevel, supportedLevels },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const errorMessage = (cause: Error) => cause.message;
|
|
140
|
+
|
|
141
|
+
const SettingsDocumentSchema = Type.Object(
|
|
142
|
+
{ defaultThinkingLevel: Type.Optional(Type.String()) },
|
|
143
|
+
{ additionalProperties: Type.Unknown() },
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const agentDir = () => process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
147
|
+
|
|
148
|
+
const globalSettingsPath = () => join(agentDir(), "settings.json");
|
|
149
|
+
|
|
150
|
+
// ponytail: Node's wx exclusive-create plus an asynchronous fixed-delay retry loop replaces
|
|
151
|
+
// proper-lockfile; the bound matches its previous policy (99 retries at a fixed 20 ms delay).
|
|
152
|
+
// The .lock suffix distinguishes owned lock files from the legacy always-present marker the
|
|
153
|
+
// previous implementation pre-created next to the settings document.
|
|
154
|
+
// Stale recovery assumes the critical section stays synchronous; use a heartbeat lock if it gains
|
|
155
|
+
// asynchronous work.
|
|
156
|
+
const SETTINGS_LOCK_RETRY_DELAY_MS = 20;
|
|
157
|
+
const SETTINGS_LOCK_RETRIES = 99;
|
|
158
|
+
const SETTINGS_LOCK_STALE_MS = 10_000;
|
|
159
|
+
|
|
160
|
+
const sleep = (ms: number): Promise<void> =>
|
|
161
|
+
new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
162
|
+
|
|
163
|
+
const acquireSettingsLock = async (lockPath: string): Promise<void> => {
|
|
164
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
165
|
+
try {
|
|
166
|
+
const handle = await open(lockPath, "wx");
|
|
167
|
+
await handle.close();
|
|
168
|
+
return;
|
|
169
|
+
} catch (cause) {
|
|
170
|
+
if (!(cause instanceof Error) || !("code" in cause) || cause.code !== "EEXIST") throw cause;
|
|
171
|
+
try {
|
|
172
|
+
const lock = await stat(lockPath);
|
|
173
|
+
if (lock.mtimeMs < Date.now() - SETTINGS_LOCK_STALE_MS) {
|
|
174
|
+
await rm(lockPath, { force: true });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
} catch (statCause) {
|
|
178
|
+
if (!(statCause instanceof Error) || !("code" in statCause) || statCause.code !== "ENOENT")
|
|
179
|
+
throw statCause;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (attempt >= SETTINGS_LOCK_RETRIES) throw cause;
|
|
183
|
+
}
|
|
184
|
+
await sleep(SETTINGS_LOCK_RETRY_DELAY_MS);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const withSettingsLock = async <T>(settingsPath: string, fn: () => T): Promise<T> => {
|
|
189
|
+
mkdirSync(join(settingsPath, ".."), { recursive: true });
|
|
190
|
+
const lockPath = `${settingsPath}.adaptive-thinking.lock`;
|
|
191
|
+
await acquireSettingsLock(lockPath);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
return await fn();
|
|
195
|
+
} finally {
|
|
196
|
+
await rm(lockPath, { force: true });
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const readDefaultThinkingLevel = (settingsPath: string): PiThinkingLevel | undefined => {
|
|
201
|
+
if (!existsSync(settingsPath)) return undefined;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const settings = Parse(SettingsDocumentSchema, JSON.parse(readFileSync(settingsPath, "utf-8")));
|
|
205
|
+
const level = settings.defaultThinkingLevel;
|
|
206
|
+
return level !== undefined && isThinkingLevel(level) ? level : undefined;
|
|
207
|
+
} catch {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const restoreDefaultThinkingLevel = (
|
|
213
|
+
settingsPath: string,
|
|
214
|
+
previousDefaultThinkingLevel: PiThinkingLevel | undefined,
|
|
215
|
+
) => {
|
|
216
|
+
if (!existsSync(settingsPath)) return;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const settings = Parse(SettingsDocumentSchema, JSON.parse(readFileSync(settingsPath, "utf-8")));
|
|
220
|
+
if (previousDefaultThinkingLevel === undefined) {
|
|
221
|
+
delete settings.defaultThinkingLevel;
|
|
222
|
+
} else {
|
|
223
|
+
settings.defaultThinkingLevel = previousDefaultThinkingLevel;
|
|
224
|
+
}
|
|
225
|
+
writeFileSync(settingsPath, JSON.stringify(settings, undefined, 2) + "\n");
|
|
226
|
+
} catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const withSessionOnlyThinkingLevelChange = async (changeThinkingLevel: () => void) => {
|
|
232
|
+
const settingsPath = globalSettingsPath();
|
|
233
|
+
|
|
234
|
+
await withSettingsLock(settingsPath, () => {
|
|
235
|
+
const previousDefaultThinkingLevel = readDefaultThinkingLevel(settingsPath);
|
|
236
|
+
|
|
237
|
+
changeThinkingLevel();
|
|
238
|
+
|
|
239
|
+
restoreDefaultThinkingLevel(settingsPath, previousDefaultThinkingLevel);
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const notify = (
|
|
244
|
+
ctx: AdaptiveThinkingContext,
|
|
245
|
+
type: NotifyType,
|
|
246
|
+
message: string,
|
|
247
|
+
config?: Pick<AdaptiveThinkingConfig, "quiet">,
|
|
248
|
+
) => {
|
|
249
|
+
if (config?.quiet) return;
|
|
250
|
+
if (!ctx.hasUI) return;
|
|
251
|
+
ctx.ui.notify(message, type);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/** Registers Adaptive Thinking against its narrow lifecycle, tool, and thinking-level host. */
|
|
255
|
+
export function registerAdaptiveThinking(pi: AdaptiveThinkingExtensionHost) {
|
|
256
|
+
let runtime: RuntimeState | undefined;
|
|
257
|
+
let runtimeHandlersRegistered = false;
|
|
258
|
+
|
|
259
|
+
const registerRuntimeHandlers = () => {
|
|
260
|
+
if (runtimeHandlersRegistered) return;
|
|
261
|
+
runtimeHandlersRegistered = true;
|
|
262
|
+
|
|
263
|
+
pi.onToolCall(async (event) => {
|
|
264
|
+
const state = runtime;
|
|
265
|
+
if (!state) return;
|
|
266
|
+
|
|
267
|
+
if (event.toolName === state.config.toolName) {
|
|
268
|
+
state.reasoningToolCallBackToBackById.set(
|
|
269
|
+
event.toolCallId,
|
|
270
|
+
state.lastToolCallWasReasoningTool ?? false,
|
|
271
|
+
);
|
|
272
|
+
state.lastToolCallWasReasoningTool = true;
|
|
273
|
+
} else {
|
|
274
|
+
state.lastToolCallWasReasoningTool = false;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
pi.onAgentEnd(async (_event, ctx) => {
|
|
279
|
+
await resetTemporaryLevel(ctx);
|
|
280
|
+
if (!runtime) return;
|
|
281
|
+
runtime.lastToolCallWasReasoningTool = false;
|
|
282
|
+
runtime.reasoningToolCallBackToBackById.clear();
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const resetTemporaryLevel = async (ctx: AdaptiveThinkingContext) => {
|
|
287
|
+
const state = runtime;
|
|
288
|
+
const resetLevel = state?.temporaryResetLevel;
|
|
289
|
+
if (!state || !resetLevel) return;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await withSessionOnlyThinkingLevelChange(() => pi.setThinkingLevel(resetLevel));
|
|
293
|
+
delete state.temporaryResetLevel;
|
|
294
|
+
} catch (cause) {
|
|
295
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
296
|
+
notify(ctx, "error", `Failed to reset thinking level: ${errorMessage(error)}`, state.config);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const registerSetThinkingLevelTool = (tool: SetThinkingLevelTool) => pi.registerTool(tool);
|
|
301
|
+
const registerGetThinkingLevelTool = (tool: GetThinkingLevelTool) => pi.registerTool(tool);
|
|
302
|
+
|
|
303
|
+
pi.onSessionStart(async (_event, ctx) => {
|
|
304
|
+
const configResult = await loadConfig({ cwd: ctx.cwd });
|
|
305
|
+
if (!configResult.success) {
|
|
306
|
+
runtime = undefined;
|
|
307
|
+
notify(ctx, "error", configResult.error.message);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { config } = configResult;
|
|
312
|
+
if (!config.enabled) {
|
|
313
|
+
runtime = undefined;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
runtime = { config, reasoningToolCallBackToBackById: new Map() };
|
|
318
|
+
|
|
319
|
+
if (configResult.usedDeprecatedSystemPrompt) {
|
|
320
|
+
notify(
|
|
321
|
+
ctx,
|
|
322
|
+
"warning",
|
|
323
|
+
"Adaptive Thinking configuration: systemPrompt is deprecated; rename it to guidance.",
|
|
324
|
+
config,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
registerSetThinkingLevelTool({
|
|
329
|
+
name: config.toolName,
|
|
330
|
+
label: "Set Thinking Level",
|
|
331
|
+
description: config.toolDescription,
|
|
332
|
+
promptSnippet: "Set the current Pi thinking level.",
|
|
333
|
+
promptGuidelines: [
|
|
334
|
+
config.guidance,
|
|
335
|
+
`Use ${config.toolName} to change the thinking level when task complexity justifies a different level.`,
|
|
336
|
+
`Use ${config.statusToolName} only when the current or supported thinking levels are uncertain; do not poll it routinely.`,
|
|
337
|
+
`Do not call ${config.toolName} twice in a row; reassess only after new evidence from other tool calls or user input.`,
|
|
338
|
+
],
|
|
339
|
+
parameters: ToolParameters,
|
|
340
|
+
execute: async (toolCallId, params: ToolParameters, _signal, _onUpdate, ctx) => {
|
|
341
|
+
const state = runtime;
|
|
342
|
+
if (!state) return textResult("Adaptive Thinking is not enabled for this session.");
|
|
343
|
+
|
|
344
|
+
const level = params.level.trim();
|
|
345
|
+
const validLevels = resolveSupportedThinkingLevels(ctx.model);
|
|
346
|
+
if (!isThinkingLevel(level) || !validLevels.includes(level)) {
|
|
347
|
+
return textResult(
|
|
348
|
+
`Invalid thinking level: ${level}. Valid levels: ${validLevels.join(", ")}.`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const persist = params.persist ?? false;
|
|
353
|
+
const currentLevel = pi.getThinkingLevel();
|
|
354
|
+
if (currentLevel === level) {
|
|
355
|
+
return textResult(`Thinking level is already ${level}; no change made.`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (state.reasoningToolCallBackToBackById.get(toolCallId) ?? false) {
|
|
359
|
+
return textResult(
|
|
360
|
+
`Thinking level change skipped because the previous tool call was also ${state.config.toolName}. Reassess after another tool call or new user input.`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const resetLevel =
|
|
365
|
+
state.persistedLevel ?? (isThinkingLevel(currentLevel) ? currentLevel : undefined);
|
|
366
|
+
|
|
367
|
+
if (!persist && !resetLevel) {
|
|
368
|
+
return textResult(
|
|
369
|
+
"Cannot apply a temporary thinking level because the Session Baseline is unknown.",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await withSessionOnlyThinkingLevelChange(() => pi.setThinkingLevel(level));
|
|
375
|
+
} catch (cause) {
|
|
376
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
377
|
+
return textResult(`Failed to set thinking level: ${errorMessage(error)}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (persist) {
|
|
381
|
+
state.persistedLevel = level;
|
|
382
|
+
delete state.temporaryResetLevel;
|
|
383
|
+
} else if (resetLevel && resetLevel !== level) {
|
|
384
|
+
state.temporaryResetLevel = resetLevel;
|
|
385
|
+
} else {
|
|
386
|
+
delete state.temporaryResetLevel;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return textResult(`Thinking level set to ${level}`);
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
registerGetThinkingLevelTool({
|
|
394
|
+
name: config.statusToolName,
|
|
395
|
+
label: "Get Thinking Level",
|
|
396
|
+
description: "Get the current and supported Pi thinking levels",
|
|
397
|
+
promptSnippet: "Inspect the current and supported Pi thinking levels.",
|
|
398
|
+
promptGuidelines: [
|
|
399
|
+
`Use ${config.statusToolName} only when thinking-level state is uncertain; do not poll it routinely.`,
|
|
400
|
+
],
|
|
401
|
+
parameters: StatusToolParameters,
|
|
402
|
+
execute: async (_toolCallId, _params, _signal, _onUpdate, ctx) => {
|
|
403
|
+
const currentLevel = pi.getThinkingLevel();
|
|
404
|
+
return thinkingLevelStatusResult(
|
|
405
|
+
isThinkingLevel(currentLevel) ? currentLevel : "unknown",
|
|
406
|
+
resolveSupportedThinkingLevels(ctx.model),
|
|
407
|
+
);
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
registerRuntimeHandlers();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Adapts Pi's complete ExtensionAPI to the Adaptive Thinking lifecycle capability. */
|
|
416
|
+
export default function adaptiveThinkingExtension(pi: ExtensionAPI) {
|
|
417
|
+
registerAdaptiveThinking({
|
|
418
|
+
onSessionStart: (handler) => pi.on("session_start", handler),
|
|
419
|
+
onToolCall: (handler) => pi.on("tool_call", handler),
|
|
420
|
+
onAgentEnd: (handler) => pi.on("agent_end", handler),
|
|
421
|
+
// ponytail: the branches look identical but narrow the union so registerTool's
|
|
422
|
+
// generics infer per concrete ToolDefinition instead of falling back to defaults.
|
|
423
|
+
registerTool: (tool) => {
|
|
424
|
+
if (isAdaptiveThinkingSetThinkingLevelTool(tool)) pi.registerTool(tool);
|
|
425
|
+
else pi.registerTool(tool);
|
|
426
|
+
},
|
|
427
|
+
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
428
|
+
setThinkingLevel: (level) => pi.setThinkingLevel(level),
|
|
429
|
+
});
|
|
430
|
+
}
|
package/src/config-loader.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
parseAdaptiveThinkingConfig,
|
|
6
|
+
type AdaptiveThinkingConfig,
|
|
7
|
+
type ParsedAdaptiveThinkingConfig,
|
|
8
|
+
} from "./config.js";
|
|
5
9
|
|
|
6
10
|
export type { AdaptiveThinkingConfig } from "./config.js";
|
|
7
11
|
|
|
@@ -19,18 +23,24 @@ export type LoadConfigResult =
|
|
|
19
23
|
}
|
|
20
24
|
| { success: false; source: string; error: Error };
|
|
21
25
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error));
|
|
26
|
+
const hasErrorCode = (cause: NodeJS.ErrnoException, code: string) =>
|
|
27
|
+
Object.hasOwn(cause, "code") && cause.code === code;
|
|
27
28
|
|
|
28
|
-
const invalidConfig = (source: string,
|
|
29
|
+
const invalidConfig = (source: string, cause: Error): LoadConfigResult => ({
|
|
29
30
|
success: false,
|
|
30
31
|
source,
|
|
31
|
-
error: new Error(`Invalid Adaptive Thinking configuration in ${source}: ${
|
|
32
|
+
error: new Error(`Invalid Adaptive Thinking configuration in ${source}: ${cause.message}`, {
|
|
33
|
+
cause,
|
|
34
|
+
}),
|
|
32
35
|
});
|
|
33
36
|
|
|
37
|
+
const readAdaptiveThinkingConfig = async (
|
|
38
|
+
source: string,
|
|
39
|
+
): Promise<ParsedAdaptiveThinkingConfig> => {
|
|
40
|
+
const raw = await readFile(source, "utf8");
|
|
41
|
+
return parseAdaptiveThinkingConfig(JSON.parse(raw));
|
|
42
|
+
};
|
|
43
|
+
|
|
34
44
|
export const loadConfig = async ({
|
|
35
45
|
cwd,
|
|
36
46
|
homeDir = homedir(),
|
|
@@ -41,26 +51,26 @@ export const loadConfig = async ({
|
|
|
41
51
|
];
|
|
42
52
|
|
|
43
53
|
for (const source of candidates) {
|
|
44
|
-
let
|
|
54
|
+
let parsedConfig: ParsedAdaptiveThinkingConfig;
|
|
45
55
|
try {
|
|
46
|
-
|
|
47
|
-
} catch (
|
|
48
|
-
|
|
56
|
+
parsedConfig = await readAdaptiveThinkingConfig(source);
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
59
|
+
const fileSystemError: NodeJS.ErrnoException = error;
|
|
60
|
+
if (hasErrorCode(fileSystemError, "ENOENT")) continue;
|
|
49
61
|
return invalidConfig(source, error);
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
60
|
-
} catch (error) {
|
|
61
|
-
return invalidConfig(source, error);
|
|
64
|
+
const result: Extract<LoadConfigResult, { success: true }> = {
|
|
65
|
+
success: true,
|
|
66
|
+
source,
|
|
67
|
+
config: parsedConfig.config,
|
|
68
|
+
};
|
|
69
|
+
if (parsedConfig.usedDeprecatedSystemPrompt) {
|
|
70
|
+
result.usedDeprecatedSystemPrompt = true;
|
|
62
71
|
}
|
|
72
|
+
return result;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
return { success: true, config:
|
|
75
|
+
return { success: true, config: parseAdaptiveThinkingConfig(undefined).config };
|
|
66
76
|
};
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JsonValue } from "@earendil-works/pi-agent-core";
|
|
1
2
|
import { Type } from "typebox";
|
|
2
3
|
import { Parse } from "typebox/value";
|
|
3
4
|
|
|
@@ -25,48 +26,54 @@ export const configDefaults: AdaptiveThinkingConfig = {
|
|
|
25
26
|
guidance: defaultGuidance,
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
const ConfigValueSchema = Type.Object({
|
|
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.String({ minLength: 1 }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const ConfigOverridesSchema = Type.Partial(
|
|
39
|
+
Type.Intersect([
|
|
40
|
+
ConfigValueSchema,
|
|
41
|
+
Type.Object({ systemPrompt: Type.Optional(Type.String({ minLength: 1 })) }),
|
|
42
|
+
]),
|
|
38
43
|
{ additionalProperties: false },
|
|
39
44
|
);
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/** Returns whether raw configuration uses the deprecated system prompt alias. */
|
|
47
|
-
export const usesDeprecatedSystemPrompt = (input: unknown) => hasOwnProperty(input, "systemPrompt");
|
|
46
|
+
/** Parsed configuration and metadata retained from the configuration ingress boundary. */
|
|
47
|
+
export type ParsedAdaptiveThinkingConfig = {
|
|
48
|
+
config: AdaptiveThinkingConfig;
|
|
49
|
+
usedDeprecatedSystemPrompt: boolean;
|
|
50
|
+
};
|
|
48
51
|
|
|
49
|
-
/** Parses configuration and normalizes
|
|
50
|
-
export const
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
/** Parses optional configuration values and normalizes the deprecated system prompt alias. */
|
|
53
|
+
export const parseAdaptiveThinkingConfig = (
|
|
54
|
+
input: JsonValue | undefined,
|
|
55
|
+
): ParsedAdaptiveThinkingConfig => {
|
|
56
|
+
const overrides = input === undefined ? {} : Parse(ConfigOverridesSchema, input);
|
|
57
|
+
const usesGuidance = overrides.guidance !== undefined;
|
|
58
|
+
const usesSystemPrompt = overrides.systemPrompt !== undefined;
|
|
53
59
|
if (usesGuidance && usesSystemPrompt) {
|
|
54
60
|
throw new Error(
|
|
55
61
|
"Adaptive Thinking configuration cannot contain both guidance and systemPrompt",
|
|
56
62
|
);
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const guidance = overrides.guidance ?? overrides.systemPrompt ?? configDefaults.guidance;
|
|
66
|
+
const config: AdaptiveThinkingConfig = {
|
|
67
|
+
enabled: overrides.enabled ?? configDefaults.enabled,
|
|
68
|
+
quiet: overrides.quiet ?? configDefaults.quiet,
|
|
69
|
+
toolName: overrides.toolName ?? configDefaults.toolName,
|
|
70
|
+
toolDescription: overrides.toolDescription ?? configDefaults.toolDescription,
|
|
71
|
+
statusToolName: overrides.statusToolName ?? configDefaults.statusToolName,
|
|
72
|
+
guidance,
|
|
64
73
|
};
|
|
65
|
-
delete (merged as Record<string, unknown>).systemPrompt;
|
|
66
|
-
|
|
67
|
-
const config = Parse(ConfigInputSchema, merged) as AdaptiveThinkingConfig;
|
|
68
74
|
if (config.toolName === config.statusToolName) {
|
|
69
75
|
throw new Error("Adaptive Thinking toolName and statusToolName must be different");
|
|
70
76
|
}
|
|
71
|
-
|
|
77
|
+
|
|
78
|
+
return { config, usedDeprecatedSystemPrompt: usesSystemPrompt };
|
|
72
79
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,329 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export { default } from "./adaptive-thinking-lifecycle.js";
|