@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.
- package/CHANGELOG.md +20 -0
- package/dist/types/auto-thinking/classifier.d.ts +35 -0
- package/dist/types/config/settings-schema.d.ts +24 -4
- package/dist/types/edit/hashline/diff.d.ts +6 -0
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/sdk.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +22 -9
- package/dist/types/thinking.d.ts +39 -1
- package/dist/types/tiny/device.d.ts +3 -3
- package/dist/types/tiny/models.d.ts +19 -0
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +180 -0
- package/src/config/settings-schema.ts +24 -4
- package/src/edit/hashline/diff.ts +10 -2
- package/src/edit/streaming.ts +17 -6
- package/src/eval/__tests__/shared-executors.test.ts +32 -0
- package/src/eval/js/shared/local-module-loader.ts +75 -10
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +13 -3
- package/src/modes/components/footer.ts +10 -3
- package/src/modes/components/model-selector.ts +20 -11
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/settings-selector.ts +4 -1
- package/src/modes/components/status-line/segments.ts +13 -5
- package/src/modes/controllers/event-controller.ts +5 -1
- package/src/modes/controllers/selector-controller.ts +20 -6
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
- package/src/prompts/system/auto-thinking-difficulty.md +12 -0
- package/src/sdk.ts +25 -7
- package/src/session/agent-session.ts +193 -32
- package/src/thinking.ts +73 -1
- package/src/tiny/device.ts +4 -10
- 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
|
|
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: "
|
|
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:
|
|
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;
|
package/dist/types/sdk.d.ts
CHANGED
|
@@ -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?:
|
|
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,
|
|
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?:
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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:
|
|
673
|
+
setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist?: boolean): void;
|
|
661
674
|
/**
|
|
662
|
-
* Cycle to next thinking level.
|
|
663
|
-
* @returns New
|
|
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():
|
|
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
|
package/dist/types/thinking.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import {
|
|
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
|
|
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: "
|
|
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
|
|
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.
|
|
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.
|
|
51
|
-
"@oh-my-pi/omp-stats": "15.7.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "15.7.
|
|
53
|
-
"@oh-my-pi/pi-ai": "15.7.
|
|
54
|
-
"@oh-my-pi/pi-mnemosyne": "15.7.
|
|
55
|
-
"@oh-my-pi/pi-natives": "15.7.
|
|
56
|
-
"@oh-my-pi/pi-tui": "15.7.
|
|
57
|
-
"@oh-my-pi/pi-utils": "15.7.
|
|
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
|
+
}
|