Unify single-agent task flow under ACP
This commit is contained in:
parent
69a339e91d
commit
b4bccf8300
1
agent.md
1
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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 控制面统一后删除
|
||||
|
||||
@ -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 == "" {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>[
|
||||
|
||||
@ -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 对应端点。',
|
||||
|
||||
@ -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';
|
||||
@ -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,
|
||||
@ -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.
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user