@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +1 -1
- package/dist/cli.js +4659 -4261
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +316 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/controller.d.ts +40 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +48 -0
- package/dist/types/live/visualizer.d.ts +33 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +314 -0
- package/dist/types/session/agent-session.d.ts +118 -755
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +49 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +22 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +143 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +5 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +14 -0
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/usage-cli.ts +10 -1
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +10 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +209 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +89 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/audio-worklet.txt +59 -0
- package/src/live/browser-runtime.txt +221 -0
- package/src/live/controller.ts +464 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +490 -0
- package/src/live/visualizer.ts +234 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +11 -2
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/live-command-controller.ts +178 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +18 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +157 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +210 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +334 -0
- package/src/session/agent-session.ts +1892 -12226
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +212 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +263 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +943 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +92 -11
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/system-prompt.ts +41 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +26 -0
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +7 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +67 -3
- package/src/tts/streaming-player.ts +62 -4
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import { type Agent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Model, ProviderSessionState, ServiceTier, ServiceTierByFamily, ServiceTierFamily } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import {
|
|
4
|
+
clearAnthropicFastModeFallback,
|
|
5
|
+
Effort,
|
|
6
|
+
realizesPriorityServiceTier,
|
|
7
|
+
resolveModelServiceTier,
|
|
8
|
+
serviceTierFamily,
|
|
9
|
+
} from "@oh-my-pi/pi-ai";
|
|
10
|
+
import { isFireworksFastModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
11
|
+
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
12
|
+
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
13
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
15
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
16
|
+
import {
|
|
17
|
+
filterAvailableModelsByEnabledPatterns,
|
|
18
|
+
formatModelStringWithRouting,
|
|
19
|
+
getModelMatchPreferences,
|
|
20
|
+
type ResolvedModelRoleValue,
|
|
21
|
+
resolveModelRoleValue,
|
|
22
|
+
} from "../config/model-resolver";
|
|
23
|
+
import { getKnownRoleIds } from "../config/model-roles";
|
|
24
|
+
import type { Settings } from "../config/settings";
|
|
25
|
+
import { containsUltrathink } from "../modes/ultrathink";
|
|
26
|
+
import {
|
|
27
|
+
AUTO_THINKING,
|
|
28
|
+
type ConfiguredThinkingLevel,
|
|
29
|
+
clampAutoThinkingEffort,
|
|
30
|
+
resolveProvisionalAutoLevel,
|
|
31
|
+
resolveThinkingLevelForModel,
|
|
32
|
+
shouldDisableReasoning,
|
|
33
|
+
toReasoningEffort,
|
|
34
|
+
} from "../thinking";
|
|
35
|
+
import type { EditMode } from "../utils/edit-mode";
|
|
36
|
+
import type { AgentSessionEvent } from "./agent-session-events";
|
|
37
|
+
import type { ModelCycleResult, ResolvedRoleModel, RoleModelCycle, RoleModelCycleResult } from "./agent-session-types";
|
|
38
|
+
import { formatRoleModelValue, resolveRoleModelFull } from "./role-models";
|
|
39
|
+
import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
|
|
40
|
+
import type { SessionManager } from "./session-manager";
|
|
41
|
+
|
|
42
|
+
/** Capabilities borrowed from the owning AgentSession. */
|
|
43
|
+
export interface ModelControlsHost {
|
|
44
|
+
agent: Agent;
|
|
45
|
+
settings: Settings;
|
|
46
|
+
modelRegistry: ModelRegistry;
|
|
47
|
+
sessionManager: SessionManager;
|
|
48
|
+
providerSessionState: Map<string, ProviderSessionState>;
|
|
49
|
+
model(): Model | undefined;
|
|
50
|
+
sessionId(): string;
|
|
51
|
+
promptGeneration(): number;
|
|
52
|
+
resolveActiveEditMode(): EditMode;
|
|
53
|
+
syncAfterModelChange(previousEditMode: EditMode): Promise<void>;
|
|
54
|
+
setModelWithProviderSessionReset(model: Model): void;
|
|
55
|
+
clearActiveRetryFallback(): void;
|
|
56
|
+
clearInheritedProviderPromptCacheKey(): void;
|
|
57
|
+
magicKeywordEnabled(keyword: "orchestrate" | "ultrathink" | "workflow"): boolean;
|
|
58
|
+
emit(event: AgentSessionEvent): void;
|
|
59
|
+
emitSessionEvent(event: AgentSessionEvent): Promise<void>;
|
|
60
|
+
emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Owns model selection, thinking effort, role cycling, and service tiers. */
|
|
64
|
+
export class ModelControls {
|
|
65
|
+
readonly #host: ModelControlsHost;
|
|
66
|
+
#scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
67
|
+
#thinkingLevel: ThinkingLevel | undefined;
|
|
68
|
+
#autoThinking = false;
|
|
69
|
+
#autoResolvedLevel: Effort | undefined;
|
|
70
|
+
#serviceTierByFamily: ServiceTierByFamily;
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
host: ModelControlsHost,
|
|
74
|
+
options: {
|
|
75
|
+
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
76
|
+
thinkingLevel?: ConfiguredThinkingLevel;
|
|
77
|
+
serviceTierByFamily?: ServiceTierByFamily;
|
|
78
|
+
},
|
|
79
|
+
) {
|
|
80
|
+
this.#host = host;
|
|
81
|
+
this.#scopedModels = options.scopedModels ?? [];
|
|
82
|
+
this.#serviceTierByFamily = options.serviceTierByFamily ?? {};
|
|
83
|
+
if (options.thinkingLevel === AUTO_THINKING) {
|
|
84
|
+
// Keep auto pending until the first turn while exposing a valid wire effort.
|
|
85
|
+
this.#autoThinking = true;
|
|
86
|
+
this.#thinkingLevel = resolveProvisionalAutoLevel(this.#model);
|
|
87
|
+
} else {
|
|
88
|
+
this.#thinkingLevel = options.thinkingLevel;
|
|
89
|
+
}
|
|
90
|
+
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get #model(): Model | undefined {
|
|
94
|
+
return this.#host.model();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Effective metadata-clamped thinking level applied to the agent. */
|
|
98
|
+
get thinkingLevel(): ThinkingLevel | undefined {
|
|
99
|
+
return this.#thinkingLevel;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Configured selector, preserving `auto` while classification is active. */
|
|
103
|
+
configuredThinkingLevel(): ConfiguredThinkingLevel | undefined {
|
|
104
|
+
return this.#autoThinking ? AUTO_THINKING : this.#thinkingLevel;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Whether per-turn automatic thinking classification is enabled. */
|
|
108
|
+
get isAutoThinking(): boolean {
|
|
109
|
+
return this.#autoThinking;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Last concrete effort selected by automatic classification. */
|
|
113
|
+
get autoResolvedThinkingLevel(): Effort | undefined {
|
|
114
|
+
return this.#autoResolvedLevel;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Models explicitly scoped to the session's cycle command. */
|
|
118
|
+
get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel?: ThinkingLevel }> {
|
|
119
|
+
return this.#scopedModels;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Live per-provider-family service-tier selection. */
|
|
123
|
+
get serviceTierByFamily(): ServiceTierByFamily {
|
|
124
|
+
return this.#serviceTierByFamily;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Restores thinking state from a transcript without persisting a new entry. */
|
|
128
|
+
restoreThinkingLevel(level: ConfiguredThinkingLevel | undefined): void {
|
|
129
|
+
this.#autoThinking = level === AUTO_THINKING;
|
|
130
|
+
this.#autoResolvedLevel = undefined;
|
|
131
|
+
this.#thinkingLevel =
|
|
132
|
+
level === AUTO_THINKING
|
|
133
|
+
? resolveProvisionalAutoLevel(this.#model)
|
|
134
|
+
: resolveThinkingLevelForModel(this.#model, level);
|
|
135
|
+
this.#applyThinkingLevelToAgent(this.#thinkingLevel);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Restores an exact thinking snapshot after a failed session switch. */
|
|
139
|
+
restoreThinkingSnapshot(level: ThinkingLevel | undefined, auto: boolean, resolved: Effort | undefined): void {
|
|
140
|
+
this.#thinkingLevel = level;
|
|
141
|
+
this.#autoThinking = auto;
|
|
142
|
+
this.#autoResolvedLevel = resolved;
|
|
143
|
+
this.#applyThinkingLevelToAgent(level);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Restores service tiers without persisting a duplicate transcript entry. */
|
|
147
|
+
restoreServiceTiers(tiers: ServiceTierByFamily): void {
|
|
148
|
+
this.#serviceTierByFamily = tiers;
|
|
149
|
+
}
|
|
150
|
+
resolveRoleModel(role: string): Model | undefined {
|
|
151
|
+
return resolveRoleModelFull(this.#host.settings, role, this.#host.modelRegistry.getAvailable(), this.#model)
|
|
152
|
+
.model;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
resolveRoleModelWithThinking(role: string): ResolvedModelRoleValue {
|
|
156
|
+
return resolveRoleModelFull(this.#host.settings, role, this.#host.modelRegistry.getAvailable(), this.#model);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resolveTemporaryModelThinkingLevel(model: Model): ConfiguredThinkingLevel | undefined {
|
|
160
|
+
const availableModels = this.#host.modelRegistry.getAvailable();
|
|
161
|
+
if (availableModels.length === 0) return undefined;
|
|
162
|
+
|
|
163
|
+
const matchPreferences = getModelMatchPreferences(this.#host.settings);
|
|
164
|
+
for (const role of getKnownRoleIds(this.#host.settings)) {
|
|
165
|
+
const roleValue = this.#host.settings.getModelRole(role);
|
|
166
|
+
if (!roleValue) continue;
|
|
167
|
+
|
|
168
|
+
const resolved = resolveModelRoleValue(roleValue, availableModels, {
|
|
169
|
+
settings: this.#host.settings,
|
|
170
|
+
matchPreferences,
|
|
171
|
+
});
|
|
172
|
+
if (!resolved.explicitThinkingLevel || resolved.thinkingLevel === undefined || !resolved.model) continue;
|
|
173
|
+
if (modelsAreEqual(resolved.model, model)) return resolved.thinkingLevel;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async setModel(
|
|
180
|
+
model: Model,
|
|
181
|
+
role: string = "default",
|
|
182
|
+
options?: {
|
|
183
|
+
selector?: string;
|
|
184
|
+
thinkingLevel?: ThinkingLevel;
|
|
185
|
+
persist?: boolean;
|
|
186
|
+
currentContextTokens?: number;
|
|
187
|
+
},
|
|
188
|
+
): Promise<{ switched: boolean }> {
|
|
189
|
+
const previousEditMode = this.#host.resolveActiveEditMode();
|
|
190
|
+
if (!this.#host.modelRegistry.hasConfiguredAuth(model)) {
|
|
191
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const targetModel = await this.#host.modelRegistry.refreshSelectedModelMetadata(model);
|
|
195
|
+
|
|
196
|
+
this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
|
|
197
|
+
this.#host.clearActiveRetryFallback();
|
|
198
|
+
this.#host.setModelWithProviderSessionReset(targetModel);
|
|
199
|
+
this.#host.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
|
|
200
|
+
if (options?.persist) {
|
|
201
|
+
this.#host.settings.setModelRole(
|
|
202
|
+
role,
|
|
203
|
+
formatRoleModelValue(
|
|
204
|
+
this.#host.settings,
|
|
205
|
+
this.#host.modelRegistry,
|
|
206
|
+
role,
|
|
207
|
+
targetModel,
|
|
208
|
+
options.selector,
|
|
209
|
+
options.thinkingLevel,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
this.#host.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
|
|
214
|
+
|
|
215
|
+
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
216
|
+
// configured defaultLevel; otherwise preserve the current level (or auto).
|
|
217
|
+
this.#reapplyThinkingLevel(targetModel.thinking?.defaultLevel);
|
|
218
|
+
await this.#host.syncAfterModelChange(previousEditMode);
|
|
219
|
+
return { switched: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Set model temporarily (for this session only).
|
|
224
|
+
* Validates that a credential source is configured (synchronously, without
|
|
225
|
+
* refreshing OAuth or running command-backed key programs), saves to session
|
|
226
|
+
* log but NOT to settings.
|
|
227
|
+
* @throws Error if no API key available for the model
|
|
228
|
+
*/
|
|
229
|
+
async setModelTemporary(
|
|
230
|
+
model: Model,
|
|
231
|
+
thinkingLevel?: ConfiguredThinkingLevel,
|
|
232
|
+
options?: { ephemeral?: boolean },
|
|
233
|
+
): Promise<void> {
|
|
234
|
+
const previousEditMode = this.#host.resolveActiveEditMode();
|
|
235
|
+
if (!this.#host.modelRegistry.hasConfiguredAuth(model)) {
|
|
236
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const targetModel = await this.#host.modelRegistry.refreshSelectedModelMetadata(model);
|
|
240
|
+
|
|
241
|
+
this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
|
|
242
|
+
this.#host.clearActiveRetryFallback();
|
|
243
|
+
this.#host.setModelWithProviderSessionReset(targetModel);
|
|
244
|
+
this.#host.sessionManager.appendModelChange(
|
|
245
|
+
`${targetModel.provider}/${targetModel.id}`,
|
|
246
|
+
options?.ephemeral ? EPHEMERAL_MODEL_CHANGE_ROLE : "temporary",
|
|
247
|
+
);
|
|
248
|
+
this.#host.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
|
|
249
|
+
|
|
250
|
+
// Apply explicit thinking level if given; otherwise prefer the model's
|
|
251
|
+
// configured defaultLevel; otherwise re-clamp the current level (or auto).
|
|
252
|
+
if (thinkingLevel !== undefined) {
|
|
253
|
+
this.setThinkingLevel(thinkingLevel);
|
|
254
|
+
} else {
|
|
255
|
+
this.#reapplyThinkingLevel(targetModel.thinking?.defaultLevel);
|
|
256
|
+
}
|
|
257
|
+
await this.#host.syncAfterModelChange(previousEditMode);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Cycle to next/previous model.
|
|
262
|
+
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
263
|
+
* @param direction - "forward" (default) or "backward"
|
|
264
|
+
* @returns The new model info, or undefined if only one model available
|
|
265
|
+
*/
|
|
266
|
+
async cycleModel(direction: "forward" | "backward" = "forward"): Promise<ModelCycleResult | undefined> {
|
|
267
|
+
if (this.#scopedModels.length > 0) {
|
|
268
|
+
return this.#cycleScopedModel(direction);
|
|
269
|
+
}
|
|
270
|
+
return this.#cycleAvailableModel(direction);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolve the configured role models in the given order plus the index of
|
|
275
|
+
* the currently active one. Roles that have no configured model, or whose
|
|
276
|
+
* configured model is not currently available, are skipped. The `default`
|
|
277
|
+
* role falls back to the active model when no explicit assignment exists.
|
|
278
|
+
*
|
|
279
|
+
* Returns `undefined` only when there is no current model or no available
|
|
280
|
+
* models at all; an empty `models` array is never returned (callers should
|
|
281
|
+
* still guard on `models.length`).
|
|
282
|
+
*/
|
|
283
|
+
getRoleModelCycle(roleOrder: readonly string[]): RoleModelCycle | undefined {
|
|
284
|
+
const availableModels = this.#host.modelRegistry.getAvailable();
|
|
285
|
+
if (availableModels.length === 0) return undefined;
|
|
286
|
+
|
|
287
|
+
const currentModel = this.#model;
|
|
288
|
+
if (!currentModel) return undefined;
|
|
289
|
+
const matchPreferences = getModelMatchPreferences(this.#host.settings);
|
|
290
|
+
const models: ResolvedRoleModel[] = [];
|
|
291
|
+
|
|
292
|
+
for (const role of roleOrder) {
|
|
293
|
+
const roleModelStr =
|
|
294
|
+
role === "default"
|
|
295
|
+
? (this.#host.settings.getModelRole("default") ?? `${currentModel.provider}/${currentModel.id}`)
|
|
296
|
+
: this.#host.settings.getModelRole(role);
|
|
297
|
+
if (!roleModelStr) continue;
|
|
298
|
+
|
|
299
|
+
const resolved = resolveModelRoleValue(roleModelStr, availableModels, {
|
|
300
|
+
settings: this.#host.settings,
|
|
301
|
+
matchPreferences,
|
|
302
|
+
});
|
|
303
|
+
if (!resolved.model) continue;
|
|
304
|
+
|
|
305
|
+
models.push({
|
|
306
|
+
role,
|
|
307
|
+
model: resolved.model,
|
|
308
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
309
|
+
explicitThinkingLevel: resolved.explicitThinkingLevel,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (models.length === 0) return undefined;
|
|
314
|
+
|
|
315
|
+
// Trust the recorded role only while its resolved model still IS the
|
|
316
|
+
// active model. A model switch through another surface (alt+m, retry
|
|
317
|
+
// fallback, /model) or a role re-configuration leaves the recorded role
|
|
318
|
+
// pointing at a model the session no longer runs; cycling from that
|
|
319
|
+
// stale slot lands on the wrong neighbor and reads as a skipped entry.
|
|
320
|
+
const lastRole = this.#host.sessionManager.getLastModelChangeRole();
|
|
321
|
+
let currentIndex = lastRole ? models.findIndex(entry => entry.role === lastRole) : -1;
|
|
322
|
+
if (currentIndex !== -1 && !modelsAreEqual(models[currentIndex].model, currentModel)) {
|
|
323
|
+
currentIndex = -1;
|
|
324
|
+
}
|
|
325
|
+
if (currentIndex === -1) {
|
|
326
|
+
currentIndex = models.findIndex(entry => modelsAreEqual(entry.model, currentModel));
|
|
327
|
+
}
|
|
328
|
+
if (currentIndex === -1) currentIndex = 0;
|
|
329
|
+
|
|
330
|
+
return { models, currentIndex };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Apply a resolved role model as the active model without changing global
|
|
335
|
+
* settings. Shared with role cycling and the plan-approval model slider.
|
|
336
|
+
*/
|
|
337
|
+
async applyRoleModel(entry: ResolvedRoleModel): Promise<void> {
|
|
338
|
+
await this.setModel(entry.model, entry.role);
|
|
339
|
+
if (entry.explicitThinkingLevel && entry.thinkingLevel !== undefined) {
|
|
340
|
+
this.setThinkingLevel(entry.thinkingLevel);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Cycle through configured role models in a fixed order.
|
|
346
|
+
* Skips missing roles and changes only the active session model.
|
|
347
|
+
* @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
|
|
348
|
+
* @param direction - "forward" (default) or "backward"
|
|
349
|
+
*/
|
|
350
|
+
async cycleRoleModels(
|
|
351
|
+
roleOrder: readonly string[],
|
|
352
|
+
direction: "forward" | "backward" = "forward",
|
|
353
|
+
): Promise<RoleModelCycleResult | undefined> {
|
|
354
|
+
const cycle = this.getRoleModelCycle(roleOrder);
|
|
355
|
+
if (!cycle || cycle.models.length <= 1) return undefined;
|
|
356
|
+
|
|
357
|
+
const step = direction === "backward" ? -1 : 1;
|
|
358
|
+
const next = cycle.models[(cycle.currentIndex + step + cycle.models.length) % cycle.models.length];
|
|
359
|
+
|
|
360
|
+
await this.applyRoleModel(next);
|
|
361
|
+
|
|
362
|
+
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async #getScopedModelsWithApiKey(): Promise<Array<{ model: Model; thinkingLevel?: ThinkingLevel }>> {
|
|
366
|
+
const apiKeysByProvider = new Map<string, string | undefined>();
|
|
367
|
+
const result: Array<{ model: Model; thinkingLevel?: ThinkingLevel }> = [];
|
|
368
|
+
|
|
369
|
+
for (const scoped of this.#scopedModels) {
|
|
370
|
+
const provider = scoped.model.provider;
|
|
371
|
+
let apiKey: string | undefined;
|
|
372
|
+
if (apiKeysByProvider.has(provider)) {
|
|
373
|
+
apiKey = apiKeysByProvider.get(provider);
|
|
374
|
+
} else {
|
|
375
|
+
apiKey = await this.#host.modelRegistry.getApiKeyForProvider(provider, this.#host.sessionId());
|
|
376
|
+
apiKeysByProvider.set(provider, apiKey);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (apiKey) {
|
|
380
|
+
result.push(scoped);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async #cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
388
|
+
const previousEditMode = this.#host.resolveActiveEditMode();
|
|
389
|
+
const scopedModels = await this.#getScopedModelsWithApiKey();
|
|
390
|
+
if (scopedModels.length <= 1) return undefined;
|
|
391
|
+
|
|
392
|
+
const currentModel = this.#model;
|
|
393
|
+
let currentIndex = scopedModels.findIndex(sm => modelsAreEqual(sm.model, currentModel));
|
|
394
|
+
|
|
395
|
+
if (currentIndex === -1) currentIndex = 0;
|
|
396
|
+
const len = scopedModels.length;
|
|
397
|
+
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
398
|
+
const next = scopedModels[nextIndex];
|
|
399
|
+
|
|
400
|
+
// Apply model
|
|
401
|
+
this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(next.model));
|
|
402
|
+
this.#host.clearActiveRetryFallback();
|
|
403
|
+
this.#host.setModelWithProviderSessionReset(next.model);
|
|
404
|
+
this.#host.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
405
|
+
this.#host.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
|
|
406
|
+
|
|
407
|
+
// Apply the scoped model's configured thinking level, preserving auto.
|
|
408
|
+
this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : next.thinkingLevel);
|
|
409
|
+
await this.#host.syncAfterModelChange(previousEditMode);
|
|
410
|
+
|
|
411
|
+
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async #cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
415
|
+
const previousEditMode = this.#host.resolveActiveEditMode();
|
|
416
|
+
const availableModels = this.#host.modelRegistry.getAvailable();
|
|
417
|
+
if (availableModels.length <= 1) return undefined;
|
|
418
|
+
|
|
419
|
+
const currentModel = this.#model;
|
|
420
|
+
let currentIndex = availableModels.findIndex(m => modelsAreEqual(m, currentModel));
|
|
421
|
+
|
|
422
|
+
if (currentIndex === -1) currentIndex = 0;
|
|
423
|
+
const len = availableModels.length;
|
|
424
|
+
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
425
|
+
const nextModel = availableModels[nextIndex];
|
|
426
|
+
|
|
427
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(nextModel, this.#host.sessionId());
|
|
428
|
+
if (!apiKey) {
|
|
429
|
+
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(nextModel));
|
|
433
|
+
this.#host.clearActiveRetryFallback();
|
|
434
|
+
this.#host.setModelWithProviderSessionReset(nextModel);
|
|
435
|
+
this.#host.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
436
|
+
this.#host.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
437
|
+
// Re-apply the current thinking level (or auto) for the newly selected model
|
|
438
|
+
this.#reapplyThinkingLevel();
|
|
439
|
+
await this.#host.syncAfterModelChange(previousEditMode);
|
|
440
|
+
|
|
441
|
+
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Get all available models with valid API keys, filtered by `enabledModels` when configured.
|
|
446
|
+
* See {@link filterAvailableModelsByEnabledPatterns} for supported pattern forms and limitations.
|
|
447
|
+
*/
|
|
448
|
+
getAvailableModels(): Model[] {
|
|
449
|
+
const all = this.#host.modelRegistry.getAvailable();
|
|
450
|
+
const patterns = this.#host.settings.get("enabledModels");
|
|
451
|
+
if (!patterns || patterns.length === 0) return all;
|
|
452
|
+
return filterAvailableModelsByEnabledPatterns(all, patterns, this.#host.settings);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// =========================================================================
|
|
456
|
+
// Thinking Level Management
|
|
457
|
+
// =========================================================================
|
|
458
|
+
|
|
459
|
+
#applyThinkingLevelToAgent(level: ThinkingLevel | undefined): void {
|
|
460
|
+
this.#host.agent.setThinkingLevel(toReasoningEffort(level));
|
|
461
|
+
this.#host.agent.setDisableReasoning(shouldDisableReasoning(level));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Set the thinking level. `auto` enables per-turn classification. Entering
|
|
466
|
+
* auto writes its provisional level plus `configured: "auto"` immediately,
|
|
467
|
+
* giving external readers an authoritative selection receipt before the next
|
|
468
|
+
* user turn. Later classifications persist only changed concrete resolutions.
|
|
469
|
+
*/
|
|
470
|
+
setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist: boolean = false): void {
|
|
471
|
+
if (level === AUTO_THINKING) {
|
|
472
|
+
const provisional = resolveProvisionalAutoLevel(this.#model);
|
|
473
|
+
const wasAuto = this.#autoThinking;
|
|
474
|
+
const previousLevel = this.#thinkingLevel;
|
|
475
|
+
this.#autoThinking = true;
|
|
476
|
+
this.#autoResolvedLevel = undefined;
|
|
477
|
+
this.#thinkingLevel = provisional;
|
|
478
|
+
if (!wasAuto) {
|
|
479
|
+
this.#host.clearInheritedProviderPromptCacheKey();
|
|
480
|
+
}
|
|
481
|
+
this.#applyThinkingLevelToAgent(provisional);
|
|
482
|
+
if (persist) {
|
|
483
|
+
this.#host.settings.set("defaultThinkingLevel", AUTO_THINKING);
|
|
484
|
+
}
|
|
485
|
+
const isChanging = !wasAuto || previousLevel !== provisional;
|
|
486
|
+
if (isChanging) {
|
|
487
|
+
this.#host.sessionManager.appendThinkingLevelChange(provisional, AUTO_THINKING);
|
|
488
|
+
this.#host.emit({ type: "thinking_level_changed", thinkingLevel: provisional, configured: AUTO_THINKING });
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const wasAuto = this.#autoThinking;
|
|
494
|
+
this.#autoThinking = false;
|
|
495
|
+
this.#autoResolvedLevel = undefined;
|
|
496
|
+
const effectiveLevel = resolveThinkingLevelForModel(this.#model, level);
|
|
497
|
+
// Leaving auto must persist even when the resolved effort is unchanged (e.g.
|
|
498
|
+
// auto resolved to medium, then the user pins medium): otherwise the latest
|
|
499
|
+
// session entry keeps `configured: "auto"` and resume re-enables auto.
|
|
500
|
+
const isChanging = wasAuto || effectiveLevel !== this.#thinkingLevel;
|
|
501
|
+
|
|
502
|
+
this.#thinkingLevel = effectiveLevel;
|
|
503
|
+
this.#applyThinkingLevelToAgent(effectiveLevel);
|
|
504
|
+
|
|
505
|
+
if (isChanging) {
|
|
506
|
+
this.#host.clearInheritedProviderPromptCacheKey();
|
|
507
|
+
this.#host.sessionManager.appendThinkingLevelChange(effectiveLevel, effectiveLevel);
|
|
508
|
+
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
|
509
|
+
this.#host.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
510
|
+
}
|
|
511
|
+
this.#host.emit({ type: "thinking_level_changed", thinkingLevel: effectiveLevel });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Re-apply the active thinking selection after a model change. Preserves `auto`
|
|
517
|
+
* (re-clamping the provisional level to the new model); otherwise re-applies the
|
|
518
|
+
* preferred default or the current effective level.
|
|
519
|
+
*/
|
|
520
|
+
#reapplyThinkingLevel(preferredDefault?: ThinkingLevel): void {
|
|
521
|
+
this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : (preferredDefault ?? this.#thinkingLevel));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Cycle to next thinking level: off → auto → minimal..max → off.
|
|
526
|
+
* @returns New selector, or undefined if model doesn't support thinking
|
|
527
|
+
*/
|
|
528
|
+
cycleThinkingLevel(): ConfiguredThinkingLevel | undefined {
|
|
529
|
+
if (!this.#model?.reasoning) return undefined;
|
|
530
|
+
|
|
531
|
+
const levels: ConfiguredThinkingLevel[] = [
|
|
532
|
+
ThinkingLevel.Off,
|
|
533
|
+
AUTO_THINKING,
|
|
534
|
+
...this.getAvailableThinkingLevels(),
|
|
535
|
+
];
|
|
536
|
+
const configured = this.configuredThinkingLevel();
|
|
537
|
+
const currentLevel = configured === ThinkingLevel.Inherit ? ThinkingLevel.Off : configured;
|
|
538
|
+
const currentIndex = currentLevel ? levels.indexOf(currentLevel) : -1;
|
|
539
|
+
const nextIndex = (currentIndex + 1) % levels.length;
|
|
540
|
+
const nextLevel = levels[nextIndex];
|
|
541
|
+
if (!nextLevel) return undefined;
|
|
542
|
+
|
|
543
|
+
this.setThinkingLevel(nextLevel);
|
|
544
|
+
return nextLevel;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Timeout (ms) for per-turn auto-thinking classification before falling back. */
|
|
548
|
+
static readonly #AUTO_THINKING_TIMEOUT_MS = 4000;
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Classify the current user turn and set the effective thinking level for it.
|
|
552
|
+
* Bounded by a timeout + abort; on any failure (no smol model, timeout, parse
|
|
553
|
+
* error) it falls back to the provisional concrete level and continues. Never
|
|
554
|
+
* throws into the turn, and never clears `#autoThinking` (auto stays active).
|
|
555
|
+
*/
|
|
556
|
+
async applyAutoThinkingLevel(promptText: string, generation: number): Promise<void> {
|
|
557
|
+
const model = this.#model;
|
|
558
|
+
if (!model?.reasoning) return;
|
|
559
|
+
// Models with reasoning but no controllable effort surface (devin-agent
|
|
560
|
+
// Cascade routes effort via sibling model ids, not a wire param) have
|
|
561
|
+
// nothing to pick — skip classification rather than discard its result.
|
|
562
|
+
if (getSupportedEfforts(model).length === 0) return;
|
|
563
|
+
|
|
564
|
+
let resolved: Effort | undefined;
|
|
565
|
+
if (this.#host.magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
|
|
566
|
+
// The user explicitly asked for maximum thinking; bypass the classifier
|
|
567
|
+
// (and its xhigh auto ceiling) and jump straight to the highest
|
|
568
|
+
// supported level for this model.
|
|
569
|
+
resolved = clampAutoThinkingEffort(model, Effort.Max);
|
|
570
|
+
} else {
|
|
571
|
+
const controller = new AbortController();
|
|
572
|
+
const timer = setTimeout(() => controller.abort(), ModelControls.#AUTO_THINKING_TIMEOUT_MS);
|
|
573
|
+
try {
|
|
574
|
+
resolved = await classifyDifficulty(promptText, {
|
|
575
|
+
settings: this.#host.settings,
|
|
576
|
+
registry: this.#host.modelRegistry,
|
|
577
|
+
model,
|
|
578
|
+
sessionId: this.#host.sessionId(),
|
|
579
|
+
signal: controller.signal,
|
|
580
|
+
metadataResolver: provider => this.#host.agent.metadataForProvider(provider),
|
|
581
|
+
});
|
|
582
|
+
} catch (error) {
|
|
583
|
+
logger.debug("auto-thinking: classification failed; using fallback level", {
|
|
584
|
+
error: error instanceof Error ? error.message : String(error),
|
|
585
|
+
});
|
|
586
|
+
} finally {
|
|
587
|
+
clearTimeout(timer);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Drop the result if the turn was aborted/superseded while classifying.
|
|
592
|
+
if (this.#host.promptGeneration() !== generation || !this.#autoThinking) return;
|
|
593
|
+
|
|
594
|
+
const effort = resolved ?? resolveProvisionalAutoLevel(model);
|
|
595
|
+
if (effort === undefined) return;
|
|
596
|
+
const shouldPersistResolution = this.#thinkingLevel !== effort;
|
|
597
|
+
this.#autoResolvedLevel = effort;
|
|
598
|
+
this.#thinkingLevel = effort;
|
|
599
|
+
this.#applyThinkingLevelToAgent(effort);
|
|
600
|
+
if (shouldPersistResolution) {
|
|
601
|
+
this.#host.sessionManager.appendThinkingLevelChange(effort, AUTO_THINKING);
|
|
602
|
+
}
|
|
603
|
+
this.#host.emit({
|
|
604
|
+
type: "thinking_level_changed",
|
|
605
|
+
thinkingLevel: effort,
|
|
606
|
+
configured: AUTO_THINKING,
|
|
607
|
+
resolved: effort,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* True when the currently selected model's family is set to `priority` — the
|
|
613
|
+
* `/fast` on/off state for the active model. Returns false when no model is
|
|
614
|
+
* selected or the model exposes no service-tier family (e.g. Fireworks, which
|
|
615
|
+
* has its own Providers › Fireworks Tier toggle).
|
|
616
|
+
*
|
|
617
|
+
* For "is priority actually applied to the next request?" use
|
|
618
|
+
* {@link isFastModeActive} instead.
|
|
619
|
+
*/
|
|
620
|
+
isFastModeEnabled(): boolean {
|
|
621
|
+
const family = this.#model ? serviceTierFamily(this.#model) : undefined;
|
|
622
|
+
return family ? this.#serviceTierByFamily[family] === "priority" : false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* True when `priority` is actually realized on the wire for the currently
|
|
627
|
+
* selected model (OpenAI/Google `service_tier`, direct Anthropic fast mode,
|
|
628
|
+
* or Fireworks priority). Returns false for tiers the active model can't
|
|
629
|
+
* realize and when no model is selected.
|
|
630
|
+
*/
|
|
631
|
+
isFastModeActive(): boolean {
|
|
632
|
+
const model = this.#model;
|
|
633
|
+
return !!model && realizesPriorityServiceTier(this.effectiveServiceTier(model), model);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Effective wire service-tier for a request to `model`. Fireworks models take
|
|
638
|
+
* the Priority serving path only when the Providers › Fireworks Tier setting
|
|
639
|
+
* is `"priority"` (and never for `-fast` variants, whose Fast serving path is
|
|
640
|
+
* mutually exclusive with Priority). Every other model resolves the live
|
|
641
|
+
* per-family tier map down to the entry for its family.
|
|
642
|
+
*/
|
|
643
|
+
effectiveServiceTier(model: Model | undefined = this.#model): ServiceTier | undefined {
|
|
644
|
+
if (model?.provider === "fireworks") {
|
|
645
|
+
return this.#host.settings.get("providers.fireworksTier") === "priority" && !isFireworksFastModelId(model.id)
|
|
646
|
+
? "priority"
|
|
647
|
+
: undefined;
|
|
648
|
+
}
|
|
649
|
+
if (!model) return undefined;
|
|
650
|
+
return resolveModelServiceTier(this.#serviceTierByFamily, model);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** The live per-family tier map, or `null` when empty (for session persistence). */
|
|
654
|
+
serviceTierEntry(): ServiceTierByFamily | null {
|
|
655
|
+
return Object.keys(this.#serviceTierByFamily).length > 0 ? this.#serviceTierByFamily : null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Set one family's tier (or clear it with `undefined`); persists the change. */
|
|
659
|
+
setServiceTierFamily(family: ServiceTierFamily, tier: ServiceTier | undefined): void {
|
|
660
|
+
if (this.#serviceTierByFamily[family] === tier) return;
|
|
661
|
+
const next: ServiceTierByFamily = { ...this.#serviceTierByFamily };
|
|
662
|
+
if (tier) next[family] = tier;
|
|
663
|
+
else delete next[family];
|
|
664
|
+
this.#applyServiceTierByFamily(next);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Replace the whole per-family tier map; persists + re-arms Anthropic fast mode. */
|
|
668
|
+
#applyServiceTierByFamily(next: ServiceTierByFamily): void {
|
|
669
|
+
// Re-arming Anthropic priority clears the per-session fast-mode auto-disable
|
|
670
|
+
// so the next request actually carries `speed: "fast"` again.
|
|
671
|
+
if (next.anthropic === "priority" && this.#serviceTierByFamily.anthropic !== "priority") {
|
|
672
|
+
clearAnthropicFastModeFallback(this.#host.providerSessionState);
|
|
673
|
+
}
|
|
674
|
+
this.#serviceTierByFamily = next;
|
|
675
|
+
this.#host.sessionManager.appendServiceTierChange(this.serviceTierEntry());
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* `/fast on|off` targets the family of the currently selected model: it sets
|
|
680
|
+
* (or clears) that family's `priority` tier. Returns `false` when the model
|
|
681
|
+
* has no service-tier family, so callers can report that fast mode is
|
|
682
|
+
* unavailable instead of claiming success.
|
|
683
|
+
*/
|
|
684
|
+
setFastMode(enabled: boolean): boolean {
|
|
685
|
+
const family = this.#model ? serviceTierFamily(this.#model) : undefined;
|
|
686
|
+
if (!family) {
|
|
687
|
+
this.#host.emitNotice(
|
|
688
|
+
"info",
|
|
689
|
+
"The current model has no service-tier control for /fast to toggle.",
|
|
690
|
+
"priority",
|
|
691
|
+
);
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
if (!enabled) {
|
|
695
|
+
if (this.#serviceTierByFamily[family] === "priority") this.setServiceTierFamily(family, undefined);
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
this.setServiceTierFamily(family, "priority");
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
toggleFastMode(): boolean {
|
|
703
|
+
if (!this.setFastMode(!this.isFastModeEnabled())) return false;
|
|
704
|
+
return this.isFastModeEnabled();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Get available thinking levels for current model.
|
|
709
|
+
*/
|
|
710
|
+
getAvailableThinkingLevels(): ReadonlyArray<Effort> {
|
|
711
|
+
if (!this.#model) return [];
|
|
712
|
+
return getSupportedEfforts(this.#model);
|
|
713
|
+
}
|
|
714
|
+
}
|