02fff8f308
普通Web工作台默认使用单Supervisor自主生成链路 补齐安全默认返工、结构化诊断与同Run恢复 绑定当前revision、入口摘要和双视口试玩完成凭证 增强Runner跨boot单飞恢复与game-chat来源隔离 新增真实Provider验收入口及确定性回归 同步更新技术方案、开发流程和共享决策
204 lines
6.5 KiB
JavaScript
204 lines
6.5 KiB
JavaScript
import { assert, codedError } from '../assertions/core.mjs';
|
|
import { fs, path } from '../dependencies.mjs';
|
|
import {
|
|
BlockedError,
|
|
configFileName,
|
|
contextCompactionSuite,
|
|
goalRuntimeSuite,
|
|
localConfigFileName,
|
|
mcpRuntimeSuite,
|
|
parallelReadSuite,
|
|
processSessionSuites,
|
|
projectSkillSuite,
|
|
repoRoot,
|
|
responseStreamSuite,
|
|
scopedAgentsSuite,
|
|
steerRunnerKillSuite,
|
|
supervisorAutonomousPlayableLaneDefenseSuite,
|
|
supervisorGameChatSingleMainPlayableSuite,
|
|
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 === mcpRuntimeSuite ||
|
|
suite === userInputRuntimeSuite ||
|
|
suite === scopedAgentsSuite ||
|
|
suite === projectSkillSuite ||
|
|
suite === parallelReadSuite ||
|
|
suite === supervisorSwarmSuite ||
|
|
suite === supervisorSwarmTransientRetrySuite ||
|
|
suite === supervisorSwarmFinalReplyTransientRetrySuite ||
|
|
suite === supervisorSwarmToolPlanHandoffRunnerKillSuite ||
|
|
suite === supervisorSwarmAutonomousChatSuite ||
|
|
suite === supervisorAutonomousPlayableLaneDefenseSuite ||
|
|
suite === supervisorGameChatSingleMainPlayableSuite ||
|
|
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
|
|
);
|
|
}
|