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
- Do not run automated tests by default. Run tests only when the user explicitly asks for testing or verification.
- Add or update widget tests and golden tests for any Flutter UI page change.
- Add or update integration tests for any core business flow change.
- Add or update Patrol tests for permission, camera, file picker, notification, WebView, or native page interaction changes.

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/runtime/gateway_runtime_helpers.dart` | Gateway runtime core/helper closure | Kept, domain-owned |
| `lib/runtime/direct_single_agent_app_server_client_helpers.dart` | Single-agent app-server client closure | Kept, domain-owned |
| `lib/app/app_controller_web_helpers.dart` | Web AppController helper closure | Kept, domain-owned |
| `lib/web/web_assistant_page_helpers.dart` | Web assistant page closure | Kept, domain-owned |
| `lib/features/assistant/assistant_page_composer_state_helpers.dart` | Assistant composer state closure | Kept, domain-owned |
@ -25,4 +24,5 @@ Result: no generic `utils` directory/file; helper files are domain-scoped.
- No cross-domain `utils` bucket was found under `lib/` and `test/`.
- Existing helper files are already tied to explicit business closures.
- Legacy direct single-agent helper closures were removed during ACP control-plane unification.
- Governance decision: continue to allow `*_helpers.dart` only when the file name contains explicit domain ownership (feature/runtime/controller scope), and avoid introducing shared catch-all helpers.

View File

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

View File

@ -140,16 +140,44 @@ func (s *Server) runSingleAgentViaExternalProvider(
if endpoint == "" {
return nil, fmt.Errorf("external provider endpoint is missing")
}
forwardParams := sanitizeExternalACPParams(method, params)
return requestExternalACP(
ctx,
endpoint,
provider.AuthorizationHeader,
method,
params,
forwardParams,
notify,
)
}
func sanitizeExternalACPParams(method string, params map[string]any) map[string]any {
if len(params) == 0 {
return map[string]any{}
}
next := make(map[string]any, len(params))
for key, value := range params {
next[key] = value
}
// Internal routing/runtime fields must not leak into external provider payloads.
delete(next, "metadata")
delete(next, "resolvedExecutionTarget")
delete(next, "resolvedEndpointTarget")
delete(next, "resolvedProviderId")
delete(next, "resolvedModel")
delete(next, "resolvedSkills")
delete(next, externalProviderEndpointKey)
delete(next, externalProviderAuthorizationHeaderKey)
delete(next, externalProviderLabelKey)
// Gateway-only fields are irrelevant in ACP single-agent forwarding.
normalizedMethod := strings.TrimSpace(method)
if normalizedMethod == "session.start" || normalizedMethod == "session.message" {
delete(next, "executionTarget")
delete(next, "agentId")
}
return next
}
func externalProviderFromParams(params map[string]any) (syncedProvider, bool) {
endpoint := strings.TrimSpace(shared.StringArg(params, externalProviderEndpointKey, ""))
if endpoint == "" {

View File

@ -78,6 +78,7 @@ func TestProvidersSyncUpdatesCapabilities(t *testing.T) {
}
func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
var lastForwardedParams map[string]any
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/acp/rpc" {
http.NotFound(w, r)
@ -88,6 +89,7 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode request: %v", err)
}
lastForwardedParams = asMap(request["params"])
method, _ := request["method"].(string)
switch method {
case "session.start":
@ -148,6 +150,12 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
if got := response["resolvedProviderId"]; got != "claude" {
t.Fatalf("expected resolved provider claude, got %#v", response)
}
if _, exists := lastForwardedParams["metadata"]; exists {
t.Fatalf("expected metadata to be stripped for external provider request, got %#v", lastForwardedParams)
}
if _, exists := lastForwardedParams[externalProviderEndpointKey]; exists {
t.Fatalf("expected internal endpoint key to be stripped, got %#v", lastForwardedParams)
}
}
func TestRunSingleAgentUsesFrozenExternalProviderParams(t *testing.T) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_single_agent_ai_gateway.dart';
import 'app_controller_desktop_single_agent_go_task_flow.dart';
import '../runtime/runtime_models.dart';
@ -19,10 +18,6 @@ extension AppControllerDesktopSingleAgent on AppController {
);
}
Future<void> abortAiGatewayRunInternal(String sessionKey) {
return abortAiGatewaySingleAgentRunDesktopInternal(this, sessionKey);
}
GatewayChatMessage assistantErrorMessageInternal(String 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_external_acp_routing.dart';
import 'app_controller_desktop_runtime_helpers.dart';
import 'app_controller_desktop_single_agent_ai_gateway.dart';
import 'app_controller_desktop_single_agent_status_messages.dart';
import 'app_controller_desktop_thread_sessions.dart';
import 'app_controller_desktop_thread_storage.dart';
@ -69,7 +68,7 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
final provider = selection == SingleAgentProvider.auto
? (availableProviders.isEmpty ? null : availableProviders.first)
: (capabilities.providers.contains(selection) ? selection : null);
final fallbackReason = provider == null
final unavailableReason = provider == null
? (selection == SingleAgentProvider.auto
? appText(
'当前没有可用的 GoTaskService Provider。',
@ -80,48 +79,28 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
'GoTaskService does not currently support ${selection.label}.',
))
: null;
if (provider == null && !routing.isAuto) {
if (controller.singleAgentUsesAiChatFallbackForSession(sessionKey)) {
appendSingleAgentFallbackStatusDesktopInternal(
if (provider == null) {
controller.upsertTaskThreadInternal(
sessionKey,
lifecycleStatus: 'ready',
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
lastResultCode: 'error',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
controller.appendAssistantThreadMessageInternal(
sessionKey,
assistantErrorMessageSingleAgentDesktopInternal(
controller,
sessionKey,
fallbackReason,
);
await sendAiGatewaySingleAgentMessageDesktopInternal(
controller,
message,
thinking: thinking,
attachments: attachments,
sessionKeyOverride: sessionKey,
appendUserMessage: false,
managePendingState: false,
);
} else {
controller.appendAssistantThreadMessageInternal(
sessionKey,
GatewayChatMessage(
id: controller.nextLocalMessageIdInternal(),
role: 'assistant',
text: singleAgentUnavailableLabelDesktopInternal(
controller,
sessionKey,
fallbackReason,
),
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
toolCallId: null,
toolName: singleAgentRuntimeDebugToolNameDesktopInternal(
controller,
provider?.label ?? selection.label,
),
stopReason: null,
pending: false,
error: false,
singleAgentUnavailableLabelDesktopInternal(
controller,
sessionKey,
unavailableReason,
),
);
}
),
);
return;
}
final effectiveProvider = provider ?? SingleAgentProvider.auto;
final effectiveProvider = provider;
appendSingleAgentRuntimeStatusDesktopInternal(
controller,
@ -248,36 +227,6 @@ void _applySingleAgentGoTaskResultDesktopInternal(
result,
);
controller.clearAiGatewayStreamingTextInternal(sessionKey);
if (!result.success &&
controller.singleAgentUsesAiChatFallbackForSession(sessionKey)) {
appendSingleAgentFallbackStatusDesktopInternal(
controller,
sessionKey,
result.errorMessage,
);
controller.upsertTaskThreadInternal(
sessionKey,
gatewayEntryState: 'only-chat',
latestResolvedRuntimeModel: controller.resolvedAiGatewayModel,
lifecycleStatus: 'ready',
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
lastResultCode: 'fallback',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
unawaited(
sendAiGatewaySingleAgentMessageDesktopInternal(
controller,
message,
thinking: thinking,
attachments: attachments,
sessionKeyOverride: sessionKey,
appendUserMessage: false,
managePendingState: false,
),
);
return;
}
if (!result.success) {
controller.appendAssistantThreadMessageInternal(
sessionKey,

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_storage.dart';
GatewayChatMessage assistantErrorMessageSingleAgentDesktopInternal(
AppController controller,
String text,
) {
return GatewayChatMessage(
id: controller.nextLocalMessageIdInternal(),
role: 'assistant',
text: text,
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
toolCallId: null,
toolName: null,
stopReason: null,
pending: false,
error: true,
);
}
String? singleAgentRuntimeDebugToolNameDesktopInternal(
AppController controller,
String label,
@ -49,43 +66,6 @@ void appendSingleAgentRuntimeStatusDesktopInternal(
);
}
void appendSingleAgentFallbackStatusDesktopInternal(
AppController controller,
String sessionKey,
String? reason,
) {
if (!controller.showsSingleAgentRuntimeDebugMessagesInternal) {
return;
}
controller.appendAssistantThreadMessageInternal(
sessionKey,
GatewayChatMessage(
id: controller.nextLocalMessageIdInternal(),
role: 'assistant',
text: singleAgentFallbackLabelDesktopInternal(reason),
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
toolCallId: null,
toolName: 'AI Chat fallback',
stopReason: null,
pending: false,
error: false,
),
);
}
String singleAgentFallbackLabelDesktopInternal(String? reason) {
final detail = reason?.trim() ?? '';
return detail.isEmpty
? appText(
'未发现可用的外部 Agent ACP 端点,已回退到 AI Chat。',
'No external Agent ACP endpoint is available. Falling back to AI Chat.',
)
: appText(
'外部 Agent ACP 连接不可用,已回退到 AI Chat$detail',
'External Agent ACP connection is unavailable. Falling back to AI Chat: $detail',
);
}
String singleAgentUnavailableLabelDesktopInternal(
AppController controller,
String sessionKey,
@ -116,12 +96,12 @@ String singleAgentUnavailableLabelDesktopInternal(
)) {
return detail.isEmpty
? appText(
'当前没有可用的外部 Agent ACP 端点,也没有可用的 AI Chat fallback。请先配置外部 Agent 连接,或配置 LLM API',
'No external Agent ACP endpoint is available, and AI Chat fallback is not configured. Configure an external Agent connection or configure LLM API first.',
'当前没有可用的外部 Agent ACP 端点。请先配置外部 Agent 连接。',
'No external Agent ACP endpoint is available. Configure an external Agent connection first.',
)
: appText(
'$detail 当前没有可用的外部 Agent ACP 端点,也没有可用的 AI Chat fallback。请先配置外部 Agent 连接,或配置 LLM API',
'$detail No external Agent ACP endpoint is available, and AI Chat fallback is not configured. Configure an external Agent connection or configure LLM API first.',
'$detail 当前没有可用的外部 Agent ACP 端点。请先配置外部 Agent 连接。',
'$detail No external Agent ACP endpoint is available. Configure an external Agent connection first.',
);
}
return detail.isEmpty

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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';
class DirectSingleAgentCapabilities {
const DirectSingleAgentCapabilities({
class SingleAgentCapabilities {
const SingleAgentCapabilities({
required this.available,
required this.supportedProviders,
required this.endpoint,
this.errorMessage,
});
const DirectSingleAgentCapabilities.unavailable({
const SingleAgentCapabilities.unavailable({
required this.endpoint,
this.errorMessage,
}) : available = false,

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

View File

@ -27,7 +27,6 @@ void main() {
'lib/features/assistant/assistant_page_main.dart': 1000,
'lib/app/app_controller_desktop_runtime_helpers.dart': 800,
'lib/app/app_controller_desktop_single_agent.dart': 200,
'lib/app/app_controller_desktop_single_agent_ai_gateway.dart': 800,
'lib/app/app_controller_desktop_single_agent_go_task_flow.dart': 800,
'lib/app/app_controller_desktop_single_agent_status_messages.dart': 400,
'lib/app/app_controller_desktop_external_acp_routing.dart': 400,

View File

@ -16,12 +16,10 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'app_controller_ai_gateway_chat_suite_core.dart';
import 'app_controller_ai_gateway_chat_suite_chat.dart';
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
void main() {
registerAppControllerAiGatewayChatSuiteChatTestsInternal();
registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal();
}

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_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'app_controller_ai_gateway_chat_suite_chat.dart';
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';

View File

@ -14,7 +14,6 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'app_controller_ai_gateway_chat_suite_core.dart';
import 'app_controller_ai_gateway_chat_suite_chat.dart';
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';

View File

@ -14,7 +14,6 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'app_controller_ai_gateway_chat_suite_core.dart';
import 'app_controller_ai_gateway_chat_suite_chat.dart';
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
import 'app_controller_ai_gateway_chat_suite_fakes.dart';

View File

@ -14,14 +14,13 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'app_controller_ai_gateway_chat_suite_core.dart';
import 'app_controller_ai_gateway_chat_suite_chat.dart';
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
group('Single Agent provider resolution', () {
test(
'AppController uses the selected Single Agent provider before AI Chat fallback',
'AppController uses the selected Single Agent provider before ACP execution',
() async {
final tempDirectory = await createTempDirectoryInternal(
'xworkmate-single-agent-provider-',
@ -607,10 +606,10 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
);
test(
'AppController falls back to AI Chat when no external CLI is available',
'AppController returns an ACP-only error when no provider is available',
() async {
final tempDirectory = await createTempDirectoryInternal(
'xworkmate-single-agent-fallback-',
'xworkmate-single-agent-acp-unavailable-',
);
final server = await FakeAiGatewayServerInternal.start(
responseMode: AiGatewayResponseModeInternal.json,
@ -652,23 +651,12 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
expect(client.executeCalls, 0);
expect(server.requestCount, 1);
expect(
controller.chatMessages.any(
(message) => message.text.contains('Codex CLI is unavailable'),
),
isFalse,
);
expect(
controller.chatMessages.any(
(message) => message.toolName == 'AI Chat fallback',
),
isFalse,
);
expect(server.requestCount, 0);
expect(
controller.chatMessages.any(
(message) =>
message.role == 'assistant' && message.text == 'FIRST_REPLY',
message.role == 'assistant' &&
message.text.contains('当前没有可用的外部 Agent ACP 端点'),
),
isTrue,
);
@ -676,10 +664,10 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
);
test(
'AppController auto-binds a thread workspace in AI Chat fallback when the thread binding is missing',
'AppController auto-binds a thread workspace before reporting ACP unavailability',
() async {
final tempDirectory = await createTempDirectoryInternal(
'xworkmate-single-agent-fallback-missing-workspace-',
'xworkmate-single-agent-acp-unavailable-missing-workspace-',
);
final server = await FakeAiGatewayServerInternal.start(
responseMode: AiGatewayResponseModeInternal.json,
@ -725,9 +713,17 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
);
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
expect(client.executeCalls, 0);
expect(server.requestCount, 1);
expect(server.requestCount, 0);
expect(workspacePath, isNotEmpty);
expect(workspacePath, contains('.xworkmate/threads/'));
expect(
controller.chatMessages.any(
(message) =>
message.role == 'assistant' &&
message.text.contains('当前没有可用的外部 Agent ACP 端点'),
),
isTrue,
);
},
);
});

View File

@ -49,7 +49,7 @@ void main() {
);
test(
'AppController keeps the current thread model source when only the global default target changes',
'AppController does not borrow LLM API model choices when single-agent has no ACP provider',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final tempDirectory = await Directory.systemTemp.createTemp(
@ -82,10 +82,8 @@ void main() {
AssistantExecutionTarget.singleAgent,
);
expect(controller.assistantModelChoices, const <String>[
'qwen2.5-coder:latest',
]);
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest');
expect(controller.assistantModelChoices, isEmpty);
expect(controller.resolvedAssistantModel, isEmpty);
expect(controller.canUseAiGatewayConversation, isTrue);
await controller.saveSettings(
@ -100,10 +98,8 @@ void main() {
),
AssistantExecutionTarget.singleAgent,
);
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest');
expect(controller.assistantModelChoices, const <String>[
'qwen2.5-coder:latest',
]);
expect(controller.resolvedAssistantModel, isEmpty);
expect(controller.assistantModelChoices, isEmpty);
},
);
@ -143,7 +139,6 @@ void main() {
await controller.setSingleAgentProvider(SingleAgentProvider.opencode);
expect(controller.currentSingleAgentHasResolvedProvider, isTrue);
expect(controller.currentSingleAgentUsesAiChatFallback, isFalse);
expect(controller.currentSingleAgentShouldShowModelControl, isFalse);
expect(controller.assistantModelChoices, isEmpty);
expect(controller.resolvedAssistantModel, isEmpty);

View File

@ -123,7 +123,7 @@ void main() {
);
test(
'AppController keeps AI Gateway model choices when single-agent falls back to AI chat',
'AppController keeps single-agent model controls empty when no ACP provider is available',
() async {
final harness = await _DesktopControllerHarness.create(
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
@ -147,12 +147,10 @@ void main() {
);
expect(controller.currentSingleAgentHasResolvedProvider, isFalse);
expect(controller.currentSingleAgentUsesAiChatFallback, isTrue);
expect(controller.currentSingleAgentShouldShowModelControl, isTrue);
expect(controller.assistantModelChoices, const <String>[
'qwen2.5-coder:latest',
]);
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest');
expect(controller.currentSingleAgentNeedsAiGatewayConfiguration, isTrue);
expect(controller.currentSingleAgentShouldShowModelControl, isFalse);
expect(controller.assistantModelChoices, isEmpty);
expect(controller.resolvedAssistantModel, isEmpty);
},
);
}

View File

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

View File

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

View File

@ -56,6 +56,9 @@ void main() {
test('legacy direct single-agent runtime implementation stays removed', () {
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_helpers.dart',
'lib/runtime/direct_single_agent_app_server_client_transport.dart',
@ -65,15 +68,10 @@ void main() {
expect(
File(relativePath).existsSync(),
isFalse,
reason: '$relativePath should stay removed after GoTaskService cutover',
reason:
'$relativePath should stay removed after GoTaskService cutover',
);
}
final runnerShim = File('lib/runtime/single_agent_runner.dart');
expect(runnerShim.existsSync(), isTrue);
final shimContent = runnerShim.readAsStringSync();
expect(shimContent.contains('DefaultSingleAgentRunner'), isFalse);
expect(shimContent.contains('DirectSingleAgentAppServerClient'), isFalse);
});
});
}