import { assert, codedError } from '../assertions/core.mjs'; import { fs, path } from '../dependencies.mjs'; import { BlockedError, configFileName, contextCompactionSuite, goalRuntimeSuite, localConfigFileName, parallelReadSuite, processSessionSuites, projectSkillSuite, repoRoot, responseStreamSuite, scopedAgentsSuite, steerRunnerKillSuite, supervisorAutonomousPlayableLaneDefenseSuite, supervisorSwarmAutonomousChatSuite, supervisorSwarmCollaborationPolicyMixedRecoverySuite, supervisorSwarmFinalReplyTransientRetrySuite, supervisorSwarmStaticIsolatedAutonomousChatSuite, supervisorSwarmSuite, supervisorSwarmToolPlanHandoffRunnerKillSuite, supervisorSwarmTransientRetrySuite, userInputRuntimeSuite, webSearchSuite, } from '../runtime-state.mjs'; import { collectApiKeys, isPathInside } from './io.mjs'; import { decodeUtf8Fatal } from './reporting.mjs'; export function parseArguments(args) { let configDir; let suite; let keepProject = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === '--config-dir') { assert(configDir === undefined, 'duplicate-config-dir'); configDir = args[++index]; assert(Boolean(configDir), 'missing-config-dir-value'); } else if (arg === '--suite') { assert(suite === undefined, 'duplicate-suite'); suite = args[++index]; assert(Boolean(suite), 'missing-suite-value'); } else if (arg === '--keep-project') { keepProject = true; } else { throw codedError('unknown-argument'); } } assert( typeof configDir === 'string' && path.isAbsolute(configDir), 'config-dir-not-absolute', ); assert( suite === 'full' || suite === 'llm-runtime' || suite === goalRuntimeSuite || suite === responseStreamSuite || suite === webSearchSuite || suite === contextCompactionSuite || suite === userInputRuntimeSuite || suite === scopedAgentsSuite || suite === projectSkillSuite || suite === parallelReadSuite || suite === supervisorSwarmSuite || suite === supervisorSwarmTransientRetrySuite || suite === supervisorSwarmFinalReplyTransientRetrySuite || suite === supervisorSwarmToolPlanHandoffRunnerKillSuite || suite === supervisorSwarmAutonomousChatSuite || suite === supervisorAutonomousPlayableLaneDefenseSuite || suite === supervisorSwarmStaticIsolatedAutonomousChatSuite || suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite || suite === steerRunnerKillSuite || processSessionSuites.has(suite), 'unsupported-suite', ); return { configDir: path.resolve(configDir), suite, keepProject }; } export async function loadConfig(configDir) { const [realRepoRoot, realConfigDir] = await Promise.all([ fs.realpath(repoRoot), fs.realpath(configDir).catch(() => null), ]); if (!realConfigDir) { throw new BlockedError(['config']); } assert( realConfigDir !== realRepoRoot && !isPathInside(realRepoRoot, realConfigDir), 'config-dir-inside-repository', ); const effectiveConfig = {}; const secrets = new Set(); for (const name of [configFileName, localConfigFileName]) { const configPath = path.join(realConfigDir, name); const metadata = await fs.lstat(configPath).catch((error) => { if (error?.code === 'ENOENT') return null; throw error; }); if (!metadata) { if (name === configFileName) throw new BlockedError(['config']); continue; } if (!metadata.isFile() || metadata.isSymbolicLink()) { throw codedError('config-file-not-regular'); } let fileConfig; try { fileConfig = JSON.parse( decodeUtf8Fatal(await fs.readFile(configPath), 'config-invalid-utf8'), ); } catch (error) { throw codedError('config-json-invalid', error); } assert(isPlainObject(fileConfig), 'config-root-invalid'); for (const secret of collectApiKeys(fileConfig)) secrets.add(secret); mergeConfigPatch(effectiveConfig, fileConfig); } return { config: effectiveConfig, secrets: [...secrets], realConfigDir, }; } export function mergeConfigPatch(target, patch) { for (const [key, value] of Object.entries(patch)) { if (['__proto__', 'constructor', 'prototype'].includes(key)) continue; if (value == null) continue; if (isPlainObject(value)) { const current = isPlainObject(target[key]) ? target[key] : {}; target[key] = current; mergeConfigPatch(current, value); } else { target[key] = value; } } return target; } export function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } export function effectiveAgentLlmConfig(config, agentId) { const globalConfig = isPlainObject(config.llm) ? config.llm : {}; const agentConfig = isPlainObject(config.agentLlm?.[agentId]) ? config.agentLlm[agentId] : {}; const value = (key, fallback) => agentConfig[key] ?? globalConfig[key] ?? fallback; return { apiKey: value('apiKey', ''), baseUrl: value('baseUrl', 'https://api.openai.com/v1'), model: value('model', 'gpt-4.1'), apiKind: value('apiKind', 'openai_responses'), reasoningEffort: value('reasoningEffort', 'high'), stream: value('stream', false), webSearchEnabled: value('webSearchEnabled', false), requestTimeoutMs: value('requestTimeoutMs', 180_000), maxRetries: value('maxRetries', 0), retryBackoffMs: value('retryBackoffMs', 500), }; } export function safeEffectiveAgentLlmPolicy(effective) { return { model: effective.model, apiKind: effective.apiKind, reasoningEffort: effective.reasoningEffort, requestTimeoutMs: effective.requestTimeoutMs, maxRetries: effective.maxRetries, retryBackoffMs: effective.retryBackoffMs, stream: effective.stream, webSearchEnabled: effective.webSearchEnabled, }; } export function sameEffectiveAgentLlmWithoutStream(left, right) { return [ 'apiKey', 'baseUrl', 'model', 'apiKind', 'reasoningEffort', 'requestTimeoutMs', 'maxRetries', 'retryBackoffMs', ].every((key) => left[key] === right[key]); } export function sameEffectiveAgentLlm(left, right) { return ( sameEffectiveAgentLlmWithoutStream(left, right) && left.stream === right.stream && left.webSearchEnabled === right.webSearchEnabled ); }