fix: sync custom single-agent providers before execution

This commit is contained in:
Haitao Pan 2026-04-06 14:33:20 +08:00
parent 10015e56f8
commit ffbd0f1c5f
4 changed files with 151 additions and 24 deletions

View File

@ -86,11 +86,12 @@ extension AppControllerDesktopSingleAgent on AppController {
try {
final routing = buildExternalAcpRoutingForSessionInternal(sessionKey);
final selection = singleAgentProviderForSession(sessionKey);
await syncExternalAcpProvidersInternal();
final capabilities = await goTaskServiceClientInternal
.loadExternalAcpCapabilities(
target: AssistantExecutionTarget.singleAgent,
forceRefresh: true,
);
target: AssistantExecutionTarget.singleAgent,
forceRefresh: true,
);
final availableProviders = configuredSingleAgentProviders
.where(capabilities.providers.contains)
.toList(growable: false);
@ -222,12 +223,15 @@ extension AppControllerDesktopSingleAgent on AppController {
final resolvedWorkingDirectory = result.resolvedWorkingDirectory.trim();
if (resolvedWorkspaceKind != null &&
resolvedWorkingDirectory.isNotEmpty) {
final existingThread = requireTaskThreadForSessionInternal(sessionKey);
final existingThread = requireTaskThreadForSessionInternal(
sessionKey,
);
upsertTaskThreadInternal(
sessionKey,
workspaceBinding: WorkspaceBinding(
workspaceId: existingThread.workspaceBinding.workspaceId,
workspaceKind: resolvedWorkspaceKind == WorkspaceRefKind.remotePath
workspaceKind:
resolvedWorkspaceKind == WorkspaceRefKind.remotePath
? WorkspaceKind.remoteFs
: WorkspaceKind.localFs,
workspacePath: resolvedWorkingDirectory,

View File

@ -15,7 +15,8 @@ typedef ExternalCodeAgentAcpProcessStarter =
String? workingDirectory,
});
class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransport {
class ExternalCodeAgentAcpDesktopTransport
implements ExternalCodeAgentAcpTransport {
ExternalCodeAgentAcpDesktopTransport({
required GatewayAcpClient acpClient,
required Uri? Function(AssistantExecutionTarget target) endpointResolver,
@ -32,7 +33,7 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp
arguments,
environment: environment,
workingDirectory: workingDirectory,
);
);
});
final GatewayAcpClient _acpClient;
@ -43,22 +44,21 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp
Process? _localProcess;
Uri? _localEndpoint;
Future<Uri?>? _localEndpointFuture;
List<ExternalCodeAgentAcpSyncedProvider> _syncedProviders =
const <ExternalCodeAgentAcpSyncedProvider>[];
@override
Future<void> syncExternalProviders(
List<ExternalCodeAgentAcpSyncedProvider> providers,
) async {
_syncedProviders = List<ExternalCodeAgentAcpSyncedProvider>.unmodifiable(
providers,
);
final endpoint = await _ensureLocalEndpoint();
if (endpoint == null) {
return;
}
await _acpClient.request(
method: 'xworkmate.providers.sync',
params: <String, dynamic>{
'providers': providers.map((item) => item.toJson()).toList(growable: false),
},
endpointOverride: endpoint,
);
await _syncProvidersToEndpoint(endpoint, _syncedProviders);
}
@override
@ -70,6 +70,10 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp
if (endpoint == null) {
return const ExternalCodeAgentAcpCapabilities.empty();
}
if (target == AssistantExecutionTarget.singleAgent ||
target == AssistantExecutionTarget.auto) {
await _syncProvidersToEndpoint(endpoint, _syncedProviders);
}
final capabilities = await _acpClient.loadCapabilities(
forceRefresh: forceRefresh,
endpointOverride: endpoint,
@ -94,6 +98,10 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp
code: 'EXTERNAL_ACP_ENDPOINT_MISSING',
);
}
if (request.target == AssistantExecutionTarget.singleAgent ||
request.target == AssistantExecutionTarget.auto) {
await _syncProvidersToEndpoint(endpoint, _syncedProviders);
}
var streamedText = '';
String? completedMessage;
final response = await _acpClient.request(
@ -251,4 +259,22 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp
await dispose();
return null;
}
Future<void> _syncProvidersToEndpoint(
Uri endpoint,
List<ExternalCodeAgentAcpSyncedProvider> providers,
) async {
if (providers.isEmpty) {
return;
}
await _acpClient.request(
method: 'xworkmate.providers.sync',
params: <String, dynamic>{
'providers': providers
.map((item) => item.toJson())
.toList(growable: false),
},
endpointOverride: endpoint,
);
}
}

View File

@ -132,13 +132,21 @@ class FakeGoTaskServiceClientInternal implements GoTaskServiceClient {
int capabilitiesCalls = 0;
int executeCalls = 0;
int cancelCalls = 0;
int syncProvidersCalls = 0;
GoTaskServiceRequest? lastRequest;
final List<GoTaskServiceRequest> requests = <GoTaskServiceRequest>[];
final List<List<ExternalCodeAgentAcpSyncedProvider>> syncedProvidersHistory =
<List<ExternalCodeAgentAcpSyncedProvider>>[];
@override
Future<void> syncExternalProviders(
List<ExternalCodeAgentAcpSyncedProvider> providers,
) async {}
) async {
syncProvidersCalls += 1;
syncedProvidersHistory.add(
List<ExternalCodeAgentAcpSyncedProvider>.unmodifiable(providers),
);
}
@override
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({

View File

@ -41,7 +41,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: 'codex-sonnet',
route: GoTaskServiceRoute.externalAcpSingle,
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(
@ -100,6 +100,93 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
},
);
test(
'AppController syncs custom single-agent providers before execution',
() async {
final tempDirectory = await createTempDirectoryInternal(
'xworkmate-single-agent-custom-provider-',
);
final store = createStoreFromTempDirectoryInternal(tempDirectory);
const customProvider = SingleAgentProvider(
providerId: 'custom-agent-1',
label: 'Codex',
badge: 'C',
);
final client = FakeGoTaskServiceClientInternal(
capabilities: ExternalCodeAgentAcpCapabilities(
singleAgent: true,
multiAgent: false,
providers: <SingleAgentProvider>{customProvider},
raw: <String, dynamic>{},
),
result: const GoTaskServiceResult(
success: true,
message: 'CUSTOM_PROVIDER_REPLY',
turnId: 'turn-custom',
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(
store: store,
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
customProvider,
],
runtimeCoordinator: RuntimeCoordinator(
gateway: FakeGatewayRuntimeInternal(store: store),
codex: FakeCodexRuntimeInternal(),
),
goTaskServiceClient: client,
);
await controller.saveSettings(
controller.settings.copyWith(
externalAcpEndpoints: normalizeExternalAcpEndpoints(
profiles: const <ExternalAcpEndpointProfile>[
ExternalAcpEndpointProfile(
providerKey: 'custom-agent-1',
label: 'Codex',
badge: 'C',
endpoint: 'ws://127.0.0.1:9101/acp',
authRef: '',
enabled: true,
),
],
),
multiAgent: controller.settings.multiAgent.copyWith(
autoSync: false,
),
),
refreshAfterSave: false,
);
await controller.setAssistantExecutionTarget(
AssistantExecutionTarget.singleAgent,
);
await controller.setSingleAgentProvider(customProvider);
await controller.sendChatMessage(
'请输出 CUSTOM_PROVIDER_REPLY',
thinking: 'low',
);
expect(client.syncProvidersCalls, greaterThanOrEqualTo(1));
expect(client.executeCalls, 1);
expect(client.lastRequest?.provider, customProvider);
expect(
client.syncedProvidersHistory.any(
(batch) => batch.any(
(provider) =>
provider.providerId == 'custom-agent-1' &&
provider.endpoint == 'ws://127.0.0.1:9101/acp',
),
),
isTrue,
);
},
);
test(
'AppController treats Auto as ready before the first routing resolution when any route is available',
() async {
@ -173,7 +260,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: 'codex-sonnet',
route: GoTaskServiceRoute.externalAcpSingle,
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(
@ -220,7 +307,9 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
final store = createStoreFromTempDirectoryInternal(tempDirectory);
await store.initialize();
await store.saveSettingsSnapshot(
SettingsSnapshot.defaults().copyWith(workspacePath: tempDirectory.path),
SettingsSnapshot.defaults().copyWith(
workspacePath: tempDirectory.path,
),
);
final client = FakeGoTaskServiceClientInternal(
capabilities: ExternalCodeAgentAcpCapabilities(
@ -236,7 +325,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: 'codex-sonnet',
route: GoTaskServiceRoute.externalAcpSingle,
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(
@ -311,7 +400,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: 'codex-sonnet',
route: GoTaskServiceRoute.externalAcpSingle,
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(
@ -607,7 +696,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(
@ -667,7 +756,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(
@ -748,7 +837,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(
@ -839,7 +928,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
route: GoTaskServiceRoute.externalAcpSingle,
),
);
final controller = await createAppControllerInternal(