refactor: consume bridge-owned single-agent routing
This commit is contained in:
parent
94ecedee84
commit
13f39dcb8a
@ -25,7 +25,7 @@ flowchart TD
|
||||
E --> F["xworkmate-bridge providerCatalog"]
|
||||
|
||||
F --> G["acp.capabilities"]
|
||||
G --> H["providers[]
|
||||
G --> H["providerCatalog[]
|
||||
singleAgent / multiAgent"]
|
||||
|
||||
H --> I["refreshSingleAgentCapabilitiesRuntimeInternal()"]
|
||||
@ -36,7 +36,7 @@ flowchart TD
|
||||
|
||||
G --> L["refreshAcpCapabilitiesRuntimeInternal()"]
|
||||
L --> M["GatewayAcpCapabilities
|
||||
providers / singleAgent / multiAgent"]
|
||||
providerCatalog / singleAgent / multiAgent"]
|
||||
M --> N["mergeAcpCapabilitiesIntoMountTargetsRuntimeInternal()"]
|
||||
N --> O["ManagedMountTargetState
|
||||
codex / opencode / claude / gemini / aris / openclaw
|
||||
@ -63,21 +63,25 @@ flowchart TD
|
||||
恢复线程已选 providerId"]
|
||||
|
||||
V --> W["sendSingleAgentMessageDesktopGoTaskFlowInternal()"]
|
||||
W --> X["再次拉取 acp.capabilities"]
|
||||
X --> Y["按本次 bridge providers 解析
|
||||
auto -> 当前 bridge 顺序第一个可用 provider
|
||||
explicit -> 当前 bridge 已广告的 provider"]
|
||||
W --> X["xworkmate.routing.resolve"]
|
||||
X --> Y["bridge 返回 resolvedExecutionTarget /
|
||||
resolvedProviderId /
|
||||
unavailableCode /
|
||||
unavailableMessage"]
|
||||
|
||||
Y --> Z{"provider resolved?"}
|
||||
Z -->|"yes"| AA["executeTask(... provider ...)"]
|
||||
Z -->|"no"| AB["provider unavailable UX"]
|
||||
Y --> Z{"unavailable?"}
|
||||
Z -->|"no"| AA["executeTask(... resolved routing ...)"]
|
||||
Z -->|"yes"| AB["provider unavailable UX
|
||||
直接使用 bridge unavailable message"]
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `externalAcpEndpoints` still matters, but only as bridge sync input.
|
||||
- Provider visibility, picker contents, and auto-provider resolution all come
|
||||
from `acp.capabilities.providers`.
|
||||
- Provider visibility and picker contents come from
|
||||
`acp.capabilities.providerCatalog`.
|
||||
- Auto-provider resolution and unavailable messaging come from
|
||||
`xworkmate.routing.resolve`.
|
||||
- `openclaw` and other mount-target discovery states are also bridge-owned and
|
||||
come from ACP capabilities merged into `ManagedMountTargetState`.
|
||||
- Persisted thread `providerId` restores the user's previous selection, but it
|
||||
|
||||
@ -62,7 +62,7 @@ flowchart TD
|
||||
E --> F["xworkmate-bridge providerCatalog"]
|
||||
|
||||
F --> G["acp.capabilities"]
|
||||
G --> H["providers[]
|
||||
G --> H["providerCatalog[]
|
||||
singleAgent / multiAgent"]
|
||||
|
||||
H --> I["refreshSingleAgentCapabilitiesRuntimeInternal()"]
|
||||
@ -73,7 +73,7 @@ flowchart TD
|
||||
|
||||
G --> L["refreshAcpCapabilitiesRuntimeInternal()"]
|
||||
L --> M["GatewayAcpCapabilities
|
||||
providers / singleAgent / multiAgent"]
|
||||
providerCatalog / singleAgent / multiAgent"]
|
||||
M --> N["mergeAcpCapabilitiesIntoMountTargetsRuntimeInternal()"]
|
||||
N --> O["ManagedMountTargetState
|
||||
codex / opencode / claude / gemini / aris / openclaw
|
||||
@ -100,14 +100,15 @@ flowchart TD
|
||||
恢复线程已选 providerId"]
|
||||
|
||||
V --> W["sendSingleAgentMessageDesktopGoTaskFlowInternal()"]
|
||||
W --> X["再次拉取 acp.capabilities"]
|
||||
X --> Y["按本次 bridge providers 解析
|
||||
auto -> 当前 bridge 顺序第一个可用 provider
|
||||
explicit -> 当前 bridge 已广告的 provider"]
|
||||
W --> X["xworkmate.routing.resolve"]
|
||||
X --> Y["resolvedProviderId /
|
||||
unavailableCode /
|
||||
unavailableMessage"]
|
||||
|
||||
Y --> Z{"provider resolved?"}
|
||||
Z -->|"yes"| AA["executeTask(... provider ...)"]
|
||||
Z -->|"no"| AB["provider unavailable UX"]
|
||||
Y --> Z{"unavailable?"}
|
||||
Z -->|"no"| AA["executeTask(... resolved routing ...)"]
|
||||
Z -->|"yes"| AB["provider unavailable UX
|
||||
直接使用 bridge unavailable message"]
|
||||
```
|
||||
|
||||
## 端侧桥接规则
|
||||
|
||||
@ -575,16 +575,19 @@ class AppController extends ChangeNotifier {
|
||||
|
||||
List<SingleAgentProvider> get configuredSingleAgentProviders =>
|
||||
normalizeSingleAgentProviderList(
|
||||
(availableSingleAgentProvidersOverrideInternal ??
|
||||
bridgeAdvertisedProvidersInternal)
|
||||
.where((item) => item != SingleAgentProvider.auto)
|
||||
.map(settings.resolveSingleAgentProvider),
|
||||
bridgeAdvertisedProvidersInternal.where(
|
||||
(item) => item != SingleAgentProvider.auto,
|
||||
),
|
||||
);
|
||||
|
||||
List<SingleAgentProvider> get availableSingleAgentProviders =>
|
||||
configuredSingleAgentProviders
|
||||
.where(canUseSingleAgentProviderInternal)
|
||||
.toList(growable: false);
|
||||
availableSingleAgentProvidersOverrideInternal != null
|
||||
? normalizeSingleAgentProviderList(
|
||||
availableSingleAgentProvidersOverrideInternal!,
|
||||
)
|
||||
: configuredSingleAgentProviders
|
||||
.where(canUseSingleAgentProviderInternal)
|
||||
.toList(growable: false);
|
||||
|
||||
List<AssistantExecutionTarget> visibleAssistantExecutionTargets(
|
||||
Iterable<AssistantExecutionTarget> supportedTargets,
|
||||
@ -625,18 +628,13 @@ class AppController extends ChangeNotifier {
|
||||
SingleAgentProvider? resolvedSingleAgentProviderInternal(
|
||||
SingleAgentProvider selection,
|
||||
) {
|
||||
if (selection != SingleAgentProvider.auto) {
|
||||
final resolvedSelection = settings.resolveSingleAgentProvider(selection);
|
||||
return canUseSingleAgentProviderInternal(resolvedSelection)
|
||||
? resolvedSelection
|
||||
: null;
|
||||
if (selection == SingleAgentProvider.auto) {
|
||||
return null;
|
||||
}
|
||||
for (final provider in configuredSingleAgentProviders) {
|
||||
if (canUseSingleAgentProviderInternal(provider)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
final resolvedSelection = settings.resolveSingleAgentProvider(selection);
|
||||
return canUseSingleAgentProviderInternal(resolvedSelection)
|
||||
? resolvedSelection
|
||||
: null;
|
||||
}
|
||||
|
||||
List<String> get aiGatewayConversationModelChoices {
|
||||
|
||||
@ -56,8 +56,7 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
|
||||
final target = controller.assistantExecutionTargetForSession(
|
||||
controller.sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
final resolvedProvider =
|
||||
target == AssistantExecutionTarget.singleAgent
|
||||
final resolvedProvider = target == AssistantExecutionTarget.singleAgent
|
||||
? (controller.singleAgentResolvedProviderForSession(
|
||||
controller.sessionsControllerInternal.currentSessionKey,
|
||||
) ??
|
||||
@ -68,9 +67,10 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
|
||||
: controller.resolveSingleAgentEndpointInternal(resolvedProvider);
|
||||
final authorizationOverride = resolvedProvider == null
|
||||
? ''
|
||||
: await controller.resolveSingleAgentAuthorizationHeaderForProviderInternal(
|
||||
resolvedProvider,
|
||||
);
|
||||
: await controller
|
||||
.resolveSingleAgentAuthorizationHeaderForProviderInternal(
|
||||
resolvedProvider,
|
||||
);
|
||||
await controller.gatewayAcpClientInternal.loadCapabilities(
|
||||
forceRefresh: forceRefresh,
|
||||
endpointOverride: endpointOverride,
|
||||
@ -109,28 +109,9 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
|
||||
forceRefresh: forceRefresh,
|
||||
);
|
||||
controller.bridgeAdvertisedProvidersInternal =
|
||||
controller.availableSingleAgentProvidersOverrideInternal != null
|
||||
? normalizeSingleAgentProviderList(
|
||||
controller.availableSingleAgentProvidersOverrideInternal!,
|
||||
)
|
||||
: normalizeSingleAgentProviderList(
|
||||
capabilities.providers.map(
|
||||
controller.settings.resolveSingleAgentProvider,
|
||||
),
|
||||
);
|
||||
normalizeSingleAgentProviderList(capabilities.providerCatalog);
|
||||
final next = <SingleAgentProvider, SingleAgentCapabilities>{};
|
||||
final candidateProviders =
|
||||
normalizeSingleAgentProviderList(<SingleAgentProvider>[
|
||||
...controller.configuredSingleAgentProviders,
|
||||
...capabilities.providers.map(
|
||||
controller.settings.resolveSingleAgentProvider,
|
||||
),
|
||||
]);
|
||||
for (final provider in candidateProviders) {
|
||||
if (!capabilities.providers.contains(provider)) {
|
||||
next[provider] = const SingleAgentCapabilities.unavailable(endpoint: '');
|
||||
continue;
|
||||
}
|
||||
for (final provider in controller.bridgeAdvertisedProvidersInternal) {
|
||||
next[provider] = SingleAgentCapabilities(
|
||||
available: true,
|
||||
supportedProviders: <SingleAgentProvider>[provider],
|
||||
@ -150,7 +131,7 @@ mergeAcpCapabilitiesIntoMountTargetsRuntimeInternal(
|
||||
GatewayAcpCapabilities capabilities,
|
||||
) {
|
||||
final source = current.isEmpty ? ManagedMountTargetState.defaults() : current;
|
||||
final providers = capabilities.providers
|
||||
final providers = capabilities.providerCatalog
|
||||
.map((item) => item.providerId)
|
||||
.toSet();
|
||||
return source
|
||||
|
||||
@ -58,30 +58,48 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final selection = controller.singleAgentProviderForSession(sessionKey);
|
||||
final capabilities = await controller.goTaskServiceClientInternal
|
||||
.loadExternalAcpCapabilities(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
forceRefresh: true,
|
||||
final preflightWorkingDirectory = controller
|
||||
.resolveSingleAgentWorkingDirectoryForSessionInternal(sessionKey);
|
||||
if (preflightWorkingDirectory == null ||
|
||||
preflightWorkingDirectory.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 aiGatewayApiKey = await controller.loadAiGatewayApiKey();
|
||||
final routingResolution = await controller.goTaskServiceClientInternal
|
||||
.resolveExternalAcpRouting(
|
||||
taskPrompt: message,
|
||||
workingDirectory: preflightWorkingDirectory,
|
||||
routing: routing,
|
||||
aiGatewayBaseUrl: controller.aiGatewayUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
final advertisedProviders =
|
||||
controller.availableSingleAgentProvidersOverrideInternal != null
|
||||
? normalizeSingleAgentProviderList(
|
||||
controller.availableSingleAgentProvidersOverrideInternal!,
|
||||
)
|
||||
: normalizeSingleAgentProviderList(
|
||||
capabilities.providers.map(
|
||||
controller.settings.resolveSingleAgentProvider,
|
||||
),
|
||||
final effectiveProvider =
|
||||
routingResolution.resolvedProviderId.trim().isEmpty
|
||||
? null
|
||||
: SingleAgentProviderCopy.fromJsonValue(
|
||||
routingResolution.resolvedProviderId,
|
||||
);
|
||||
controller.bridgeAdvertisedProvidersInternal = advertisedProviders;
|
||||
final availableProviders = advertisedProviders
|
||||
.where(capabilities.providers.contains)
|
||||
.toList(growable: false);
|
||||
final provider = selection == SingleAgentProvider.auto
|
||||
? (availableProviders.isEmpty ? null : availableProviders.first)
|
||||
: (capabilities.providers.contains(selection) ? selection : null);
|
||||
final unavailableReason = provider == null
|
||||
? (selection == SingleAgentProvider.auto
|
||||
final unavailableReason =
|
||||
routingResolution.unavailable ||
|
||||
(routingResolution.resolvedExecutionTarget == 'single-agent' &&
|
||||
effectiveProvider == null)
|
||||
? (routingResolution.unavailableMessage.isNotEmpty
|
||||
? routingResolution.unavailableMessage
|
||||
: selection == SingleAgentProvider.auto
|
||||
? appText(
|
||||
'当前没有可用的 GoTaskService Provider。',
|
||||
'No GoTaskService provider is currently available.',
|
||||
@ -91,7 +109,7 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
'GoTaskService does not currently support ${selection.label}.',
|
||||
))
|
||||
: null;
|
||||
if (provider == null) {
|
||||
if (unavailableReason != null) {
|
||||
controller.upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: 'ready',
|
||||
@ -112,34 +130,23 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
);
|
||||
return;
|
||||
}
|
||||
final effectiveProvider = provider;
|
||||
|
||||
appendSingleAgentRuntimeStatusDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
effectiveProvider,
|
||||
);
|
||||
if (effectiveProvider != null) {
|
||||
appendSingleAgentRuntimeStatusDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
effectiveProvider,
|
||||
);
|
||||
}
|
||||
final workingDirectory = controller
|
||||
.resolveSingleAgentWorkingDirectoryForSessionInternal(
|
||||
sessionKey,
|
||||
provider: provider,
|
||||
provider: effectiveProvider,
|
||||
);
|
||||
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 resolvedWorkingDirectory =
|
||||
workingDirectory == null || workingDirectory.trim().isEmpty
|
||||
? preflightWorkingDirectory
|
||||
: workingDirectory;
|
||||
|
||||
final selectedSkills = controller
|
||||
.assistantSelectedSkillsForSession(sessionKey)
|
||||
@ -152,19 +159,26 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
threadId: sessionKey,
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
prompt: message,
|
||||
workingDirectory: workingDirectory,
|
||||
model: controller.assistantModelForSession(sessionKey),
|
||||
workingDirectory: resolvedWorkingDirectory,
|
||||
model: routingResolution.resolvedModel.trim().isNotEmpty
|
||||
? routingResolution.resolvedModel
|
||||
: controller.assistantModelForSession(sessionKey),
|
||||
thinking: thinking,
|
||||
selectedSkills: selectedSkills,
|
||||
selectedSkills: routingResolution.resolvedSkills.isNotEmpty
|
||||
? routingResolution.resolvedSkills
|
||||
: selectedSkills,
|
||||
inlineAttachments: attachments,
|
||||
localAttachments: localAttachments,
|
||||
aiGatewayBaseUrl: controller.aiGatewayUrl,
|
||||
aiGatewayApiKey: await controller.loadAiGatewayApiKey(),
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
agentId: '',
|
||||
metadata: const <String, dynamic>{},
|
||||
routing: routing,
|
||||
routing: _resolvedRoutingConfigDesktopInternal(
|
||||
routing,
|
||||
routingResolution,
|
||||
),
|
||||
routingHint: 'single-agent',
|
||||
provider: effectiveProvider,
|
||||
provider: effectiveProvider ?? SingleAgentProvider.auto,
|
||||
remoteWorkingDirectoryHint:
|
||||
controller
|
||||
.requireTaskThreadForSessionInternal(sessionKey)
|
||||
@ -218,6 +232,33 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
});
|
||||
}
|
||||
|
||||
ExternalCodeAgentAcpRoutingConfig _resolvedRoutingConfigDesktopInternal(
|
||||
ExternalCodeAgentAcpRoutingConfig original,
|
||||
ExternalCodeAgentAcpRoutingResolution resolution,
|
||||
) {
|
||||
final explicitExecutionTarget = switch (resolution.resolvedExecutionTarget
|
||||
.trim()
|
||||
.toLowerCase()) {
|
||||
'single-agent' => 'singleAgent',
|
||||
'gateway' =>
|
||||
resolution.resolvedEndpointTarget.trim().toLowerCase() == 'remote'
|
||||
? 'remote'
|
||||
: 'local',
|
||||
_ => original.explicitExecutionTarget,
|
||||
};
|
||||
return ExternalCodeAgentAcpRoutingConfig(
|
||||
mode: ExternalCodeAgentAcpRoutingMode.explicit,
|
||||
preferredGatewayTarget: original.preferredGatewayTarget,
|
||||
explicitExecutionTarget: explicitExecutionTarget,
|
||||
explicitProviderId: resolution.resolvedProviderId,
|
||||
explicitModel: resolution.resolvedModel,
|
||||
explicitSkills: resolution.resolvedSkills,
|
||||
allowSkillInstall: original.allowSkillInstall,
|
||||
availableSkills: original.availableSkills,
|
||||
installApproval: original.installApproval,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _applySingleAgentGoTaskResultDesktopInternal(
|
||||
AppController controller, {
|
||||
required String sessionKey,
|
||||
@ -245,7 +286,11 @@ Future<void> _applySingleAgentGoTaskResultDesktopInternal(
|
||||
lastRemoteWorkspaceRefKind: result.remoteWorkspaceRefKind,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
await _persistSingleAgentArtifactsDesktopInternal(controller, sessionKey, result);
|
||||
await _persistSingleAgentArtifactsDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
result,
|
||||
);
|
||||
controller.clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
if (!result.success) {
|
||||
controller.appendAssistantThreadMessageInternal(
|
||||
@ -356,7 +401,9 @@ String _sanitizeArtifactRelativePathInternal(String raw) {
|
||||
}
|
||||
final cleaned = trimmed
|
||||
.split('/')
|
||||
.where((segment) => segment.isNotEmpty && segment != '.' && segment != '..')
|
||||
.where(
|
||||
(segment) => segment.isNotEmpty && segment != '.' && segment != '..',
|
||||
)
|
||||
.join('/');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
@ -36,35 +36,49 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
);
|
||||
final result = _castMap(response['result']);
|
||||
final caps = _castMap(result['capabilities']);
|
||||
final providers = <SingleAgentProvider>{};
|
||||
for (final raw in <Object?>[
|
||||
..._asList(result['providers']),
|
||||
..._asList(caps['providers']),
|
||||
]) {
|
||||
if (raw == null) {
|
||||
continue;
|
||||
}
|
||||
final provider = SingleAgentProviderCopy.fromJsonValue(
|
||||
raw.toString().trim().toLowerCase(),
|
||||
);
|
||||
if (provider != SingleAgentProvider.auto) {
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
final providerCatalog = _parseProviderCatalog(
|
||||
result['providerCatalog'] ?? caps['providerCatalog'],
|
||||
);
|
||||
return ExternalCodeAgentAcpCapabilities(
|
||||
singleAgent:
|
||||
_boolValue(result['singleAgent']) ??
|
||||
_boolValue(caps['single_agent']) ??
|
||||
providers.isNotEmpty,
|
||||
providerCatalog.isNotEmpty,
|
||||
multiAgent:
|
||||
_boolValue(result['multiAgent']) ??
|
||||
_boolValue(caps['multi_agent']) ??
|
||||
true,
|
||||
providers: providers,
|
||||
providerCatalog: providerCatalog,
|
||||
raw: result,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) async {
|
||||
await _syncProviders();
|
||||
final response = await _bridge.request(
|
||||
method: 'xworkmate.routing.resolve',
|
||||
params: <String, dynamic>{
|
||||
'taskPrompt': taskPrompt,
|
||||
'workingDirectory': workingDirectory.trim(),
|
||||
if (aiGatewayBaseUrl.trim().isNotEmpty)
|
||||
'aiGatewayBaseUrl': aiGatewayBaseUrl.trim(),
|
||||
if (aiGatewayApiKey.trim().isNotEmpty)
|
||||
'aiGatewayApiKey': aiGatewayApiKey.trim(),
|
||||
'routing': routing.toJson(),
|
||||
},
|
||||
);
|
||||
return ExternalCodeAgentAcpRoutingResolution(
|
||||
raw: _castMap(response['result']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GoTaskServiceResult> executeTask(
|
||||
GoTaskServiceRequest request, {
|
||||
@ -200,4 +214,24 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<SingleAgentProvider> _parseProviderCatalog(Object? raw) {
|
||||
final providers = <SingleAgentProvider>[];
|
||||
for (final item in _asList(raw)) {
|
||||
final entry = _castMap(item);
|
||||
final providerId = entry['providerId']?.toString().trim() ?? '';
|
||||
if (providerId.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final label = entry['label']?.toString().trim();
|
||||
final provider = SingleAgentProviderCopy.fromJsonValue(
|
||||
providerId,
|
||||
label: label?.isNotEmpty == true ? label : null,
|
||||
);
|
||||
if (provider != SingleAgentProvider.auto) {
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
return normalizeSingleAgentProviderList(providers);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ class GatewayAcpCapabilities {
|
||||
const GatewayAcpCapabilities({
|
||||
required this.singleAgent,
|
||||
required this.multiAgent,
|
||||
required this.providers,
|
||||
required this.providerCatalog,
|
||||
required this.raw,
|
||||
this.diagnostics = const <String, dynamic>{},
|
||||
});
|
||||
@ -28,13 +28,13 @@ class GatewayAcpCapabilities {
|
||||
const GatewayAcpCapabilities.empty()
|
||||
: singleAgent = false,
|
||||
multiAgent = false,
|
||||
providers = const <SingleAgentProvider>{},
|
||||
providerCatalog = const <SingleAgentProvider>[],
|
||||
raw = const <String, dynamic>{},
|
||||
diagnostics = const <String, dynamic>{};
|
||||
|
||||
final bool singleAgent;
|
||||
final bool multiAgent;
|
||||
final Set<SingleAgentProvider> providers;
|
||||
final List<SingleAgentProvider> providerCatalog;
|
||||
final Map<String, dynamic> raw;
|
||||
final Map<String, dynamic> diagnostics;
|
||||
}
|
||||
@ -123,25 +123,13 @@ class GatewayAcpClient {
|
||||
);
|
||||
final result = asMap(response['result']);
|
||||
final caps = asMap(result['capabilities']);
|
||||
final providers = <SingleAgentProvider>{};
|
||||
for (final raw in <Object?>[
|
||||
...asList(result['providers']),
|
||||
...asList(caps['providers']),
|
||||
]) {
|
||||
if (raw == null) {
|
||||
continue;
|
||||
}
|
||||
final provider = SingleAgentProviderCopy.fromJsonValue(
|
||||
raw.toString().trim().toLowerCase(),
|
||||
);
|
||||
if (provider != SingleAgentProvider.auto) {
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
final providerCatalog = _parseProviderCatalog(
|
||||
result['providerCatalog'] ?? caps['providerCatalog'],
|
||||
);
|
||||
final singleAgent =
|
||||
boolValue(result['singleAgent']) ??
|
||||
boolValue(caps['single_agent']) ??
|
||||
providers.isNotEmpty;
|
||||
providerCatalog.isNotEmpty;
|
||||
final multiAgent =
|
||||
boolValue(result['multiAgent']) ??
|
||||
boolValue(caps['multi_agent']) ??
|
||||
@ -149,7 +137,7 @@ class GatewayAcpClient {
|
||||
_cachedCapabilities = GatewayAcpCapabilities(
|
||||
singleAgent: singleAgent,
|
||||
multiAgent: multiAgent,
|
||||
providers: providers,
|
||||
providerCatalog: providerCatalog,
|
||||
raw: result,
|
||||
diagnostics: asMap(response['_xworkmateDiagnostics']),
|
||||
);
|
||||
@ -157,6 +145,26 @@ class GatewayAcpClient {
|
||||
return _cachedCapabilities;
|
||||
}
|
||||
|
||||
List<SingleAgentProvider> _parseProviderCatalog(Object? raw) {
|
||||
final providers = <SingleAgentProvider>[];
|
||||
for (final item in asList(raw)) {
|
||||
final entry = asMap(item);
|
||||
final providerId = entry['providerId']?.toString().trim() ?? '';
|
||||
if (providerId.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final label = entry['label']?.toString().trim();
|
||||
final provider = SingleAgentProviderCopy.fromJsonValue(
|
||||
providerId,
|
||||
label: label?.isNotEmpty == true ? label : null,
|
||||
);
|
||||
if (provider != SingleAgentProvider.auto) {
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
return normalizeSingleAgentProviderList(providers);
|
||||
}
|
||||
|
||||
Stream<MultiAgentRunEvent> runMultiAgent(
|
||||
GatewayAcpMultiAgentRequest request,
|
||||
) {
|
||||
|
||||
@ -8,22 +8,68 @@ class ExternalCodeAgentAcpCapabilities {
|
||||
const ExternalCodeAgentAcpCapabilities({
|
||||
required this.singleAgent,
|
||||
required this.multiAgent,
|
||||
required this.providers,
|
||||
required this.providerCatalog,
|
||||
required this.raw,
|
||||
});
|
||||
|
||||
const ExternalCodeAgentAcpCapabilities.empty()
|
||||
: singleAgent = false,
|
||||
multiAgent = false,
|
||||
providers = const <SingleAgentProvider>{},
|
||||
providerCatalog = const <SingleAgentProvider>[],
|
||||
raw = const <String, dynamic>{};
|
||||
|
||||
final bool singleAgent;
|
||||
final bool multiAgent;
|
||||
final Set<SingleAgentProvider> providers;
|
||||
final List<SingleAgentProvider> providerCatalog;
|
||||
final Map<String, dynamic> raw;
|
||||
}
|
||||
|
||||
class ExternalCodeAgentAcpRoutingResolution {
|
||||
const ExternalCodeAgentAcpRoutingResolution({required this.raw});
|
||||
|
||||
final Map<String, dynamic> raw;
|
||||
|
||||
String get resolvedExecutionTarget =>
|
||||
raw['resolvedExecutionTarget']?.toString().trim() ?? '';
|
||||
|
||||
String get resolvedEndpointTarget =>
|
||||
raw['resolvedEndpointTarget']?.toString().trim() ?? '';
|
||||
|
||||
String get resolvedProviderId =>
|
||||
raw['resolvedProviderId']?.toString().trim() ?? '';
|
||||
|
||||
String get resolvedModel => raw['resolvedModel']?.toString().trim() ?? '';
|
||||
|
||||
List<String> get resolvedSkills {
|
||||
final rawList = raw['resolvedSkills'];
|
||||
if (rawList is! List) {
|
||||
return const <String>[];
|
||||
}
|
||||
return rawList
|
||||
.map((item) => item?.toString().trim() ?? '')
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
String get skillResolutionSource =>
|
||||
raw['skillResolutionSource']?.toString().trim() ?? '';
|
||||
|
||||
bool get needsSkillInstall => _boolValue(raw['needsSkillInstall']) ?? false;
|
||||
|
||||
String get skillInstallRequestId =>
|
||||
raw['skillInstallRequestId']?.toString().trim() ?? '';
|
||||
|
||||
List<Map<String, dynamic>> get skillCandidates =>
|
||||
_castMapList(raw['skillCandidates']);
|
||||
|
||||
bool get unavailable => _boolValue(raw['unavailable']) ?? false;
|
||||
|
||||
String get unavailableCode => raw['unavailableCode']?.toString().trim() ?? '';
|
||||
|
||||
String get unavailableMessage =>
|
||||
raw['unavailableMessage']?.toString().trim() ?? '';
|
||||
}
|
||||
|
||||
class ExternalCodeAgentAcpSyncedProvider {
|
||||
const ExternalCodeAgentAcpSyncedProvider({
|
||||
required this.providerId,
|
||||
@ -493,9 +539,8 @@ class GoTaskServiceResult {
|
||||
return rawArtifacts
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) => GoTaskServiceArtifact.fromJson(
|
||||
item.cast<String, dynamic>(),
|
||||
),
|
||||
(item) =>
|
||||
GoTaskServiceArtifact.fromJson(item.cast<String, dynamic>()),
|
||||
)
|
||||
.where((item) => item.relativePath.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
@ -571,6 +616,14 @@ abstract class ExternalCodeAgentAcpTransport {
|
||||
bool forceRefresh = false,
|
||||
});
|
||||
|
||||
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
});
|
||||
|
||||
Future<GoTaskServiceResult> executeTask(
|
||||
GoTaskServiceRequest request, {
|
||||
required void Function(GoTaskServiceUpdate update) onUpdate,
|
||||
@ -601,6 +654,14 @@ abstract class GoTaskServiceClient {
|
||||
bool forceRefresh = false,
|
||||
});
|
||||
|
||||
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
});
|
||||
|
||||
Future<GoTaskServiceResult> executeTask(
|
||||
GoTaskServiceRequest request, {
|
||||
required void Function(GoTaskServiceUpdate update) onUpdate,
|
||||
|
||||
@ -24,6 +24,21 @@ class DesktopGoTaskService implements GoTaskServiceClient {
|
||||
forceRefresh: forceRefresh,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) => _acpTransport.resolveExternalAcpRouting(
|
||||
taskPrompt: taskPrompt,
|
||||
workingDirectory: workingDirectory,
|
||||
routing: routing,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<GoTaskServiceResult> executeTask(
|
||||
GoTaskServiceRequest request, {
|
||||
|
||||
@ -28,7 +28,23 @@ class _FakeGoAcpStdioBridge extends GoAcpStdioBridge {
|
||||
'result': <String, dynamic>{
|
||||
'singleAgent': true,
|
||||
'multiAgent': true,
|
||||
'providers': <String>['codex', 'opencode', 'gemini'],
|
||||
'providerCatalog': <Map<String, dynamic>>[
|
||||
<String, dynamic>{'providerId': 'codex', 'label': 'Codex'},
|
||||
<String, dynamic>{'providerId': 'opencode', 'label': 'OpenCode'},
|
||||
<String, dynamic>{'providerId': 'gemini', 'label': 'Gemini'},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method == 'xworkmate.routing.resolve') {
|
||||
return <String, dynamic>{
|
||||
'result': <String, dynamic>{
|
||||
'resolvedExecutionTarget': 'single-agent',
|
||||
'resolvedEndpointTarget': 'singleAgent',
|
||||
'resolvedProviderId': 'gemini',
|
||||
'resolvedModel': 'gemini-2.5-pro',
|
||||
'resolvedSkills': <String>['pptx'],
|
||||
'unavailable': false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -43,38 +59,64 @@ class _FakeGoAcpStdioBridge extends GoAcpStdioBridge {
|
||||
|
||||
void main() {
|
||||
group('ExternalCodeAgentAcpDesktopTransport', () {
|
||||
test('reads bridge capabilities without pushing an empty provider sync', () async {
|
||||
final bridge = _FakeGoAcpStdioBridge();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
test(
|
||||
'reads bridge capabilities without pushing an empty provider sync',
|
||||
() async {
|
||||
final bridge = _FakeGoAcpStdioBridge();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
|
||||
final capabilities = await transport.loadExternalAcpCapabilities(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
final capabilities = await transport.loadExternalAcpCapabilities(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
|
||||
expect(bridge.methods, <String>['acp.capabilities']);
|
||||
expect(
|
||||
capabilities.providers.map((item) => item.providerId).toList()..sort(),
|
||||
<String>['codex', 'gemini', 'opencode'],
|
||||
);
|
||||
});
|
||||
expect(bridge.methods, <String>['acp.capabilities']);
|
||||
expect(
|
||||
capabilities.providerCatalog.map((item) => item.providerId).toList(),
|
||||
<String>['codex', 'opencode', 'gemini'],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('only syncs when app has explicit provider overrides to send', () async {
|
||||
final bridge = _FakeGoAcpStdioBridge();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
test(
|
||||
'only syncs when app has explicit provider overrides to send',
|
||||
() async {
|
||||
final bridge = _FakeGoAcpStdioBridge();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
|
||||
await transport.syncExternalProviders(
|
||||
const <ExternalCodeAgentAcpSyncedProvider>[
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: 'codex',
|
||||
label: 'Codex',
|
||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
||||
authorizationHeader: '',
|
||||
enabled: true,
|
||||
await transport
|
||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: 'codex',
|
||||
label: 'Codex',
|
||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
||||
authorizationHeader: '',
|
||||
enabled: true,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(bridge.methods, <String>['xworkmate.providers.sync']);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'uses bridge routing resolve for preflight provider selection',
|
||||
() async {
|
||||
final bridge = _FakeGoAcpStdioBridge();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
|
||||
final resolution = await transport.resolveExternalAcpRouting(
|
||||
taskPrompt: 'make slides',
|
||||
workingDirectory: '/tmp/workspace',
|
||||
routing: const ExternalCodeAgentAcpRoutingConfig.auto(
|
||||
preferredGatewayTarget: 'local',
|
||||
),
|
||||
],
|
||||
);
|
||||
);
|
||||
|
||||
expect(bridge.methods, <String>['xworkmate.providers.sync']);
|
||||
});
|
||||
expect(bridge.methods, <String>['xworkmate.routing.resolve']);
|
||||
expect(resolution.resolvedProviderId, 'gemini');
|
||||
expect(resolution.resolvedModel, 'gemini-2.5-pro');
|
||||
expect(resolution.resolvedSkills, <String>['pptx']);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user