Refactor thread state and runtime naming alignment

This commit is contained in:
Haitao Pan 2026-04-06 12:32:59 +08:00
parent abe3815d2e
commit 0e0ec2fbc2
35 changed files with 564 additions and 1027 deletions

View File

@ -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 差异

View File

@ -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 bridgeWeb 走远端 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` 组织。

View File

@ -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"]

View File

@ -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(主体区域 同步显示)"]

View File

@ -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["线程持久化<br/>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<br/>GoAgentCoreDesktopTransport / go_core / codex_ffi_bindings"]
D9["Web transport<br/>GoAgentCoreWebTransport / WebAcpClient / Relay"]
D8["Desktop ACP transport<br/>ExternalCodeAgentAcpDesktopTransport / go_core / codex_ffi_bindings"]
D9["Web ACP transport<br/>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<br/>或 Gateway 执行请求"]
C1 --> D["构造 GoTaskServiceRequest<br/>或 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 负责实际执行与扩展
一句话概括:

View File

@ -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 {
<String, List<GatewayChatMessage>>{};
final Map<String, String> aiGatewayStreamingTextBySessionInternal =
<String, String>{};
final Map<String, String> singleAgentRuntimeModelBySessionInternal =
<String, String>{};
final Map<String, Map<String, dynamic>>
latestRoutingResolutionBySessionInternal =
<String, Map<String, dynamic>>{};
final Map<String, ExternalCodeAgentAcpSyncedProvider>
syncedGoAgentProvidersInternal = <String, ExternalCodeAgentAcpSyncedProvider>{};
final DesktopThreadArtifactService threadArtifactServiceInternal =

View File

@ -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<List<ExternalCodeAgentAcpSyncedProvider>>
buildExternalAcpSyncedProvidersInternal() async {
final providers = <ExternalCodeAgentAcpSyncedProvider>[];
@ -75,27 +75,4 @@ extension AppControllerDesktopGoAgentCoreRouting on AppController {
);
await goTaskServiceClientInternal.syncExternalProviders(providers);
}
void updateLatestRoutingResolutionInternal(
String sessionKey,
GoTaskServiceResult result,
) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
latestRoutingResolutionBySessionInternal[normalizedSessionKey] =
<String, dynamic>{
'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,
};
}
}

View File

@ -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<void> refreshAcpCapabilitiesRuntimeInternal(
@ -101,7 +101,7 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
next[provider] = DirectSingleAgentCapabilities(
available: true,
supportedProviders: <SingleAgentProvider>[provider],
endpoint: 'go-agent-core',
endpoint: 'go-task-service',
);
}
controller.singleAgentCapabilitiesByProviderInternal = next;

View File

@ -669,7 +669,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
);
}
Uri? resolveGoAgentCoreEndpointForTargetInternal(
Uri? resolveExternalAcpEndpointForTargetInternal(
AssistantExecutionTarget target,
) {
if (target == AssistantExecutionTarget.singleAgent) {

View File

@ -269,7 +269,6 @@ extension AppControllerDesktopSettings on AppController {
aiGatewayStreamingClientsInternal.clear();
aiGatewayPendingSessionKeysInternal.clear();
aiGatewayAbortedSessionKeysInternal.clear();
latestRoutingResolutionBySessionInternal.clear();
singleAgentExternalCliPendingSessionKeysInternal.clear();
assistantThreadTurnQueuesInternal.clear();
multiAgentRunPendingInternal = false;

View File

@ -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();
}

View File

@ -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,

View File

@ -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;

View File

@ -68,14 +68,6 @@ extension AppControllerDesktopThreadSessions on AppController {
);
}
Map<String, dynamic> latestRoutingResolutionForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
return latestRoutingResolutionBySessionInternal[normalizedSessionKey] ??
const <String, dynamic>{};
}
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(<String>[
latestResolvedEndpointTarget.isEmpty
? appText('OpenClaw Gateway', 'OpenClaw Gateway')
: latestResolvedEndpointTarget,
resolvedGatewayEntryState.isNotEmpty) {
final detail = switch (resolvedGatewayEntryState) {
'local' => joinConnectionPartsInternal(<String>[
appText('OpenClaw Gateway', 'OpenClaw Gateway'),
latestResolvedModel,
]),
'multi-agent' => joinConnectionPartsInternal(<String>[
appText('Multi-Agent', 'Multi-Agent'),
'remote' => joinConnectionPartsInternal(<String>[
appText('OpenClaw Gateway', 'OpenClaw Gateway'),
latestResolvedModel,
]),
_ => joinConnectionPartsInternal(<String>[
latestResolvedProviderId.isEmpty
? appText('Single Agent', 'Single Agent')
: latestResolvedProviderId,
singleAgentResolvedProviderForSession(normalizedSessionKey)
?.label
.isNotEmpty ==
true
? singleAgentResolvedProviderForSession(normalizedSessionKey)!.label
: appText('Single Agent', 'Single Agent'),
latestResolvedModel,
]),
};

View File

@ -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,

View File

@ -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<String, String> streamingTextBySessionInternal = <String, String>{};
final Map<String, Future<void>> threadTurnQueuesInternal =
<String, Future<void>>{};
final Map<String, String> singleAgentRuntimeModelBySessionInternal =
<String, String>{};
final WebTasksController tasksControllerInternal = WebTasksController();
String currentSessionKeyInternal = '';
String? lastAssistantErrorInternal;

View File

@ -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,

View File

@ -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();

View File

@ -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();

View File

@ -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() ??
'';
}

View File

@ -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';

View File

@ -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 = '';

View File

@ -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 <SingleAgentProvider>{},
raw = const <String, dynamic>{};
final bool singleAgent;
final bool multiAgent;
final Set<SingleAgentProvider> providers;
final Map<String, dynamic> 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<String, dynamic> toJson() {
return <String, dynamic>{
'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<String, dynamic> toJson() {
return <String, dynamic>{
'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 <GoAgentCoreAvailableSkill>[],
}) : mode = GoAgentCoreRoutingMode.auto,
explicitExecutionTarget = '',
explicitProviderId = '',
explicitModel = '',
explicitSkills = const <String>[],
allowSkillInstall = false,
installApproval = null;
final GoAgentCoreRoutingMode mode;
final String preferredGatewayTarget;
final String explicitExecutionTarget;
final String explicitProviderId;
final String explicitModel;
final List<String> explicitSkills;
final bool allowSkillInstall;
final List<GoAgentCoreAvailableSkill> availableSkills;
final GoAgentCoreSkillInstallApproval? installApproval;
bool get isAuto => mode == GoAgentCoreRoutingMode.auto;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'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<String> approvedSkillKeys;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'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<String> selectedSkills;
final List<GatewayChatAttachmentPayload> inlineAttachments;
final List<CollaborationAttachment> localAttachments;
final String aiGatewayBaseUrl;
final String aiGatewayApiKey;
final String agentId;
final Map<String, dynamic> 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<String, dynamic> toAcpParams() {
final resolvedRouting = effectiveRouting;
final params = <String, dynamic>{
'sessionId': sessionId,
'threadId': threadId,
'mode': mode,
'taskPrompt': prompt,
'workingDirectory': workingDirectory.trim(),
'selectedSkills': selectedSkills,
'attachments': <Map<String, dynamic>>[
...localAttachments.map(
(item) => <String, dynamic>{
'name': item.name,
'description': item.description,
'path': item.path,
},
),
...inlineAttachments.map(
(item) => <String, dynamic>{
'name': item.fileName,
'description': item.mimeType,
'path': '',
},
),
],
if (inlineAttachments.isNotEmpty)
'inlineAttachments': inlineAttachments
.map(
(item) => <String, dynamic>{
'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)) ...<String, dynamic>{
'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 <GoAgentCoreAvailableSkill>[],
);
}
}
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<String, dynamic> 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<String, dynamic> 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<String> get resolvedSkills {
final rawList = raw['resolvedSkills'];
if (rawList is! List) {
return const <String>[];
}
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<Map<String, dynamic>> get skillCandidates =>
_castMapList(raw['skillCandidates']);
List<Map<String, dynamic>> 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<void> syncProviders(List<GoAgentCoreSyncedProvider> providers);
Future<GoAgentCoreCapabilities> loadCapabilities({
required AssistantExecutionTarget target,
bool forceRefresh = false,
});
Future<GoAgentCoreRunResult> executeSession(
GoAgentCoreSessionRequest request, {
required void Function(GoAgentCoreSessionUpdate update) onUpdate,
});
Future<void> cancelSession({
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
});
Future<void> closeSession({
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
});
Future<void> dispose();
}
GoAgentCoreSessionUpdate? goAgentCoreUpdateFromNotification(
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> mergeGoAgentCoreResponseResult(
Map<String, dynamic> response,
Map<String, dynamic> overlay,
) {
if (overlay.isEmpty) {
return response;
}
final next = Map<String, dynamic>.from(response);
final result = Map<String, dynamic>.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<String, dynamic> _castMap(Object? value) {
if (value is Map<String, dynamic>) {
return value;
}
if (value is Map) {
return value.cast<String, dynamic>();
}
return const <String, dynamic>{};
}
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<Map<String, dynamic>> _castMapList(Object? raw) {
if (raw is! List) {
return const <Map<String, dynamic>>[];
}
return raw
.map((item) => _castMap(item))
.where((item) => item.isNotEmpty)
.toList(growable: false);
}

View File

@ -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<void> syncExternalProviders(
List<ExternalCodeAgentAcpSyncedProvider> providers,

View File

@ -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.

View File

@ -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<void> syncExternalProviders(
List<ExternalCodeAgentAcpSyncedProvider> 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;
}

View File

@ -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');

View File

@ -112,8 +112,8 @@ class FakeCodexRuntimeInternal extends CodexRuntime {
Future<void> 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<void> dispose() async {}
}
class FallbackOnlyGoAgentCoreClientInternal
extends FakeGoAgentCoreClientInternal {
FallbackOnlyGoAgentCoreClientInternal()
class FallbackOnlyGoTaskServiceClientInternal
extends FakeGoTaskServiceClientInternal {
FallbackOnlyGoTaskServiceClientInternal()
: super(capabilities: const ExternalCodeAgentAcpCapabilities.empty());
}

View File

@ -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 <SingleAgentProvider>[
@ -433,7 +433,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
});
final store = createStoreFromTempDirectoryInternal(tempDirectory);
final client = FallbackOnlyGoAgentCoreClientInternal();
final client = FallbackOnlyGoTaskServiceClientInternal();
final controller = await createAppControllerInternal(
store: store,
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
@ -502,7 +502,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
});
final store = createStoreFromTempDirectoryInternal(tempDirectory);
final client = FallbackOnlyGoAgentCoreClientInternal();
final client = FallbackOnlyGoTaskServiceClientInternal();
final controller = await createAppControllerInternal(
store: store,
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
@ -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,

View File

@ -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;

View File

@ -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<GatewayAcpException>().having(
(error) => error.code,
'code',
'GO_AGENT_CORE_ENDPOINT_MISSING',
'EXTERNAL_ACP_ENDPOINT_MISSING',
),
),
);

View File

@ -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: <String>['PPT', 'Browser Automation'],
inlineAttachments: <GatewayChatAttachmentPayload>[
GatewayChatAttachmentPayload(
type: 'inline',
fileName: 'note.txt',
mimeType: 'text/plain',
content: 'aGVsbG8=',
),
],
localAttachments: <CollaborationAttachment>[
CollaborationAttachment(
name: 'spec.md',
path: '/tmp/workspace/spec.md',
description: 'workspace spec',
),
],
aiGatewayBaseUrl: 'https://gateway.example.com',
aiGatewayApiKey: 'secret',
agentId: '',
metadata: <String, dynamic>{},
routing: GoAgentCoreRoutingConfig.auto(
preferredGatewayTarget: 'local',
availableSkills: <GoAgentCoreAvailableSkill>[
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'], <String>['PPT', 'Browser Automation']);
expect(params['attachments'], <Map<String, dynamic>>[
<String, dynamic>{
'name': 'spec.md',
'description': 'workspace spec',
'path': '/tmp/workspace/spec.md',
},
<String, dynamic>{
'name': 'note.txt',
'description': 'text/plain',
'path': '',
},
]);
expect(params['inlineAttachments'], <Map<String, dynamic>>[
<String, dynamic>{
'name': 'note.txt',
'mimeType': 'text/plain',
'content': 'aGVsbG8=',
'sizeBytes': 5,
},
]);
expect(params['routing'], <String, dynamic>{
'routingMode': 'auto',
'preferredGatewayTarget': 'local',
'explicitSkills': const <String>[],
'allowSkillInstall': false,
'availableSkills': <Map<String, dynamic>>[
<String, dynamic>{
'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: <String>['PPTX'],
inlineAttachments: <GatewayChatAttachmentPayload>[],
localAttachments: <CollaborationAttachment>[],
aiGatewayBaseUrl: '',
aiGatewayApiKey: '',
agentId: '',
metadata: <String, dynamic>{},
provider: SingleAgentProvider.opencode,
);
final params = request.toAcpParams();
expect(params['routing'], <String, dynamic>{
'routingMode': 'explicit',
'preferredGatewayTarget': 'local',
'explicitExecutionTarget': 'singleAgent',
'explicitProviderId': 'opencode',
'explicitModel': 'codex-sonnet',
'explicitSkills': const <String>['PPTX'],
'allowSkillInstall': false,
'availableSkills': const <Map<String, dynamic>>[],
});
});
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: <String>[],
inlineAttachments: <GatewayChatAttachmentPayload>[],
localAttachments: <CollaborationAttachment>[],
aiGatewayBaseUrl: '',
aiGatewayApiKey: '',
agentId: 'agent-1',
metadata: <String, dynamic>{'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'], <String, dynamic>{
'routingMode': 'explicit',
'preferredGatewayTarget': 'local',
'explicitExecutionTarget': 'local',
'explicitSkills': const <String>[],
'allowSkillInstall': false,
'availableSkills': const <Map<String, dynamic>>[],
});
});
test(
'run result prefers completion text and preserves resolved workspace',
() {
final result = goAgentCoreRunResultFromResponse(
<String, dynamic>{
'result': <String, dynamic>{
'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(<String, dynamic>{
'method': 'session.update',
'params': <String, dynamic>{
'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);
});
});
}

View File

@ -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();
}

View File

@ -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: <String>['PPT', 'Browser Automation'],
inlineAttachments: <GatewayChatAttachmentPayload>[
GatewayChatAttachmentPayload(
type: 'inline',
fileName: 'note.txt',
mimeType: 'text/plain',
content: 'aGVsbG8=',
),
],
localAttachments: <CollaborationAttachment>[
CollaborationAttachment(
name: 'spec.md',
path: '/tmp/workspace/spec.md',
description: 'workspace spec',
),
],
aiGatewayBaseUrl: 'https://gateway.example.com',
aiGatewayApiKey: 'secret',
agentId: '',
metadata: <String, dynamic>{},
routing: ExternalCodeAgentAcpRoutingConfig.auto(
preferredGatewayTarget: 'local',
availableSkills: <ExternalCodeAgentAcpAvailableSkill>[
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'], <String>['PPT', 'Browser Automation']);
expect(params['attachments'], <Map<String, dynamic>>[
<String, dynamic>{
'name': 'spec.md',
'description': 'workspace spec',
'path': '/tmp/workspace/spec.md',
},
<String, dynamic>{
'name': 'note.txt',
'description': 'text/plain',
'path': '',
},
]);
expect(params['inlineAttachments'], <Map<String, dynamic>>[
<String, dynamic>{
'name': 'note.txt',
'mimeType': 'text/plain',
'content': 'aGVsbG8=',
'sizeBytes': 5,
},
]);
expect(params['routing'], <String, dynamic>{
'routingMode': 'auto',
'preferredGatewayTarget': 'local',
'explicitSkills': const <String>[],
'allowSkillInstall': false,
'availableSkills': <Map<String, dynamic>>[
<String, dynamic>{
'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: <String>['PPTX'],
inlineAttachments: <GatewayChatAttachmentPayload>[],
localAttachments: <CollaborationAttachment>[],
aiGatewayBaseUrl: '',
aiGatewayApiKey: '',
agentId: '',
metadata: <String, dynamic>{},
provider: SingleAgentProvider.opencode,
);
final params = request.toExternalAcpParams();
expect(params['routing'], <String, dynamic>{
'routingMode': 'explicit',
'preferredGatewayTarget': 'local',
'explicitExecutionTarget': 'singleAgent',
'explicitProviderId': 'opencode',
'explicitModel': 'codex-sonnet',
'explicitSkills': const <String>['PPTX'],
'allowSkillInstall': false,
'availableSkills': const <Map<String, dynamic>>[],
});
});
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: <String>[],
inlineAttachments: <GatewayChatAttachmentPayload>[],
localAttachments: <CollaborationAttachment>[],
aiGatewayBaseUrl: '',
aiGatewayApiKey: '',
agentId: 'agent-1',
metadata: <String, dynamic>{'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'], <String, dynamic>{
'routingMode': 'explicit',
'preferredGatewayTarget': 'local',
'explicitExecutionTarget': 'local',
'explicitSkills': const <String>[],
'allowSkillInstall': false,
'availableSkills': const <Map<String, dynamic>>[],
});
},
);
test(
'run result prefers completion text and preserves resolved workspace',
() {
final result = goTaskServiceResultFromAcpResponse(
<String, dynamic>{
'result': <String, dynamic>{
'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(<String, dynamic>{
'method': 'session.update',
'params': <String, dynamic>{
'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);
});
});
}

View File

@ -19,7 +19,7 @@ void main() {
];
const guardedFiles = <String>[
'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',
);
}