From 90aaa084b0085550d6bca3703e5cb5e608ac72e8 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 24 Apr 2026 10:09:20 +0800 Subject: [PATCH] fix: use bridge session lifecycle methods --- .../public-api/runtime-contracts.md | 4 +- .../task-control-plane-unification.md | 2 +- docs/testing/api-script-runbook.md | 6 +- .../app-external-service-api-test-matrix.md | 38 +++--- ...rnal_code_agent_acp_desktop_transport.dart | 2 +- lib/runtime/gateway_acp_client.dart | 5 +- scripts/ci/verify_api_scenario_contract.sh | 14 +- .../runtime/gateway_acp_client_auth_test.dart | 124 +++++++++++++++++- 8 files changed, 157 insertions(+), 38 deletions(-) diff --git a/docs/architecture/public-api/runtime-contracts.md b/docs/architecture/public-api/runtime-contracts.md index ea371cf4..5b4b811c 100644 --- a/docs/architecture/public-api/runtime-contracts.md +++ b/docs/architecture/public-api/runtime-contracts.md @@ -62,7 +62,7 @@ - Source: `lib/runtime/gateway_acp_client.dart` - Type: `class` - Responsibility: - 描述一次 multi-agent thread/start / turn/start 请求。 + 描述一次 multi-agent session.start / session.message 请求。 ### Constructor Parameters @@ -74,7 +74,7 @@ | `workingDirectory` | `String` | Yes | 工作目录 | | `attachments` | `List` | Yes | 本地文件附件 | | `selectedSkills` | `List` | Yes | 显式选中的技能键 | -| `resumeSession` | `bool` | Yes | `false` 时发 `thread/start`,`true` 时发 `turn/start` | +| `resumeSession` | `bool` | Yes | `false` 时发 `session.start`,`true` 时发 `session.message` | ## `GatewayAcpClient` diff --git a/docs/architecture/task-control-plane-unification.md b/docs/architecture/task-control-plane-unification.md index 32a6291f..bd5360d5 100644 --- a/docs/architecture/task-control-plane-unification.md +++ b/docs/architecture/task-control-plane-unification.md @@ -36,7 +36,7 @@ flowchart TD subgraph CONTRACT["Bridge-facing contract"] B1["acp.capabilities"] B2["xworkmate.routing.resolve"] - B3["thread/start / turn/start / session.cancel / session.close"] + B3["session.start / session.message / session.cancel / session.close"] B4["xworkmate.gateway.connect / request / disconnect"] end diff --git a/docs/testing/api-script-runbook.md b/docs/testing/api-script-runbook.md index a253647e..7fdae35f 100644 --- a/docs/testing/api-script-runbook.md +++ b/docs/testing/api-script-runbook.md @@ -73,14 +73,14 @@ make check-api-external - profile sync 元数据读取 - bridge capabilities 拉取 - routing resolve -- `thread/start` -- `turn/start` +- `session.start` +- `session.message` - `session.cancel` - `session.close` ## 5. 已知行为 -- `thread/start` / `turn/start` 在当前环境下可能返回下游连接失败,这不代表脚本失效 +- `session.start` / `session.message` 在当前环境下可能返回下游连接失败,这不代表脚本失效 - 只要脚本正确拿到 JSON-RPC 返回并解析出结果,就认为脚本可用 - 脚本不会把 `BRIDGE_SERVER_URL` 当成 runtime 真源;它只作为可选显式输入 diff --git a/docs/testing/app-external-service-api-test-matrix.md b/docs/testing/app-external-service-api-test-matrix.md index e628e1b5..5130a4db 100644 --- a/docs/testing/app-external-service-api-test-matrix.md +++ b/docs/testing/app-external-service-api-test-matrix.md @@ -6,7 +6,7 @@ Last Updated: 2026-04-22 - 账户服务 `accounts.svc.plus` - 桥接服务 `xworkmate-bridge.svc.plus` -- 桥接侧 JSON-RPC 会话接口 `thread/start` / `turn/start` / `session.cancel` / `session.close` +- 桥接侧 JSON-RPC 会话接口 `session.start` / `session.message` / `session.cancel` / `session.close` 本文目标不是抽象协议说明,而是把 APP 真实会调用的接口、请求体、返回体、鉴权方式、已验证结果和当前风险点整理成可执行测试清单。 @@ -20,8 +20,8 @@ Last Updated: 2026-04-22 - `acp.capabilities` - `xworkmate.routing.resolve` - 会话生命周期接口: - - `thread/start` - - `turn/start` + - `session.start` + - `session.message` - `session.cancel` - `session.close` @@ -79,8 +79,8 @@ Last Updated: 2026-04-22 | bridge | `/acp/rpc` | `POST` | `Authorization: Bearer ` | bridge JSON-RPC 主入口 | 所有运行时任务统一走这里 | | bridge | `acp.capabilities` | JSON-RPC method | 同上 | 拉取 provider catalog / target catalog | capability 只读快照 | | bridge | `xworkmate.routing.resolve` | JSON-RPC method | 同上 | 解析 provider / gateway / skills 路由 | 返回 resolved / unavailable 信息 | -| bridge | `thread/start` | JSON-RPC method | 同上 | 开启新会话 | session 生命周期起点 | -| bridge | `turn/start` | JSON-RPC method | 同上 | 继续现有会话 | 续写 / follow-up | +| bridge | `session.start` | JSON-RPC method | 同上 | 开启新会话 | session 生命周期起点 | +| bridge | `session.message` | JSON-RPC method | 同上 | 继续现有会话 | 续写 / follow-up | | bridge | `session.cancel` | JSON-RPC method | 同上 | 取消正在进行的会话 | 终止流式任务 | | bridge | `session.close` | JSON-RPC method | 同上 | 关闭会话 | 释放会话资源 | @@ -332,7 +332,7 @@ Last Updated: 2026-04-22 这些字段由 [`GoTaskServiceRequest.toExternalAcpParams()`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/runtime/go_task_service_client.dart) 生成。 -### 6.2 `thread/start` +### 6.2 `session.start` #### 目标 @@ -345,7 +345,7 @@ Last Updated: 2026-04-22 { "jsonrpc": "2.0", "id": "1", - "method": "thread/start", + "method": "session.start", "params": { "sessionId": "test-session-001", "threadId": "test-session-001", @@ -409,7 +409,7 @@ Last Updated: 2026-04-22 - 不能回退到本地 hardcoded endpoint 作为 APP runtime 真源 - 不能把 provider 路由走成 `/acp-server/*` 直连 -### 6.3 `turn/start` +### 6.3 `session.message` #### 目标 @@ -422,7 +422,7 @@ Last Updated: 2026-04-22 { "jsonrpc": "2.0", "id": "2", - "method": "turn/start", + "method": "session.message", "params": { "sessionId": "test-session-001", "threadId": "test-session-001", @@ -450,7 +450,7 @@ Last Updated: 2026-04-22 #### 返回体重点 -- 与 `thread/start` 相同的会话结果字段 +- 与 `session.start` 相同的会话结果字段 #### 当前实测结果 @@ -548,7 +548,7 @@ Last Updated: 2026-04-22 | [`lib/runtime/account_runtime_client.dart`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/runtime/account_runtime_client.dart) | 账户登录、会话、同步接口封装 | | [`lib/runtime/gateway_acp_client.dart`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/runtime/gateway_acp_client.dart) | bridge JSON-RPC 请求、capabilities、routing、session 生命周期 | | [`lib/runtime/go_task_service_client.dart`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/runtime/go_task_service_client.dart) | 会话请求参数组装 | -| [`lib/runtime/external_code_agent_acp_desktop_transport.dart`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/runtime/external_code_agent_acp_desktop_transport.dart) | thread/start / turn/start 触发路径 | +| [`lib/runtime/external_code_agent_acp_desktop_transport.dart`](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/lib/runtime/external_code_agent_acp_desktop_transport.dart) | session.start / session.message 触发路径 | ## 8. 自动化测试建议 @@ -562,20 +562,20 @@ Last Updated: 2026-04-22 #### 桥接协议层 - `test/runtime/gateway_acp_client_session_test.dart` - - 覆盖 `thread/start` - - 覆盖 `turn/start` + - 覆盖 `session.start` + - 覆盖 `session.message` - 覆盖 `session.cancel` - 覆盖 `session.close` - 断言 Bearer 头、JSON-RPC method、params 结构 #### 失败分支 -- `thread/start` 下游连接失败时,返回值中应保留: +- `session.start` 下游连接失败时,返回值中应保留: - `success: false` - `error` - `turnId` - `resolvedProviderId` -- `turn/start` follow-up 失败时,不应破坏会话标识 +- `session.message` follow-up 失败时,不应破坏会话标识 ### 8.3 建议断言 @@ -587,7 +587,7 @@ Last Updated: 2026-04-22 ## 9. 当前已知风险 -- `thread/start` / `turn/start` 当前环境下下游连接到 `127.0.0.1:9001` 失败 +- `session.start` / `session.message` 当前环境下下游连接到 `127.0.0.1:9001` 失败 - 这说明 bridge 主入口可用,但后端 provider 适配层在当前运行环境里未就绪 - 这是 bridge 后端运行态问题,不是 APP 侧 JSON-RPC 协议错误 @@ -598,8 +598,8 @@ Last Updated: 2026-04-22 3. `GET /api/auth/xworkmate/profile/sync` 4. `acp.capabilities` 5. `xworkmate.routing.resolve` -6. `thread/start` -7. `turn/start` +6. `session.start` +7. `session.message` 8. `session.cancel` 9. `session.close` @@ -610,6 +610,6 @@ Last Updated: 2026-04-22 - 账户侧负责登录与同步元数据 - bridge 侧负责 capability、路由解析和会话生命周期 - 任务执行必须统一经过 `/acp/rpc` -- `thread/start` / `turn/start` 的协议入口已验证通畅 +- `session.start` / `session.message` 的协议入口已验证通畅 - `session.cancel` / `session.close` 的协议入口已验证可用 - 当前剩余风险集中在桥接后端下游 provider 连接,而不是 APP 侧接口拼接 diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart index 78bec178..24c6bd85 100644 --- a/lib/runtime/external_code_agent_acp_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -105,7 +105,7 @@ class ExternalCodeAgentAcpDesktopTransport ); } final response = await _client.request( - method: request.resumeSession ? 'turn/start' : 'thread/start', + method: request.resumeSession ? 'session.message' : 'session.start', params: request.toExternalAcpParams(), endpointOverride: endpointOverride, onNotification: (notification) { diff --git a/lib/runtime/gateway_acp_client.dart b/lib/runtime/gateway_acp_client.dart index 7d3407c1..abae9f6c 100644 --- a/lib/runtime/gateway_acp_client.dart +++ b/lib/runtime/gateway_acp_client.dart @@ -145,7 +145,8 @@ class GatewayAcpClient { singleAgent: singleAgent, multiAgent: multiAgent, availableExecutionTargets: _parseAvailableExecutionTargets( - result['availableExecutionTargets'] ?? caps['availableExecutionTargets'], + result['availableExecutionTargets'] ?? + caps['availableExecutionTargets'], singleAgent: singleAgent, gatewayProviderCatalog: gatewayProviderCatalog, ), @@ -266,7 +267,7 @@ class GatewayAcpClient { } final rpcRequest = _GatewayAcpRpcRequest( id: _nextRequestId('multi-agent'), - method: request.resumeSession ? 'turn/start' : 'thread/start', + method: request.resumeSession ? 'session.message' : 'session.start', params: { 'sessionId': request.sessionId, 'threadId': request.threadId, diff --git a/scripts/ci/verify_api_scenario_contract.sh b/scripts/ci/verify_api_scenario_contract.sh index 6edad7bf..b305e182 100755 --- a/scripts/ci/verify_api_scenario_contract.sh +++ b/scripts/ci/verify_api_scenario_contract.sh @@ -136,7 +136,7 @@ capabilities_json="$( -H "Authorization: Bearer ${bridge_auth_token}" )" -start_payload='{"jsonrpc":"2.0","id":"start","method":"thread/start","params":{"sessionId":"scenario-session-001","threadId":"scenario-session-001","mode":"gateway-chat","taskPrompt":"Say hello in one short sentence.","workingDirectory":"/tmp","selectedSkills":[],"attachments":[],"provider":"codex","routing":{"routingMode":"auto","preferredGatewayTarget":"codex","explicitExecutionTarget":"agent","explicitProviderId":"codex","explicitModel":"","explicitSkills":[],"allowSkillInstall":false,"availableSkills":[]},"requestedExecutionTarget":"agent","executionTarget":"agent"}}' +start_payload='{"jsonrpc":"2.0","id":"start","method":"session.start","params":{"sessionId":"scenario-session-001","threadId":"scenario-session-001","mode":"gateway-chat","taskPrompt":"Say hello in one short sentence.","workingDirectory":"/tmp","selectedSkills":[],"attachments":[],"provider":"codex","routing":{"routingMode":"auto","preferredGatewayTarget":"codex","explicitExecutionTarget":"agent","explicitProviderId":"codex","explicitModel":"","explicitSkills":[],"allowSkillInstall":false,"availableSkills":[]},"requestedExecutionTarget":"agent","executionTarget":"agent"}}' start_json="$( json_post \ "${bridge_server_url}/acp/rpc" \ @@ -153,12 +153,12 @@ payload = json.loads(os.environ["RESPONSE_JSON"]) result = payload.get("result") or payload.get("payload") or {} turn_id = str(result.get("turnId") or "").strip() if not turn_id: - raise SystemExit("thread/start did not return turnId") + raise SystemExit("session.start did not return turnId") print(turn_id) PY )" -message_payload='{"jsonrpc":"2.0","id":"message","method":"turn/start","params":{"sessionId":"scenario-session-001","threadId":"scenario-session-001","mode":"gateway-chat","taskPrompt":"Continue with a very short acknowledgement.","workingDirectory":"/tmp","selectedSkills":[],"attachments":[],"provider":"codex","routing":{"routingMode":"auto","preferredGatewayTarget":"codex","explicitExecutionTarget":"agent","explicitProviderId":"codex","explicitModel":"","explicitSkills":[],"allowSkillInstall":false,"availableSkills":[]},"requestedExecutionTarget":"agent","executionTarget":"agent"}}' +message_payload='{"jsonrpc":"2.0","id":"message","method":"session.message","params":{"sessionId":"scenario-session-001","threadId":"scenario-session-001","mode":"gateway-chat","taskPrompt":"Continue with a very short acknowledgement.","workingDirectory":"/tmp","selectedSkills":[],"attachments":[],"provider":"codex","routing":{"routingMode":"auto","preferredGatewayTarget":"codex","explicitExecutionTarget":"agent","explicitProviderId":"codex","explicitModel":"","explicitSkills":[],"allowSkillInstall":false,"availableSkills":[]},"requestedExecutionTarget":"agent","executionTarget":"agent"}}' message_json="$( json_post \ "${bridge_server_url}/acp/rpc" \ @@ -207,9 +207,9 @@ import os payload = json.loads(os.environ["RESPONSE_JSON"]) result = payload.get("result") or payload.get("payload") or {} if result.get("resolvedProviderId") != "codex": - raise SystemExit("thread/start did not resolve codex") + raise SystemExit("session.start did not resolve codex") if not str(result.get("error") or "").strip(): - raise SystemExit("thread/start in this environment should expose downstream error details") + raise SystemExit("session.start in this environment should expose downstream error details") PY RESPONSE_JSON="${message_json}" python3 - <<'PY' @@ -219,9 +219,9 @@ import os payload = json.loads(os.environ["RESPONSE_JSON"]) result = payload.get("result") or payload.get("payload") or {} if result.get("turnId") == "": - raise SystemExit("turn/start did not return turnId") + raise SystemExit("session.message did not return turnId") if str(result.get("turnId") or "").strip() == "": - raise SystemExit("turn/start did not return turnId") + raise SystemExit("session.message did not return turnId") PY RESPONSE_JSON="${cancel_json}" python3 - <<'PY' diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 6fc43624..6ca665da 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -347,11 +347,13 @@ void main() { capture.requestBody, contains('"requestedExecutionTarget":"gateway"'), ); + expect(capture.requestBody, contains('"method":"session.start"')); + expect(capture.requestBody, isNot(contains('"method":"thread/start"'))); }, ); test( - 'desktop task execution uses thread/start instead of legacy session.start', + 'desktop task execution uses session.start for new sessions', () async { final capture = await _startAcpHttpServer(); addTearDown(capture.close); @@ -374,16 +376,129 @@ void main() { onUpdate: (_) {}, ); - expect(capture.requestBody, contains('"method":"thread/start"')); - expect(capture.requestBody, isNot(contains('"method":"session.start"'))); + expect(capture.requestBody, contains('"method":"session.start"')); + expect(capture.requestBody, isNot(contains('"method":"thread/start"'))); }, ); + + test('desktop follow-up execution uses session.message', () async { + final capture = await _startAcpHttpServer(); + addTearDown(capture.close); + final client = GatewayAcpClient( + endpointResolver: () => capture.baseEndpoint, + authorizationResolver: (_) async => 'bridge-token', + ); + + final transport = ExternalCodeAgentAcpDesktopTransport( + client: client, + endpointResolver: (_) => capture.baseEndpoint, + taskEndpointResolver: (_) => capture.baseEndpoint, + ); + + await transport.executeTask( + _taskRequest( + target: AssistantExecutionTarget.agent, + provider: SingleAgentProvider.codex, + resumeSession: true, + ), + onUpdate: (_) {}, + ); + + expect(capture.requestBody, contains('"method":"session.message"')); + expect(capture.requestBody, isNot(contains('"method":"turn/start"'))); + }); + + 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: [], + selectedSkills: [], + resumeSession: false, + ), + ) + .toList(); + + expect(events, isNotEmpty); + expect( + capture.requestBodies, + contains( + predicate((body) { + return body.contains('"method":"session.start"'); + }), + ), + ); + expect( + capture.requestBodies, + isNot( + contains( + predicate((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: [], + selectedSkills: [], + resumeSession: true, + ), + ) + .toList(); + + expect( + capture.requestBodies, + contains( + predicate((body) { + return body.contains('"method":"session.message"'); + }), + ), + ); + expect( + capture.requestBodies, + isNot( + contains( + predicate((body) { + return body.contains('"method":"turn/start"'); + }), + ), + ), + ); + }); }); } GoTaskServiceRequest _taskRequest({ required AssistantExecutionTarget target, required SingleAgentProvider provider, + bool resumeSession = false, }) { return GoTaskServiceRequest( sessionId: 'session-1', @@ -399,6 +514,7 @@ GoTaskServiceRequest _taskRequest({ agentId: '', metadata: const {}, provider: provider, + resumeSession: resumeSession, ); } @@ -414,6 +530,7 @@ Future<_CapturedAcpHttpServer> _startAcpHttpServer() async { capture.requestPath = request.uri.path; final body = await utf8.decoder.bind(request).join(); capture.requestBody = body; + capture.requestBodies.add(body); final id = _decodeRequestId(body); request.response.headers.contentType = ContentType.json; request.response.write( @@ -444,6 +561,7 @@ class _CapturedAcpHttpServer { String authorizationHeader = ''; String requestPath = ''; String requestBody = ''; + final List requestBodies = []; Future close() => _server.close(force: true); }