@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.7.2] - 2026-05-31
6
+ ### Added
7
+
8
+ - Added `providers.autoThinkingModel` setting so users can choose the `auto` thinking classifier backend (online smol or local tiny-memory model)
9
+ - Added an `auto` thinking level that classifies each real user turn and resolves to a concrete low-through-xhigh effort, with online smol classification by default and an opt-in local on-device classifier.
10
+
11
+ ### Changed
12
+
13
+ - Updated the interactive thinking selectors in model/model-role pickers and ACP thinking options to include `auto` as a selectable level
14
+ - Updated footer and status-line rendering to show `auto` while auto-thinking is being resolved and `auto → <level>` once it resolves
15
+ - Changed the local tiny-model device default to CPU on every platform; explicit `providers.tinyModelDevice` / `PI_TINY_DEVICE` values still opt into accelerated ONNX providers.
16
+
17
+ ### Fixed
18
+
19
+ - Prevented auto-thinking classification from running on non-user synthetic turns and non-reasoning models, keeping the session on its provisional concrete effort
20
+ - Added a bounded auto-thinking classification path that falls back to the provisional effort on failures/timeouts so prompts continue without interruption
21
+ - Bypassed auto classifier for `ultrathink` prompts and resolved directly to the highest supported auto effort
22
+ - Fixed the JavaScript `eval` kernel crashing the whole process with a segfault (`SIGTRAP`, `getImportedModule` on a null record) when imported code reached a local module whose relative-import graph contains a cycle — e.g. `await import("…/edit/streaming.ts")`, or any workspace path with cyclic re-exports. The `LocalModuleLoader` linked and evaluated each local module individually inside the recursive `vm.SourceTextModule` linker callback, which re-entered Bun's `node:vm` module linker mid-instantiation and detonated JSC on the first cycle. The loader now constructs the entire local module graph first and drives a single `link()` + `evaluate()` from the graph root, so cyclic graphs instantiate in one pass; external (`node_modules`) modules stay eagerly loaded since they carry no imports and cannot form a cycle.
23
+ - Fixed the streaming `edit` preview rendering a blank box for hashline edits whose payload sits on the trailing in-flight line (the common single-op `replace`/`insert` case). The preview path trimmed that still-typing line before diffing, so a single-payload op collapsed to a "No changes" result — shown as an empty box — for almost the entire stream. Hashline previews now feed the raw in-flight text through `applyPartialTo`, whose streaming-tolerant parser drops a payload-less trailing op and projects a partially-typed payload line as it grows, so the diff appears and fills in live. Transient errors from the actively-typed trailing section are also suppressed while streaming (regardless of section count) so a mid-typed op can't wipe an already-good preview frame; real errors still surface once args are complete.
24
+
5
25
  ## [15.7.0] - 2026-05-31
6
26
 
7
27
  ### Added
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Per-prompt difficulty classifier for the `auto` thinking level.
3
+ *
4
+ * Picks a coding-difficulty bucket for a user prompt and maps it to a concrete
5
+ * {@link Effort}, clamped into the active model's supported range (never below
6
+ * {@link Effort.Low}). Two backends, selected by `providers.autoThinkingModel`:
7
+ *
8
+ * - `online` (default): a smol model classifies into `low|medium|high|xhigh`.
9
+ * - a local key: an on-device memory model classifies into the coarser
10
+ * `trivial|moderate|hard` scheme (3-class is more reliable than 4-way ordinal
11
+ * on sub-2B models), mapped to `low|high|xhigh`.
12
+ *
13
+ * Throws on any failure (no model, no key, unparseable output, abort/timeout);
14
+ * the caller falls back to a concrete level and continues the turn.
15
+ */
16
+ import { Effort, type Model } from "@oh-my-pi/pi-ai";
17
+ import type { ModelRegistry } from "../config/model-registry";
18
+ import type { Settings } from "../config/settings";
19
+ export interface ClassifyDifficultyDeps {
20
+ settings: Settings;
21
+ registry: ModelRegistry;
22
+ model: Model;
23
+ sessionId?: string;
24
+ signal?: AbortSignal;
25
+ metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
26
+ }
27
+ /**
28
+ * Classify `promptText` and return a concrete effort clamped to `deps.model`.
29
+ * @throws when the backend cannot produce a usable classification.
30
+ */
31
+ export declare function classifyDifficulty(promptText: string, deps: ClassifyDifficultyDeps): Promise<Effort>;
32
+ /** Map the online 4-way level keyword to an {@link Effort}; earliest match wins. */
33
+ export declare function parseDifficultyLevel(text: string): Effort | undefined;
34
+ /** Map the local 3-way bucket keyword to an {@link Effort}; earliest match wins. */
35
+ export declare function parseDifficultyBucket(text: string): Effort | undefined;
@@ -704,13 +704,13 @@ export declare const SETTINGS_SCHEMA: {
704
704
  };
705
705
  readonly defaultThinkingLevel: {
706
706
  readonly type: "enum";
707
- readonly values: readonly import("@oh-my-pi/pi-ai").Effort[];
707
+ readonly values: readonly [...import("@oh-my-pi/pi-ai").Effort[], "auto"];
708
708
  readonly default: "high";
709
709
  readonly ui: {
710
710
  readonly tab: "model";
711
711
  readonly label: "Thinking Level";
712
712
  readonly description: "Reasoning depth for thinking-capable models";
713
- readonly options: readonly import("../thinking").ThinkingLevelMetadata[];
713
+ readonly options: readonly [import("../thinking").ConfiguredThinkingLevelMetadata, ...import("../thinking").ThinkingLevelMetadata[]];
714
714
  };
715
715
  };
716
716
  readonly hideThinkingBlock: {
@@ -3385,11 +3385,11 @@ export declare const SETTINGS_SCHEMA: {
3385
3385
  readonly ui: {
3386
3386
  readonly tab: "providers";
3387
3387
  readonly label: "Tiny Model Device";
3388
- readonly description: "ONNX execution provider for local tiny models (titles + memory). Default picks DirectML on Windows, CUDA on Linux x64, CPU elsewhere. The PI_TINY_DEVICE env var overrides this.";
3388
+ readonly description: "ONNX execution provider for local tiny models (titles + memory). Default uses CPU-only inference. The PI_TINY_DEVICE env var overrides this.";
3389
3389
  readonly options: readonly [{
3390
3390
  readonly value: "default";
3391
3391
  readonly label: "Default";
3392
- readonly description: "DirectML on Windows, CUDA on Linux x64, CPU elsewhere";
3392
+ readonly description: "CPU-only inference";
3393
3393
  }, {
3394
3394
  readonly value: "gpu";
3395
3395
  readonly label: "GPU";
@@ -3532,6 +3532,26 @@ export declare const SETTINGS_SCHEMA: {
3532
3532
  })[];
3533
3533
  };
3534
3534
  };
3535
+ readonly "providers.autoThinkingModel": {
3536
+ readonly type: "enum";
3537
+ readonly values: readonly ["online", "qwen3-1.7b", "gemma-3-1b", "qwen2.5-1.5b", "lfm2-1.2b"];
3538
+ readonly default: "online";
3539
+ readonly ui: {
3540
+ readonly tab: "model";
3541
+ readonly label: "Auto Thinking Model";
3542
+ readonly description: "Difficulty classifier for the `auto` thinking level: online smol by default, or a local on-device model";
3543
+ readonly condition: "autoThinkingActive";
3544
+ readonly options: ({
3545
+ value: "online";
3546
+ label: string;
3547
+ description: string;
3548
+ } | {
3549
+ value: "gemma-3-1b" | "lfm2-1.2b" | "qwen2.5-1.5b" | "qwen3-1.7b";
3550
+ label: "Gemma 3 1B" | "LFM2 1.2B" | "Qwen2.5 1.5B" | "Qwen3 1.7B";
3551
+ description: "Best consolidation/dedup; lighter footprint, but leaks small talk during extraction." | "Best extraction granularity (atomic facts); weaker consolidation." | "Fastest load; solid all-rounder, slightly noisier extraction labels." | "Recommended; most disciplined extraction (ignores chit-chat), good consolidation, about 1.1 GB cached.";
3552
+ })[];
3553
+ };
3554
+ };
3535
3555
  readonly "providers.kimiApiFormat": {
3536
3556
  readonly type: "enum";
3537
3557
  readonly values: readonly ["openai", "anthropic"];
@@ -17,6 +17,12 @@ export interface HashlineDiffOptions {
17
17
  * preview path only.
18
18
  */
19
19
  streaming?: boolean;
20
+ /**
21
+ * Skip snapshot-tag validation. Streaming previews use this so transient
22
+ * stale/missing tags do not flash re-read errors while the model is still
23
+ * authoring input; the final apply path still validates through Patcher.
24
+ */
25
+ skipHashValidation?: boolean;
20
26
  }
21
27
  export declare function computeHashlineSectionDiff(section: PatchSection, cwd: string, snapshots: SnapshotStore, options?: HashlineDiffOptions): Promise<{
22
28
  diff: string;
@@ -1,12 +1,13 @@
1
- import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
1
  import { type Model } from "@oh-my-pi/pi-ai";
3
2
  import { Container, Input, type TUI } from "@oh-my-pi/pi-tui";
4
3
  import type { ModelRegistry } from "../../config/model-registry";
5
4
  import type { Settings } from "../../config/settings";
5
+ import { type ConfiguredThinkingLevel } from "../../thinking";
6
6
  interface ScopedModelItem {
7
7
  model: Model;
8
8
  thinkingLevel?: string;
9
9
  }
10
+ type RoleSelectCallback = (model: Model, role: string | null, thinkingLevel?: ConfiguredThinkingLevel, selector?: string) => void;
10
11
  /**
11
12
  * Component that renders a model selector with provider tabs and context menu.
12
13
  * - Tab/Arrow Left/Right: Switch between provider tabs
@@ -16,7 +17,7 @@ interface ScopedModelItem {
16
17
  */
17
18
  export declare class ModelSelectorComponent extends Container {
18
19
  #private;
19
- constructor(tui: TUI, _currentModel: Model | undefined, settings: Settings, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, onSelect: (model: Model, role: string | null, thinkingLevel?: ThinkingLevel, selector?: string) => void, onCancel: () => void, options?: {
20
+ constructor(tui: TUI, _currentModel: Model | undefined, settings: Settings, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, onSelect: RoleSelectCallback, onCancel: () => void, options?: {
20
21
  temporaryOnly?: boolean;
21
22
  initialSearchInput?: string;
22
23
  });
@@ -6,7 +6,7 @@ export type SymbolPreset = "unicode" | "nerd" | "ascii";
6
6
  /**
7
7
  * All available symbol keys organized by category.
8
8
  */
9
- export type SymbolKey = "status.success" | "status.error" | "status.warning" | "status.info" | "status.pending" | "status.disabled" | "status.enabled" | "status.running" | "status.shadowed" | "status.aborted" | "nav.cursor" | "nav.selected" | "nav.expand" | "nav.collapse" | "nav.back" | "tree.branch" | "tree.last" | "tree.vertical" | "tree.horizontal" | "tree.hook" | "boxRound.topLeft" | "boxRound.topRight" | "boxRound.bottomLeft" | "boxRound.bottomRight" | "boxRound.horizontal" | "boxRound.vertical" | "boxSharp.topLeft" | "boxSharp.topRight" | "boxSharp.bottomLeft" | "boxSharp.bottomRight" | "boxSharp.horizontal" | "boxSharp.vertical" | "boxSharp.cross" | "boxSharp.teeDown" | "boxSharp.teeUp" | "boxSharp.teeRight" | "boxSharp.teeLeft" | "sep.powerline" | "sep.powerlineThin" | "sep.powerlineLeft" | "sep.powerlineRight" | "sep.powerlineThinLeft" | "sep.powerlineThinRight" | "sep.block" | "sep.space" | "sep.asciiLeft" | "sep.asciiRight" | "sep.dot" | "sep.slash" | "sep.pipe" | "icon.model" | "icon.plan" | "icon.goal" | "icon.pause" | "icon.loop" | "icon.folder" | "icon.scratchFolder" | "icon.file" | "icon.git" | "icon.branch" | "icon.pr" | "icon.tokens" | "icon.context" | "icon.cost" | "icon.time" | "icon.pi" | "icon.agents" | "icon.cache" | "icon.input" | "icon.output" | "icon.host" | "icon.session" | "icon.package" | "icon.warning" | "icon.rewind" | "icon.auto" | "icon.fast" | "icon.extensionSkill" | "icon.extensionTool" | "icon.extensionSlashCommand" | "icon.extensionMcp" | "icon.extensionRule" | "icon.extensionHook" | "icon.extensionPrompt" | "icon.extensionContextFile" | "icon.extensionInstruction" | "icon.mic" | "thinking.minimal" | "thinking.low" | "thinking.medium" | "thinking.high" | "thinking.xhigh" | "checkbox.checked" | "checkbox.unchecked" | "format.bullet" | "format.dash" | "format.bracketLeft" | "format.bracketRight" | "md.quoteBorder" | "md.hrChar" | "md.bullet" | "md.colorSwatch" | "lang.default" | "lang.typescript" | "lang.javascript" | "lang.python" | "lang.rust" | "lang.go" | "lang.java" | "lang.c" | "lang.cpp" | "lang.csharp" | "lang.ruby" | "lang.php" | "lang.swift" | "lang.kotlin" | "lang.shell" | "lang.html" | "lang.css" | "lang.json" | "lang.yaml" | "lang.markdown" | "lang.sql" | "lang.docker" | "lang.lua" | "lang.text" | "lang.env" | "lang.toml" | "lang.xml" | "lang.ini" | "lang.conf" | "lang.log" | "lang.csv" | "lang.tsv" | "lang.image" | "lang.pdf" | "lang.archive" | "lang.binary" | "tab.appearance" | "tab.model" | "tab.interaction" | "tab.context" | "tab.editing" | "tab.tools" | "tab.memory" | "tab.tasks" | "tab.providers";
9
+ export type SymbolKey = "status.success" | "status.error" | "status.warning" | "status.info" | "status.pending" | "status.disabled" | "status.enabled" | "status.running" | "status.shadowed" | "status.aborted" | "nav.cursor" | "nav.selected" | "nav.expand" | "nav.collapse" | "nav.back" | "tree.branch" | "tree.last" | "tree.vertical" | "tree.horizontal" | "tree.hook" | "boxRound.topLeft" | "boxRound.topRight" | "boxRound.bottomLeft" | "boxRound.bottomRight" | "boxRound.horizontal" | "boxRound.vertical" | "boxSharp.topLeft" | "boxSharp.topRight" | "boxSharp.bottomLeft" | "boxSharp.bottomRight" | "boxSharp.horizontal" | "boxSharp.vertical" | "boxSharp.cross" | "boxSharp.teeDown" | "boxSharp.teeUp" | "boxSharp.teeRight" | "boxSharp.teeLeft" | "sep.powerline" | "sep.powerlineThin" | "sep.powerlineLeft" | "sep.powerlineRight" | "sep.powerlineThinLeft" | "sep.powerlineThinRight" | "sep.block" | "sep.space" | "sep.asciiLeft" | "sep.asciiRight" | "sep.dot" | "sep.slash" | "sep.pipe" | "icon.model" | "icon.plan" | "icon.goal" | "icon.pause" | "icon.loop" | "icon.folder" | "icon.scratchFolder" | "icon.file" | "icon.git" | "icon.branch" | "icon.pr" | "icon.tokens" | "icon.context" | "icon.cost" | "icon.time" | "icon.pi" | "icon.agents" | "icon.cache" | "icon.input" | "icon.output" | "icon.host" | "icon.session" | "icon.package" | "icon.warning" | "icon.rewind" | "icon.auto" | "icon.fast" | "icon.extensionSkill" | "icon.extensionTool" | "icon.extensionSlashCommand" | "icon.extensionMcp" | "icon.extensionRule" | "icon.extensionHook" | "icon.extensionPrompt" | "icon.extensionContextFile" | "icon.extensionInstruction" | "icon.mic" | "thinking.minimal" | "thinking.low" | "thinking.medium" | "thinking.high" | "thinking.xhigh" | "thinking.autoPending" | "checkbox.checked" | "checkbox.unchecked" | "format.bullet" | "format.dash" | "format.bracketLeft" | "format.bracketRight" | "md.quoteBorder" | "md.hrChar" | "md.bullet" | "md.colorSwatch" | "lang.default" | "lang.typescript" | "lang.javascript" | "lang.python" | "lang.rust" | "lang.go" | "lang.java" | "lang.c" | "lang.cpp" | "lang.csharp" | "lang.ruby" | "lang.php" | "lang.swift" | "lang.kotlin" | "lang.shell" | "lang.html" | "lang.css" | "lang.json" | "lang.yaml" | "lang.markdown" | "lang.sql" | "lang.docker" | "lang.lua" | "lang.text" | "lang.env" | "lang.toml" | "lang.xml" | "lang.ini" | "lang.conf" | "lang.log" | "lang.csv" | "lang.tsv" | "lang.image" | "lang.pdf" | "lang.archive" | "lang.binary" | "tab.appearance" | "tab.model" | "tab.interaction" | "tab.context" | "tab.editing" | "tab.tools" | "tab.memory" | "tab.tasks" | "tab.providers";
10
10
  export type SpinnerType = "status" | "activity";
11
11
  export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" | "mdLink" | "mdLinkUrl" | "mdCode" | "mdCodeBlock" | "mdCodeBlockBorder" | "mdQuote" | "mdQuoteBorder" | "mdHr" | "mdListBullet" | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext" | "syntaxComment" | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator" | "syntaxPunctuation" | "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "bashMode" | "pythonMode" | "statusLineSep" | "statusLineModel" | "statusLinePath" | "statusLineGitClean" | "statusLineGitDirty" | "statusLineContext" | "statusLineSpend" | "statusLineStaged" | "statusLineDirty" | "statusLineUntracked" | "statusLineOutput" | "statusLineCost" | "statusLineSubagents";
12
12
  /** Check if a string is a valid ThemeColor value */
@@ -161,6 +161,7 @@ export declare class Theme {
161
161
  medium: string;
162
162
  high: string;
163
163
  xhigh: string;
164
+ autoPending: string;
164
165
  };
165
166
  get checkbox(): {
166
167
  checked: string;
@@ -19,6 +19,7 @@ import { AgentSession } from "./session/agent-session";
19
19
  import { AuthStorage } from "./session/auth-storage";
20
20
  import { SessionManager } from "./session/session-manager";
21
21
  import { type BuildSystemPromptResult } from "./system-prompt";
22
+ import { type ConfiguredThinkingLevel } from "./thinking";
22
23
  import { BashTool, BUILTIN_TOOLS, createTools, EditTool, EvalTool, FindTool, HIDDEN_TOOLS, type LspStartupServerInfo, loadSshTool, ReadTool, ResolveTool, SearchTool, type Tool, type ToolSession, WebSearchTool, WriteTool } from "./tools";
23
24
  import { EventBus } from "./utils/event-bus";
24
25
  import { type WorkspaceTree } from "./workspace-tree";
@@ -39,7 +40,7 @@ export interface CreateAgentSessionOptions {
39
40
  * Used when model lookup is deferred because extension-provided models aren't registered yet. */
40
41
  modelPattern?: string;
41
42
  /** Thinking selector. Default: from settings, else unset */
42
- thinkingLevel?: ThinkingLevel;
43
+ thinkingLevel?: ConfiguredThinkingLevel;
43
44
  /** Models available for cycling (Ctrl+P in interactive mode) */
44
45
  scopedModels?: Array<{
45
46
  model: Model;
@@ -15,7 +15,8 @@
15
15
  import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
16
16
  import { type Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
17
  import { type CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
18
- import type { AssistantMessage, Effort, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ServiceTier, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai";
18
+ import type { AssistantMessage, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ServiceTier, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai";
19
+ import { Effort } from "@oh-my-pi/pi-ai";
19
20
  import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
20
21
  import type { Rule } from "../capability/rule";
21
22
  import { type ModelRegistry } from "../config/model-registry";
@@ -39,6 +40,7 @@ import { type MnemosyneSessionState } from "../mnemosyne/state";
39
40
  import type { PlanModeState } from "../plan-mode/state";
40
41
  import { type AgentRegistry } from "../registry/agent-registry";
41
42
  import { type SecretObfuscator } from "../secrets/obfuscator";
43
+ import { type ConfiguredThinkingLevel } from "../thinking";
42
44
  import { type DiscoverableTool, type DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
43
45
  import type { CheckpointState } from "../tools/checkpoint";
44
46
  import { type TodoItem, type TodoPhase } from "../tools/todo-write";
@@ -102,6 +104,10 @@ export type AgentSessionEvent = AgentEvent | {
102
104
  } | {
103
105
  type: "thinking_level_changed";
104
106
  thinkingLevel: ThinkingLevel | undefined;
107
+ /** The user-configured selector when it differs from the effective level (e.g. `auto`). */
108
+ configured?: ConfiguredThinkingLevel;
109
+ /** The level `auto` resolved to this turn, once classified. */
110
+ resolved?: Effort;
105
111
  } | {
106
112
  type: "goal_updated";
107
113
  goal: Goal | null;
@@ -125,7 +131,7 @@ export interface AgentSessionConfig {
125
131
  thinkingLevel?: ThinkingLevel;
126
132
  }>;
127
133
  /** Initial session thinking selector. */
128
- thinkingLevel?: ThinkingLevel;
134
+ thinkingLevel?: ConfiguredThinkingLevel;
129
135
  /** Prompt templates for expansion */
130
136
  promptTemplates?: PromptTemplate[];
131
137
  /** File-based slash commands for expansion */
@@ -364,8 +370,14 @@ export declare class AgentSession {
364
370
  get state(): AgentState;
365
371
  /** Current model (may be undefined if not yet selected) */
366
372
  get model(): Model | undefined;
367
- /** Current thinking level */
373
+ /** Effective thinking level applied to the agent (the resolved level when `auto`). */
368
374
  get thinkingLevel(): ThinkingLevel | undefined;
375
+ /** The selector the user configured: `auto` when auto mode is active, else the effective level. */
376
+ configuredThinkingLevel(): ConfiguredThinkingLevel | undefined;
377
+ /** True when `auto` thinking mode is active. */
378
+ get isAutoThinking(): boolean;
379
+ /** The level `auto` resolved to for the current turn (undefined until classified). */
380
+ autoResolvedThinkingLevel(): Effort | undefined;
369
381
  get serviceTier(): ServiceTier | undefined;
370
382
  /** Whether agent is currently streaming a response */
371
383
  get isStreaming(): boolean;
@@ -654,15 +666,16 @@ export declare class AgentSession {
654
666
  */
655
667
  getAvailableModels(): Model[];
656
668
  /**
657
- * Set thinking level.
658
- * Saves the effective metadata-clamped level to session and settings only if it changes.
669
+ * Set the thinking level. `auto` enables per-turn classification (session-level,
670
+ * never written to the session log); a concrete level clears auto. The effective
671
+ * metadata-clamped level is saved to the session/settings only when it changes.
659
672
  */
660
- setThinkingLevel(level: ThinkingLevel | undefined, persist?: boolean): void;
673
+ setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist?: boolean): void;
661
674
  /**
662
- * Cycle to next thinking level.
663
- * @returns New level, or undefined if model doesn't support thinking
675
+ * Cycle to next thinking level: off → auto → minimal..xhigh → off.
676
+ * @returns New selector, or undefined if model doesn't support thinking
664
677
  */
665
- cycleThinkingLevel(): ThinkingLevel | undefined;
678
+ cycleThinkingLevel(): ConfiguredThinkingLevel | undefined;
666
679
  /**
667
680
  * True when *any* fast-mode-granting service tier is configured, regardless
668
681
  * of whether the active model's provider actually realizes it. Used by the
@@ -1,5 +1,5 @@
1
1
  import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import { type Effort, type Model } from "@oh-my-pi/pi-ai";
2
+ import { Effort, type Model } from "@oh-my-pi/pi-ai";
3
3
  /**
4
4
  * Metadata used to render thinking selector values in the coding-agent UI.
5
5
  */
@@ -28,3 +28,41 @@ export declare function toReasoningEffort(level: ThinkingLevel | undefined): Eff
28
28
  * Resolves a selector against the current model while preserving explicit "off".
29
29
  */
30
30
  export declare function resolveThinkingLevelForModel(model: Model | undefined, level: ThinkingLevel | undefined): ResolvedThinkingLevel | undefined;
31
+ /**
32
+ * Sentinel selector for the coding-agent "auto" thinking mode. Kept entirely
33
+ * inside the coding-agent layer: it is never an {@link Effort} or
34
+ * {@link ThinkingLevel}, so provider mapping/clamping keeps seeing concrete
35
+ * efforts. The session resolves `auto` to a concrete effort each turn.
36
+ */
37
+ export declare const AUTO_THINKING: "auto";
38
+ /** A thinking selector as configured by the user — a concrete level or `auto`. */
39
+ export type ConfiguredThinkingLevel = ThinkingLevel | typeof AUTO_THINKING;
40
+ /** Metadata used to render the `auto` selector value alongside concrete levels. */
41
+ export interface ConfiguredThinkingLevelMetadata {
42
+ value: ConfiguredThinkingLevel;
43
+ label: string;
44
+ description: string;
45
+ }
46
+ /**
47
+ * Parses a configured thinking selector, accepting `auto` in addition to every
48
+ * value {@link parseThinkingLevel} accepts. {@link parseThinkingLevel} itself
49
+ * stays strict so model-suffix parsing (`model:high`) keeps rejecting `auto`.
50
+ */
51
+ export declare function parseConfiguredThinkingLevel(value: string | null | undefined): ConfiguredThinkingLevel | undefined;
52
+ /** Returns display metadata for a configured selector, including `auto`. */
53
+ export declare function getConfiguredThinkingLevelMetadata(level: ConfiguredThinkingLevel): ConfiguredThinkingLevelMetadata;
54
+ /**
55
+ * Resolves an auto-classified effort against the active model's supported
56
+ * range. Unlike {@link clampThinkingLevelForModel}, `auto` never resolves below
57
+ * {@link Effort.Low}: the eligible pool is the model's supported efforts at or
58
+ * above Low (falling back to the full supported set only when the model maxes
59
+ * out below Low). Within that pool the request snaps to the highest level not
60
+ * exceeding it, or the pool minimum when the request is below the pool.
61
+ */
62
+ export declare function clampAutoThinkingEffort(model: Model | undefined, effort: Effort): Effort;
63
+ /**
64
+ * The provisional concrete level shown while `auto` is configured but before a
65
+ * turn has been classified. Prefers the model's `defaultLevel`, otherwise High,
66
+ * clamped into the auto range. Returns `undefined` for non-reasoning models.
67
+ */
68
+ export declare function resolveProvisionalAutoLevel(model: Model | undefined): Effort | undefined;
@@ -7,7 +7,7 @@ export interface TinyModelDevicePreference {
7
7
  export declare function normalizeTinyModelDevice(value: string | undefined): TinyModelDevice | undefined;
8
8
  export declare function resolveTinyModelDevicePreference(value?: string | undefined): TinyModelDevicePreference;
9
9
  export declare function tinyModelDeviceLoadOrder(preference: TinyModelDevicePreference): readonly TinyModelDevice[];
10
- /** Sentinel `providers.tinyModelDevice` value meaning "use the built-in platform default". */
10
+ /** Sentinel `providers.tinyModelDevice` value meaning "use the built-in CPU default". */
11
11
  export declare const TINY_MODEL_DEVICE_DEFAULT = "default";
12
12
  /** Accepted values for the `providers.tinyModelDevice` setting (validation + UI). */
13
13
  export declare const TINY_MODEL_DEVICE_SETTING_VALUES: readonly ["default", "gpu", "cpu", "metal", "webgpu", "cuda", "dml", "coreml", "auto", "wasm", "webnn", "webnn-gpu", "webnn-cpu", "webnn-npu"];
@@ -15,7 +15,7 @@ export declare const TINY_MODEL_DEVICE_SETTING_VALUES: readonly ["default", "gpu
15
15
  export declare const TINY_MODEL_DEVICE_SETTING_OPTIONS: readonly [{
16
16
  readonly value: "default";
17
17
  readonly label: "Default";
18
- readonly description: "DirectML on Windows, CUDA on Linux x64, CPU elsewhere";
18
+ readonly description: "CPU-only inference";
19
19
  }, {
20
20
  readonly value: "gpu";
21
21
  readonly label: "GPU";
@@ -72,7 +72,7 @@ export declare const TINY_MODEL_DEVICE_SETTING_OPTIONS: readonly [{
72
72
  /**
73
73
  * Map a `providers.tinyModelDevice` setting value onto a `PI_TINY_DEVICE` env
74
74
  * value for the worker. Returns `undefined` for the default sentinel so the
75
- * worker keeps its built-in platform default; the worker still validates the
75
+ * worker keeps its built-in CPU default; the worker still validates the
76
76
  * forwarded value via {@link normalizeTinyModelDevice}.
77
77
  */
78
78
  export declare function tinyModelDeviceSettingToEnv(value: string | undefined): string | undefined;
@@ -183,3 +183,22 @@ export declare const TINY_LOCAL_MODELS: readonly [{
183
183
  readonly description: "Fastest load; solid all-rounder, slightly noisier extraction labels.";
184
184
  readonly contextNote: "Use when local startup cost is the priority.";
185
185
  }];
186
+ /**
187
+ * Difficulty-classifier model for the `auto` thinking level. Defaults to the
188
+ * online smol path; the local options reuse the memory-model registry because
189
+ * the shared worker's `complete()` only accepts memory local keys, and the
190
+ * 1B+ memory models classify coding difficulty far more reliably than the
191
+ * sub-1B title models.
192
+ */
193
+ export declare const ONLINE_AUTO_THINKING_MODEL_KEY = "online";
194
+ export declare const AUTO_THINKING_MODEL_VALUES: readonly ["online", "qwen3-1.7b", "gemma-3-1b", "qwen2.5-1.5b", "lfm2-1.2b"];
195
+ export type AutoThinkingModelKey = TinyMemoryModelKey;
196
+ export declare const AUTO_THINKING_MODEL_OPTIONS: ({
197
+ value: "online";
198
+ label: string;
199
+ description: string;
200
+ } | {
201
+ value: "gemma-3-1b" | "lfm2-1.2b" | "qwen2.5-1.5b" | "qwen3-1.7b";
202
+ label: "Gemma 3 1B" | "LFM2 1.2B" | "Qwen2.5 1.5B" | "Qwen3 1.7B";
203
+ description: "Best consolidation/dedup; lighter footprint, but leaks small talk during extraction." | "Best extraction granularity (atomic facts); weaker consolidation." | "Fastest load; solid all-rounder, slightly noisier extraction labels." | "Recommended; most disciplined extraction (ignores chit-chat), good consolidation, about 1.1 GB cached.";
204
+ })[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.7.1",
4
+ "version": "15.7.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,14 +47,14 @@
47
47
  "@agentclientprotocol/sdk": "0.22.1",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.7.1",
51
- "@oh-my-pi/omp-stats": "15.7.1",
52
- "@oh-my-pi/pi-agent-core": "15.7.1",
53
- "@oh-my-pi/pi-ai": "15.7.1",
54
- "@oh-my-pi/pi-mnemosyne": "15.7.1",
55
- "@oh-my-pi/pi-natives": "15.7.1",
56
- "@oh-my-pi/pi-tui": "15.7.1",
57
- "@oh-my-pi/pi-utils": "15.7.1",
50
+ "@oh-my-pi/hashline": "15.7.2",
51
+ "@oh-my-pi/omp-stats": "15.7.2",
52
+ "@oh-my-pi/pi-agent-core": "15.7.2",
53
+ "@oh-my-pi/pi-ai": "15.7.2",
54
+ "@oh-my-pi/pi-mnemosyne": "15.7.2",
55
+ "@oh-my-pi/pi-natives": "15.7.2",
56
+ "@oh-my-pi/pi-tui": "15.7.2",
57
+ "@oh-my-pi/pi-utils": "15.7.2",
58
58
  "@puppeteer/browsers": "^3.0.4",
59
59
  "@types/turndown": "5.0.6",
60
60
  "@xterm/headless": "^6.0.0",
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Per-prompt difficulty classifier for the `auto` thinking level.
3
+ *
4
+ * Picks a coding-difficulty bucket for a user prompt and maps it to a concrete
5
+ * {@link Effort}, clamped into the active model's supported range (never below
6
+ * {@link Effort.Low}). Two backends, selected by `providers.autoThinkingModel`:
7
+ *
8
+ * - `online` (default): a smol model classifies into `low|medium|high|xhigh`.
9
+ * - a local key: an on-device memory model classifies into the coarser
10
+ * `trivial|moderate|hard` scheme (3-class is more reliable than 4-way ordinal
11
+ * on sub-2B models), mapped to `low|high|xhigh`.
12
+ *
13
+ * Throws on any failure (no model, no key, unparseable output, abort/timeout);
14
+ * the caller falls back to a concrete level and continues the turn.
15
+ */
16
+ import { type AssistantMessage, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
17
+ import { prompt } from "@oh-my-pi/pi-utils";
18
+ import type { ModelRegistry } from "../config/model-registry";
19
+ import { resolveRoleSelection } from "../config/model-resolver";
20
+ import type { Settings } from "../config/settings";
21
+ import difficultySystemPrompt from "../prompts/system/auto-thinking-difficulty.md" with { type: "text" };
22
+ import difficultyLocalPrompt from "../prompts/system/auto-thinking-difficulty-local.md" with { type: "text" };
23
+ import { clampAutoThinkingEffort } from "../thinking";
24
+ import { isTinyMemoryLocalModelKey, ONLINE_AUTO_THINKING_MODEL_KEY } from "../tiny/models";
25
+ import { tinyModelClient } from "../tiny/title-client";
26
+
27
+ const DIFFICULTY_SYSTEM_PROMPT = prompt.render(difficultySystemPrompt);
28
+
29
+ /** Upper bound on prompt characters fed to the classifier. */
30
+ const MAX_INPUT_CHARS = 6000;
31
+ const HEAD_CHARS = 4000;
32
+ const TAIL_CHARS = 2000;
33
+ /** The answer is a single word; keep budgets tiny for non-reasoning backends. */
34
+ const ANSWER_MAX_TOKENS = 8;
35
+ /**
36
+ * Reasoning backends ignore `disableReasoning` on some providers, so reserve
37
+ * enough output room for the keyword to still land after unavoidable thinking.
38
+ */
39
+ const REASONING_SAFE_MAX_TOKENS = 1024;
40
+
41
+ export interface ClassifyDifficultyDeps {
42
+ settings: Settings;
43
+ registry: ModelRegistry;
44
+ model: Model;
45
+ sessionId?: string;
46
+ signal?: AbortSignal;
47
+ metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
48
+ }
49
+
50
+ /**
51
+ * Classify `promptText` and return a concrete effort clamped to `deps.model`.
52
+ * @throws when the backend cannot produce a usable classification.
53
+ */
54
+ export async function classifyDifficulty(promptText: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
55
+ const backend = deps.settings.get("providers.autoThinkingModel");
56
+ const input = prepareClassifierInput(promptText);
57
+ const effort =
58
+ backend === ONLINE_AUTO_THINKING_MODEL_KEY
59
+ ? await classifyOnline(input, deps)
60
+ : await classifyLocal(input, backend, deps);
61
+ return clampAutoThinkingEffort(deps.model, effort);
62
+ }
63
+
64
+ async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
65
+ const resolved = resolveRoleSelection(["smol"], deps.settings, deps.registry.getAvailable(), deps.registry);
66
+ const model = resolved?.model;
67
+ if (!model) {
68
+ throw new Error("auto-thinking: no smol model available for classification");
69
+ }
70
+ const apiKey = await deps.registry.getApiKey(model, deps.sessionId);
71
+ if (!apiKey) {
72
+ throw new Error(`auto-thinking: no API key for ${model.provider}/${model.id}`);
73
+ }
74
+ // Resolve metadata after getApiKey so the session-sticky credential is recorded first.
75
+ const metadata = deps.metadataResolver?.(model.provider);
76
+ const maxTokens = model.reasoning ? Math.max(ANSWER_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS) : ANSWER_MAX_TOKENS;
77
+
78
+ const response = await completeSimple(
79
+ model,
80
+ {
81
+ systemPrompt: [DIFFICULTY_SYSTEM_PROMPT],
82
+ messages: [{ role: "user", content: input, timestamp: Date.now() }],
83
+ },
84
+ {
85
+ apiKey,
86
+ maxTokens,
87
+ disableReasoning: true,
88
+ metadata,
89
+ signal: deps.signal,
90
+ },
91
+ );
92
+
93
+ if (response.stopReason === "error") {
94
+ throw new Error(`auto-thinking: online classification failed: ${response.errorMessage ?? "unknown error"}`);
95
+ }
96
+
97
+ const text = extractText(response.content);
98
+ const effort = parseDifficultyLevel(text);
99
+ if (!effort) {
100
+ throw new Error(`auto-thinking: unparseable online classification: ${JSON.stringify(text)}`);
101
+ }
102
+ return effort;
103
+ }
104
+
105
+ async function classifyLocal(input: string, modelKey: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
106
+ if (!isTinyMemoryLocalModelKey(modelKey)) {
107
+ throw new Error(`auto-thinking: unsupported local classifier model: ${modelKey}`);
108
+ }
109
+ const builtPrompt = prompt.render(difficultyLocalPrompt, { prompt: input });
110
+ const text = await tinyModelClient.complete(modelKey, builtPrompt, {
111
+ maxTokens: ANSWER_MAX_TOKENS,
112
+ signal: deps.signal,
113
+ });
114
+ if (!text) {
115
+ throw new Error("auto-thinking: local classification returned no output");
116
+ }
117
+ const effort = parseDifficultyBucket(text);
118
+ if (!effort) {
119
+ throw new Error(`auto-thinking: unparseable local classification: ${JSON.stringify(text)}`);
120
+ }
121
+ return effort;
122
+ }
123
+
124
+ /** Map the online 4-way level keyword to an {@link Effort}; earliest match wins. */
125
+ export function parseDifficultyLevel(text: string): Effort | undefined {
126
+ const lower = text.toLowerCase();
127
+ const candidates: Array<[number, Effort]> = [];
128
+ // `xhigh` must be probed as its own token: `\bhigh\b` cannot match the "high"
129
+ // inside "xhigh" (no word boundary between `x` and `h`), so the two never collide.
130
+ const xhigh = lower.search(/x[\s_-]?high/);
131
+ if (xhigh >= 0) candidates.push([xhigh, Effort.XHigh]);
132
+ const high = lower.search(/\bhigh\b/);
133
+ if (high >= 0) candidates.push([high, Effort.High]);
134
+ const medium = lower.search(/\bmed(?:ium)?\b/);
135
+ if (medium >= 0) candidates.push([medium, Effort.Medium]);
136
+ const low = lower.search(/\blow\b/);
137
+ if (low >= 0) candidates.push([low, Effort.Low]);
138
+ return earliest(candidates);
139
+ }
140
+
141
+ /** Map the local 3-way bucket keyword to an {@link Effort}; earliest match wins. */
142
+ export function parseDifficultyBucket(text: string): Effort | undefined {
143
+ const lower = text.toLowerCase();
144
+ const candidates: Array<[number, Effort]> = [];
145
+ const trivial = lower.search(/\btrivial\b/);
146
+ if (trivial >= 0) candidates.push([trivial, Effort.Low]);
147
+ const moderate = lower.search(/\bmoderate\b/);
148
+ if (moderate >= 0) candidates.push([moderate, Effort.High]);
149
+ const hard = lower.search(/\bhard\b/);
150
+ if (hard >= 0) candidates.push([hard, Effort.XHigh]);
151
+ return earliest(candidates);
152
+ }
153
+
154
+ function earliest(candidates: Array<[number, Effort]>): Effort | undefined {
155
+ if (candidates.length === 0) return undefined;
156
+ let best = candidates[0];
157
+ for (const candidate of candidates) {
158
+ if (candidate[0] < best[0]) best = candidate;
159
+ }
160
+ return best[1];
161
+ }
162
+
163
+ function extractText(content: AssistantMessage["content"]): string {
164
+ return content
165
+ .filter((block): block is Extract<AssistantMessage["content"][number], { type: "text" }> => block.type === "text")
166
+ .map(block => block.text)
167
+ .join(" ")
168
+ .trim();
169
+ }
170
+
171
+ /**
172
+ * Bound the classifier input. Code blocks are kept (a large diff is signal), but
173
+ * very long prompts are head+tail trimmed so the intent (start) and any trailing
174
+ * error/stacktrace (end) both survive.
175
+ */
176
+ function prepareClassifierInput(text: string): string {
177
+ const trimmed = text.trim();
178
+ if (trimmed.length <= MAX_INPUT_CHARS) return trimmed;
179
+ return `${trimmed.slice(0, HEAD_CHARS)}\n…\n${trimmed.slice(-TAIL_CHARS)}`;
180
+ }