feat(coding-agent): added temperature configuration setting for LLM sampling control

- Added `temperature` configuration setting to control LLM sampling temperature with values from 0 (deterministic) to 1 (creative) to -1 (provider default).
- Added temperature option selector in settings UI with preset values: Default, 0, 0.2, 0.5, 0.7, 1.
- Renamed `settingsInstance` parameter to `settings` in `CreateAgentSessionOptions` for consistency.
- Updated all internal references from `settingsInstance` to `settings` throughout SDK and components.
- Integrated temperature setting into agent configuration and selector controller.
This commit is contained in:
can1357
2026-02-10 15:23:53 +01:00
parent f3b185d5ec
commit 1eaf40b784
16 changed files with 118 additions and 60 deletions
+4
View File
@@ -1,6 +1,10 @@
# Changelog
## [Unreleased]
### Added
- Added `temperature` option to `AgentOptions` to control LLM sampling temperature
- Added `temperature` getter and setter to `Agent` class for runtime configuration
## [11.6.0] - 2026-02-07
+22
View File
@@ -96,6 +96,11 @@ export interface AgentOptions {
*/
thinkingBudgets?: ThinkingBudgets;
/**
* Sampling temperature for LLM calls. `undefined` uses provider default.
*/
temperature?: number;
/**
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
* If the server's requested delay exceeds this value, the request fails immediately,
@@ -155,6 +160,7 @@ export class Agent {
#interruptMode: "immediate" | "wait";
#sessionId?: string;
#thinkingBudgets?: ThinkingBudgets;
#temperature?: number;
#maxRetryDelayMs?: number;
#getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
#cursorExecHandlers?: CursorExecHandlers;
@@ -179,6 +185,7 @@ export class Agent {
this.streamFn = opts.streamFn || streamSimple;
this.#sessionId = opts.sessionId;
this.#thinkingBudgets = opts.thinkingBudgets;
this.#temperature = opts.temperature;
this.#maxRetryDelayMs = opts.maxRetryDelayMs;
this.getApiKey = opts.getApiKey;
this.#getToolContext = opts.getToolContext;
@@ -216,6 +223,20 @@ export class Agent {
this.#thinkingBudgets = value;
}
/**
* Get the current sampling temperature.
*/
get temperature(): number | undefined {
return this.#temperature;
}
/**
* Set sampling temperature for LLM calls. `undefined` uses provider default.
*/
set temperature(value: number | undefined) {
this.#temperature = value;
}
/**
* Get the current max retry delay in milliseconds.
*/
@@ -564,6 +585,7 @@ export class Agent {
const config: AgentLoopConfig = {
model,
reasoning,
temperature: this.#temperature,
interruptMode: this.#interruptMode,
sessionId: this.#sessionId,
thinkingBudgets: this.#thinkingBudgets,
+9
View File
@@ -1,6 +1,15 @@
# Changelog
## [Unreleased]
### Added
- Added `temperature` setting to control sampling temperature (0 = deterministic, 1 = creative, -1 = provider default)
- Added temperature option selector in settings UI with preset values (Default, 0, 0.2, 0.5, 0.7, 1)
### Changed
- Renamed `settingsInstance` parameter to `settings` in `CreateAgentSessionOptions` for consistency
- Updated all internal references from `settingsInstance` to `settings` throughout SDK and components
## [11.9.0] - 2026-02-10
+12 -13
View File
@@ -272,7 +272,7 @@ const { session } = await createAgentSession({
`agentDir` is used for:
- Global settings (`config.yml` + `agent.db`)
- Primary auth/models locations (`agent.db`, `models.yml`, `models.json`)
- Primary auth/models locations (`agent.db`, `models.yml`, `models.json`)
- Prompt templates (`prompts/`)
- Custom TS commands (`commands/`)
@@ -328,13 +328,12 @@ API key resolution priority (handled by AuthStorage):
3. Environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)
4. Fallback resolver (for custom provider keys from `models.yml`)
`discoverAuthStorage` opens the `agent.db` SQLite database in the agent directory.
`discoverAuthStorage` opens the `agent.db` SQLite database in the agent directory.
```typescript
import { AuthStorage, ModelRegistry, discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent";
// Default: uses agentDir/agent.db and agentDir/models.yml
// Default: uses agentDir/agent.db and agentDir/models.yml
const authStorage = await discoverAuthStorage();
const modelRegistry = discoverModels(authStorage);
@@ -434,14 +433,14 @@ For advanced use cases, you can create tools directly using `createTools`:
```typescript
import { createTools, Settings, type ToolSession } from "@oh-my-pi/pi-coding-agent";
const settingsInstance = await Settings.init({ cwd: "/path/to/project" });
const settings = await Settings.init({ cwd: "/path/to/project" });
const session: ToolSession = {
cwd: "/path/to/project",
hasUI: false,
getSessionFile: () => null,
getSessionSpawns: () => "*",
settings: settingsInstance,
settings,
};
const tools = await createTools(session);
@@ -722,15 +721,15 @@ sm.createBranchedSession(leafId); // Extract path to new file
import { createAgentSession, Settings, SessionManager } from "@oh-my-pi/pi-coding-agent";
// Default: loads from files (global config.yml + project settings.json merged)
const settingsInstance = await Settings.init();
const settings = await Settings.init();
const { session } = await createAgentSession({
settingsInstance,
settings,
});
// Read/write settings
const enabled = settingsInstance.get("compaction.enabled");
settingsInstance.set("compaction.enabled", false);
const enabled = settings.get("compaction.enabled");
settings.set("compaction.enabled", false);
// In-memory (no file I/O, for testing)
const isolated = Settings.isolated({
@@ -739,7 +738,7 @@ const isolated = Settings.isolated({
});
const { session } = await createAgentSession({
settingsInstance: isolated,
settings: isolated,
sessionManager: SessionManager.inMemory(),
});
@@ -905,7 +904,7 @@ const model = getModel("anthropic", "claude-opus-4-5");
if (!model) throw new Error("Model not found");
// In-memory settings with overrides
const settingsInstance = Settings.isolated({
const settings = Settings.isolated({
"compaction.enabled": false,
"retry.enabled": true,
});
@@ -929,7 +928,7 @@ const { session } = await createAgentSession({
slashCommands: [],
sessionManager: SessionManager.inMemory(),
settingsInstance,
settings,
});
session.subscribe((event) => {
@@ -237,8 +237,8 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
* Initialize capability system with settings manager for persistence.
* Call this once on startup to enable persistent provider state.
*/
export function initializeWithSettings(settingsInstance: Settings): void {
settings = settingsInstance;
export function initializeWithSettings(activeSettings: Settings): void {
settings = activeSettings;
// Load disabled providers from settings
const disabled = settings.get("disabledProviders");
disabledProviders.clear();
@@ -58,7 +58,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
cwd: input.cwd,
authStorage: input.authStorage,
modelRegistry: input.modelRegistry,
settingsInstance: input.settings,
settings: input.settings,
model: input.model,
systemPrompt,
customTools: tools,
@@ -28,8 +28,7 @@ interface CommitExecutionContext {
export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
const cwd = process.cwd();
const git = new ControlledGit(cwd);
const [settingsInstance, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
const settings = settingsInstance;
const [settings, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
writeStdout("● Resolving model...");
const modelRegistry = new ModelRegistry(authStorage);
@@ -11,7 +11,7 @@ import type { Settings } from "../config/settings";
export async function resolvePrimaryModel(
override: string | undefined,
settings: Settings,
settings,
modelRegistry: {
getAvailable: () => Model<Api>[];
getApiKey: (model: Model<Api>) => Promise<string | undefined>;
@@ -39,7 +39,7 @@ export async function resolvePrimaryModel(
}
export async function resolveSmolModel(
settings: Settings,
settings,
modelRegistry: {
getAvailable: () => Model<Api>[];
getApiKey: (model: Model<Api>) => Promise<string | undefined>;
+4 -4
View File
@@ -39,18 +39,18 @@ export async function runCommitCommand(args: CommitCommandArgs): Promise<void> {
async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
const cwd = process.cwd();
const settingsInstance = await Settings.init();
const commitSettings = settingsInstance.getGroup("commit");
const settings = await Settings.init();
const commitSettings = settings.getGroup("commit");
const authStorage = await discoverAuthStorage();
const modelRegistry = new ModelRegistry(authStorage);
const { model: primaryModel, apiKey: primaryApiKey } = await resolvePrimaryModel(
args.model,
settingsInstance,
settings,
modelRegistry,
);
const { model: smolModel, apiKey: smolApiKey } = await resolveSmolModel(
settingsInstance,
settings,
modelRegistry,
primaryModel,
primaryApiKey,
@@ -172,6 +172,16 @@ export const SETTINGS_SCHEMA = {
submenu: true,
},
},
temperature: {
type: "number",
default: -1,
ui: {
tab: "agent",
label: "Temperature",
description: "Sampling temperature (0 = deterministic, 1 = creative, -1 = provider default)",
submenu: true,
},
},
hideThinkingBlock: {
type: "boolean",
default: false,
@@ -38,7 +38,7 @@ export class ExtensionDashboard extends Container {
private constructor(
private readonly cwd: string,
private readonly settingsInstance: Settings | null,
private readonly settings: Settings | null,
private readonly terminalHeight: number,
) {
super();
@@ -46,16 +46,16 @@ export class ExtensionDashboard extends Container {
static async create(
cwd: string,
settingsInstance: Settings | null = null,
settings: Settings | null = null,
terminalHeight?: number,
): Promise<ExtensionDashboard> {
const dashboard = new ExtensionDashboard(cwd, settingsInstance, terminalHeight ?? process.stdout.rows ?? 24);
const dashboard = new ExtensionDashboard(cwd, settings, terminalHeight ?? process.stdout.rows ?? 24);
await dashboard.#init();
return dashboard;
}
async #init(): Promise<void> {
const sm = this.settingsInstance ?? (await Settings.init());
const sm = this.settings ?? (await Settings.init());
const disabledIds = sm ? ((sm.get("disabledExtensions") as string[]) ?? []) : [];
this.#state = await createInitialState(this.cwd, disabledIds);
@@ -163,7 +163,7 @@ export class ExtensionDashboard extends Container {
}
#handleExtensionToggle(extensionId: string, enabled: boolean): void {
const sm = this.settingsInstance ?? Settings.instance;
const sm = this.settings ?? Settings.instance;
if (!sm) return;
const disabled = ((sm.get("disabledExtensions") as string[]) ?? []).slice();
@@ -187,7 +187,7 @@ export class ExtensionDashboard extends Container {
// Remember current tab ID before refresh
const currentTabId = this.#state.tabs[this.#state.activeTabIndex]?.id;
const sm = this.settingsInstance ?? Settings.instance;
const sm = this.settings ?? Settings.instance;
const disabledIds = sm ? ((sm.get("disabledExtensions") as string[]) ?? []) : [];
this.#state = await refreshState(this.#state, this.cwd, disabledIds);
@@ -90,7 +90,7 @@ export class ModelSelectorComponent extends Container {
constructor(
tui: TUI,
_currentModel: Model | undefined,
settings: Settings,
settings,
modelRegistry: ModelRegistry,
scopedModels: ReadonlyArray<ScopedModelItem>,
onSelect: (model: Model, role: ModelRole | null) => void,
@@ -164,6 +164,15 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
{ value: "high", label: "high", description: "Deep (~16k tokens)" },
{ value: "xhigh", label: "xhigh", description: "Maximum (~32k tokens)" },
],
// Temperature
temperature: [
{ value: "-1", label: "Default", description: "Use provider default" },
{ value: "0", label: "0", description: "Deterministic" },
{ value: "0.2", label: "0.2", description: "Focused" },
{ value: "0.5", label: "0.5", description: "Balanced" },
{ value: "0.7", label: "0.7", description: "Creative" },
{ value: "1", label: "1", description: "Maximum variety" },
],
// Symbol preset
symbolPreset: [
{ value: "unicode", label: "Unicode", description: "Standard symbols (default)" },
@@ -232,6 +232,11 @@ export class SelectorController {
});
break;
}
case "temperature": {
const temp = typeof value === "number" ? value : Number(value);
this.ctx.session.agent.temperature = temp >= 0 ? temp : undefined;
break;
}
case "statusLinePreset":
case "statusLineSeparator":
case "statusLineShowHooks":
+29 -28
View File
@@ -159,7 +159,7 @@ export interface CreateAgentSessionOptions {
sessionManager?: SessionManager;
/** Settings instance. Default: Settings.init({ cwd, agentDir }) */
settingsInstance?: Settings;
settings?: Settings;
/** Whether UI is available (enables interactive tools like ask). Default: false */
hasUI?: boolean;
@@ -489,14 +489,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
time("discoverModels");
const settingsInstance = options.settingsInstance ?? (await Settings.init({ cwd, agentDir }));
const settings = options.settings ?? (await Settings.init({ cwd, agentDir }));
time("settings");
initializeWithSettings(settingsInstance);
initializeWithSettings(settings);
time("initializeWithSettings");
// Initialize provider preferences from settings
setPreferredSearchProvider(settingsInstance.get("providers.webSearch") ?? "auto");
setPreferredImageProvider(settingsInstance.get("providers.image") ?? "auto");
setPreferredSearchProvider(settings.get("providers.webSearch") ?? "auto");
setPreferredImageProvider(settings.get("providers.image") ?? "auto");
const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
time("sessionManager");
@@ -529,7 +529,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// If still no model, try settings default
if (!model) {
const settingsDefaultModel = settingsInstance.getModelRole("default");
const settingsDefaultModel = settings.getModelRole("default");
if (settingsDefaultModel) {
const parsedModel = parseModelString(settingsDefaultModel);
if (parsedModel) {
@@ -566,12 +566,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
if (thinkingLevel === undefined && hasExistingSession) {
thinkingLevel = hasThinkingEntry
? (existingSession.thinkingLevel as ThinkingLevel)
: ((settingsInstance.get("defaultThinkingLevel") ?? "off") as ThinkingLevel);
: ((settings.get("defaultThinkingLevel") ?? "off") as ThinkingLevel);
}
// Fall back to settings default
if (thinkingLevel === undefined) {
thinkingLevel = settingsInstance.get("defaultThinkingLevel") ?? "off";
thinkingLevel = settings.get("defaultThinkingLevel") ?? "off";
}
// Clamp to model capabilities
@@ -587,7 +587,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
skills = options.skills;
skillWarnings = [];
} else {
const skillsSettings = settingsInstance.getGroup("skills") as SkillsSettings;
const skillsSettings = settings.getGroup("skills") as SkillsSettings;
const discovered = await discoverSkills(cwd, agentDir, skillsSettings);
skills = discovered.skills;
skillWarnings = discovered.warnings;
@@ -596,7 +596,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
debugStartup("sdk:discoverSkills");
// Discover rules
const ttsrSettings = settingsInstance.getGroup("ttsr");
const ttsrSettings = settings.getGroup("ttsr");
const ttsrManager = new TtsrManager(ttsrSettings);
const rulesResult = await loadCapability<Rule>(ruleCapability.id, { cwd });
for (const rule of rulesResult.items) {
@@ -648,7 +648,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
},
getPlanModeState: () => session.getPlanModeState(),
getCompactContext: () => session.formatCompactContext(),
settings: settingsInstance,
settings,
authStorage,
modelRegistry,
};
@@ -663,7 +663,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
internalRouter.register(new ArtifactProtocolHandler({ getArtifactsDir }));
internalRouter.register(
new PlanProtocolHandler({
getPlansDirectory: () => settingsInstance.getPlansDirectory(),
getPlansDirectory: () => settings.getPlansDirectory(),
cwd,
}),
);
@@ -706,10 +706,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
);
}
},
enableProjectConfig: settingsInstance.get("mcp.enableProjectConfig") ?? true,
enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
// Always filter Exa - we have native integration
filterExa: true,
cacheStorage: settingsInstance.getStorage(),
cacheStorage: settings.getStorage(),
authStorage,
});
time("discoverAndLoadMCPTools");
@@ -742,7 +742,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
time("getGeminiImageTools");
// Add specialized Exa web search tools if EXA_API_KEY is available
const exaSettings = settingsInstance.getGroup("exa");
const exaSettings = settings.getGroup("exa");
if (exaSettings.enabled && exaSettings.enableSearch) {
const exaSearchTools = await getSearchTools({
enableLinkedin: exaSettings.enableLinkedin as boolean,
@@ -777,13 +777,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// Merge CLI extension paths with settings extension paths
const configuredPaths = [
...(options.additionalExtensionPaths ?? []),
...((settingsInstance.get("extensions") as string[]) ?? []),
...((settings.get("extensions") as string[]) ?? []),
];
extensionsResult = await discoverAndLoadExtensions(
configuredPaths,
cwd,
eventBus,
(settingsInstance.get("disabledExtensions") as string[]) ?? [],
(settings.get("disabledExtensions") as string[]) ?? [],
);
time("discoverAndLoadExtensions");
debugStartup("sdk:discoverAndLoadExtensions");
@@ -906,7 +906,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
tools,
toolNames,
rules: rulebookRules,
skillsSettings: settingsInstance.getGroup("skills") as SkillsSettings,
skillsSettings: settings.getGroup("skills") as SkillsSettings,
});
if (options.systemPrompt === undefined) {
@@ -921,7 +921,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
tools,
toolNames,
rules: rulebookRules,
skillsSettings: settingsInstance.getGroup("skills") as SkillsSettings,
skillsSettings: settings.getGroup("skills") as SkillsSettings,
customPrompt: options.systemPrompt,
});
}
@@ -961,7 +961,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
const converted = convertToLlm(messages);
// Check setting dynamically so mid-session changes take effect
if (!settingsInstance.get("images.blockImages")) {
if (!settings.get("images.blockImages")) {
return converted;
}
// Filter out ImageContent from all messages, replacing with text placeholder
@@ -1014,11 +1014,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
return extensionRunner.emitContext(messages);
}
: undefined,
steeringMode: settingsInstance.get("steeringMode") ?? "one-at-a-time",
followUpMode: settingsInstance.get("followUpMode") ?? "one-at-a-time",
interruptMode: settingsInstance.get("interruptMode") ?? "immediate",
thinkingBudgets: settingsInstance.getGroup("thinkingBudgets"),
kimiApiFormat: settingsInstance.get("providers.kimiApiFormat") ?? "anthropic",
steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
interruptMode: settings.get("interruptMode") ?? "immediate",
thinkingBudgets: settings.getGroup("thinkingBudgets"),
temperature: settings.get("temperature") >= 0 ? settings.get("temperature") : undefined,
kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
getToolContext: tc => toolContextStore.getContext(tc),
getApiKey: async provider => {
// Use the provider argument from the in-flight request;
@@ -1052,7 +1053,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
session = new AgentSession({
agent,
sessionManager,
settings: settingsInstance,
settings,
scopedModels: options.scopedModels,
promptTemplates,
slashCommands,
@@ -1060,7 +1061,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
customCommands: customCommandsResult.commands,
skills,
skillWarnings,
skillsSettings: settingsInstance.getGroup("skills") as Required<SkillsSettings>,
skillsSettings: settings.getGroup("skills") as Required<SkillsSettings>,
modelRegistry,
toolRegistry,
rebuildSystemPrompt,
@@ -1071,7 +1072,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// Warm up LSP servers (connects to detected servers)
let lspServers: CreateAgentSessionResult["lspServers"];
if (enableLsp && settingsInstance.get("lsp.diagnosticsOnWrite")) {
if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {
try {
debugStartup("sdk:warmupLspServers:start");
const result = await warmupLspServers(cwd, {
+1 -1
View File
@@ -855,7 +855,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
cwd: worktree ?? cwd,
authStorage,
modelRegistry,
settingsInstance: settings,
settings,
model,
thinkingLevel: effectiveThinkingLevel,
toolNames,