From 92f4672e9a0f59ba62c77e732d997e27039e71e1 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Tue, 23 Jun 2026 08:29:22 -0400 Subject: [PATCH 1/3] feat: allow configuring subAgent temperature from config.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add temperature configuration for subAgents via config.yaml with: - Global default under process.subAgent.temperature - Per-skill override under process.subAgent.skills[].temperature - Environment variable propagation via MADZ_SUBAGENT_TEMPERATURE - Optional per-call temperature parameter in subAgent tool schema - Resolution hierarchy: per-call > per-skill > global > provider default Follows existing subAgent config patterns (timeout, maxConcurrent). All changes are backward compatible — existing configs work unchanged. --- .../.openspec.yaml | 2 + .../subagent-temperature-config/design.md | 82 ++++++++++++++ .../subagent-temperature-config/proposal.md | 27 +++++ .../specs/subagent-temperature/spec.md | 103 ++++++++++++++++++ .../subagent-temperature-config/tasks.md | 37 +++++++ 5 files changed, 251 insertions(+) create mode 100644 openspec/changes/subagent-temperature-config/.openspec.yaml create mode 100644 openspec/changes/subagent-temperature-config/design.md create mode 100644 openspec/changes/subagent-temperature-config/proposal.md create mode 100644 openspec/changes/subagent-temperature-config/specs/subagent-temperature/spec.md create mode 100644 openspec/changes/subagent-temperature-config/tasks.md diff --git a/openspec/changes/subagent-temperature-config/.openspec.yaml b/openspec/changes/subagent-temperature-config/.openspec.yaml new file mode 100644 index 0000000..a4ac4d7 --- /dev/null +++ b/openspec/changes/subagent-temperature-config/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-23 diff --git a/openspec/changes/subagent-temperature-config/design.md b/openspec/changes/subagent-temperature-config/design.md new file mode 100644 index 0000000..1e5afb6 --- /dev/null +++ b/openspec/changes/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/subagent-temperature-config/proposal.md b/openspec/changes/subagent-temperature-config/proposal.md new file mode 100644 index 0000000..fff09c4 --- /dev/null +++ b/openspec/changes/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/subagent-temperature-config/specs/subagent-temperature/spec.md b/openspec/changes/subagent-temperature-config/specs/subagent-temperature/spec.md new file mode 100644 index 0000000..5a36e9b --- /dev/null +++ b/openspec/changes/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/subagent-temperature-config/tasks.md b/openspec/changes/subagent-temperature-config/tasks.md new file mode 100644 index 0000000..1ac348b --- /dev/null +++ b/openspec/changes/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 From 29020aaebaf4cd70bf70c89b0b27caf621767848 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Tue, 23 Jun 2026 08:56:15 -0400 Subject: [PATCH 2/3] feat: allow configuring subAgent temperature from config.yaml --- config.yaml | 1 + .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/subagent-temperature/spec.md | 0 .../tasks.md | 0 openspec/specs/subagent-temperature/spec.md | 107 +++++++++ src/config/schemas.js | 20 ++ src/provider/openai.js | 33 ++- src/tools/subAgent.js | 60 ++++- tests/unit/config.test.js | 216 ++++++++++++++++++ tests/unit/provider.test.js | 155 +++++++++++++ 12 files changed, 576 insertions(+), 16 deletions(-) rename openspec/changes/{subagent-temperature-config => archive/2026-06-23-subagent-temperature-config}/.openspec.yaml (100%) rename openspec/changes/{subagent-temperature-config => archive/2026-06-23-subagent-temperature-config}/design.md (100%) rename openspec/changes/{subagent-temperature-config => archive/2026-06-23-subagent-temperature-config}/proposal.md (100%) rename openspec/changes/{subagent-temperature-config => archive/2026-06-23-subagent-temperature-config}/specs/subagent-temperature/spec.md (100%) rename openspec/changes/{subagent-temperature-config => archive/2026-06-23-subagent-temperature-config}/tasks.md (100%) create mode 100644 openspec/specs/subagent-temperature/spec.md diff --git a/config.yaml b/config.yaml index c89bbb9..a521400 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/subagent-temperature-config/.openspec.yaml b/openspec/changes/archive/2026-06-23-subagent-temperature-config/.openspec.yaml similarity index 100% rename from openspec/changes/subagent-temperature-config/.openspec.yaml rename to openspec/changes/archive/2026-06-23-subagent-temperature-config/.openspec.yaml diff --git a/openspec/changes/subagent-temperature-config/design.md b/openspec/changes/archive/2026-06-23-subagent-temperature-config/design.md similarity index 100% rename from openspec/changes/subagent-temperature-config/design.md rename to openspec/changes/archive/2026-06-23-subagent-temperature-config/design.md diff --git a/openspec/changes/subagent-temperature-config/proposal.md b/openspec/changes/archive/2026-06-23-subagent-temperature-config/proposal.md similarity index 100% rename from openspec/changes/subagent-temperature-config/proposal.md rename to openspec/changes/archive/2026-06-23-subagent-temperature-config/proposal.md diff --git a/openspec/changes/subagent-temperature-config/specs/subagent-temperature/spec.md b/openspec/changes/archive/2026-06-23-subagent-temperature-config/specs/subagent-temperature/spec.md similarity index 100% rename from openspec/changes/subagent-temperature-config/specs/subagent-temperature/spec.md rename to openspec/changes/archive/2026-06-23-subagent-temperature-config/specs/subagent-temperature/spec.md diff --git a/openspec/changes/subagent-temperature-config/tasks.md b/openspec/changes/archive/2026-06-23-subagent-temperature-config/tasks.md similarity index 100% rename from openspec/changes/subagent-temperature-config/tasks.md rename to openspec/changes/archive/2026-06-23-subagent-temperature-config/tasks.md diff --git a/openspec/specs/subagent-temperature/spec.md b/openspec/specs/subagent-temperature/spec.md new file mode 100644 index 0000000..83c8b49 --- /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 a9f0ec2..4f1710b 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 bcf67a9..f4e17f8 100644 --- a/src/provider/openai.js +++ b/src/provider/openai.js @@ -1,16 +1,23 @@ 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 + * Parse and validate subAgent temperature from environment variable. + * @param {string} envValue - The MADZ_SUBAGENT_TEMPERATURE env var value + * @param {number} providerDefault - The provider's default temperature + * @returns {number} Validated temperature value */ +function parseSubAgentTemperature(envValue, providerDefault) { + if (envValue === undefined || envValue === "") { + return providerDefault; + } + + const parsed = parseFloat(envValue); + if (isNaN(parsed) || parsed < 0 || parsed > 2) { + return providerDefault; + } + + return parsed; +} /** * Create a ChatOpenAI model instance from provider configuration. @@ -19,9 +26,15 @@ import { ChatOpenAI } from "@langchain/openai"; * @returns {ChatOpenAI} A configured ChatOpenAI instance */ export function createChatModel(config) { + // Check for subAgent temperature override from spawned process env var + const subAgentTemp = parseSubAgentTemperature( + process.env.MADZ_SUBAGENT_TEMPERATURE, + config.temperature, + ); + return new ChatOpenAI({ model: config.model, - temperature: config.temperature, + temperature: subAgentTemp, maxTokens: config.maxTokens, apiKey: config.credentials.apiKey, streaming: config.streaming !== false, diff --git a/src/tools/subAgent.js b/src/tools/subAgent.js index 225a868..5371801 100644 --- a/src/tools/subAgent.js +++ b/src/tools/subAgent.js @@ -101,9 +101,10 @@ function msToSeconds(ms) { * @param {string} prompt - The full prompt (context ||| delegation) * @param {string} sessionsDir - Path to sessions directory * @param {number} timeout - Timeout in milliseconds + * @param {object} [temperatureConfig] - Temperature configuration { value: number, configured: boolean } * @returns {Promise<{ ok: boolean, result: string, error?: string, sessionId?: string }>} */ -export function spawnSubAgentProcess(prompt, sessionsDir, timeout) { +export function spawnSubAgentProcess(prompt, sessionsDir, timeout, temperatureConfig) { return new Promise((resolve) => { const sessionId = generateSessionId(); const timeoutSeconds = msToSeconds(timeout); @@ -115,6 +116,7 @@ export function spawnSubAgentProcess(prompt, sessionsDir, timeout) { env: { ...process.env, MADZ_SESSION_ID: sessionId, + ...(temperatureConfig?.configured ? { MADZ_SUBAGENT_TEMPERATURE: String(temperatureConfig.value) } : {}), }, }); @@ -179,9 +181,10 @@ export function spawnSubAgentProcess(prompt, sessionsDir, timeout) { * @param {"continue" | "fail-fast"} onError - Error handling strategy * @param {string} sessionsDir - Path to sessions directory * @param {number} timeout - Timeout in milliseconds + * @param {object} [temperatureConfig] - Temperature configuration { value: number, configured: boolean } * @returns {Promise<{ ok: boolean, result: string, error?: string }>} */ -async function executeFanOut(tasks, strategy, maxConcurrent, onError, sessionsDir, timeout) { +async function executeFanOut(tasks, strategy, maxConcurrent, onError, sessionsDir, timeout, temperatureConfig) { const results = []; let failed = false; @@ -190,7 +193,7 @@ async function executeFanOut(tasks, strategy, maxConcurrent, onError, sessionsDi if (failed && onError === "fail-fast") break; const prompt = task.context ? `${task.context}\n\n${task.delegation}` : task.delegation; - const result = await spawnSubAgentProcess(prompt, sessionsDir, timeout); + const result = await spawnSubAgentProcess(prompt, sessionsDir, timeout, temperatureConfig); if (task.id) { results.push({ id: task.id, ...result }); @@ -213,7 +216,7 @@ async function executeFanOut(tasks, strategy, maxConcurrent, onError, sessionsDi const task = queue.shift(); const promise = (async () => { const prompt = task.context ? `${task.context}\n\n${task.delegation}` : task.delegation; - const result = await spawnSubAgentProcess(prompt, sessionsDir, timeout); + const result = await spawnSubAgentProcess(prompt, sessionsDir, timeout, temperatureConfig); if (task.id) { results.push({ id: task.id, ...result }); @@ -273,6 +276,39 @@ function resolveTimeout(perCallTimeout, config) { return 600000; // Default 10 minutes } +/** + * Resolve temperature with priority: per-call > per-skill config > global config > provider default. + * Returns { value: number, configured: boolean } where configured indicates if temperature + * was explicitly set (true) or using provider default (false). + * @param {number | undefined} perCallTemperature - Per-call temperature parameter + * @param {string} skillName - Name of the skill being executed (for per-skill lookup) + * @param {object} config - Resolved config object + * @returns {{ value: number, configured: boolean }} Resolved temperature and whether it was explicitly configured + */ +function resolveTemperature(perCallTemperature, skillName, config) { + // Per-call override + if (perCallTemperature !== undefined && perCallTemperature !== null) { + return { value: perCallTemperature, configured: true }; + } + + // Per-skill config + if (skillName && config?.process?.subAgent?.skills) { + const skillConfig = config.process.subAgent.skills.find((s) => s.name === skillName); + if (skillConfig?.temperature !== undefined) { + return { value: skillConfig.temperature, configured: true }; + } + } + + // Global config + const globalTemp = config?.process?.subAgent?.temperature; + if (globalTemp !== undefined && globalTemp !== null) { + return { value: globalTemp, configured: true }; + } + + // Provider default — not explicitly configured + return { value: undefined, configured: false }; +} + /** * Create a subAgent tool with runtime options. * @param {object} options - Runtime options @@ -286,11 +322,14 @@ export function createSubAgentTool(options = {}) { return tool( async (input) => { try { - const { delegation, context, tasks, strategy, maxConcurrent, onError, returnParams, timeout } = input; + const { delegation, context, tasks, strategy, maxConcurrent, onError, returnParams, timeout, temperature } = input; // Resolve timeout const resolvedTimeout = resolveTimeout(timeout, config); + // Resolve temperature: per-call > per-skill > global > provider default + const resolvedTemperature = resolveTemperature(temperature, undefined, config); + // Fan-out mode if (tasks && Array.isArray(tasks) && tasks.length > 0) { const fanOutStrategy = strategy || config?.process?.subAgent?.defaultStrategy || "parallel"; @@ -304,6 +343,7 @@ export function createSubAgentTool(options = {}) { fanOutOnError, sessionsDir, resolvedTimeout, + resolvedTemperature, ); // Apply returnParams filtering if specified @@ -327,7 +367,7 @@ export function createSubAgentTool(options = {}) { } const prompt = context ? `${context}\n\n${delegation}` : delegation; - const result = await spawnSubAgentProcess(prompt, sessionsDir, resolvedTimeout); + const result = await spawnSubAgentProcess(prompt, sessionsDir, resolvedTimeout, resolvedTemperature); // Apply returnParams filtering if specified if (returnParams && returnParams.length > 0 && result.ok) { @@ -409,6 +449,14 @@ export function createSubAgentTool(options = {}) { .describe( "Timeout in milliseconds for this sub-agent execution. Overrides MADZ_SUBAGENT_TIMEOUT env var and config default.", ), + temperature: z + .number() + .min(0) + .max(2) + .optional() + .describe( + "Temperature for this sub-agent execution (0-2, OpenAI spec). Overrides per-skill, global config, and provider default.", + ), }), }, ); diff --git a/tests/unit/config.test.js b/tests/unit/config.test.js index bdfb9e7..365bf46 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 5cfba30..edf5d07 100644 --- a/tests/unit/provider.test.js +++ b/tests/unit/provider.test.js @@ -95,4 +95,159 @@ describe("createChatModel", () => { const model = createChatModel(config); assert.strictEqual(model.streaming, false); }); + + describe("subAgent temperature override", () => { + it("overrides provider temperature when MADZ_SUBAGENT_TEMPERATURE is set", () => { + const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; + process.env.MADZ_SUBAGENT_TEMPERATURE = "0.3"; + + const config = { + model: "test", + temperature: 0.7, + maxTokens: 4096, + credentials: { apiKey: "sk-test" }, + base_url: "https://api.openai.com/v1", + }; + + const model = createChatModel(config); + assert.strictEqual(model.temperature, 0.3); + + // Clean up + delete process.env.MADZ_SUBAGENT_TEMPERATURE; + if (originalEnv !== undefined) { + process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; + } + }); + + it("falls back to provider default when MADZ_SUBAGENT_TEMPERATURE is invalid", () => { + const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; + process.env.MADZ_SUBAGENT_TEMPERATURE = "invalid"; + + const config = { + model: "test", + temperature: 0.7, + maxTokens: 4096, + credentials: { apiKey: "sk-test" }, + base_url: "https://api.openai.com/v1", + }; + + const model = createChatModel(config); + assert.strictEqual(model.temperature, 0.7); + + // Clean up + delete process.env.MADZ_SUBAGENT_TEMPERATURE; + if (originalEnv !== undefined) { + process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; + } + }); + + it("falls back to provider default when MADZ_SUBAGENT_TEMPERATURE is empty", () => { + const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; + process.env.MADZ_SUBAGENT_TEMPERATURE = ""; + + const config = { + model: "test", + temperature: 0.7, + maxTokens: 4096, + credentials: { apiKey: "sk-test" }, + base_url: "https://api.openai.com/v1", + }; + + const model = createChatModel(config); + assert.strictEqual(model.temperature, 0.7); + + // Clean up + delete process.env.MADZ_SUBAGENT_TEMPERATURE; + if (originalEnv !== undefined) { + process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; + } + }); + + it("falls back to provider default when MADZ_SUBAGENT_TEMPERATURE is out of range", () => { + const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; + process.env.MADZ_SUBAGENT_TEMPERATURE = "3.0"; + + const config = { + model: "test", + temperature: 0.7, + maxTokens: 4096, + credentials: { apiKey: "sk-test" }, + base_url: "https://api.openai.com/v1", + }; + + const model = createChatModel(config); + assert.strictEqual(model.temperature, 0.7); + + // Clean up + delete process.env.MADZ_SUBAGENT_TEMPERATURE; + if (originalEnv !== undefined) { + process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; + } + }); + + it("uses provider default when MADZ_SUBAGENT_TEMPERATURE is not set", () => { + const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; + delete process.env.MADZ_SUBAGENT_TEMPERATURE; + + const config = { + model: "test", + temperature: 0.7, + maxTokens: 4096, + credentials: { apiKey: "sk-test" }, + base_url: "https://api.openai.com/v1", + }; + + const model = createChatModel(config); + assert.strictEqual(model.temperature, 0.7); + + // Restore + if (originalEnv !== undefined) { + process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; + } + }); + + it("accepts temperature at boundary 0", () => { + const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; + process.env.MADZ_SUBAGENT_TEMPERATURE = "0"; + + const config = { + model: "test", + temperature: 0.7, + maxTokens: 4096, + credentials: { apiKey: "sk-test" }, + base_url: "https://api.openai.com/v1", + }; + + const model = createChatModel(config); + assert.strictEqual(model.temperature, 0); + + // Clean up + delete process.env.MADZ_SUBAGENT_TEMPERATURE; + if (originalEnv !== undefined) { + process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; + } + }); + + it("accepts temperature at boundary 2", () => { + const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; + process.env.MADZ_SUBAGENT_TEMPERATURE = "2"; + + const config = { + model: "test", + temperature: 0.7, + maxTokens: 4096, + credentials: { apiKey: "sk-test" }, + base_url: "https://api.openai.com/v1", + }; + + const model = createChatModel(config); + assert.strictEqual(model.temperature, 2); + + // Clean up + delete process.env.MADZ_SUBAGENT_TEMPERATURE; + if (originalEnv !== undefined) { + process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; + } + }); + }); }); From 78ac9637672532046ca2397b6e29977b471b6a3a Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Wed, 24 Jun 2026 19:36:29 -0400 Subject: [PATCH 3/3] feat: remove SUB_AGENT_TEMPERATURE env var, use config.yaml for subAgent temperature --- src/provider/openai.js | 29 +------ src/tools/subAgent.js | 64 ++------------- tests/unit/provider.test.js | 154 ------------------------------------ 3 files changed, 9 insertions(+), 238 deletions(-) diff --git a/src/provider/openai.js b/src/provider/openai.js index f4e17f8..af0e46f 100644 --- a/src/provider/openai.js +++ b/src/provider/openai.js @@ -1,24 +1,5 @@ import { ChatOpenAI } from "@langchain/openai"; -/** - * Parse and validate subAgent temperature from environment variable. - * @param {string} envValue - The MADZ_SUBAGENT_TEMPERATURE env var value - * @param {number} providerDefault - The provider's default temperature - * @returns {number} Validated temperature value - */ -function parseSubAgentTemperature(envValue, providerDefault) { - if (envValue === undefined || envValue === "") { - return providerDefault; - } - - const parsed = parseFloat(envValue); - if (isNaN(parsed) || parsed < 0 || parsed > 2) { - return providerDefault; - } - - return parsed; -} - /** * Create a ChatOpenAI model instance from provider configuration. * This is a thin model client factory — it does NOT contain graph or agent logic. @@ -26,15 +7,9 @@ function parseSubAgentTemperature(envValue, providerDefault) { * @returns {ChatOpenAI} A configured ChatOpenAI instance */ export function createChatModel(config) { - // Check for subAgent temperature override from spawned process env var - const subAgentTemp = parseSubAgentTemperature( - process.env.MADZ_SUBAGENT_TEMPERATURE, - config.temperature, - ); - return new ChatOpenAI({ model: config.model, - temperature: subAgentTemp, + temperature: config.temperature, maxTokens: config.maxTokens, apiKey: config.credentials.apiKey, streaming: config.streaming !== false, @@ -42,4 +17,4 @@ export function createChatModel(config) { baseURL: config.base_url, }, }); -} +} \ No newline at end of file diff --git a/src/tools/subAgent.js b/src/tools/subAgent.js index b7f7496..60408c7 100644 --- a/src/tools/subAgent.js +++ b/src/tools/subAgent.js @@ -101,10 +101,9 @@ function msToSeconds(ms) { * @param {string} prompt - The full prompt (context ||| delegation) * @param {string} sessionsDir - Path to sessions directory * @param {number} timeout - Timeout in milliseconds - * @param {object} [temperatureConfig] - Temperature configuration { value: number, configured: boolean } * @returns {Promise<{ ok: boolean, result: string, error?: string, sessionId?: string }>} */ -export function spawnSubAgentProcess(prompt, sessionsDir, timeout, temperatureConfig) { +export function spawnSubAgentProcess(prompt, sessionsDir, timeout) { return new Promise((resolve) => { const sessionId = generateSessionId(); const timeoutSeconds = msToSeconds(timeout); @@ -113,10 +112,7 @@ export function spawnSubAgentProcess(prompt, sessionsDir, timeout, temperatureCo // timeout sends SIGTERM first, then SIGKILL after --kill-after delay const child = spawn("timeout", ["--kill-after=10", timeoutSeconds.toString(), "node", "index.js", prompt, sessionsDir], { stdio: ["pipe", "pipe", "pipe"], -env: { - ...process.env, - ...(temperatureConfig?.configured ? { MADZ_SUBAGENT_TEMPERATURE: String(temperatureConfig.value) } : {}), - }, + env: process.env, }); const logPath = `/tmp/sub-agent-${sessionId}.log`; @@ -180,10 +176,9 @@ env: { * @param {"continue" | "fail-fast"} onError - Error handling strategy * @param {string} sessionsDir - Path to sessions directory * @param {number} timeout - Timeout in milliseconds - * @param {object} [temperatureConfig] - Temperature configuration { value: number, configured: boolean } * @returns {Promise<{ ok: boolean, result: string, error?: string }>} */ -async function executeFanOut(tasks, strategy, maxConcurrent, onError, sessionsDir, timeout, temperatureConfig) { +async function executeFanOut(tasks, strategy, maxConcurrent, onError, sessionsDir, timeout) { const results = []; let failed = false; @@ -192,7 +187,7 @@ async function executeFanOut(tasks, strategy, maxConcurrent, onError, sessionsDi if (failed && onError === "fail-fast") break; const prompt = task.context ? `${task.context}\n\n${task.delegation}` : task.delegation; - const result = await spawnSubAgentProcess(prompt, sessionsDir, timeout, temperatureConfig); + const result = await spawnSubAgentProcess(prompt, sessionsDir, timeout); if (task.id) { results.push({ id: task.id, ...result }); @@ -215,7 +210,7 @@ async function executeFanOut(tasks, strategy, maxConcurrent, onError, sessionsDi const task = queue.shift(); const promise = (async () => { const prompt = task.context ? `${task.context}\n\n${task.delegation}` : task.delegation; - const result = await spawnSubAgentProcess(prompt, sessionsDir, timeout, temperatureConfig); + const result = await spawnSubAgentProcess(prompt, sessionsDir, timeout); if (task.id) { results.push({ id: task.id, ...result }); @@ -270,39 +265,6 @@ function resolveTimeout(perCallTimeout, config) { return 600000; // Default 10 minutes } -/** - * Resolve temperature with priority: per-call > per-skill config > global config > provider default. - * Returns { value: number, configured: boolean } where configured indicates if temperature - * was explicitly set (true) or using provider default (false). - * @param {number | undefined} perCallTemperature - Per-call temperature parameter - * @param {string} skillName - Name of the skill being executed (for per-skill lookup) - * @param {object} config - Resolved config object - * @returns {{ value: number, configured: boolean }} Resolved temperature and whether it was explicitly configured - */ -function resolveTemperature(perCallTemperature, skillName, config) { - // Per-call override - if (perCallTemperature !== undefined && perCallTemperature !== null) { - return { value: perCallTemperature, configured: true }; - } - - // Per-skill config - if (skillName && config?.process?.subAgent?.skills) { - const skillConfig = config.process.subAgent.skills.find((s) => s.name === skillName); - if (skillConfig?.temperature !== undefined) { - return { value: skillConfig.temperature, configured: true }; - } - } - - // Global config - const globalTemp = config?.process?.subAgent?.temperature; - if (globalTemp !== undefined && globalTemp !== null) { - return { value: globalTemp, configured: true }; - } - - // Provider default — not explicitly configured - return { value: undefined, configured: false }; -} - /** * Create a subAgent tool with runtime options. * @param {object} options - Runtime options @@ -316,14 +278,11 @@ export function createSubAgentTool(options = {}) { return tool( async (input) => { try { - const { delegation, context, tasks, strategy, maxConcurrent, onError, returnParams, timeout, temperature } = input; + const { delegation, context, tasks, strategy, maxConcurrent, onError, returnParams, timeout } = input; // Resolve timeout const resolvedTimeout = resolveTimeout(timeout, config); - // Resolve temperature: per-call > per-skill > global > provider default - const resolvedTemperature = resolveTemperature(temperature, undefined, config); - // Fan-out mode if (tasks && Array.isArray(tasks) && tasks.length > 0) { const fanOutStrategy = strategy || config?.process?.subAgent?.defaultStrategy || "parallel"; @@ -337,7 +296,6 @@ export function createSubAgentTool(options = {}) { fanOutOnError, sessionsDir, resolvedTimeout, - resolvedTemperature, ); // Apply returnParams filtering if specified @@ -361,7 +319,7 @@ export function createSubAgentTool(options = {}) { } const prompt = context ? `${context}\n\n${delegation}` : delegation; - const result = await spawnSubAgentProcess(prompt, sessionsDir, resolvedTimeout, resolvedTemperature); + const result = await spawnSubAgentProcess(prompt, sessionsDir, resolvedTimeout); // Apply returnParams filtering if specified if (returnParams && returnParams.length > 0 && result.ok) { @@ -443,14 +401,6 @@ export function createSubAgentTool(options = {}) { .describe( "Timeout in milliseconds for this sub-agent execution. Overrides config default.", ), - temperature: z - .number() - .min(0) - .max(2) - .optional() - .describe( - "Temperature for this sub-agent execution (0-2, OpenAI spec). Overrides per-skill, global config, and provider default.", - ), }), }, ); diff --git a/tests/unit/provider.test.js b/tests/unit/provider.test.js index edf5d07..ea4aa9a 100644 --- a/tests/unit/provider.test.js +++ b/tests/unit/provider.test.js @@ -96,158 +96,4 @@ describe("createChatModel", () => { assert.strictEqual(model.streaming, false); }); - describe("subAgent temperature override", () => { - it("overrides provider temperature when MADZ_SUBAGENT_TEMPERATURE is set", () => { - const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; - process.env.MADZ_SUBAGENT_TEMPERATURE = "0.3"; - - const config = { - model: "test", - temperature: 0.7, - maxTokens: 4096, - credentials: { apiKey: "sk-test" }, - base_url: "https://api.openai.com/v1", - }; - - const model = createChatModel(config); - assert.strictEqual(model.temperature, 0.3); - - // Clean up - delete process.env.MADZ_SUBAGENT_TEMPERATURE; - if (originalEnv !== undefined) { - process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; - } - }); - - it("falls back to provider default when MADZ_SUBAGENT_TEMPERATURE is invalid", () => { - const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; - process.env.MADZ_SUBAGENT_TEMPERATURE = "invalid"; - - const config = { - model: "test", - temperature: 0.7, - maxTokens: 4096, - credentials: { apiKey: "sk-test" }, - base_url: "https://api.openai.com/v1", - }; - - const model = createChatModel(config); - assert.strictEqual(model.temperature, 0.7); - - // Clean up - delete process.env.MADZ_SUBAGENT_TEMPERATURE; - if (originalEnv !== undefined) { - process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; - } - }); - - it("falls back to provider default when MADZ_SUBAGENT_TEMPERATURE is empty", () => { - const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; - process.env.MADZ_SUBAGENT_TEMPERATURE = ""; - - const config = { - model: "test", - temperature: 0.7, - maxTokens: 4096, - credentials: { apiKey: "sk-test" }, - base_url: "https://api.openai.com/v1", - }; - - const model = createChatModel(config); - assert.strictEqual(model.temperature, 0.7); - - // Clean up - delete process.env.MADZ_SUBAGENT_TEMPERATURE; - if (originalEnv !== undefined) { - process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; - } - }); - - it("falls back to provider default when MADZ_SUBAGENT_TEMPERATURE is out of range", () => { - const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; - process.env.MADZ_SUBAGENT_TEMPERATURE = "3.0"; - - const config = { - model: "test", - temperature: 0.7, - maxTokens: 4096, - credentials: { apiKey: "sk-test" }, - base_url: "https://api.openai.com/v1", - }; - - const model = createChatModel(config); - assert.strictEqual(model.temperature, 0.7); - - // Clean up - delete process.env.MADZ_SUBAGENT_TEMPERATURE; - if (originalEnv !== undefined) { - process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; - } - }); - - it("uses provider default when MADZ_SUBAGENT_TEMPERATURE is not set", () => { - const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; - delete process.env.MADZ_SUBAGENT_TEMPERATURE; - - const config = { - model: "test", - temperature: 0.7, - maxTokens: 4096, - credentials: { apiKey: "sk-test" }, - base_url: "https://api.openai.com/v1", - }; - - const model = createChatModel(config); - assert.strictEqual(model.temperature, 0.7); - - // Restore - if (originalEnv !== undefined) { - process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; - } - }); - - it("accepts temperature at boundary 0", () => { - const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; - process.env.MADZ_SUBAGENT_TEMPERATURE = "0"; - - const config = { - model: "test", - temperature: 0.7, - maxTokens: 4096, - credentials: { apiKey: "sk-test" }, - base_url: "https://api.openai.com/v1", - }; - - const model = createChatModel(config); - assert.strictEqual(model.temperature, 0); - - // Clean up - delete process.env.MADZ_SUBAGENT_TEMPERATURE; - if (originalEnv !== undefined) { - process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; - } - }); - - it("accepts temperature at boundary 2", () => { - const originalEnv = process.env.MADZ_SUBAGENT_TEMPERATURE; - process.env.MADZ_SUBAGENT_TEMPERATURE = "2"; - - const config = { - model: "test", - temperature: 0.7, - maxTokens: 4096, - credentials: { apiKey: "sk-test" }, - base_url: "https://api.openai.com/v1", - }; - - const model = createChatModel(config); - assert.strictEqual(model.temperature, 2); - - // Clean up - delete process.env.MADZ_SUBAGENT_TEMPERATURE; - if (originalEnv !== undefined) { - process.env.MADZ_SUBAGENT_TEMPERATURE = originalEnv; - } - }); - }); });