@oh-my-pi/pi-coding-agent 15.7.1 → 15.7.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/types/auto-thinking/classifier.d.ts +35 -0
  3. package/dist/types/config/settings-schema.d.ts +24 -4
  4. package/dist/types/edit/hashline/diff.d.ts +6 -0
  5. package/dist/types/modes/components/model-selector.d.ts +3 -2
  6. package/dist/types/modes/theme/theme.d.ts +2 -1
  7. package/dist/types/sdk.d.ts +2 -1
  8. package/dist/types/session/agent-session.d.ts +22 -9
  9. package/dist/types/thinking.d.ts +39 -1
  10. package/dist/types/tiny/device.d.ts +3 -3
  11. package/dist/types/tiny/models.d.ts +19 -0
  12. package/package.json +9 -9
  13. package/src/auto-thinking/classifier.ts +180 -0
  14. package/src/config/settings-schema.ts +24 -4
  15. package/src/edit/hashline/diff.ts +10 -2
  16. package/src/edit/streaming.ts +17 -6
  17. package/src/eval/__tests__/shared-executors.test.ts +32 -0
  18. package/src/eval/js/shared/local-module-loader.ts +75 -10
  19. package/src/internal-urls/docs-index.generated.ts +2 -2
  20. package/src/main.ts +6 -1
  21. package/src/modes/acp/acp-agent.ts +13 -3
  22. package/src/modes/components/footer.ts +10 -3
  23. package/src/modes/components/model-selector.ts +20 -11
  24. package/src/modes/components/settings-defs.ts +7 -0
  25. package/src/modes/components/settings-selector.ts +4 -1
  26. package/src/modes/components/status-line/segments.ts +13 -5
  27. package/src/modes/controllers/event-controller.ts +5 -1
  28. package/src/modes/controllers/selector-controller.ts +20 -6
  29. package/src/modes/theme/theme.ts +6 -0
  30. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  31. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  32. package/src/sdk.ts +25 -7
  33. package/src/session/agent-session.ts +193 -32
  34. package/src/thinking.ts +73 -1
  35. package/src/tiny/device.ts +4 -10
  36. package/src/tiny/models.ts +24 -0
@@ -52,7 +52,6 @@ import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "@oh-my-pi/pi-agent-core/
52
52
  import type {
53
53
  AssistantMessage,
54
54
  Context,
55
- Effort,
56
55
  ImageContent,
57
56
  Message,
58
57
  MessageAttribution,
@@ -69,6 +68,7 @@ import type {
69
68
  import {
70
69
  calculateRateLimitBackoffMs,
71
70
  clearAnthropicFastModeFallback,
71
+ Effort,
72
72
  getSupportedEfforts,
73
73
  isContextOverflow,
74
74
  isUsageLimitError,
@@ -88,6 +88,7 @@ import {
88
88
  Snowflake,
89
89
  } from "@oh-my-pi/pi-utils";
90
90
  import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
91
+ import { classifyDifficulty } from "../auto-thinking/classifier";
91
92
  import { reset as resetCapabilities } from "../capability";
92
93
  import type { Rule } from "../capability/rule";
93
94
  import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
@@ -165,7 +166,14 @@ import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" w
165
166
  import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
166
167
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
167
168
  import { invalidateHostMetadata } from "../ssh/connection-manager";
168
- import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
169
+ import {
170
+ AUTO_THINKING,
171
+ type ConfiguredThinkingLevel,
172
+ clampAutoThinkingEffort,
173
+ resolveProvisionalAutoLevel,
174
+ resolveThinkingLevelForModel,
175
+ toReasoningEffort,
176
+ } from "../thinking";
169
177
  import { shutdownTinyTitleClient } from "../tiny/title-client";
170
178
  import {
171
179
  buildDiscoverableToolSearchIndex,
@@ -241,7 +249,14 @@ export type AgentSessionEvent =
241
249
  | { type: "todo_auto_clear" }
242
250
  | { type: "irc_message"; message: CustomMessage }
243
251
  | { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string }
244
- | { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
252
+ | {
253
+ type: "thinking_level_changed";
254
+ thinkingLevel: ThinkingLevel | undefined;
255
+ /** The user-configured selector when it differs from the effective level (e.g. `auto`). */
256
+ configured?: ConfiguredThinkingLevel;
257
+ /** The level `auto` resolved to this turn, once classified. */
258
+ resolved?: Effort;
259
+ }
245
260
  | { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
246
261
 
247
262
  /** Listener function for agent session events */
@@ -265,7 +280,7 @@ export interface AgentSessionConfig {
265
280
  /** Models to cycle through with Ctrl+P (from --models flag) */
266
281
  scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
267
282
  /** Initial session thinking selector. */
268
- thinkingLevel?: ThinkingLevel;
283
+ thinkingLevel?: ConfiguredThinkingLevel;
269
284
  /** Prompt templates for expansion */
270
285
  promptTemplates?: PromptTemplate[];
271
286
  /** File-based slash commands for expansion */
@@ -445,8 +460,8 @@ interface RetryFallbackSelector {
445
460
  interface ActiveRetryFallbackState {
446
461
  role: string;
447
462
  originalSelector: string;
448
- originalThinkingLevel: ThinkingLevel | undefined;
449
- lastAppliedFallbackThinkingLevel: ThinkingLevel | undefined;
463
+ originalThinkingLevel: ConfiguredThinkingLevel | undefined;
464
+ lastAppliedFallbackThinkingLevel: ConfiguredThinkingLevel | undefined;
450
465
  }
451
466
 
452
467
  function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
@@ -782,7 +797,12 @@ export class AgentSession {
782
797
  readonly configWarnings: string[] = [];
783
798
 
784
799
  #scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
800
+ /** Effective, metadata-clamped thinking level applied to the agent (never `auto`). */
785
801
  #thinkingLevel: ThinkingLevel | undefined;
802
+ /** True when the user configured `auto`; the effective level is resolved per turn. */
803
+ #autoThinking: boolean = false;
804
+ /** The level `auto` last resolved to (for UI); undefined until a turn is classified. */
805
+ #autoResolvedLevel: Effort | undefined;
786
806
  #promptTemplates: PromptTemplate[];
787
807
  #slashCommands: FileSlashCommand[];
788
808
 
@@ -1041,7 +1061,15 @@ export class AgentSession {
1041
1061
  this.#parentEvalSessionId = config.parentEvalSessionId;
1042
1062
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
1043
1063
  this.#scopedModels = config.scopedModels ?? [];
1044
- this.#thinkingLevel = config.thinkingLevel;
1064
+ if (config.thinkingLevel === AUTO_THINKING) {
1065
+ // `auto` is session-level: keep the flag and show a provisional concrete
1066
+ // level (the agent's initial effort was already set by the caller) until
1067
+ // the first user turn is classified.
1068
+ this.#autoThinking = true;
1069
+ this.#thinkingLevel = resolveProvisionalAutoLevel(this.model);
1070
+ } else {
1071
+ this.#thinkingLevel = config.thinkingLevel;
1072
+ }
1045
1073
  this.#promptTemplates = config.promptTemplates ?? [];
1046
1074
  this.#slashCommands = config.slashCommands ?? [];
1047
1075
  this.#extensionRunner = config.extensionRunner;
@@ -2935,11 +2963,26 @@ export class AgentSession {
2935
2963
  return this.agent.state.model;
2936
2964
  }
2937
2965
 
2938
- /** Current thinking level */
2966
+ /** Effective thinking level applied to the agent (the resolved level when `auto`). */
2939
2967
  get thinkingLevel(): ThinkingLevel | undefined {
2940
2968
  return this.#thinkingLevel;
2941
2969
  }
2942
2970
 
2971
+ /** The selector the user configured: `auto` when auto mode is active, else the effective level. */
2972
+ configuredThinkingLevel(): ConfiguredThinkingLevel | undefined {
2973
+ return this.#autoThinking ? AUTO_THINKING : this.#thinkingLevel;
2974
+ }
2975
+
2976
+ /** True when `auto` thinking mode is active. */
2977
+ get isAutoThinking(): boolean {
2978
+ return this.#autoThinking;
2979
+ }
2980
+
2981
+ /** The level `auto` resolved to for the current turn (undefined until classified). */
2982
+ autoResolvedThinkingLevel(): Effort | undefined {
2983
+ return this.#autoResolvedLevel;
2984
+ }
2985
+
2943
2986
  get serviceTier(): ServiceTier | undefined {
2944
2987
  return this.agent.serviceTier;
2945
2988
  }
@@ -4304,6 +4347,17 @@ export class AgentSession {
4304
4347
  return;
4305
4348
  }
4306
4349
 
4350
+ // Auto thinking: classify this real user turn and set the effective level
4351
+ // before the model request. Synthetic/tool-continuation turns (developer/
4352
+ // custom roles) and non-auto sessions are skipped. Never blocks the turn —
4353
+ // failures fall back to a concrete level inside the helper.
4354
+ if (this.#autoThinking && message.role === "user") {
4355
+ await this.#applyAutoThinkingLevel(expandedText, generation);
4356
+ if (this.#promptGeneration !== generation) {
4357
+ return;
4358
+ }
4359
+ }
4360
+
4307
4361
  const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
4308
4362
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
4309
4363
  if (!options?.skipPostPromptRecoveryWait) {
@@ -5061,8 +5115,8 @@ export class AgentSession {
5061
5115
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
5062
5116
 
5063
5117
  // Re-apply thinking for the newly selected model. Prefer the model's
5064
- // configured defaultLevel; otherwise preserve the current level.
5065
- this.setThinkingLevel(model.thinking?.defaultLevel ?? this.thinkingLevel);
5118
+ // configured defaultLevel; otherwise preserve the current level (or auto).
5119
+ this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
5066
5120
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5067
5121
  }
5068
5122
 
@@ -5084,8 +5138,12 @@ export class AgentSession {
5084
5138
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
5085
5139
 
5086
5140
  // Apply explicit thinking level if given; otherwise prefer the model's
5087
- // configured defaultLevel; otherwise re-clamp the current level.
5088
- this.setThinkingLevel(thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
5141
+ // configured defaultLevel; otherwise re-clamp the current level (or auto).
5142
+ if (thinkingLevel !== undefined) {
5143
+ this.setThinkingLevel(thinkingLevel);
5144
+ } else {
5145
+ this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
5146
+ }
5089
5147
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5090
5148
  }
5091
5149
 
@@ -5233,8 +5291,8 @@ export class AgentSession {
5233
5291
  this.settings.setModelRole("default", this.#formatRoleModelValue("default", next.model));
5234
5292
  this.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
5235
5293
 
5236
- // Apply the scoped model's configured thinking level
5237
- this.setThinkingLevel(next.thinkingLevel);
5294
+ // Apply the scoped model's configured thinking level, preserving auto.
5295
+ this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : next.thinkingLevel);
5238
5296
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5239
5297
 
5240
5298
  return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
@@ -5263,8 +5321,8 @@ export class AgentSession {
5263
5321
  this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
5264
5322
  this.settings.setModelRole("default", this.#formatRoleModelValue("default", nextModel));
5265
5323
  this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
5266
- // Re-apply the current thinking level for the newly selected model
5267
- this.setThinkingLevel(this.thinkingLevel);
5324
+ // Re-apply the current thinking level (or auto) for the newly selected model
5325
+ this.#reapplyThinkingLevel();
5268
5326
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5269
5327
 
5270
5328
  return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
@@ -5282,10 +5340,29 @@ export class AgentSession {
5282
5340
  // =========================================================================
5283
5341
 
5284
5342
  /**
5285
- * Set thinking level.
5286
- * Saves the effective metadata-clamped level to session and settings only if it changes.
5343
+ * Set the thinking level. `auto` enables per-turn classification (session-level,
5344
+ * never written to the session log); a concrete level clears auto. The effective
5345
+ * metadata-clamped level is saved to the session/settings only when it changes.
5287
5346
  */
5288
- setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
5347
+ setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist: boolean = false): void {
5348
+ if (level === AUTO_THINKING) {
5349
+ const provisional = resolveProvisionalAutoLevel(this.model);
5350
+ const wasAuto = this.#autoThinking;
5351
+ this.#autoThinking = true;
5352
+ this.#autoResolvedLevel = undefined;
5353
+ this.#thinkingLevel = provisional;
5354
+ this.agent.setThinkingLevel(toReasoningEffort(provisional));
5355
+ if (persist) {
5356
+ this.settings.set("defaultThinkingLevel", AUTO_THINKING);
5357
+ }
5358
+ if (!wasAuto || this.#thinkingLevel !== provisional) {
5359
+ this.#emit({ type: "thinking_level_changed", thinkingLevel: provisional, configured: AUTO_THINKING });
5360
+ }
5361
+ return;
5362
+ }
5363
+
5364
+ this.#autoThinking = false;
5365
+ this.#autoResolvedLevel = undefined;
5289
5366
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
5290
5367
  const isChanging = effectiveLevel !== this.#thinkingLevel;
5291
5368
 
@@ -5302,14 +5379,28 @@ export class AgentSession {
5302
5379
  }
5303
5380
 
5304
5381
  /**
5305
- * Cycle to next thinking level.
5306
- * @returns New level, or undefined if model doesn't support thinking
5382
+ * Re-apply the active thinking selection after a model change. Preserves `auto`
5383
+ * (re-clamping the provisional level to the new model); otherwise re-applies the
5384
+ * preferred default or the current effective level.
5307
5385
  */
5308
- cycleThinkingLevel(): ThinkingLevel | undefined {
5386
+ #reapplyThinkingLevel(preferredDefault?: ThinkingLevel): void {
5387
+ this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : (preferredDefault ?? this.#thinkingLevel));
5388
+ }
5389
+
5390
+ /**
5391
+ * Cycle to next thinking level: off → auto → minimal..xhigh → off.
5392
+ * @returns New selector, or undefined if model doesn't support thinking
5393
+ */
5394
+ cycleThinkingLevel(): ConfiguredThinkingLevel | undefined {
5309
5395
  if (!this.model?.reasoning) return undefined;
5310
5396
 
5311
- const levels = [ThinkingLevel.Off, ...this.getAvailableThinkingLevels()];
5312
- const currentLevel = this.thinkingLevel === ThinkingLevel.Inherit ? ThinkingLevel.Off : this.thinkingLevel;
5397
+ const levels: ConfiguredThinkingLevel[] = [
5398
+ ThinkingLevel.Off,
5399
+ AUTO_THINKING,
5400
+ ...this.getAvailableThinkingLevels(),
5401
+ ];
5402
+ const configured = this.configuredThinkingLevel();
5403
+ const currentLevel = configured === ThinkingLevel.Inherit ? ThinkingLevel.Off : configured;
5313
5404
  const currentIndex = currentLevel ? levels.indexOf(currentLevel) : -1;
5314
5405
  const nextIndex = (currentIndex + 1) % levels.length;
5315
5406
  const nextLevel = levels[nextIndex];
@@ -5319,6 +5410,61 @@ export class AgentSession {
5319
5410
  return nextLevel;
5320
5411
  }
5321
5412
 
5413
+ /** Timeout (ms) for per-turn auto-thinking classification before falling back. */
5414
+ static readonly #AUTO_THINKING_TIMEOUT_MS = 4000;
5415
+
5416
+ /**
5417
+ * Classify the current user turn and set the effective thinking level for it.
5418
+ * Bounded by a timeout + abort; on any failure (no smol model, timeout, parse
5419
+ * error) it falls back to the provisional concrete level and continues. Never
5420
+ * throws into the turn, and never clears `#autoThinking` (auto stays active).
5421
+ */
5422
+ async #applyAutoThinkingLevel(promptText: string, generation: number): Promise<void> {
5423
+ const model = this.model;
5424
+ if (!model?.reasoning) return;
5425
+
5426
+ let resolved: Effort | undefined;
5427
+ if (containsUltrathink(promptText)) {
5428
+ // The user explicitly asked for maximum thinking; bypass the classifier
5429
+ // and jump straight to the highest auto-supported level for this model.
5430
+ resolved = clampAutoThinkingEffort(model, Effort.XHigh);
5431
+ } else {
5432
+ const controller = new AbortController();
5433
+ const timer = setTimeout(() => controller.abort(), AgentSession.#AUTO_THINKING_TIMEOUT_MS);
5434
+ try {
5435
+ resolved = await classifyDifficulty(promptText, {
5436
+ settings: this.settings,
5437
+ registry: this.#modelRegistry,
5438
+ model,
5439
+ sessionId: this.sessionId,
5440
+ signal: controller.signal,
5441
+ metadataResolver: provider => this.agent.metadataForProvider(provider),
5442
+ });
5443
+ } catch (error) {
5444
+ logger.debug("auto-thinking: classification failed; using fallback level", {
5445
+ error: error instanceof Error ? error.message : String(error),
5446
+ });
5447
+ } finally {
5448
+ clearTimeout(timer);
5449
+ }
5450
+ }
5451
+
5452
+ // Drop the result if the turn was aborted/superseded while classifying.
5453
+ if (this.#promptGeneration !== generation || !this.#autoThinking) return;
5454
+
5455
+ const effort = resolved ?? resolveProvisionalAutoLevel(model);
5456
+ if (effort === undefined) return;
5457
+ this.#autoResolvedLevel = effort;
5458
+ this.#thinkingLevel = effort;
5459
+ this.agent.setThinkingLevel(toReasoningEffort(effort));
5460
+ this.#emit({
5461
+ type: "thinking_level_changed",
5462
+ thinkingLevel: effort,
5463
+ configured: AUTO_THINKING,
5464
+ resolved: effort,
5465
+ });
5466
+ }
5467
+
5322
5468
  /**
5323
5469
  * True when *any* fast-mode-granting service tier is configured, regardless
5324
5470
  * of whether the active model's provider actually realizes it. Used by the
@@ -7260,7 +7406,9 @@ export class AgentSession {
7260
7406
  throw new Error(`No API key for retry fallback ${selector.raw}`);
7261
7407
  }
7262
7408
 
7263
- const currentThinkingLevel = this.thinkingLevel;
7409
+ // Capture the configured selector (auto-aware) so a fallback chain preserves
7410
+ // `auto` instead of collapsing it to the level it resolved to this turn.
7411
+ const currentThinkingLevel = this.configuredThinkingLevel();
7264
7412
  const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
7265
7413
 
7266
7414
  this.#setModelWithProviderSessionReset(candidate);
@@ -7333,7 +7481,7 @@ export class AgentSession {
7333
7481
  const apiKey = await this.#modelRegistry.getApiKey(primaryModel, this.sessionId);
7334
7482
  if (!apiKey) return;
7335
7483
 
7336
- const currentThinkingLevel = this.thinkingLevel;
7484
+ const currentThinkingLevel = this.configuredThinkingLevel();
7337
7485
  const thinkingToApply =
7338
7486
  currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
7339
7487
  this.#setModelWithProviderSessionReset(primaryModel);
@@ -8244,6 +8392,8 @@ export class AgentSession {
8244
8392
  const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
8245
8393
  const previousModel = this.model;
8246
8394
  const previousThinkingLevel = this.#thinkingLevel;
8395
+ const previousAutoThinking = this.#autoThinking;
8396
+ const previousAutoResolvedLevel = this.#autoResolvedLevel;
8247
8397
  const previousServiceTier = this.agent.serviceTier;
8248
8398
  const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
8249
8399
  const previousTools = [...this.agent.state.tools];
@@ -8321,12 +8471,21 @@ export class AgentSession {
8321
8471
  .some(entry => entry.type === "service_tier_change");
8322
8472
  const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
8323
8473
  const configuredServiceTier = this.settings.get("serviceTier");
8324
- const nextThinkingLevel = resolveThinkingLevelForModel(
8325
- this.model,
8326
- hasThinkingEntry ? (sessionContext.thinkingLevel as ThinkingLevel | undefined) : defaultThinkingLevel,
8327
- );
8328
- this.#thinkingLevel = nextThinkingLevel;
8329
- this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
8474
+ // Session log entries only ever store concrete levels (auto is never
8475
+ // written), so `auto` can only arrive via the settings default.
8476
+ const restoredThinkingLevel: ConfiguredThinkingLevel | undefined = hasThinkingEntry
8477
+ ? (sessionContext.thinkingLevel as ThinkingLevel | undefined)
8478
+ : defaultThinkingLevel;
8479
+ if (restoredThinkingLevel === AUTO_THINKING) {
8480
+ this.#autoThinking = true;
8481
+ this.#autoResolvedLevel = undefined;
8482
+ this.#thinkingLevel = resolveProvisionalAutoLevel(this.model);
8483
+ } else {
8484
+ this.#autoThinking = false;
8485
+ this.#autoResolvedLevel = undefined;
8486
+ this.#thinkingLevel = resolveThinkingLevelForModel(this.model, restoredThinkingLevel);
8487
+ }
8488
+ this.agent.setThinkingLevel(toReasoningEffort(this.#thinkingLevel));
8330
8489
  this.agent.serviceTier = hasServiceTierEntry
8331
8490
  ? sessionContext.serviceTier
8332
8491
  : configuredServiceTier === "none"
@@ -8375,6 +8534,8 @@ export class AgentSession {
8375
8534
  this.#syncToolCallBatchCap(undefined);
8376
8535
  }
8377
8536
  this.#thinkingLevel = previousThinkingLevel;
8537
+ this.#autoThinking = previousAutoThinking;
8538
+ this.#autoResolvedLevel = previousAutoResolvedLevel;
8378
8539
  this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
8379
8540
  this.agent.serviceTier = previousServiceTier;
8380
8541
  this.#syncTodoPhasesFromBranch();
package/src/thinking.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import { clampThinkingLevelForModel, type Effort, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
+ import { clampThinkingLevelForModel, Effort, getSupportedEfforts, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
3
3
 
4
4
  /**
5
5
  * Metadata used to render thinking selector values in the coding-agent UI.
@@ -85,3 +85,75 @@ export function resolveThinkingLevelForModel(
85
85
  }
86
86
  return clampThinkingLevelForModel(model, level);
87
87
  }
88
+
89
+ /**
90
+ * Sentinel selector for the coding-agent "auto" thinking mode. Kept entirely
91
+ * inside the coding-agent layer: it is never an {@link Effort} or
92
+ * {@link ThinkingLevel}, so provider mapping/clamping keeps seeing concrete
93
+ * efforts. The session resolves `auto` to a concrete effort each turn.
94
+ */
95
+ export const AUTO_THINKING = "auto" as const;
96
+
97
+ /** A thinking selector as configured by the user — a concrete level or `auto`. */
98
+ export type ConfiguredThinkingLevel = ThinkingLevel | typeof AUTO_THINKING;
99
+
100
+ /** Metadata used to render the `auto` selector value alongside concrete levels. */
101
+ export interface ConfiguredThinkingLevelMetadata {
102
+ value: ConfiguredThinkingLevel;
103
+ label: string;
104
+ description: string;
105
+ }
106
+
107
+ const AUTO_THINKING_METADATA: ConfiguredThinkingLevelMetadata = {
108
+ value: AUTO_THINKING,
109
+ label: "auto",
110
+ description: "Auto-detect per prompt (low–xhigh)",
111
+ };
112
+
113
+ /**
114
+ * Parses a configured thinking selector, accepting `auto` in addition to every
115
+ * value {@link parseThinkingLevel} accepts. {@link parseThinkingLevel} itself
116
+ * stays strict so model-suffix parsing (`model:high`) keeps rejecting `auto`.
117
+ */
118
+ export function parseConfiguredThinkingLevel(value: string | null | undefined): ConfiguredThinkingLevel | undefined {
119
+ if (value === AUTO_THINKING) return AUTO_THINKING;
120
+ return parseThinkingLevel(value);
121
+ }
122
+
123
+ /** Returns display metadata for a configured selector, including `auto`. */
124
+ export function getConfiguredThinkingLevelMetadata(level: ConfiguredThinkingLevel): ConfiguredThinkingLevelMetadata {
125
+ return level === AUTO_THINKING ? AUTO_THINKING_METADATA : getThinkingLevelMetadata(level);
126
+ }
127
+
128
+ /**
129
+ * Resolves an auto-classified effort against the active model's supported
130
+ * range. Unlike {@link clampThinkingLevelForModel}, `auto` never resolves below
131
+ * {@link Effort.Low}: the eligible pool is the model's supported efforts at or
132
+ * above Low (falling back to the full supported set only when the model maxes
133
+ * out below Low). Within that pool the request snaps to the highest level not
134
+ * exceeding it, or the pool minimum when the request is below the pool.
135
+ */
136
+ export function clampAutoThinkingEffort(model: Model | undefined, effort: Effort): Effort {
137
+ const supported = model ? getSupportedEfforts(model) : THINKING_EFFORTS;
138
+ if (supported.length === 0) return effort;
139
+ const lowIndex = THINKING_EFFORTS.indexOf(Effort.Low);
140
+ const eligible = supported.filter(level => THINKING_EFFORTS.indexOf(level) >= lowIndex);
141
+ const pool = eligible.length > 0 ? eligible : supported;
142
+ const requestedIndex = THINKING_EFFORTS.indexOf(effort);
143
+ let chosen = pool[0];
144
+ for (const candidate of pool) {
145
+ if (THINKING_EFFORTS.indexOf(candidate) > requestedIndex) break;
146
+ chosen = candidate;
147
+ }
148
+ return chosen;
149
+ }
150
+
151
+ /**
152
+ * The provisional concrete level shown while `auto` is configured but before a
153
+ * turn has been classified. Prefers the model's `defaultLevel`, otherwise High,
154
+ * clamped into the auto range. Returns `undefined` for non-reasoning models.
155
+ */
156
+ export function resolveProvisionalAutoLevel(model: Model | undefined): Effort | undefined {
157
+ if (!model?.reasoning) return undefined;
158
+ return clampAutoThinkingEffort(model, model.thinking?.defaultLevel ?? Effort.High);
159
+ }
@@ -27,12 +27,6 @@ const DEVICE_VALUES: Record<TinyModelDevice, true> = {
27
27
  "webnn-cpu": true,
28
28
  };
29
29
 
30
- function defaultTinyModelDevice(): TinyModelDevice {
31
- if (process.platform === "win32") return "dml";
32
- if (process.platform === "linux" && process.arch === "x64") return "cuda";
33
- return CPU_DEVICE;
34
- }
35
-
36
30
  function usesDarwinWorkerWebGpu(device: TinyModelDevice): boolean {
37
31
  return process.platform === "darwin" && (device === "gpu" || device === "webgpu" || device === "auto");
38
32
  }
@@ -51,7 +45,7 @@ export function resolveTinyModelDevicePreference(
51
45
  value: string | undefined = $env.PI_TINY_DEVICE,
52
46
  ): TinyModelDevicePreference {
53
47
  return {
54
- device: normalizeTinyModelDevice(value) ?? defaultTinyModelDevice(),
48
+ device: normalizeTinyModelDevice(value) ?? CPU_DEVICE,
55
49
  raw: value,
56
50
  };
57
51
  }
@@ -62,7 +56,7 @@ export function tinyModelDeviceLoadOrder(preference: TinyModelDevicePreference):
62
56
  return [preference.device, CPU_DEVICE];
63
57
  }
64
58
 
65
- /** Sentinel `providers.tinyModelDevice` value meaning "use the built-in platform default". */
59
+ /** Sentinel `providers.tinyModelDevice` value meaning "use the built-in CPU default". */
66
60
  export const TINY_MODEL_DEVICE_DEFAULT = "default";
67
61
 
68
62
  /** Accepted values for the `providers.tinyModelDevice` setting (validation + UI). */
@@ -85,7 +79,7 @@ export const TINY_MODEL_DEVICE_SETTING_VALUES = [
85
79
 
86
80
  /** Submenu metadata for the `providers.tinyModelDevice` setting. */
87
81
  export const TINY_MODEL_DEVICE_SETTING_OPTIONS = [
88
- { value: "default", label: "Default", description: "DirectML on Windows, CUDA on Linux x64, CPU elsewhere" },
82
+ { value: "default", label: "Default", description: "CPU-only inference" },
89
83
  { value: "gpu", label: "GPU", description: "Accelerated provider (WebGPU/Metal, CUDA, or DirectML)" },
90
84
  { value: "cpu", label: "CPU", description: "CPU-only inference" },
91
85
  { value: "metal", label: "Metal", description: "WebGPU alias for Apple GPUs" },
@@ -108,7 +102,7 @@ export const TINY_MODEL_DEVICE_SETTING_OPTIONS = [
108
102
  /**
109
103
  * Map a `providers.tinyModelDevice` setting value onto a `PI_TINY_DEVICE` env
110
104
  * value for the worker. Returns `undefined` for the default sentinel so the
111
- * worker keeps its built-in platform default; the worker still validates the
105
+ * worker keeps its built-in CPU default; the worker still validates the
112
106
  * forwarded value via {@link normalizeTinyModelDevice}.
113
107
  */
114
108
  export function tinyModelDeviceSettingToEnv(value: string | undefined): string | undefined {
@@ -216,3 +216,27 @@ export const TINY_LOCAL_MODELS = [
216
216
  ...TINY_TITLE_LOCAL_MODELS,
217
217
  ...TINY_MEMORY_LOCAL_MODELS,
218
218
  ] as const satisfies readonly TinyTitleLocalModelSpec[];
219
+
220
+ /**
221
+ * Difficulty-classifier model for the `auto` thinking level. Defaults to the
222
+ * online smol path; the local options reuse the memory-model registry because
223
+ * the shared worker's `complete()` only accepts memory local keys, and the
224
+ * 1B+ memory models classify coding difficulty far more reliably than the
225
+ * sub-1B title models.
226
+ */
227
+ export const ONLINE_AUTO_THINKING_MODEL_KEY = ONLINE_MEMORY_MODEL_KEY;
228
+ export const AUTO_THINKING_MODEL_VALUES = TINY_MEMORY_MODEL_VALUES;
229
+ export type AutoThinkingModelKey = TinyMemoryModelKey;
230
+
231
+ export const AUTO_THINKING_MODEL_OPTIONS = [
232
+ {
233
+ value: ONLINE_AUTO_THINKING_MODEL_KEY,
234
+ label: "Online (smol)",
235
+ description: "Classify prompt difficulty with the online smol model; no local download or on-device inference.",
236
+ },
237
+ ...TINY_MEMORY_LOCAL_MODELS.map(model => ({
238
+ value: model.key,
239
+ label: model.label,
240
+ description: model.description,
241
+ })),
242
+ ] satisfies ReadonlyArray<{ value: AutoThinkingModelKey; label: string; description: string }>;