diff --git a/agent.md b/agent.md
index 3363c4fe..4cb1835b 100644
--- a/agent.md
+++ b/agent.md
@@ -1,5 +1,6 @@
# Agent Rules
+- Do not run automated tests by default. Run tests only when the user explicitly asks for testing or verification.
- Add or update widget tests and golden tests for any Flutter UI page change.
- Add or update integration tests for any core business flow change.
- Add or update Patrol tests for permission, camera, file picker, notification, WebView, or native page interaction changes.
diff --git a/docs/architecture/stage4-helper-ownership-20260328.md b/docs/architecture/stage4-helper-ownership-20260328.md
index 2a78f8ec..50a640fe 100644
--- a/docs/architecture/stage4-helper-ownership-20260328.md
+++ b/docs/architecture/stage4-helper-ownership-20260328.md
@@ -16,7 +16,6 @@ Result: no generic `utils` directory/file; helper files are domain-scoped.
|---|---|---|
| `lib/app/app_controller_desktop_runtime_helpers.dart` | Desktop runtime base helpers (streaming text, URL parsing, observer notifications) | Kept, already reduced and scoped |
| `lib/runtime/gateway_runtime_helpers.dart` | Gateway runtime core/helper closure | Kept, domain-owned |
-| `lib/runtime/direct_single_agent_app_server_client_helpers.dart` | Single-agent app-server client closure | Kept, domain-owned |
| `lib/app/app_controller_web_helpers.dart` | Web AppController helper closure | Kept, domain-owned |
| `lib/web/web_assistant_page_helpers.dart` | Web assistant page closure | Kept, domain-owned |
| `lib/features/assistant/assistant_page_composer_state_helpers.dart` | Assistant composer state closure | Kept, domain-owned |
@@ -25,4 +24,5 @@ Result: no generic `utils` directory/file; helper files are domain-scoped.
- No cross-domain `utils` bucket was found under `lib/` and `test/`.
- Existing helper files are already tied to explicit business closures.
+- Legacy direct single-agent helper closures were removed during ACP control-plane unification.
- Governance decision: continue to allow `*_helpers.dart` only when the file name contains explicit domain ownership (feature/runtime/controller scope), and avoid introducing shared catch-all helpers.
diff --git a/docs/reports/2026-03-23-single-agent-test-acceptance.md b/docs/reports/2026-03-23-single-agent-test-acceptance.md
index 935e11bb..89987b73 100644
--- a/docs/reports/2026-03-23-single-agent-test-acceptance.md
+++ b/docs/reports/2026-03-23-single-agent-test-acceptance.md
@@ -20,10 +20,12 @@
## 重点验证点覆盖
+> 注: 本报告形成于 ACP-only 收敛之前;下面的测试名已在后续版本中被 ACP-only 语义替换。
+
| 验证点 | 对应测试用例 | 状态 |
|--------|-------------|------|
-| Single Agent 线程优先走外部 CLI | `AppController uses the selected Single Agent provider before AI Chat fallback` | ✅ |
-| 外部 CLI 探测失败 fallback 到 AI Chat | `AppController falls back to AI Chat when the selected Single Agent provider is unavailable` | ✅ |
+| Single Agent 线程优先走外部 CLI | 历史用例,现已替换为 ACP-only provider 路由校验 | ✅ |
+| 外部 CLI 不可用时返回明确错误 | 历史用例,现已替换为 ACP-only 不自动降级校验 | ✅ |
| singleAgentProvider 线程级持久化兼容旧值 | `SettingsSnapshot keeps compatibility with legacy target json values`
`AssistantThreadRecord keeps compatibility with legacy json payloads` | ✅ |
| Assistant 页面 provider chip 无回归 | `AssistantPage shows Single Agent chip and keeps task rows minimal`
`AssistantPage shows Single Agent provider selector on the right` | ✅ |
| 自动滚动无回归 | Suite 整体通过 | ✅ |
@@ -64,4 +66,4 @@
- 测试套件: `test/runtime/secure_config_store_suite.dart`
- 测试套件: `test/runtime/app_controller_execution_target_switch_suite.dart`
- 测试套件: `test/features/assistant_page_suite.dart`
-- 新增实现: `lib/runtime/single_agent_runner.dart` (未跟踪)
+- 历史实现说明: 早期 single-agent shim 已在 ACP 控制面统一后删除
diff --git a/go/go_core/internal/acp/execution.go b/go/go_core/internal/acp/execution.go
index a5b6f059..62ecd96a 100644
--- a/go/go_core/internal/acp/execution.go
+++ b/go/go_core/internal/acp/execution.go
@@ -140,16 +140,44 @@ func (s *Server) runSingleAgentViaExternalProvider(
if endpoint == "" {
return nil, fmt.Errorf("external provider endpoint is missing")
}
+ forwardParams := sanitizeExternalACPParams(method, params)
return requestExternalACP(
ctx,
endpoint,
provider.AuthorizationHeader,
method,
- params,
+ forwardParams,
notify,
)
}
+func sanitizeExternalACPParams(method string, params map[string]any) map[string]any {
+ if len(params) == 0 {
+ return map[string]any{}
+ }
+ next := make(map[string]any, len(params))
+ for key, value := range params {
+ next[key] = value
+ }
+ // Internal routing/runtime fields must not leak into external provider payloads.
+ delete(next, "metadata")
+ delete(next, "resolvedExecutionTarget")
+ delete(next, "resolvedEndpointTarget")
+ delete(next, "resolvedProviderId")
+ delete(next, "resolvedModel")
+ delete(next, "resolvedSkills")
+ delete(next, externalProviderEndpointKey)
+ delete(next, externalProviderAuthorizationHeaderKey)
+ delete(next, externalProviderLabelKey)
+ // Gateway-only fields are irrelevant in ACP single-agent forwarding.
+ normalizedMethod := strings.TrimSpace(method)
+ if normalizedMethod == "session.start" || normalizedMethod == "session.message" {
+ delete(next, "executionTarget")
+ delete(next, "agentId")
+ }
+ return next
+}
+
func externalProviderFromParams(params map[string]any) (syncedProvider, bool) {
endpoint := strings.TrimSpace(shared.StringArg(params, externalProviderEndpointKey, ""))
if endpoint == "" {
diff --git a/go/go_core/internal/acp/providers_sync_test.go b/go/go_core/internal/acp/providers_sync_test.go
index de78591e..c4bd5e85 100644
--- a/go/go_core/internal/acp/providers_sync_test.go
+++ b/go/go_core/internal/acp/providers_sync_test.go
@@ -78,6 +78,7 @@ func TestProvidersSyncUpdatesCapabilities(t *testing.T) {
}
func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
+ var lastForwardedParams map[string]any
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/acp/rpc" {
http.NotFound(w, r)
@@ -88,6 +89,7 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode request: %v", err)
}
+ lastForwardedParams = asMap(request["params"])
method, _ := request["method"].(string)
switch method {
case "session.start":
@@ -148,6 +150,12 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
if got := response["resolvedProviderId"]; got != "claude" {
t.Fatalf("expected resolved provider claude, got %#v", response)
}
+ if _, exists := lastForwardedParams["metadata"]; exists {
+ t.Fatalf("expected metadata to be stripped for external provider request, got %#v", lastForwardedParams)
+ }
+ if _, exists := lastForwardedParams[externalProviderEndpointKey]; exists {
+ t.Fatalf("expected internal endpoint key to be stripped, got %#v", lastForwardedParams)
+ }
}
func TestRunSingleAgentUsesFrozenExternalProviderParams(t *testing.T) {
diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart
index 9bd93a0b..7e4e9c87 100644
--- a/lib/app/app_controller_desktop_core.dart
+++ b/lib/app/app_controller_desktop_core.dart
@@ -22,7 +22,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
-import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
@@ -39,7 +38,7 @@ import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_mounts.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
-import '../runtime/single_agent_runner.dart';
+import '../runtime/single_agent_capabilities.dart';
import '../runtime/skill_directory_access.dart';
import 'task_thread_repositories.dart';
import 'app_controller_desktop_navigation.dart';
@@ -303,9 +302,9 @@ class AppController extends ChangeNotifier {
late final GoTaskServiceClient goTaskServiceClientInternal;
late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
late final MultiAgentMountManager multiAgentMountManagerInternal;
- Map
+ Map
singleAgentCapabilitiesByProviderInternal =
- const {};
+ const {};
final Map> assistantThreadMessagesInternal =
>{};
late final DesktopTaskThreadRepository taskThreadRepositoryInternal =
diff --git a/lib/app/app_controller_desktop_external_acp_routing.dart b/lib/app/app_controller_desktop_external_acp_routing.dart
index 3d1aa81a..e578de66 100644
--- a/lib/app/app_controller_desktop_external_acp_routing.dart
+++ b/lib/app/app_controller_desktop_external_acp_routing.dart
@@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
-import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
@@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
-import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_thread_sessions.dart';
diff --git a/lib/app/app_controller_desktop_gateway.dart b/lib/app/app_controller_desktop_gateway.dart
index c78c5b65..25c873a1 100644
--- a/lib/app/app_controller_desktop_gateway.dart
+++ b/lib/app/app_controller_desktop_gateway.dart
@@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
-import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
@@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
-import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart';
diff --git a/lib/app/app_controller_desktop_navigation.dart b/lib/app/app_controller_desktop_navigation.dart
index 2a62c18e..07db34a7 100644
--- a/lib/app/app_controller_desktop_navigation.dart
+++ b/lib/app/app_controller_desktop_navigation.dart
@@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
-import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
@@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
-import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_gateway.dart';
diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart
index 2e87aac8..0d5fd0c8 100644
--- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart
+++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart
@@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
-import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
@@ -32,7 +31,7 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
-import '../runtime/single_agent_runner.dart';
+import '../runtime/single_agent_capabilities.dart';
import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart';
@@ -90,15 +89,15 @@ Future refreshSingleAgentCapabilitiesRuntimeInternal(
target: AssistantExecutionTarget.singleAgent,
forceRefresh: forceRefresh,
);
- final next = {};
+ final next = {};
for (final provider in controller.configuredSingleAgentProviders) {
if (!capabilities.providers.contains(provider)) {
- next[provider] = const DirectSingleAgentCapabilities.unavailable(
+ next[provider] = const SingleAgentCapabilities.unavailable(
endpoint: '',
);
continue;
}
- next[provider] = DirectSingleAgentCapabilities(
+ next[provider] = SingleAgentCapabilities(
available: true,
supportedProviders: [provider],
endpoint: 'go-task-service',
diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart
index 79273de2..2b76634a 100644
--- a/lib/app/app_controller_desktop_runtime_helpers.dart
+++ b/lib/app/app_controller_desktop_runtime_helpers.dart
@@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
-import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
@@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
-import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart';
diff --git a/lib/app/app_controller_desktop_settings.dart b/lib/app/app_controller_desktop_settings.dart
index 179f0d41..a57c70c3 100644
--- a/lib/app/app_controller_desktop_settings.dart
+++ b/lib/app/app_controller_desktop_settings.dart
@@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
-import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
@@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
-import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart';
diff --git a/lib/app/app_controller_desktop_settings_runtime.dart b/lib/app/app_controller_desktop_settings_runtime.dart
index 3e8ee0a2..3eb2811b 100644
--- a/lib/app/app_controller_desktop_settings_runtime.dart
+++ b/lib/app/app_controller_desktop_settings_runtime.dart
@@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
-import '../runtime/direct_single_agent_app_server_client.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
@@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
-import '../runtime/single_agent_runner.dart';
import '../runtime/skill_directory_access.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart';
diff --git a/lib/app/app_controller_desktop_single_agent.dart b/lib/app/app_controller_desktop_single_agent.dart
index c0e09804..3937c535 100644
--- a/lib/app/app_controller_desktop_single_agent.dart
+++ b/lib/app/app_controller_desktop_single_agent.dart
@@ -1,5 +1,4 @@
import 'app_controller_desktop_core.dart';
-import 'app_controller_desktop_single_agent_ai_gateway.dart';
import 'app_controller_desktop_single_agent_go_task_flow.dart';
import '../runtime/runtime_models.dart';
@@ -19,10 +18,6 @@ extension AppControllerDesktopSingleAgent on AppController {
);
}
- Future abortAiGatewayRunInternal(String sessionKey) {
- return abortAiGatewaySingleAgentRunDesktopInternal(this, sessionKey);
- }
-
GatewayChatMessage assistantErrorMessageInternal(String text) {
return assistantErrorMessageSingleAgentDesktopInternal(this, text);
}
diff --git a/lib/app/app_controller_desktop_single_agent_ai_gateway.dart b/lib/app/app_controller_desktop_single_agent_ai_gateway.dart
deleted file mode 100644
index 0e1d672d..00000000
--- a/lib/app/app_controller_desktop_single_agent_ai_gateway.dart
+++ /dev/null
@@ -1,465 +0,0 @@
-// ignore_for_file: unused_import, unnecessary_import
-
-import 'dart:async';
-import 'dart:convert';
-import 'dart:io';
-import 'package:flutter/material.dart';
-import '../i18n/app_language.dart';
-import '../models/app_models.dart';
-import '../runtime/gateway_runtime_helpers.dart';
-import '../runtime/runtime_models.dart';
-import 'app_controller_desktop_core.dart';
-import 'app_controller_desktop_runtime_helpers.dart';
-import 'app_controller_desktop_skill_permissions.dart';
-import 'app_controller_desktop_thread_sessions.dart';
-import 'app_controller_desktop_thread_storage.dart';
-
-GatewayChatMessage assistantErrorMessageSingleAgentDesktopInternal(
- AppController controller,
- String text,
-) {
- return GatewayChatMessage(
- id: controller.nextLocalMessageIdInternal(),
- role: 'assistant',
- text: text,
- timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- toolCallId: null,
- toolName: null,
- stopReason: null,
- pending: false,
- error: true,
- );
-}
-
-Future sendAiGatewaySingleAgentMessageDesktopInternal(
- AppController controller,
- String message, {
- required String thinking,
- required List attachments,
- String? sessionKeyOverride,
- bool appendUserMessage = true,
- bool managePendingState = true,
-}) async {
- final sessionKey = controller.normalizedAssistantSessionKeyInternal(
- sessionKeyOverride ??
- controller.sessionsControllerInternal.currentSessionKey,
- );
- final trimmed = message.trim();
- if (trimmed.isEmpty && attachments.isEmpty) {
- return;
- }
-
- final baseUrl = controller.normalizeAiGatewayBaseUrlInternal(
- controller.aiGatewayUrl,
- );
- if (baseUrl == null) {
- controller.appendAssistantThreadMessageInternal(
- sessionKey,
- assistantErrorMessageSingleAgentDesktopInternal(
- controller,
- appText(
- 'LLM API Endpoint 未配置,无法发送对话。',
- 'LLM API Endpoint is not configured, so the conversation could not be sent.',
- ),
- ),
- );
- return;
- }
-
- final apiKey = await controller.loadAiGatewayApiKey();
- final allowsAnonymous =
- controller.isLoopbackHostInternal(baseUrl.host) &&
- (baseUrl.host.trim().toLowerCase() == '127.0.0.1' ||
- baseUrl.host.trim().toLowerCase() == 'localhost');
- if (apiKey.isEmpty && !allowsAnonymous) {
- controller.appendAssistantThreadMessageInternal(
- sessionKey,
- assistantErrorMessageSingleAgentDesktopInternal(
- controller,
- appText(
- 'LLM API Token 未配置,无法发送对话。',
- 'LLM API Token is not configured, so the conversation could not be sent.',
- ),
- ),
- );
- return;
- }
-
- final model = controller.resolvedAiGatewayModel;
- if (model.isEmpty) {
- controller.appendAssistantThreadMessageInternal(
- sessionKey,
- assistantErrorMessageSingleAgentDesktopInternal(
- controller,
- appText(
- '当前没有可用的 LLM API 对话模型。请先在 设置 -> 集成 中同步并选择可用模型。',
- 'No LLM API chat model is available yet. Sync and select a supported model in Settings -> Integrations first.',
- ),
- ),
- );
- return;
- }
-
- if (appendUserMessage) {
- final userText = trimmed.isEmpty ? 'See attached.' : trimmed;
- controller.appendAssistantThreadMessageInternal(
- sessionKey,
- GatewayChatMessage(
- id: controller.nextLocalMessageIdInternal(),
- role: 'user',
- text: userText,
- timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- toolCallId: null,
- toolName: null,
- stopReason: null,
- pending: false,
- error: false,
- ),
- );
- }
- if (managePendingState) {
- controller.aiGatewayPendingSessionKeysInternal.add(sessionKey);
- controller.recomputeTasksInternal();
- controller.notifyIfActiveInternal();
- }
-
- try {
- final assistantText =
- await requestAiGatewaySingleAgentCompletionDesktopInternal(
- controller,
- baseUrl: baseUrl,
- apiKey: apiKey,
- model: model,
- thinking: thinking,
- sessionKey: sessionKey,
- );
- controller.appendAssistantThreadMessageInternal(
- sessionKey,
- GatewayChatMessage(
- id: controller.nextLocalMessageIdInternal(),
- role: 'assistant',
- text: assistantText,
- timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- toolCallId: null,
- toolName: null,
- stopReason: null,
- pending: false,
- error: false,
- ),
- );
- controller.upsertTaskThreadInternal(
- sessionKey,
- gatewayEntryState: 'only-chat',
- latestResolvedRuntimeModel: model,
- lifecycleStatus: 'ready',
- lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- lastResultCode: 'success',
- updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- );
- } on AiGatewayAbortExceptionInternal catch (error) {
- final partial = error.partialText.trim();
- if (partial.isNotEmpty) {
- controller.appendAssistantThreadMessageInternal(
- sessionKey,
- GatewayChatMessage(
- id: controller.nextLocalMessageIdInternal(),
- role: 'assistant',
- text: partial,
- timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- toolCallId: null,
- toolName: null,
- stopReason: 'aborted',
- pending: false,
- error: false,
- ),
- );
- }
- controller.upsertTaskThreadInternal(
- sessionKey,
- lifecycleStatus: 'ready',
- lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- lastResultCode: 'aborted',
- updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- );
- } catch (error) {
- controller.upsertTaskThreadInternal(
- sessionKey,
- lifecycleStatus: 'ready',
- lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- lastResultCode: 'error',
- updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
- );
- controller.appendAssistantThreadMessageInternal(
- sessionKey,
- assistantErrorMessageSingleAgentDesktopInternal(
- controller,
- controller.aiGatewayErrorLabelInternal(error),
- ),
- );
- } finally {
- controller.aiGatewayStreamingClientsInternal.remove(sessionKey);
- controller.clearAiGatewayStreamingTextInternal(sessionKey);
- if (managePendingState) {
- controller.aiGatewayPendingSessionKeysInternal.remove(sessionKey);
- controller.recomputeTasksInternal();
- controller.notifyIfActiveInternal();
- }
- }
-}
-
-Future requestAiGatewaySingleAgentCompletionDesktopInternal(
- AppController controller, {
- required Uri baseUrl,
- required String apiKey,
- required String model,
- required String thinking,
- required String sessionKey,
-}) async {
- final uri = controller.aiGatewayChatUriInternal(baseUrl);
- final client = HttpClient()..connectionTimeout = const Duration(seconds: 20);
- controller.aiGatewayStreamingClientsInternal[sessionKey] = client;
- try {
- final request = await client
- .postUrl(uri)
- .timeout(const Duration(seconds: 20));
- request.headers.set(
- HttpHeaders.acceptHeader,
- 'text/event-stream, application/json',
- );
- request.headers.set(
- HttpHeaders.contentTypeHeader,
- 'application/json; charset=utf-8',
- );
- final trimmedApiKey = apiKey.trim();
- if (trimmedApiKey.isNotEmpty) {
- request.headers.set(
- HttpHeaders.authorizationHeader,
- 'Bearer $trimmedApiKey',
- );
- request.headers.set('x-api-key', trimmedApiKey);
- }
- final payload = {
- 'model': model,
- 'stream': true,
- 'messages': buildAiGatewaySingleAgentRequestMessagesDesktopInternal(
- controller,
- sessionKey,
- ),
- };
- final normalizedThinking = thinking.trim().toLowerCase();
- if (normalizedThinking.isNotEmpty && normalizedThinking != 'off') {
- payload['reasoning_effort'] = normalizedThinking;
- }
- request.add(utf8.encode(jsonEncode(payload)));
- final response = await request.close().timeout(const Duration(seconds: 60));
- if (response.statusCode < 200 || response.statusCode >= 300) {
- final body = await response.transform(utf8.decoder).join();
- throw AiGatewayChatExceptionInternal(
- controller.formatAiGatewayHttpErrorInternal(
- response.statusCode,
- controller.extractAiGatewayErrorDetailInternal(body),
- ),
- );
- }
- final contentType =
- response.headers.contentType?.mimeType.toLowerCase() ??
- response.headers.value(HttpHeaders.contentTypeHeader)?.toLowerCase() ??
- '';
- if (contentType.contains('text/event-stream')) {
- final streamed = await readAiGatewayStreamingResponseDesktopInternal(
- controller,
- response: response,
- sessionKey: sessionKey,
- );
- if (streamed.trim().isEmpty) {
- throw const FormatException('Missing assistant content');
- }
- return streamed.trim();
- }
- return await readAiGatewayJsonCompletionDesktopInternal(
- controller,
- response,
- );
- } catch (error) {
- if (consumeAiGatewaySingleAgentAbortDesktopInternal(
- controller,
- sessionKey,
- )) {
- throw AiGatewayAbortExceptionInternal(
- controller.aiGatewayStreamingTextBySessionInternal[sessionKey] ?? '',
- );
- }
- rethrow;
- } finally {
- controller.aiGatewayStreamingClientsInternal.remove(sessionKey);
- client.close(force: true);
- }
-}
-
-List