From c214594fa1b18f2fc3a36b4ebe468c8bfbf8338e Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Tue, 21 Apr 2026 12:45:26 +0800 Subject: [PATCH] refactor(runtime): eliminate local agent process management and CLI probing - Refactor MultiAgentOrchestrator to remove local process management. - Disable local CLI execution and existence checks in orchestration workflows. - Remove local process discovery and state reconciliation in mount adapters. - Mark local agent process launching as deprecated/disabled across runtime. --- lib/runtime/codex_runtime.dart | 45 +--- lib/runtime/multi_agent_mounts.dart | 210 +++------------- .../multi_agent_orchestrator_core.dart | 48 +--- .../multi_agent_orchestrator_workflow.dart | 237 +----------------- 4 files changed, 45 insertions(+), 495 deletions(-) diff --git a/lib/runtime/codex_runtime.dart b/lib/runtime/codex_runtime.dart index 05c95ce8..f13e6af8 100644 --- a/lib/runtime/codex_runtime.dart +++ b/lib/runtime/codex_runtime.dart @@ -302,49 +302,8 @@ class CodexRuntime extends ChangeNotifier { CodexAccount? get account => _account; Stream get events => _events.stream; - /// Find Codex binary in PATH or common locations. - Future findCodexBinary() async { - // Check environment variable first - final envPath = Platform.environment['CODEX_PATH']; - if (envPath != null && envPath.isNotEmpty) { - final file = File(envPath); - if (await file.exists()) { - return envPath; - } - } - - // Try common locations - final paths = defaultCodexBinaryCandidates(); - - for (final path in paths) { - final file = File(path); - if (await file.exists()) { - return path; - } - } - - // Try to find via platform-native lookup. - try { - final result = await Process.run( - _lookupExecutableProgram(), - _lookupExecutableArguments(), - ); - if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout as String, - ).map((line) => line.trim()).where((line) => line.isNotEmpty); - for (final path in lines) { - if (await File(path).exists()) { - return path; - } - } - } - } catch (_) { - // Ignore - } - - return null; - } + /// Find Codex binary (DEPRECATED: Use bridge instead). + Future findCodexBinary() async => null; /// Start Codex App Server in stdio mode (DEPRECATED: Use bridge instead). Future startStdio({ diff --git a/lib/runtime/multi_agent_mounts.dart b/lib/runtime/multi_agent_mounts.dart index cdebdb59..b26998f9 100644 --- a/lib/runtime/multi_agent_mounts.dart +++ b/lib/runtime/multi_agent_mounts.dart @@ -6,6 +6,9 @@ import 'multi_agent_mount_resolver.dart'; import 'opencode_config_bridge.dart'; import 'runtime_models.dart'; +/// 协作模式挂载管理器 +/// +/// 在云中性设计下,挂载目标的发现与状态调和应通过桥接同步到远程端点。 class MultiAgentMountManager { MultiAgentMountManager({ CodexConfigBridge? codexConfigBridge, @@ -62,14 +65,13 @@ class MultiAgentMountManager { } Future _buildArisProbe() async { - // ARIS is legacy and has been removed from assets. return const ArisMountProbe( available: false, bundleVersion: '', llmChatServerPath: '', skillCount: 0, bridgeAvailable: false, - error: 'ARIS has been removed from application assets.', + error: 'Legacy local agent execution is disabled.', ); } @@ -79,29 +81,12 @@ class MultiAgentMountManager { }) async { final states = []; for (final adapter in _adapters) { - try { - states.add( - await adapter.reconcile( - config: config, - aiGatewayUrl: aiGatewayUrl, - ), - ); - } catch (error) { - states.add( - ManagedMountTargetState.placeholder( - targetId: adapter.targetId, - label: adapter.label, - supportsSkills: adapter.supportsSkills, - supportsMcp: adapter.supportsMcp, - supportsAiGatewayInjection: adapter.supportsAiGatewayInjection, - ).copyWith( - available: await adapter.isInstalled(), - discoveryState: 'error', - syncState: 'error', - detail: error.toString(), - ), - ); - } + states.add( + await adapter.reconcile( + config: config, + aiGatewayUrl: aiGatewayUrl, + ), + ); } return config.copyWith( mountTargets: states, @@ -125,36 +110,6 @@ abstract class CliMountAdapter { required String aiGatewayUrl, }); - Future _runCommand(List command) async { - final result = await Process.run( - command.first, - command.sublist(1), - runInShell: true, - ); - final stdout = '${result.stdout}'.trim(); - final stderr = '${result.stderr}'.trim(); - return stdout.isNotEmpty ? stdout : stderr; - } - - Future _countListedEntries(List command) async { - final output = await _runCommand(command); - if (output.isEmpty || - output.contains('No MCP servers configured') || - output.contains('No MCP servers configured yet') || - output.contains('No MCP servers configured.')) { - return 0; - } - return output - .split('\n') - .map((item) => item.trim()) - .where((item) => item.isNotEmpty) - .where((item) => !item.startsWith('Usage:')) - .where((item) => !item.startsWith('┌')) - .where((item) => !item.startsWith('│')) - .where((item) => !item.startsWith('└')) - .length; - } - int countMcpTomlSections(String content) { return RegExp( r'^\[mcp_servers\.[^\]]+\]', @@ -191,28 +146,6 @@ class CodexMountAdapter extends CliMountAdapter { required MultiAgentConfig config, required String aiGatewayUrl, }) async { - final available = await isInstalled(); - final configFile = File('${_bridge.codexHome}/config.toml'); - final content = await configFile.exists() - ? await configFile.readAsString() - : ''; - final discoveredMcpCount = countMcpTomlSections(content); - final managedMcpServers = config.managedMcpServers - .where((item) => item.enabled && item.command.trim().isNotEmpty) - .toList(growable: false); - if (available && config.autoSync && managedMcpServers.isNotEmpty) { - await _bridge.configureManagedMcpServers( - servers: managedMcpServers - .map( - (item) => CodexMcpServer( - name: item.id, - command: item.command, - args: item.args, - ), - ) - .toList(growable: false), - ); - } return ManagedMountTargetState.placeholder( targetId: targetId, label: label, @@ -220,18 +153,10 @@ class CodexMountAdapter extends CliMountAdapter { supportsMcp: supportsMcp, supportsAiGatewayInjection: supportsAiGatewayInjection, ).copyWith( - available: available, - discoveryState: available ? 'ready' : 'missing', - syncState: !available - ? 'missing' - : config.autoSync - ? 'ready' - : 'disabled', - discoveredMcpCount: discoveredMcpCount, - managedMcpCount: managedMcpServers.length, - detail: aiGatewayUrl.isNotEmpty - ? 'LLM API uses launch-scoped defaults for collaboration runs.' - : 'LLM API not configured.', + available: false, + discoveryState: 'missing', + syncState: 'missing', + detail: 'Local CLI interaction is disabled. Use bridge for orchestration.', ); } } @@ -260,10 +185,6 @@ class ClaudeMountAdapter extends CliMountAdapter { required MultiAgentConfig config, required String aiGatewayUrl, }) async { - final available = await isInstalled(); - final discoveredMcpCount = available - ? await _countListedEntries(['claude', 'mcp', 'list']) - : 0; return ManagedMountTargetState.placeholder( targetId: targetId, label: label, @@ -271,15 +192,10 @@ class ClaudeMountAdapter extends CliMountAdapter { supportsMcp: supportsMcp, supportsAiGatewayInjection: supportsAiGatewayInjection, ).copyWith( - available: available, - discoveryState: available ? 'ready' : 'missing', - syncState: available && config.autoSync ? 'launch-only' : 'disabled', - discoveredMcpCount: discoveredMcpCount, - managedMcpCount: config.managedMcpServers - .where((item) => item.enabled) - .length, - detail: - 'MCP discovery uses `claude mcp list`; LLM API stays launch-scoped.', + available: false, + discoveryState: 'missing', + syncState: 'disabled', + detail: 'Local CLI interaction is disabled.', ); } } @@ -308,10 +224,6 @@ class GeminiMountAdapter extends CliMountAdapter { required MultiAgentConfig config, required String aiGatewayUrl, }) async { - final available = await isInstalled(); - final discoveredMcpCount = available - ? await _countListedEntries(['gemini', 'mcp', 'list']) - : 0; return ManagedMountTargetState.placeholder( targetId: targetId, label: label, @@ -319,15 +231,10 @@ class GeminiMountAdapter extends CliMountAdapter { supportsMcp: supportsMcp, supportsAiGatewayInjection: supportsAiGatewayInjection, ).copyWith( - available: available, - discoveryState: available ? 'ready' : 'missing', - syncState: available && config.autoSync ? 'launch-only' : 'disabled', - discoveredMcpCount: discoveredMcpCount, - managedMcpCount: config.managedMcpServers - .where((item) => item.enabled) - .length, - detail: - 'MCP discovery uses `gemini mcp list`; LLM API stays launch-scoped.', + available: false, + discoveryState: 'missing', + syncState: 'disabled', + detail: 'Local CLI interaction is disabled.', ); } } @@ -360,26 +267,6 @@ class OpencodeMountAdapter extends CliMountAdapter { required MultiAgentConfig config, required String aiGatewayUrl, }) async { - final available = await isInstalled(); - final content = await _bridge.readConfig(); - final discoveredMcpCount = countMcpTomlSections(content); - final managedMcpServers = config.managedMcpServers - .where((item) => item.enabled) - .toList(growable: false); - if (available && config.autoSync && managedMcpServers.isNotEmpty) { - await _bridge.configureManagedMcpServers( - servers: managedMcpServers - .map( - (item) => OpencodeMcpServer( - name: item.id, - command: item.command, - url: item.url, - args: item.args, - ), - ) - .toList(growable: false), - ); - } return ManagedMountTargetState.placeholder( targetId: targetId, label: label, @@ -387,16 +274,10 @@ class OpencodeMountAdapter extends CliMountAdapter { supportsMcp: supportsMcp, supportsAiGatewayInjection: supportsAiGatewayInjection, ).copyWith( - available: available, - discoveryState: available ? 'ready' : 'missing', - syncState: !available - ? 'missing' - : config.autoSync - ? 'ready' - : 'disabled', - discoveredMcpCount: discoveredMcpCount, - managedMcpCount: managedMcpServers.length, - detail: 'Managed MCP config is preserved in ~/.opencode/config.toml.', + available: false, + discoveryState: 'missing', + syncState: 'missing', + detail: 'Local CLI interaction is disabled.', ); } } @@ -425,36 +306,6 @@ class OpenClawMountAdapter extends CliMountAdapter { required MultiAgentConfig config, required String aiGatewayUrl, }) async { - final available = await isInstalled(); - final configFile = File( - '${Platform.environment['HOME'] ?? ''}/.openclaw/openclaw.json', - ); - var discoveredSkillCount = 0; - var detail = 'OpenClaw acts as the host/control plane mount.'; - if (await configFile.exists()) { - try { - final decoded = jsonDecode(await configFile.readAsString()); - final agents = - (decoded is Map && - decoded['agents'] is Map && - (decoded['agents'] as Map)['list'] is List) - ? ((decoded['agents'] as Map)['list'] as List) - .length - : 0; - final skillsDir = Directory( - '${Platform.environment['HOME'] ?? ''}/.openclaw/skills', - ); - if (await skillsDir.exists()) { - discoveredSkillCount = await skillsDir - .list() - .where((entity) => entity is File || entity is Directory) - .length; - } - detail = 'agents: $agents · skills: $discoveredSkillCount'; - } catch (_) { - detail = 'OpenClaw config detected but could not be fully parsed.'; - } - } return ManagedMountTargetState.placeholder( targetId: targetId, label: label, @@ -462,11 +313,10 @@ class OpenClawMountAdapter extends CliMountAdapter { supportsMcp: supportsMcp, supportsAiGatewayInjection: supportsAiGatewayInjection, ).copyWith( - available: available, - discoveryState: available ? 'ready' : 'missing', - syncState: available && config.autoSync ? 'launch-only' : 'disabled', - discoveredSkillCount: discoveredSkillCount, - detail: detail, + available: false, + discoveryState: 'missing', + syncState: 'disabled', + detail: 'Local CLI interaction is disabled.', ); } } diff --git a/lib/runtime/multi_agent_orchestrator_core.dart b/lib/runtime/multi_agent_orchestrator_core.dart index 1d786413..35f11eff 100644 --- a/lib/runtime/multi_agent_orchestrator_core.dart +++ b/lib/runtime/multi_agent_orchestrator_core.dart @@ -14,39 +14,21 @@ import 'multi_agent_orchestrator_support.dart'; /// 多 Agent 协作编排器 /// /// 管理 Architect(调度/文档)→ Lead Engineer(主程)→ Worker/Review(并行 worker + 复审) -/// 的工作流,通过 Ollama 与外部 CLI 工具桥接首批云模型协作能力。 +/// 的工作流。 /// -/// 角色分工: -/// - Architect(调度/文档):负责任务分解、接受标准、工作流设计 -/// - Lead Engineer(主程):负责关键实现、重构、集成收口 -/// - Worker/Review(并行 worker):负责补充实现、复审、回归建议 +/// 在云中性设计下,编排逻辑应通过桥接转发到远程 ACP 端点执行。 class MultiAgentOrchestrator extends ChangeNotifier { MultiAgentOrchestrator({ required MultiAgentConfig config, - Future Function(String command)? binaryExistsResolver, HttpClient Function()? httpClientFactory, - CliProcessStarter? processStarter, }) : configInternal = config, - binaryExistsResolverInternal = binaryExistsResolver, - httpClientFactoryInternal = httpClientFactory ?? HttpClient.new, - processStarterInternal = - processStarter ?? - ((executable, arguments, {environment, workingDirectory}) { - return Process.start( - executable, - arguments, - environment: environment, - workingDirectory: workingDirectory, - ); - }); + httpClientFactoryInternal = httpClientFactory ?? HttpClient.new; /// 当前配置 MultiAgentConfig configInternal; MultiAgentConfig get config => configInternal; - final Future Function(String command)? binaryExistsResolverInternal; final HttpClient Function() httpClientFactoryInternal; - final CliProcessStarter processStarterInternal; - Process? activeCliProcessInternal; + HttpClient? activeHttpClientInternal; bool abortRequestedInternal = false; @@ -80,15 +62,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { Future abort() async { abortRequestedInternal = true; - final process = activeCliProcessInternal; - activeCliProcessInternal = null; - if (process != null) { - try { - process.kill(); - } catch (_) { - // Best effort only. - } - } final client = activeHttpClientInternal; activeHttpClientInternal = null; if (client != null) { @@ -100,16 +73,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { } } - void assertEmbeddedProcessesAllowedInternal() { - if (shouldBlockEmbeddedAgentLaunch( - isAppleHost: Platform.isIOS || Platform.isMacOS, - )) { - throw UnsupportedError( - 'App Store builds do not allow launching embedded multi-agent subprocesses.', - ); - } - } - /// 启用协作模式 void enable() { configInternal = configInternal.copyWith(enabled: true); @@ -135,8 +98,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { } /// 执行完整的协作工作流 - /// - /// 流程:Architect 分析 → Engineer 实现 → Tester 审阅 → 迭代(如需要) Future runCollaboration({ required String taskPrompt, required String workingDirectory, @@ -144,7 +105,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { List selectedSkills = const [], void Function(MultiAgentRunEvent event)? onEvent, }) async { - assertEmbeddedProcessesAllowedInternal(); if (isRunningInternal) { throw StateError('Collaboration is already running'); } diff --git a/lib/runtime/multi_agent_orchestrator_workflow.dart b/lib/runtime/multi_agent_orchestrator_workflow.dart index ea577c92..7657f518 100644 --- a/lib/runtime/multi_agent_orchestrator_workflow.dart +++ b/lib/runtime/multi_agent_orchestrator_workflow.dart @@ -269,7 +269,7 @@ $originalCode ); } - /// 通用的 CLI 进程执行方法 + /// 通用的 CLI 进程执行方法 (DEPRECATED: Use bridge instead) Future runCliPromptInternal({ required MultiAgentRole role, required String tool, @@ -277,167 +277,9 @@ $originalCode required String prompt, required String cwd, }) async { - late final List args; - late final String command; - late final Map envVars; - final useOllamaLaunch = prefersOllamaLaunchInternal( - tool: tool, - model: model, - ); - - switch (tool) { - case 'claude': - command = useOllamaLaunch ? 'ollama' : resolveCliPathInternal('claude'); - envVars = buildCliEnvVarsInternal(tool: tool); - if (useOllamaLaunch) { - args = buildOllamaLaunchArgsInternal( - tool: tool, - model: model, - prompt: prompt, - cwd: cwd, - ); - } else if (model.isNotEmpty) { - args = ['--model', model, '-p', prompt]; - } else { - args = ['-p', prompt]; - } - break; - - case 'codex': - command = useOllamaLaunch ? 'ollama' : resolveCliPathInternal('codex'); - envVars = buildCliEnvVarsInternal(tool: tool); - if (useOllamaLaunch) { - args = buildOllamaLaunchArgsInternal( - tool: tool, - model: model, - prompt: prompt, - cwd: cwd, - ); - } else if (model.isNotEmpty) { - args = [ - 'exec', - '--skip-git-repo-check', - '--color', - 'never', - if (cwd.isNotEmpty) ...['-C', cwd], - '-m', - model, - prompt, - ]; - } else { - args = [ - 'exec', - '--skip-git-repo-check', - '--color', - 'never', - if (cwd.isNotEmpty) ...['-C', cwd], - prompt, - ]; - } - break; - - case 'gemini': - command = resolveCliPathInternal('gemini'); - envVars = buildCliEnvVarsInternal(tool: tool); - if (model.isNotEmpty) { - args = ['--model', model, '-p', prompt]; - } else { - args = ['-p', prompt]; - } - break; - - case 'opencode': - command = useOllamaLaunch - ? 'ollama' - : resolveCliPathInternal('opencode'); - envVars = buildCliEnvVarsInternal(tool: tool); - args = useOllamaLaunch - ? buildOllamaLaunchArgsInternal( - tool: tool, - model: model, - prompt: prompt, - cwd: cwd, - ) - : [ - 'run', - '--format', - 'default', - if (cwd.isNotEmpty) ...['--dir', cwd], - if (model.isNotEmpty) ...['-m', model], - prompt, - ]; - break; - - default: - throw ArgumentError('Unknown tool: $tool'); - } - - final cliAvailable = await binaryExistsInternal(command); - if (configInternal.usesAris && !cliAvailable) { - return runArisFallbackInternal(role: role, model: model, prompt: prompt); - } - - try { - final process = await processStarterInternal( - command, - args, - environment: envVars, - workingDirectory: cwd.isNotEmpty ? cwd : null, - ); - activeCliProcessInternal = process; - - await process.stdin.close(); - - // 超时控制 - final timeout = Duration(seconds: configInternal.timeoutSeconds); - - final stdoutFuture = process.stdout - .transform(utf8.decoder) - .join() - .timeout( - timeout, - onTimeout: () { - process.kill(); - return '[超时或进程已终止]'; - }, - ); - - final stderrFuture = process.stderr - .transform(utf8.decoder) - .join() - .timeout(timeout, onTimeout: () => ''); - - final results = await Future.wait([stdoutFuture, stderrFuture]); - final exitCode = await process.exitCode.timeout( - timeout, - onTimeout: () => -1, - ); - activeCliProcessInternal = null; - - final cliResult = CliResult( - output: results[0], - error: results[1], - exitCode: exitCode, - ); - if (configInternal.usesAris && !cliResult.success) { - return runArisFallbackInternal( - role: role, - model: model, - prompt: prompt, - ); - } - return cliResult; - } catch (e) { - activeCliProcessInternal = null; - if (configInternal.usesAris) { - return runArisFallbackInternal( - role: role, - model: model, - prompt: prompt, - ); - } - return CliResult(output: '', error: e.toString(), exitCode: -1); - } + // In cloud-neutral architecture, local CLI execution is disabled. + // We should fallback to OpenAI compatible API or bridge execution. + return runArisFallbackInternal(role: role, model: model, prompt: prompt); } /// 构建 Architect 的 Prompt @@ -481,49 +323,6 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi MultiAgentRole role, String configuredTool, ) async { - if (!configInternal.usesAris) { - return configuredTool; - } - final configuredModel = resolvedModelForRoleInternal( - role, - configuredModel: modelForRoleInternal(role).trim(), - ); - final candidates = switch (role) { - MultiAgentRole.architect => [ - configuredTool, - 'claude', - 'codex', - 'opencode', - 'gemini', - ], - MultiAgentRole.engineer => [ - configuredTool, - 'codex', - 'opencode', - 'claude', - 'gemini', - ], - MultiAgentRole.testerDoc => [ - configuredTool, - 'opencode', - 'codex', - 'claude', - 'gemini', - ], - }; - for (final candidate in candidates) { - final trimmed = candidate.trim(); - if (trimmed.isEmpty) { - continue; - } - if (prefersOllamaLaunchInternal(tool: trimmed, model: configuredModel)) { - if (await binaryExistsInternal('ollama')) { - return trimmed; - } - } else if (await binaryExistsInternal(resolveCliPathInternal(trimmed))) { - return trimmed; - } - } return configuredTool; } @@ -546,16 +345,7 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi } Future binaryExistsInternal(String command) async { - final resolver = binaryExistsResolverInternal; - if (resolver != null) { - return resolver(command); - } - final check = await Process.run( - Platform.isWindows ? 'where' : 'which', - [command], - runInShell: true, - ); - return check.exitCode == 0 && '${check.stdout}'.trim().isNotEmpty; + return false; } Future runArisFallbackInternal({ @@ -594,19 +384,10 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi required String model, required String prompt, }) async { - if (await binaryExistsInternal(resolveCliPathInternal('claude'))) { - return runCliPromptInternal( - role: MultiAgentRole.testerDoc, - tool: 'claude', - model: model, - prompt: prompt, - cwd: '', - ); - } - return CliResult( - output: '', - error: 'Claude CLI is unavailable for claude-review', - exitCode: -1, + return runArisFallbackInternal( + role: MultiAgentRole.testerDoc, + model: model, + prompt: prompt, ); }