Unify single-agent task flow under ACP

This commit is contained in:
Haitao Pan 2026-04-08 20:27:35 +08:00
parent 69a339e91d
commit b4bccf8300
42 changed files with 168 additions and 1088 deletions

View File

@ -1,5 +1,6 @@
# Agent Rules # 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 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 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. - Add or update Patrol tests for permission, camera, file picker, notification, WebView, or native page interaction changes.

View File

@ -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/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/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/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/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 | | `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/`. - No cross-domain `utils` bucket was found under `lib/` and `test/`.
- Existing helper files are already tied to explicit business closures. - 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. - 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.

View File

@ -20,10 +20,12 @@
## 重点验证点覆盖 ## 重点验证点覆盖
> 注: 本报告形成于 ACP-only 收敛之前;下面的测试名已在后续版本中被 ACP-only 语义替换。
| 验证点 | 对应测试用例 | 状态 | | 验证点 | 对应测试用例 | 状态 |
|--------|-------------|------| |--------|-------------|------|
| Single Agent 线程优先走外部 CLI | `AppController uses the selected Single Agent provider before AI Chat fallback` | ✅ | | Single Agent 线程优先走外部 CLI | 历史用例,现已替换为 ACP-only provider 路由校验 | ✅ |
| 外部 CLI 探测失败 fallback 到 AI Chat | `AppController falls back to AI Chat when the selected Single Agent provider is unavailable` | ✅ | | 外部 CLI 不可用时返回明确错误 | 历史用例,现已替换为 ACP-only 不自动降级校验 | ✅ |
| singleAgentProvider 线程级持久化兼容旧值 | `SettingsSnapshot keeps compatibility with legacy target json values`<br>`AssistantThreadRecord keeps compatibility with legacy json payloads` | ✅ | | singleAgentProvider 线程级持久化兼容旧值 | `SettingsSnapshot keeps compatibility with legacy target json values`<br>`AssistantThreadRecord keeps compatibility with legacy json payloads` | ✅ |
| Assistant 页面 provider chip 无回归 | `AssistantPage shows Single Agent chip and keeps task rows minimal`<br>`AssistantPage shows Single Agent provider selector on the right` | ✅ | | Assistant 页面 provider chip 无回归 | `AssistantPage shows Single Agent chip and keeps task rows minimal`<br>`AssistantPage shows Single Agent provider selector on the right` | ✅ |
| 自动滚动无回归 | Suite 整体通过 | ✅ | | 自动滚动无回归 | Suite 整体通过 | ✅ |
@ -64,4 +66,4 @@
- 测试套件: `test/runtime/secure_config_store_suite.dart` - 测试套件: `test/runtime/secure_config_store_suite.dart`
- 测试套件: `test/runtime/app_controller_execution_target_switch_suite.dart` - 测试套件: `test/runtime/app_controller_execution_target_switch_suite.dart`
- 测试套件: `test/features/assistant_page_suite.dart` - 测试套件: `test/features/assistant_page_suite.dart`
- 新增实现: `lib/runtime/single_agent_runner.dart` (未跟踪) - 历史实现说明: 早期 single-agent shim 已在 ACP 控制面统一后删除

View File

@ -140,16 +140,44 @@ func (s *Server) runSingleAgentViaExternalProvider(
if endpoint == "" { if endpoint == "" {
return nil, fmt.Errorf("external provider endpoint is missing") return nil, fmt.Errorf("external provider endpoint is missing")
} }
forwardParams := sanitizeExternalACPParams(method, params)
return requestExternalACP( return requestExternalACP(
ctx, ctx,
endpoint, endpoint,
provider.AuthorizationHeader, provider.AuthorizationHeader,
method, method,
params, forwardParams,
notify, 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) { func externalProviderFromParams(params map[string]any) (syncedProvider, bool) {
endpoint := strings.TrimSpace(shared.StringArg(params, externalProviderEndpointKey, "")) endpoint := strings.TrimSpace(shared.StringArg(params, externalProviderEndpointKey, ""))
if endpoint == "" { if endpoint == "" {

View File

@ -78,6 +78,7 @@ func TestProvidersSyncUpdatesCapabilities(t *testing.T) {
} }
func TestExecuteSessionTaskUsesSyncedExternalProvider(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) { externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/acp/rpc" { if r.URL.Path != "/acp/rpc" {
http.NotFound(w, r) http.NotFound(w, r)
@ -88,6 +89,7 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&request); err != nil { if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode request: %v", err) t.Fatalf("decode request: %v", err)
} }
lastForwardedParams = asMap(request["params"])
method, _ := request["method"].(string) method, _ := request["method"].(string)
switch method { switch method {
case "session.start": case "session.start":
@ -148,6 +150,12 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
if got := response["resolvedProviderId"]; got != "claude" { if got := response["resolvedProviderId"]; got != "claude" {
t.Fatalf("expected resolved provider claude, got %#v", response) 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) { func TestRunSingleAgentUsesFrozenExternalProviderParams(t *testing.T) {

View File

@ -22,7 +22,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.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_mounts.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.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 '../runtime/skill_directory_access.dart';
import 'task_thread_repositories.dart'; import 'task_thread_repositories.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';
@ -303,9 +302,9 @@ class AppController extends ChangeNotifier {
late final GoTaskServiceClient goTaskServiceClientInternal; late final GoTaskServiceClient goTaskServiceClientInternal;
late final MultiAgentOrchestrator multiAgentOrchestratorInternal; late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
late final MultiAgentMountManager multiAgentMountManagerInternal; late final MultiAgentMountManager multiAgentMountManagerInternal;
Map<SingleAgentProvider, DirectSingleAgentCapabilities> Map<SingleAgentProvider, SingleAgentCapabilities>
singleAgentCapabilitiesByProviderInternal = singleAgentCapabilitiesByProviderInternal =
const <SingleAgentProvider, DirectSingleAgentCapabilities>{}; const <SingleAgentProvider, SingleAgentCapabilities>{};
final Map<String, List<GatewayChatMessage>> assistantThreadMessagesInternal = final Map<String, List<GatewayChatMessage>> assistantThreadMessagesInternal =
<String, List<GatewayChatMessage>>{}; <String, List<GatewayChatMessage>>{};
late final DesktopTaskThreadRepository taskThreadRepositoryInternal = late final DesktopTaskThreadRepository taskThreadRepositoryInternal =

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_thread_sessions.dart'; import 'app_controller_desktop_thread_sessions.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_gateway.dart'; import 'app_controller_desktop_gateway.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,7 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.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 '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';
@ -90,15 +89,15 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
target: AssistantExecutionTarget.singleAgent, target: AssistantExecutionTarget.singleAgent,
forceRefresh: forceRefresh, forceRefresh: forceRefresh,
); );
final next = <SingleAgentProvider, DirectSingleAgentCapabilities>{}; final next = <SingleAgentProvider, SingleAgentCapabilities>{};
for (final provider in controller.configuredSingleAgentProviders) { for (final provider in controller.configuredSingleAgentProviders) {
if (!capabilities.providers.contains(provider)) { if (!capabilities.providers.contains(provider)) {
next[provider] = const DirectSingleAgentCapabilities.unavailable( next[provider] = const SingleAgentCapabilities.unavailable(
endpoint: '', endpoint: '',
); );
continue; continue;
} }
next[provider] = DirectSingleAgentCapabilities( next[provider] = SingleAgentCapabilities(
available: true, available: true,
supportedProviders: <SingleAgentProvider>[provider], supportedProviders: <SingleAgentProvider>[provider],
endpoint: 'go-task-service', endpoint: 'go-task-service',

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -1,5 +1,4 @@
import 'app_controller_desktop_core.dart'; 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 'app_controller_desktop_single_agent_go_task_flow.dart';
import '../runtime/runtime_models.dart'; import '../runtime/runtime_models.dart';
@ -19,10 +18,6 @@ extension AppControllerDesktopSingleAgent on AppController {
); );
} }
Future<void> abortAiGatewayRunInternal(String sessionKey) {
return abortAiGatewaySingleAgentRunDesktopInternal(this, sessionKey);
}
GatewayChatMessage assistantErrorMessageInternal(String text) { GatewayChatMessage assistantErrorMessageInternal(String text) {
return assistantErrorMessageSingleAgentDesktopInternal(this, text); return assistantErrorMessageSingleAgentDesktopInternal(this, text);
} }

View File

@ -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<void> sendAiGatewaySingleAgentMessageDesktopInternal(
AppController controller,
String message, {
required String thinking,
required List<GatewayChatAttachmentPayload> 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<String> 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 = <String, dynamic>{
'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<Map<String, String>>
buildAiGatewaySingleAgentRequestMessagesDesktopInternal(
AppController controller,
String sessionKey,
) {
final history = <GatewayChatMessage>[
...(controller.gatewayHistoryCacheInternal[sessionKey] ??
const <GatewayChatMessage>[]),
...(controller.assistantThreadMessagesInternal[sessionKey] ??
const <GatewayChatMessage>[]),
];
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) => <String, String>{
'role': message.role.trim().toLowerCase() == 'assistant'
? 'assistant'
: 'user',
'content': message.text.trim(),
},
)
.toList(growable: false);
}
Future<String> 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<String> readAiGatewayStreamingResponseDesktopInternal(
AppController controller, {
required HttpClientResponse response,
required String sessionKey,
}) async {
final buffer = StringBuffer();
final eventLines = <String>[];
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<void> 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),
);
}

View File

@ -9,7 +9,6 @@ import '../runtime/runtime_models.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_external_acp_routing.dart'; import 'app_controller_desktop_external_acp_routing.dart';
import 'app_controller_desktop_runtime_helpers.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_single_agent_status_messages.dart';
import 'app_controller_desktop_thread_sessions.dart'; import 'app_controller_desktop_thread_sessions.dart';
import 'app_controller_desktop_thread_storage.dart'; import 'app_controller_desktop_thread_storage.dart';
@ -69,7 +68,7 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
final provider = selection == SingleAgentProvider.auto final provider = selection == SingleAgentProvider.auto
? (availableProviders.isEmpty ? null : availableProviders.first) ? (availableProviders.isEmpty ? null : availableProviders.first)
: (capabilities.providers.contains(selection) ? selection : null); : (capabilities.providers.contains(selection) ? selection : null);
final fallbackReason = provider == null final unavailableReason = provider == null
? (selection == SingleAgentProvider.auto ? (selection == SingleAgentProvider.auto
? appText( ? appText(
'当前没有可用的 GoTaskService Provider。', '当前没有可用的 GoTaskService Provider。',
@ -80,48 +79,28 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
'GoTaskService does not currently support ${selection.label}.', 'GoTaskService does not currently support ${selection.label}.',
)) ))
: null; : null;
if (provider == null && !routing.isAuto) { if (provider == null) {
if (controller.singleAgentUsesAiChatFallbackForSession(sessionKey)) { controller.upsertTaskThreadInternal(
appendSingleAgentFallbackStatusDesktopInternal( sessionKey,
lifecycleStatus: 'ready',
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
lastResultCode: 'error',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
controller.appendAssistantThreadMessageInternal(
sessionKey,
assistantErrorMessageSingleAgentDesktopInternal(
controller, controller,
sessionKey, singleAgentUnavailableLabelDesktopInternal(
fallbackReason, controller,
); sessionKey,
await sendAiGatewaySingleAgentMessageDesktopInternal( unavailableReason,
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,
), ),
); ),
} );
return; return;
} }
final effectiveProvider = provider ?? SingleAgentProvider.auto; final effectiveProvider = provider;
appendSingleAgentRuntimeStatusDesktopInternal( appendSingleAgentRuntimeStatusDesktopInternal(
controller, controller,
@ -248,36 +227,6 @@ void _applySingleAgentGoTaskResultDesktopInternal(
result, result,
); );
controller.clearAiGatewayStreamingTextInternal(sessionKey); 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) { if (!result.success) {
controller.appendAssistantThreadMessageInternal( controller.appendAssistantThreadMessageInternal(
sessionKey, sessionKey,

View File

@ -8,6 +8,23 @@ import 'app_controller_desktop_runtime_helpers.dart';
import 'app_controller_desktop_thread_sessions.dart'; import 'app_controller_desktop_thread_sessions.dart';
import 'app_controller_desktop_thread_storage.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( String? singleAgentRuntimeDebugToolNameDesktopInternal(
AppController controller, AppController controller,
String label, 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( String singleAgentUnavailableLabelDesktopInternal(
AppController controller, AppController controller,
String sessionKey, String sessionKey,
@ -116,12 +96,12 @@ String singleAgentUnavailableLabelDesktopInternal(
)) { )) {
return detail.isEmpty return detail.isEmpty
? appText( ? appText(
'当前没有可用的外部 Agent ACP 端点,也没有可用的 AI Chat fallback。请先配置外部 Agent 连接,或配置 LLM API', '当前没有可用的外部 Agent ACP 端点。请先配置外部 Agent 连接。',
'No external Agent ACP endpoint is available, and AI Chat fallback is not configured. Configure an external Agent connection or configure LLM API first.', 'No external Agent ACP endpoint is available. Configure an external Agent connection first.',
) )
: appText( : appText(
'$detail 当前没有可用的外部 Agent ACP 端点,也没有可用的 AI Chat fallback。请先配置外部 Agent 连接,或配置 LLM API', '$detail 当前没有可用的外部 Agent ACP 端点。请先配置外部 Agent 连接。',
'$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 No external Agent ACP endpoint is available. Configure an external Agent connection first.',
); );
} }
return detail.isEmpty return detail.isEmpty

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';
@ -73,11 +71,6 @@ extension AppControllerDesktopThreadActions on AppController {
localAttachments: localAttachments, localAttachments: localAttachments,
); );
Future<void> abortAiGatewayRunInternal(String sessionKey) =>
AppControllerDesktopSingleAgent(
this,
).abortAiGatewayRunInternal(sessionKey);
Future<void> connectSavedGateway() async { Future<void> connectSavedGateway() async {
final target = currentAssistantExecutionTarget; final target = currentAssistantExecutionTarget;
if (target == AssistantExecutionTarget.singleAgent) { if (target == AssistantExecutionTarget.singleAgent) {
@ -474,7 +467,6 @@ extension AppControllerDesktopThreadActions on AppController {
notifyIfActiveInternal(); notifyIfActiveInternal();
return; return;
} }
await abortAiGatewayRunInternal(sessionKey);
return; return;
} }
final sessionKey = normalizedAssistantSessionKeyInternal( final sessionKey = normalizedAssistantSessionKeyInternal(

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';
@ -123,17 +121,6 @@ extension AppControllerDesktopThreadSessions on AppController {
if (latestResolvedModel.isNotEmpty) { if (latestResolvedModel.isNotEmpty) {
return latestResolvedModel; return latestResolvedModel;
} }
if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
final recordModel =
assistantThreadRecordsInternal[normalizedSessionKey]
?.assistantModelId
.trim() ??
'';
if (recordModel.isNotEmpty) {
return recordModel;
}
return resolvedAiGatewayModel;
}
return singleAgentRuntimeModelForSession(normalizedSessionKey); return singleAgentRuntimeModelForSession(normalizedSessionKey);
} }
final recordModel = final recordModel =
@ -238,20 +225,6 @@ extension AppControllerDesktopThreadSessions on AppController {
SingleAgentProvider? get currentSingleAgentResolvedProvider => SingleAgentProvider? get currentSingleAgentResolvedProvider =>
singleAgentResolvedProviderForSession(currentSessionKey); 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) { bool singleAgentNeedsAiGatewayConfigurationForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal( final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey, sessionKey,
@ -260,7 +233,7 @@ extension AppControllerDesktopThreadSessions on AppController {
AssistantExecutionTarget.singleAgent) { AssistantExecutionTarget.singleAgent) {
return false; return false;
} }
return !hasAnyAvailableSingleAgentProvider && !canUseAiGatewayConversation; return !hasAnyAvailableSingleAgentProvider;
} }
bool get currentSingleAgentNeedsAiGatewayConfiguration => bool get currentSingleAgentNeedsAiGatewayConfiguration =>
@ -319,9 +292,6 @@ extension AppControllerDesktopThreadSessions on AppController {
if (model.isNotEmpty) { if (model.isNotEmpty) {
return model; return model;
} }
if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
return appText('AI Chat fallback', 'AI Chat fallback');
}
final provider = final provider =
singleAgentResolvedProviderForSession(normalizedSessionKey) ?? singleAgentResolvedProviderForSession(normalizedSessionKey) ??
singleAgentProviderForSession(normalizedSessionKey); singleAgentProviderForSession(normalizedSessionKey);
@ -342,9 +312,6 @@ extension AppControllerDesktopThreadSessions on AppController {
AssistantExecutionTarget.singleAgent) { AssistantExecutionTarget.singleAgent) {
return true; return true;
} }
if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
return true;
}
return singleAgentRuntimeModelForSession(normalizedSessionKey).isNotEmpty; return singleAgentRuntimeModelForSession(normalizedSessionKey).isNotEmpty;
} }
@ -370,9 +337,6 @@ extension AppControllerDesktopThreadSessions on AppController {
if (provider != SingleAgentProvider.auto) { if (provider != SingleAgentProvider.auto) {
return provider.label; return provider.label;
} }
if (currentSingleAgentUsesAiChatFallback) {
return appText('AI Chat fallback', 'AI Chat fallback');
}
return appText('单机智能体', 'Single Agent'); return appText('单机智能体', 'Single Agent');
} }
@ -393,19 +357,9 @@ extension AppControllerDesktopThreadSessions on AppController {
normalizedSessionKey, normalizedSessionKey,
); );
final model = assistantModelForSession(normalizedSessionKey); final model = assistantModelForSession(normalizedSessionKey);
final fallbackReady = singleAgentUsesAiChatFallbackForSession(
normalizedSessionKey,
);
final host = aiGatewayHostLabelInternal(aiGatewayUrl);
final providerReady = resolvedProvider != null; final providerReady = resolvedProvider != null;
final detail = providerReady final detail = providerReady
? joinConnectionPartsInternal(<String>[resolvedProvider.label, model]) ? joinConnectionPartsInternal(<String>[resolvedProvider.label, model])
: fallbackReady
? joinConnectionPartsInternal(<String>[
appText('AI Chat fallback', 'AI Chat fallback'),
model,
host,
])
: singleAgentShouldSuggestAcpSwitchForSession(normalizedSessionKey) : singleAgentShouldSuggestAcpSwitchForSession(normalizedSessionKey)
? appText( ? appText(
'${provider.label} 不可用,请切到可用的 ACP Server。', '${provider.label} 不可用,请切到可用的 ACP Server。',
@ -415,8 +369,8 @@ extension AppControllerDesktopThreadSessions on AppController {
normalizedSessionKey, normalizedSessionKey,
) )
? appText( ? appText(
'没有可用的外部 Agent ACP 端点,请配置 LLM API fallback', '没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server',
'No external Agent ACP endpoint is available. Configure LLM API fallback.', 'No external Agent ACP endpoint is available. Configure an ACP Server first.',
) )
: appText( : appText(
'当前线程的外部 Agent ACP 连接尚未就绪。', '当前线程的外部 Agent ACP 连接尚未就绪。',
@ -424,14 +378,14 @@ extension AppControllerDesktopThreadSessions on AppController {
); );
return AssistantThreadConnectionState( return AssistantThreadConnectionState(
executionTarget: target, executionTarget: target,
status: providerReady || fallbackReady status: providerReady
? RuntimeConnectionStatus.connected ? RuntimeConnectionStatus.connected
: RuntimeConnectionStatus.offline, : RuntimeConnectionStatus.offline,
primaryLabel: primaryLabel, primaryLabel: primaryLabel,
detailLabel: detail.isEmpty detailLabel: detail.isEmpty
? appText('未配置单机智能体', 'Single Agent is not configured') ? appText('未配置单机智能体', 'Single Agent is not configured')
: detail, : detail,
ready: providerReady || fallbackReady, ready: providerReady,
pairingRequired: false, pairingRequired: false,
gatewayTokenMissing: false, gatewayTokenMissing: false,
lastError: null, lastError: null,

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart'; import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart'; import '../runtime/runtime_coordinator.dart';
import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart'; import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart'; import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart'; import '../runtime/codex_config_bridge.dart';
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart'; import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart'; import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart'; import '../runtime/platform_environment.dart';
import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart'; import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_navigation.dart';

View File

@ -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) { String singleAgentRuntimeModelForSession(String sessionKey) {
return taskThreadForSessionInternal( return taskThreadForSessionInternal(
normalizedSessionKeyInternal(sessionKey), normalizedSessionKeyInternal(sessionKey),
@ -174,12 +166,6 @@ extension AppControllerWebSessions on AppController {
threadRecordsInternal[normalizedSessionKey]?.assistantModelId.trim() ?? threadRecordsInternal[normalizedSessionKey]?.assistantModelId.trim() ??
''; '';
if (target == AssistantExecutionTarget.singleAgent) { if (target == AssistantExecutionTarget.singleAgent) {
if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
if (recordModel.isNotEmpty) {
return recordModel;
}
return resolvedAiGatewayModel;
}
final runtimeModel = singleAgentRuntimeModelForSession( final runtimeModel = singleAgentRuntimeModelForSession(
normalizedSessionKey, normalizedSessionKey,
); );
@ -189,7 +175,7 @@ extension AppControllerWebSessions on AppController {
if (recordModel.isNotEmpty) { if (recordModel.isNotEmpty) {
return recordModel; return recordModel;
} }
return resolvedAiGatewayModel; return '';
} }
if (recordModel.isNotEmpty) { if (recordModel.isNotEmpty) {
return recordModel; return recordModel;
@ -203,9 +189,6 @@ extension AppControllerWebSessions on AppController {
List<String> assistantModelChoicesForSession(String sessionKey) { List<String> assistantModelChoicesForSession(String sessionKey) {
final target = assistantExecutionTargetForSession(sessionKey); final target = assistantExecutionTargetForSession(sessionKey);
if (target == AssistantExecutionTarget.singleAgent) { if (target == AssistantExecutionTarget.singleAgent) {
if (singleAgentUsesAiChatFallbackForSession(sessionKey)) {
return aiGatewayConversationModelChoices;
}
final runtime = singleAgentRuntimeModelForSession(sessionKey); final runtime = singleAgentRuntimeModelForSession(sessionKey);
if (runtime.isNotEmpty) { if (runtime.isNotEmpty) {
return <String>[runtime]; return <String>[runtime];
@ -214,7 +197,7 @@ extension AppControllerWebSessions on AppController {
if (recordModel.isNotEmpty) { if (recordModel.isNotEmpty) {
return <String>[recordModel]; return <String>[recordModel];
} }
return aiGatewayConversationModelChoices; return const <String>[];
} }
final model = settingsInternal.defaultModel.trim(); final model = settingsInternal.defaultModel.trim();
if (model.isEmpty) { if (model.isEmpty) {
@ -285,7 +268,11 @@ extension AppControllerWebSessions on AppController {
} }
bool get currentSingleAgentNeedsAiGatewayConfiguration => bool get currentSingleAgentNeedsAiGatewayConfiguration =>
currentSingleAgentUsesAiChatFallback && !canUseAiGatewayConversation; assistantExecutionTargetForSession(currentSessionKeyInternal) ==
AssistantExecutionTarget.singleAgent &&
!availableSingleAgentProviders.any(
webAcpClientInternal.capabilities.providers.contains,
);
List<SecretReferenceEntry> get secretReferences { List<SecretReferenceEntry> get secretReferences {
final entries = <SecretReferenceEntry>[ final entries = <SecretReferenceEntry>[

View File

@ -498,7 +498,6 @@ class AssistantEmptyStateInternal extends StatelessWidget {
final connectionState = controller.currentAssistantConnectionState; final connectionState = controller.currentAssistantConnectionState;
final singleAgent = connectionState.isSingleAgent; final singleAgent = connectionState.isSingleAgent;
final connected = connectionState.connected; final connected = connectionState.connected;
final singleAgentFallback = controller.currentSingleAgentUsesAiChatFallback;
final singleAgentNeedsAiGateway = final singleAgentNeedsAiGateway =
controller.currentSingleAgentNeedsAiGatewayConfiguration; controller.currentSingleAgentNeedsAiGatewayConfiguration;
final singleAgentSuggestsAcpSwitch = final singleAgentSuggestsAcpSwitch =
@ -509,7 +508,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
? connected ? connected
? appText('开始 ACP Server 任务', 'Start an ACP Server task') ? appText('开始 ACP Server 任务', 'Start an ACP Server task')
: singleAgentNeedsAiGateway : singleAgentNeedsAiGateway
? appText('先配置 LLM API', 'Configure LLM API first') ? appText('先配置 ACP Server', 'Configure ACP Server first')
: appText('先准备 ACP Server', 'Prepare the ACP Server first') : appText('先准备 ACP Server', 'Prepare the ACP Server first')
: connected : connected
? appText('开始对话或运行任务', 'Start a chat or run a task') ? appText('开始对话或运行任务', 'Start a chat or run a task')
@ -518,15 +517,10 @@ class AssistantEmptyStateInternal extends StatelessWidget {
: appText('先连接 Gateway', 'Connect a gateway first'); : appText('先连接 Gateway', 'Connect a gateway first');
final description = singleAgent final description = singleAgent
? connected ? connected
? (singleAgentFallback ? appText(
? appText( '当前线程通过 ACP Server 处理任务,不会建立 OpenClaw Gateway 会话。',
'当前没有可用的外部 Agent ACP 连接,这个线程已降级到 AI Chat fallback不会建立 OpenClaw Gateway 会话。', 'This thread runs through the ACP Server path and does not open an OpenClaw Gateway session.',
'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.',
))
: singleAgentSuggestsAcpSwitch : singleAgentSuggestsAcpSwitch
? appText( ? appText(
'当前线程固定为 $providerLabel,但它在这台设备上不可用。请改成可用的 ACP Server。', '当前线程固定为 $providerLabel,但它在这台设备上不可用。请改成可用的 ACP Server。',
@ -534,8 +528,8 @@ class AssistantEmptyStateInternal extends StatelessWidget {
) )
: singleAgentNeedsAiGateway : singleAgentNeedsAiGateway
? appText( ? appText(
'请先在 设置 -> 集成 中配置 LLM API Endpoint、LLM API Token 和默认模型,然后以 ACP Server 模式继续当前任务。', '请先在 设置 -> 集成 中配置可用的外部 Agent ACP 端点,然后以 ACP Server 模式继续当前任务。',
'Set the LLM API Endpoint, LLM API Token, and default model in Settings -> Integrations, then continue this task in ACP Server mode.', 'Configure an external Agent ACP endpoint in Settings -> Integrations, then continue this task in ACP Server mode.',
) )
: appText( : appText(
'当前线程的外部 Agent ACP 连接尚未就绪。请先配置 $providerLabel 对应端点。', '当前线程的外部 Agent ACP 连接尚未就绪。请先配置 $providerLabel 对应端点。',

View File

@ -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';

View File

@ -1,14 +1,14 @@
import 'runtime_models.dart'; import 'runtime_models.dart';
class DirectSingleAgentCapabilities { class SingleAgentCapabilities {
const DirectSingleAgentCapabilities({ const SingleAgentCapabilities({
required this.available, required this.available,
required this.supportedProviders, required this.supportedProviders,
required this.endpoint, required this.endpoint,
this.errorMessage, this.errorMessage,
}); });
const DirectSingleAgentCapabilities.unavailable({ const SingleAgentCapabilities.unavailable({
required this.endpoint, required this.endpoint,
this.errorMessage, this.errorMessage,
}) : available = false, }) : available = false,

View File

@ -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.

View File

@ -116,14 +116,15 @@ class SkillsFocusPreviewInternal extends StatelessWidget {
message: typedController.isSingleAgentMode message: typedController.isSingleAgentMode
? (typedController.currentSingleAgentNeedsAiGatewayConfiguration ? (typedController.currentSingleAgentNeedsAiGatewayConfiguration
? appText( ? appText(
'当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback', '当前没有可用的外部 Agent ACP 端点,请先配置 ACP Server',
'No external Agent ACP endpoint is available. Configure LLM API fallback first.', 'No external Agent ACP endpoint is available. Configure an ACP server first.',
) )
: appText( : appText(
'当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。', '当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。',
'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.', '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( ? appText(
'当前代理没有已加载技能。', '当前代理没有已加载技能。',
'No skills are loaded for the active agent.', 'No skills are loaded for the active agent.',
@ -306,7 +307,9 @@ class SecretsFocusPreviewInternal extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller); 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) { if (items.isEmpty) {
return PreviewEmptyStateInternal( return PreviewEmptyStateInternal(
message: appText( message: appText(

View File

@ -27,7 +27,6 @@ void main() {
'lib/features/assistant/assistant_page_main.dart': 1000, 'lib/features/assistant/assistant_page_main.dart': 1000,
'lib/app/app_controller_desktop_runtime_helpers.dart': 800, '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.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_go_task_flow.dart': 800,
'lib/app/app_controller_desktop_single_agent_status_messages.dart': 400, 'lib/app/app_controller_desktop_single_agent_status_messages.dart': 400,
'lib/app/app_controller_desktop_external_acp_routing.dart': 400, 'lib/app/app_controller_desktop_external_acp_routing.dart': 400,

View File

@ -16,12 +16,10 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.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_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_single_agent.dart';
import 'app_controller_ai_gateway_chat_suite_fakes.dart'; import 'app_controller_ai_gateway_chat_suite_fakes.dart';
import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
void main() { void main() {
registerAppControllerAiGatewayChatSuiteChatTestsInternal();
registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal(); registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal();
} }

View File

@ -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 <SingleAgentProvider>[],
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 <String>['qwen2.5-coder:latest'],
selectedModels: const <String>['qwen2.5-coder:latest'],
),
defaultModel: 'gpt-5.4',
multiAgent: controller.settings.multiAgent.copyWith(
autoSync: false,
mountTargets: withAvailableMountTargetsInternal(
controller.settings.multiAgent.mountTargets,
const <String>[],
),
),
),
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 <SingleAgentProvider>[],
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'], <Map<String, dynamic>>[
<String, dynamic>{'role': 'user', 'content': firstQuestion},
]);
expect(server.requests.last['messages'], <Map<String, dynamic>>[
<String, dynamic>{'role': 'user', 'content': firstQuestion},
<String, dynamic>{'role': 'assistant', 'content': 'FIRST_REPLY'},
<String, dynamic>{'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 <SingleAgentProvider>[],
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 <String>['moonshotai/kimi-k2.5'],
selectedModels: const <String>['moonshotai/kimi-k2.5'],
),
defaultModel: 'moonshotai/kimi-k2.5',
multiAgent: controller.settings.multiAgent.copyWith(
autoSync: false,
mountTargets: withAvailableMountTargetsInternal(
controller.settings.multiAgent.mountTargets,
const <String>[],
),
),
),
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 <SingleAgentProvider>[],
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 <String>['z-ai/glm5'],
selectedModels: const <String>['z-ai/glm5'],
),
defaultModel: 'z-ai/glm5',
multiAgent: controller.settings.multiAgent.copyWith(
autoSync: false,
mountTargets: withAvailableMountTargetsInternal(
controller.settings.multiAgent.mountTargets,
const <String>[],
),
),
),
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,
);
},
);
});
}

View File

@ -12,7 +12,6 @@ import 'package:xworkmate/runtime/gateway_runtime.dart';
import 'package:xworkmate/runtime/runtime_coordinator.dart'; import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.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_single_agent.dart';
import 'app_controller_ai_gateway_chat_suite_fakes.dart'; import 'app_controller_ai_gateway_chat_suite_fakes.dart';
import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; import 'app_controller_ai_gateway_chat_suite_fixtures.dart';

View File

@ -14,7 +14,6 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.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_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_single_agent.dart';
import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; import 'app_controller_ai_gateway_chat_suite_fixtures.dart';

View File

@ -14,7 +14,6 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.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_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_single_agent.dart';
import 'app_controller_ai_gateway_chat_suite_fakes.dart'; import 'app_controller_ai_gateway_chat_suite_fakes.dart';

View File

@ -14,14 +14,13 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.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_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_fakes.dart';
import 'app_controller_ai_gateway_chat_suite_fixtures.dart'; import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
group('Single Agent provider resolution', () { group('Single Agent provider resolution', () {
test( test(
'AppController uses the selected Single Agent provider before AI Chat fallback', 'AppController uses the selected Single Agent provider before ACP execution',
() async { () async {
final tempDirectory = await createTempDirectoryInternal( final tempDirectory = await createTempDirectoryInternal(
'xworkmate-single-agent-provider-', 'xworkmate-single-agent-provider-',
@ -607,10 +606,10 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
); );
test( 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 { () async {
final tempDirectory = await createTempDirectoryInternal( final tempDirectory = await createTempDirectoryInternal(
'xworkmate-single-agent-fallback-', 'xworkmate-single-agent-acp-unavailable-',
); );
final server = await FakeAiGatewayServerInternal.start( final server = await FakeAiGatewayServerInternal.start(
responseMode: AiGatewayResponseModeInternal.json, responseMode: AiGatewayResponseModeInternal.json,
@ -652,23 +651,12 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1)); expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
expect(client.executeCalls, 0); expect(client.executeCalls, 0);
expect(server.requestCount, 1); expect(server.requestCount, 0);
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( expect(
controller.chatMessages.any( controller.chatMessages.any(
(message) => (message) =>
message.role == 'assistant' && message.text == 'FIRST_REPLY', message.role == 'assistant' &&
message.text.contains('当前没有可用的外部 Agent ACP 端点'),
), ),
isTrue, isTrue,
); );
@ -676,10 +664,10 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
); );
test( 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 { () async {
final tempDirectory = await createTempDirectoryInternal( final tempDirectory = await createTempDirectoryInternal(
'xworkmate-single-agent-fallback-missing-workspace-', 'xworkmate-single-agent-acp-unavailable-missing-workspace-',
); );
final server = await FakeAiGatewayServerInternal.start( final server = await FakeAiGatewayServerInternal.start(
responseMode: AiGatewayResponseModeInternal.json, responseMode: AiGatewayResponseModeInternal.json,
@ -725,9 +713,17 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
); );
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1)); expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
expect(client.executeCalls, 0); expect(client.executeCalls, 0);
expect(server.requestCount, 1); expect(server.requestCount, 0);
expect(workspacePath, isNotEmpty); expect(workspacePath, isNotEmpty);
expect(workspacePath, contains('.xworkmate/threads/')); expect(workspacePath, contains('.xworkmate/threads/'));
expect(
controller.chatMessages.any(
(message) =>
message.role == 'assistant' &&
message.text.contains('当前没有可用的外部 Agent ACP 端点'),
),
isTrue,
);
}, },
); );
}); });

View File

@ -49,7 +49,7 @@ void main() {
); );
test( 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 { () async {
SharedPreferences.setMockInitialValues(<String, Object>{}); SharedPreferences.setMockInitialValues(<String, Object>{});
final tempDirectory = await Directory.systemTemp.createTemp( final tempDirectory = await Directory.systemTemp.createTemp(
@ -82,10 +82,8 @@ void main() {
AssistantExecutionTarget.singleAgent, AssistantExecutionTarget.singleAgent,
); );
expect(controller.assistantModelChoices, const <String>[ expect(controller.assistantModelChoices, isEmpty);
'qwen2.5-coder:latest', expect(controller.resolvedAssistantModel, isEmpty);
]);
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest');
expect(controller.canUseAiGatewayConversation, isTrue); expect(controller.canUseAiGatewayConversation, isTrue);
await controller.saveSettings( await controller.saveSettings(
@ -100,10 +98,8 @@ void main() {
), ),
AssistantExecutionTarget.singleAgent, AssistantExecutionTarget.singleAgent,
); );
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest'); expect(controller.resolvedAssistantModel, isEmpty);
expect(controller.assistantModelChoices, const <String>[ expect(controller.assistantModelChoices, isEmpty);
'qwen2.5-coder:latest',
]);
}, },
); );
@ -143,7 +139,6 @@ void main() {
await controller.setSingleAgentProvider(SingleAgentProvider.opencode); await controller.setSingleAgentProvider(SingleAgentProvider.opencode);
expect(controller.currentSingleAgentHasResolvedProvider, isTrue); expect(controller.currentSingleAgentHasResolvedProvider, isTrue);
expect(controller.currentSingleAgentUsesAiChatFallback, isFalse);
expect(controller.currentSingleAgentShouldShowModelControl, isFalse); expect(controller.currentSingleAgentShouldShowModelControl, isFalse);
expect(controller.assistantModelChoices, isEmpty); expect(controller.assistantModelChoices, isEmpty);
expect(controller.resolvedAssistantModel, isEmpty); expect(controller.resolvedAssistantModel, isEmpty);

View File

@ -123,7 +123,7 @@ void main() {
); );
test( 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 { () async {
final harness = await _DesktopControllerHarness.create( final harness = await _DesktopControllerHarness.create(
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[], availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
@ -147,12 +147,10 @@ void main() {
); );
expect(controller.currentSingleAgentHasResolvedProvider, isFalse); expect(controller.currentSingleAgentHasResolvedProvider, isFalse);
expect(controller.currentSingleAgentUsesAiChatFallback, isTrue); expect(controller.currentSingleAgentNeedsAiGatewayConfiguration, isTrue);
expect(controller.currentSingleAgentShouldShowModelControl, isTrue); expect(controller.currentSingleAgentShouldShowModelControl, isFalse);
expect(controller.assistantModelChoices, const <String>[ expect(controller.assistantModelChoices, isEmpty);
'qwen2.5-coder:latest', expect(controller.resolvedAssistantModel, isEmpty);
]);
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest');
}, },
); );
} }

View File

@ -147,7 +147,7 @@ void registerExecutionTargetSwitchConnectionTests() {
expect(controller.assistantConnectionStatusLabel, 'ACP Server Local'); expect(controller.assistantConnectionStatusLabel, 'ACP Server Local');
expect( expect(
controller.assistantConnectionTargetLabel, controller.assistantConnectionTargetLabel,
'没有可用的外部 Agent ACP 端点,请配置 LLM API fallback', '没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server',
); );
expect( expect(
gateway.connectedProfiles, gateway.connectedProfiles,

View File

@ -198,7 +198,7 @@ void registerExecutionTargetSwitchThreadTests() {
expect(controller.assistantConnectionStatusLabel, 'ACP Server Local'); expect(controller.assistantConnectionStatusLabel, 'ACP Server Local');
expect( expect(
controller.assistantConnectionTargetLabel, controller.assistantConnectionTargetLabel,
'没有可用的外部 Agent ACP 端点,请配置 LLM API fallback', '没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server',
); );
}, },
); );
@ -240,19 +240,20 @@ void registerExecutionTargetSwitchThreadTests() {
await controller.setAssistantExecutionTarget( await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.local, AssistantExecutionTarget.local,
); );
controller.chatControllerInternal.messagesInternal = <GatewayChatMessage>[ controller.chatControllerInternal.messagesInternal =
GatewayChatMessage( <GatewayChatMessage>[
id: 'gateway-old-message', GatewayChatMessage(
role: 'assistant', id: 'gateway-old-message',
text: 'previous desktop gateway history', role: 'assistant',
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), text: 'previous desktop gateway history',
toolCallId: null, timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
toolName: null, toolCallId: null,
stopReason: null, toolName: null,
pending: false, stopReason: null,
error: false, pending: false,
), error: false,
]; ),
];
controller.initializeAssistantThreadContext( controller.initializeAssistantThreadContext(
'draft:fresh-thread', 'draft:fresh-thread',

View File

@ -56,6 +56,9 @@ void main() {
test('legacy direct single-agent runtime implementation stays removed', () { test('legacy direct single-agent runtime implementation stays removed', () {
const removedFiles = <String>[ const removedFiles = <String>[
'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_core.dart',
'lib/runtime/direct_single_agent_app_server_client_helpers.dart', 'lib/runtime/direct_single_agent_app_server_client_helpers.dart',
'lib/runtime/direct_single_agent_app_server_client_transport.dart', 'lib/runtime/direct_single_agent_app_server_client_transport.dart',
@ -65,15 +68,10 @@ void main() {
expect( expect(
File(relativePath).existsSync(), File(relativePath).existsSync(),
isFalse, 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);
}); });
}); });
} }