Refactor thread state and runtime naming alignment
This commit is contained in:
parent
abe3815d2e
commit
0e0ec2fbc2
@ -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 差异
|
||||
|
||||
@ -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` 组织。
|
||||
|
||||
|
||||
@ -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"]
|
||||
|
||||
|
||||
@ -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(主体区域 同步显示)"]
|
||||
|
||||
@ -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 负责实际执行与扩展
|
||||
|
||||
一句话概括:
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -669,7 +669,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
);
|
||||
}
|
||||
|
||||
Uri? resolveGoAgentCoreEndpointForTargetInternal(
|
||||
Uri? resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget target,
|
||||
) {
|
||||
if (target == AssistantExecutionTarget.singleAgent) {
|
||||
|
||||
@ -269,7 +269,6 @@ extension AppControllerDesktopSettings on AppController {
|
||||
aiGatewayStreamingClientsInternal.clear();
|
||||
aiGatewayPendingSessionKeysInternal.clear();
|
||||
aiGatewayAbortedSessionKeysInternal.clear();
|
||||
latestRoutingResolutionBySessionInternal.clear();
|
||||
singleAgentExternalCliPendingSessionKeysInternal.clear();
|
||||
assistantThreadTurnQueuesInternal.clear();
|
||||
multiAgentRunPendingInternal = false;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
]),
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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() ??
|
||||
'';
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 = '';
|
||||
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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');
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user