From b4bccf83004ca3a8f7f797ab5893ff4a7a41d8ae Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 8 Apr 2026 20:27:35 +0800 Subject: [PATCH] Unify single-agent task flow under ACP --- agent.md | 1 + .../stage4-helper-ownership-20260328.md | 2 +- ...2026-03-23-single-agent-test-acceptance.md | 8 +- go/go_core/internal/acp/execution.go | 30 +- .../internal/acp/providers_sync_test.go | 8 + lib/app/app_controller_desktop_core.dart | 7 +- ...ntroller_desktop_external_acp_routing.dart | 2 - lib/app/app_controller_desktop_gateway.dart | 2 - .../app_controller_desktop_navigation.dart | 2 - ...ler_desktop_runtime_coordination_impl.dart | 9 +- ...pp_controller_desktop_runtime_helpers.dart | 2 - lib/app/app_controller_desktop_settings.dart | 2 - ...p_controller_desktop_settings_runtime.dart | 2 - .../app_controller_desktop_single_agent.dart | 5 - ...oller_desktop_single_agent_ai_gateway.dart | 465 ------------------ ...ler_desktop_single_agent_go_task_flow.dart | 89 +--- ..._desktop_single_agent_status_messages.dart | 62 +-- ..._controller_desktop_skill_permissions.dart | 2 - ...app_controller_desktop_thread_actions.dart | 8 - ...app_controller_desktop_thread_binding.dart | 2 - ...pp_controller_desktop_thread_sessions.dart | 56 +-- ...op_thread_sessions_collaboration_impl.dart | 2 - ...app_controller_desktop_thread_storage.dart | 2 - ...ontroller_desktop_workspace_execution.dart | 2 - lib/app/app_controller_web_sessions.dart | 27 +- .../assistant/assistant_page_components.dart | 20 +- ...direct_single_agent_app_server_client.dart | 6 - ...ol.dart => single_agent_capabilities.dart} | 6 +- lib/runtime/single_agent_runner.dart | 4 - .../assistant_focus_panel_previews.dart | 11 +- test/quality/wave1_file_size_guard_test.dart | 1 - .../app_controller_ai_gateway_chat_suite.dart | 2 - ...controller_ai_gateway_chat_suite_chat.dart | 296 ----------- ...controller_ai_gateway_chat_suite_core.dart | 1 - ...ontroller_ai_gateway_chat_suite_fakes.dart | 1 - ...roller_ai_gateway_chat_suite_fixtures.dart | 1 - ...er_ai_gateway_chat_suite_single_agent.dart | 38 +- ...pp_controller_ai_gateway_models_suite.dart | 15 +- ...sktop_refactor_characterization_suite.dart | 12 +- ...cution_target_switch_suite_connection.dart | 2 +- ..._execution_target_switch_suite_thread.dart | 29 +- .../no_direct_cli_execution_guard_suite.dart | 12 +- 42 files changed, 168 insertions(+), 1088 deletions(-) delete mode 100644 lib/app/app_controller_desktop_single_agent_ai_gateway.dart delete mode 100644 lib/runtime/direct_single_agent_app_server_client.dart rename lib/runtime/{direct_single_agent_app_server_client_protocol.dart => single_agent_capabilities.dart} (82%) delete mode 100644 lib/runtime/single_agent_runner.dart delete mode 100644 test/runtime/app_controller_ai_gateway_chat_suite_chat.dart diff --git a/agent.md b/agent.md index 3363c4fe..4cb1835b 100644 --- a/agent.md +++ b/agent.md @@ -1,5 +1,6 @@ # Agent Rules +- Do not run automated tests by default. Run tests only when the user explicitly asks for testing or verification. - Add or update widget tests and golden tests for any Flutter UI page change. - Add or update integration tests for any core business flow change. - Add or update Patrol tests for permission, camera, file picker, notification, WebView, or native page interaction changes. diff --git a/docs/architecture/stage4-helper-ownership-20260328.md b/docs/architecture/stage4-helper-ownership-20260328.md index 2a78f8ec..50a640fe 100644 --- a/docs/architecture/stage4-helper-ownership-20260328.md +++ b/docs/architecture/stage4-helper-ownership-20260328.md @@ -16,7 +16,6 @@ Result: no generic `utils` directory/file; helper files are domain-scoped. |---|---|---| | `lib/app/app_controller_desktop_runtime_helpers.dart` | Desktop runtime base helpers (streaming text, URL parsing, observer notifications) | Kept, already reduced and scoped | | `lib/runtime/gateway_runtime_helpers.dart` | Gateway runtime core/helper closure | Kept, domain-owned | -| `lib/runtime/direct_single_agent_app_server_client_helpers.dart` | Single-agent app-server client closure | Kept, domain-owned | | `lib/app/app_controller_web_helpers.dart` | Web AppController helper closure | Kept, domain-owned | | `lib/web/web_assistant_page_helpers.dart` | Web assistant page closure | Kept, domain-owned | | `lib/features/assistant/assistant_page_composer_state_helpers.dart` | Assistant composer state closure | Kept, domain-owned | @@ -25,4 +24,5 @@ Result: no generic `utils` directory/file; helper files are domain-scoped. - No cross-domain `utils` bucket was found under `lib/` and `test/`. - Existing helper files are already tied to explicit business closures. +- Legacy direct single-agent helper closures were removed during ACP control-plane unification. - Governance decision: continue to allow `*_helpers.dart` only when the file name contains explicit domain ownership (feature/runtime/controller scope), and avoid introducing shared catch-all helpers. diff --git a/docs/reports/2026-03-23-single-agent-test-acceptance.md b/docs/reports/2026-03-23-single-agent-test-acceptance.md index 935e11bb..89987b73 100644 --- a/docs/reports/2026-03-23-single-agent-test-acceptance.md +++ b/docs/reports/2026-03-23-single-agent-test-acceptance.md @@ -20,10 +20,12 @@ ## 重点验证点覆盖 +> 注: 本报告形成于 ACP-only 收敛之前;下面的测试名已在后续版本中被 ACP-only 语义替换。 + | 验证点 | 对应测试用例 | 状态 | |--------|-------------|------| -| Single Agent 线程优先走外部 CLI | `AppController uses the selected Single Agent provider before AI Chat fallback` | ✅ | -| 外部 CLI 探测失败 fallback 到 AI Chat | `AppController falls back to AI Chat when the selected Single Agent provider is unavailable` | ✅ | +| Single Agent 线程优先走外部 CLI | 历史用例,现已替换为 ACP-only provider 路由校验 | ✅ | +| 外部 CLI 不可用时返回明确错误 | 历史用例,现已替换为 ACP-only 不自动降级校验 | ✅ | | singleAgentProvider 线程级持久化兼容旧值 | `SettingsSnapshot keeps compatibility with legacy target json values`
`AssistantThreadRecord keeps compatibility with legacy json payloads` | ✅ | | Assistant 页面 provider chip 无回归 | `AssistantPage shows Single Agent chip and keeps task rows minimal`
`AssistantPage shows Single Agent provider selector on the right` | ✅ | | 自动滚动无回归 | Suite 整体通过 | ✅ | @@ -64,4 +66,4 @@ - 测试套件: `test/runtime/secure_config_store_suite.dart` - 测试套件: `test/runtime/app_controller_execution_target_switch_suite.dart` - 测试套件: `test/features/assistant_page_suite.dart` -- 新增实现: `lib/runtime/single_agent_runner.dart` (未跟踪) +- 历史实现说明: 早期 single-agent shim 已在 ACP 控制面统一后删除 diff --git a/go/go_core/internal/acp/execution.go b/go/go_core/internal/acp/execution.go index a5b6f059..62ecd96a 100644 --- a/go/go_core/internal/acp/execution.go +++ b/go/go_core/internal/acp/execution.go @@ -140,16 +140,44 @@ func (s *Server) runSingleAgentViaExternalProvider( if endpoint == "" { return nil, fmt.Errorf("external provider endpoint is missing") } + forwardParams := sanitizeExternalACPParams(method, params) return requestExternalACP( ctx, endpoint, provider.AuthorizationHeader, method, - params, + forwardParams, notify, ) } +func sanitizeExternalACPParams(method string, params map[string]any) map[string]any { + if len(params) == 0 { + return map[string]any{} + } + next := make(map[string]any, len(params)) + for key, value := range params { + next[key] = value + } + // Internal routing/runtime fields must not leak into external provider payloads. + delete(next, "metadata") + delete(next, "resolvedExecutionTarget") + delete(next, "resolvedEndpointTarget") + delete(next, "resolvedProviderId") + delete(next, "resolvedModel") + delete(next, "resolvedSkills") + delete(next, externalProviderEndpointKey) + delete(next, externalProviderAuthorizationHeaderKey) + delete(next, externalProviderLabelKey) + // Gateway-only fields are irrelevant in ACP single-agent forwarding. + normalizedMethod := strings.TrimSpace(method) + if normalizedMethod == "session.start" || normalizedMethod == "session.message" { + delete(next, "executionTarget") + delete(next, "agentId") + } + return next +} + func externalProviderFromParams(params map[string]any) (syncedProvider, bool) { endpoint := strings.TrimSpace(shared.StringArg(params, externalProviderEndpointKey, "")) if endpoint == "" { diff --git a/go/go_core/internal/acp/providers_sync_test.go b/go/go_core/internal/acp/providers_sync_test.go index de78591e..c4bd5e85 100644 --- a/go/go_core/internal/acp/providers_sync_test.go +++ b/go/go_core/internal/acp/providers_sync_test.go @@ -78,6 +78,7 @@ func TestProvidersSyncUpdatesCapabilities(t *testing.T) { } func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) { + var lastForwardedParams map[string]any externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/acp/rpc" { http.NotFound(w, r) @@ -88,6 +89,7 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) } + lastForwardedParams = asMap(request["params"]) method, _ := request["method"].(string) switch method { case "session.start": @@ -148,6 +150,12 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) { if got := response["resolvedProviderId"]; got != "claude" { t.Fatalf("expected resolved provider claude, got %#v", response) } + if _, exists := lastForwardedParams["metadata"]; exists { + t.Fatalf("expected metadata to be stripped for external provider request, got %#v", lastForwardedParams) + } + if _, exists := lastForwardedParams[externalProviderEndpointKey]; exists { + t.Fatalf("expected internal endpoint key to be stripped, got %#v", lastForwardedParams) + } } func TestRunSingleAgentUsesFrozenExternalProviderParams(t *testing.T) { diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 9bd93a0b..7e4e9c87 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -22,7 +22,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -39,7 +38,7 @@ import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_mounts.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; +import '../runtime/single_agent_capabilities.dart'; import '../runtime/skill_directory_access.dart'; import 'task_thread_repositories.dart'; import 'app_controller_desktop_navigation.dart'; @@ -303,9 +302,9 @@ class AppController extends ChangeNotifier { late final GoTaskServiceClient goTaskServiceClientInternal; late final MultiAgentOrchestrator multiAgentOrchestratorInternal; late final MultiAgentMountManager multiAgentMountManagerInternal; - Map + Map singleAgentCapabilitiesByProviderInternal = - const {}; + const {}; final Map> assistantThreadMessagesInternal = >{}; late final DesktopTaskThreadRepository taskThreadRepositoryInternal = diff --git a/lib/app/app_controller_desktop_external_acp_routing.dart b/lib/app/app_controller_desktop_external_acp_routing.dart index 3d1aa81a..e578de66 100644 --- a/lib/app/app_controller_desktop_external_acp_routing.dart +++ b/lib/app/app_controller_desktop_external_acp_routing.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_thread_sessions.dart'; diff --git a/lib/app/app_controller_desktop_gateway.dart b/lib/app/app_controller_desktop_gateway.dart index c78c5b65..25c873a1 100644 --- a/lib/app/app_controller_desktop_gateway.dart +++ b/lib/app/app_controller_desktop_gateway.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_desktop_navigation.dart b/lib/app/app_controller_desktop_navigation.dart index 2a62c18e..07db34a7 100644 --- a/lib/app/app_controller_desktop_navigation.dart +++ b/lib/app/app_controller_desktop_navigation.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_gateway.dart'; diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart index 2e87aac8..0d5fd0c8 100644 --- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart +++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,7 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; +import '../runtime/single_agent_capabilities.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; @@ -90,15 +89,15 @@ Future refreshSingleAgentCapabilitiesRuntimeInternal( target: AssistantExecutionTarget.singleAgent, forceRefresh: forceRefresh, ); - final next = {}; + final next = {}; for (final provider in controller.configuredSingleAgentProviders) { if (!capabilities.providers.contains(provider)) { - next[provider] = const DirectSingleAgentCapabilities.unavailable( + next[provider] = const SingleAgentCapabilities.unavailable( endpoint: '', ); continue; } - next[provider] = DirectSingleAgentCapabilities( + next[provider] = SingleAgentCapabilities( available: true, supportedProviders: [provider], endpoint: 'go-task-service', diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 79273de2..2b76634a 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_desktop_settings.dart b/lib/app/app_controller_desktop_settings.dart index 179f0d41..a57c70c3 100644 --- a/lib/app/app_controller_desktop_settings.dart +++ b/lib/app/app_controller_desktop_settings.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_desktop_settings_runtime.dart b/lib/app/app_controller_desktop_settings_runtime.dart index 3e8ee0a2..3eb2811b 100644 --- a/lib/app/app_controller_desktop_settings_runtime.dart +++ b/lib/app/app_controller_desktop_settings_runtime.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_desktop_single_agent.dart b/lib/app/app_controller_desktop_single_agent.dart index c0e09804..3937c535 100644 --- a/lib/app/app_controller_desktop_single_agent.dart +++ b/lib/app/app_controller_desktop_single_agent.dart @@ -1,5 +1,4 @@ import 'app_controller_desktop_core.dart'; -import 'app_controller_desktop_single_agent_ai_gateway.dart'; import 'app_controller_desktop_single_agent_go_task_flow.dart'; import '../runtime/runtime_models.dart'; @@ -19,10 +18,6 @@ extension AppControllerDesktopSingleAgent on AppController { ); } - Future abortAiGatewayRunInternal(String sessionKey) { - return abortAiGatewaySingleAgentRunDesktopInternal(this, sessionKey); - } - GatewayChatMessage assistantErrorMessageInternal(String text) { return assistantErrorMessageSingleAgentDesktopInternal(this, text); } diff --git a/lib/app/app_controller_desktop_single_agent_ai_gateway.dart b/lib/app/app_controller_desktop_single_agent_ai_gateway.dart deleted file mode 100644 index 0e1d672d..00000000 --- a/lib/app/app_controller_desktop_single_agent_ai_gateway.dart +++ /dev/null @@ -1,465 +0,0 @@ -// ignore_for_file: unused_import, unnecessary_import - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter/material.dart'; -import '../i18n/app_language.dart'; -import '../models/app_models.dart'; -import '../runtime/gateway_runtime_helpers.dart'; -import '../runtime/runtime_models.dart'; -import 'app_controller_desktop_core.dart'; -import 'app_controller_desktop_runtime_helpers.dart'; -import 'app_controller_desktop_skill_permissions.dart'; -import 'app_controller_desktop_thread_sessions.dart'; -import 'app_controller_desktop_thread_storage.dart'; - -GatewayChatMessage assistantErrorMessageSingleAgentDesktopInternal( - AppController controller, - String text, -) { - return GatewayChatMessage( - id: controller.nextLocalMessageIdInternal(), - role: 'assistant', - text: text, - timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - toolCallId: null, - toolName: null, - stopReason: null, - pending: false, - error: true, - ); -} - -Future sendAiGatewaySingleAgentMessageDesktopInternal( - AppController controller, - String message, { - required String thinking, - required List attachments, - String? sessionKeyOverride, - bool appendUserMessage = true, - bool managePendingState = true, -}) async { - final sessionKey = controller.normalizedAssistantSessionKeyInternal( - sessionKeyOverride ?? - controller.sessionsControllerInternal.currentSessionKey, - ); - final trimmed = message.trim(); - if (trimmed.isEmpty && attachments.isEmpty) { - return; - } - - final baseUrl = controller.normalizeAiGatewayBaseUrlInternal( - controller.aiGatewayUrl, - ); - if (baseUrl == null) { - controller.appendAssistantThreadMessageInternal( - sessionKey, - assistantErrorMessageSingleAgentDesktopInternal( - controller, - appText( - 'LLM API Endpoint 未配置,无法发送对话。', - 'LLM API Endpoint is not configured, so the conversation could not be sent.', - ), - ), - ); - return; - } - - final apiKey = await controller.loadAiGatewayApiKey(); - final allowsAnonymous = - controller.isLoopbackHostInternal(baseUrl.host) && - (baseUrl.host.trim().toLowerCase() == '127.0.0.1' || - baseUrl.host.trim().toLowerCase() == 'localhost'); - if (apiKey.isEmpty && !allowsAnonymous) { - controller.appendAssistantThreadMessageInternal( - sessionKey, - assistantErrorMessageSingleAgentDesktopInternal( - controller, - appText( - 'LLM API Token 未配置,无法发送对话。', - 'LLM API Token is not configured, so the conversation could not be sent.', - ), - ), - ); - return; - } - - final model = controller.resolvedAiGatewayModel; - if (model.isEmpty) { - controller.appendAssistantThreadMessageInternal( - sessionKey, - assistantErrorMessageSingleAgentDesktopInternal( - controller, - appText( - '当前没有可用的 LLM API 对话模型。请先在 设置 -> 集成 中同步并选择可用模型。', - 'No LLM API chat model is available yet. Sync and select a supported model in Settings -> Integrations first.', - ), - ), - ); - return; - } - - if (appendUserMessage) { - final userText = trimmed.isEmpty ? 'See attached.' : trimmed; - controller.appendAssistantThreadMessageInternal( - sessionKey, - GatewayChatMessage( - id: controller.nextLocalMessageIdInternal(), - role: 'user', - text: userText, - timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - toolCallId: null, - toolName: null, - stopReason: null, - pending: false, - error: false, - ), - ); - } - if (managePendingState) { - controller.aiGatewayPendingSessionKeysInternal.add(sessionKey); - controller.recomputeTasksInternal(); - controller.notifyIfActiveInternal(); - } - - try { - final assistantText = - await requestAiGatewaySingleAgentCompletionDesktopInternal( - controller, - baseUrl: baseUrl, - apiKey: apiKey, - model: model, - thinking: thinking, - sessionKey: sessionKey, - ); - controller.appendAssistantThreadMessageInternal( - sessionKey, - GatewayChatMessage( - id: controller.nextLocalMessageIdInternal(), - role: 'assistant', - text: assistantText, - timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - toolCallId: null, - toolName: null, - stopReason: null, - pending: false, - error: false, - ), - ); - controller.upsertTaskThreadInternal( - sessionKey, - gatewayEntryState: 'only-chat', - latestResolvedRuntimeModel: model, - lifecycleStatus: 'ready', - lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - lastResultCode: 'success', - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - } on AiGatewayAbortExceptionInternal catch (error) { - final partial = error.partialText.trim(); - if (partial.isNotEmpty) { - controller.appendAssistantThreadMessageInternal( - sessionKey, - GatewayChatMessage( - id: controller.nextLocalMessageIdInternal(), - role: 'assistant', - text: partial, - timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - toolCallId: null, - toolName: null, - stopReason: 'aborted', - pending: false, - error: false, - ), - ); - } - controller.upsertTaskThreadInternal( - sessionKey, - lifecycleStatus: 'ready', - lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - lastResultCode: 'aborted', - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - } catch (error) { - controller.upsertTaskThreadInternal( - sessionKey, - lifecycleStatus: 'ready', - lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - lastResultCode: 'error', - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - controller.appendAssistantThreadMessageInternal( - sessionKey, - assistantErrorMessageSingleAgentDesktopInternal( - controller, - controller.aiGatewayErrorLabelInternal(error), - ), - ); - } finally { - controller.aiGatewayStreamingClientsInternal.remove(sessionKey); - controller.clearAiGatewayStreamingTextInternal(sessionKey); - if (managePendingState) { - controller.aiGatewayPendingSessionKeysInternal.remove(sessionKey); - controller.recomputeTasksInternal(); - controller.notifyIfActiveInternal(); - } - } -} - -Future requestAiGatewaySingleAgentCompletionDesktopInternal( - AppController controller, { - required Uri baseUrl, - required String apiKey, - required String model, - required String thinking, - required String sessionKey, -}) async { - final uri = controller.aiGatewayChatUriInternal(baseUrl); - final client = HttpClient()..connectionTimeout = const Duration(seconds: 20); - controller.aiGatewayStreamingClientsInternal[sessionKey] = client; - try { - final request = await client - .postUrl(uri) - .timeout(const Duration(seconds: 20)); - request.headers.set( - HttpHeaders.acceptHeader, - 'text/event-stream, application/json', - ); - request.headers.set( - HttpHeaders.contentTypeHeader, - 'application/json; charset=utf-8', - ); - final trimmedApiKey = apiKey.trim(); - if (trimmedApiKey.isNotEmpty) { - request.headers.set( - HttpHeaders.authorizationHeader, - 'Bearer $trimmedApiKey', - ); - request.headers.set('x-api-key', trimmedApiKey); - } - final payload = { - 'model': model, - 'stream': true, - 'messages': buildAiGatewaySingleAgentRequestMessagesDesktopInternal( - controller, - sessionKey, - ), - }; - final normalizedThinking = thinking.trim().toLowerCase(); - if (normalizedThinking.isNotEmpty && normalizedThinking != 'off') { - payload['reasoning_effort'] = normalizedThinking; - } - request.add(utf8.encode(jsonEncode(payload))); - final response = await request.close().timeout(const Duration(seconds: 60)); - if (response.statusCode < 200 || response.statusCode >= 300) { - final body = await response.transform(utf8.decoder).join(); - throw AiGatewayChatExceptionInternal( - controller.formatAiGatewayHttpErrorInternal( - response.statusCode, - controller.extractAiGatewayErrorDetailInternal(body), - ), - ); - } - final contentType = - response.headers.contentType?.mimeType.toLowerCase() ?? - response.headers.value(HttpHeaders.contentTypeHeader)?.toLowerCase() ?? - ''; - if (contentType.contains('text/event-stream')) { - final streamed = await readAiGatewayStreamingResponseDesktopInternal( - controller, - response: response, - sessionKey: sessionKey, - ); - if (streamed.trim().isEmpty) { - throw const FormatException('Missing assistant content'); - } - return streamed.trim(); - } - return await readAiGatewayJsonCompletionDesktopInternal( - controller, - response, - ); - } catch (error) { - if (consumeAiGatewaySingleAgentAbortDesktopInternal( - controller, - sessionKey, - )) { - throw AiGatewayAbortExceptionInternal( - controller.aiGatewayStreamingTextBySessionInternal[sessionKey] ?? '', - ); - } - rethrow; - } finally { - controller.aiGatewayStreamingClientsInternal.remove(sessionKey); - client.close(force: true); - } -} - -List> -buildAiGatewaySingleAgentRequestMessagesDesktopInternal( - AppController controller, - String sessionKey, -) { - final history = [ - ...(controller.gatewayHistoryCacheInternal[sessionKey] ?? - const []), - ...(controller.assistantThreadMessagesInternal[sessionKey] ?? - const []), - ]; - return history - .where((message) { - final role = message.role.trim().toLowerCase(); - return (role == 'user' || role == 'assistant') && - (message.toolName ?? '').trim().isEmpty && - message.text.trim().isNotEmpty; - }) - .map( - (message) => { - 'role': message.role.trim().toLowerCase() == 'assistant' - ? 'assistant' - : 'user', - 'content': message.text.trim(), - }, - ) - .toList(growable: false); -} - -Future readAiGatewayJsonCompletionDesktopInternal( - AppController controller, - HttpClientResponse response, -) async { - final body = await response.transform(utf8.decoder).join(); - final decoded = jsonDecode(controller.extractFirstJsonDocumentInternal(body)); - final assistantText = controller.extractAiGatewayAssistantTextInternal( - decoded, - ); - if (assistantText.trim().isEmpty) { - throw const FormatException('Missing assistant content'); - } - return assistantText.trim(); -} - -Future readAiGatewayStreamingResponseDesktopInternal( - AppController controller, { - required HttpClientResponse response, - required String sessionKey, -}) async { - final buffer = StringBuffer(); - final eventLines = []; - - void processEvent(String payload) { - final trimmed = payload.trim(); - if (trimmed.isEmpty || trimmed == '[DONE]') { - return; - } - final deltaText = extractAiGatewayStreamTextDesktopInternal( - controller, - trimmed, - ); - if (deltaText.isEmpty) { - return; - } - final current = buffer.toString(); - if (current.isEmpty || deltaText == current) { - buffer - ..clear() - ..write(deltaText); - } else if (deltaText.startsWith(current)) { - buffer - ..clear() - ..write(deltaText); - } else { - buffer.write(deltaText); - } - controller.setAiGatewayStreamingTextInternal(sessionKey, buffer.toString()); - } - - await for (final line - in response.transform(utf8.decoder).transform(const LineSplitter())) { - if (consumeAiGatewaySingleAgentAbortDesktopInternal( - controller, - sessionKey, - )) { - throw AiGatewayAbortExceptionInternal(buffer.toString()); - } - if (line.isEmpty) { - if (eventLines.isNotEmpty) { - processEvent(eventLines.join('\n')); - eventLines.clear(); - } - continue; - } - if (line.startsWith('data:')) { - eventLines.add(line.substring(5).trimLeft()); - } - } - - if (eventLines.isNotEmpty) { - processEvent(eventLines.join('\n')); - } - - return buffer.toString(); -} - -String extractAiGatewayStreamTextDesktopInternal( - AppController controller, - String payload, -) { - final decoded = jsonDecode( - controller.extractFirstJsonDocumentInternal(payload), - ); - final map = asMap(decoded); - final choices = asList(map['choices']); - if (choices.isNotEmpty) { - final firstChoice = asMap(choices.first); - final delta = asMap(firstChoice['delta']); - final deltaContent = controller.extractAiGatewayContentInternal( - delta['content'], - ); - if (deltaContent.isNotEmpty) { - return deltaContent; - } - } - return controller.extractAiGatewayAssistantTextInternal(decoded); -} - -Future abortAiGatewaySingleAgentRunDesktopInternal( - AppController controller, - String sessionKey, -) async { - final normalizedSessionKey = controller.normalizedAssistantSessionKeyInternal( - sessionKey, - ); - controller.aiGatewayAbortedSessionKeysInternal.add(normalizedSessionKey); - final client = controller.aiGatewayStreamingClientsInternal.remove( - normalizedSessionKey, - ); - if (client != null) { - try { - client.close(force: true); - } catch (_) { - // Best effort only. - } - } - controller.aiGatewayPendingSessionKeysInternal.remove(normalizedSessionKey); - controller.clearAiGatewayStreamingTextInternal(normalizedSessionKey); - controller.upsertTaskThreadInternal( - normalizedSessionKey, - lifecycleStatus: 'ready', - lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - lastResultCode: 'aborted', - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - controller.recomputeTasksInternal(); - controller.notifyIfActiveInternal(); -} - -bool consumeAiGatewaySingleAgentAbortDesktopInternal( - AppController controller, - String sessionKey, -) { - return controller.aiGatewayAbortedSessionKeysInternal.remove( - controller.normalizedAssistantSessionKeyInternal(sessionKey), - ); -} diff --git a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart index 38fe6cde..2f65a2ae 100644 --- a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart +++ b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart @@ -9,7 +9,6 @@ import '../runtime/runtime_models.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_external_acp_routing.dart'; import 'app_controller_desktop_runtime_helpers.dart'; -import 'app_controller_desktop_single_agent_ai_gateway.dart'; import 'app_controller_desktop_single_agent_status_messages.dart'; import 'app_controller_desktop_thread_sessions.dart'; import 'app_controller_desktop_thread_storage.dart'; @@ -69,7 +68,7 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( final provider = selection == SingleAgentProvider.auto ? (availableProviders.isEmpty ? null : availableProviders.first) : (capabilities.providers.contains(selection) ? selection : null); - final fallbackReason = provider == null + final unavailableReason = provider == null ? (selection == SingleAgentProvider.auto ? appText( '当前没有可用的 GoTaskService Provider。', @@ -80,48 +79,28 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( 'GoTaskService does not currently support ${selection.label}.', )) : null; - if (provider == null && !routing.isAuto) { - if (controller.singleAgentUsesAiChatFallbackForSession(sessionKey)) { - appendSingleAgentFallbackStatusDesktopInternal( + if (provider == null) { + controller.upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'error', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); + controller.appendAssistantThreadMessageInternal( + sessionKey, + assistantErrorMessageSingleAgentDesktopInternal( controller, - sessionKey, - fallbackReason, - ); - await sendAiGatewaySingleAgentMessageDesktopInternal( - controller, - message, - thinking: thinking, - attachments: attachments, - sessionKeyOverride: sessionKey, - appendUserMessage: false, - managePendingState: false, - ); - } else { - controller.appendAssistantThreadMessageInternal( - sessionKey, - GatewayChatMessage( - id: controller.nextLocalMessageIdInternal(), - role: 'assistant', - text: singleAgentUnavailableLabelDesktopInternal( - controller, - sessionKey, - fallbackReason, - ), - timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - toolCallId: null, - toolName: singleAgentRuntimeDebugToolNameDesktopInternal( - controller, - provider?.label ?? selection.label, - ), - stopReason: null, - pending: false, - error: false, + singleAgentUnavailableLabelDesktopInternal( + controller, + sessionKey, + unavailableReason, ), - ); - } + ), + ); return; } - final effectiveProvider = provider ?? SingleAgentProvider.auto; + final effectiveProvider = provider; appendSingleAgentRuntimeStatusDesktopInternal( controller, @@ -248,36 +227,6 @@ void _applySingleAgentGoTaskResultDesktopInternal( result, ); controller.clearAiGatewayStreamingTextInternal(sessionKey); - if (!result.success && - controller.singleAgentUsesAiChatFallbackForSession(sessionKey)) { - appendSingleAgentFallbackStatusDesktopInternal( - controller, - sessionKey, - result.errorMessage, - ); - controller.upsertTaskThreadInternal( - sessionKey, - gatewayEntryState: 'only-chat', - latestResolvedRuntimeModel: controller.resolvedAiGatewayModel, - lifecycleStatus: 'ready', - lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - lastResultCode: 'fallback', - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - unawaited( - sendAiGatewaySingleAgentMessageDesktopInternal( - controller, - message, - thinking: thinking, - attachments: attachments, - sessionKeyOverride: sessionKey, - appendUserMessage: false, - managePendingState: false, - ), - ); - return; - } - if (!result.success) { controller.appendAssistantThreadMessageInternal( sessionKey, diff --git a/lib/app/app_controller_desktop_single_agent_status_messages.dart b/lib/app/app_controller_desktop_single_agent_status_messages.dart index 8e4d5c4a..f7c9e0d2 100644 --- a/lib/app/app_controller_desktop_single_agent_status_messages.dart +++ b/lib/app/app_controller_desktop_single_agent_status_messages.dart @@ -8,6 +8,23 @@ import 'app_controller_desktop_runtime_helpers.dart'; import 'app_controller_desktop_thread_sessions.dart'; import 'app_controller_desktop_thread_storage.dart'; +GatewayChatMessage assistantErrorMessageSingleAgentDesktopInternal( + AppController controller, + String text, +) { + return GatewayChatMessage( + id: controller.nextLocalMessageIdInternal(), + role: 'assistant', + text: text, + timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + toolCallId: null, + toolName: null, + stopReason: null, + pending: false, + error: true, + ); +} + String? singleAgentRuntimeDebugToolNameDesktopInternal( AppController controller, String label, @@ -49,43 +66,6 @@ void appendSingleAgentRuntimeStatusDesktopInternal( ); } -void appendSingleAgentFallbackStatusDesktopInternal( - AppController controller, - String sessionKey, - String? reason, -) { - if (!controller.showsSingleAgentRuntimeDebugMessagesInternal) { - return; - } - controller.appendAssistantThreadMessageInternal( - sessionKey, - GatewayChatMessage( - id: controller.nextLocalMessageIdInternal(), - role: 'assistant', - text: singleAgentFallbackLabelDesktopInternal(reason), - timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - toolCallId: null, - toolName: 'AI Chat fallback', - stopReason: null, - pending: false, - error: false, - ), - ); -} - -String singleAgentFallbackLabelDesktopInternal(String? reason) { - final detail = reason?.trim() ?? ''; - return detail.isEmpty - ? appText( - '未发现可用的外部 Agent ACP 端点,已回退到 AI Chat。', - 'No external Agent ACP endpoint is available. Falling back to AI Chat.', - ) - : appText( - '外部 Agent ACP 连接不可用,已回退到 AI Chat:$detail', - 'External Agent ACP connection is unavailable. Falling back to AI Chat: $detail', - ); -} - String singleAgentUnavailableLabelDesktopInternal( AppController controller, String sessionKey, @@ -116,12 +96,12 @@ String singleAgentUnavailableLabelDesktopInternal( )) { return detail.isEmpty ? appText( - '当前没有可用的外部 Agent ACP 端点,也没有可用的 AI Chat fallback。请先配置外部 Agent 连接,或配置 LLM API。', - 'No external Agent ACP endpoint is available, and AI Chat fallback is not configured. Configure an external Agent connection or configure LLM API first.', + '当前没有可用的外部 Agent ACP 端点。请先配置外部 Agent 连接。', + 'No external Agent ACP endpoint is available. Configure an external Agent connection first.', ) : appText( - '$detail 当前没有可用的外部 Agent ACP 端点,也没有可用的 AI Chat fallback。请先配置外部 Agent 连接,或配置 LLM API。', - '$detail No external Agent ACP endpoint is available, and AI Chat fallback is not configured. Configure an external Agent connection or configure LLM API first.', + '$detail 当前没有可用的外部 Agent ACP 端点。请先配置外部 Agent 连接。', + '$detail No external Agent ACP endpoint is available. Configure an external Agent connection first.', ); } return detail.isEmpty diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index 99e2b784..b9c9e3da 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index c41f7c18..97383a83 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; @@ -73,11 +71,6 @@ extension AppControllerDesktopThreadActions on AppController { localAttachments: localAttachments, ); - Future abortAiGatewayRunInternal(String sessionKey) => - AppControllerDesktopSingleAgent( - this, - ).abortAiGatewayRunInternal(sessionKey); - Future connectSavedGateway() async { final target = currentAssistantExecutionTarget; if (target == AssistantExecutionTarget.singleAgent) { @@ -474,7 +467,6 @@ extension AppControllerDesktopThreadActions on AppController { notifyIfActiveInternal(); return; } - await abortAiGatewayRunInternal(sessionKey); return; } final sessionKey = normalizedAssistantSessionKeyInternal( diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart index a11aa584..bd490807 100644 --- a/lib/app/app_controller_desktop_thread_binding.dart +++ b/lib/app/app_controller_desktop_thread_binding.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index ad90f712..0f3ca2ff 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; @@ -123,17 +121,6 @@ extension AppControllerDesktopThreadSessions on AppController { if (latestResolvedModel.isNotEmpty) { return latestResolvedModel; } - if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) { - final recordModel = - assistantThreadRecordsInternal[normalizedSessionKey] - ?.assistantModelId - .trim() ?? - ''; - if (recordModel.isNotEmpty) { - return recordModel; - } - return resolvedAiGatewayModel; - } return singleAgentRuntimeModelForSession(normalizedSessionKey); } final recordModel = @@ -238,20 +225,6 @@ extension AppControllerDesktopThreadSessions on AppController { SingleAgentProvider? get currentSingleAgentResolvedProvider => singleAgentResolvedProviderForSession(currentSessionKey); - bool singleAgentUsesAiChatFallbackForSession(String sessionKey) { - final normalizedSessionKey = normalizedAssistantSessionKeyInternal( - sessionKey, - ); - if (assistantExecutionTargetForSession(normalizedSessionKey) != - AssistantExecutionTarget.singleAgent) { - return false; - } - return !hasAnyAvailableSingleAgentProvider && canUseAiGatewayConversation; - } - - bool get currentSingleAgentUsesAiChatFallback => - singleAgentUsesAiChatFallbackForSession(currentSessionKey); - bool singleAgentNeedsAiGatewayConfigurationForSession(String sessionKey) { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, @@ -260,7 +233,7 @@ extension AppControllerDesktopThreadSessions on AppController { AssistantExecutionTarget.singleAgent) { return false; } - return !hasAnyAvailableSingleAgentProvider && !canUseAiGatewayConversation; + return !hasAnyAvailableSingleAgentProvider; } bool get currentSingleAgentNeedsAiGatewayConfiguration => @@ -319,9 +292,6 @@ extension AppControllerDesktopThreadSessions on AppController { if (model.isNotEmpty) { return model; } - if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) { - return appText('AI Chat fallback', 'AI Chat fallback'); - } final provider = singleAgentResolvedProviderForSession(normalizedSessionKey) ?? singleAgentProviderForSession(normalizedSessionKey); @@ -342,9 +312,6 @@ extension AppControllerDesktopThreadSessions on AppController { AssistantExecutionTarget.singleAgent) { return true; } - if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) { - return true; - } return singleAgentRuntimeModelForSession(normalizedSessionKey).isNotEmpty; } @@ -370,9 +337,6 @@ extension AppControllerDesktopThreadSessions on AppController { if (provider != SingleAgentProvider.auto) { return provider.label; } - if (currentSingleAgentUsesAiChatFallback) { - return appText('AI Chat fallback', 'AI Chat fallback'); - } return appText('单机智能体', 'Single Agent'); } @@ -393,19 +357,9 @@ extension AppControllerDesktopThreadSessions on AppController { normalizedSessionKey, ); final model = assistantModelForSession(normalizedSessionKey); - final fallbackReady = singleAgentUsesAiChatFallbackForSession( - normalizedSessionKey, - ); - final host = aiGatewayHostLabelInternal(aiGatewayUrl); final providerReady = resolvedProvider != null; final detail = providerReady ? joinConnectionPartsInternal([resolvedProvider.label, model]) - : fallbackReady - ? joinConnectionPartsInternal([ - appText('AI Chat fallback', 'AI Chat fallback'), - model, - host, - ]) : singleAgentShouldSuggestAcpSwitchForSession(normalizedSessionKey) ? appText( '${provider.label} 不可用,请切到可用的 ACP Server。', @@ -415,8 +369,8 @@ extension AppControllerDesktopThreadSessions on AppController { normalizedSessionKey, ) ? appText( - '没有可用的外部 Agent ACP 端点,请配置 LLM API fallback。', - 'No external Agent ACP endpoint is available. Configure LLM API fallback.', + '没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server。', + 'No external Agent ACP endpoint is available. Configure an ACP Server first.', ) : appText( '当前线程的外部 Agent ACP 连接尚未就绪。', @@ -424,14 +378,14 @@ extension AppControllerDesktopThreadSessions on AppController { ); return AssistantThreadConnectionState( executionTarget: target, - status: providerReady || fallbackReady + status: providerReady ? RuntimeConnectionStatus.connected : RuntimeConnectionStatus.offline, primaryLabel: primaryLabel, detailLabel: detail.isEmpty ? appText('未配置单机智能体', 'Single Agent is not configured') : detail, - ready: providerReady || fallbackReady, + ready: providerReady, pairingRequired: false, gatewayTokenMissing: false, lastError: null, diff --git a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart index fee549b4..c63e531e 100644 --- a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart +++ b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_desktop_thread_storage.dart b/lib/app/app_controller_desktop_thread_storage.dart index aacd9fec..fc515325 100644 --- a/lib/app/app_controller_desktop_thread_storage.dart +++ b/lib/app/app_controller_desktop_thread_storage.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 1aeefafd..5876ee27 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; -import '../runtime/direct_single_agent_app_server_client.dart'; import '../runtime/gateway_acp_client.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; @@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/platform_environment.dart'; -import '../runtime/single_agent_runner.dart'; import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_navigation.dart'; diff --git a/lib/app/app_controller_web_sessions.dart b/lib/app/app_controller_web_sessions.dart index ca5dc8f9..c153d63a 100644 --- a/lib/app/app_controller_web_sessions.dart +++ b/lib/app/app_controller_web_sessions.dart @@ -149,14 +149,6 @@ extension AppControllerWebSessions on AppController { ); } - bool singleAgentUsesAiChatFallbackForSession(String sessionKey) { - final provider = singleAgentProviderForSession(sessionKey); - return provider == SingleAgentProvider.auto && canUseAiGatewayConversation; - } - - bool get currentSingleAgentUsesAiChatFallback => - singleAgentUsesAiChatFallbackForSession(currentSessionKeyInternal); - String singleAgentRuntimeModelForSession(String sessionKey) { return taskThreadForSessionInternal( normalizedSessionKeyInternal(sessionKey), @@ -174,12 +166,6 @@ extension AppControllerWebSessions on AppController { threadRecordsInternal[normalizedSessionKey]?.assistantModelId.trim() ?? ''; if (target == AssistantExecutionTarget.singleAgent) { - if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) { - if (recordModel.isNotEmpty) { - return recordModel; - } - return resolvedAiGatewayModel; - } final runtimeModel = singleAgentRuntimeModelForSession( normalizedSessionKey, ); @@ -189,7 +175,7 @@ extension AppControllerWebSessions on AppController { if (recordModel.isNotEmpty) { return recordModel; } - return resolvedAiGatewayModel; + return ''; } if (recordModel.isNotEmpty) { return recordModel; @@ -203,9 +189,6 @@ extension AppControllerWebSessions on AppController { List assistantModelChoicesForSession(String sessionKey) { final target = assistantExecutionTargetForSession(sessionKey); if (target == AssistantExecutionTarget.singleAgent) { - if (singleAgentUsesAiChatFallbackForSession(sessionKey)) { - return aiGatewayConversationModelChoices; - } final runtime = singleAgentRuntimeModelForSession(sessionKey); if (runtime.isNotEmpty) { return [runtime]; @@ -214,7 +197,7 @@ extension AppControllerWebSessions on AppController { if (recordModel.isNotEmpty) { return [recordModel]; } - return aiGatewayConversationModelChoices; + return const []; } final model = settingsInternal.defaultModel.trim(); if (model.isEmpty) { @@ -285,7 +268,11 @@ extension AppControllerWebSessions on AppController { } bool get currentSingleAgentNeedsAiGatewayConfiguration => - currentSingleAgentUsesAiChatFallback && !canUseAiGatewayConversation; + assistantExecutionTargetForSession(currentSessionKeyInternal) == + AssistantExecutionTarget.singleAgent && + !availableSingleAgentProviders.any( + webAcpClientInternal.capabilities.providers.contains, + ); List get secretReferences { final entries = [ diff --git a/lib/features/assistant/assistant_page_components.dart b/lib/features/assistant/assistant_page_components.dart index 5a1cdea1..0f6ba0b7 100644 --- a/lib/features/assistant/assistant_page_components.dart +++ b/lib/features/assistant/assistant_page_components.dart @@ -498,7 +498,6 @@ class AssistantEmptyStateInternal extends StatelessWidget { final connectionState = controller.currentAssistantConnectionState; final singleAgent = connectionState.isSingleAgent; final connected = connectionState.connected; - final singleAgentFallback = controller.currentSingleAgentUsesAiChatFallback; final singleAgentNeedsAiGateway = controller.currentSingleAgentNeedsAiGatewayConfiguration; final singleAgentSuggestsAcpSwitch = @@ -509,7 +508,7 @@ class AssistantEmptyStateInternal extends StatelessWidget { ? connected ? appText('开始 ACP Server 任务', 'Start an ACP Server task') : singleAgentNeedsAiGateway - ? appText('先配置 LLM API', 'Configure LLM API first') + ? appText('先配置 ACP Server', 'Configure ACP Server first') : appText('先准备 ACP Server', 'Prepare the ACP Server first') : connected ? appText('开始对话或运行任务', 'Start a chat or run a task') @@ -518,15 +517,10 @@ class AssistantEmptyStateInternal extends StatelessWidget { : appText('先连接 Gateway', 'Connect a gateway first'); final description = singleAgent ? connected - ? (singleAgentFallback - ? appText( - '当前没有可用的外部 Agent ACP 连接,这个线程已降级到 AI Chat fallback,不会建立 OpenClaw Gateway 会话。', - 'No external Agent ACP connection is available for this thread, so it is running in AI Chat fallback without opening an OpenClaw Gateway session.', - ) - : appText( - '当前线程通过 ACP Server 处理任务,不会建立 OpenClaw Gateway 会话。', - 'This thread runs through the ACP Server path and does not open an OpenClaw Gateway session.', - )) + ? appText( + '当前线程通过 ACP Server 处理任务,不会建立 OpenClaw Gateway 会话。', + 'This thread runs through the ACP Server path and does not open an OpenClaw Gateway session.', + ) : singleAgentSuggestsAcpSwitch ? appText( '当前线程固定为 $providerLabel,但它在这台设备上不可用。请改成可用的 ACP Server。', @@ -534,8 +528,8 @@ class AssistantEmptyStateInternal extends StatelessWidget { ) : singleAgentNeedsAiGateway ? appText( - '请先在 设置 -> 集成 中配置 LLM API Endpoint、LLM API Token 和默认模型,然后以 ACP Server 模式继续当前任务。', - 'Set the LLM API Endpoint, LLM API Token, and default model in Settings -> Integrations, then continue this task in ACP Server mode.', + '请先在 设置 -> 集成 中配置可用的外部 Agent ACP 端点,然后以 ACP Server 模式继续当前任务。', + 'Configure an external Agent ACP endpoint in Settings -> Integrations, then continue this task in ACP Server mode.', ) : appText( '当前线程的外部 Agent ACP 连接尚未就绪。请先配置 $providerLabel 对应端点。', diff --git a/lib/runtime/direct_single_agent_app_server_client.dart b/lib/runtime/direct_single_agent_app_server_client.dart deleted file mode 100644 index d8c2a300..00000000 --- a/lib/runtime/direct_single_agent_app_server_client.dart +++ /dev/null @@ -1,6 +0,0 @@ -// Legacy compatibility surface retained while the app imports are cleaned up. -// -// The direct single-agent app-server runtime has been retired in favor of the -// GoTaskService ACP lane. This library intentionally exports only the capability -// DTOs still consumed by the UI-facing state layer. -export 'direct_single_agent_app_server_client_protocol.dart'; diff --git a/lib/runtime/direct_single_agent_app_server_client_protocol.dart b/lib/runtime/single_agent_capabilities.dart similarity index 82% rename from lib/runtime/direct_single_agent_app_server_client_protocol.dart rename to lib/runtime/single_agent_capabilities.dart index 7a29e1b3..a1f13dfd 100644 --- a/lib/runtime/direct_single_agent_app_server_client_protocol.dart +++ b/lib/runtime/single_agent_capabilities.dart @@ -1,14 +1,14 @@ import 'runtime_models.dart'; -class DirectSingleAgentCapabilities { - const DirectSingleAgentCapabilities({ +class SingleAgentCapabilities { + const SingleAgentCapabilities({ required this.available, required this.supportedProviders, required this.endpoint, this.errorMessage, }); - const DirectSingleAgentCapabilities.unavailable({ + const SingleAgentCapabilities.unavailable({ required this.endpoint, this.errorMessage, }) : available = false, diff --git a/lib/runtime/single_agent_runner.dart b/lib/runtime/single_agent_runner.dart deleted file mode 100644 index 0b611380..00000000 --- a/lib/runtime/single_agent_runner.dart +++ /dev/null @@ -1,4 +0,0 @@ -// Legacy compatibility shim retained until remaining imports are cleaned up. -// -// Single-agent execution now flows through GoTaskService and the ACP -// transport; the previous direct runner no longer owns runtime strategy. diff --git a/lib/widgets/assistant_focus_panel_previews.dart b/lib/widgets/assistant_focus_panel_previews.dart index 13971003..63e9ac9a 100644 --- a/lib/widgets/assistant_focus_panel_previews.dart +++ b/lib/widgets/assistant_focus_panel_previews.dart @@ -116,14 +116,15 @@ class SkillsFocusPreviewInternal extends StatelessWidget { message: typedController.isSingleAgentMode ? (typedController.currentSingleAgentNeedsAiGatewayConfiguration ? appText( - '当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback。', - 'No external Agent ACP endpoint is available. Configure LLM API fallback first.', + '当前没有可用的外部 Agent ACP 端点,请先配置 ACP Server。', + 'No external Agent ACP endpoint is available. Configure an ACP server first.', ) : appText( '当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。', 'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.', )) - : typedController.connection.status == RuntimeConnectionStatus.connected + : typedController.connection.status == + RuntimeConnectionStatus.connected ? appText( '当前代理没有已加载技能。', 'No skills are loaded for the active agent.', @@ -306,7 +307,9 @@ class SecretsFocusPreviewInternal extends StatelessWidget { @override Widget build(BuildContext context) { final typedController = castAssistantFocusControllerInternal(controller); - final items = typedController.secretReferences.take(4).toList(growable: false); + final items = typedController.secretReferences + .take(4) + .toList(growable: false); if (items.isEmpty) { return PreviewEmptyStateInternal( message: appText( diff --git a/test/quality/wave1_file_size_guard_test.dart b/test/quality/wave1_file_size_guard_test.dart index 9f221332..0be67992 100644 --- a/test/quality/wave1_file_size_guard_test.dart +++ b/test/quality/wave1_file_size_guard_test.dart @@ -27,7 +27,6 @@ void main() { 'lib/features/assistant/assistant_page_main.dart': 1000, 'lib/app/app_controller_desktop_runtime_helpers.dart': 800, 'lib/app/app_controller_desktop_single_agent.dart': 200, - 'lib/app/app_controller_desktop_single_agent_ai_gateway.dart': 800, 'lib/app/app_controller_desktop_single_agent_go_task_flow.dart': 800, 'lib/app/app_controller_desktop_single_agent_status_messages.dart': 400, 'lib/app/app_controller_desktop_external_acp_routing.dart': 400, diff --git a/test/runtime/app_controller_ai_gateway_chat_suite.dart b/test/runtime/app_controller_ai_gateway_chat_suite.dart index 68f8ebe7..aa0bd314 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite.dart @@ -16,12 +16,10 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import 'app_controller_ai_gateway_chat_suite_core.dart'; -import 'app_controller_ai_gateway_chat_suite_chat.dart'; import 'app_controller_ai_gateway_chat_suite_single_agent.dart'; import 'app_controller_ai_gateway_chat_suite_fakes.dart'; import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; void main() { - registerAppControllerAiGatewayChatSuiteChatTestsInternal(); registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal(); } diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_chat.dart b/test/runtime/app_controller_ai_gateway_chat_suite_chat.dart deleted file mode 100644 index 2f383f64..00000000 --- a/test/runtime/app_controller_ai_gateway_chat_suite_chat.dart +++ /dev/null @@ -1,296 +0,0 @@ -// ignore_for_file: unused_import, unnecessary_import - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:xworkmate/app/app_controller.dart'; -import 'package:xworkmate/runtime/codex_runtime.dart'; -import 'package:xworkmate/runtime/device_identity_store.dart'; -import 'package:xworkmate/runtime/gateway_runtime.dart'; -import 'package:xworkmate/runtime/runtime_coordinator.dart'; -import 'package:xworkmate/runtime/runtime_models.dart'; -import 'package:xworkmate/runtime/secure_config_store.dart'; -import 'app_controller_ai_gateway_chat_suite_core.dart'; -import 'app_controller_ai_gateway_chat_suite_single_agent.dart'; -import 'app_controller_ai_gateway_chat_suite_fakes.dart'; -import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; - -void registerAppControllerAiGatewayChatSuiteChatTestsInternal() { - group('AI Gateway chat streaming', () { - test( - 'AppController streams and restores persistent Single Agent conversation turns', - () async { - final tempDirectory = await createTempDirectoryInternal( - 'xworkmate-ai-gateway-session-', - ); - final server = await FakeAiGatewayServerInternal.start( - responseMode: AiGatewayResponseModeInternal.sse, - ); - addTearDown(() async { - await server.close(); - }); - - final store = createStoreFromTempDirectoryInternal(tempDirectory); - final gateway = FakeGatewayRuntimeInternal(store: store); - final controller = await createAppControllerInternal( - store: store, - availableSingleAgentProvidersOverride: const [], - runtimeCoordinator: RuntimeCoordinator( - gateway: gateway, - codex: FakeCodexRuntimeInternal(), - ), - goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(), - ); - - await controller.settingsController.saveAiGatewayApiKey('live-key'); - await controller.saveSettings( - controller.settings.copyWith( - aiGateway: controller.settings.aiGateway.copyWith( - baseUrl: server.baseUrl, - availableModels: const ['qwen2.5-coder:latest'], - selectedModels: const ['qwen2.5-coder:latest'], - ), - defaultModel: 'gpt-5.4', - multiAgent: controller.settings.multiAgent.copyWith( - autoSync: false, - mountTargets: withAvailableMountTargetsInternal( - controller.settings.multiAgent.mountTargets, - const [], - ), - ), - ), - refreshAfterSave: false, - ); - await controller.setAssistantExecutionTarget( - AssistantExecutionTarget.singleAgent, - ); - - const firstQuestion = - 'Execution context:\n' - '- target: single-agent\n' - '- permission: full-access\n\n' - '今天聊点什么'; - const secondQuestion = '继续刚才的话题'; - - final firstTurn = controller.sendChatMessage( - firstQuestion, - thinking: 'low', - ); - await waitForInternal( - () => controller.chatMessages.any( - (message) => message.role == 'assistant' && message.pending, - ), - ); - expect(controller.hasAssistantPendingRun, isTrue); - server.allowCompletion(1); - await firstTurn; - - await waitForInternal( - () => controller.chatMessages.any( - (message) => - message.role == 'assistant' && message.text == 'FIRST_REPLY', - ), - ); - - final secondStore = createStoreFromTempDirectoryInternal(tempDirectory); - final secondGateway = FakeGatewayRuntimeInternal(store: secondStore); - final secondController = await createAppControllerInternal( - store: secondStore, - availableSingleAgentProvidersOverride: const [], - runtimeCoordinator: RuntimeCoordinator( - gateway: secondGateway, - codex: FakeCodexRuntimeInternal(), - ), - goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(), - ); - - await secondController.settingsController.saveAiGatewayApiKey( - 'live-key', - ); - - expect(secondController.chatMessages.last.text, 'FIRST_REPLY'); - expect( - secondController.settings.assistantExecutionTarget, - AssistantExecutionTarget.singleAgent, - ); - - final secondTurn = secondController.sendChatMessage( - secondQuestion, - thinking: 'low', - ); - await waitForInternal( - () => secondController.chatMessages.any( - (message) => message.role == 'assistant' && message.pending, - ), - ); - server.allowCompletion(2); - await secondTurn; - - await waitForInternal( - () => secondController.chatMessages.any( - (message) => - message.role == 'assistant' && message.text == 'SECOND_REPLY', - ), - ); - - expect(server.requestCount, 2); - expect(server.lastAuthorization, 'Bearer live-key'); - expect(server.requests.first['model'], 'qwen2.5-coder:latest'); - expect(server.requests.first['stream'], isTrue); - expect(server.requests.first['messages'], >[ - {'role': 'user', 'content': firstQuestion}, - ]); - expect(server.requests.last['messages'], >[ - {'role': 'user', 'content': firstQuestion}, - {'role': 'assistant', 'content': 'FIRST_REPLY'}, - {'role': 'user', 'content': secondQuestion}, - ]); - expect( - secondController.connection.status, - RuntimeConnectionStatus.offline, - ); - expect(secondController.assistantConnectionStatusLabel, '单机智能体'); - expect( - secondController.assistantConnectionTargetLabel, - 'AI Chat fallback · qwen2.5-coder:latest · 127.0.0.1:${server.port}', - ); - expect(secondController.chatMessages.last.text, 'SECOND_REPLY'); - expect(gateway.connectedProfiles, isEmpty); - expect(secondGateway.connectedProfiles, isEmpty); - }, - ); - - test('AppController falls back when LLM API ignores stream mode', () async { - final tempDirectory = await createTempDirectoryInternal( - 'xworkmate-ai-gateway-json-fallback-', - ); - final server = await FakeAiGatewayServerInternal.start( - responseMode: AiGatewayResponseModeInternal.json, - ); - addTearDown(() async { - await server.close(); - }); - - final store = createStoreFromTempDirectoryInternal(tempDirectory); - final controller = await createAppControllerInternal( - store: store, - availableSingleAgentProvidersOverride: const [], - runtimeCoordinator: RuntimeCoordinator( - gateway: FakeGatewayRuntimeInternal(store: store), - codex: FakeCodexRuntimeInternal(), - ), - goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(), - ); - - await controller.settingsController.saveAiGatewayApiKey('live-key'); - await controller.saveSettings( - controller.settings.copyWith( - aiGateway: controller.settings.aiGateway.copyWith( - baseUrl: server.baseUrl, - availableModels: const ['moonshotai/kimi-k2.5'], - selectedModels: const ['moonshotai/kimi-k2.5'], - ), - defaultModel: 'moonshotai/kimi-k2.5', - multiAgent: controller.settings.multiAgent.copyWith( - autoSync: false, - mountTargets: withAvailableMountTargetsInternal( - controller.settings.multiAgent.mountTargets, - const [], - ), - ), - ), - refreshAfterSave: false, - ); - await controller.setAssistantExecutionTarget( - AssistantExecutionTarget.singleAgent, - ); - - await controller.sendChatMessage('你好', thinking: 'low'); - - await waitForInternal( - () => controller.chatMessages.any( - (message) => - message.role == 'assistant' && message.text == 'FIRST_REPLY', - ), - ); - - expect(server.requests.single['stream'], isTrue); - expect(controller.chatMessages.last.pending, isFalse); - }); - - test( - 'AppController abortRun stops Single Agent streaming requests', - () async { - final tempDirectory = await createTempDirectoryInternal( - 'xworkmate-ai-gateway-abort-', - ); - final server = await FakeAiGatewayServerInternal.start( - responseMode: AiGatewayResponseModeInternal.sse, - ); - addTearDown(() async { - await server.close(); - }); - - final store = createStoreFromTempDirectoryInternal(tempDirectory); - final controller = await createAppControllerInternal( - store: store, - availableSingleAgentProvidersOverride: const [], - runtimeCoordinator: RuntimeCoordinator( - gateway: FakeGatewayRuntimeInternal(store: store), - codex: FakeCodexRuntimeInternal(), - ), - goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(), - ); - - await controller.settingsController.saveAiGatewayApiKey('live-key'); - await controller.saveSettings( - controller.settings.copyWith( - aiGateway: controller.settings.aiGateway.copyWith( - baseUrl: server.baseUrl, - availableModels: const ['z-ai/glm5'], - selectedModels: const ['z-ai/glm5'], - ), - defaultModel: 'z-ai/glm5', - multiAgent: controller.settings.multiAgent.copyWith( - autoSync: false, - mountTargets: withAvailableMountTargetsInternal( - controller.settings.multiAgent.mountTargets, - const [], - ), - ), - ), - refreshAfterSave: false, - ); - await controller.setAssistantExecutionTarget( - AssistantExecutionTarget.singleAgent, - ); - - final pendingTurn = controller.sendChatMessage( - '今天聊点什么', - thinking: 'low', - ); - await waitForInternal( - () => controller.chatMessages.any( - (message) => message.role == 'assistant' && message.pending, - ), - ); - - await controller.abortRun(); - server.allowCompletion(1); - await pendingTurn; - await waitForInternal(() => !controller.hasAssistantPendingRun); - - expect( - controller.chatMessages.where((message) => message.pending), - isEmpty, - ); - expect( - controller.chatMessages.where((message) => message.error), - isEmpty, - ); - }, - ); - }); -} diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_core.dart b/test/runtime/app_controller_ai_gateway_chat_suite_core.dart index 8ec685ed..67dccc8e 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_core.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_core.dart @@ -12,7 +12,6 @@ import 'package:xworkmate/runtime/gateway_runtime.dart'; import 'package:xworkmate/runtime/runtime_coordinator.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; -import 'app_controller_ai_gateway_chat_suite_chat.dart'; import 'app_controller_ai_gateway_chat_suite_single_agent.dart'; import 'app_controller_ai_gateway_chat_suite_fakes.dart'; import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart b/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart index 306a036f..8f923281 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart @@ -14,7 +14,6 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import 'app_controller_ai_gateway_chat_suite_core.dart'; -import 'app_controller_ai_gateway_chat_suite_chat.dart'; import 'app_controller_ai_gateway_chat_suite_single_agent.dart'; import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_fixtures.dart b/test/runtime/app_controller_ai_gateway_chat_suite_fixtures.dart index d3734b50..a63ef0ab 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_fixtures.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_fixtures.dart @@ -14,7 +14,6 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import 'app_controller_ai_gateway_chat_suite_core.dart'; -import 'app_controller_ai_gateway_chat_suite_chat.dart'; import 'app_controller_ai_gateway_chat_suite_single_agent.dart'; import 'app_controller_ai_gateway_chat_suite_fakes.dart'; diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart index f0abd906..c0b2977c 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart @@ -14,14 +14,13 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import 'app_controller_ai_gateway_chat_suite_core.dart'; -import 'app_controller_ai_gateway_chat_suite_chat.dart'; import 'app_controller_ai_gateway_chat_suite_fakes.dart'; import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { group('Single Agent provider resolution', () { test( - 'AppController uses the selected Single Agent provider before AI Chat fallback', + 'AppController uses the selected Single Agent provider before ACP execution', () async { final tempDirectory = await createTempDirectoryInternal( 'xworkmate-single-agent-provider-', @@ -607,10 +606,10 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ); test( - 'AppController falls back to AI Chat when no external CLI is available', + 'AppController returns an ACP-only error when no provider is available', () async { final tempDirectory = await createTempDirectoryInternal( - 'xworkmate-single-agent-fallback-', + 'xworkmate-single-agent-acp-unavailable-', ); final server = await FakeAiGatewayServerInternal.start( responseMode: AiGatewayResponseModeInternal.json, @@ -652,23 +651,12 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { expect(client.capabilitiesCalls, greaterThanOrEqualTo(1)); expect(client.executeCalls, 0); - expect(server.requestCount, 1); - expect( - controller.chatMessages.any( - (message) => message.text.contains('Codex CLI is unavailable'), - ), - isFalse, - ); - expect( - controller.chatMessages.any( - (message) => message.toolName == 'AI Chat fallback', - ), - isFalse, - ); + expect(server.requestCount, 0); expect( controller.chatMessages.any( (message) => - message.role == 'assistant' && message.text == 'FIRST_REPLY', + message.role == 'assistant' && + message.text.contains('当前没有可用的外部 Agent ACP 端点'), ), isTrue, ); @@ -676,10 +664,10 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ); test( - 'AppController auto-binds a thread workspace in AI Chat fallback when the thread binding is missing', + 'AppController auto-binds a thread workspace before reporting ACP unavailability', () async { final tempDirectory = await createTempDirectoryInternal( - 'xworkmate-single-agent-fallback-missing-workspace-', + 'xworkmate-single-agent-acp-unavailable-missing-workspace-', ); final server = await FakeAiGatewayServerInternal.start( responseMode: AiGatewayResponseModeInternal.json, @@ -725,9 +713,17 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ); expect(client.capabilitiesCalls, greaterThanOrEqualTo(1)); expect(client.executeCalls, 0); - expect(server.requestCount, 1); + expect(server.requestCount, 0); expect(workspacePath, isNotEmpty); expect(workspacePath, contains('.xworkmate/threads/')); + expect( + controller.chatMessages.any( + (message) => + message.role == 'assistant' && + message.text.contains('当前没有可用的外部 Agent ACP 端点'), + ), + isTrue, + ); }, ); }); diff --git a/test/runtime/app_controller_ai_gateway_models_suite.dart b/test/runtime/app_controller_ai_gateway_models_suite.dart index 223b1b59..f6a35b4d 100644 --- a/test/runtime/app_controller_ai_gateway_models_suite.dart +++ b/test/runtime/app_controller_ai_gateway_models_suite.dart @@ -49,7 +49,7 @@ void main() { ); test( - 'AppController keeps the current thread model source when only the global default target changes', + 'AppController does not borrow LLM API model choices when single-agent has no ACP provider', () async { SharedPreferences.setMockInitialValues({}); final tempDirectory = await Directory.systemTemp.createTemp( @@ -82,10 +82,8 @@ void main() { AssistantExecutionTarget.singleAgent, ); - expect(controller.assistantModelChoices, const [ - 'qwen2.5-coder:latest', - ]); - expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest'); + expect(controller.assistantModelChoices, isEmpty); + expect(controller.resolvedAssistantModel, isEmpty); expect(controller.canUseAiGatewayConversation, isTrue); await controller.saveSettings( @@ -100,10 +98,8 @@ void main() { ), AssistantExecutionTarget.singleAgent, ); - expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest'); - expect(controller.assistantModelChoices, const [ - 'qwen2.5-coder:latest', - ]); + expect(controller.resolvedAssistantModel, isEmpty); + expect(controller.assistantModelChoices, isEmpty); }, ); @@ -143,7 +139,6 @@ void main() { await controller.setSingleAgentProvider(SingleAgentProvider.opencode); expect(controller.currentSingleAgentHasResolvedProvider, isTrue); - expect(controller.currentSingleAgentUsesAiChatFallback, isFalse); expect(controller.currentSingleAgentShouldShowModelControl, isFalse); expect(controller.assistantModelChoices, isEmpty); expect(controller.resolvedAssistantModel, isEmpty); diff --git a/test/runtime/app_controller_desktop_refactor_characterization_suite.dart b/test/runtime/app_controller_desktop_refactor_characterization_suite.dart index a2a963ac..e59e2a76 100644 --- a/test/runtime/app_controller_desktop_refactor_characterization_suite.dart +++ b/test/runtime/app_controller_desktop_refactor_characterization_suite.dart @@ -123,7 +123,7 @@ void main() { ); test( - 'AppController keeps AI Gateway model choices when single-agent falls back to AI chat', + 'AppController keeps single-agent model controls empty when no ACP provider is available', () async { final harness = await _DesktopControllerHarness.create( availableSingleAgentProvidersOverride: const [], @@ -147,12 +147,10 @@ void main() { ); expect(controller.currentSingleAgentHasResolvedProvider, isFalse); - expect(controller.currentSingleAgentUsesAiChatFallback, isTrue); - expect(controller.currentSingleAgentShouldShowModelControl, isTrue); - expect(controller.assistantModelChoices, const [ - 'qwen2.5-coder:latest', - ]); - expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest'); + expect(controller.currentSingleAgentNeedsAiGatewayConfiguration, isTrue); + expect(controller.currentSingleAgentShouldShowModelControl, isFalse); + expect(controller.assistantModelChoices, isEmpty); + expect(controller.resolvedAssistantModel, isEmpty); }, ); } diff --git a/test/runtime/app_controller_execution_target_switch_suite_connection.dart b/test/runtime/app_controller_execution_target_switch_suite_connection.dart index 8f9fe48f..6c6c6b20 100644 --- a/test/runtime/app_controller_execution_target_switch_suite_connection.dart +++ b/test/runtime/app_controller_execution_target_switch_suite_connection.dart @@ -147,7 +147,7 @@ void registerExecutionTargetSwitchConnectionTests() { expect(controller.assistantConnectionStatusLabel, 'ACP Server Local'); expect( controller.assistantConnectionTargetLabel, - '没有可用的外部 Agent ACP 端点,请配置 LLM API fallback。', + '没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server。', ); expect( gateway.connectedProfiles, diff --git a/test/runtime/app_controller_execution_target_switch_suite_thread.dart b/test/runtime/app_controller_execution_target_switch_suite_thread.dart index 24fe1013..3a735537 100644 --- a/test/runtime/app_controller_execution_target_switch_suite_thread.dart +++ b/test/runtime/app_controller_execution_target_switch_suite_thread.dart @@ -198,7 +198,7 @@ void registerExecutionTargetSwitchThreadTests() { expect(controller.assistantConnectionStatusLabel, 'ACP Server Local'); expect( controller.assistantConnectionTargetLabel, - '没有可用的外部 Agent ACP 端点,请配置 LLM API fallback。', + '没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server。', ); }, ); @@ -240,19 +240,20 @@ void registerExecutionTargetSwitchThreadTests() { await controller.setAssistantExecutionTarget( AssistantExecutionTarget.local, ); - controller.chatControllerInternal.messagesInternal = [ - GatewayChatMessage( - id: 'gateway-old-message', - role: 'assistant', - text: 'previous desktop gateway history', - timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - toolCallId: null, - toolName: null, - stopReason: null, - pending: false, - error: false, - ), - ]; + controller.chatControllerInternal.messagesInternal = + [ + GatewayChatMessage( + id: 'gateway-old-message', + role: 'assistant', + text: 'previous desktop gateway history', + timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + toolCallId: null, + toolName: null, + stopReason: null, + pending: false, + error: false, + ), + ]; controller.initializeAssistantThreadContext( 'draft:fresh-thread', diff --git a/test/runtime/no_direct_cli_execution_guard_suite.dart b/test/runtime/no_direct_cli_execution_guard_suite.dart index 6b92ecb4..0b3a5df9 100644 --- a/test/runtime/no_direct_cli_execution_guard_suite.dart +++ b/test/runtime/no_direct_cli_execution_guard_suite.dart @@ -56,6 +56,9 @@ void main() { test('legacy direct single-agent runtime implementation stays removed', () { const removedFiles = [ + 'lib/runtime/direct_single_agent_app_server_client.dart', + 'lib/runtime/direct_single_agent_app_server_client_protocol.dart', + 'lib/runtime/single_agent_runner.dart', 'lib/runtime/direct_single_agent_app_server_client_core.dart', 'lib/runtime/direct_single_agent_app_server_client_helpers.dart', 'lib/runtime/direct_single_agent_app_server_client_transport.dart', @@ -65,15 +68,10 @@ void main() { expect( File(relativePath).existsSync(), isFalse, - reason: '$relativePath should stay removed after GoTaskService cutover', + reason: + '$relativePath should stay removed after GoTaskService cutover', ); } - - final runnerShim = File('lib/runtime/single_agent_runner.dart'); - expect(runnerShim.existsSync(), isTrue); - final shimContent = runnerShim.readAsStringSync(); - expect(shimContent.contains('DefaultSingleAgentRunner'), isFalse); - expect(shimContent.contains('DirectSingleAgentAppServerClient'), isFalse); }); }); }