Fix gateway dispatch test pipeline
This commit is contained in:
parent
ae16a2d978
commit
7f42fbbda0
@ -23,6 +23,7 @@ import '../runtime/settings_store.dart';
|
||||
import '../runtime/secure_config_store.dart';
|
||||
import '../runtime/embedded_agent_launch_policy.dart';
|
||||
import '../runtime/runtime_coordinator.dart';
|
||||
import '../runtime/runtime_dispatch_resolver.dart';
|
||||
import '../runtime/gateway_acp_client.dart';
|
||||
import '../runtime/codex_runtime.dart';
|
||||
import '../runtime/codex_config_bridge.dart';
|
||||
@ -67,6 +68,7 @@ class AppController extends ChangeNotifier {
|
||||
AccountRuntimeClient Function(String baseUrl)? accountClientFactory,
|
||||
Map<String, String>? environmentOverride,
|
||||
GoTaskServiceClient? goTaskServiceClient,
|
||||
RuntimeDispatchResolver? dispatchResolver,
|
||||
}) {
|
||||
environmentOverrideInternal = environmentOverride == null
|
||||
? null
|
||||
@ -145,10 +147,11 @@ class AppController extends ChangeNotifier {
|
||||
desktopPlatformServiceInternal =
|
||||
desktopPlatformService ?? createDesktopPlatformService();
|
||||
runtimeCoordinatorInternal.attachDispatchResolver(
|
||||
GoRuntimeDispatchDesktopClient(
|
||||
client: gatewayAcpClientInternal,
|
||||
endpointResolver: resolveGatewayAcpEndpointInternal,
|
||||
),
|
||||
dispatchResolver ??
|
||||
GoRuntimeDispatchDesktopClient(
|
||||
client: gatewayAcpClientInternal,
|
||||
endpointResolver: resolveGatewayAcpEndpointInternal,
|
||||
),
|
||||
);
|
||||
goTaskServiceClientInternal =
|
||||
goTaskServiceClient ??
|
||||
|
||||
@ -933,6 +933,14 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
..writeln('TaskThread workspace context:')
|
||||
..writeln('- sessionKey: $sessionKey')
|
||||
..writeln('- currentTaskWorkspace: $currentTaskWorkspace');
|
||||
if (workingDirectory.trim().isNotEmpty) {
|
||||
buffer.writeln('- localWorkspace: ${workingDirectory.trim()}');
|
||||
}
|
||||
if (remoteWorkingDirectoryHint.trim().isNotEmpty) {
|
||||
buffer.writeln(
|
||||
'- remoteWorkspaceHint: ${remoteWorkingDirectoryHint.trim()}',
|
||||
);
|
||||
}
|
||||
final visibleTaskInputAttachments = taskInputAttachments
|
||||
.where((item) => item.name.trim().isNotEmpty && item.key.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
|
||||
@ -70,7 +70,8 @@ class CodeAgentNodeOrchestrator {
|
||||
metadata: resolution.metadata,
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error: $e\n$stackTrace');
|
||||
// Dispatch metadata is advisory; task execution still carries routing.
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,9 +64,15 @@ class RuntimeCoordinator extends ChangeNotifier {
|
||||
}
|
||||
final normalizedCommand = provider.command.trim();
|
||||
if (normalizedCommand.isEmpty) {
|
||||
throw ArgumentError.value(provider.command, 'provider.command', 'Cannot be empty');
|
||||
throw ArgumentError.value(
|
||||
provider.command,
|
||||
'provider.command',
|
||||
'Cannot be empty',
|
||||
);
|
||||
}
|
||||
final normalizedCapabilities = _normalizeCapabilitySet(provider.capabilities).toList(growable: false)..sort();
|
||||
final normalizedCapabilities = _normalizeCapabilitySet(
|
||||
provider.capabilities,
|
||||
).toList(growable: false)..sort();
|
||||
|
||||
_externalCodeAgents[normalizedId] = ExternalCodeAgentProvider(
|
||||
id: normalizedId,
|
||||
@ -96,7 +102,8 @@ class RuntimeCoordinator extends ChangeNotifier {
|
||||
Iterable<String> requiredCapabilities = const <String>[],
|
||||
}) {
|
||||
final required = _normalizeCapabilitySet(requiredCapabilities);
|
||||
final providers = _externalCodeAgents.values
|
||||
final providers =
|
||||
_externalCodeAgents.values
|
||||
.where((provider) => _providerSupports(provider, required))
|
||||
.toList(growable: false)
|
||||
..sort((a, b) => a.id.compareTo(b.id));
|
||||
@ -147,7 +154,9 @@ class RuntimeCoordinator extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
final discovered = discoverExternalCodeAgents(requiredCapabilities: required);
|
||||
final discovered = discoverExternalCodeAgents(
|
||||
requiredCapabilities: required,
|
||||
);
|
||||
if (discovered.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
@ -250,7 +259,9 @@ class RuntimeCoordinator extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> stopCodeAgentRuntime() async {
|
||||
_state = gateway.isConnected ? CoordinatorState.ready : CoordinatorState.disconnected;
|
||||
_state = gateway.isConnected
|
||||
? CoordinatorState.ready
|
||||
: CoordinatorState.disconnected;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -264,12 +275,18 @@ class RuntimeCoordinator extends ChangeNotifier {
|
||||
|
||||
bool supportsCapability(String capability) {
|
||||
switch (capability) {
|
||||
case 'cloud-memory': return capabilities.hasCloudMemory;
|
||||
case 'task-queue': return capabilities.hasTaskQueue;
|
||||
case 'multi-agent': return capabilities.hasMultiAgent;
|
||||
case 'local-models': return capabilities.hasLocalModels;
|
||||
case 'code-agent': return capabilities.hasCodeAgent;
|
||||
default: return false;
|
||||
case 'cloud-memory':
|
||||
return capabilities.hasCloudMemory;
|
||||
case 'task-queue':
|
||||
return capabilities.hasTaskQueue;
|
||||
case 'multi-agent':
|
||||
return capabilities.hasMultiAgent;
|
||||
case 'local-models':
|
||||
return capabilities.hasLocalModels;
|
||||
case 'code-agent':
|
||||
return capabilities.hasCodeAgent;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -295,16 +312,24 @@ class RuntimeCoordinator extends ChangeNotifier {
|
||||
|
||||
Future<ModeSwitchResult> _switchMode(GatewayMode mode) {
|
||||
switch (mode) {
|
||||
case GatewayMode.remote: return modeSwitcher.switchToRemote();
|
||||
case GatewayMode.offline: return modeSwitcher.switchToOffline();
|
||||
case GatewayMode.remote:
|
||||
return modeSwitcher.switchToRemote();
|
||||
case GatewayMode.offline:
|
||||
return modeSwitcher.switchToOffline();
|
||||
}
|
||||
}
|
||||
|
||||
static Set<String> _normalizeCapabilitySet(Iterable<String> capabilities) {
|
||||
return capabilities.map((item) => item.trim().toLowerCase()).where((item) => item.isNotEmpty).toSet();
|
||||
return capabilities
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet();
|
||||
}
|
||||
|
||||
static bool _providerSupports(ExternalCodeAgentProvider provider, Set<String> requiredCapabilities) {
|
||||
static bool _providerSupports(
|
||||
ExternalCodeAgentProvider provider,
|
||||
Set<String> requiredCapabilities,
|
||||
) {
|
||||
if (requiredCapabilities.isEmpty) return true;
|
||||
final provided = _normalizeCapabilitySet(provider.capabilities);
|
||||
return requiredCapabilities.every(provided.contains);
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:xworkmate/features/assistant/assistant_page_composer_skill_picke
|
||||
import 'package:xworkmate/runtime/gateway_acp_client.dart';
|
||||
import 'package:xworkmate/runtime/go_task_service_client.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/runtime_dispatch_resolver.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
||||
import 'package:xworkmate/runtime/desktop_platform_service.dart';
|
||||
@ -1645,6 +1646,7 @@ void main() {
|
||||
'continue with the same image',
|
||||
attachments: <GatewayChatAttachmentPayload>[imageAttachment],
|
||||
);
|
||||
await fakeGoTaskService.waitForRequestCount(2);
|
||||
|
||||
expect(fakeGoTaskService.requests, hasLength(2));
|
||||
expect(
|
||||
@ -4608,6 +4610,7 @@ AppController _sandboxController({
|
||||
'HOME': actualHome,
|
||||
},
|
||||
goTaskServiceClient: goTaskServiceClient,
|
||||
dispatchResolver: _NoopRuntimeDispatchResolver(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -4847,6 +4850,16 @@ class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> waitForRequestCount(int count) async {
|
||||
final deadline = DateTime.now().add(const Duration(seconds: 15));
|
||||
while (requests.length < count && DateTime.now().isBefore(deadline)) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
if (requests.length < count) {
|
||||
throw StateError('Timed out waiting for $count requests.');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GoTaskServiceResult> getTask({
|
||||
required AssistantExecutionTarget target,
|
||||
@ -5008,3 +5021,107 @@ class _BlockingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
}
|
||||
|
||||
class _NoopRuntimeDispatchResolver implements RuntimeDispatchResolver {
|
||||
@override
|
||||
Future<RuntimeDispatchResolution> resolveGatewayDispatch({
|
||||
required List<ExternalCodeAgentProvider> providers,
|
||||
required String preferredProviderId,
|
||||
required Iterable<String> requiredCapabilities,
|
||||
required Map<String, dynamic> nodeState,
|
||||
required Map<String, dynamic> nodeInfo,
|
||||
}) async {
|
||||
final selectedProvider = _selectLocalProvider(
|
||||
providers,
|
||||
preferredProviderId,
|
||||
requiredCapabilities,
|
||||
);
|
||||
final metadata = <String, dynamic>{
|
||||
'node': <String, dynamic>{
|
||||
'id': nodeInfo['id']?.toString() ?? 'xworkmate-app',
|
||||
'name': nodeInfo['name']?.toString() ?? '',
|
||||
'version': nodeInfo['version']?.toString() ?? '',
|
||||
'kind': 'app-mediated-cooperative-node',
|
||||
'gatewayTransport': 'websocket-rpc',
|
||||
},
|
||||
'dispatch': <String, dynamic>{
|
||||
'mode': nodeState['bridgeEnabled'] == true
|
||||
? 'cooperative'
|
||||
: 'gateway-only',
|
||||
'executionTarget': nodeState['executionTarget']?.toString() ?? '',
|
||||
},
|
||||
'bridge': <String, dynamic>{
|
||||
'enabled': nodeState['bridgeEnabled'] == true,
|
||||
'state': nodeState['bridgeState']?.toString() ?? '',
|
||||
'gatewayConnected': nodeState['gatewayConnected'] == true,
|
||||
'runtimeMode': nodeState['runtimeMode']?.toString() ?? '',
|
||||
'localTransport': 'stdio-jsonrpc',
|
||||
},
|
||||
if (selectedProvider != null)
|
||||
'provider': <String, dynamic>{
|
||||
'id': selectedProvider.id,
|
||||
'name': selectedProvider.name,
|
||||
'defaultArgs': selectedProvider.defaultArgs,
|
||||
'capabilities': selectedProvider.capabilities,
|
||||
},
|
||||
};
|
||||
return RuntimeDispatchResolution(
|
||||
agentId:
|
||||
nodeState['selectedAgentId']?.toString().trim().isNotEmpty == true
|
||||
? nodeState['selectedAgentId'].toString().trim()
|
||||
: null,
|
||||
providerId: selectedProvider?.id,
|
||||
metadata: metadata,
|
||||
raw: metadata,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> selectProviderId({
|
||||
required List<ExternalCodeAgentProvider> providers,
|
||||
String preferredProviderId = '',
|
||||
Iterable<String> requiredCapabilities = const <String>[],
|
||||
}) async {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
|
||||
ExternalCodeAgentProvider? _selectLocalProvider(
|
||||
List<ExternalCodeAgentProvider> providers,
|
||||
String preferredProviderId,
|
||||
Iterable<String> requiredCapabilities,
|
||||
) {
|
||||
final required = requiredCapabilities
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
bool supports(ExternalCodeAgentProvider provider) {
|
||||
if (required.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
final capabilities = provider.capabilities
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet();
|
||||
return required.every(capabilities.contains);
|
||||
}
|
||||
|
||||
final normalizedPreferred = preferredProviderId.trim();
|
||||
if (normalizedPreferred.isNotEmpty) {
|
||||
for (final provider in providers) {
|
||||
if (provider.id == normalizedPreferred && supports(provider)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final provider in providers) {
|
||||
if (supports(provider)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user