test,docs: fix all stale references to deleted multi-agent subsystem

Test fixes (6 files, -303 lines):
- Delete app_controller_acp_mount_resilience_test.dart (entirely about deleted types)
- Remove multi-agent test cases from gateway_acp_client_auth_test.dart
- Rename _manifestWithDesktopMultiAgentEnabled → _defaultDesktopManifest
  in assistant_execution_target_test, assistant_lower_pane_test,
  mobile_assistant_page_test

Docs fixes (6 files):
- Regenerate public-symbol-inventory.json/md via make docs-public-api
- Remove multi-agent sections from public-api/models-and-config.md,
  app-orchestration.md, runtime-contracts.md
- Fix xworkmate/ → xworkmate-app/ paths in cloud-session doc
- Remove multiAgent references from app-external-service-api-test-matrix.md
This commit is contained in:
Cowork 3P 2026-06-04 06:31:45 +00:00
parent c320992b95
commit b3da52d0c5
12 changed files with 1306 additions and 1577 deletions

View File

@ -20,15 +20,15 @@ Terminal 先不作为当前目标的一等端,只保留为未来可扩展方
当前仓库里已经存在一组很适合作为目标架构起点的实现:
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/runtime/runtime_models_runtime_payloads.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/runtime/runtime_models_runtime_payloads.dart)
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/runtime/runtime_models_runtime_payloads.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/runtime/runtime_models_runtime_payloads.dart)
已定义 `TaskThread`、`WorkspaceBinding`、`ExecutionBinding`、`ThreadContextState`、`ThreadLifecycleState` 等核心线程模型。
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/web/web_session_repository.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/web/web_session_repository.dart)
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/web/web_session_repository.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/web/web_session_repository.dart)
已有浏览器本地缓存和远端会话仓库的抽象雏形。
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/web/web_store.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/web/web_store.dart)
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/web/web_store.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/web/web_store.dart)
已有 Web 端本地持久化、会话 client id、远端 relay token 的存储能力。
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/app/app_controller_web_gateway_relay.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/app/app_controller_web_gateway_relay.dart)
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/app/app_controller_web_gateway_relay.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/app/app_controller_web_gateway_relay.dart)
已有远端 relay 连接、会话刷新、历史同步和模型刷新逻辑。
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/app/app_controller_desktop_thread_sessions.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/app/app_controller_desktop_thread_sessions.dart)
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/app/app_controller_desktop_thread_sessions.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/app/app_controller_desktop_thread_sessions.dart)
已有桌面端围绕 `sessionKey` / `TaskThread` 的线程上下文读取逻辑。
因此,目标不是重写,而是把“本地控制器 + relay + 线程模型”升级成“跨端会话平台”。
@ -288,7 +288,7 @@ Terminal 先不纳入当前落地范围。
### 7.2 `WebSessionRepository` 升级方向
当前 [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/web/web_session_repository.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/web/web_session_repository.dart) 已经有 `BrowserWebSessionRepository` 和 `RemoteWebSessionRepository`。
当前 [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/web/web_session_repository.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/web/web_session_repository.dart) 已经有 `BrowserWebSessionRepository` 和 `RemoteWebSessionRepository`。
建议后续演进为:
@ -304,7 +304,7 @@ Terminal 先不纳入当前落地范围。
### 7.3 `WebStore` 的边界
当前 [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/web/web_store.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/lib/web/web_store.dart) 适合作为:
当前 [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/web/web_store.dart`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/web/web_store.dart) 适合作为:
- client id 存储
- 本地草稿缓存
@ -456,11 +456,11 @@ sequenceDiagram
## 12. 与现有文档的关系
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-control-plane-unification.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-control-plane-unification.md)
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-control-plane-unification.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-control-plane-unification.md)
说明当前 `TaskThread` 主模型。
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-thread-session-key-isolation-20260329.md`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/task-thread-session-key-isolation-20260329.md)
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-thread-session-key-isolation-20260329.md`](file:///Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-thread-session-key-isolation-20260329.md)
说明 `sessionKey` 与线程身份隔离约束。
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/xworkmate-layered-architecture.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate/docs/architecture/xworkmate-layered-architecture.md)
- [`/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/xworkmate-layered-architecture.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/xworkmate-layered-architecture.md)
说明当前内部状态如何围绕 `TaskThread` 组织。
本文在这些基础上进一步上升一层,定义跨设备会话的目标架构。

View File

@ -45,9 +45,7 @@
| `accountClientFactory` | `AccountRuntimeClient Function(String)?` | No | default impl | account runtime client 构造 |
| `environmentOverride` | `Map<String, String>?` | No | `null` | 测试/运行时环境覆盖 |
| `singleAgentSharedSkillScanRootOverrides` | `List<String>?` | No | `null` | 共享 skill 扫描根覆写 |
| `arisBundleRepository` | `ArisBundleRepository?` | No | default impl | ARIS bundle 发现仓库 |
| `goTaskServiceClient` | `GoTaskServiceClient?` | No | `DesktopGoTaskService` | 外部 ACP / gateway 任务入口 |
| `multiAgentMountManager` | `MultiAgentMountManager?` | No | default impl | 多 agent mount 管理器 |
### Returns
@ -161,9 +159,6 @@
| --- | --- | --- | --- |
| `loadAssistantArtifactSnapshot` | `{String? sessionKey}` | `Future<AssistantArtifactSnapshot>` | 加载某会话 artifact 清单快照 |
| `loadAssistantArtifactPreview` | `AssistantArtifactEntry artifact, {String? sessionKey}` | `Future<AssistantArtifactPreview>` | 读取 artifact 预览内容 |
| `saveMultiAgentConfig` | `MultiAgentConfig config` | `Future<void>` | 持久化多 agent 配置 |
| `refreshMultiAgentMounts` | `{bool sync=false}` | `Future<void>` | 刷新或同步 mount |
| `runMultiAgentCollaboration` | named args | `Future<void>` | 触发多 agent 协作执行 |
| `openOnlineWorkspace` | none | `Future<void>` | 打开在线工作区入口 |
## `AppControllerDesktopWorkspaceExecution`

View File

@ -184,49 +184,6 @@
| `copyWith(...)` | `AiGatewayProfile` | 更新 profile |
| `toJson()` / `fromJson(...)` | contract mapping | 序列化 / 恢复 |
## `MultiAgentConfig`
- Source: `lib/runtime/runtime_models_multi_agent.dart`
- Type: `class`
- Responsibility:
描述 native / ARIS 多 agent 协作的总配置,包括角色 worker、迭代次数、超时、AI gateway 注入策略、managed skills/MCP/mount targets。
### Constructor Parameters
| Param Group | Meaning |
| --- | --- |
| enable/framework | `enabled`, `autoSync`, `framework`, `arisEnabled`, `arisMode`, `arisBundleVersion`, `arisCompatStatus` |
| worker roles | `architect`, `engineer`, `tester` |
| execution policy | `ollamaEndpoint`, `maxIterations`, `minAcceptableScore`, `timeoutSeconds`, `aiGatewayInjectionPolicy` |
| managed assets | `managedSkills`, `managedMcpServers`, `mountTargets` |
### Key Returns
| API / Getter | Returns | Meaning |
| --- | --- | --- |
| `defaults()` | `MultiAgentConfig` | 默认协作配置 |
| `architectEnabled` / `architectTool` / `architectModel` | primitive | Architect 快捷访问 |
| `engineerTool` / `engineerModel` | primitive | Engineer 快捷访问 |
| `testerTool` / `testerModel` | primitive | Tester 快捷访问 |
| `usesAris` | `bool` | 当前是否落到 ARIS |
## `AgentWorkerConfig`
- Source: `lib/runtime/runtime_models_multi_agent.dart`
- Type: `class`
- Responsibility:
描述单个角色 worker 的 CLI、模型与重试策略。
### Constructor Parameters
| Param | Type | Required | Default | Meaning |
| --- | --- | --- | --- | --- |
| `role` | `MultiAgentRole` | Yes | none | 角色 |
| `cliTool` | `String` | Yes | none | `claude/codex/opencode/...` |
| `model` | `String` | Yes | none | 对应模型 |
| `enabled` | `bool` | Yes | none | 是否启用 |
| `maxRetries` | `int` | No | `2` | 最大重试次数 |
## `AppTheme` / `AppPalette`
- Source: `lib/theme/app_theme.dart`, `lib/theme/app_palette.dart`

View File

@ -8,7 +8,7 @@
- task request / routing request / provider catalog
- settings 与 secret 持久化访问
- gateway session / agent / chat 控制器
- desktop 平台能力、skill 目录授权、多 agent mount
- desktop 平台能力、skill 目录授权
- Codex CLI 与 config bridge
## `GatewayAcpException`
@ -37,14 +37,13 @@
- Source: `lib/runtime/gateway_acp_client.dart`
- Type: `class`
- Responsibility:
保存 `acp.capabilities` 的解析结果,是 app 侧 single-agent / multi-agent / execution-target / provider catalog 的只读快照。
保存 `acp.capabilities` 的解析结果,是 app 侧 single-agent / execution-target / provider catalog 的只读快照。
### Constructor Parameters
| Param | Type | Required | Meaning |
| --- | --- | --- | --- |
| `singleAgent` | `bool` | Yes | 是否支持单 agent |
| `multiAgent` | `bool` | Yes | 是否支持多 agent |
| `availableExecutionTargets` | `List<AssistantExecutionTarget>` | Yes | 当前 bridge 允许的执行目标 |
| `providerCatalog` | `List<SingleAgentProvider>` | Yes | agent 侧 provider catalog |
| `gatewayProviderCatalog` | `List<SingleAgentProvider>` | Yes | gateway 侧 provider catalog |
@ -57,25 +56,6 @@
| --- | --- | --- |
| `GatewayAcpCapabilities.empty()` | `GatewayAcpCapabilities` | 空 capability 占位 |
## `GatewayAcpMultiAgentRequest`
- Source: `lib/runtime/gateway_acp_client.dart`
- Type: `class`
- Responsibility:
描述一次 multi-agent session.start / session.message 请求。
### Constructor Parameters
| Param | Type | Required | Meaning |
| --- | --- | --- | --- |
| `sessionId` | `String` | Yes | 协作会话 ID |
| `threadId` | `String` | Yes | thread ID |
| `prompt` | `String` | Yes | 主任务提示词 |
| `workingDirectory` | `String` | Yes | 工作目录 |
| `attachments` | `List<CollaborationAttachment>` | Yes | 本地文件附件 |
| `selectedSkills` | `List<String>` | Yes | 显式选中的技能键 |
| `resumeSession` | `bool` | Yes | `false` 时发 `session.start`,`true` 时发 `session.message` |
## `GatewayAcpClient`
- Source: `lib/runtime/gateway_acp_client.dart`
@ -95,19 +75,17 @@
| Method | Parameters | Returns | Meaning |
| --- | --- | --- | --- |
| `loadCapabilities` | `{bool forceRefresh=false, Uri? endpointOverride, String authorizationOverride=''}` | `Future<GatewayAcpCapabilities>` | 拉取并缓存 15 秒 capability 快照 |
| `runMultiAgent` | `GatewayAcpMultiAgentRequest request` | `Stream<MultiAgentRunEvent>` | 打开 ACP multi-agent 事件流 |
### Main Call Chain
- `AppController.refreshAcpCapabilitiesRuntimeInternal` -> `loadCapabilities`
- `GoRuntimeDispatchDesktopClient` / multi-agent flows -> `GatewayAcpClient`
- `GoRuntimeDispatchDesktopClient` -> `GatewayAcpClient`
- `GatewayAcpClient` -> ACP JSON-RPC -> bridge
### Side Effects
- 网络请求
- 本地 capability 缓存
- 将 ACP 通知转换成 `MultiAgentRunEvent`
## `ExternalCodeAgentAcpCapabilities`
@ -188,14 +166,11 @@
| `provider` | `SingleAgentProvider` | No | 当前 provider |
| `remoteWorkingDirectoryHint` | `String` | No | 远端工作目录 hint |
| `resumeSession` | `bool` | No | 是否续跑 |
| `collaborationMode` | `GoTaskServiceCollaborationMode` | No | standard / multiAgent |
| `multiAgent` | `bool` | No | 是否强制多 agent |
### Returns
| Getter / Method | Returns | Meaning |
| --- | --- | --- |
| `isMultiAgentRequest` | `bool` | 是否走多 agent route |
| `route` | `GoTaskServiceRoute` | 计算出的实际 route |
| `acpMode` | `String` | ACP session mode |
| `routingExecutionTarget` | `String` | 发送给 routing 的目标值 |

View File

@ -76,7 +76,7 @@ Last Updated: 2026-04-22
| accounts | `/api/auth/login` | `POST` | 无 | 登录并拿 session token | Apple 审核只读账号使用 |
| accounts | `/api/auth/session` | `GET` | `Authorization: Bearer <session token>` | 获取当前会话和用户信息 | APP 端登录态校验 |
| accounts | `/api/auth/xworkmate/profile/sync` | `GET` | `Authorization: Bearer <session token>` | 拉取 bridge 同步元数据 | 返回 `BRIDGE_SERVER_URL` / `BRIDGE_AUTH_TOKEN` |
| bridge | `/acp/rpc` | `POST` | `Authorization: Bearer <BRIDGE_AUTH_TOKEN>` | bridge JSON-RPC 主入口 | capabilities、routing、agent / multi-agent 任务、OpenClaw gateway 任务、cancel、close |
| bridge | `/acp/rpc` | `POST` | `Authorization: Bearer <BRIDGE_AUTH_TOKEN>` | bridge JSON-RPC 主入口 | capabilities、routing、agent 任务、OpenClaw gateway 任务、cancel、close |
| bridge | `acp.capabilities` | JSON-RPC method | 同上 | 拉取 provider catalog / target catalog | capability 只读快照 |
| bridge | `xworkmate.routing.resolve` | JSON-RPC method | 同上 | 解析 provider / gateway / skills 路由 | 返回 resolved / unavailable 信息 |
| bridge | `session.start` | JSON-RPC method | 同上 | 开启新会话 | session 生命周期起点 |
@ -243,7 +243,6 @@ Last Updated: 2026-04-22
- `providerCatalog`
- `gatewayProviders`
- `singleAgent`
- `multiAgent`
- `capabilities`
#### 当前实测结果
@ -580,7 +579,7 @@ Last Updated: 2026-04-22
### 8.3 建议断言
- `Authorization` 必须是 `Bearer <token>`
- capability、routing、agent / multi-agent 任务、OpenClaw gateway `session.start` / `session.message`、`session.cancel`、`session.close` 请求路径必须是 `/acp/rpc`
- capability、routing、agent 任务、OpenClaw gateway `session.start` / `session.message`、`session.cancel`、`session.close` 请求路径必须是 `/acp/rpc`
- 不允许出现 `/acp-server/` 直连请求
- 不允许出现 `/gateway/openclaw` app-facing 请求;OpenClaw gateway 目标必须通过 routing metadata 表达
- `session.cancel` / `session.close` 必须接受 `sessionId` + `threadId`
@ -610,7 +609,7 @@ Last Updated: 2026-04-22
- 账户侧负责登录与同步元数据
- bridge 侧负责 capability、路由解析和会话生命周期
- capability、routing、agent / multi-agent 任务、OpenClaw gateway 任务、cancel、close 经过 `/acp/rpc`
- capability、routing、agent 任务、OpenClaw gateway 任务、cancel、close 经过 `/acp/rpc`
- OpenClaw `session.start` / `session.message` 通过 `/acp/rpc` routing metadata 表达 gateway/openclaw 目标
- `session.cancel` / `session.close` 的协议入口已验证可用
- 当前剩余风险集中在桥接后端下游 provider 连接,而不是 APP 侧接口拼接

View File

@ -64,7 +64,7 @@ void main() {
testWidgets('shows mode-specific provider catalogs', (tester) async {
final controller = AppController(
environmentOverride: const <String, String>{},
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.opencode,
@ -214,7 +214,7 @@ void main() {
) async {
final controller = AppController(
environmentOverride: const <String, String>{},
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.opencode,
@ -264,7 +264,7 @@ void main() {
) async {
final controller = AppController(
environmentOverride: const <String, String>{},
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.opencode,
@ -377,7 +377,7 @@ void main() {
(tester) async {
final controller = AppController(
environmentOverride: const <String, String>{},
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.opencode,
@ -733,19 +733,9 @@ void main() {
});
}
UiFeatureManifest _manifestWithDesktopMultiAgentEnabled() {
UiFeatureManifest _defaultDesktopManifest() {
return UiFeatureManifest.fromYamlString(
File(UiFeatureManifest.assetPath).readAsStringSync(),
).copyWithFeature(
platform: UiFeaturePlatform.desktop,
module: 'assistant',
feature: 'multi_agent',
enabled: true,
buildModes: const <UiFeatureBuildMode>{
UiFeatureBuildMode.debug,
UiFeatureBuildMode.profile,
UiFeatureBuildMode.release,
},
);
}

View File

@ -105,7 +105,7 @@ void main() {
) async {
final controller = AppController(
environmentOverride: const <String, String>{},
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
],
@ -248,18 +248,8 @@ Widget _buildTestApp({
);
}
UiFeatureManifest _manifestWithDesktopMultiAgentEnabled() {
UiFeatureManifest _defaultDesktopManifest() {
return UiFeatureManifest.fromYamlString(
File(UiFeatureManifest.assetPath).readAsStringSync(),
).copyWithFeature(
platform: UiFeaturePlatform.desktop,
module: 'assistant',
feature: 'multi_agent',
enabled: true,
buildModes: const <UiFeatureBuildMode>{
UiFeatureBuildMode.debug,
UiFeatureBuildMode.profile,
UiFeatureBuildMode.release,
},
);
}

View File

@ -1,163 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/app_controller.dart';
import 'package:xworkmate/runtime/multi_agent_mount_resolver.dart';
import 'package:xworkmate/runtime/multi_agent_mounts.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
test(
'capability refresh does not fail when remote mount reconcile is absent',
() async {
final server = await _CapabilityServer.start();
addTearDown(server.close);
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-mount-resilience-',
);
addTearDown(() async {
if (await storeRoot.exists()) {
await storeRoot.delete(recursive: true);
}
});
final store = SecureConfigStore(
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
supportRootPathResolver: () async => '${storeRoot.path}/support',
);
await store.initialize();
final settings = SettingsSnapshot.defaults();
final bridgeConfig = settings.acpBridgeServerModeConfig;
final selfHosted = bridgeConfig.selfHosted.copyWith(
serverUrl: server.endpoint,
username: 'admin',
);
await store.saveSecretValueByRef(selfHosted.passwordRef, 'bridge-token');
await store.saveSettingsSnapshot(
settings.copyWith(
acpBridgeServerModeConfig: bridgeConfig.copyWith(
selfHosted: selfHosted,
effective: AcpBridgeServerEffectiveConfig(
endpoint: server.endpoint,
tokenRef: selfHosted.passwordRef,
source: 'bridge',
reason: 'test bridge',
),
),
),
);
final controller = AppController(
store: store,
environmentOverride: <String, String>{'HOME': storeRoot.path},
multiAgentMountManager: MultiAgentMountManager(
resolver: _MissingRemoteMountResolver(),
),
);
addTearDown(controller.dispose);
await _waitForInitialization(controller);
expect(controller.bootstrapError, isNull);
expect(controller.bridgeCapabilitiesRefreshErrorInternal, isEmpty);
expect(
controller.gatewayProviderCatalog.map((item) => item.providerId),
contains('openclaw'),
);
},
);
}
Future<void> _waitForInitialization(AppController controller) async {
final deadline = DateTime.now().add(const Duration(seconds: 10));
while (controller.initializing && DateTime.now().isBefore(deadline)) {
await Future<void>.delayed(const Duration(milliseconds: 100));
}
if (controller.initializing) {
fail('controller did not initialize');
}
}
class _MissingRemoteMountResolver implements MultiAgentMountResolver {
@override
Future<MultiAgentConfig?> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
required String codexHome,
required String opencodeHome,
required ArisMountProbe arisProbe,
}) {
throw StateError('unknown method: xworkmate.mounts.reconcile');
}
@override
Future<void> dispose() async {}
}
class _CapabilityServer {
_CapabilityServer(this._server);
final HttpServer _server;
String get endpoint => 'http://${_server.address.host}:${_server.port}';
static Future<_CapabilityServer> start() async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
final wrapper = _CapabilityServer(server);
server.listen(wrapper._handle);
return wrapper;
}
Future<void> close() => _server.close(force: true);
Future<void> _handle(HttpRequest request) async {
final raw = await utf8.decoder.bind(request).join();
final decoded = jsonDecode(raw) as Map<String, dynamic>;
final method = decoded['method']?.toString() ?? '';
request.response.headers.contentType = ContentType.json;
if (method != 'acp.capabilities') {
request.response.write(
jsonEncode(<String, Object?>{
'jsonrpc': '2.0',
'id': decoded['id'],
'error': <String, Object?>{
'code': -32601,
'message': 'unknown method: $method',
},
}),
);
await request.response.close();
return;
}
request.response.write(
jsonEncode(<String, Object?>{
'jsonrpc': '2.0',
'id': decoded['id'],
'result': <String, Object?>{
'singleAgent': true,
'multiAgent': true,
'availableExecutionTargets': <String>['agent', 'gateway'],
'providerCatalog': <Map<String, Object?>>[
<String, Object?>{
'providerId': 'codex',
'label': 'Codex',
'targets': <String>['agent'],
},
],
'gatewayProviders': <Map<String, Object?>>[
<String, Object?>{
'providerId': 'openclaw',
'label': 'OpenClaw',
'targets': <String>['gateway'],
},
],
},
}),
);
await request.response.close();
}
}

View File

@ -64,7 +64,7 @@ void main() {
() async {
final controller = AppController(
environmentOverride: const <String, String>{},
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.openclaw,
@ -110,7 +110,7 @@ void main() {
() async {
final controller = AppController(
environmentOverride: const <String, String>{},
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
initialBridgeProviderCatalog: const <SingleAgentProvider>[
SingleAgentProvider.codex,
SingleAgentProvider.opencode,
@ -4219,26 +4219,16 @@ List<Map<String, dynamic>> _generatedArtifactPayloads() {
];
}
UiFeatureManifest _manifestWithDesktopMultiAgentEnabled() {
UiFeatureManifest _defaultDesktopManifest() {
return UiFeatureManifest.fromYamlString(
File(UiFeatureManifest.assetPath).readAsStringSync(),
).copyWithFeature(
platform: UiFeaturePlatform.desktop,
module: 'assistant',
feature: 'multi_agent',
enabled: true,
buildModes: const <UiFeatureBuildMode>{
UiFeatureBuildMode.debug,
UiFeatureBuildMode.profile,
UiFeatureBuildMode.release,
},
);
}
AppController _connectedController(GoTaskServiceClient client) {
return AppController(
goTaskServiceClient: client,
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
environmentOverride: const <String, String>{
'BRIDGE_AUTH_TOKEN': 'bridge-token',
},
@ -4254,7 +4244,7 @@ AppController _connectedController(GoTaskServiceClient client) {
AppController _connectedGatewayController(GoTaskServiceClient client) {
return AppController(
goTaskServiceClient: client,
uiFeatureManifest: _manifestWithDesktopMultiAgentEnabled(),
uiFeatureManifest: _defaultDesktopManifest(),
environmentOverride: const <String, String>{
'BRIDGE_AUTH_TOKEN': 'bridge-token',
},

View File

@ -1900,14 +1900,6 @@ void main() {
provider: SingleAgentProvider.unspecified,
),
);
final multiAgentGateway = controller
.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.openclaw,
multiAgent: true,
),
);
final agentTask = controller.resolveExternalAcpEndpointForRequestInternal(
_taskRequest(
target: AssistantExecutionTarget.agent,
@ -1918,7 +1910,6 @@ void main() {
expect(openClawStart?.path, '/acp/rpc');
expect(openClawFollowUp?.path, '/acp/rpc');
expect(unspecifiedGateway?.path, '/acp/rpc');
expect(multiAgentGateway?.path, '/acp/rpc');
expect(agentTask?.path, '/acp/rpc');
});
@ -2138,90 +2129,7 @@ void main() {
},
);
test('multi-agent execution uses session lifecycle methods', () async {
final capture = await _startAcpHttpServer();
addTearDown(capture.close);
final client = GatewayAcpClient(
endpointResolver: () => capture.baseEndpoint,
authorizationResolver: (_) async => 'bridge-token',
);
final events = await client
.runMultiAgent(
const GatewayAcpMultiAgentRequest(
sessionId: 'session-1',
threadId: 'session-1',
prompt: 'hi',
workingDirectory: '/tmp',
attachments: <CollaborationAttachment>[],
selectedSkills: <String>[],
resumeSession: false,
),
)
.toList();
expect(events, isNotEmpty);
expect(
capture.requestBodies,
contains(
predicate<String>((body) {
return body.contains('"method":"session.start"');
}),
),
);
expect(
capture.requestBodies,
isNot(
contains(
predicate<String>((body) {
return body.contains('"method":"thread/start"');
}),
),
),
);
});
test('multi-agent follow-up uses session.message', () async {
final capture = await _startAcpHttpServer();
addTearDown(capture.close);
final client = GatewayAcpClient(
endpointResolver: () => capture.baseEndpoint,
authorizationResolver: (_) async => 'bridge-token',
);
await client
.runMultiAgent(
const GatewayAcpMultiAgentRequest(
sessionId: 'session-1',
threadId: 'session-1',
prompt: 'hi',
workingDirectory: '/tmp',
attachments: <CollaborationAttachment>[],
selectedSkills: <String>[],
resumeSession: true,
),
)
.toList();
expect(
capture.requestBodies,
contains(
predicate<String>((body) {
return body.contains('"method":"session.message"');
}),
),
);
expect(
capture.requestBodies,
isNot(
contains(
predicate<String>((body) {
return body.contains('"method":"turn/start"');
}),
),
),
);
});
});
}
@ -2249,7 +2157,6 @@ GoTaskServiceRequest _taskRequest({
required AssistantExecutionTarget target,
required SingleAgentProvider provider,
bool resumeSession = false,
bool multiAgent = false,
String remoteWorkingDirectoryHint = '',
}) {
return GoTaskServiceRequest(
@ -2268,7 +2175,6 @@ GoTaskServiceRequest _taskRequest({
provider: provider,
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint,
resumeSession: resumeSession,
multiAgent: multiAgent,
);
}