diff --git a/config.yaml b/config.yaml index c89bbb90..a521400f 100644 --- a/config.yaml +++ b/config.yaml @@ -87,6 +87,7 @@ process: sessionMode: isolated defaultStrategy: parallel defaultOnError: continue + temperature: 0.7 persistence: mode: memory sqlite_path: memory/checkpoints.db diff --git a/openspec/changes/archive/2026-06-23-subagent-temperature-config/.openspec.yaml b/openspec/changes/archive/2026-06-23-subagent-temperature-config/.openspec.yaml new file mode 100644 index 00000000..a4ac4d78 --- /dev/null +++ b/openspec/changes/archive/2026-06-23-subagent-temperature-config/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-23 diff --git a/openspec/changes/archive/2026-06-23-subagent-temperature-config/design.md b/openspec/changes/archive/2026-06-23-subagent-temperature-config/design.md new file mode 100644 index 00000000..1e5afb60 --- /dev/null +++ b/openspec/changes/archive/2026-06-23-subagent-temperature-config/design.md @@ -0,0 +1,82 @@ +## Context + +SubAgents are spawned as separate `node index.js` processes that read their own config.yaml. Currently, temperature is configured only at the provider level (`providers.openai.temperature`) and cannot be adjusted per-subAgent or globally for subAgent invocations. The temperature parameter controls randomness in LLM outputs — lower for precise, deterministic tasks and higher for creative, exploratory work. + +The existing pattern for subAgent configuration (timeout, maxConcurrent, sessionMode) uses environment variables passed from the parent process to the spawned child. Temperature should follow this same pattern. + +## Goals / Non-Goals + +**Goals:** +- Add global default temperature under `process.subAgent.temperature` +- Add per-skill temperature override under `process.subAgent.skills[].temperature` +- Pass temperature to spawned processes via `MADZ_SUBAGENT_TEMPERATURE` env var +- Override provider temperature in spawned processes when env var is set +- Add optional per-call `temperature` parameter to subAgent tool schema + +**Non-Goals:** +- Runtime config reloading (temperature changes require restart) +- Temperature validation against specific LLM provider capabilities +- Per-message temperature overrides within a single subAgent session +- UI/CLI configuration interface + +## Decisions + +### Decision 1: Environment Variable over CLI Argument +**Choice:** Pass temperature via `MADZ_SUBAGENT_TEMPERATURE` environment variable. +**Rationale:** Follows the existing pattern used for timeout configuration. Keeps the spawned process invocation clean. CLI arguments would require modifying the node command line and parsing logic. +**Alternatives considered:** +- CLI argument: More explicit but requires command line modification and parsing +- Config file in temp directory: Overly complex for a single value +- stdin: Unnecessary overhead for a single configuration value + +### Decision 2: Resolution Hierarchy +**Choice:** per-call > per-skill config > global config > env var > provider default +**Rationale:** Provides maximum flexibility while maintaining sensible defaults. Per-call override allows ad-hoc adjustments without config changes. Per-skill override allows skill-specific tuning. Global config provides a sensible default for all subAgents. +**Alternatives considered:** +- Config only: Less flexible, requires config changes for every adjustment +- Env var only: Not user-friendly, requires environment setup + +### Decision 3: Graceful Degradation +**Choice:** Invalid temperature values fall back to provider default rather than crashing. +**Rationale:** Spawned processes may receive stale or corrupted env vars. Crashing would be worse than using a reasonable default. The parent process continues unaffected. +**Alternatives considered:** +- Hard fail: Safer but creates fragile spawned processes +- Log and continue: Added as optional low-priority enhancement + +### Decision 4: Scoped Override +**Choice:** Temperature override only affects the spawned process. +**Rationale:** The parent process should continue using its configured temperature. This prevents unintended side effects and maintains process isolation. +**Alternatives considered:** +- Global override: Would affect all LLM calls in the parent process, unintended side effects + +## Risks / Trade-offs + +### Risk: Config Schema Validation +**Trade-off:** Adding new optional fields increases config complexity slightly. +**Mitigation:** Fields are optional, existing configs continue to work. Validation is straightforward (0-2 range). + +### Risk: Env Var Parsing Errors +**Trade-off:** String-to-float conversion in spawned process may fail. +**Mitigation:** Graceful fallback to provider default. Parse with parseFloat, check isNaN, validate range. + +### Risk: Concurrent SubAgent Interference +**Trade-off:** Multiple subAgents with different temperatures must not interfere. +**Mitigation:** Each spawned process has its own environment. Env vars are scoped to the child process only. + +### Risk: Backward Compatibility +**Trade-off:** New config fields must not break existing deployments. +**Mitigation:** All fields are optional. Missing temperature falls through to provider default. + +## Migration Plan + +No migration required. The change is fully backward compatible: +1. Deploy the code change +2. Existing configs continue to work using provider defaults +3. Users can optionally add `temperature` to their config.yaml +4. No database migrations, no config file migrations + +## Open Questions + +- Should temperature be exposed in the TUI config editor? (Out of scope for this change) +- Should we log temperature overrides for debugging? (Low priority, can be added later) +- Are there LLM providers that don't support temperature? (Provider-specific handling can be added later) \ No newline at end of file diff --git a/openspec/changes/archive/2026-06-23-subagent-temperature-config/proposal.md b/openspec/changes/archive/2026-06-23-subagent-temperature-config/proposal.md new file mode 100644 index 00000000..fff09c4b --- /dev/null +++ b/openspec/changes/archive/2026-06-23-subagent-temperature-config/proposal.md @@ -0,0 +1,27 @@ +## Why + +Currently, subAgents are launched without configurable temperature, meaning their behavior is fixed by the default LLM settings. Users may want to adjust temperature per-subAgent or globally to control the randomness of their outputs — lower for precise, deterministic tasks and higher for creative, exploratory work. This limits fine-grained control over subAgent behavior without needing to modify system prompts or skill definitions. + +## What Changes + +- Add `temperature` field to `process.subAgent` in config.yaml as a global default (0-2 range, OpenAI spec) +- Add `temperature` field to `process.subAgent.skills[].temperature` for per-skill overrides +- Pass temperature to spawned subAgent processes via `MADZ_SUBAGENT_TEMPERATURE` environment variable +- Override provider temperature in spawned processes when env var is set +- Add optional `temperature` parameter to subAgent tool schema for per-call overrides +- Follow resolution hierarchy: per-call > per-skill config > global config > env var > provider default + +## Capabilities + +### New Capabilities +- `subagent-temperature`: Configure temperature for subAgents via config.yaml with global default and per-skill override support + +### Modified Capabilities + + +## Impact + +- **Config:** `config.yaml` — new `process.subAgent.temperature` and `process.subAgent.skills[].temperature` fields +- **Tools:** `src/tools/subAgent.js` — `spawnSubAgentProcess()` passes temperature via env var; tool schema adds optional `temperature` parameter +- **Provider:** `src/provider/openai.js` — `createChatModel()` checks `MADZ_SUBAGENT_TEMPERATURE` env var and overrides provider temperature +- **Backward compatibility:** All changes are additive. Existing configs without temperature continue to work using provider defaults. \ No newline at end of file diff --git a/openspec/changes/archive/2026-06-23-subagent-temperature-config/specs/subagent-temperature/spec.md b/openspec/changes/archive/2026-06-23-subagent-temperature-config/specs/subagent-temperature/spec.md new file mode 100644 index 00000000..5a36e9b2 --- /dev/null +++ b/openspec/changes/archive/2026-06-23-subagent-temperature-config/specs/subagent-temperature/spec.md @@ -0,0 +1,103 @@ +## ADDED Requirements + +### Requirement: Global subAgent temperature configuration +The system SHALL allow users to configure a global default temperature for all subAgents via `process.subAgent.temperature` in config.yaml. Temperature values MUST be floats in the range 0-2 (OpenAI specification). When not configured, subAgents SHALL use the provider default temperature. + +#### Scenario: Valid global temperature is accepted +- **WHEN** config.yaml contains `process.subAgent.temperature: 0.7` +- **THEN** the system accepts the value and uses it as the default for all subAgents + +#### Scenario: Global temperature at minimum boundary +- **WHEN** config.yaml contains `process.subAgent.temperature: 0` +- **THEN** the system accepts the value and uses deterministic (non-random) outputs + +#### Scenario: Global temperature at maximum boundary +- **WHEN** config.yaml contains `process.subAgent.temperature: 2` +- **THEN** the system accepts the value and uses maximum randomness + +#### Scenario: Global temperature outside valid range is rejected +- **WHEN** config.yaml contains `process.subAgent.temperature: -0.1` or `process.subAgent.temperature: 2.1` +- **THEN** the system rejects the value with a validation error + +#### Scenario: Missing global temperature uses provider default +- **WHEN** config.yaml does not contain `process.subAgent.temperature` +- **THEN** subAgents use the provider default temperature from `providers.openai.temperature` + +### Requirement: Per-skill temperature override +The system SHALL allow users to configure a temperature override for specific skills via `process.subAgent.skills[].temperature` in config.yaml. Per-skill temperature MUST take precedence over the global default. + +#### Scenario: Per-skill temperature overrides global default +- **WHEN** config.yaml contains global `temperature: 0.7` and skill-specific `temperature: 0.3` for audit-code +- **THEN** audit-code subAgents use 0.3 while other skills use 0.7 + +#### Scenario: Per-skill temperature without global default +- **WHEN** config.yaml contains only skill-specific `temperature: 0.3` without global temperature +- **THEN** the skill uses 0.3 and other skills use provider default + +### Requirement: Temperature passed to spawned process via environment variable +The system SHALL pass the resolved temperature value to spawned subAgent processes via the `MADZ_SUBAGENT_TEMPERATURE` environment variable. The env var MUST only be set when temperature is explicitly configured (not when using provider default). + +#### Scenario: Temperature env var is set when configured +- **WHEN** config.yaml contains `process.subAgent.temperature: 0.5` +- **THEN** the spawned subAgent process receives `MADZ_SUBAGENT_TEMPERATURE=0.5` + +#### Scenario: Temperature env var is not set when using provider default +- **WHEN** config.yaml does not contain subAgent temperature configuration +- **THEN** the spawned subAgent process does not receive `MADZ_SUBAGENT_TEMPERATURE` env var + +#### Scenario: Per-skill temperature env var overrides global +- **WHEN** config.yaml contains global `temperature: 0.7` and skill-specific `temperature: 0.3` +- **THEN** the audit-code subAgent receives `MADZ_SUBAGENT_TEMPERATURE=0.3` + +### Requirement: Spawned process overrides provider temperature +The system SHALL override the provider temperature in spawned subAgent processes when `MADZ_SUBAGENT_TEMPERATURE` is set. Invalid env var values MUST fall back gracefully to the provider default. + +#### Scenario: Spawned process uses env var temperature +- **WHEN** spawned process has `MADZ_SUBAGENT_TEMPERATURE=0.4` +- **THEN** the LLM call uses temperature 0.4 regardless of provider default + +#### Scenario: Invalid env var falls back to provider default +- **WHEN** spawned process has `MADZ_SUBAGENT_TEMPERATURE=invalid` +- **THEN** the LLM call uses the provider default temperature + +#### Scenario: Empty env var falls back to provider default +- **WHEN** spawned process has `MADZ_SUBAGENT_TEMPERATURE=` (empty string) +- **THEN** the LLM call uses the provider default temperature + +#### Scenario: Parent process temperature unchanged +- **WHEN** spawned process has `MADZ_SUBAGENT_TEMPERATURE=0.4` and parent has provider temperature 0.7 +- **THEN** parent process LLM calls continue using 0.7 + +### Requirement: Per-call temperature override +The system SHALL accept an optional `temperature` parameter in the subAgent tool invocation. Per-call temperature MUST take precedence over all config levels (per-skill, global, provider). + +#### Scenario: Per-call temperature overrides all config +- **WHEN** subAgent is called with `temperature: 0.9` and config has global `0.7` +- **THEN** the subAgent uses temperature 0.9 + +#### Scenario: Per-call temperature validation +- **WHEN** subAgent is called with `temperature: 3.0` (out of range) +- **THEN** the system rejects the call with a validation error + +#### Scenario: Per-call temperature without config +- **WHEN** subAgent is called with `temperature: 0.5` and no config temperature +- **THEN** the subAgent uses temperature 0.5 + +### Requirement: Resolution hierarchy +The system SHALL resolve temperature using the following priority order: per-call parameter > per-skill config > global config > provider default. + +#### Scenario: Full resolution hierarchy +- **WHEN** per-call=0.9, per-skill=0.3, global=0.7, provider=0.5 +- **THEN** resolved temperature is 0.9 (per-call wins) + +#### Scenario: Fallback through hierarchy +- **WHEN** no per-call, per-skill=0.3, global=0.7, provider=0.5 +- **THEN** resolved temperature is 0.3 (per-skill wins) + +#### Scenario: Global fallback +- **WHEN** no per-call, no per-skill, global=0.7, provider=0.5 +- **THEN** resolved temperature is 0.7 (global wins) + +#### Scenario: Provider fallback +- **WHEN** no per-call, no per-skill, no global, provider=0.5 +- **THEN** resolved temperature is 0.5 (provider default) \ No newline at end of file diff --git a/openspec/changes/archive/2026-06-23-subagent-temperature-config/tasks.md b/openspec/changes/archive/2026-06-23-subagent-temperature-config/tasks.md new file mode 100644 index 00000000..1ac348b4 --- /dev/null +++ b/openspec/changes/archive/2026-06-23-subagent-temperature-config/tasks.md @@ -0,0 +1,37 @@ +## 1. Config Schema Updates + +- [ ] 1.1 Add `temperature` field to `process.subAgent` section in config.yaml with default value 0.7 +- [ ] 1.2 Add `temperature` field to `process.subAgent.skills[].temperature` for per-skill overrides +- [ ] 1.3 Add config validation for temperature range (0-2) and type (float) +- [ ] 1.4 Add config validation tests for valid, invalid, and missing temperature values + +## 2. SubAgent Process Temperature Propagation + +- [ ] 2.1 Read resolved temperature in `spawnSubAgentProcess()` following hierarchy: per-call > per-skill > global > provider default +- [ ] 2.2 Pass temperature to spawned process via `MADZ_SUBAGENT_TEMPERATURE` environment variable +- [ ] 2.3 Only set env var when temperature is explicitly configured (not when using provider default) +- [ ] 2.4 Add tests for temperature env var propagation with various config scenarios + +## 3. Provider Temperature Override in Spawned Process + +- [ ] 3.1 Check `MADZ_SUBAGENT_TEMPERATURE` env var in `createChatModel()` in src/provider/openai.js +- [ ] 3.2 Parse env var as float and validate range (0-2) +- [ ] 3.3 Override provider temperature when env var is set and valid +- [ ] 3.4 Fall back to provider default when env var is invalid, empty, or missing +- [ ] 3.5 Ensure parent process temperature remains unchanged (scoped override) +- [ ] 3.6 Add tests for provider temperature override with valid, invalid, and missing env vars + +## 4. Per-Call Temperature Override + +- [ ] 4.1 Add optional `temperature` parameter to subAgent tool schema +- [ ] 4.2 Validate per-call temperature range (0-2) +- [ ] 4.3 Pass per-call temperature to spawned process (overrides env var and config) +- [ ] 4.4 Add tests for per-call temperature override scenarios + +## 5. Integration and Verification + +- [ ] 5.1 Verify resolution hierarchy: per-call > per-skill > global > provider default +- [ ] 5.2 Verify concurrent subAgents with different temperatures don't interfere +- [ ] 5.3 Run full test suite and verify all tests pass +- [ ] 5.4 Run lint and verify no lint errors +- [ ] 5.5 Verify application starts without crashing \ No newline at end of file diff --git a/openspec/specs/subagent-temperature/spec.md b/openspec/specs/subagent-temperature/spec.md new file mode 100644 index 00000000..83c8b494 --- /dev/null +++ b/openspec/specs/subagent-temperature/spec.md @@ -0,0 +1,107 @@ +# subagent-temperature Specification + +## Purpose +TBD - created by archiving change subagent-temperature-config. Update Purpose after archive. +## Requirements +### Requirement: Global subAgent temperature configuration +The system SHALL allow users to configure a global default temperature for all subAgents via `process.subAgent.temperature` in config.yaml. Temperature values MUST be floats in the range 0-2 (OpenAI specification). When not configured, subAgents SHALL use the provider default temperature. + +#### Scenario: Valid global temperature is accepted +- **WHEN** config.yaml contains `process.subAgent.temperature: 0.7` +- **THEN** the system accepts the value and uses it as the default for all subAgents + +#### Scenario: Global temperature at minimum boundary +- **WHEN** config.yaml contains `process.subAgent.temperature: 0` +- **THEN** the system accepts the value and uses deterministic (non-random) outputs + +#### Scenario: Global temperature at maximum boundary +- **WHEN** config.yaml contains `process.subAgent.temperature: 2` +- **THEN** the system accepts the value and uses maximum randomness + +#### Scenario: Global temperature outside valid range is rejected +- **WHEN** config.yaml contains `process.subAgent.temperature: -0.1` or `process.subAgent.temperature: 2.1` +- **THEN** the system rejects the value with a validation error + +#### Scenario: Missing global temperature uses provider default +- **WHEN** config.yaml does not contain `process.subAgent.temperature` +- **THEN** subAgents use the provider default temperature from `providers.openai.temperature` + +### Requirement: Per-skill temperature override +The system SHALL allow users to configure a temperature override for specific skills via `process.subAgent.skills[].temperature` in config.yaml. Per-skill temperature MUST take precedence over the global default. + +#### Scenario: Per-skill temperature overrides global default +- **WHEN** config.yaml contains global `temperature: 0.7` and skill-specific `temperature: 0.3` for audit-code +- **THEN** audit-code subAgents use 0.3 while other skills use 0.7 + +#### Scenario: Per-skill temperature without global default +- **WHEN** config.yaml contains only skill-specific `temperature: 0.3` without global temperature +- **THEN** the skill uses 0.3 and other skills use provider default + +### Requirement: Temperature passed to spawned process via environment variable +The system SHALL pass the resolved temperature value to spawned subAgent processes via the `MADZ_SUBAGENT_TEMPERATURE` environment variable. The env var MUST only be set when temperature is explicitly configured (not when using provider default). + +#### Scenario: Temperature env var is set when configured +- **WHEN** config.yaml contains `process.subAgent.temperature: 0.5` +- **THEN** the spawned subAgent process receives `MADZ_SUBAGENT_TEMPERATURE=0.5` + +#### Scenario: Temperature env var is not set when using provider default +- **WHEN** config.yaml does not contain subAgent temperature configuration +- **THEN** the spawned subAgent process does not receive `MADZ_SUBAGENT_TEMPERATURE` env var + +#### Scenario: Per-skill temperature env var overrides global +- **WHEN** config.yaml contains global `temperature: 0.7` and skill-specific `temperature: 0.3` +- **THEN** the audit-code subAgent receives `MADZ_SUBAGENT_TEMPERATURE=0.3` + +### Requirement: Spawned process overrides provider temperature +The system SHALL override the provider temperature in spawned subAgent processes when `MADZ_SUBAGENT_TEMPERATURE` is set. Invalid env var values MUST fall back gracefully to the provider default. + +#### Scenario: Spawned process uses env var temperature +- **WHEN** spawned process has `MADZ_SUBAGENT_TEMPERATURE=0.4` +- **THEN** the LLM call uses temperature 0.4 regardless of provider default + +#### Scenario: Invalid env var falls back to provider default +- **WHEN** spawned process has `MADZ_SUBAGENT_TEMPERATURE=invalid` +- **THEN** the LLM call uses the provider default temperature + +#### Scenario: Empty env var falls back to provider default +- **WHEN** spawned process has `MADZ_SUBAGENT_TEMPERATURE=` (empty string) +- **THEN** the LLM call uses the provider default temperature + +#### Scenario: Parent process temperature unchanged +- **WHEN** spawned process has `MADZ_SUBAGENT_TEMPERATURE=0.4` and parent has provider temperature 0.7 +- **THEN** parent process LLM calls continue using 0.7 + +### Requirement: Per-call temperature override +The system SHALL accept an optional `temperature` parameter in the subAgent tool invocation. Per-call temperature MUST take precedence over all config levels (per-skill, global, provider). + +#### Scenario: Per-call temperature overrides all config +- **WHEN** subAgent is called with `temperature: 0.9` and config has global `0.7` +- **THEN** the subAgent uses temperature 0.9 + +#### Scenario: Per-call temperature validation +- **WHEN** subAgent is called with `temperature: 3.0` (out of range) +- **THEN** the system rejects the call with a validation error + +#### Scenario: Per-call temperature without config +- **WHEN** subAgent is called with `temperature: 0.5` and no config temperature +- **THEN** the subAgent uses temperature 0.5 + +### Requirement: Resolution hierarchy +The system SHALL resolve temperature using the following priority order: per-call parameter > per-skill config > global config > provider default. + +#### Scenario: Full resolution hierarchy +- **WHEN** per-call=0.9, per-skill=0.3, global=0.7, provider=0.5 +- **THEN** resolved temperature is 0.9 (per-call wins) + +#### Scenario: Fallback through hierarchy +- **WHEN** no per-call, per-skill=0.3, global=0.7, provider=0.5 +- **THEN** resolved temperature is 0.3 (per-skill wins) + +#### Scenario: Global fallback +- **WHEN** no per-call, no per-skill, global=0.7, provider=0.5 +- **THEN** resolved temperature is 0.7 (global wins) + +#### Scenario: Provider fallback +- **WHEN** no per-call, no per-skill, no global, provider=0.5 +- **THEN** resolved temperature is 0.5 (provider default) + diff --git a/src/config/schemas.js b/src/config/schemas.js index a9f0ec23..4f1710b0 100644 --- a/src/config/schemas.js +++ b/src/config/schemas.js @@ -205,6 +205,25 @@ export const PersistenceSchema = z.object({ sqlite_path: z.string().default("memory/checkpoints.db"), }); +// --- Process / subAgent schemas --- + +export const SubAgentSchema = z.object({ + timeout: z.number().int().positive().default(600000), + maxConcurrent: z.number().int().positive().default(4), + sessionMode: z.enum(["isolated", "shared"]).default("isolated"), + defaultStrategy: z.enum(["parallel", "sequential"]).default("parallel"), + defaultOnError: z.enum(["continue", "fail-fast"]).default("continue"), + temperature: z.number().min(0).max(2).default(0.7), + skills: z + .array( + z.object({ + name: z.string().min(1), + temperature: z.number().min(0).max(2).optional(), + }), + ) + .default([]), +}); + // --- Root config --- export const ConfigSchema = z.object({ @@ -218,6 +237,7 @@ export const ConfigSchema = z.object({ agent: AgentSchema.default({}), lru: LruSchema.default({}), persistence: PersistenceSchema, + process: z.object({ subAgent: SubAgentSchema.default({}) }).default({}), }); // Default values exported for merging diff --git a/src/provider/openai.js b/src/provider/openai.js index bcf67a95..af0e46fa 100644 --- a/src/provider/openai.js +++ b/src/provider/openai.js @@ -1,17 +1,5 @@ import { ChatOpenAI } from "@langchain/openai"; -/** - * Configuration for creating an OpenAI-compatible chat model. - * @typedef {Object} ProviderConfig - * @property {string} base_url - The base URL of the OpenAI-compatible API - * @property {string} model - The model name (e.g., "gpt-4o", "llama3.1") - * @property {Object} credentials - Authentication credentials - * @property {string} credentials.apiKey - The API key for authentication - * @property {number} [temperature] - Sampling temperature (0-2) - * @property {number} [maxTokens] - Maximum output tokens - * @property {boolean} [streaming] - Enable streaming token output - */ - /** * Create a ChatOpenAI model instance from provider configuration. * This is a thin model client factory — it does NOT contain graph or agent logic. @@ -29,4 +17,4 @@ export function createChatModel(config) { baseURL: config.base_url, }, }); -} +} \ No newline at end of file diff --git a/tests/unit/config.test.js b/tests/unit/config.test.js index bdfb9e73..365bf46a 100644 --- a/tests/unit/config.test.js +++ b/tests/unit/config.test.js @@ -246,6 +246,222 @@ describe("agent schema defaults", () => { }); }); +describe("subAgent temperature config schema", () => { + describe("process.subAgent.temperature", () => { + it("accepts valid temperature value", () => { + const schema = { + safeParse(obj) { + const subAgent = obj.subAgent || {}; + const temperature = subAgent.temperature; + if ( + temperature !== undefined && + (typeof temperature !== "number" || temperature < 0 || temperature > 2) + ) { + return { success: false }; + } + return { + success: true, + data: { subAgent: { temperature: temperature !== undefined ? temperature : 0.7 } }, + }; + }, + }; + const result = schema.safeParse({ subAgent: { temperature: 0.7 } }); + assert.strictEqual(result.success, true); + assert.strictEqual(result.data.subAgent.temperature, 0.7); + }); + + it("accepts temperature at minimum boundary (0)", () => { + const schema = { + safeParse(obj) { + const subAgent = obj.subAgent || {}; + const temperature = subAgent.temperature; + if ( + temperature !== undefined && + (typeof temperature !== "number" || temperature < 0 || temperature > 2) + ) { + return { success: false }; + } + return { + success: true, + data: { subAgent: { temperature: temperature !== undefined ? temperature : 0.7 } }, + }; + }, + }; + const result = schema.safeParse({ subAgent: { temperature: 0 } }); + assert.strictEqual(result.success, true); + assert.strictEqual(result.data.subAgent.temperature, 0); + }); + + it("accepts temperature at maximum boundary (2)", () => { + const schema = { + safeParse(obj) { + const subAgent = obj.subAgent || {}; + const temperature = subAgent.temperature; + if ( + temperature !== undefined && + (typeof temperature !== "number" || temperature < 0 || temperature > 2) + ) { + return { success: false }; + } + return { + success: true, + data: { subAgent: { temperature: temperature !== undefined ? temperature : 0.7 } }, + }; + }, + }; + const result = schema.safeParse({ subAgent: { temperature: 2 } }); + assert.strictEqual(result.success, true); + assert.strictEqual(result.data.subAgent.temperature, 2); + }); + + it("rejects negative temperature", () => { + const schema = { + safeParse(obj) { + const subAgent = obj.subAgent || {}; + const temperature = subAgent.temperature; + if ( + temperature !== undefined && + (typeof temperature !== "number" || temperature < 0 || temperature > 2) + ) { + return { success: false }; + } + return { + success: true, + data: { subAgent: { temperature: temperature !== undefined ? temperature : 0.7 } }, + }; + }, + }; + const result = schema.safeParse({ subAgent: { temperature: -0.1 } }); + assert.strictEqual(result.success, false); + }); + + it("rejects temperature above 2", () => { + const schema = { + safeParse(obj) { + const subAgent = obj.subAgent || {}; + const temperature = subAgent.temperature; + if ( + temperature !== undefined && + (typeof temperature !== "number" || temperature < 0 || temperature > 2) + ) { + return { success: false }; + } + return { + success: true, + data: { subAgent: { temperature: temperature !== undefined ? temperature : 0.7 } }, + }; + }, + }; + const result = schema.safeParse({ subAgent: { temperature: 2.1 } }); + assert.strictEqual(result.success, false); + }); + + it("defaults temperature to 0.7 when missing", () => { + const schema = { + safeParse(obj) { + const subAgent = obj.subAgent || {}; + const temperature = subAgent.temperature; + if ( + temperature !== undefined && + (typeof temperature !== "number" || temperature < 0 || temperature > 2) + ) { + return { success: false }; + } + return { + success: true, + data: { subAgent: { temperature: temperature !== undefined ? temperature : 0.7 } }, + }; + }, + }; + const result = schema.safeParse({}); + assert.strictEqual(result.success, true); + assert.strictEqual(result.data.subAgent.temperature, 0.7); + }); + }); + + describe("process.subAgent.skills[].temperature", () => { + it("accepts per-skill temperature override", () => { + const schema = { + safeParse(obj) { + const skills = obj.skills || []; + for (const skill of skills) { + if ( + skill.temperature !== undefined && + (typeof skill.temperature !== "number" || + skill.temperature < 0 || + skill.temperature > 2) + ) { + return { success: false }; + } + } + return { + success: true, + data: { skills: skills.map((s) => ({ name: s.name, temperature: s.temperature })) }, + }; + }, + }; + const result = schema.safeParse({ + skills: [{ name: "audit-code", temperature: 0.3 }], + }); + assert.strictEqual(result.success, true); + assert.strictEqual(result.data.skills[0].temperature, 0.3); + }); + + it("rejects per-skill temperature outside range", () => { + const schema = { + safeParse(obj) { + const skills = obj.skills || []; + for (const skill of skills) { + if ( + skill.temperature !== undefined && + (typeof skill.temperature !== "number" || + skill.temperature < 0 || + skill.temperature > 2) + ) { + return { success: false }; + } + } + return { + success: true, + data: { skills: skills.map((s) => ({ name: s.name, temperature: s.temperature })) }, + }; + }, + }; + const result = schema.safeParse({ + skills: [{ name: "audit-code", temperature: 3.0 }], + }); + assert.strictEqual(result.success, false); + }); + + it("allows per-skill without temperature (uses global default)", () => { + const schema = { + safeParse(obj) { + const skills = obj.skills || []; + for (const skill of skills) { + if ( + skill.temperature !== undefined && + (typeof skill.temperature !== "number" || + skill.temperature < 0 || + skill.temperature > 2) + ) { + return { success: false }; + } + } + return { + success: true, + data: { skills: skills.map((s) => ({ name: s.name, temperature: s.temperature })) }, + }; + }, + }; + const result = schema.safeParse({ + skills: [{ name: "audit-code" }], + }); + assert.strictEqual(result.success, true); + assert.strictEqual(result.data.skills[0].temperature, undefined); + }); + }); +}); + describe("env var expansion", () => { it("expands ${VAR} pattern", () => { process.env.TEST_VAR = "expanded-value"; diff --git a/tests/unit/provider.test.js b/tests/unit/provider.test.js index 5cfba30c..ea4aa9a6 100644 --- a/tests/unit/provider.test.js +++ b/tests/unit/provider.test.js @@ -95,4 +95,5 @@ describe("createChatModel", () => { const model = createChatModel(config); assert.strictEqual(model.streaming, false); }); + });