diff --git a/docs/architecture/assistant-thread-information-architecture.md b/docs/architecture/assistant-thread-information-architecture.md index c5213b79..42148f2b 100644 --- a/docs/architecture/assistant-thread-information-architecture.md +++ b/docs/architecture/assistant-thread-information-architecture.md @@ -1,6 +1,6 @@ # Assistant TaskThread 信息架构 -本文描述当前 XWorkmate 中,线程信息如何围绕 `TaskThread` 进入 UI、进入 controller / runtime 的执行请求构造、再通过 `Go Agent-core` 回写到 UI。 +本文描述当前 XWorkmate 中,线程信息如何围绕 `TaskThread` 进入 UI、进入 controller / runtime 的执行请求构造、再通过 `GoTaskService` 回写到 UI。 本文统一采用 `TaskThread` 聚合对象作为线程信息架构主语义。 @@ -57,7 +57,7 @@ TaskThread - 当前线程执行模式 - provider / endpoint 绑定 -- 为 agent-core / runtime 协调层提供调度输入 +- 为 `GoTaskService / runtime` 协调层提供调度输入 ### 2.4 contextState @@ -98,7 +98,7 @@ flowchart LR D3 --> E D4 --> E - E --> F["Go Agent-core\nDesktop: local bridge\nWeb: remote ACP / RPC"] + E --> F["GoTaskService\nDesktop: GatewayRuntime / ExternalCodeAgentAcpDesktopTransport\nWeb: relay / ExternalCodeAgentAcpWebTransport"] F --> G["执行结果"] G --> H["回写线程上下文\n(主体区域 同步显示)"] @@ -111,7 +111,7 @@ flowchart LR 这张图表达的是当前线程信息架构,而不是旧的“工作目录 fallback 流程”: - `读取 TaskThread` 是 UI 与执行层共享的唯一线程信息入口 -- `构造执行请求` 在 agent-core / runtime 协调层完成 +- `构造执行请求` 在 `GoTaskService / runtime` 协调层完成 - `右栏显示` 明确依赖 `TaskThread` 当前记录 - `workspaceBinding` 更新只允许发生在当前线程已完整的前提下 - prompt 中的 `workspace_root` side-channel 已退出主链;workspace 更新只允许来自 create/load 显式绑定或结构化执行结果回写 @@ -123,9 +123,9 @@ flowchart LR | 当前线程身份 | `threadId` | UI 按 `threadId` 选中线程,再读取完整 `TaskThread` | | owner 信息 | `ownerScope` | 线程归属、owner 展示与 remote owner path 推导 | | 工作空间路径展示 | `workspaceBinding.displayPath` | 右栏当前路径展示 | -| 执行工作空间 | `workspaceBinding.workspacePath` | agent-core / runtime 构造执行请求时使用 | +| 执行工作空间 | `workspaceBinding.workspacePath` | `GoTaskService / runtime` 构造执行请求时使用 | | 工作空间类型 | `workspaceBinding.workspaceKind` | 区分 `localFs / remoteFs` | -| 执行模式 | `executionBinding.executionMode` | 映射 Go Agent-core 调度输入与 transport 选择 | +| 执行模式 | `executionBinding.executionMode` | 映射 `GoTaskService` 调度输入与 transport 选择 | | provider / endpoint | `executionBinding.providerId / endpointId` | 当前执行通道来源 | | 消息历史 | `contextState.messages` | 主体区域消息列表 | | 模型 | `contextState.selectedModelId` | 当前线程模型选择 | @@ -143,7 +143,7 @@ flowchart LR - UI 仍保持现有结构与呈现方式 - UI 不负责执行请求构造 -- controller / runtime 负责根据 `TaskThread` 构造请求并调用 `Go Agent-core` +- controller / runtime 负责根据 `TaskThread` 构造请求并调用 `GoTaskService` - 执行结果先回写线程上下文,主体区域同步显示 - 右栏显示与预览结果来自当前 `TaskThread` 最新记录 - Desktop / Web 共用同一套 session 语义,只保留 local bridge / remote ACP-RPC transport 差异 diff --git a/docs/architecture/assistant-thread-target-model-20260328.md b/docs/architecture/assistant-thread-target-model-20260328.md index 3658c360..71310274 100644 --- a/docs/architecture/assistant-thread-target-model-20260328.md +++ b/docs/architecture/assistant-thread-target-model-20260328.md @@ -11,8 +11,9 @@ 3. UI 选中线程后,系统必须读取完整 `TaskThread`,而不是从页面状态拼装线程信息。 4. `TaskThread` 持久化 schema 保持不变,但 `workspaceBinding` 在 create/load 时必须完整;缺失 binding 的旧记录按非法数据处理并跳过加载。 5. 执行请求由 controller / runtime 根据 `ownerScope / workspaceBinding / executionBinding / contextState` 构造。 -6. controller / runtime 统一通过 `Go Agent-core` 调度执行:Desktop 走 App 内 local bridge,Web 走远端 ACP / RPC endpoint。 +6. controller / runtime 统一通过 `GoTaskService` 调度执行:OpenClaw task 走 `TaskThread -> GoTaskService -> GatewayRuntime / Web relay -> OpenClaw gateway`;`singleAgent / multiAgent` 走 `TaskThread -> GoTaskService -> ExternalCodeAgentAcp* -> ACP/provider route`。 7. 执行结果先回写 `TaskThread.contextState`,主体区域同步显示;UI 与执行始终只读取当前 `TaskThread.workspaceBinding`,不再存在 runtime first-binding 或 fallback 到 `main`。 +8. `contextState` 是线程上下文真相源;`lifecycleState` 只表达生命周期摘要;controller 侧缓存不承载线程持久语义。 ## 2. TaskThread 结构 @@ -77,7 +78,7 @@ ExecutionBinding - 定义线程当前执行模式 - 定义 provider / endpoint 绑定 -- 为 agent-core / runtime 协调层提供调度输入 +- 为 `GoTaskService / runtime` 协调层提供调度输入 ### 2.4 contextState @@ -132,7 +133,7 @@ flowchart LR D3 --> E D4 --> E - E --> F["Go Agent-core\nDesktop: local bridge\nWeb: remote ACP / RPC"] + E --> F["GoTaskService\nDesktop: GatewayRuntime / ExternalCodeAgentAcpDesktopTransport\nWeb: relay / ExternalCodeAgentAcpWebTransport"] F --> G["执行结果"] G --> H["回写线程上下文\n(主体区域 同步显示)"] @@ -146,8 +147,8 @@ flowchart LR 1. UI 仍保持现有形态,但只负责选择 `threadId` 与消费回写结果。 2. 线程的执行输入来自完整 `TaskThread`。 -3. `构造执行请求` 属于 agent-core / runtime 协调层,不属于 UI。 -4. `Go Agent-core` 是唯一执行调度面;Desktop / Web 共用同一套 session 语义,只在 transport 上有差异。 +3. `构造执行请求` 属于 `GoTaskService / runtime` 协调层,不属于 UI。 +4. `GoTaskService` 是唯一执行调度面;Desktop / Web 共用同一套 session 语义,只在 transport 上有差异。 5. `回写线程上下文` 是执行结束后的第一落点;主体区域同步显示依赖这一回写。 6. `workspaceBinding` 不是运行时补齐对象;线程在 create/load 时必须已经完整。 7. `右栏显示` 与执行请求都读取当前 `TaskThread.workspaceBinding`,因此它与主体区域共享同一线程事实来源。 @@ -161,10 +162,10 @@ flowchart LR - UI 不是工作空间推断器。 - UI 不是线程状态的独立真相源。 -### 4.2 agent-core / runtime 协调层约束 +### 4.2 GoTaskService / runtime 协调层约束 - 根据 `ownerScope / workspaceBinding / executionBinding / contextState` 构造执行请求。 -- 负责把线程请求调度到 `Go Agent-core`,而不是让 Flutter UI 直接承担 runtime 职责。 +- 负责把线程请求调度到 `GoTaskService`,而不是让 Flutter UI 直接承担 runtime 职责。 - 接收执行结果并驱动 `TaskThread` 回写。 ### 4.3 TaskThread 约束 @@ -179,7 +180,7 @@ flowchart LR - [task-thread-session-key-isolation-20260329.md](task-thread-session-key-isolation-20260329.md) 补充“任务线必须先成为真实 `TaskThread/sessionKey`”的隔离约束,说明为什么 single-agent 的工作目录只能围绕当前线程身份解析。 - [assistant-thread-information-architecture.md](assistant-thread-information-architecture.md) - 说明线程信息如何进入 UI、agent-core / runtime 请求构造、结果回写和右栏展示。 + 说明线程信息如何进入 UI、`GoTaskService / runtime` 请求构造、结果回写和右栏展示。 - [xworkmate-internal-state-architecture.md](xworkmate-internal-state-architecture.md) 说明控制器、状态存储和派生 UI 状态如何围绕 `TaskThread` 组织。 diff --git a/docs/architecture/task-thread-session-key-isolation-20260329.md b/docs/architecture/task-thread-session-key-isolation-20260329.md index 20002253..a3adfb61 100644 --- a/docs/architecture/task-thread-session-key-isolation-20260329.md +++ b/docs/architecture/task-thread-session-key-isolation-20260329.md @@ -21,7 +21,7 @@ currentSessionKey -> normalizedAssistantSessionKeyInternal(sessionKey) -> assistantWorkspacePathForSession(sessionKey) -> resolveSingleAgentWorkingDirectoryForSessionInternal(sessionKey) --> GoAgentCoreSessionRequest.workingDirectory +-> GoTaskServiceRequest.workingDirectory ``` 这条链路说明: @@ -201,7 +201,7 @@ flowchart LR J --> L["executionBinding"] J --> M["contextState"] - K --> N["GoAgentCoreSessionRequest.workingDirectory"] + K --> N["GoTaskServiceRequest.workingDirectory"] L --> O["provider / execution mode"] M --> P["messages / model / skills"] diff --git a/docs/architecture/xworkmate-internal-state-architecture.md b/docs/architecture/xworkmate-internal-state-architecture.md index 5a13b6ae..c68bc0cb 100644 --- a/docs/architecture/xworkmate-internal-state-architecture.md +++ b/docs/architecture/xworkmate-internal-state-architecture.md @@ -8,11 +8,11 @@ Last Updated: 2026-03-29 - Settings 中心配置状态 - 当前 `TaskThread` 状态 -- agent-core / runtime 协调状态 +- `GoTaskService / runtime` 协调状态 - 派生 UI 状态 - 技能、模型、执行通道与会话内容 -本文以 Desktop 为主说明,因为 Desktop 控制器拥有最完整的运行时与持久化路径;Web 保持同一 `TaskThread` 与 session 语义,但 transport 走远端 ACP / RPC。 +本文以 Desktop 为主说明,因为 Desktop 控制器拥有最完整的运行时与持久化路径;Web 保持同一 `TaskThread` 与 session 语义,但 transport 走远端 ACP / relay。 ## 1. Core Rule @@ -58,10 +58,10 @@ graph TB webCurrentThreadId["_currentThreadId"] end - subgraph R["Agent-Core / Runtime Coordination"] + subgraph R["GoTaskService / Runtime Coordination"] threadReader["read TaskThread by threadId"] requestBuilder["build execution request"] - dispatcher["dispatch to Go Agent-core\nDesktop: local bridge\nWeb: remote ACP / RPC"] + dispatcher["dispatch to GoTaskService\nDesktop: GatewayRuntime / ExternalCodeAgentAcpDesktopTransport\nWeb: relay / ExternalCodeAgentAcpWebTransport"] resultWriter["write result back to TaskThread"] end @@ -104,7 +104,7 @@ graph TB - `TaskThread` 是线程主状态,不再由散落 session 字段共同充当 - `threadId` 是读取线程状态的唯一入口键 -- `build execution request` 属于 agent-core / runtime 协调层 +- `build execution request` 属于 `GoTaskService / runtime` 协调层 - UI 只消费当前 `TaskThread` 与派生状态 ## 3. State Ownership @@ -184,13 +184,13 @@ Ownership summary: - `TaskThread` 在 create/load 时必须已经拥有完整 `workspaceBinding` - 缺少 `workspaceBinding` 的旧记录属于非法线程数据,应在恢复阶段跳过并通过启动告警暴露 -### 3.3 Agent-Core / Runtime 协调状态 +### 3.3 GoTaskService / Runtime 协调状态 Primary responsibilities: - 根据 `threadId` 读取完整 `TaskThread` - 基于 `ownerScope / workspaceBinding / executionBinding / contextState` 构造执行请求 -- 调度到 `Go Agent-core` +- 调度到 `GoTaskService` - 接收执行结果并回写 `TaskThread` 重要规则: @@ -200,6 +200,7 @@ Primary responsibilities: - 工作空间选择不再通过旧式运行前猜测获得 - 不允许 runtime fallback 到 `main`、`Directory.current` 或 prompt first-binding - 结果回写先更新线程上下文,再驱动主体区域与右栏刷新 +- controller 侧 runtime cache 只允许承载瞬时 streaming / pending / preview 状态,不承载线程长期语义 - Desktop / Web 共用相同 session 生命周期;不再单独发明 relay-only 执行协议 ### 3.4 Derived UI State @@ -244,7 +245,7 @@ Examples: 3. `executionBinding` 4. `contextState` -然后由 agent-core / runtime 协调层构造执行请求并调度运行。 +然后由 `GoTaskService / runtime` 协调层构造执行请求并调度运行。 ### 4.3 结果回写优先级 @@ -270,7 +271,7 @@ flowchart LR D3 --> E D4 --> E - E --> F["Go Agent-core\nDesktop: local bridge\nWeb: remote ACP / RPC"] + E --> F["GoTaskService\nDesktop: GatewayRuntime / ExternalCodeAgentAcpDesktopTransport\nWeb: relay / ExternalCodeAgentAcpWebTransport"] F --> G["执行结果"] G --> H["回写线程上下文\n(主体区域 同步显示)"] diff --git a/docs/architecture/xworkmate-layered-architecture.md b/docs/architecture/xworkmate-layered-architecture.md index 462094b8..f6d7cfe3 100644 --- a/docs/architecture/xworkmate-layered-architecture.md +++ b/docs/architecture/xworkmate-layered-architecture.md @@ -9,7 +9,7 @@ Last Updated: 2026-03-29 - 本地用户、Web 用户、远程租户如何进入系统 - 任务线程如何成为 UI 与执行之间的控制面主对象 -- Desktop / Mobile / Web 三个界面层如何共用同一套 agent core +- Desktop / Mobile / Web 三个界面层如何共用同一套 `GoTaskService` 执行主链 - 本地 agent、OpenClaw Gateway、ACP endpoint、AI Gateway、Skills / MCP 等扩展能力应该落在哪一层 @@ -21,7 +21,7 @@ Last Updated: 2026-03-29 ## 为什么要重新规划 -如果只用“用户 -> UI -> Agent-core -> 服务”四层表达,当前项目里最关键的 +如果只用“用户 -> UI -> GoTaskService -> 服务”四层表达,当前项目里最关键的 一个事实会被隐藏掉: **XWorkmate 的运行主对象不是某个页面,也不是某个 gateway session,而是 @@ -40,7 +40,7 @@ Last Updated: 2026-03-29 1. 访问与归属层 2. 多端 UI 层 3. 线程控制面 -4. Agent-core 调度层 +4. `GoTaskService` 调度层 5. 对接服务与扩展层 6. 安全与持久化基座(横切,不单独作为主业务层) @@ -48,8 +48,8 @@ Last Updated: 2026-03-29 - UI 不是执行状态真值源 - `TaskThread` 才是线程级控制面真值源 -- Agent-core 负责把线程状态翻译成可执行请求 -- 真正的 provider / gateway / ACP / Skills / MCP 都应放在 Agent-core 之下 +- `GoTaskService` 负责把线程状态翻译成可执行请求 +- 真正的 provider / gateway / ACP / Skills / MCP 都应放在 `GoTaskService` 之下 ## 整体架构 @@ -77,16 +77,16 @@ flowchart TB C7["线程持久化
SettingsStore / WebSessionRepository"] end - subgraph L4["④ Agent-core 调度层"] + subgraph L4["④ GoTaskService 调度层"] D1["AppControllerDesktop / AppControllerWeb"] - D2["GoAgentCoreClient 抽象"] + D2["GoTaskService / GoTaskServiceClient"] D3["RuntimeCoordinator / GatewayRuntime / ModeSwitcher"] D4["CodeAgentNodeOrchestrator"] D5["SingleAgentRunner / DirectSingleAgentAppServerClient / CodexRuntime"] D6["MultiAgentOrchestrator / MultiAgentMountManager"] D7["CodexConfigBridge / OpencodeConfigBridge"] - D8["Desktop transport
GoAgentCoreDesktopTransport / go_core / codex_ffi_bindings"] - D9["Web transport
GoAgentCoreWebTransport / WebAcpClient / Relay"] + D8["Desktop ACP transport
ExternalCodeAgentAcpDesktopTransport / go_core / codex_ffi_bindings"] + D9["Web ACP transport
ExternalCodeAgentAcpWebTransport / WebAcpClient / Relay"] end subgraph L5["⑤ 对接服务与扩展层"] @@ -215,9 +215,9 @@ flowchart TB - `RuntimeCoordinator` - `GatewayRuntime` - `CodeAgentNodeOrchestrator` -- `GoAgentCoreClient` -- `GoAgentCoreDesktopTransport` -- `GoAgentCoreWebTransport` +- `GoTaskServiceClient` +- `ExternalCodeAgentAcpDesktopTransport` +- `ExternalCodeAgentAcpWebTransport` - `SingleAgentRunner` - `MultiAgentOrchestrator` - `MultiAgentMountManager` @@ -227,7 +227,7 @@ flowchart TB 重规划后的职责边界应当是: - `AppController*` 负责从 `TaskThread` 解析出当前线程的执行上下文 -- `GoAgentCoreClient` 负责统一 Desktop / Web 的 agent-core 会话调用抽象 +- `GoTaskServiceClient` 负责统一 Desktop / Web 的执行请求与结果映射抽象 - `RuntimeCoordinator` / `GatewayRuntime` 负责 runtime 与 gateway 连接能力 - `CodeAgentNodeOrchestrator` 负责 app-mediated cooperative node metadata - `MultiAgentOrchestrator` / `MultiAgentMountManager` 负责协作执行与挂载 @@ -289,17 +289,15 @@ flowchart LR C --> C3["executionBinding"] C --> C4["contextState"] - C1 --> D["构造 GoAgentCoreSessionRequest
或 Gateway 执行请求"] + C1 --> D["构造 GoTaskServiceRequest
或 Gateway 执行请求"] C2 --> D C3 --> D C4 --> D D --> E{"executionMode"} - E -->|localAgent| F["GoAgentCoreDesktopTransport / SingleAgentRunner"] - E -->|gatewayLocal| G["GatewayRuntime / OpenClaw local"] - E -->|gatewayRemote| H["GatewayAcpClient / WebAcpClient / Remote ACP"] + E -->|openclaw task| G["GoTaskService -> GatewayRuntime / Web relay"] + E -->|singleAgent / multiAgent| H["GoTaskService -> ExternalCodeAgentAcp* / ACP route"] - F --> I["执行结果 / delta / resolvedWorkingDirectory"] G --> I H --> I @@ -327,17 +325,17 @@ flowchart LR | 访问与归属层 | `ThreadOwnerScope`、`DeviceIdentityStore`、Web session identity | `lib/runtime/runtime_models_runtime_payloads.dart`, `lib/runtime/device_identity_store.dart`, `lib/web/web_session_repository.dart` | 定义线程归属、设备身份、远程会话身份 | | 多端 UI 层 | `AppShellDesktop`、`mobile_shell_*`、`AppShellWeb`、`AssistantPage`、`SettingsPage` | `lib/app/`, `lib/features/assistant/`, `lib/features/mobile/`, `lib/features/settings/` | 接收用户操作、展示线程与设置 | | 线程控制面 | `TaskThread` + thread records | `lib/runtime/runtime_models_runtime_payloads.dart`, `lib/runtime/settings_store.dart`, `lib/web/web_session_repository.dart` | 保存线程级真值状态 | -| Agent-core 调度层 | `AppControllerDesktop/Web`、`GoAgentCoreClient`、`RuntimeCoordinator`、`CodeAgentNodeOrchestrator`、`MultiAgentOrchestrator` | `lib/app/`, `lib/runtime/`, `lib/web/` | 把线程状态翻译为执行请求并协调 transport | -| 对接服务与扩展层 | local agent、OpenClaw Gateway、ACP endpoint、AI Gateway、Skills / MCP / adapters | `lib/runtime/go_agent_core_desktop_transport.dart`, `lib/web/go_agent_core_web_transport.dart`, `lib/runtime/multi_agent_mounts.dart` | 真实执行与扩展接入 | +| `GoTaskService` 调度层 | `AppControllerDesktop/Web`、`GoTaskServiceClient`、`RuntimeCoordinator`、`CodeAgentNodeOrchestrator`、`MultiAgentOrchestrator` | `lib/app/`, `lib/runtime/`, `lib/web/` | 把线程状态翻译为执行请求并协调 transport | +| 对接服务与扩展层 | local agent、OpenClaw Gateway、ACP endpoint、AI Gateway、Skills / MCP / adapters | `lib/runtime/external_code_agent_acp_desktop_transport.dart`, `lib/web/external_code_agent_acp_web_transport.dart`, `lib/runtime/multi_agent_mounts.dart` | 真实执行与扩展接入 | | 安全与持久化基座 | `SettingsStore`、`SecretStore`、`SecureConfigStore`、`WebStore` | `lib/runtime/`, `lib/web/web_store.dart` | 提供持久化与 secret 保护 | ## 三端职责矩阵 -| 平台 | UI 入口 | 线程控制面 | agent-core 重点 | 当前执行特点 | +| 平台 | UI 入口 | 线程控制面 | `GoTaskService` 重点 | 当前执行特点 | | --- | --- | --- | --- | --- | | Desktop | `AppShellDesktop` + workspace 页面 | `TaskThread` 持久化最完整 | `AppControllerDesktop` + `RuntimeCoordinator` + Desktop transport | 支持本地 single-agent、gateway local、gateway remote | | Mobile | `mobile_shell_*` | 复用同一线程模型 | 仍走 native host/controller 体系 | 当前以 remote gateway 场景为主 | -| Web | `AppShellWeb` | 同 schema 的 thread records | `AppControllerWeb` + `GoAgentCoreWebTransport` + relay/acp client | 远程 ACP / relay / AI Gateway 路径 | +| Web | `AppShellWeb` | 同 schema 的 thread records | `AppControllerWeb` + `ExternalCodeAgentAcpWebTransport` + relay/acp client | 远程 ACP / relay / AI Gateway 路径 | ## 对你给出的旧图,按代码需要做的三个修正 @@ -358,14 +356,14 @@ flowchart LR 从代码现实出发,当前更准确的 seam 是: -- `GoAgentCoreClient` -- `GoAgentCoreDesktopTransport` -- `GoAgentCoreWebTransport` +- `GoTaskServiceClient` +- `ExternalCodeAgentAcpDesktopTransport` +- `ExternalCodeAgentAcpWebTransport` - `GatewayAcpClient` - `WebAcpClient` - `MultiAgentOrchestrator` -因此,新的整体架构里应把“broker / ACP / transport”归到 Agent-core 调度层内部, +因此,新的整体架构里应把“broker / ACP / transport”归到 `GoTaskService` 调度层内部, 而不是单独挂成一个与 UI 并列的主系统。 ### 修正 3:`Assistant composer / Settings / Feature flags` 属于 UI 层,不属于运行时层 @@ -395,7 +393,7 @@ flowchart LR 如果未来新增新的执行目标或新的 gateway 类型: - 先扩展 `executionBinding` -- 再扩展 `GoAgentCoreClient` transport 或 runtime coordinator +- 再扩展 `GoTaskServiceClient` transport 或 runtime coordinator - 不要先改页面分支逻辑 ### 新 provider / adapter / MCP / skill capability @@ -425,7 +423,7 @@ flowchart LR - 一个以 `TaskThread` 为控制面核心的多端 agent workspace App - UI 负责交互 - 线程控制面负责真值 -- Agent-core 负责调度与 transport +- `GoTaskService` 负责调度与 transport - Gateway / ACP / local agent / Skills / MCP 负责实际执行与扩展 一句话概括: diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index e8407da7..1dce5bab 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -29,7 +29,7 @@ import '../runtime/codex_config_bridge.dart'; import '../runtime/code_agent_node_orchestrator.dart'; import '../runtime/assistant_artifacts.dart'; import '../runtime/desktop_thread_artifact_service.dart'; -import '../runtime/go_agent_core_desktop_transport.dart'; +import '../runtime/external_code_agent_acp_desktop_transport.dart'; import '../runtime/go_task_service_client.dart'; import '../runtime/go_task_service_desktop_service.dart'; import '../runtime/go_gateway_runtime_desktop_client.dart'; @@ -221,7 +221,7 @@ class AppController extends ChangeNotifier { gateway: runtimeCoordinatorInternal.gateway, acpTransport: ExternalCodeAgentAcpDesktopTransport( acpClient: gatewayAcpClientInternal, - endpointResolver: resolveGoAgentCoreEndpointForTargetInternal, + endpointResolver: resolveExternalAcpEndpointForTargetInternal, goCoreLocator: goCoreLocatorInternal, ), ); @@ -318,11 +318,6 @@ class AppController extends ChangeNotifier { >{}; final Map aiGatewayStreamingTextBySessionInternal = {}; - final Map singleAgentRuntimeModelBySessionInternal = - {}; - final Map> - latestRoutingResolutionBySessionInternal = - >{}; final Map syncedGoAgentProvidersInternal = {}; final DesktopThreadArtifactService threadArtifactServiceInternal = diff --git a/lib/app/app_controller_desktop_go_agent_core_routing.dart b/lib/app/app_controller_desktop_external_acp_routing.dart similarity index 73% rename from lib/app/app_controller_desktop_go_agent_core_routing.dart rename to lib/app/app_controller_desktop_external_acp_routing.dart index 068a278e..99d03c14 100644 --- a/lib/app/app_controller_desktop_go_agent_core_routing.dart +++ b/lib/app/app_controller_desktop_external_acp_routing.dart @@ -38,7 +38,7 @@ import '../runtime/skill_directory_access.dart'; import 'app_controller_desktop_core.dart'; import 'app_controller_desktop_thread_sessions.dart'; -extension AppControllerDesktopGoAgentCoreRouting on AppController { +extension AppControllerDesktopExternalAcpRouting on AppController { Future> buildExternalAcpSyncedProvidersInternal() async { final providers = []; @@ -75,27 +75,4 @@ extension AppControllerDesktopGoAgentCoreRouting on AppController { ); await goTaskServiceClientInternal.syncExternalProviders(providers); } - - void updateLatestRoutingResolutionInternal( - String sessionKey, - GoTaskServiceResult result, - ) { - final normalizedSessionKey = normalizedAssistantSessionKeyInternal( - sessionKey, - ); - latestRoutingResolutionBySessionInternal[normalizedSessionKey] = - { - 'resolvedExecutionTarget': result.resolvedExecutionTarget, - 'resolvedEndpointTarget': result.resolvedEndpointTarget, - 'resolvedProviderId': result.resolvedProviderId, - 'resolvedModel': result.resolvedModel.trim(), - 'resolvedSkills': result.resolvedSkills, - 'skillResolutionSource': result.skillResolutionSource, - 'skillCandidates': result.skillCandidates, - 'needsSkillInstall': result.needsSkillInstall, - 'skillInstallRequestId': result.skillInstallRequestId, - 'memorySources': result.memorySources, - 'updatedAtMs': DateTime.now().millisecondsSinceEpoch, - }; - } } diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart index f19e4c95..6c587b1d 100644 --- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart +++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart @@ -45,7 +45,7 @@ import 'app_controller_desktop_workspace_execution.dart'; import 'app_controller_desktop_settings_runtime.dart'; import 'app_controller_desktop_thread_storage.dart'; import 'app_controller_desktop_skill_permissions.dart'; -import 'app_controller_desktop_go_agent_core_routing.dart'; +import 'app_controller_desktop_external_acp_routing.dart'; import 'app_controller_desktop_runtime_helpers.dart'; Future refreshAcpCapabilitiesRuntimeInternal( @@ -101,7 +101,7 @@ Future refreshSingleAgentCapabilitiesRuntimeInternal( next[provider] = DirectSingleAgentCapabilities( available: true, supportedProviders: [provider], - endpoint: 'go-agent-core', + endpoint: 'go-task-service', ); } controller.singleAgentCapabilitiesByProviderInternal = next; diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 710cad6e..84dc10ae 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -669,7 +669,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { ); } - Uri? resolveGoAgentCoreEndpointForTargetInternal( + Uri? resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget target, ) { if (target == AssistantExecutionTarget.singleAgent) { diff --git a/lib/app/app_controller_desktop_settings.dart b/lib/app/app_controller_desktop_settings.dart index ac55257b..c37878eb 100644 --- a/lib/app/app_controller_desktop_settings.dart +++ b/lib/app/app_controller_desktop_settings.dart @@ -269,7 +269,6 @@ extension AppControllerDesktopSettings on AppController { aiGatewayStreamingClientsInternal.clear(); aiGatewayPendingSessionKeysInternal.clear(); aiGatewayAbortedSessionKeysInternal.clear(); - latestRoutingResolutionBySessionInternal.clear(); singleAgentExternalCliPendingSessionKeysInternal.clear(); assistantThreadTurnQueuesInternal.clear(); multiAgentRunPendingInternal = false; diff --git a/lib/app/app_controller_desktop_single_agent.dart b/lib/app/app_controller_desktop_single_agent.dart index 1755176e..c6a5f706 100644 --- a/lib/app/app_controller_desktop_single_agent.dart +++ b/lib/app/app_controller_desktop_single_agent.dart @@ -45,7 +45,7 @@ import 'app_controller_desktop_workspace_execution.dart'; import 'app_controller_desktop_settings_runtime.dart'; import 'app_controller_desktop_thread_storage.dart'; import 'app_controller_desktop_skill_permissions.dart'; -import 'app_controller_desktop_go_agent_core_routing.dart'; +import 'app_controller_desktop_external_acp_routing.dart'; import 'app_controller_desktop_runtime_helpers.dart'; extension AppControllerDesktopSingleAgent on AppController { @@ -100,12 +100,12 @@ extension AppControllerDesktopSingleAgent on AppController { final fallbackReason = provider == null ? (selection == SingleAgentProvider.auto ? appText( - '当前没有可用的 Go Agent-core Provider。', - 'No Go Agent-core provider is currently available.', + '当前没有可用的 GoTaskService Provider。', + 'No GoTaskService provider is currently available.', ) : appText( - '当前 Go Agent-core 不支持 ${selection.label}。', - 'Go Agent-core does not currently support ${selection.label}.', + '当前 GoTaskService 不支持 ${selection.label}。', + 'GoTaskService does not currently support ${selection.label}.', )) : null; if (provider == null && !routing.isAuto) { @@ -205,25 +205,17 @@ extension AppControllerDesktopSingleAgent on AppController { }, ); final resolvedRuntimeModel = result.resolvedModel.trim(); - updateLatestRoutingResolutionInternal(sessionKey, result); - if (resolvedRuntimeModel.isNotEmpty) { - singleAgentRuntimeModelBySessionInternal[sessionKey] = - resolvedRuntimeModel; - } - final resolvedGatewayEntryState = - (result.resolvedExecutionTarget == 'gateway' || - result.resolvedExecutionTarget == 'gateway-chat') - ? (result.resolvedEndpointTarget.trim().isNotEmpty - ? result.resolvedEndpointTarget.trim() - : AssistantExecutionTarget.local.promptValue) - : result.resolvedExecutionTarget == 'single-agent' - ? AssistantExecutionTarget.singleAgent.promptValue - : (sessionTarget == AssistantExecutionTarget.auto - ? AssistantExecutionTarget.auto.promptValue - : AssistantExecutionTarget.singleAgent.promptValue); + final resolvedGatewayEntryState = goTaskServiceGatewayEntryState( + requestedTarget: sessionTarget, + result: result, + ); upsertTaskThreadInternal( sessionKey, gatewayEntryState: resolvedGatewayEntryState, + latestResolvedRuntimeModel: resolvedRuntimeModel, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: result.success ? 'success' : 'error', updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); final resolvedWorkspaceKind = result.resolvedWorkspaceRefKind; @@ -256,6 +248,10 @@ extension AppControllerDesktopSingleAgent on AppController { upsertTaskThreadInternal( sessionKey, gatewayEntryState: 'only-chat', + latestResolvedRuntimeModel: resolvedAiGatewayModel, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'fallback', updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); await sendAiGatewayMessageInternal( @@ -295,8 +291,8 @@ extension AppControllerDesktopSingleAgent on AppController { sessionKey, assistantErrorMessageInternal( appText( - 'Go Agent-core 执行失败:${result.errorMessage}', - 'Go Agent-core execution failed: ${result.errorMessage}', + 'GoTaskService 执行失败:${result.errorMessage}', + 'GoTaskService execution failed: ${result.errorMessage}', ), ), ); @@ -308,8 +304,8 @@ extension AppControllerDesktopSingleAgent on AppController { sessionKey, assistantErrorMessageInternal( appText( - 'Go Agent-core 没有返回可显示的输出。', - 'Go Agent-core returned no displayable output.', + 'GoTaskService 没有返回可显示的输出。', + 'GoTaskService returned no displayable output.', ), ), ); @@ -332,6 +328,13 @@ extension AppControllerDesktopSingleAgent on AppController { ); } catch (error) { clearAiGatewayStreamingTextInternal(sessionKey); + upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'error', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); appendAssistantThreadMessageInternal( sessionKey, assistantErrorMessageInternal(error.toString()), @@ -452,6 +455,15 @@ extension AppControllerDesktopSingleAgent on AppController { error: false, ), ); + upsertTaskThreadInternal( + sessionKey, + gatewayEntryState: 'only-chat', + latestResolvedRuntimeModel: model, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'success', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); } on AiGatewayAbortExceptionInternal catch (error) { final partial = error.partialText.trim(); if (partial.isNotEmpty) { @@ -470,7 +482,21 @@ extension AppControllerDesktopSingleAgent on AppController { ), ); } + upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'aborted', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); } catch (error) { + upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'error', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); appendAssistantThreadMessageInternal( sessionKey, assistantErrorMessageInternal(aiGatewayErrorLabelInternal(error)), @@ -698,6 +724,13 @@ extension AppControllerDesktopSingleAgent on AppController { } aiGatewayPendingSessionKeysInternal.remove(normalizedSessionKey); clearAiGatewayStreamingTextInternal(normalizedSessionKey); + upsertTaskThreadInternal( + normalizedSessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'aborted', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); recomputeTasksInternal(); notifyIfActiveInternal(); } diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index 233c9de4..988d6330 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -273,6 +273,10 @@ extension AppControllerDesktopSkillPermissions on AppController { ThreadSelectionSource? assistantModelSource, ThreadSelectionSource? selectedSkillsSource, String? gatewayEntryState, + String? latestResolvedRuntimeModel, + String? lifecycleStatus, + double? lastRunAtMs, + String? lastResultCode, }) { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, @@ -385,10 +389,14 @@ extension AppControllerDesktopSkillPermissions on AppController { selectedSkillsSource: selectedSkillsSource ?? existing?.contextState.selectedSkillsSource, + latestResolvedRuntimeModel: latestResolvedRuntimeModel, gatewayEntryState: gatewayEntryState, ); final nextStatus = - lifecycleState?.status ?? existing?.lifecycleState.status ?? 'ready'; + lifecycleStatus ?? + lifecycleState?.status ?? + existing?.lifecycleState.status ?? + 'ready'; final nextLifecycleState = (lifecycleState ?? existing?.lifecycleState ?? @@ -407,6 +415,8 @@ extension AppControllerDesktopSkillPermissions on AppController { existing?.archived ?? isAssistantTaskArchived(normalizedSessionKey), status: nextStatus, + lastRunAtMs: lastRunAtMs, + lastResultCode: lastResultCode, ); final nextRecord = TaskThread( threadId: normalizedSessionKey, diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index 7d891b9a..8527c935 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -341,14 +341,26 @@ extension AppControllerDesktopThreadActions on AppController { }, ); clearAiGatewayStreamingTextInternal(sessionKey); + upsertTaskThreadInternal( + sessionKey, + gatewayEntryState: goTaskServiceGatewayEntryState( + requestedTarget: currentTarget, + result: result, + ), + latestResolvedRuntimeModel: result.resolvedModel.trim(), + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: result.success ? 'success' : 'error', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); if (!result.success) { appendLocalSessionMessageInternal( sessionKey, assistantErrorMessageInternal( result.errorMessage.trim().isEmpty ? appText( - 'Go Agent-core 执行失败。', - 'Go Agent-core execution failed.', + 'GoTaskService 执行失败。', + 'GoTaskService execution failed.', ) : result.errorMessage, ), @@ -362,8 +374,8 @@ extension AppControllerDesktopThreadActions on AppController { sessionKey, assistantErrorMessageInternal( appText( - 'Go Agent-core 没有返回可显示的输出。', - 'Go Agent-core returned no displayable output.', + 'GoTaskService 没有返回可显示的输出。', + 'GoTaskService returned no displayable output.', ), ), persistInThreadContext: true, @@ -387,6 +399,13 @@ extension AppControllerDesktopThreadActions on AppController { ); } catch (error) { clearAiGatewayStreamingTextInternal(sessionKey); + upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'error', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); appendLocalSessionMessageInternal( sessionKey, assistantErrorMessageInternal(error.toString()), @@ -417,6 +436,13 @@ extension AppControllerDesktopThreadActions on AppController { // Best effort cancellation only. } multiAgentRunPendingInternal = false; + upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'aborted', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); recomputeTasksInternal(); notifyIfActiveInternal(); return; @@ -434,6 +460,13 @@ extension AppControllerDesktopThreadActions on AppController { ); aiGatewayPendingSessionKeysInternal.remove(sessionKey); clearAiGatewayStreamingTextInternal(sessionKey); + upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'aborted', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); recomputeTasksInternal(); notifyIfActiveInternal(); return; @@ -458,6 +491,13 @@ extension AppControllerDesktopThreadActions on AppController { ); aiGatewayPendingSessionKeysInternal.remove(sessionKey); clearAiGatewayStreamingTextInternal(sessionKey); + upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'aborted', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); recomputeTasksInternal(); notifyIfActiveInternal(); return; diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 0332d5f5..fef7a809 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -68,14 +68,6 @@ extension AppControllerDesktopThreadSessions on AppController { ); } - Map latestRoutingResolutionForSession(String sessionKey) { - final normalizedSessionKey = normalizedAssistantSessionKeyInternal( - sessionKey, - ); - return latestRoutingResolutionBySessionInternal[normalizedSessionKey] ?? - const {}; - } - int assistantSkillCountForSession(String sessionKey) { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, @@ -123,11 +115,11 @@ extension AppControllerDesktopThreadSessions on AppController { sessionKey, ); final target = assistantExecutionTargetForSession(normalizedSessionKey); - final latestRouting = latestRoutingResolutionForSession( - normalizedSessionKey, - ); final latestResolvedModel = - latestRouting['resolvedModel']?.toString().trim() ?? ''; + taskThreadForSessionInternal(normalizedSessionKey) + ?.latestResolvedRuntimeModel + .trim() ?? + ''; if (target == AssistantExecutionTarget.singleAgent || target == AssistantExecutionTarget.auto) { if (latestResolvedModel.isNotEmpty) { @@ -318,8 +310,9 @@ extension AppControllerDesktopThreadSessions on AppController { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, ); - return singleAgentRuntimeModelBySessionInternal[normalizedSessionKey] - ?.trim() ?? + return taskThreadForSessionInternal(normalizedSessionKey) + ?.latestResolvedRuntimeModel + .trim() ?? ''; } @@ -409,17 +402,14 @@ extension AppControllerDesktopThreadSessions on AppController { final target = assistantExecutionTargetForSession(normalizedSessionKey); if (target == AssistantExecutionTarget.singleAgent || target == AssistantExecutionTarget.auto) { - final latestRouting = latestRoutingResolutionForSession( - normalizedSessionKey, - ); - final latestResolvedExecutionTarget = - latestRouting['resolvedExecutionTarget']?.toString().trim() ?? ''; - final latestResolvedEndpointTarget = - latestRouting['resolvedEndpointTarget']?.toString().trim() ?? ''; - final latestResolvedProviderId = - latestRouting['resolvedProviderId']?.toString().trim() ?? ''; - final latestResolvedModel = - latestRouting['resolvedModel']?.toString().trim() ?? ''; + final thread = taskThreadForSessionInternal(normalizedSessionKey); + final resolvedGatewayEntryState = switch ( + thread?.gatewayEntryState?.trim() ?? '' + ) { + 'auto' => '', + final value => value, + }; + final latestResolvedModel = thread?.latestResolvedRuntimeModel.trim() ?? ''; final primaryLabel = target == AssistantExecutionTarget.auto ? 'Auto' : target.label; @@ -427,7 +417,7 @@ extension AppControllerDesktopThreadSessions on AppController { ? appText('当前: ', 'Current: ') : ''; if (target == AssistantExecutionTarget.auto && - latestResolvedExecutionTarget.isEmpty) { + resolvedGatewayEntryState.isEmpty) { final autoReady = autoRouteReadyForSession(normalizedSessionKey); return AssistantThreadConnectionState( executionTarget: target, @@ -443,22 +433,23 @@ extension AppControllerDesktopThreadSessions on AppController { ); } if (target == AssistantExecutionTarget.auto && - latestResolvedExecutionTarget.isNotEmpty) { - final detail = switch (latestResolvedExecutionTarget) { - 'gateway' => joinConnectionPartsInternal([ - latestResolvedEndpointTarget.isEmpty - ? appText('OpenClaw Gateway', 'OpenClaw Gateway') - : latestResolvedEndpointTarget, + resolvedGatewayEntryState.isNotEmpty) { + final detail = switch (resolvedGatewayEntryState) { + 'local' => joinConnectionPartsInternal([ + appText('OpenClaw Gateway', 'OpenClaw Gateway'), latestResolvedModel, ]), - 'multi-agent' => joinConnectionPartsInternal([ - appText('Multi-Agent', 'Multi-Agent'), + 'remote' => joinConnectionPartsInternal([ + appText('OpenClaw Gateway', 'OpenClaw Gateway'), latestResolvedModel, ]), _ => joinConnectionPartsInternal([ - latestResolvedProviderId.isEmpty - ? appText('Single Agent', 'Single Agent') - : latestResolvedProviderId, + singleAgentResolvedProviderForSession(normalizedSessionKey) + ?.label + .isNotEmpty == + true + ? singleAgentResolvedProviderForSession(normalizedSessionKey)!.label + : appText('Single Agent', 'Single Agent'), latestResolvedModel, ]), }; diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 618e6166..613c1dc9 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -111,11 +111,11 @@ extension AppControllerDesktopWorkspaceExecution on AppController { singleAgentProvider: currentSingleAgentProvider, ); } - singleAgentRuntimeModelBySessionInternal.remove(sessionKey); upsertTaskThreadInternal( sessionKey, singleAgentProvider: sanitizedProvider, singleAgentProviderSource: ThreadSelectionSource.explicit, + latestResolvedRuntimeModel: '', updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); recomputeTasksInternal(); @@ -180,7 +180,11 @@ extension AppControllerDesktopWorkspaceExecution on AppController { sessionKey, ); if (resolvedTarget != AssistantExecutionTarget.singleAgent) { - singleAgentRuntimeModelBySessionInternal.remove(normalizedSessionKey); + upsertTaskThreadInternal( + normalizedSessionKey, + latestResolvedRuntimeModel: '', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); } if (!matchesSessionKey( normalizedSessionKey, diff --git a/lib/app/app_controller_web_core.dart b/lib/app/app_controller_web_core.dart index c799c100..ad491161 100644 --- a/lib/app/app_controller_web_core.dart +++ b/lib/app/app_controller_web_core.dart @@ -8,7 +8,7 @@ import '../runtime/assistant_artifacts.dart'; import '../runtime/go_task_service_client.dart'; import '../runtime/runtime_models.dart'; import '../web/web_acp_client.dart'; -import '../web/go_agent_core_web_transport.dart'; +import '../web/external_code_agent_acp_web_transport.dart'; import '../web/go_task_service_web_service.dart'; import '../web/web_ai_gateway_client.dart'; import '../web/web_artifact_proxy_client.dart'; @@ -105,8 +105,6 @@ class AppController extends ChangeNotifier { final Map streamingTextBySessionInternal = {}; final Map> threadTurnQueuesInternal = >{}; - final Map singleAgentRuntimeModelBySessionInternal = - {}; final WebTasksController tasksControllerInternal = WebTasksController(); String currentSessionKeyInternal = ''; String? lastAssistantErrorInternal; diff --git a/lib/app/app_controller_web_gateway_chat.dart b/lib/app/app_controller_web_gateway_chat.dart index 4e43fa83..207a6cb0 100644 --- a/lib/app/app_controller_web_gateway_chat.dart +++ b/lib/app/app_controller_web_gateway_chat.dart @@ -142,6 +142,13 @@ extension AppControllerWebGatewayChat on AppController { text: error.toString(), error: true, ); + upsertThreadRecordInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'error', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); lastAssistantErrorInternal = error.toString(); pendingSessionKeysInternal.remove(sessionKey); streamingTextBySessionInternal.remove(sessionKey); @@ -213,6 +220,13 @@ extension AppControllerWebGatewayChat on AppController { acpBusyInternal = false; pendingSessionKeysInternal.remove(sessionKey); clearStreamingTextInternal(sessionKey); + upsertThreadRecordInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: lastAssistantErrorInternal == null ? 'success' : 'error', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); await persistThreadsInternal(); notifyChangedInternal(); } @@ -294,6 +308,18 @@ extension AppControllerWebGatewayChat on AppController { ), ); } + upsertThreadRecordInternal( + sessionKey, + gatewayEntryState: goTaskServiceGatewayEntryState( + requestedTarget: target, + result: result, + ), + latestResolvedRuntimeModel: result.resolvedModel.trim(), + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'success', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); appendAssistantMessageInternal( sessionKey: sessionKey, text: message, diff --git a/lib/app/app_controller_web_helpers.dart b/lib/app/app_controller_web_helpers.dart index 06195746..ce488ab4 100644 --- a/lib/app/app_controller_web_helpers.dart +++ b/lib/app/app_controller_web_helpers.dart @@ -384,6 +384,13 @@ extension AppControllerWebHelpers on AppController { text: text, error: false, ); + upsertThreadRecordInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'success', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); } if (state == 'final' || state == 'aborted' || state == 'error') { pendingSessionKeysInternal.remove(sessionKey); @@ -394,6 +401,17 @@ extension AppControllerWebHelpers on AppController { error: true, ); } + upsertThreadRecordInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: switch (state) { + 'aborted' => 'aborted', + 'error' => 'error', + _ => 'success', + }, + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); clearStreamingTextInternal(sessionKey); unawaited(refreshRelaySessions()); unawaited(refreshRelayHistory(sessionKey: sessionKey)); @@ -470,8 +488,12 @@ extension AppControllerWebHelpers on AppController { ThreadSelectionSource? selectedSkillsSource, String? gatewayEntryState, bool clearGatewayEntryState = false, + String? latestResolvedRuntimeModel, String? workspacePath, WorkspaceKind? workspaceKind, + String? lifecycleStatus, + double? lastRunAtMs, + String? lastResultCode, }) { final key = normalizedSessionKeyInternal(sessionKey); final resolvedTarget = @@ -500,6 +522,7 @@ extension AppControllerWebHelpers on AppController { assistantModelSource ?? existing.contextState.selectedModelSource, selectedSkillsSource: selectedSkillsSource ?? existing.contextState.selectedSkillsSource, + latestResolvedRuntimeModel: latestResolvedRuntimeModel, gatewayEntryState: gatewayEntryState ?? existing.gatewayEntryState, clearGatewayEntryState: clearGatewayEntryState, workspaceBinding: @@ -531,7 +554,11 @@ extension AppControllerWebHelpers on AppController { providerSource: singleAgentProviderSource ?? existing.executionBinding.providerSource, ), - lifecycleState: existing.lifecycleState.copyWith(status: 'ready'), + lifecycleState: existing.lifecycleState.copyWith( + status: lifecycleStatus ?? 'ready', + lastRunAtMs: lastRunAtMs, + lastResultCode: lastResultCode, + ), ), ); recomputeDerivedWorkspaceStateInternal(); diff --git a/lib/app/app_controller_web_session_actions.dart b/lib/app/app_controller_web_session_actions.dart index e66fd9b7..599efae7 100644 --- a/lib/app/app_controller_web_session_actions.dart +++ b/lib/app/app_controller_web_session_actions.dart @@ -154,11 +154,11 @@ extension AppControllerWebSessionActions on AppController { if (singleAgentProviderForSession(sessionKey) == resolvedProvider) { return; } - singleAgentRuntimeModelBySessionInternal.remove(sessionKey); upsertThreadRecordInternal( sessionKey, singleAgentProvider: resolvedProvider, singleAgentProviderSource: ThreadSelectionSource.explicit, + latestResolvedRuntimeModel: '', updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); await persistThreadsInternal(); diff --git a/lib/app/app_controller_web_sessions.dart b/lib/app/app_controller_web_sessions.dart index f708a702..d76fe42b 100644 --- a/lib/app/app_controller_web_sessions.dart +++ b/lib/app/app_controller_web_sessions.dart @@ -150,10 +150,11 @@ extension AppControllerWebSessions on AppController { singleAgentUsesAiChatFallbackForSession(currentSessionKeyInternal); String singleAgentRuntimeModelForSession(String sessionKey) { - return singleAgentRuntimeModelBySessionInternal[normalizedSessionKeyInternal( - sessionKey, - )] - ?.trim() ?? + return taskThreadForSessionInternal( + normalizedSessionKeyInternal(sessionKey), + ) + ?.latestResolvedRuntimeModel + .trim() ?? ''; } diff --git a/lib/runtime/direct_single_agent_app_server_client.dart b/lib/runtime/direct_single_agent_app_server_client.dart index 67cad407..d8c2a300 100644 --- a/lib/runtime/direct_single_agent_app_server_client.dart +++ b/lib/runtime/direct_single_agent_app_server_client.dart @@ -1,6 +1,6 @@ // Legacy compatibility surface retained while the app imports are cleaned up. // // The direct single-agent app-server runtime has been retired in favor of the -// GoAgentCore ACP path. This library intentionally exports only the capability +// GoTaskService ACP lane. This library intentionally exports only the capability // DTOs still consumed by the UI-facing state layer. export 'direct_single_agent_app_server_client_protocol.dart'; diff --git a/lib/runtime/go_agent_core_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart similarity index 98% rename from lib/runtime/go_agent_core_desktop_transport.dart rename to lib/runtime/external_code_agent_acp_desktop_transport.dart index 68c9bca0..843e6a2e 100644 --- a/lib/runtime/go_agent_core_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -90,8 +90,8 @@ class ExternalCodeAgentAcpDesktopTransport implements ExternalCodeAgentAcpTransp final endpoint = await _resolveEndpoint(request.target); if (endpoint == null) { throw const GatewayAcpException( - 'Missing Go Agent-core endpoint', - code: 'GO_AGENT_CORE_ENDPOINT_MISSING', + 'Missing external ACP endpoint', + code: 'EXTERNAL_ACP_ENDPOINT_MISSING', ); } var streamedText = ''; diff --git a/lib/runtime/go_agent_core_client.dart b/lib/runtime/go_agent_core_client.dart deleted file mode 100644 index 55d567bd..00000000 --- a/lib/runtime/go_agent_core_client.dart +++ /dev/null @@ -1,584 +0,0 @@ -import 'runtime_models.dart'; - -class GoAgentCoreCapabilities { - const GoAgentCoreCapabilities({ - required this.singleAgent, - required this.multiAgent, - required this.providers, - required this.raw, - }); - - const GoAgentCoreCapabilities.empty() - : singleAgent = false, - multiAgent = false, - providers = const {}, - raw = const {}; - - final bool singleAgent; - final bool multiAgent; - final Set providers; - final Map raw; -} - -class GoAgentCoreSyncedProvider { - const GoAgentCoreSyncedProvider({ - required this.providerId, - required this.label, - required this.endpoint, - required this.authorizationHeader, - required this.enabled, - }); - - final String providerId; - final String label; - final String endpoint; - final String authorizationHeader; - final bool enabled; - - Map toJson() { - return { - 'providerId': providerId.trim(), - 'label': label.trim(), - 'endpoint': endpoint.trim(), - 'authorizationHeader': authorizationHeader.trim(), - 'enabled': enabled, - }; - } -} - -enum GoAgentCoreRoutingMode { auto, explicit } - -class GoAgentCoreAvailableSkill { - const GoAgentCoreAvailableSkill({ - required this.id, - required this.label, - required this.description, - this.installed = true, - }); - - final String id; - final String label; - final String description; - final bool installed; - - Map toJson() { - return { - 'id': id.trim(), - 'label': label.trim(), - 'description': description.trim(), - 'installed': installed, - }; - } -} - -class GoAgentCoreRoutingConfig { - const GoAgentCoreRoutingConfig({ - required this.mode, - required this.preferredGatewayTarget, - required this.explicitExecutionTarget, - required this.explicitProviderId, - required this.explicitModel, - required this.explicitSkills, - required this.allowSkillInstall, - required this.availableSkills, - this.installApproval, - }); - - const GoAgentCoreRoutingConfig.auto({ - this.preferredGatewayTarget = '', - this.availableSkills = const [], - }) : mode = GoAgentCoreRoutingMode.auto, - explicitExecutionTarget = '', - explicitProviderId = '', - explicitModel = '', - explicitSkills = const [], - allowSkillInstall = false, - installApproval = null; - - final GoAgentCoreRoutingMode mode; - final String preferredGatewayTarget; - final String explicitExecutionTarget; - final String explicitProviderId; - final String explicitModel; - final List explicitSkills; - final bool allowSkillInstall; - final List availableSkills; - final GoAgentCoreSkillInstallApproval? installApproval; - - bool get isAuto => mode == GoAgentCoreRoutingMode.auto; - - Map toJson() { - return { - 'routingMode': mode.name, - if (preferredGatewayTarget.trim().isNotEmpty) - 'preferredGatewayTarget': preferredGatewayTarget.trim(), - if (explicitExecutionTarget.trim().isNotEmpty) - 'explicitExecutionTarget': explicitExecutionTarget.trim(), - if (explicitProviderId.trim().isNotEmpty) - 'explicitProviderId': explicitProviderId.trim(), - if (explicitModel.trim().isNotEmpty) - 'explicitModel': explicitModel.trim(), - 'explicitSkills': explicitSkills - .map((item) => item.trim()) - .where((item) => item.isNotEmpty) - .toList(growable: false), - 'allowSkillInstall': allowSkillInstall, - 'availableSkills': availableSkills - .map((item) => item.toJson()) - .toList(growable: false), - if (installApproval != null) 'installApproval': installApproval!.toJson(), - }; - } -} - -class GoAgentCoreSkillInstallApproval { - const GoAgentCoreSkillInstallApproval({ - required this.requestId, - required this.approvedSkillKeys, - }); - - final String requestId; - final List approvedSkillKeys; - - Map toJson() { - return { - 'requestId': requestId.trim(), - 'approvedSkillKeys': approvedSkillKeys - .map((item) => item.trim()) - .where((item) => item.isNotEmpty) - .toList(growable: false), - }; - } -} - -class GoAgentCoreSessionRequest { - const GoAgentCoreSessionRequest({ - required this.sessionId, - required this.threadId, - required this.target, - required this.prompt, - required this.workingDirectory, - required this.model, - required this.thinking, - required this.selectedSkills, - required this.inlineAttachments, - required this.localAttachments, - required this.aiGatewayBaseUrl, - required this.aiGatewayApiKey, - required this.agentId, - required this.metadata, - this.routing, - this.provider = SingleAgentProvider.auto, - this.resumeSession = false, - this.multiAgent = false, - }); - - final String sessionId; - final String threadId; - final AssistantExecutionTarget target; - final String prompt; - final String workingDirectory; - final String model; - final String thinking; - final List selectedSkills; - final List inlineAttachments; - final List localAttachments; - final String aiGatewayBaseUrl; - final String aiGatewayApiKey; - final String agentId; - final Map metadata; - final GoAgentCoreRoutingConfig? routing; - final SingleAgentProvider provider; - final bool resumeSession; - final bool multiAgent; - - String get mode { - if (multiAgent) { - return 'multi-agent'; - } - return switch (target) { - AssistantExecutionTarget.auto => 'single-agent', - AssistantExecutionTarget.singleAgent => 'single-agent', - AssistantExecutionTarget.local => _gatewaySessionMode, - AssistantExecutionTarget.remote => _gatewaySessionMode, - }; - } - - String get routingExecutionTarget { - if (multiAgent) { - return 'multi-agent'; - } - return switch (target) { - AssistantExecutionTarget.auto => 'single-agent', - AssistantExecutionTarget.singleAgent => 'single-agent', - AssistantExecutionTarget.local => 'gateway', - AssistantExecutionTarget.remote => 'gateway', - }; - } - - bool get hasInlineAttachments => inlineAttachments.isNotEmpty; - - GoAgentCoreRoutingConfig get effectiveRouting => - routing ?? _synthesizedRouting(); - - Map toAcpParams() { - final resolvedRouting = effectiveRouting; - final params = { - 'sessionId': sessionId, - 'threadId': threadId, - 'mode': mode, - 'taskPrompt': prompt, - 'workingDirectory': workingDirectory.trim(), - 'selectedSkills': selectedSkills, - 'attachments': >[ - ...localAttachments.map( - (item) => { - 'name': item.name, - 'description': item.description, - 'path': item.path, - }, - ), - ...inlineAttachments.map( - (item) => { - 'name': item.fileName, - 'description': item.mimeType, - 'path': '', - }, - ), - ], - if (inlineAttachments.isNotEmpty) - 'inlineAttachments': inlineAttachments - .map( - (item) => { - 'name': item.fileName, - 'mimeType': item.mimeType, - 'content': item.content, - 'sizeBytes': goAgentCoreBase64Size(item.content), - }, - ) - .toList(growable: false), - if (provider != SingleAgentProvider.auto) 'provider': provider.providerId, - if (model.trim().isNotEmpty) 'model': model.trim(), - if (thinking.trim().isNotEmpty) 'thinking': thinking.trim(), - if (aiGatewayBaseUrl.trim().isNotEmpty) - 'aiGatewayBaseUrl': aiGatewayBaseUrl.trim(), - if (aiGatewayApiKey.trim().isNotEmpty) - 'aiGatewayApiKey': aiGatewayApiKey.trim(), - 'routing': resolvedRouting.toJson(), - if (_usesGatewaySessionMode(mode)) ...{ - 'executionTarget': target.promptValue, - if (agentId.trim().isNotEmpty) 'agentId': agentId.trim(), - if (metadata.isNotEmpty) 'metadata': metadata, - }, - }; - return params; - } - - GoAgentCoreRoutingConfig _synthesizedRouting() { - final preferredGatewayTarget = switch (target) { - AssistantExecutionTarget.remote => 'remote', - _ => 'local', - }; - final explicitExecutionTarget = switch (target) { - AssistantExecutionTarget.local => 'local', - AssistantExecutionTarget.remote => 'remote', - AssistantExecutionTarget.singleAgent => 'singleAgent', - AssistantExecutionTarget.auto => '', - }; - final explicitProviderId = provider == SingleAgentProvider.auto - ? '' - : provider.providerId; - final explicitModelValue = model.trim(); - final explicitSkillsValue = selectedSkills - .map((item) => item.trim()) - .where((item) => item.isNotEmpty) - .toList(growable: false); - final hasExplicitSelection = - explicitExecutionTarget.isNotEmpty || - explicitProviderId.isNotEmpty || - explicitModelValue.isNotEmpty || - explicitSkillsValue.isNotEmpty; - if (!hasExplicitSelection) { - return GoAgentCoreRoutingConfig.auto( - preferredGatewayTarget: preferredGatewayTarget, - ); - } - return GoAgentCoreRoutingConfig( - mode: GoAgentCoreRoutingMode.explicit, - preferredGatewayTarget: preferredGatewayTarget, - explicitExecutionTarget: explicitExecutionTarget, - explicitProviderId: explicitProviderId, - explicitModel: explicitModelValue, - explicitSkills: explicitSkillsValue, - allowSkillInstall: false, - availableSkills: const [], - ); - } -} - -const String _gatewaySessionMode = 'gateway-chat'; - -bool _usesGatewaySessionMode(String mode) { - final normalized = mode.trim(); - return normalized == 'gateway' || normalized == _gatewaySessionMode; -} - -class GoAgentCoreSessionUpdate { - const GoAgentCoreSessionUpdate({ - required this.sessionId, - required this.threadId, - required this.turnId, - required this.type, - required this.text, - required this.message, - required this.pending, - required this.error, - required this.payload, - }); - - final String sessionId; - final String threadId; - final String turnId; - final String type; - final String text; - final String message; - final bool pending; - final bool error; - final Map payload; - - bool get isDelta => type == 'delta' && text.isNotEmpty; - bool get isDone => type == 'done' || payload['event'] == 'completed'; -} - -class GoAgentCoreRunResult { - const GoAgentCoreRunResult({ - required this.success, - required this.message, - required this.turnId, - required this.raw, - required this.errorMessage, - required this.resolvedModel, - }); - - final bool success; - final String message; - final String turnId; - final Map raw; - final String errorMessage; - final String resolvedModel; - - String get resolvedWorkingDirectory => - raw['resolvedWorkingDirectory']?.toString().trim() ?? - raw['workingDirectory']?.toString().trim() ?? - ''; - - String get resolvedExecutionTarget => - raw['resolvedExecutionTarget']?.toString().trim() ?? ''; - - String get resolvedEndpointTarget => - raw['resolvedEndpointTarget']?.toString().trim() ?? ''; - - String get resolvedProviderId => - raw['resolvedProviderId']?.toString().trim() ?? ''; - - List get resolvedSkills { - final rawList = raw['resolvedSkills']; - if (rawList is! List) { - return const []; - } - 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> get skillCandidates => - _castMapList(raw['skillCandidates']); - - List> get memorySources => - _castMapList(raw['memorySources']); - - WorkspaceRefKind? get resolvedWorkspaceRefKind { - final rawValue = raw['resolvedWorkspaceRefKind']?.toString().trim() ?? ''; - if (rawValue.isEmpty) { - return null; - } - return WorkspaceRefKindCopy.fromJsonValue(rawValue); - } -} - -abstract class GoAgentCoreClient { - Future syncProviders(List providers); - - Future loadCapabilities({ - required AssistantExecutionTarget target, - bool forceRefresh = false, - }); - - Future executeSession( - GoAgentCoreSessionRequest request, { - required void Function(GoAgentCoreSessionUpdate update) onUpdate, - }); - - Future cancelSession({ - required AssistantExecutionTarget target, - required String sessionId, - required String threadId, - }); - - Future closeSession({ - required AssistantExecutionTarget target, - required String sessionId, - required String threadId, - }); - - Future dispose(); -} - -GoAgentCoreSessionUpdate? goAgentCoreUpdateFromNotification( - Map notification, -) { - final method = notification['method']?.toString().trim().toLowerCase() ?? ''; - if (method != 'session.update' && method != 'acp.session.update') { - return null; - } - final params = _castMap(notification['params']); - final payload = params.isNotEmpty - ? params - : _castMap(notification['payload']); - final type = - payload['type']?.toString().trim().toLowerCase() ?? - payload['state']?.toString().trim().toLowerCase() ?? - payload['event']?.toString().trim().toLowerCase() ?? - 'status'; - return GoAgentCoreSessionUpdate( - sessionId: payload['sessionId']?.toString().trim().isNotEmpty == true - ? payload['sessionId'].toString().trim() - : payload['threadId']?.toString().trim() ?? '', - threadId: payload['threadId']?.toString().trim() ?? '', - turnId: payload['turnId']?.toString().trim() ?? '', - type: type, - text: - payload['delta']?.toString() ?? - payload['text']?.toString() ?? - _castMap(payload['message'])['content']?.toString() ?? - '', - message: payload['message']?.toString() ?? '', - pending: _boolValue(payload['pending']) ?? false, - error: _boolValue(payload['error']) ?? false, - payload: payload, - ); -} - -GoAgentCoreRunResult goAgentCoreRunResultFromResponse( - Map response, { - String streamedText = '', - String? completedMessage, -}) { - final result = _castMap(response['result']); - final primaryText = - (completedMessage?.trim().isNotEmpty == true - ? completedMessage!.trim() - : streamedText.trim().isNotEmpty - ? streamedText.trim() - : (result['output']?.toString().trim().isNotEmpty == true - ? result['output'].toString().trim() - : result['summary']?.toString().trim().isNotEmpty == true - ? result['summary'].toString().trim() - : result['message']?.toString().trim() ?? '')) - .trim(); - return GoAgentCoreRunResult( - success: _boolValue(result['success']) ?? true, - message: primaryText, - turnId: result['turnId']?.toString().trim() ?? '', - raw: result, - errorMessage: result['error']?.toString() ?? '', - resolvedModel: - result['model']?.toString().trim() ?? - result['resolvedModel']?.toString().trim() ?? - '', - ); -} - -Map mergeGoAgentCoreResponseResult( - Map response, - Map overlay, -) { - if (overlay.isEmpty) { - return response; - } - final next = Map.from(response); - final result = Map.from(_castMap(next['result'])); - overlay.forEach((key, value) { - if (value == null) { - return; - } - if (value is String && value.trim().isEmpty) { - if (result.containsKey(key)) { - return; - } - } - result[key] = value; - }); - next['result'] = result; - return next; -} - -int goAgentCoreBase64Size(String base64) { - final normalized = base64.trim().split(',').last.trim(); - if (normalized.isEmpty) { - return 0; - } - final padding = normalized.endsWith('==') - ? 2 - : (normalized.endsWith('=') ? 1 : 0); - return (normalized.length * 3 ~/ 4) - padding; -} - -Map _castMap(Object? value) { - if (value is Map) { - return value; - } - if (value is Map) { - return value.cast(); - } - return const {}; -} - -bool? _boolValue(Object? raw) { - if (raw is bool) { - return raw; - } - if (raw is num) { - return raw != 0; - } - final text = raw?.toString().trim().toLowerCase(); - if (text == null || text.isEmpty) { - return null; - } - if (text == 'true' || text == '1' || text == 'yes') { - return true; - } - if (text == 'false' || text == '0' || text == 'no') { - return false; - } - return null; -} - -List> _castMapList(Object? raw) { - if (raw is! List) { - return const >[]; - } - return raw - .map((item) => _castMap(item)) - .where((item) => item.isNotEmpty) - .toList(growable: false); -} diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index e8186f01..ec3d6e75 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -433,6 +433,35 @@ class GoTaskServiceResult { } } +String? goTaskServiceGatewayEntryState({ + required AssistantExecutionTarget requestedTarget, + required GoTaskServiceResult result, +}) { + final resolvedExecutionTarget = result.resolvedExecutionTarget.trim().toLowerCase(); + switch (resolvedExecutionTarget) { + case 'gateway': + final resolvedEndpointTarget = result.resolvedEndpointTarget.trim().toLowerCase(); + if (resolvedEndpointTarget == AssistantExecutionTarget.remote.promptValue.toLowerCase()) { + return AssistantExecutionTarget.remote.promptValue; + } + if (resolvedEndpointTarget == AssistantExecutionTarget.local.promptValue.toLowerCase()) { + return AssistantExecutionTarget.local.promptValue; + } + return requestedTarget == AssistantExecutionTarget.remote + ? AssistantExecutionTarget.remote.promptValue + : AssistantExecutionTarget.local.promptValue; + case 'single-agent': + return AssistantExecutionTarget.singleAgent.promptValue; + case 'multi-agent': + return AssistantExecutionTarget.singleAgent.promptValue; + default: + if (requestedTarget == AssistantExecutionTarget.auto) { + return null; + } + return requestedTarget.promptValue; + } +} + abstract class ExternalCodeAgentAcpTransport { Future syncExternalProviders( List providers, diff --git a/lib/runtime/single_agent_runner.dart b/lib/runtime/single_agent_runner.dart index 965961a9..0b611380 100644 --- a/lib/runtime/single_agent_runner.dart +++ b/lib/runtime/single_agent_runner.dart @@ -1,4 +1,4 @@ // Legacy compatibility shim retained until remaining imports are cleaned up. // -// Single-agent execution now flows through GoAgentCoreClient and the ACP +// Single-agent execution now flows through GoTaskService and the ACP // transport; the previous direct runner no longer owns runtime strategy. diff --git a/lib/web/go_agent_core_web_transport.dart b/lib/web/external_code_agent_acp_web_transport.dart similarity index 89% rename from lib/web/go_agent_core_web_transport.dart rename to lib/web/external_code_agent_acp_web_transport.dart index aaca1a84..666c4391 100644 --- a/lib/web/go_agent_core_web_transport.dart +++ b/lib/web/external_code_agent_acp_web_transport.dart @@ -12,13 +12,14 @@ class ExternalCodeAgentAcpWebTransport implements ExternalCodeAgentAcpTransport final WebAcpClient _acpClient; final Uri? Function(AssistantExecutionTarget target) _endpointResolver; - Uri? get _goCoreEndpoint => _endpointResolver(AssistantExecutionTarget.singleAgent); + Uri? get _externalAcpEndpoint => + _endpointResolver(AssistantExecutionTarget.singleAgent); @override Future syncExternalProviders( List providers, ) async { - final endpoint = _goCoreEndpoint; + final endpoint = _externalAcpEndpoint; if (endpoint == null) { return; } @@ -36,7 +37,7 @@ class ExternalCodeAgentAcpWebTransport implements ExternalCodeAgentAcpTransport required AssistantExecutionTarget target, bool forceRefresh = false, }) async { - final endpoint = _goCoreEndpoint; + final endpoint = _externalAcpEndpoint; if (endpoint == null) { return const ExternalCodeAgentAcpCapabilities.empty(); } @@ -54,11 +55,11 @@ class ExternalCodeAgentAcpWebTransport implements ExternalCodeAgentAcpTransport GoTaskServiceRequest request, { required void Function(GoTaskServiceUpdate update) onUpdate, }) async { - final endpoint = _goCoreEndpoint; + final endpoint = _externalAcpEndpoint; if (endpoint == null) { throw const WebAcpException( - 'Missing Go Agent-core endpoint', - code: 'GO_AGENT_CORE_ENDPOINT_MISSING', + 'Missing external ACP endpoint', + code: 'EXTERNAL_ACP_ENDPOINT_MISSING', ); } var streamedText = ''; @@ -95,7 +96,7 @@ class ExternalCodeAgentAcpWebTransport implements ExternalCodeAgentAcpTransport required String sessionId, required String threadId, }) async { - final endpoint = _goCoreEndpoint; + final endpoint = _externalAcpEndpoint; if (endpoint == null) { return; } @@ -112,7 +113,7 @@ class ExternalCodeAgentAcpWebTransport implements ExternalCodeAgentAcpTransport required String sessionId, required String threadId, }) async { - final endpoint = _goCoreEndpoint; + final endpoint = _externalAcpEndpoint; if (endpoint == null) { return; } diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_chat.dart b/test/runtime/app_controller_ai_gateway_chat_suite_chat.dart index 28fe192a..2f383f64 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_chat.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_chat.dart @@ -9,7 +9,6 @@ import 'package:xworkmate/app/app_controller.dart'; import 'package:xworkmate/runtime/codex_runtime.dart'; import 'package:xworkmate/runtime/device_identity_store.dart'; import 'package:xworkmate/runtime/gateway_runtime.dart'; -import 'package:xworkmate/runtime/go_agent_core_client.dart'; import 'package:xworkmate/runtime/runtime_coordinator.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; @@ -42,7 +41,7 @@ void registerAppControllerAiGatewayChatSuiteChatTestsInternal() { gateway: gateway, codex: FakeCodexRuntimeInternal(), ), - goTaskServiceClient: FallbackOnlyGoAgentCoreClientInternal(), + goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(), ); await controller.settingsController.saveAiGatewayApiKey('live-key'); @@ -104,7 +103,7 @@ void registerAppControllerAiGatewayChatSuiteChatTestsInternal() { gateway: secondGateway, codex: FakeCodexRuntimeInternal(), ), - goTaskServiceClient: FallbackOnlyGoAgentCoreClientInternal(), + goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(), ); await secondController.settingsController.saveAiGatewayApiKey( @@ -182,7 +181,7 @@ void registerAppControllerAiGatewayChatSuiteChatTestsInternal() { gateway: FakeGatewayRuntimeInternal(store: store), codex: FakeCodexRuntimeInternal(), ), - goTaskServiceClient: FallbackOnlyGoAgentCoreClientInternal(), + goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(), ); await controller.settingsController.saveAiGatewayApiKey('live-key'); @@ -242,7 +241,7 @@ void registerAppControllerAiGatewayChatSuiteChatTestsInternal() { gateway: FakeGatewayRuntimeInternal(store: store), codex: FakeCodexRuntimeInternal(), ), - goTaskServiceClient: FallbackOnlyGoAgentCoreClientInternal(), + goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(), ); await controller.settingsController.saveAiGatewayApiKey('live-key'); diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart b/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart index 628b9805..5e80dab5 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_fakes.dart @@ -112,8 +112,8 @@ class FakeCodexRuntimeInternal extends CodexRuntime { Future stop() async {} } -class FakeGoAgentCoreClientInternal implements GoTaskServiceClient { - FakeGoAgentCoreClientInternal({ +class FakeGoTaskServiceClientInternal implements GoTaskServiceClient { + FakeGoTaskServiceClientInternal({ this.capabilities = const ExternalCodeAgentAcpCapabilities.empty(), this.result = const GoTaskServiceResult( success: false, @@ -198,9 +198,9 @@ class FakeGoAgentCoreClientInternal implements GoTaskServiceClient { Future dispose() async {} } -class FallbackOnlyGoAgentCoreClientInternal - extends FakeGoAgentCoreClientInternal { - FallbackOnlyGoAgentCoreClientInternal() +class FallbackOnlyGoTaskServiceClientInternal + extends FakeGoTaskServiceClientInternal { + FallbackOnlyGoTaskServiceClientInternal() : super(capabilities: const ExternalCodeAgentAcpCapabilities.empty()); } diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart index e073eb1c..1436469c 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart @@ -27,7 +27,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { 'xworkmate-single-agent-provider-', ); final store = createStoreFromTempDirectoryInternal(tempDirectory); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, @@ -107,7 +107,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { 'xworkmate-auto-route-ready-', ); final store = createStoreFromTempDirectoryInternal(tempDirectory); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, @@ -159,7 +159,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { 'xworkmate-single-agent-provider-debug-', ); final store = createStoreFromTempDirectoryInternal(tempDirectory); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, @@ -222,7 +222,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { await store.saveSettingsSnapshot( SettingsSnapshot.defaults().copyWith(workspacePath: tempDirectory.path), ); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, @@ -297,7 +297,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ), ), ); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, @@ -367,7 +367,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { }); final store = createStoreFromTempDirectoryInternal(tempDirectory); - final client = FallbackOnlyGoAgentCoreClientInternal(); + final client = FallbackOnlyGoTaskServiceClientInternal(); final controller = await createAppControllerInternal( store: store, availableSingleAgentProvidersOverride: const [ @@ -433,7 +433,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { }); final store = createStoreFromTempDirectoryInternal(tempDirectory); - final client = FallbackOnlyGoAgentCoreClientInternal(); + final client = FallbackOnlyGoTaskServiceClientInternal(); final controller = await createAppControllerInternal( store: store, availableSingleAgentProvidersOverride: const [], @@ -502,7 +502,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { }); final store = createStoreFromTempDirectoryInternal(tempDirectory); - final client = FallbackOnlyGoAgentCoreClientInternal(); + final client = FallbackOnlyGoTaskServiceClientInternal(); final controller = await createAppControllerInternal( store: store, availableSingleAgentProvidersOverride: const [], @@ -593,7 +593,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ), ]); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, @@ -653,7 +653,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ), ); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, @@ -730,7 +730,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ), ); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, @@ -822,7 +822,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ), ); - final client = FakeGoAgentCoreClientInternal( + final client = FakeGoTaskServiceClientInternal( capabilities: ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: false, diff --git a/test/runtime/app_controller_assistant_flow_suite.dart b/test/runtime/app_controller_assistant_flow_suite.dart index c32d3408..9da62859 100644 --- a/test/runtime/app_controller_assistant_flow_suite.dart +++ b/test/runtime/app_controller_assistant_flow_suite.dart @@ -29,12 +29,12 @@ void main() { databasePathResolver: () async => '${tempDirectory.path}/settings.db', fallbackDirectoryPathResolver: () async => tempDirectory.path, ); - final goCoreClient = _FakeGoAgentCoreClient( + final goTaskServiceClient = _FakeGoTaskServiceClient( onExecute: gateway.recordGoCoreTurn, ); final controller = AppController( store: store, - goTaskServiceClient: goCoreClient, + goTaskServiceClient: goTaskServiceClient, ); addTearDown(() async { controller.dispose(); @@ -76,23 +76,23 @@ void main() { ), isTrue, ); - expect(goCoreClient.lastRequest?.agentId, 'main'); + expect(goTaskServiceClient.lastRequest?.agentId, 'main'); expect( - ((goCoreClient.lastRequest?.metadata as Map?)?['node'] + ((goTaskServiceClient.lastRequest?.metadata as Map?)?['node'] as Map?)?['kind'], 'app-mediated-cooperative-node', ); expect( - ((goCoreClient.lastRequest?.metadata as Map?)?['dispatch'] + ((goTaskServiceClient.lastRequest?.metadata as Map?)?['dispatch'] as Map?)?['mode'], 'gateway-only', ); expect( - goCoreClient.lastRequest?.routing?.mode, + goTaskServiceClient.lastRequest?.routing?.mode, ExternalCodeAgentAcpRoutingMode.auto, ); expect( - goCoreClient.lastRequest?.routing?.preferredGatewayTarget, + goTaskServiceClient.lastRequest?.routing?.preferredGatewayTarget, 'local', ); }, @@ -113,10 +113,10 @@ void main() { databasePathResolver: () async => '${tempDirectory.path}/settings.db', fallbackDirectoryPathResolver: () async => tempDirectory.path, ); - final goCoreClient = _FakeGoAgentCoreClient(); + final goTaskServiceClient = _FakeGoTaskServiceClient(); final controller = AppController( store: store, - goTaskServiceClient: goCoreClient, + goTaskServiceClient: goTaskServiceClient, ); addTearDown(controller.dispose); @@ -138,14 +138,17 @@ void main() { await controller.sendChatMessage('只回复 EXPLICIT_OK', thinking: 'low'); expect( - goCoreClient.lastRequest?.routing?.mode, + goTaskServiceClient.lastRequest?.routing?.mode, ExternalCodeAgentAcpRoutingMode.explicit, ); expect( - goCoreClient.lastRequest?.routing?.explicitExecutionTarget, + goTaskServiceClient.lastRequest?.routing?.explicitExecutionTarget, 'singleAgent', ); - expect(goCoreClient.lastRequest?.routing?.explicitProviderId, 'opencode'); + expect( + goTaskServiceClient.lastRequest?.routing?.explicitProviderId, + 'opencode', + ); }, ); @@ -167,7 +170,7 @@ void main() { ); final controller = AppController( store: store, - goTaskServiceClient: _FakeGoAgentCoreClient( + goTaskServiceClient: _FakeGoTaskServiceClient( onExecute: gateway.recordGoCoreTurn, ), ); @@ -217,7 +220,7 @@ void main() { ); final controller = AppController( store: store, - goTaskServiceClient: _FakeGoAgentCoreClient( + goTaskServiceClient: _FakeGoTaskServiceClient( onExecute: gateway.recordGoCoreTurn, ), ); @@ -625,8 +628,8 @@ class _FakeGatewayServer { } } -class _FakeGoAgentCoreClient implements GoTaskServiceClient { - _FakeGoAgentCoreClient({this.onExecute}); +class _FakeGoTaskServiceClient implements GoTaskServiceClient { + _FakeGoTaskServiceClient({this.onExecute}); GoTaskServiceRequest? lastRequest; final void Function(GoTaskServiceRequest request)? onExecute; diff --git a/test/runtime/go_agent_core_desktop_transport_suite.dart b/test/runtime/external_code_agent_acp_desktop_transport_test.dart similarity index 97% rename from test/runtime/go_agent_core_desktop_transport_suite.dart rename to test/runtime/external_code_agent_acp_desktop_transport_test.dart index 9a5d565d..47c44903 100644 --- a/test/runtime/go_agent_core_desktop_transport_suite.dart +++ b/test/runtime/external_code_agent_acp_desktop_transport_test.dart @@ -6,8 +6,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/runtime/external_code_agent_acp_desktop_transport.dart'; import 'package:xworkmate/runtime/gateway_acp_client.dart'; -import 'package:xworkmate/runtime/go_agent_core_desktop_transport.dart'; import 'package:xworkmate/runtime/go_task_service_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; @@ -82,7 +82,7 @@ void main() { isA().having( (error) => error.code, 'code', - 'GO_AGENT_CORE_ENDPOINT_MISSING', + 'EXTERNAL_ACP_ENDPOINT_MISSING', ), ), ); diff --git a/test/runtime/go_agent_core_client_suite.dart b/test/runtime/go_agent_core_client_suite.dart deleted file mode 100644 index 15ef61cc..00000000 --- a/test/runtime/go_agent_core_client_suite.dart +++ /dev/null @@ -1,215 +0,0 @@ -@TestOn('vm') -library; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:xworkmate/runtime/go_agent_core_client.dart'; -import 'package:xworkmate/runtime/runtime_models.dart'; - -void main() { - group('GoAgentCore client mapping', () { - test('session request maps skills, attachments, and provider into ACP', () { - const request = GoAgentCoreSessionRequest( - sessionId: 'session-1', - threadId: 'thread-1', - target: AssistantExecutionTarget.singleAgent, - prompt: 'hello world', - workingDirectory: '/tmp/workspace', - model: 'codex-sonnet', - thinking: 'medium', - selectedSkills: ['PPT', 'Browser Automation'], - inlineAttachments: [ - GatewayChatAttachmentPayload( - type: 'inline', - fileName: 'note.txt', - mimeType: 'text/plain', - content: 'aGVsbG8=', - ), - ], - localAttachments: [ - CollaborationAttachment( - name: 'spec.md', - path: '/tmp/workspace/spec.md', - description: 'workspace spec', - ), - ], - aiGatewayBaseUrl: 'https://gateway.example.com', - aiGatewayApiKey: 'secret', - agentId: '', - metadata: {}, - routing: GoAgentCoreRoutingConfig.auto( - preferredGatewayTarget: 'local', - availableSkills: [ - GoAgentCoreAvailableSkill( - id: 'pptx', - label: 'PPTX', - description: 'deck skill', - ), - ], - ), - provider: SingleAgentProvider.opencode, - ); - - final params = request.toAcpParams(); - - expect(params['sessionId'], 'session-1'); - expect(params['threadId'], 'thread-1'); - expect(params['mode'], 'single-agent'); - expect(params['workingDirectory'], '/tmp/workspace'); - expect(params['provider'], 'opencode'); - expect(params['model'], 'codex-sonnet'); - expect(params['thinking'], 'medium'); - expect(params['selectedSkills'], ['PPT', 'Browser Automation']); - expect(params['attachments'], >[ - { - 'name': 'spec.md', - 'description': 'workspace spec', - 'path': '/tmp/workspace/spec.md', - }, - { - 'name': 'note.txt', - 'description': 'text/plain', - 'path': '', - }, - ]); - expect(params['inlineAttachments'], >[ - { - 'name': 'note.txt', - 'mimeType': 'text/plain', - 'content': 'aGVsbG8=', - 'sizeBytes': 5, - }, - ]); - expect(params['routing'], { - 'routingMode': 'auto', - 'preferredGatewayTarget': 'local', - 'explicitSkills': const [], - 'allowSkillInstall': false, - 'availableSkills': >[ - { - 'id': 'pptx', - 'label': 'PPTX', - 'description': 'deck skill', - 'installed': true, - }, - ], - }); - }); - - test('session request synthesizes routing when caller omits it', () { - const request = GoAgentCoreSessionRequest( - sessionId: 'session-implicit-routing', - threadId: 'thread-implicit-routing', - target: AssistantExecutionTarget.singleAgent, - prompt: 'hello world', - workingDirectory: '/tmp/workspace', - model: 'codex-sonnet', - thinking: '', - selectedSkills: ['PPTX'], - inlineAttachments: [], - localAttachments: [], - aiGatewayBaseUrl: '', - aiGatewayApiKey: '', - agentId: '', - metadata: {}, - provider: SingleAgentProvider.opencode, - ); - - final params = request.toAcpParams(); - - expect(params['routing'], { - 'routingMode': 'explicit', - 'preferredGatewayTarget': 'local', - 'explicitExecutionTarget': 'singleAgent', - 'explicitProviderId': 'opencode', - 'explicitModel': 'codex-sonnet', - 'explicitSkills': const ['PPTX'], - 'allowSkillInstall': false, - 'availableSkills': const >[], - }); - }); - - test('routing execution target uses gateway while session mode stays compatible', () { - const request = GoAgentCoreSessionRequest( - sessionId: 'session-2', - threadId: 'thread-2', - target: AssistantExecutionTarget.local, - prompt: 'search latest news', - workingDirectory: '/tmp/workspace', - model: '', - thinking: '', - selectedSkills: [], - inlineAttachments: [], - localAttachments: [], - aiGatewayBaseUrl: '', - aiGatewayApiKey: '', - agentId: 'agent-1', - metadata: {'source': 'test'}, - provider: SingleAgentProvider.auto, - ); - - final params = request.toAcpParams(); - - expect(request.routingExecutionTarget, 'gateway'); - expect(params['mode'], 'gateway-chat'); - expect(params['executionTarget'], 'local'); - expect(params['agentId'], 'agent-1'); - expect(params['routing'], { - 'routingMode': 'explicit', - 'preferredGatewayTarget': 'local', - 'explicitExecutionTarget': 'local', - 'explicitSkills': const [], - 'allowSkillInstall': false, - 'availableSkills': const >[], - }); - }); - - test( - 'run result prefers completion text and preserves resolved workspace', - () { - final result = goAgentCoreRunResultFromResponse( - { - 'result': { - 'success': true, - 'turnId': 'turn-7', - 'summary': 'summary text', - 'resolvedModel': 'codex-sonnet', - 'resolvedWorkingDirectory': '/tmp/thread', - 'resolvedWorkspaceRefKind': 'remotePath', - }, - }, - streamedText: 'partial output', - completedMessage: 'final output', - ); - - expect(result.success, isTrue); - expect(result.turnId, 'turn-7'); - expect(result.message, 'final output'); - expect(result.resolvedModel, 'codex-sonnet'); - expect(result.resolvedWorkingDirectory, '/tmp/thread'); - expect(result.resolvedWorkspaceRefKind, WorkspaceRefKind.remotePath); - }, - ); - - test('session update recognizes delta notifications', () { - final update = goAgentCoreUpdateFromNotification({ - 'method': 'session.update', - 'params': { - 'sessionId': 'session-2', - 'threadId': 'thread-2', - 'turnId': 'turn-2', - 'type': 'delta', - 'delta': 'hello', - 'pending': true, - }, - }); - - expect(update, isNotNull); - expect(update!.sessionId, 'session-2'); - expect(update.threadId, 'thread-2'); - expect(update.turnId, 'turn-2'); - expect(update.isDelta, isTrue); - expect(update.text, 'hello'); - expect(update.pending, isTrue); - }); - }); -} diff --git a/test/runtime/go_agent_core_client_test.dart b/test/runtime/go_agent_core_client_test.dart deleted file mode 100644 index b0eef63d..00000000 --- a/test/runtime/go_agent_core_client_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -import '../test_suite_stub.dart' - if (dart.library.io) 'go_agent_core_client_suite.dart' - as suite; - -void main() { - suite.main(); -} diff --git a/test/runtime/go_task_service_client_test.dart b/test/runtime/go_task_service_client_test.dart index 51a05617..39b2f2a1 100644 --- a/test/runtime/go_task_service_client_test.dart +++ b/test/runtime/go_task_service_client_test.dart @@ -59,4 +59,214 @@ void main() { ); }); }); + + group('GoTaskService ACP mapping', () { + test('request maps skills, attachments, and provider into ACP params', () { + const request = GoTaskServiceRequest( + sessionId: 'session-1', + threadId: 'thread-1', + target: AssistantExecutionTarget.singleAgent, + prompt: 'hello world', + workingDirectory: '/tmp/workspace', + model: 'codex-sonnet', + thinking: 'medium', + selectedSkills: ['PPT', 'Browser Automation'], + inlineAttachments: [ + GatewayChatAttachmentPayload( + type: 'inline', + fileName: 'note.txt', + mimeType: 'text/plain', + content: 'aGVsbG8=', + ), + ], + localAttachments: [ + CollaborationAttachment( + name: 'spec.md', + path: '/tmp/workspace/spec.md', + description: 'workspace spec', + ), + ], + aiGatewayBaseUrl: 'https://gateway.example.com', + aiGatewayApiKey: 'secret', + agentId: '', + metadata: {}, + routing: ExternalCodeAgentAcpRoutingConfig.auto( + preferredGatewayTarget: 'local', + availableSkills: [ + ExternalCodeAgentAcpAvailableSkill( + id: 'pptx', + label: 'PPTX', + description: 'deck skill', + ), + ], + ), + provider: SingleAgentProvider.opencode, + ); + + final params = request.toExternalAcpParams(); + + expect(params['sessionId'], 'session-1'); + expect(params['threadId'], 'thread-1'); + expect(params['mode'], 'single-agent'); + expect(params['workingDirectory'], '/tmp/workspace'); + expect(params['provider'], 'opencode'); + expect(params['model'], 'codex-sonnet'); + expect(params['thinking'], 'medium'); + expect(params['selectedSkills'], ['PPT', 'Browser Automation']); + expect(params['attachments'], >[ + { + 'name': 'spec.md', + 'description': 'workspace spec', + 'path': '/tmp/workspace/spec.md', + }, + { + 'name': 'note.txt', + 'description': 'text/plain', + 'path': '', + }, + ]); + expect(params['inlineAttachments'], >[ + { + 'name': 'note.txt', + 'mimeType': 'text/plain', + 'content': 'aGVsbG8=', + 'sizeBytes': 5, + }, + ]); + expect(params['routing'], { + 'routingMode': 'auto', + 'preferredGatewayTarget': 'local', + 'explicitSkills': const [], + 'allowSkillInstall': false, + 'availableSkills': >[ + { + 'id': 'pptx', + 'label': 'PPTX', + 'description': 'deck skill', + 'installed': true, + }, + ], + }); + }); + + test('request synthesizes routing when caller omits it', () { + const request = GoTaskServiceRequest( + sessionId: 'session-implicit-routing', + threadId: 'thread-implicit-routing', + target: AssistantExecutionTarget.singleAgent, + prompt: 'hello world', + workingDirectory: '/tmp/workspace', + model: 'codex-sonnet', + thinking: '', + selectedSkills: ['PPTX'], + inlineAttachments: [], + localAttachments: [], + aiGatewayBaseUrl: '', + aiGatewayApiKey: '', + agentId: '', + metadata: {}, + provider: SingleAgentProvider.opencode, + ); + + final params = request.toExternalAcpParams(); + + expect(params['routing'], { + 'routingMode': 'explicit', + 'preferredGatewayTarget': 'local', + 'explicitExecutionTarget': 'singleAgent', + 'explicitProviderId': 'opencode', + 'explicitModel': 'codex-sonnet', + 'explicitSkills': const ['PPTX'], + 'allowSkillInstall': false, + 'availableSkills': const >[], + }); + }); + + test( + 'request keeps gateway ACP compatibility while controller semantics stay route-based', + () { + const request = GoTaskServiceRequest( + sessionId: 'session-2', + threadId: 'thread-2', + target: AssistantExecutionTarget.local, + prompt: 'search latest news', + workingDirectory: '/tmp/workspace', + model: '', + thinking: '', + selectedSkills: [], + inlineAttachments: [], + localAttachments: [], + aiGatewayBaseUrl: '', + aiGatewayApiKey: '', + agentId: 'agent-1', + metadata: {'source': 'test'}, + ); + + final params = request.toExternalAcpParams(); + + expect(request.routingExecutionTarget, 'gateway'); + expect(params['mode'], 'gateway-chat'); + expect(params['executionTarget'], 'local'); + expect(params['agentId'], 'agent-1'); + expect(params['routing'], { + 'routingMode': 'explicit', + 'preferredGatewayTarget': 'local', + 'explicitExecutionTarget': 'local', + 'explicitSkills': const [], + 'allowSkillInstall': false, + 'availableSkills': const >[], + }); + }, + ); + + test( + 'run result prefers completion text and preserves resolved workspace', + () { + final result = goTaskServiceResultFromAcpResponse( + { + 'result': { + 'success': true, + 'turnId': 'turn-7', + 'summary': 'summary text', + 'resolvedModel': 'codex-sonnet', + 'resolvedWorkingDirectory': '/tmp/thread', + 'resolvedWorkspaceRefKind': 'remotePath', + }, + }, + route: GoTaskServiceRoute.externalAcpSingle, + streamedText: 'partial output', + completedMessage: 'final output', + ); + + expect(result.success, isTrue); + expect(result.turnId, 'turn-7'); + expect(result.message, 'final output'); + expect(result.resolvedModel, 'codex-sonnet'); + expect(result.resolvedWorkingDirectory, '/tmp/thread'); + expect(result.resolvedWorkspaceRefKind, WorkspaceRefKind.remotePath); + }, + ); + + test('session update recognizes delta notifications', () { + final update = goTaskServiceUpdateFromAcpNotification({ + 'method': 'session.update', + 'params': { + 'sessionId': 'session-2', + 'threadId': 'thread-2', + 'turnId': 'turn-2', + 'type': 'delta', + 'delta': 'hello', + 'pending': true, + }, + }); + + expect(update, isNotNull); + expect(update!.sessionId, 'session-2'); + expect(update.threadId, 'thread-2'); + expect(update.turnId, 'turn-2'); + expect(update.isDelta, isTrue); + expect(update.text, 'hello'); + expect(update.pending, isTrue); + }); + }); } diff --git a/test/runtime/no_direct_cli_execution_guard_suite.dart b/test/runtime/no_direct_cli_execution_guard_suite.dart index 12a5c1fc..6b92ecb4 100644 --- a/test/runtime/no_direct_cli_execution_guard_suite.dart +++ b/test/runtime/no_direct_cli_execution_guard_suite.dart @@ -19,7 +19,7 @@ void main() { ]; const guardedFiles = [ 'lib/app/app_controller_desktop.dart', - 'lib/runtime/go_agent_core_client.dart', + 'lib/runtime/go_task_service_client.dart', 'lib/runtime/runtime_coordinator.dart', 'lib/runtime/gateway_acp_client.dart', ]; @@ -65,7 +65,7 @@ void main() { expect( File(relativePath).existsSync(), isFalse, - reason: '$relativePath should stay removed after GoAgentCore cutover', + reason: '$relativePath should stay removed after GoTaskService cutover', ); }