Refactor single-agent execution chain
This commit is contained in:
parent
078bb841c5
commit
1a4c00a1eb
@ -75,4 +75,100 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
|
||||
);
|
||||
await goTaskServiceClientInternal.syncExternalProviders(providers);
|
||||
}
|
||||
|
||||
ExternalCodeAgentAcpRoutingConfig buildExternalAcpRoutingForSessionInternal(
|
||||
String sessionKey, {
|
||||
String? explicitExecutionTarget,
|
||||
}) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final thread = assistantThreadRecordsInternal[normalizedSessionKey];
|
||||
final sessionTarget = assistantExecutionTargetForSession(
|
||||
normalizedSessionKey,
|
||||
);
|
||||
final preferredGatewayTarget = switch (sessionTarget) {
|
||||
AssistantExecutionTarget.auto => 'local',
|
||||
AssistantExecutionTarget.local => 'local',
|
||||
AssistantExecutionTarget.remote => 'remote',
|
||||
AssistantExecutionTarget.singleAgent =>
|
||||
settings.assistantExecutionTarget == AssistantExecutionTarget.remote
|
||||
? 'remote'
|
||||
: 'local',
|
||||
};
|
||||
final availableSkills =
|
||||
assistantImportedSkillsForSession(normalizedSessionKey)
|
||||
.map((item) {
|
||||
return ExternalCodeAgentAcpAvailableSkill(
|
||||
id: item.key,
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
final selectedSkills =
|
||||
assistantSelectedSkillsForSession(normalizedSessionKey)
|
||||
.map((item) {
|
||||
return item.label.trim().isNotEmpty ? item.label : item.key;
|
||||
})
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
|
||||
final resolvedExplicitExecutionTarget =
|
||||
sessionTarget == AssistantExecutionTarget.auto
|
||||
? ''
|
||||
: explicitExecutionTarget?.trim().isNotEmpty == true
|
||||
? explicitExecutionTarget!.trim()
|
||||
: (thread?.hasExplicitExecutionTargetSelection ?? false)
|
||||
? _routingExecutionTargetValueInternal(
|
||||
assistantExecutionTargetForSession(normalizedSessionKey),
|
||||
)
|
||||
: '';
|
||||
final resolvedExplicitProviderId =
|
||||
sessionTarget == AssistantExecutionTarget.auto
|
||||
? ''
|
||||
: thread?.hasExplicitProviderSelection ?? false
|
||||
? singleAgentProviderForSession(normalizedSessionKey).providerId
|
||||
: '';
|
||||
final resolvedExplicitModel = thread?.hasExplicitModelSelection ?? false
|
||||
? (sessionTarget == AssistantExecutionTarget.auto
|
||||
? ''
|
||||
: assistantModelForSession(normalizedSessionKey))
|
||||
: '';
|
||||
final resolvedExplicitSkills = thread?.hasExplicitSkillSelection ?? false
|
||||
? selectedSkills
|
||||
: const <String>[];
|
||||
final hasExplicitSelection =
|
||||
resolvedExplicitExecutionTarget.isNotEmpty ||
|
||||
resolvedExplicitProviderId.isNotEmpty ||
|
||||
resolvedExplicitModel.trim().isNotEmpty ||
|
||||
resolvedExplicitSkills.isNotEmpty;
|
||||
|
||||
if (!hasExplicitSelection) {
|
||||
return ExternalCodeAgentAcpRoutingConfig.auto(
|
||||
preferredGatewayTarget: preferredGatewayTarget,
|
||||
availableSkills: availableSkills,
|
||||
);
|
||||
}
|
||||
|
||||
return ExternalCodeAgentAcpRoutingConfig(
|
||||
mode: ExternalCodeAgentAcpRoutingMode.explicit,
|
||||
preferredGatewayTarget: preferredGatewayTarget,
|
||||
explicitExecutionTarget: resolvedExplicitExecutionTarget,
|
||||
explicitProviderId: resolvedExplicitProviderId,
|
||||
explicitModel: resolvedExplicitModel,
|
||||
explicitSkills: resolvedExplicitSkills,
|
||||
allowSkillInstall: false,
|
||||
availableSkills: availableSkills,
|
||||
);
|
||||
}
|
||||
|
||||
String _routingExecutionTargetValueInternal(AssistantExecutionTarget target) {
|
||||
return switch (target) {
|
||||
AssistantExecutionTarget.auto => 'singleAgent',
|
||||
AssistantExecutionTarget.singleAgent => 'singleAgent',
|
||||
AssistantExecutionTarget.local => 'local',
|
||||
AssistantExecutionTarget.remote => 'remote',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -532,7 +532,7 @@ extension AppControllerDesktopSettingsRuntime on AppController {
|
||||
if (disposedInternal) {
|
||||
return;
|
||||
}
|
||||
final startupTarget = sanitizeExecutionTargetInternal(
|
||||
final startupTarget = sanitizePersistedExecutionTargetInternal(
|
||||
settings.assistantExecutionTarget,
|
||||
);
|
||||
agentsControllerInternal.restoreSelection(
|
||||
@ -796,7 +796,7 @@ extension AppControllerDesktopSettingsRuntime on AppController {
|
||||
Future<void> applyPersistedGatewaySettingsInternal(
|
||||
SettingsSnapshot snapshot,
|
||||
) async {
|
||||
final target = sanitizeExecutionTargetInternal(
|
||||
final target = sanitizePersistedExecutionTargetInternal(
|
||||
snapshot.assistantExecutionTarget,
|
||||
);
|
||||
final sessionKey = normalizedAssistantSessionKeyInternal(
|
||||
@ -805,6 +805,8 @@ extension AppControllerDesktopSettingsRuntime on AppController {
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
executionTarget: target,
|
||||
gatewayEntryState: gatewayEntryStateForTargetInternal(target),
|
||||
latestResolvedRuntimeModel: '',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
|
||||
@ -1,52 +1,7 @@
|
||||
// ignore_for_file: unused_import, unnecessary_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_metadata.dart';
|
||||
import 'app_capabilities.dart';
|
||||
import 'app_store_policy.dart';
|
||||
import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
import '../runtime/gateway_runtime.dart';
|
||||
import '../runtime/runtime_controllers.dart';
|
||||
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';
|
||||
import '../runtime/code_agent_node_orchestrator.dart';
|
||||
import '../runtime/assistant_artifacts.dart';
|
||||
import '../runtime/desktop_thread_artifact_service.dart';
|
||||
import '../runtime/go_task_service_client.dart';
|
||||
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';
|
||||
import 'app_controller_desktop_gateway.dart';
|
||||
import 'app_controller_desktop_settings.dart';
|
||||
import 'app_controller_desktop_thread_sessions.dart';
|
||||
import 'app_controller_desktop_thread_actions.dart';
|
||||
import 'app_controller_desktop_workspace_execution.dart';
|
||||
import 'app_controller_desktop_settings_runtime.dart';
|
||||
import 'app_controller_desktop_thread_storage.dart';
|
||||
import 'app_controller_desktop_skill_permissions.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_go_task_flow.dart';
|
||||
import '../runtime/runtime_models.dart';
|
||||
|
||||
extension AppControllerDesktopSingleAgent on AppController {
|
||||
Future<void> sendSingleAgentMessageInternal(
|
||||
@ -54,919 +9,21 @@ extension AppControllerDesktopSingleAgent on AppController {
|
||||
required String thinking,
|
||||
required List<GatewayChatAttachmentPayload> attachments,
|
||||
required List<CollaborationAttachment> localAttachments,
|
||||
}) async {
|
||||
final sessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
}) {
|
||||
return sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
this,
|
||||
message,
|
||||
thinking: thinking,
|
||||
attachments: attachments,
|
||||
localAttachments: localAttachments,
|
||||
);
|
||||
final trimmed = message.trim();
|
||||
if (trimmed.isEmpty && attachments.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await enqueueThreadTurnInternal<void>(sessionKey, () async {
|
||||
final sessionTarget = assistantExecutionTargetForSession(sessionKey);
|
||||
final userText = trimmed.isEmpty ? 'See attached.' : trimmed;
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'user',
|
||||
text: userText,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
aiGatewayPendingSessionKeysInternal.add(sessionKey);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
|
||||
try {
|
||||
final routing = buildExternalAcpRoutingForSessionInternal(sessionKey);
|
||||
final selection = singleAgentProviderForSession(sessionKey);
|
||||
await syncExternalAcpProvidersInternal();
|
||||
final capabilities = await goTaskServiceClientInternal
|
||||
.loadExternalAcpCapabilities(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
forceRefresh: true,
|
||||
);
|
||||
final availableProviders = configuredSingleAgentProviders
|
||||
.where(capabilities.providers.contains)
|
||||
.toList(growable: false);
|
||||
final provider = selection == SingleAgentProvider.auto
|
||||
? (availableProviders.isEmpty ? null : availableProviders.first)
|
||||
: (capabilities.providers.contains(selection) ? selection : null);
|
||||
final fallbackReason = provider == null
|
||||
? (selection == SingleAgentProvider.auto
|
||||
? appText(
|
||||
'当前没有可用的 GoTaskService Provider。',
|
||||
'No GoTaskService provider is currently available.',
|
||||
)
|
||||
: appText(
|
||||
'当前 GoTaskService 不支持 ${selection.label}。',
|
||||
'GoTaskService does not currently support ${selection.label}.',
|
||||
))
|
||||
: null;
|
||||
if (provider == null && !routing.isAuto) {
|
||||
if (singleAgentUsesAiChatFallbackForSession(sessionKey)) {
|
||||
appendSingleAgentFallbackStatusMessageInternal(
|
||||
sessionKey,
|
||||
fallbackReason,
|
||||
);
|
||||
await sendAiGatewayMessageInternal(
|
||||
message,
|
||||
thinking: thinking,
|
||||
attachments: attachments,
|
||||
sessionKeyOverride: sessionKey,
|
||||
appendUserMessage: false,
|
||||
managePendingState: false,
|
||||
);
|
||||
} else {
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: singleAgentUnavailableLabelInternal(
|
||||
sessionKey,
|
||||
fallbackReason,
|
||||
),
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: singleAgentRuntimeDebugToolNameInternal(
|
||||
provider?.label ?? selection.label,
|
||||
),
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final effectiveProvider = sessionTarget == AssistantExecutionTarget.auto
|
||||
? SingleAgentProvider.auto
|
||||
: (provider ?? SingleAgentProvider.auto);
|
||||
|
||||
appendSingleAgentRuntimeStatusMessageInternal(
|
||||
sessionKey,
|
||||
effectiveProvider,
|
||||
);
|
||||
final workingDirectory =
|
||||
resolveSingleAgentWorkingDirectoryForSessionInternal(
|
||||
sessionKey,
|
||||
provider: provider,
|
||||
);
|
||||
if (workingDirectory == null || workingDirectory.trim().isEmpty) {
|
||||
final error = StateError(
|
||||
appText(
|
||||
'当前线程缺少可运行的工作路径,无法启动单机智能体。',
|
||||
'This thread does not have a runnable workspace path, so Single Agent cannot start.',
|
||||
),
|
||||
);
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(error.message),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
final selectedSkills = assistantSelectedSkillsForSession(sessionKey)
|
||||
.map((item) => item.label.trim().isNotEmpty ? item.label : item.key)
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
final result = await goTaskServiceClientInternal.executeTask(
|
||||
GoTaskServiceRequest(
|
||||
sessionId: sessionKey,
|
||||
threadId: sessionKey,
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
prompt: message,
|
||||
workingDirectory: workingDirectory,
|
||||
model: sessionTarget == AssistantExecutionTarget.auto
|
||||
? ''
|
||||
: assistantModelForSession(sessionKey),
|
||||
thinking: thinking,
|
||||
selectedSkills: selectedSkills,
|
||||
inlineAttachments: attachments,
|
||||
localAttachments: localAttachments,
|
||||
aiGatewayBaseUrl: aiGatewayUrl,
|
||||
aiGatewayApiKey: await loadAiGatewayApiKey(),
|
||||
agentId: '',
|
||||
metadata: const <String, dynamic>{},
|
||||
routing: routing,
|
||||
provider: effectiveProvider,
|
||||
),
|
||||
onUpdate: (update) {
|
||||
if (update.isDelta) {
|
||||
appendAiGatewayStreamingTextInternal(sessionKey, update.text);
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
},
|
||||
);
|
||||
final resolvedRuntimeModel = result.resolvedModel.trim();
|
||||
final resolvedGatewayEntryState = goTaskServiceGatewayEntryState(
|
||||
requestedTarget: sessionTarget,
|
||||
result: result,
|
||||
);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
gatewayEntryState: resolvedGatewayEntryState,
|
||||
latestResolvedRuntimeModel: resolvedRuntimeModel,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: result.success ? 'success' : 'error',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
final resolvedWorkspaceKind = result.resolvedWorkspaceRefKind;
|
||||
final resolvedWorkingDirectory = result.resolvedWorkingDirectory.trim();
|
||||
if (resolvedWorkspaceKind != null &&
|
||||
resolvedWorkingDirectory.isNotEmpty) {
|
||||
final existingThread = requireTaskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: existingThread.workspaceBinding.workspaceId,
|
||||
workspaceKind:
|
||||
resolvedWorkspaceKind == WorkspaceRefKind.remotePath
|
||||
? WorkspaceKind.remoteFs
|
||||
: WorkspaceKind.localFs,
|
||||
workspacePath: resolvedWorkingDirectory,
|
||||
displayPath: resolvedWorkingDirectory,
|
||||
writable: existingThread.workspaceBinding.writable,
|
||||
),
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
}
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
if (!result.success &&
|
||||
singleAgentUsesAiChatFallbackForSession(sessionKey)) {
|
||||
if (singleAgentUsesAiChatFallbackForSession(sessionKey)) {
|
||||
appendSingleAgentFallbackStatusMessageInternal(
|
||||
sessionKey,
|
||||
result.errorMessage,
|
||||
);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
gatewayEntryState: 'only-chat',
|
||||
latestResolvedRuntimeModel: resolvedAiGatewayModel,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: 'fallback',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
await sendAiGatewayMessageInternal(
|
||||
message,
|
||||
thinking: thinking,
|
||||
attachments: attachments,
|
||||
sessionKeyOverride: sessionKey,
|
||||
appendUserMessage: false,
|
||||
managePendingState: false,
|
||||
);
|
||||
} else {
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: singleAgentUnavailableLabelInternal(
|
||||
sessionKey,
|
||||
result.errorMessage,
|
||||
),
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: singleAgentRuntimeDebugToolNameInternal(
|
||||
effectiveProvider.label,
|
||||
),
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
appText(
|
||||
'GoTaskService 执行失败:${result.errorMessage}',
|
||||
'GoTaskService execution failed: ${result.errorMessage}',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.message.trim().isEmpty) {
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
appText(
|
||||
'GoTaskService 没有返回可显示的输出。',
|
||||
'GoTaskService returned no displayable output.',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: result.message,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: 'error',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(error.toString()),
|
||||
);
|
||||
} finally {
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
aiGatewayPendingSessionKeysInternal.remove(sessionKey);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> sendAiGatewayMessageInternal(
|
||||
String message, {
|
||||
required String thinking,
|
||||
required List<GatewayChatAttachmentPayload> attachments,
|
||||
String? sessionKeyOverride,
|
||||
bool appendUserMessage = true,
|
||||
bool managePendingState = true,
|
||||
}) async {
|
||||
final sessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKeyOverride ?? sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
final trimmed = message.trim();
|
||||
if (trimmed.isEmpty && attachments.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final baseUrl = normalizeAiGatewayBaseUrlInternal(aiGatewayUrl);
|
||||
if (baseUrl == null) {
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
appText(
|
||||
'LLM API Endpoint 未配置,无法发送对话。',
|
||||
'LLM API Endpoint is not configured, so the conversation could not be sent.',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final apiKey = await loadAiGatewayApiKey();
|
||||
final allowsAnonymous =
|
||||
isLoopbackHostInternal(baseUrl.host) &&
|
||||
(baseUrl.host.trim().toLowerCase() == '127.0.0.1' ||
|
||||
baseUrl.host.trim().toLowerCase() == 'localhost');
|
||||
if (apiKey.isEmpty && !allowsAnonymous) {
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
appText(
|
||||
'LLM API Token 未配置,无法发送对话。',
|
||||
'LLM API Token is not configured, so the conversation could not be sent.',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final model = resolvedAiGatewayModel;
|
||||
if (model.isEmpty) {
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
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;
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'user',
|
||||
text: userText,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (managePendingState) {
|
||||
aiGatewayPendingSessionKeysInternal.add(sessionKey);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
|
||||
try {
|
||||
final assistantText = await requestAiGatewayCompletionInternal(
|
||||
baseUrl: baseUrl,
|
||||
apiKey: apiKey,
|
||||
model: model,
|
||||
thinking: thinking,
|
||||
sessionKey: sessionKey,
|
||||
);
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: assistantText,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
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) {
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: partial,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: 'aborted',
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: 'aborted',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
} catch (error) {
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: 'error',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(aiGatewayErrorLabelInternal(error)),
|
||||
);
|
||||
} finally {
|
||||
aiGatewayStreamingClientsInternal.remove(sessionKey);
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
if (managePendingState) {
|
||||
aiGatewayPendingSessionKeysInternal.remove(sessionKey);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> requestAiGatewayCompletionInternal({
|
||||
required Uri baseUrl,
|
||||
required String apiKey,
|
||||
required String model,
|
||||
required String thinking,
|
||||
required String sessionKey,
|
||||
}) async {
|
||||
final uri = aiGatewayChatUriInternal(baseUrl);
|
||||
final client = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 20);
|
||||
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': buildAiGatewayRequestMessagesInternal(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(
|
||||
formatAiGatewayHttpErrorInternal(
|
||||
response.statusCode,
|
||||
extractAiGatewayErrorDetailInternal(body),
|
||||
),
|
||||
);
|
||||
}
|
||||
final contentType =
|
||||
response.headers.contentType?.mimeType.toLowerCase() ??
|
||||
response.headers
|
||||
.value(HttpHeaders.contentTypeHeader)
|
||||
?.toLowerCase() ??
|
||||
'';
|
||||
if (contentType.contains('text/event-stream')) {
|
||||
final streamed = await readAiGatewayStreamingResponseInternal(
|
||||
response: response,
|
||||
sessionKey: sessionKey,
|
||||
);
|
||||
if (streamed.trim().isEmpty) {
|
||||
throw const FormatException('Missing assistant content');
|
||||
}
|
||||
return streamed.trim();
|
||||
}
|
||||
return await readAiGatewayJsonCompletionInternal(response);
|
||||
} catch (error) {
|
||||
if (consumeAiGatewayAbortInternal(sessionKey)) {
|
||||
throw AiGatewayAbortExceptionInternal(
|
||||
aiGatewayStreamingTextBySessionInternal[sessionKey] ?? '',
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
} finally {
|
||||
aiGatewayStreamingClientsInternal.remove(sessionKey);
|
||||
client.close(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, String>> buildAiGatewayRequestMessagesInternal(
|
||||
String sessionKey,
|
||||
) {
|
||||
final history = <GatewayChatMessage>[
|
||||
...(gatewayHistoryCacheInternal[sessionKey] ??
|
||||
const <GatewayChatMessage>[]),
|
||||
...(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> readAiGatewayJsonCompletionInternal(
|
||||
HttpClientResponse response,
|
||||
) async {
|
||||
final body = await response.transform(utf8.decoder).join();
|
||||
final decoded = jsonDecode(extractFirstJsonDocumentInternal(body));
|
||||
final assistantText = extractAiGatewayAssistantTextInternal(decoded);
|
||||
if (assistantText.trim().isEmpty) {
|
||||
throw const FormatException('Missing assistant content');
|
||||
}
|
||||
return assistantText.trim();
|
||||
}
|
||||
|
||||
Future<String> readAiGatewayStreamingResponseInternal({
|
||||
required HttpClientResponse response,
|
||||
required String sessionKey,
|
||||
}) async {
|
||||
final buffer = StringBuffer();
|
||||
final eventLines = <String>[];
|
||||
|
||||
void processEvent(String payload) {
|
||||
final trimmed = payload.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (trimmed == '[DONE]') {
|
||||
return;
|
||||
}
|
||||
final deltaText = extractAiGatewayStreamTextInternal(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);
|
||||
}
|
||||
setAiGatewayStreamingTextInternal(sessionKey, buffer.toString());
|
||||
}
|
||||
|
||||
await for (final line
|
||||
in response.transform(utf8.decoder).transform(const LineSplitter())) {
|
||||
if (consumeAiGatewayAbortInternal(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 extractAiGatewayStreamTextInternal(String payload) {
|
||||
final decoded = jsonDecode(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 = extractAiGatewayContentInternal(delta['content']);
|
||||
if (deltaContent.isNotEmpty) {
|
||||
return deltaContent;
|
||||
}
|
||||
}
|
||||
return extractAiGatewayAssistantTextInternal(decoded);
|
||||
}
|
||||
|
||||
Future<void> abortAiGatewayRunInternal(String sessionKey) async {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
aiGatewayAbortedSessionKeysInternal.add(normalizedSessionKey);
|
||||
final client = aiGatewayStreamingClientsInternal.remove(
|
||||
normalizedSessionKey,
|
||||
);
|
||||
if (client != null) {
|
||||
try {
|
||||
client.close(force: true);
|
||||
} catch (_) {
|
||||
// Best effort only.
|
||||
}
|
||||
}
|
||||
aiGatewayPendingSessionKeysInternal.remove(normalizedSessionKey);
|
||||
clearAiGatewayStreamingTextInternal(normalizedSessionKey);
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: 'aborted',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
|
||||
bool consumeAiGatewayAbortInternal(String sessionKey) {
|
||||
return aiGatewayAbortedSessionKeysInternal.remove(
|
||||
normalizedAssistantSessionKeyInternal(sessionKey),
|
||||
);
|
||||
Future<void> abortAiGatewayRunInternal(String sessionKey) {
|
||||
return abortAiGatewaySingleAgentRunDesktopInternal(this, sessionKey);
|
||||
}
|
||||
|
||||
GatewayChatMessage assistantErrorMessageInternal(String text) {
|
||||
return GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: text,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: true,
|
||||
);
|
||||
}
|
||||
|
||||
String? singleAgentRuntimeDebugToolNameInternal(String label) {
|
||||
if (!showsSingleAgentRuntimeDebugMessagesInternal) {
|
||||
return null;
|
||||
}
|
||||
final trimmed = label.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
void appendSingleAgentRuntimeStatusMessageInternal(
|
||||
String sessionKey,
|
||||
SingleAgentProvider provider,
|
||||
) {
|
||||
if (!showsSingleAgentRuntimeDebugMessagesInternal) {
|
||||
return;
|
||||
}
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: appText(
|
||||
'单机智能体已切换到 ${provider.label} 执行当前任务。',
|
||||
'Single Agent is using ${provider.label} for this task.',
|
||||
),
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: provider.label,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void appendSingleAgentFallbackStatusMessageInternal(
|
||||
String sessionKey,
|
||||
String? reason,
|
||||
) {
|
||||
if (!showsSingleAgentRuntimeDebugMessagesInternal) {
|
||||
return;
|
||||
}
|
||||
appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: singleAgentFallbackLabelInternal(reason),
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: 'AI Chat fallback',
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String singleAgentFallbackLabelInternal(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 singleAgentUnavailableLabelInternal(
|
||||
String sessionKey,
|
||||
String? reason,
|
||||
) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final detail = reason?.trim() ?? '';
|
||||
final selection = singleAgentProviderForSession(normalizedSessionKey);
|
||||
if (singleAgentShouldSuggestAutoSwitchForSession(normalizedSessionKey)) {
|
||||
return detail.isEmpty
|
||||
? appText(
|
||||
'当前线程固定为 ${selection.label},但它在这台设备上不可用。检测到其他外部 Agent ACP 端点时不会自动改线,可切到 Auto。',
|
||||
'This thread is pinned to ${selection.label}, but it is unavailable on this device. XWorkmate will not reroute to another external Agent ACP endpoint automatically. Switch to Auto instead.',
|
||||
)
|
||||
: appText(
|
||||
'当前线程固定为 ${selection.label}:$detail 检测到其他外部 Agent ACP 端点时不会自动改线,可切到 Auto。',
|
||||
'This thread is pinned to ${selection.label}: $detail XWorkmate will not reroute to another external Agent ACP endpoint automatically. Switch to Auto instead.',
|
||||
);
|
||||
}
|
||||
if (singleAgentNeedsAiGatewayConfigurationForSession(
|
||||
normalizedSessionKey,
|
||||
)) {
|
||||
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.',
|
||||
)
|
||||
: 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.',
|
||||
);
|
||||
}
|
||||
return detail.isEmpty
|
||||
? appText(
|
||||
'当前线程的外部 Agent ACP 连接尚未就绪。',
|
||||
'The external Agent ACP connection for this thread is not ready yet.',
|
||||
)
|
||||
: appText(
|
||||
'当前线程的外部 Agent ACP 连接尚未就绪:$detail',
|
||||
'The external Agent ACP connection for this thread is not ready yet: $detail',
|
||||
);
|
||||
}
|
||||
|
||||
ExternalCodeAgentAcpRoutingConfig buildExternalAcpRoutingForSessionInternal(
|
||||
String sessionKey, {
|
||||
String? explicitExecutionTarget,
|
||||
}) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final thread = assistantThreadRecordsInternal[normalizedSessionKey];
|
||||
final sessionTarget = assistantExecutionTargetForSession(
|
||||
normalizedSessionKey,
|
||||
);
|
||||
final preferredGatewayTarget = switch (sessionTarget) {
|
||||
AssistantExecutionTarget.auto => 'local',
|
||||
AssistantExecutionTarget.local => 'local',
|
||||
AssistantExecutionTarget.remote => 'remote',
|
||||
AssistantExecutionTarget.singleAgent =>
|
||||
settings.assistantExecutionTarget == AssistantExecutionTarget.remote
|
||||
? 'remote'
|
||||
: 'local',
|
||||
};
|
||||
final availableSkills =
|
||||
assistantImportedSkillsForSession(normalizedSessionKey)
|
||||
.map(
|
||||
(item) => ExternalCodeAgentAcpAvailableSkill(
|
||||
id: item.key,
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
final selectedSkills =
|
||||
assistantSelectedSkillsForSession(normalizedSessionKey)
|
||||
.map((item) => item.label.trim().isNotEmpty ? item.label : item.key)
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
|
||||
final resolvedExplicitExecutionTarget =
|
||||
sessionTarget == AssistantExecutionTarget.auto
|
||||
? ''
|
||||
: explicitExecutionTarget?.trim().isNotEmpty == true
|
||||
? explicitExecutionTarget!.trim()
|
||||
: (thread?.hasExplicitExecutionTargetSelection ?? false)
|
||||
? _routingExecutionTargetValue(
|
||||
assistantExecutionTargetForSession(normalizedSessionKey),
|
||||
)
|
||||
: '';
|
||||
final resolvedExplicitProviderId =
|
||||
sessionTarget == AssistantExecutionTarget.auto
|
||||
? ''
|
||||
: thread?.hasExplicitProviderSelection ?? false
|
||||
? singleAgentProviderForSession(normalizedSessionKey).providerId
|
||||
: '';
|
||||
final resolvedExplicitModel = thread?.hasExplicitModelSelection ?? false
|
||||
? (sessionTarget == AssistantExecutionTarget.auto
|
||||
? ''
|
||||
: assistantModelForSession(normalizedSessionKey))
|
||||
: '';
|
||||
final resolvedExplicitSkills = thread?.hasExplicitSkillSelection ?? false
|
||||
? selectedSkills
|
||||
: const <String>[];
|
||||
final hasExplicitSelection =
|
||||
resolvedExplicitExecutionTarget.isNotEmpty ||
|
||||
resolvedExplicitProviderId.isNotEmpty ||
|
||||
resolvedExplicitModel.trim().isNotEmpty ||
|
||||
resolvedExplicitSkills.isNotEmpty;
|
||||
|
||||
if (!hasExplicitSelection) {
|
||||
return ExternalCodeAgentAcpRoutingConfig.auto(
|
||||
preferredGatewayTarget: preferredGatewayTarget,
|
||||
availableSkills: availableSkills,
|
||||
);
|
||||
}
|
||||
|
||||
return ExternalCodeAgentAcpRoutingConfig(
|
||||
mode: ExternalCodeAgentAcpRoutingMode.explicit,
|
||||
preferredGatewayTarget: preferredGatewayTarget,
|
||||
explicitExecutionTarget: resolvedExplicitExecutionTarget,
|
||||
explicitProviderId: resolvedExplicitProviderId,
|
||||
explicitModel: resolvedExplicitModel,
|
||||
explicitSkills: resolvedExplicitSkills,
|
||||
allowSkillInstall: false,
|
||||
availableSkills: availableSkills,
|
||||
);
|
||||
}
|
||||
|
||||
String _routingExecutionTargetValue(AssistantExecutionTarget target) {
|
||||
return switch (target) {
|
||||
AssistantExecutionTarget.auto => 'singleAgent',
|
||||
AssistantExecutionTarget.singleAgent => 'singleAgent',
|
||||
AssistantExecutionTarget.local => 'local',
|
||||
AssistantExecutionTarget.remote => 'remote',
|
||||
};
|
||||
return assistantErrorMessageSingleAgentDesktopInternal(this, text);
|
||||
}
|
||||
}
|
||||
|
||||
465
lib/app/app_controller_desktop_single_agent_ai_gateway.dart
Normal file
465
lib/app/app_controller_desktop_single_agent_ai_gateway.dart
Normal file
@ -0,0 +1,465 @@
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
354
lib/app/app_controller_desktop_single_agent_go_task_flow.dart
Normal file
354
lib/app/app_controller_desktop_single_agent_go_task_flow.dart
Normal file
@ -0,0 +1,354 @@
|
||||
// ignore_for_file: unused_import, unnecessary_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/go_task_service_client.dart';
|
||||
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';
|
||||
import 'app_controller_desktop_skill_permissions.dart';
|
||||
|
||||
Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
AppController controller,
|
||||
String message, {
|
||||
required String thinking,
|
||||
required List<GatewayChatAttachmentPayload> attachments,
|
||||
required List<CollaborationAttachment> localAttachments,
|
||||
}) async {
|
||||
final sessionKey = controller.normalizedAssistantSessionKeyInternal(
|
||||
controller.sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
final trimmed = message.trim();
|
||||
if (trimmed.isEmpty && attachments.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await controller.enqueueThreadTurnInternal<void>(sessionKey, () async {
|
||||
final sessionTarget = controller.assistantExecutionTargetForSession(
|
||||
sessionKey,
|
||||
);
|
||||
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,
|
||||
),
|
||||
);
|
||||
controller.aiGatewayPendingSessionKeysInternal.add(sessionKey);
|
||||
controller.recomputeTasksInternal();
|
||||
controller.notifyIfActiveInternal();
|
||||
|
||||
try {
|
||||
final routing = controller.buildExternalAcpRoutingForSessionInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final selection = controller.singleAgentProviderForSession(sessionKey);
|
||||
await controller.syncExternalAcpProvidersInternal();
|
||||
final capabilities = await controller.goTaskServiceClientInternal
|
||||
.loadExternalAcpCapabilities(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
forceRefresh: true,
|
||||
);
|
||||
final availableProviders = controller.configuredSingleAgentProviders
|
||||
.where(capabilities.providers.contains)
|
||||
.toList(growable: false);
|
||||
final provider = selection == SingleAgentProvider.auto
|
||||
? (availableProviders.isEmpty ? null : availableProviders.first)
|
||||
: (capabilities.providers.contains(selection) ? selection : null);
|
||||
final fallbackReason = provider == null
|
||||
? (selection == SingleAgentProvider.auto
|
||||
? appText(
|
||||
'当前没有可用的 GoTaskService Provider。',
|
||||
'No GoTaskService provider is currently available.',
|
||||
)
|
||||
: appText(
|
||||
'当前 GoTaskService 不支持 ${selection.label}。',
|
||||
'GoTaskService does not currently support ${selection.label}.',
|
||||
))
|
||||
: null;
|
||||
if (provider == null && !routing.isAuto) {
|
||||
if (controller.singleAgentUsesAiChatFallbackForSession(sessionKey)) {
|
||||
appendSingleAgentFallbackStatusDesktopInternal(
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final effectiveProvider = sessionTarget == AssistantExecutionTarget.auto
|
||||
? SingleAgentProvider.auto
|
||||
: (provider ?? SingleAgentProvider.auto);
|
||||
|
||||
appendSingleAgentRuntimeStatusDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
effectiveProvider,
|
||||
);
|
||||
final workingDirectory = controller
|
||||
.resolveSingleAgentWorkingDirectoryForSessionInternal(
|
||||
sessionKey,
|
||||
provider: provider,
|
||||
);
|
||||
if (workingDirectory == null || workingDirectory.trim().isEmpty) {
|
||||
final error = StateError(
|
||||
appText(
|
||||
'当前线程缺少可运行的工作路径,无法启动单机智能体。',
|
||||
'This thread does not have a runnable workspace path, so Single Agent cannot start.',
|
||||
),
|
||||
);
|
||||
controller.appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageSingleAgentDesktopInternal(
|
||||
controller,
|
||||
error.message,
|
||||
),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
final selectedSkills = controller
|
||||
.assistantSelectedSkillsForSession(sessionKey)
|
||||
.map((item) => item.label.trim().isNotEmpty ? item.label : item.key)
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
final result = await controller.goTaskServiceClientInternal.executeTask(
|
||||
GoTaskServiceRequest(
|
||||
sessionId: sessionKey,
|
||||
threadId: sessionKey,
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
prompt: message,
|
||||
workingDirectory: workingDirectory,
|
||||
model: sessionTarget == AssistantExecutionTarget.auto
|
||||
? ''
|
||||
: controller.assistantModelForSession(sessionKey),
|
||||
thinking: thinking,
|
||||
selectedSkills: selectedSkills,
|
||||
inlineAttachments: attachments,
|
||||
localAttachments: localAttachments,
|
||||
aiGatewayBaseUrl: controller.aiGatewayUrl,
|
||||
aiGatewayApiKey: await controller.loadAiGatewayApiKey(),
|
||||
agentId: '',
|
||||
metadata: const <String, dynamic>{},
|
||||
routing: routing,
|
||||
provider: effectiveProvider,
|
||||
),
|
||||
onUpdate: (update) {
|
||||
if (update.isDelta) {
|
||||
controller.appendAiGatewayStreamingTextInternal(
|
||||
sessionKey,
|
||||
update.text,
|
||||
);
|
||||
controller.notifyIfActiveInternal();
|
||||
}
|
||||
},
|
||||
);
|
||||
_applySingleAgentGoTaskResultDesktopInternal(
|
||||
controller,
|
||||
sessionKey: sessionKey,
|
||||
sessionTarget: sessionTarget,
|
||||
message: message,
|
||||
thinking: thinking,
|
||||
attachments: attachments,
|
||||
result: result,
|
||||
);
|
||||
} catch (error) {
|
||||
controller.clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
controller.upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: 'error',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
controller.appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageSingleAgentDesktopInternal(
|
||||
controller,
|
||||
error.toString(),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
controller.clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
controller.aiGatewayPendingSessionKeysInternal.remove(sessionKey);
|
||||
controller.recomputeTasksInternal();
|
||||
controller.notifyIfActiveInternal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _applySingleAgentGoTaskResultDesktopInternal(
|
||||
AppController controller, {
|
||||
required String sessionKey,
|
||||
required AssistantExecutionTarget sessionTarget,
|
||||
required String message,
|
||||
required String thinking,
|
||||
required List<GatewayChatAttachmentPayload> attachments,
|
||||
required GoTaskServiceResult result,
|
||||
}) {
|
||||
final resolvedRuntimeModel = result.resolvedModel.trim();
|
||||
final resolvedGatewayEntryState = goTaskServiceGatewayEntryState(
|
||||
requestedTarget: sessionTarget,
|
||||
result: result,
|
||||
);
|
||||
controller.upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
gatewayEntryState: resolvedGatewayEntryState,
|
||||
latestResolvedRuntimeModel: resolvedRuntimeModel,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: result.success ? 'success' : 'error',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
_updateSingleAgentWorkspaceBindingFromResultDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
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,
|
||||
assistantErrorMessageSingleAgentDesktopInternal(
|
||||
controller,
|
||||
appText(
|
||||
'GoTaskService 执行失败:${result.errorMessage}',
|
||||
'GoTaskService execution failed: ${result.errorMessage}',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.message.trim().isEmpty) {
|
||||
controller.appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageSingleAgentDesktopInternal(
|
||||
controller,
|
||||
appText(
|
||||
'GoTaskService 没有返回可显示的输出。',
|
||||
'GoTaskService returned no displayable output.',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: controller.nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: result.message,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateSingleAgentWorkspaceBindingFromResultDesktopInternal(
|
||||
AppController controller,
|
||||
String sessionKey,
|
||||
GoTaskServiceResult result,
|
||||
) {
|
||||
final resolvedWorkspaceKind = result.resolvedWorkspaceRefKind;
|
||||
final resolvedWorkingDirectory = result.resolvedWorkingDirectory.trim();
|
||||
if (resolvedWorkspaceKind == null || resolvedWorkingDirectory.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final existingThread = controller.requireTaskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
);
|
||||
controller.upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: existingThread.workspaceBinding.workspaceId,
|
||||
workspaceKind: resolvedWorkspaceKind == WorkspaceRefKind.remotePath
|
||||
? WorkspaceKind.remoteFs
|
||||
: WorkspaceKind.localFs,
|
||||
workspacePath: resolvedWorkingDirectory,
|
||||
displayPath: resolvedWorkingDirectory,
|
||||
writable: existingThread.workspaceBinding.writable,
|
||||
),
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
}
|
||||
136
lib/app/app_controller_desktop_single_agent_status_messages.dart
Normal file
136
lib/app/app_controller_desktop_single_agent_status_messages.dart
Normal file
@ -0,0 +1,136 @@
|
||||
// ignore_for_file: unused_import, unnecessary_import
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../runtime/runtime_models.dart';
|
||||
import 'app_controller_desktop_core.dart';
|
||||
import 'app_controller_desktop_runtime_helpers.dart';
|
||||
import 'app_controller_desktop_thread_sessions.dart';
|
||||
import 'app_controller_desktop_thread_storage.dart';
|
||||
|
||||
String? singleAgentRuntimeDebugToolNameDesktopInternal(
|
||||
AppController controller,
|
||||
String label,
|
||||
) {
|
||||
if (!controller.showsSingleAgentRuntimeDebugMessagesInternal) {
|
||||
return null;
|
||||
}
|
||||
final trimmed = label.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
void appendSingleAgentRuntimeStatusDesktopInternal(
|
||||
AppController controller,
|
||||
String sessionKey,
|
||||
SingleAgentProvider provider,
|
||||
) {
|
||||
if (!controller.showsSingleAgentRuntimeDebugMessagesInternal) {
|
||||
return;
|
||||
}
|
||||
controller.appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: controller.nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: appText(
|
||||
'单机智能体已切换到 ${provider.label} 执行当前任务。',
|
||||
'Single Agent is using ${provider.label} for this task.',
|
||||
),
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: provider.label,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
String? reason,
|
||||
) {
|
||||
final normalizedSessionKey = controller.normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final detail = reason?.trim() ?? '';
|
||||
final selection = controller.singleAgentProviderForSession(
|
||||
normalizedSessionKey,
|
||||
);
|
||||
if (controller.singleAgentShouldSuggestAutoSwitchForSession(
|
||||
normalizedSessionKey,
|
||||
)) {
|
||||
return detail.isEmpty
|
||||
? appText(
|
||||
'当前线程固定为 ${selection.label},但它在这台设备上不可用。检测到其他外部 Agent ACP 端点时不会自动改线,可切到 Auto。',
|
||||
'This thread is pinned to ${selection.label}, but it is unavailable on this device. XWorkmate will not reroute to another external Agent ACP endpoint automatically. Switch to Auto instead.',
|
||||
)
|
||||
: appText(
|
||||
'当前线程固定为 ${selection.label}:$detail 检测到其他外部 Agent ACP 端点时不会自动改线,可切到 Auto。',
|
||||
'This thread is pinned to ${selection.label}: $detail XWorkmate will not reroute to another external Agent ACP endpoint automatically. Switch to Auto instead.',
|
||||
);
|
||||
}
|
||||
if (controller.singleAgentNeedsAiGatewayConfigurationForSession(
|
||||
normalizedSessionKey,
|
||||
)) {
|
||||
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.',
|
||||
)
|
||||
: 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.',
|
||||
);
|
||||
}
|
||||
return detail.isEmpty
|
||||
? appText(
|
||||
'当前线程的外部 Agent ACP 连接尚未就绪。',
|
||||
'The external Agent ACP connection for this thread is not ready yet.',
|
||||
)
|
||||
: appText(
|
||||
'当前线程的外部 Agent ACP 连接尚未就绪:$detail',
|
||||
'The external Agent ACP connection for this thread is not ready yet: $detail',
|
||||
);
|
||||
}
|
||||
@ -39,6 +39,7 @@ import 'app_controller_desktop_core.dart';
|
||||
import 'app_controller_desktop_navigation.dart';
|
||||
import 'app_controller_desktop_gateway.dart';
|
||||
import 'app_controller_desktop_settings.dart';
|
||||
import 'app_controller_desktop_external_acp_routing.dart';
|
||||
import 'app_controller_desktop_single_agent.dart';
|
||||
import 'app_controller_desktop_thread_binding.dart';
|
||||
import 'app_controller_desktop_thread_sessions.dart';
|
||||
@ -479,10 +480,11 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
);
|
||||
if (aiGatewayPendingSessionKeysInternal.contains(sessionKey)) {
|
||||
await goTaskServiceClientInternal.cancelTask(
|
||||
route: assistantExecutionTargetForSession(sessionKey) ==
|
||||
AssistantExecutionTarget.singleAgent ||
|
||||
route:
|
||||
assistantExecutionTargetForSession(sessionKey) ==
|
||||
AssistantExecutionTarget.auto
|
||||
AssistantExecutionTarget.singleAgent ||
|
||||
assistantExecutionTargetForSession(sessionKey) ==
|
||||
AssistantExecutionTarget.auto
|
||||
? GoTaskServiceRoute.externalAcpSingle
|
||||
: GoTaskServiceRoute.openClawTask,
|
||||
target: assistantExecutionTargetForSession(sessionKey),
|
||||
|
||||
@ -76,6 +76,11 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
return '/owners/$realm/$subjectType/$subjectId/threads/$normalizedSessionKey';
|
||||
}
|
||||
|
||||
bool isOwnerScopedRemoteWorkspacePathInternal(String path) {
|
||||
final normalizedPath = path.trim();
|
||||
return normalizedPath.startsWith('/owners/');
|
||||
}
|
||||
|
||||
String threadWorkspaceDirectoryNameInternal(String sessionKey) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
@ -139,9 +144,14 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
required ThreadOwnerScope ownerScope,
|
||||
WorkspaceBinding? existingBinding,
|
||||
}) {
|
||||
if (existingBinding != null &&
|
||||
final preservesRemoteSingleAgentBinding =
|
||||
existingBinding != null &&
|
||||
existingBinding.workspaceKind == WorkspaceKind.remoteFs &&
|
||||
existingBinding.workspacePath.trim().isNotEmpty) {
|
||||
existingBinding.workspacePath.trim().isNotEmpty &&
|
||||
!isOwnerScopedRemoteWorkspacePathInternal(
|
||||
existingBinding.workspacePath,
|
||||
);
|
||||
if (preservesRemoteSingleAgentBinding) {
|
||||
return existingBinding.copyWith(
|
||||
displayPath: existingBinding.displayPath.trim().isEmpty
|
||||
? existingBinding.workspacePath
|
||||
|
||||
@ -661,7 +661,7 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
sessionKey,
|
||||
);
|
||||
final record = taskThreadForSessionInternal(normalizedSessionKey);
|
||||
return sanitizeExecutionTargetInternal(
|
||||
return sanitizePersistedExecutionTargetInternal(
|
||||
record == null
|
||||
? settings.assistantExecutionTarget
|
||||
: assistantExecutionTargetFromExecutionMode(
|
||||
|
||||
@ -314,7 +314,7 @@ List<String> assistantModelChoicesForSessionThreadSessionInternal(
|
||||
final normalizedSessionKey = normalizeAssistantSessionKeyThreadInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final target = controller.sanitizeExecutionTargetInternal(
|
||||
final target = controller.sanitizePersistedExecutionTargetInternal(
|
||||
controller.taskThreadForSessionInternal(normalizedSessionKey) == null
|
||||
? controller.settings.assistantExecutionTarget
|
||||
: assistantExecutionTargetFromExecutionMode(
|
||||
|
||||
@ -223,6 +223,15 @@ extension AppControllerDesktopThreadStorage on AppController {
|
||||
).sanitizeExecutionTarget(target);
|
||||
}
|
||||
|
||||
AssistantExecutionTarget sanitizePersistedExecutionTargetInternal(
|
||||
AssistantExecutionTarget? target,
|
||||
) {
|
||||
if (target == AssistantExecutionTarget.auto) {
|
||||
return AssistantExecutionTarget.auto;
|
||||
}
|
||||
return sanitizeExecutionTargetInternal(target);
|
||||
}
|
||||
|
||||
MultiAgentConfig resolveMultiAgentConfigInternal(SettingsSnapshot snapshot) {
|
||||
final defaults = MultiAgentConfig.defaults();
|
||||
final current = snapshot.multiAgent;
|
||||
|
||||
@ -52,7 +52,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
Future<void> setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget target,
|
||||
) async {
|
||||
final resolvedTarget = sanitizeExecutionTargetInternal(target);
|
||||
final resolvedTarget = sanitizePersistedExecutionTargetInternal(target);
|
||||
final currentTarget = assistantExecutionTargetForSession(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
@ -78,6 +78,8 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
executionTarget: resolvedTarget,
|
||||
executionTargetSource: ThreadSelectionSource.explicit,
|
||||
gatewayEntryState: gatewayEntryStateForTargetInternal(resolvedTarget),
|
||||
latestResolvedRuntimeModel: '',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
@ -177,7 +179,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
required String sessionKey,
|
||||
required bool persistDefaultSelection,
|
||||
}) async {
|
||||
final resolvedTarget = sanitizeExecutionTargetInternal(target);
|
||||
final resolvedTarget = sanitizePersistedExecutionTargetInternal(target);
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
@ -356,12 +358,10 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
singleAgentProviderForSession(currentSessionKey),
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
unawaited(
|
||||
ensureDesktopTaskThreadBindingInternal(
|
||||
normalizedSessionKey,
|
||||
executionTarget: resolvedTarget,
|
||||
),
|
||||
);
|
||||
// Re-read the current thread target when the async binding sync runs so a
|
||||
// just-created thread cannot be rebound back to a stale target if the user
|
||||
// switches execution mode immediately afterwards.
|
||||
unawaited(ensureDesktopTaskThreadBindingInternal(normalizedSessionKey));
|
||||
unawaited(persistAssistantLastSessionKeyInternal(normalizedSessionKey));
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
|
||||
@ -206,8 +206,6 @@ class SingleAgentProvider {
|
||||
final SingleAgentProviderSource source;
|
||||
|
||||
bool get isAuto => providerId == auto.providerId;
|
||||
bool get isBuiltInReserved =>
|
||||
source == SingleAgentProviderSource.builtInReserved;
|
||||
bool get isExternalExtension =>
|
||||
source == SingleAgentProviderSource.externalExtension;
|
||||
|
||||
@ -278,11 +276,7 @@ extension SingleAgentProviderCopy on SingleAgentProvider {
|
||||
}) => SingleAgentProvider.fromJsonValue(value, label: label, badge: badge);
|
||||
}
|
||||
|
||||
enum SingleAgentProviderSource { externalExtension, builtInReserved }
|
||||
|
||||
SingleAgentProvider normalizeSingleAgentProviderSelection(
|
||||
SingleAgentProvider provider,
|
||||
) => provider;
|
||||
enum SingleAgentProviderSource { externalExtension }
|
||||
|
||||
List<SingleAgentProvider> normalizeSingleAgentProviderList(
|
||||
Iterable<SingleAgentProvider> providers,
|
||||
@ -290,9 +284,8 @@ List<SingleAgentProvider> normalizeSingleAgentProviderList(
|
||||
final normalized = <SingleAgentProvider>[];
|
||||
final seen = <String>{};
|
||||
for (final provider in providers) {
|
||||
final resolved = normalizeSingleAgentProviderSelection(provider);
|
||||
if (seen.add(resolved.providerId)) {
|
||||
normalized.add(resolved);
|
||||
if (seen.add(provider.providerId)) {
|
||||
normalized.add(provider);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
|
||||
@ -526,17 +526,14 @@ class SettingsSnapshot {
|
||||
}
|
||||
|
||||
SingleAgentProvider resolveSingleAgentProvider(SingleAgentProvider provider) {
|
||||
final normalizedSelection = normalizeSingleAgentProviderSelection(provider);
|
||||
if (normalizedSelection.isAuto) {
|
||||
if (provider.isAuto) {
|
||||
return SingleAgentProvider.auto;
|
||||
}
|
||||
final profile = externalAcpEndpointForProviderId(
|
||||
normalizedSelection.providerId,
|
||||
);
|
||||
final profile = externalAcpEndpointForProviderId(provider.providerId);
|
||||
if (profile != null) {
|
||||
return profile.toProvider();
|
||||
}
|
||||
return normalizedSelection;
|
||||
return provider;
|
||||
}
|
||||
|
||||
SingleAgentProvider singleAgentProviderForId(String providerId) {
|
||||
@ -544,9 +541,7 @@ class SettingsSnapshot {
|
||||
if (resolved.isEmpty || resolved == SingleAgentProvider.auto.providerId) {
|
||||
return SingleAgentProvider.auto;
|
||||
}
|
||||
final normalizedSelection = normalizeSingleAgentProviderSelection(
|
||||
SingleAgentProvider.fromJsonValue(resolved),
|
||||
);
|
||||
final normalizedSelection = SingleAgentProvider.fromJsonValue(resolved);
|
||||
final profile = externalAcpEndpointForProviderId(
|
||||
normalizedSelection.providerId,
|
||||
);
|
||||
|
||||
@ -26,6 +26,11 @@ void main() {
|
||||
// Tightened in T2/T3 after assistant + app/runtime closure split.
|
||||
'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,
|
||||
'lib/app/app_controller_desktop_thread_sessions.dart': 800,
|
||||
'lib/app/app_controller_desktop_runtime_coordination_impl.dart': 800,
|
||||
'lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart':
|
||||
|
||||
Loading…
Reference in New Issue
Block a user