From 0342841f7c2eba51468764809645357801e79cd1 Mon Sep 17 00:00:00 2001 From: limityan Date: Fri, 14 Aug 2026 13:03:41 +0800 Subject: [PATCH] perf(app-server): move TypeScript schemas to protocol owner --- Cargo.lock | 4 - docs/architecture/app-server-architecture.md | 7 + docs/performance/01-compile-performance.md | 36 +- scripts/check-core-boundaries.test.mjs | 23 + .../core-boundaries/rules/feature-rules.mjs | 27 +- src/apps/cli/src/agent/tui_client.rs | 22 +- src/apps/cli/src/shared_tui_backend.rs | 8 +- src/crates/interfaces/AGENTS-CN.md | 3 + src/crates/interfaces/AGENTS.md | 4 +- .../interfaces/app-server-protocol/Cargo.toml | 7 +- .../app-server-protocol/src/config.rs | 110 +++ .../interfaces/app-server-protocol/src/lib.rs | 4 +- .../app-server-protocol/src/schemas/agent.rs | 166 +++- .../app-server-protocol/src/schemas/event.rs | 9 + .../app-server-protocol/src/schemas/git.rs | 107 +++ .../src/schemas}/i18n.rs | 13 +- .../app-server-protocol/src/schemas/mod.rs | 3 + .../src/schemas}/permission.rs | 41 +- .../src/schemas/session.rs | 63 +- .../tests/legacy_wire_contracts.rs | 84 ++ src/crates/interfaces/app-server/AGENTS-CN.md | 15 +- src/crates/interfaces/app-server/AGENTS.md | 18 +- src/crates/interfaces/app-server/Cargo.toml | 18 +- .../interfaces/app-server/src/client.rs | 781 ------------------ src/crates/interfaces/app-server/src/lib.rs | 32 +- .../interfaces/app-server/src/schema/agent.rs | 234 ------ .../interfaces/app-server/src/schema/app.rs | 3 - .../app-server/src/schema/config.rs | 111 --- .../app-server/src/schema/events.rs | 127 --- .../interfaces/app-server/src/schema/git.rs | 47 -- .../interfaces/app-server/src/schema/mod.rs | 30 +- .../app-server/src/schema/session.rs | 209 ----- .../interfaces/app-server/src/server.rs | 1 + .../app-server/src/server/event_forwarder.rs | 7 +- .../app-server/src/server/handlers/agent.rs | 9 +- .../app-server/src/server/handlers/config.rs | 21 +- .../app-server/src/server/handlers/git.rs | 7 +- .../app-server/src/server/handlers/session.rs | 46 +- .../interfaces/app-server/src/server/wire.rs | 381 +++++++++ .../app-server/tests/agent_kernel.rs | 52 -- src/web-ui/package.json | 2 +- src/web-ui/scripts/gen-api-barrel.mjs | 28 +- .../api/adapters/websocket-adapter.ts | 2 +- 43 files changed, 1125 insertions(+), 1797 deletions(-) create mode 100644 src/crates/interfaces/app-server-protocol/src/schemas/git.rs rename src/crates/interfaces/{app-server/src/schema => app-server-protocol/src/schemas}/i18n.rs (78%) rename src/crates/interfaces/{app-server/src/schema => app-server-protocol/src/schemas}/permission.rs (58%) create mode 100644 src/crates/interfaces/app-server-protocol/tests/legacy_wire_contracts.rs delete mode 100644 src/crates/interfaces/app-server/src/client.rs delete mode 100644 src/crates/interfaces/app-server/src/schema/agent.rs delete mode 100644 src/crates/interfaces/app-server/src/schema/app.rs delete mode 100644 src/crates/interfaces/app-server/src/schema/config.rs delete mode 100644 src/crates/interfaces/app-server/src/schema/events.rs delete mode 100644 src/crates/interfaces/app-server/src/schema/git.rs delete mode 100644 src/crates/interfaces/app-server/src/schema/session.rs create mode 100644 src/crates/interfaces/app-server/src/server/wire.rs diff --git a/Cargo.lock b/Cargo.lock index 54d562583f..33b1566f33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -908,17 +908,13 @@ dependencies = [ "bitfun-app-server-client", "bitfun-app-server-protocol", "bitfun-core", - "bitfun-events", "bitfun-product-domains", "bitfun-runtime-ports", - "futures", "log", "serde", "serde_json", "tokio", - "tokio-util", "tracing", - "ts-rs", "url", ] diff --git a/docs/architecture/app-server-architecture.md b/docs/architecture/app-server-architecture.md index a459383461..8cf3a63e62 100644 --- a/docs/architecture/app-server-architecture.md +++ b/docs/architecture/app-server-architecture.md @@ -388,6 +388,13 @@ Embedded direct invocation 可以依赖同进程构造身份,但仍必须传 - server wiring 可以依赖生产 handler 所需的明确 owner feature,但禁止选择 `bitfun-core/product-full`。 - 新 domain 只能增加真实 handler 所需的最窄 owner feature,并通过边界检查证明依赖方向。 - protocol DTO 不复制 Runtime 内部对象;只暴露 Rich Client 需要的稳定字段和 read model。 +- `app-server-protocol` 是 TypeScript wire schema 的唯一导出 owner;`app-server/ts` + 仅保留为兼容转发。owner 对象到 wire read model 的转换留在 server adapter,不能为了 + 生成类型把 Core、Agent Runtime 或 Service 实现依赖下沉到 protocol。 +- `app-server::schema` 只保留对 protocol 类型的兼容 re-export;正式 typed client 由 + `app-server-client` 单独拥有,server crate 不维护第二套隐藏 client。该 re-export 保证 + 已有平铺类型路径和 wire shape,不承诺保留 server-only 的 inherent/`From` 转换 helper; + 这些内部调用应迁到 server adapter 或稳定 contract 方法。 - transport 实现留在 Host/adapter 边界,generic role/transport helper 保持 schema-free。 - App Server 不持有第二份 Session、Permission、Config 或 capability 权威状态。 diff --git a/docs/performance/01-compile-performance.md b/docs/performance/01-compile-performance.md index 274959bdc1..f0996738b0 100644 --- a/docs/performance/01-compile-performance.md +++ b/docs/performance/01-compile-performance.md @@ -1,8 +1,8 @@ # BitFun 编译与依赖治理计划 -> 最近核实:2026-08-13 +> 最近核实:2026-08-14 > -> 实现复核基线:`gcwing/main@a4d944e5b` +> 实现复核基线:`gcwing/main@76f8b89e2` > > 性能 A/B 基线:`gcwing/main@1f538b96d` > @@ -312,7 +312,7 @@ Clap、Tracing Subscriber、Notify 等默认即产品契约或缺少独立收益 |---|---|---| | 已稳定 | 根 `Cargo.lock`、Reqwest Rustls 单栈、workspace Tokio 最小基线 | 不重复治理 | | 本轮完成 | Core 空默认与 capability-local 工具依赖、ACP client/server 角色、Core Agent Runtime capability、文档转换与订阅认证 modifier、SDK Host 显式 owner closure、Installer/CLI/Desktop/Core/MiniApp Market/Page Function 未使用直接依赖 | 以真实入口 closure 收敛,不建立新的产品 umbrella;根 lock 不增加 package | -| 当前不动 | App Server / Server | 只为保持现有 handler 编译显式声明其已消费的 Core owner;不在改造稳定前继续拆其生产路径 | +| 本轮完成 | App Server schema owner | wire DTO 与 TypeScript 导出收敛到 protocol;server 只保留 handler 与 owner-to-wire 转换,正式 client 继续由独立 client crate 持有 | | 明确保留 | Desktop screenshots backend | 替换方案必须同时保持三平台坐标/权限/区域捕获语义且不增加根 lock package;当前候选不满足 | | 明确保留 | `portable-pty 0.8/0.9` | 非 OHOS 与 OHOS 的平台兼容选择,不为去重破坏 | @@ -353,7 +353,33 @@ consumer 零影响。完整 Rust Runtime SDK 同时将兼容版本提升为 v6 这里仍只报告依赖图和 focused-test 输入,不宣称完整产品 wall-clock 提速。根 `Cargo.lock` package 集合与字节均不变,新增/升级/降级 package 为 0;`.github` 和 `ci.yml` 不变。 -### 3.3 CI 与本地验证 +### 3.3 App Server TypeScript schema owner + +变更前,Web `gen:types` 先编译 protocol,再编译完整 `bitfun-app-server`,后者会把 +Core、Agent Runtime 和具体 Service handler 闭包带入前端类型生成。最新同代码状态的 +Frontend Build 样本中,`gen:types` 约 430 秒,占 `build:web` 约 484 秒的 88.8%;其中 +protocol 导出约 42.58 秒,App Server 导出约 6 分 26 秒。单次 CI 样本只用于定位热点, +不能独立证明合入后的 wall-clock 收益。 + +本轮将 behavior-light wire DTO 和 TS derive 统一到 `bitfun-app-server-protocol`,server 端 +保留 Core/Runtime/Service owner 到 wire read model 的显式转换。既有 `app-server::schema` +继续兼容 re-export,`bitfun-app-server/ts` 继续兼容转发;隐藏且无生产 consumer 的第二套 +client 删除,正式 client 仍由 `bitfun-app-server-client` 拥有。兼容面是已有平铺类型路径、 +method 和 wire shape;旧 server-only inherent/`From` helper 不是版本化公共 SDK,仓内调用已 +迁入 adapter 或稳定 contract 方法。三平台 +`normal,build,dev` 版本化 package instance 去重结果如下: + +| TS 导出闭包 | Windows | macOS | Linux | +|---|---:|---:|---:| +| 变更前:完整 App Server | 433 | 429 | 437 | +| 变更后:App Server Protocol | 153 | 159 | 159 | +| 收敛 | -280 | -270 | -278 | + +Web 实际引用的 19 个直接类型及其递归导入闭包保持生成内容字节一致;不再把 95 个历史 +导出文件全部视为兼容面。根 `Cargo.lock` package 集合和版本不变,只从 App Server package +记录删除不再直接消费的依赖边;`.github`、CI job 和矩阵均不变。 + +### 3.4 CI 与本地验证 - 现有 CI 已覆盖 workspace check、Core/Desktop lib、平台敏感 owner 测试和独立 runtime/CLI 验证;本轮不新增 job、矩阵或 changed-path 分类器。 - CI 不负责穷举所有测试;新增验证只有具备独立 owner、平台矩阵或失败归因价值时才进入既有流水线,否则由最近模块的 focused command 维护。 @@ -393,7 +419,7 @@ consumer 零影响。完整 Rust Runtime SDK 同时将兼容版本提升为 v6 |---|---| | CI 收敛 | 先积累多次相同 owner 的 step wall-clock、cache hit/miss 和失败历史;只有能证明收益且不会静默缩小覆盖时再独立设计 | | Desktop 截图后端 | 新候选同时满足三平台行为等价、区域捕获无性能回退、系统依赖可 feature-gate,且根 lock package 不增加 | -| App Server / Server | 当前改造合入并稳定后,重新锁定最新生产调用链和可信 owner 边界 | +| App Server / Server | 观察 protocol 单一 schema owner 合入后的 Frontend Build 样本;没有新的生产 owner 或稳定行为收益前,不继续拆 handler 路径 | | 其他产品入口重型 capability | 证明入口不消费该能力,具备 typed unsupported/fallback 行为,并能让一个真实重依赖子图退出 | | 重复 native/sys 库版本 | 同一 owner 能升级收敛且三平台打包/ABI 有证据;不因版本数字重复强行 patch | diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index 7e78f01f92..9f5b3bd30e 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -64,6 +64,29 @@ const MODULES = [ const TEST_ROOT = join('C:', 'repo'); +test('App Server TypeScript capability is owned by the protocol crate', () => { + const appServerTs = coreClosedFeatureProfileRules.find( + (rule) => rule.manifestPath === 'src/crates/interfaces/app-server/Cargo.toml' + && rule.featureName === 'ts', + ); + assert.deepEqual(appServerTs?.requiredFeatureRefs, [ + 'bitfun-app-server-protocol/ts', + ]); + assert.equal(appServerTs?.exact, true); + + const protocolTs = coreClosedFeatureProfileRules.find( + (rule) => rule.manifestPath === 'src/crates/interfaces/app-server-protocol/Cargo.toml' + && rule.featureName === 'ts', + ); + assert.deepEqual(protocolTs?.requiredFeatureRefs, [ + 'bitfun-core-types/ts', + 'bitfun-product-domains/ts', + 'bitfun-runtime-ports/ts', + 'dep:ts-rs', + ]); + assert.equal(protocolTs?.exact, true); +}); + test('Agent Runtime leaf capabilities have one managed feature and source contract', async () => { const rule = capabilityContractDependencyRules.find( (candidate) => candidate.packageName === 'bitfun-agent-runtime', diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index 5dd1834eff..5e87f618b6 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -385,13 +385,11 @@ export const capabilityContractDependencyRules = [ capabilityEdge(['agent-api', 'git-port']), ])], ['bitfun-agent-tools', capabilityConsumer([capabilityEdge()])], - ['bitfun-app-server', capabilityConsumer( - [capabilityEdge(['agent-api'])], + ['bitfun-app-server', capabilityConsumer([capabilityEdge(['agent-api'])])], + ['bitfun-app-server-protocol', capabilityConsumer( + [capabilityEdge(['agent-api', 'git-port'])], [capabilityForwarder('ts', 'ts')], )], - ['bitfun-app-server-protocol', capabilityConsumer([ - capabilityEdge(['agent-api', 'git-port']), - ])], ['bitfun-cli', capabilityConsumer([ capabilityEdge(['agent-api', 'git-port', 'permission', 'plugin-runtime', 'workspace-ports']), ])], @@ -744,6 +742,25 @@ export const acpClosedFeatureProfileRules = [ ]; export const coreClosedFeatureProfileRules = [ + { + manifestPath: 'src/crates/interfaces/app-server/Cargo.toml', + featureName: 'ts', + requiredFeatureRefs: ['bitfun-app-server-protocol/ts'], + exact: true, + reason: 'App Server must delegate TypeScript wire export to the protocol owner', + }, + { + manifestPath: 'src/crates/interfaces/app-server-protocol/Cargo.toml', + featureName: 'ts', + requiredFeatureRefs: [ + 'bitfun-core-types/ts', + 'bitfun-product-domains/ts', + 'bitfun-runtime-ports/ts', + 'dep:ts-rs', + ], + exact: true, + reason: 'App Server Protocol must own the complete behavior-light TypeScript wire surface', + }, { manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'default', diff --git a/src/apps/cli/src/agent/tui_client.rs b/src/apps/cli/src/agent/tui_client.rs index 03b521f71a..527508dfd5 100644 --- a/src/apps/cli/src/agent/tui_client.rs +++ b/src/apps/cli/src/agent/tui_client.rs @@ -34,16 +34,16 @@ use bitfun_runtime_ports::{ put_agent_workspace_references, AgentContextReloadRequest, AgentDialogSteerRequest, AgentDialogTurnExecution, AgentDialogTurnRequest, AgentInputAttachment, AgentMessageWorkspaceReferencesRequest, AgentSessionCompactionRequest, - AgentSessionCreateRequest, AgentSessionDeleteRequest, - AgentSessionLineageCancellationRequest, AgentSessionLineageInspection, - AgentSessionLineageRequest, AgentSessionLineageSnapshot, AgentSessionLineageTranscriptRequest, - AgentSessionListRequest, AgentSessionModeUpdateRequest, AgentSessionModelUpdateRequest, - AgentSessionRenameRequest, AgentSessionRevertRequest, AgentSessionRevertResult, - AgentSessionSummary, AgentSessionUsageRequest, AgentSessionWorkspaceBinding, - AgentSubmissionSource, AgentTurnCancellationRequest, AgentTurnCancellationResult, - AgentTurnSettlementRequest, AgentUserShellCommandRequest, AgentWorkspaceReference, - AgentWorkspaceReferenceSearchRequest, AgentWorkspaceReferenceSearchResult, - DialogSubmissionPolicy, SessionExecutionTarget, SessionTranscript, WorkspaceDiffSnapshot, + AgentSessionCreateRequest, AgentSessionDeleteRequest, AgentSessionLineageCancellationRequest, + AgentSessionLineageInspection, AgentSessionLineageRequest, AgentSessionLineageSnapshot, + AgentSessionLineageTranscriptRequest, AgentSessionListRequest, AgentSessionModeUpdateRequest, + AgentSessionModelUpdateRequest, AgentSessionRenameRequest, AgentSessionRevertRequest, + AgentSessionRevertResult, AgentSessionSummary, AgentSessionUsageRequest, + AgentSessionWorkspaceBinding, AgentSubmissionSource, AgentTurnCancellationRequest, + AgentTurnCancellationResult, AgentTurnSettlementRequest, AgentUserShellCommandRequest, + AgentWorkspaceReference, AgentWorkspaceReferenceSearchRequest, + AgentWorkspaceReferenceSearchResult, DialogSubmissionPolicy, SessionExecutionTarget, + SessionTranscript, WorkspaceDiffSnapshot, }; use tokio::sync::{broadcast, Mutex}; @@ -1358,7 +1358,7 @@ impl TuiAgentClient { .map_err(|error| anyhow::anyhow!(error.message))?; let result = self .backend - .submit_dialog_turn(SubmitDialogTurnRequest(AgentDialogTurnRequest { + .submit_dialog_turn(SubmitDialogTurnRequest::from(AgentDialogTurnRequest { session_id: session_id.clone(), message, original_message, diff --git a/src/apps/cli/src/shared_tui_backend.rs b/src/apps/cli/src/shared_tui_backend.rs index 8c7d3871cf..f4c515e4d8 100644 --- a/src/apps/cli/src/shared_tui_backend.rs +++ b/src/apps/cli/src/shared_tui_backend.rs @@ -42,7 +42,7 @@ use bitfun_app_server_protocol::worktree::*; use bitfun_app_server_protocol::{MIN_PROTOCOL_VERSION, PROTOCOL_VERSION}; use bitfun_runtime_ports::{ AgentSessionCompactionResult, AgentSessionForkResult, AgentSessionWorkspaceBinding, - AgentUserShellCommandResult, + AgentSubmissionSource, AgentUserShellCommandResult, DialogSubmissionPolicy, }; use tokio::sync::broadcast; @@ -421,7 +421,11 @@ impl TuiBackend for SharedTuiBackend { request: SubmitDialogTurnRequest, ) -> Result { match self - .request(RuntimeIpcOperation::SubmitTurn { request: request.0 }) + .request(RuntimeIpcOperation::SubmitTurn { + request: request.0.to_request(DialogSubmissionPolicy::for_source( + AgentSubmissionSource::Cli, + )), + }) .await? { RuntimeIpcOperationResult::TurnAccepted { diff --git a/src/crates/interfaces/AGENTS-CN.md b/src/crates/interfaces/AGENTS-CN.md index 3faaaeb2aa..410e5a7568 100644 --- a/src/crates/interfaces/AGENTS-CN.md +++ b/src/crates/interfaces/AGENTS-CN.md @@ -9,6 +9,9 @@ | Crate | 职责 | 本地文档 | |---|---|---| | `acp` | 基于已组装产品 runtime 的 Agent Client Protocol 接口 | [AGENTS.md](acp/AGENTS.md) | +| `app-server-protocol` | behavior-light App Server method、wire DTO、错误、事件和 protocol role | [AGENTS.md](app-server/AGENTS.md) | +| `app-server-client` | transport-agnostic 类型化 App Server client 和事件分发 | [AGENTS.md](app-server/AGENTS.md) | +| `app-server` | 生产 App Server handler 注册、生命周期和 owner-to-wire adapter | [AGENTS.md](app-server/AGENTS.md) | | `sdk-host` | 版本化的本地 Agent SDK Host 协议与连接用例;进程启动和 stdio framing 仍由 `src/apps/sdk-host` 负责 | — | ## 放置规则 diff --git a/src/crates/interfaces/AGENTS.md b/src/crates/interfaces/AGENTS.md index 231ecea2ce..6bb083499b 100644 --- a/src/crates/interfaces/AGENTS.md +++ b/src/crates/interfaces/AGENTS.md @@ -12,7 +12,9 @@ product behavior. UI apps and delivery hosts remain under `src/apps`, | Crate | Responsibility | Local doc | |---|---|---| | `acp` | Agent Client Protocol interface over the assembled product runtime | [AGENTS.md](acp/AGENTS.md) | -| `app-server` | Protocol-agnostic JSON-RPC app-server/client scaffold over `agent_client_protocol` custom roles | [AGENTS.md](app-server/AGENTS.md) | +| `app-server-protocol` | Behavior-light App Server methods, wire DTOs, errors, events, and protocol roles | [AGENTS.md](app-server/AGENTS.md) | +| `app-server-client` | Transport-agnostic typed App Server client and event distribution | [AGENTS.md](app-server/AGENTS.md) | +| `app-server` | Production App Server handler registration, lifecycle, and owner-to-wire adapters | [AGENTS.md](app-server/AGENTS.md) | | `sdk-host` | Versioned local Agent SDK Host protocol and connection use cases; process bootstrap and stdio framing remain in `src/apps/sdk-host` | — | ## Placement Rules diff --git a/src/crates/interfaces/app-server-protocol/Cargo.toml b/src/crates/interfaces/app-server-protocol/Cargo.toml index f758c59ee3..2503e45d93 100644 --- a/src/crates/interfaces/app-server-protocol/Cargo.toml +++ b/src/crates/interfaces/app-server-protocol/Cargo.toml @@ -20,7 +20,12 @@ ts-rs = { workspace = true, optional = true } [features] default = [] -ts = ["dep:ts-rs"] +ts = [ + "dep:ts-rs", + "bitfun-core-types/ts", + "bitfun-product-domains/ts", + "bitfun-runtime-ports/ts", +] [lints] workspace = true diff --git a/src/crates/interfaces/app-server-protocol/src/config.rs b/src/crates/interfaces/app-server-protocol/src/config.rs index c30fc57dba..741cd73c0c 100644 --- a/src/crates/interfaces/app-server-protocol/src/config.rs +++ b/src/crates/interfaces/app-server-protocol/src/config.rs @@ -5,6 +5,116 @@ use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; + +/// API view of a mode configuration. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(default)] +pub struct AgentProfileView { + pub profile_id: String, + pub enabled_tools: Vec, + pub default_tools: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub disabled_user_skills: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub enabled_user_skills: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[request(method = "config/getAgentProfileConfigs", response = GetAgentProfileConfigsResponse)] +pub struct GetAgentProfileConfigsMessage {} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct GetAgentProfileConfigsResponse { + pub profiles: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[request(method = "config/getAgentProfileConfig", response = GetAgentProfileConfigResponse)] +pub struct GetAgentProfileConfigMessage { + pub agent_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct GetAgentProfileConfigResponse(pub AgentProfileView); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[request(method = "config/getModelConfigs", response = GetModelConfigsResponse)] +pub struct GetModelConfigsMessage {} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct GetModelConfigsResponse { + pub models: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[serde(rename_all = "camelCase")] +#[request(method = "config/getConfig", response = GetConfigResponse)] +pub struct GetConfigMessage { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(default, skip_serializing_if = "skip_if_false")] + pub skip_retry_on_not_found: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct GetConfigResponse(pub serde_json::Value); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[serde(rename_all = "camelCase")] +#[request(method = "config/getConfigs", response = GetConfigsResponse)] +pub struct GetConfigsMessage { + pub paths: Vec, + #[serde(default, skip_serializing_if = "skip_if_false")] + pub skip_retry_on_not_found: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct GetConfigsResponse { + pub configs: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[serde(rename_all = "camelCase")] +#[request(method = "config/setConfig", response = SetConfigResponse)] +pub struct SetConfigMessage { + pub path: String, + pub value: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct SetConfigResponse {} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(rename_all = "camelCase")] +#[request(method = "config/setAgentProfileConfig", response = SetAgentProfileConfigResponse)] +pub struct SetAgentProfileConfigMessage { + pub agent_id: String, + pub config: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct SetAgentProfileConfigResponse(pub AgentProfileView); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(rename_all = "camelCase")] +#[request(method = "config/resetAgentProfileConfig", response = ResetAgentProfileConfigResponse)] +pub struct ResetAgentProfileConfigMessage { + pub agent_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct ResetAgentProfileConfigResponse(pub AgentProfileView); + +fn skip_if_false(value: &bool) -> bool { + !*value +} #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] diff --git a/src/crates/interfaces/app-server-protocol/src/lib.rs b/src/crates/interfaces/app-server-protocol/src/lib.rs index 7f96360609..dc16279890 100644 --- a/src/crates/interfaces/app-server-protocol/src/lib.rs +++ b/src/crates/interfaces/app-server-protocol/src/lib.rs @@ -16,8 +16,8 @@ pub use role::{AppClient, AppServer}; // Keep the established public domain paths while the schema sources live in a // single physical directory. pub use schemas::{ - account, agent, app, error, event, external_source, hook, mcp, method, model, session, skill, - subagent, workspace, worktree, + account, agent, app, error, event, external_source, git, hook, i18n, mcp, method, model, + permission, session, skill, subagent, workspace, worktree, }; /// Current App Server protocol version. diff --git a/src/crates/interfaces/app-server-protocol/src/schemas/agent.rs b/src/crates/interfaces/app-server-protocol/src/schemas/agent.rs index 3bc4b8cd52..44cb3eae53 100644 --- a/src/crates/interfaces/app-server-protocol/src/schemas/agent.rs +++ b/src/crates/interfaces/app-server-protocol/src/schemas/agent.rs @@ -3,10 +3,12 @@ use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; use bitfun_product_domains::tool_permissions::{PermissionReply, PermissionRequest}; use bitfun_runtime_ports::{ - AgentDialogSteerRequest, AgentDialogTurnRequest, AgentSessionCreateRequest, - AgentSessionCreateResult, AgentSessionDeleteRequest, AgentSessionListRequest, - AgentSessionSummary, AgentTurnCancellationRequest, AgentTurnCancellationResult, - AgentUserShellCommandRequest, AgentUserShellCommandResult, DialogSubmitOutcome, + AgentDialogSteerRequest, AgentDialogTurnExecution, AgentDialogTurnRequest, + AgentInputAttachment, AgentSessionCreateRequest, AgentSessionCreateResult, + AgentSessionDeleteRequest, AgentSessionListRequest, AgentSessionSummary, + AgentSubmissionRequest, AgentSubmissionResult, AgentSubmissionSource, + AgentTurnCancellationRequest, AgentTurnCancellationResult, AgentUserShellCommandRequest, + AgentUserShellCommandResult, DialogSubmissionPolicy, }; use serde::{Deserialize, Serialize}; @@ -48,6 +50,8 @@ pub struct AgentModeSummary { pub struct ListSessionsRequest(pub AgentSessionListRequest); #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +/// `agent/listSessions` response body. pub struct ListSessionsResponse { pub sessions: Vec, } @@ -92,37 +96,94 @@ unit_response!(DeleteSessionResponse); #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] #[request(method = "agent/submitDialogTurn", response = SubmitDialogTurnResponse)] -pub struct SubmitDialogTurnRequest(pub AgentDialogTurnRequest); +pub struct SubmitDialogTurnRequest(pub SubmitDialogTurnBody); + +pub use SubmitDialogTurnRequest as SubmitDialogTurnMessage; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(rename_all = "camelCase")] +/// Wire form of [`AgentDialogTurnRequest`] with an optional `policy`. +pub struct SubmitDialogTurnBody { + pub session_id: String, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub original_message: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub turn_id: Option, + #[serde(default, skip_serializing_if = "AgentDialogTurnExecution::is_standard")] + pub execution: AgentDialogTurnExecution, + pub agent_type: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_connection_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_ssh_host: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub policy: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub attachments: Vec, + #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] + pub metadata: serde_json::Map, +} + +impl SubmitDialogTurnBody { + /// Project this wire body into the runtime contract using the adapter's + /// explicit fallback policy when the legacy payload omits one. + pub fn to_request(self, default_policy: DialogSubmissionPolicy) -> AgentDialogTurnRequest { + AgentDialogTurnRequest { + session_id: self.session_id, + message: self.message, + original_message: self.original_message, + turn_id: self.turn_id, + execution: self.execution, + agent_type: self.agent_type, + workspace_path: self.workspace_path, + remote_connection_id: self.remote_connection_id, + remote_ssh_host: self.remote_ssh_host, + policy: self.policy.unwrap_or(default_policy), + reply_route: None, + prepended_reminders: Vec::new(), + attachments: self.attachments, + metadata: self.metadata, + } + } +} + +impl From for SubmitDialogTurnRequest { + fn from(request: AgentDialogTurnRequest) -> Self { + // The App Server wire contract predates reply routing and prepended + // reminders. Keep the currently effective behavior: those runtime-only + // fields are not sent over this compatibility method. + Self(SubmitDialogTurnBody { + session_id: request.session_id, + message: request.message, + original_message: request.original_message, + turn_id: request.turn_id, + execution: request.execution, + agent_type: request.agent_type, + workspace_path: request.workspace_path, + remote_connection_id: request.remote_connection_id, + remote_ssh_host: request.remote_ssh_host, + policy: Some(request.policy), + attachments: request.attachments, + metadata: request.metadata, + }) + } +} #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[serde(rename_all = "camelCase", tag = "status")] +/// `agent/submitDialogTurn` response body. pub enum SubmitDialogTurnResponse { Started { session_id: String, turn_id: String }, Queued { session_id: String, turn_id: String }, } -impl From for SubmitDialogTurnResponse { - fn from(outcome: DialogSubmitOutcome) -> Self { - match outcome { - DialogSubmitOutcome::Started { - session_id, - turn_id, - } => Self::Started { - session_id, - turn_id, - }, - DialogSubmitOutcome::Queued { - session_id, - turn_id, - } => Self::Queued { - session_id, - turn_id, - }, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "agent/respondPermission", response = RespondPermissionResponse)] pub struct RespondPermissionRequest { pub request_id: String, @@ -146,3 +207,56 @@ pub struct CancelTurnRequest(pub AgentTurnCancellationRequest); #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] pub struct CancelTurnResponse(pub AgentTurnCancellationResult); + +pub use CancelTurnRequest as CancelTurnMessage; +pub use CreateSessionRequest as CreateSessionMessage; +pub use DeleteSessionRequest as DeleteSessionMessage; +pub use ListSessionsRequest as ListSessionsMessage; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[request(method = "agent/submitTurn", response = SubmitTurnResponse)] +pub struct SubmitTurnMessage(pub AgentSubmissionRequest); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct SubmitTurnResponse(pub AgentSubmissionResult); + +/// `agent/run` response body. +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(rename_all = "camelCase")] +pub struct RunResponse { + pub session_id: String, + pub turn_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_type: Option, + #[serde(default)] + pub accepted: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(rename_all = "camelCase", tag = "kind")] +pub enum RunSessionSpec { + Existing { + session_id: String, + }, + Create { + session_name: String, + agent_type: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + workspace_path: Option, + }, +} + +/// Compatibility request for `agent/run`. +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[request(method = "agent/run", response = RunResponse)] +#[serde(rename_all = "camelCase")] +pub struct RunMessage { + pub session: RunSessionSpec, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub turn_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source: Option, +} diff --git a/src/crates/interfaces/app-server-protocol/src/schemas/event.rs b/src/crates/interfaces/app-server-protocol/src/schemas/event.rs index 425674a832..78cae12e25 100644 --- a/src/crates/interfaces/app-server-protocol/src/schemas/event.rs +++ b/src/crates/interfaces/app-server-protocol/src/schemas/event.rs @@ -5,6 +5,15 @@ use bitfun_events::AgenticEventEnvelope; use bitfun_product_domains::tool_permissions::{PermissionRequest, PermissionRequestEvent}; use serde::{Deserialize, Serialize}; +/// Browser-facing projected runtime or permission event. +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcNotification)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[notification(method = "agent/frontendEvent")] +pub struct FrontendEventNotification { + pub event: String, + pub payload: serde_json::Value, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum EventStream { diff --git a/src/crates/interfaces/app-server-protocol/src/schemas/git.rs b/src/crates/interfaces/app-server-protocol/src/schemas/git.rs new file mode 100644 index 0000000000..0a4b4f71ca --- /dev/null +++ b/src/crates/interfaces/app-server-protocol/src/schemas/git.rs @@ -0,0 +1,107 @@ +//! Git-domain App Server wire schemas. + +use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[request(method = "git/isRepository", response = GitIsRepositoryResponse)] +pub struct GitIsRepositoryMessage(pub GitRepositoryPathRequest); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct GitIsRepositoryResponse(pub bool); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[request(method = "git/getStatus", response = GitGetStatusResponse)] +pub struct GitGetStatusMessage(pub GitRepositoryPathRequest); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct GitGetStatusResponse(pub GitStatus); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[request(method = "git/getBranches", response = GitGetBranchesResponse)] +pub struct GitGetBranchesMessage(pub GitBranchesRequest); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +pub struct GitGetBranchesResponse { + pub branches: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(rename_all = "camelCase")] +pub struct GitRepositoryPathRequest { + pub repository_path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(rename_all = "camelCase")] +pub struct GitBranchesRequest { + pub repository_path: String, + #[serde(default)] + pub include_remote: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitStatus { + pub staged: Vec, + pub unstaged: Vec, + pub untracked: Vec, + #[serde(default)] + pub conflicts: Vec, + pub current_branch: String, + pub ahead: i32, + pub behind: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitFileStatus { + pub path: String, + pub status: String, + pub index_status: Option, + pub workdir_status: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct GitBranch { + pub name: String, + pub current: bool, + pub remote: bool, + pub upstream: Option, + pub ahead: i32, + pub behind: i32, + pub last_commit: Option, + pub last_commit_date: Option, + pub base_branch: Option, + pub child_branches: Option>, + pub merged_branches: Option>, + pub branch_type: Option, + pub has_conflicts: Option, + pub can_merge: Option, + pub is_stale: Option, + pub merge_status: Option, + pub stats: Option, + pub created_at: Option, + pub last_activity_at: Option, + pub tags: Option>, + pub description: Option, + pub linked_issues: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct GitBranchStats { + pub commit_count: i32, + pub contributor_count: i32, + pub file_changes: i32, + pub lines_changed: GitLinesChanged, + pub activity_score: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct GitLinesChanged { + pub additions: i32, + pub deletions: i32, +} diff --git a/src/crates/interfaces/app-server/src/schema/i18n.rs b/src/crates/interfaces/app-server-protocol/src/schemas/i18n.rs similarity index 78% rename from src/crates/interfaces/app-server/src/schema/i18n.rs rename to src/crates/interfaces/app-server-protocol/src/schemas/i18n.rs index c84a04de7a..7948742167 100644 --- a/src/crates/interfaces/app-server/src/schema/i18n.rs +++ b/src/crates/interfaces/app-server-protocol/src/schemas/i18n.rs @@ -1,19 +1,18 @@ +//! Internationalization App Server wire schemas. + use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "i18n/getCurrentLanguage", response = I18nGetCurrentLanguageResponse)] pub struct I18nGetCurrentLanguageMessage {} #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct I18nGetCurrentLanguageResponse { pub language: String, } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "i18n/setLanguage", response = I18nSetLanguageResponse)] #[serde(rename_all = "camelCase")] pub struct I18nSetLanguageMessage { @@ -21,18 +20,15 @@ pub struct I18nSetLanguageMessage { } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct I18nSetLanguageResponse { pub language: String, } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "i18n/getConfig", response = I18nGetConfigResponse)] pub struct I18nGetConfigMessage {} #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct I18nGetConfigResponse { pub current_language: String, pub fallback_language: String, @@ -40,7 +36,6 @@ pub struct I18nGetConfigResponse { } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "i18n/setConfig", response = I18nSetConfigResponse)] #[serde(rename_all = "camelCase")] pub struct I18nSetConfigMessage { @@ -53,16 +48,13 @@ pub struct I18nSetConfigMessage { } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct I18nSetConfigResponse {} #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "i18n/getSupportedLanguages", response = I18nGetSupportedLanguagesResponse)] pub struct I18nGetSupportedLanguagesMessage {} #[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[serde(rename_all = "camelCase")] pub struct I18nLocaleMetadata { pub id: String, @@ -73,7 +65,6 @@ pub struct I18nLocaleMetadata { } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct I18nGetSupportedLanguagesResponse { pub locales: Vec, } diff --git a/src/crates/interfaces/app-server-protocol/src/schemas/mod.rs b/src/crates/interfaces/app-server-protocol/src/schemas/mod.rs index 973b949187..4a6580fcc2 100644 --- a/src/crates/interfaces/app-server-protocol/src/schemas/mod.rs +++ b/src/crates/interfaces/app-server-protocol/src/schemas/mod.rs @@ -6,10 +6,13 @@ pub mod app; pub mod error; pub mod event; pub mod external_source; +pub mod git; pub mod hook; +pub mod i18n; pub mod mcp; pub mod method; pub mod model; +pub mod permission; pub mod session; pub mod skill; pub mod subagent; diff --git a/src/crates/interfaces/app-server/src/schema/permission.rs b/src/crates/interfaces/app-server-protocol/src/schemas/permission.rs similarity index 58% rename from src/crates/interfaces/app-server/src/schema/permission.rs rename to src/crates/interfaces/app-server-protocol/src/schemas/permission.rs index b3178c7a3a..07855997b4 100644 --- a/src/crates/interfaces/app-server/src/schema/permission.rs +++ b/src/crates/interfaces/app-server-protocol/src/schemas/permission.rs @@ -1,24 +1,14 @@ +//! Permission-domain App Server wire schemas. + use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; -use bitfun_agent_runtime::sdk::{ - PermissionAuditRecord, PermissionGrant, PermissionGrantKey, PermissionReply, PermissionRequest, +use bitfun_product_domains::tool_permissions::{ + PermissionAuditRecord, PermissionGrant, PermissionGrantKey, PermissionReply, }; use serde::{Deserialize, Serialize}; -/// `agent/respondPermission` request body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "agent/respondPermission", response = RespondPermissionResponse)] -pub struct RespondPermissionMessage { - pub request_id: String, - pub reply: PermissionReply, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct RespondPermissionResponse {} +pub type RespondPermissionMessage = super::agent::RespondPermissionRequest; #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "agent/respondPermissionBatch", response = RespondPermissionBatchResponse)] pub struct RespondPermissionBatchMessage { pub request_id: String, @@ -26,24 +16,14 @@ pub struct RespondPermissionBatchMessage { } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct RespondPermissionBatchResponse { pub request_ids: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "agent/listPendingPermissionRequests", response = ListPendingPermissionRequestsResponse)] -pub struct ListPendingPermissionRequestsMessage {} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct ListPendingPermissionRequestsResponse { - pub requests: Vec, -} +pub type ListPendingPermissionRequestsMessage = super::agent::PendingPermissionsRequest; +pub type ListPendingPermissionRequestsResponse = super::agent::PendingPermissionsResponse; #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "agent/listProjectPermissionGrants", response = ListProjectPermissionGrantsResponse)] pub struct ListProjectPermissionGrantsMessage { pub project_id: String, @@ -56,7 +36,6 @@ pub struct ListProjectPermissionGrantsResponse { } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] #[request(method = "agent/removeProjectPermissionGrant", response = RemoveProjectPermissionGrantResponse)] pub struct RemoveProjectPermissionGrantMessage(pub PermissionGrantKey); @@ -67,29 +46,23 @@ pub struct RemoveProjectPermissionGrantResponse { } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "agent/clearProjectPermissionGrants", response = ClearProjectPermissionGrantsResponse)] pub struct ClearProjectPermissionGrantsMessage { pub project_id: String, } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct ClearProjectPermissionGrantsResponse { pub cleared: usize, } -pub use bitfun_app_server_protocol::event::PermissionEventNotification; - #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[request(method = "agent/listProjectPermissionAudit", response = ListProjectPermissionAuditResponse)] pub struct ListProjectPermissionAuditMessage { pub project_id: String, } #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct ListProjectPermissionAuditResponse { pub records: Vec, } diff --git a/src/crates/interfaces/app-server-protocol/src/schemas/session.rs b/src/crates/interfaces/app-server-protocol/src/schemas/session.rs index c2ad96c846..5c0d51180a 100644 --- a/src/crates/interfaces/app-server-protocol/src/schemas/session.rs +++ b/src/crates/interfaces/app-server-protocol/src/schemas/session.rs @@ -4,15 +4,15 @@ use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; use bitfun_core_types::SessionUsageReport; use bitfun_product_domains::tool_permissions::PermissionRequest; use bitfun_runtime_ports::{ - AgentContextReloadRequest, AgentSessionCompactionRequest, AgentSessionCompactionResult, - AgentSessionForkBeforeTurnRequest, AgentSessionForkRequest, AgentSessionForkResult, - AgentSessionLineageCancellationRequest, AgentSessionLineageInspection, - AgentSessionLineageRequest, AgentSessionLineageSnapshot, AgentSessionLineageTranscriptRequest, - AgentSessionModeUpdateRequest, AgentSessionModelUpdateRequest, AgentSessionRenameRequest, - AgentSessionRevertRequest, AgentSessionRevertResult, AgentSessionSummary, - AgentSessionUsageRequest, AgentSessionWorkspaceBinding, AgentSessionWorkspaceRequest, - AgentTurnCancellationResult, AgentTurnSettlementRequest, SessionTranscript, - SessionTranscriptRequest, + AgentContextReloadRequest, AgentSessionArchiveStateRequest, AgentSessionCompactionRequest, + AgentSessionCompactionResult, AgentSessionForkAtTurnRequest, AgentSessionForkBeforeTurnRequest, + AgentSessionForkRequest, AgentSessionForkResult, AgentSessionLineageCancellationRequest, + AgentSessionLineageInspection, AgentSessionLineageRequest, AgentSessionLineageSnapshot, + AgentSessionLineageTranscriptRequest, AgentSessionModeUpdateRequest, + AgentSessionModelUpdateRequest, AgentSessionRenameRequest, AgentSessionRevertRequest, + AgentSessionRevertResult, AgentSessionSummary, AgentSessionUsageRequest, + AgentSessionWorkspaceBinding, AgentSessionWorkspaceRequest, AgentTurnCancellationResult, + AgentTurnSettlementRequest, SessionTranscript, SessionTranscriptRequest, }; use serde::{Deserialize, Serialize}; @@ -49,6 +49,7 @@ pub struct SyncSessionResponse { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[serde( tag = "kind", rename_all = "camelCase", @@ -67,6 +68,7 @@ pub enum SessionRuntimeState { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[serde(rename_all = "camelCase")] pub enum SessionProcessingPhase { Starting, @@ -77,6 +79,30 @@ pub enum SessionProcessingPhase { ToolConfirming, } +/// Compatibility request for `session/restore`. +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[request(method = "session/restore", response = RestoreSessionResponse)] +#[serde(rename_all = "camelCase")] +pub struct RestoreSessionMessage { + pub workspace_path: String, + pub session_id: String, + #[serde(default)] + pub include_internal: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_connection_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_ssh_host: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[serde(rename_all = "camelCase")] +pub struct RestoreSessionResponse { + pub session: AgentSessionSummary, + pub state: SessionRuntimeState, +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] #[request(method = "session/readTranscript", response = ReadTranscriptResponse)] pub struct ReadTranscriptRequest(pub SessionTranscriptRequest); @@ -164,6 +190,7 @@ pub struct ForkSessionRequest(pub AgentSessionForkRequest); pub struct ForkSessionBeforeTurnRequest(pub AgentSessionForkBeforeTurnRequest); #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct ForkSessionResponse(pub AgentSessionForkResult); #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] @@ -177,3 +204,21 @@ unit_response!(UpdateSessionModelResponse); pub struct UpdateSessionModeRequest(pub AgentSessionModeUpdateRequest); unit_response!(UpdateSessionModeResponse); + +pub use ForkSessionBeforeTurnRequest as ForkSessionBeforeTurnMessage; +pub use ForkSessionRequest as ForkSessionMessage; +pub use RenameSessionRequest as RenameSessionMessage; +pub use UpdateSessionModeRequest as UpdateSessionModeMessage; +pub use UpdateSessionModelRequest as UpdateSessionModelMessage; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[request(method = "session/setArchived", response = SetSessionArchivedResponse)] +pub struct SetSessionArchivedMessage(pub AgentSessionArchiveStateRequest); + +unit_response!(SetSessionArchivedResponse); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[request(method = "session/forkAtTurn", response = ForkSessionResponse)] +pub struct ForkSessionAtTurnMessage(pub AgentSessionForkAtTurnRequest); diff --git a/src/crates/interfaces/app-server-protocol/tests/legacy_wire_contracts.rs b/src/crates/interfaces/app-server-protocol/tests/legacy_wire_contracts.rs new file mode 100644 index 0000000000..bcbbe5bf10 --- /dev/null +++ b/src/crates/interfaces/app-server-protocol/tests/legacy_wire_contracts.rs @@ -0,0 +1,84 @@ +use agent_client_protocol::JsonRpcMessage; +use bitfun_app_server_protocol::{ + agent::{RunResponse, SubmitDialogTurnMessage, SubmitDialogTurnRequest}, + config::GetAgentProfileConfigMessage, + permission::RespondPermissionMessage, + session::{RestoreSessionMessage, SessionProcessingPhase, SessionRuntimeState}, +}; +use serde_json::json; + +#[test] +fn legacy_submit_dialog_contract_keeps_optional_policy_and_method() { + let request: SubmitDialogTurnRequest = serde_json::from_value(json!({ + "sessionId": "session-1", + "message": "hello", + "agentType": "general" + })) + .expect("legacy dialog payload should accept a missing policy"); + let body = request.0; + + assert!(body.policy.is_none()); + assert!(body.attachments.is_empty()); + assert!(body.metadata.is_empty()); + assert!(SubmitDialogTurnMessage::matches_method( + "agent/submitDialogTurn" + )); + assert_eq!( + std::any::TypeId::of::(), + std::any::TypeId::of::() + ); +} + +#[test] +fn legacy_restore_and_session_state_keep_their_wire_shape() { + let request: RestoreSessionMessage = serde_json::from_value(json!({ + "workspacePath": "/workspace", + "sessionId": "session-1" + })) + .expect("restore request should accept omitted includeInternal"); + assert!(!request.include_internal); + + let state = SessionRuntimeState::Processing { + current_turn_id: "turn-1".to_string(), + phase: SessionProcessingPhase::ToolCalling, + }; + assert_eq!( + serde_json::to_value(state).expect("state should serialize"), + json!({ + "kind": "processing", + "currentTurnId": "turn-1", + "phase": "toolCalling" + }) + ); +} + +#[test] +fn legacy_mixed_case_fields_and_response_defaults_remain_compatible() { + let profile: GetAgentProfileConfigMessage = serde_json::from_value(json!({ + "agent_id": "general" + })) + .expect("legacy get-profile request should keep snake_case"); + assert_eq!(profile.agent_id, "general"); + + let permission: RespondPermissionMessage = serde_json::from_value(json!({ + "request_id": "permission-1", + "reply": { "reply": "once" } + })) + .expect("legacy permission request should keep snake_case"); + assert_eq!(permission.request_id, "permission-1"); + + let response: RunResponse = serde_json::from_value(json!({ + "sessionId": "session-1", + "turnId": "turn-1" + })) + .expect("accepted should default to false"); + assert!(!response.accepted); + assert_eq!( + serde_json::to_value(response).expect("response should serialize"), + json!({ + "sessionId": "session-1", + "turnId": "turn-1", + "accepted": false + }) + ); +} diff --git a/src/crates/interfaces/app-server/AGENTS-CN.md b/src/crates/interfaces/app-server/AGENTS-CN.md index 5f3b412e41..c8ab0b4cae 100644 --- a/src/crates/interfaces/app-server/AGENTS-CN.md +++ b/src/crates/interfaces/app-server/AGENTS-CN.md @@ -1,8 +1,9 @@ **中文** | [English](AGENTS.md) -# App Server 服务端与接线指南 +# App Server 接口族指南 -适用范围:本指南只适用于 `src/crates/interfaces/app-server` 及其服务端生产接线。 +适用范围:本指南适用于相邻的 `app-server-protocol`、`app-server-client`、`app-server` +三个 crate 及 App Server 生产接线。除非另有说明,服务端专属规则只约束 `app-server`。 App Server 接口由四个 owner 分工: @@ -13,10 +14,13 @@ App Server 接口由四个 owner 分工: | `app-server` | server 生命周期、生产 handler 注册、事件转发、Runtime/domain 到 wire 的转换和错误映射 | | `src/apps/*` 下的产品 Host | 具体 transport、认证、连接作用域、capability/limit 构造、平台能力、进程监督和关闭流程 | -不要在本 crate 新增 protocol 或 client 所有权。消费者迁移期间可以保留 compatibility +不要在 `bitfun-app-server` 中新增 protocol 或 client 所有权。消费者迁移期间可以保留 compatibility module 和 re-export,但新 method、DTO、wire error 和类型化 client 行为必须放入相邻的 protocol/client crate。 +`bitfun-app-server/ts` 只保留为兼容转发 feature。protocol crate 是唯一的 TypeScript +schema 导出 owner;不要把 `ts-rs`、Runtime 实现类型或第二条导出命令重新加入本 crate。 + ## 护栏 - compatibility role 和 transport helper 必须保持 schema-free。不要在 `AppServer` / @@ -55,7 +59,8 @@ kind 和结构化 data,不泄露 Runtime 内部细节。Host transport/auth/sc ## 验证 ```bash -cargo check -p bitfun-app-server --offline -cargo test -p bitfun-app-server --offline +cargo check --locked -p bitfun-app-server --offline +cargo test --locked -p bitfun-app-server --offline --lib server::wire::tests +cargo test --locked -p bitfun-app-server-protocol --offline --test legacy_wire_contracts pnpm run check:core-boundaries ``` diff --git a/src/crates/interfaces/app-server/AGENTS.md b/src/crates/interfaces/app-server/AGENTS.md index ef859abc20..6078559e7b 100644 --- a/src/crates/interfaces/app-server/AGENTS.md +++ b/src/crates/interfaces/app-server/AGENTS.md @@ -1,9 +1,10 @@ [中文](AGENTS-CN.md) | **English** -# App Server Server and Wiring Guide +# App Server Interface Family Guide -Scope: this guide applies only to `src/crates/interfaces/app-server` and its -server-side production wiring. +Scope: this guide applies to the adjacent `app-server-protocol`, +`app-server-client`, and `app-server` crates, plus App Server production wiring. +Server-specific rules apply to `app-server` unless stated otherwise. The App Server surface is split across four owners: @@ -14,11 +15,15 @@ The App Server surface is split across four owners: | `app-server` | Server lifecycle, production handler registration, event forwarding, Runtime/domain-to-wire conversion, and error mapping | | Product Host under `src/apps/*` | Concrete transport, authentication, connection scope, capability/limit construction, platform capabilities, process supervision, and shutdown | -Do not add new protocol or client ownership to this crate. Compatibility +Do not add new protocol or client ownership to `bitfun-app-server`. Compatibility modules and re-exports may remain while consumers migrate, but new methods, DTOs, wire errors, and typed client behavior belong in the adjacent protocol and client crates. +The `bitfun-app-server/ts` feature is a compatibility forwarder only. The +protocol crate is the sole TypeScript schema exporter; do not add `ts-rs`, +runtime implementation types, or a second export command back to this crate. + ## Guardrails - Keep compatibility role and transport helpers schema-free. Do not hard-code @@ -71,7 +76,8 @@ failures use helpers such as `BitfunAppRuntime::runtime_error` and ## Verification ```bash -cargo check -p bitfun-app-server --offline -cargo test -p bitfun-app-server --offline +cargo check --locked -p bitfun-app-server --offline +cargo test --locked -p bitfun-app-server --offline --lib server::wire::tests +cargo test --locked -p bitfun-app-server-protocol --offline --test legacy_wire_contracts pnpm run check:core-boundaries ``` diff --git a/src/crates/interfaces/app-server/Cargo.toml b/src/crates/interfaces/app-server/Cargo.toml index 46fc3e42bc..b2a81f826c 100644 --- a/src/crates/interfaces/app-server/Cargo.toml +++ b/src/crates/interfaces/app-server/Cargo.toml @@ -16,33 +16,21 @@ bitfun-app-server-protocol = { path = "../app-server-protocol" } # protocol surface must not inherit the broad bitfun-core/product-full union. bitfun-core = { path = "../../assembly/core", features = ["external-sources", "git", "i18n-runtime", "remote-connect"] } bitfun-agent-runtime = { path = "../../execution/agent-runtime", features = ["agent-runtime"] } -bitfun-events = { path = "../../contracts/events" } bitfun-product-domains = { path = "../../contracts/product-domains", features = ["external-sources"] } bitfun-runtime-ports = { path = "../../contracts/runtime-ports", features = ["agent-api"] } tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } -tokio-util = { workspace = true, features = ["compat"] } -futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } anyhow = { workspace = true } log = { workspace = true } tracing = { workspace = true } url = { workspace = true } -ts-rs = { workspace = true, optional = true } [features] default = [] -# Schema-first TypeScript binding export. Derives `ts_rs::TS` (via -# `cfg_attr`) on the wire types under `src/schema/` and propagates the `ts` feature -# to the upstream contract/service crates so every type the schema references -# also implements `TS`. Run `cargo test --features ts export_types` (or the -# frontend `gen:types` script) to emit `.ts` into `TS_RS_EXPORT_DIR`. -ts = [ - "dep:ts-rs", - "bitfun-app-server-protocol/ts", - "bitfun-core/ts", - "bitfun-runtime-ports/ts", -] +# Compatibility feature for existing callers. TypeScript schemas are owned and +# exported by `bitfun-app-server-protocol`. +ts = ["bitfun-app-server-protocol/ts"] [lints] workspace = true diff --git a/src/crates/interfaces/app-server/src/client.rs b/src/crates/interfaces/app-server/src/client.rs deleted file mode 100644 index d4a64c724e..0000000000 --- a/src/crates/interfaces/app-server/src/client.rs +++ /dev/null @@ -1,781 +0,0 @@ -//! Generic app-server client: drives an [`AppClient`] connection over a -//! host-supplied transport and exposes the agent kernel RPC surface. -//! -//! This module is the client counterpart of [`crate::server::BitfunAppServer`]. -//! It is **transport-agnostic**: the host picks the transport (in-memory pair, -//! stdio, websocket, ...) and passes the client half to [`connect`]. The -//! server half is driven by the host through [`BitfunAppServer::serve`], so -//! this client never decides how the server is started -- it only owns the -//! client end of one connection. -//! -//! [`connect`] builds an [`AppClient`] on the given transport, parks the -//! [`ConnectionTo`] for the lifetime of the returned handle, and -//! registers `on_receive_notification` for `agent/event`. Each forwarded -//! [`AgenticEventEnvelope`] is projected to the frontend shape (via -//! [`bitfun_events::project_agentic_frontend_event`]) and fanned out through a -//! [`tokio::sync::broadcast`] channel; consumers receive them via -//! [`AppServerClient::subscribe_events`]. The client never subscribes to the -//! runtime event queue directly, so all agent interfaces uniformly go through -//! the app-server protocol surface. -//! -//! The connection-driving pattern mirrors the ACP client manager -//! (`acp/src/client/manager.rs`): the `connect_with` main function parks the -//! [`ConnectionTo`] through a oneshot and then awaits a shutdown -//! signal so the connection task stays alive for the lifetime of the host. -//! -//! # In-process pairing -//! -//! A host that runs the server and client in the same process pairs them with -//! [`crate::transport::in_memory_channel_pair`]: -//! -//! ```no_run -//! # use bitfun_app_server::{AppClient, AppServer, transport}; -//! # use bitfun_app_server::client::connect; -//! # async fn example() -> anyhow::Result<()> { -//! // let (server_transport, client_transport) = transport::in_memory_channel_pair(); -//! // let _server = tokio::spawn(async { /* serve BitfunAppServer on server_transport */ }); -//! // let client = connect(client_transport).await?; -//! # Ok(()) -//! # } -//! ``` - -use std::sync::Arc; -use std::time::Duration; - -use agent_client_protocol::ConnectTo; -use agent_client_protocol::{ConnectionTo, JsonRpcResponse, Result, SentRequest}; -use bitfun_events::project_agentic_frontend_event; -use tokio::sync::{broadcast, oneshot}; - -use crate::schema::{ - CancelTurnMessage, CancelTurnResponse, ClearProjectPermissionGrantsMessage, - ClearProjectPermissionGrantsResponse, ConfigEventNotification, ConfigUpdate, - CreateSessionMessage, CreateSessionResponse, DeleteSessionMessage, DeleteSessionResponse, - ForkSessionAtTurnMessage, ForkSessionBeforeTurnMessage, ForkSessionMessage, - ForkSessionResponse, GetAgentProfileConfigMessage, GetAgentProfileConfigResponse, - GetAgentProfileConfigsMessage, GetAgentProfileConfigsResponse, GetConfigMessage, - GetConfigResponse, GetConfigsMessage, GetConfigsResponse, GetModelConfigsMessage, - GetModelConfigsResponse, GitBranchesRequest, GitGetBranchesMessage, GitGetBranchesResponse, - GitGetStatusMessage, GitGetStatusResponse, GitIsRepositoryMessage, GitIsRepositoryResponse, - GitRepositoryPathRequest, I18nGetConfigMessage, I18nGetCurrentLanguageMessage, - I18nGetCurrentLanguageResponse, I18nGetSupportedLanguagesMessage, - I18nGetSupportedLanguagesResponse, I18nSetConfigMessage, I18nSetConfigResponse, - I18nSetLanguageMessage, I18nSetLanguageResponse, ListPendingPermissionRequestsMessage, - ListPendingPermissionRequestsResponse, ListProjectPermissionAuditMessage, - ListProjectPermissionAuditResponse, ListProjectPermissionGrantsMessage, - ListProjectPermissionGrantsResponse, ListSessionsMessage, ListSessionsResponse, - PermissionEventNotification, RemoveProjectPermissionGrantMessage, - RemoveProjectPermissionGrantResponse, RenameSessionMessage, RenameSessionResponse, - ResetAgentProfileConfigMessage, ResetAgentProfileConfigResponse, RespondPermissionBatchMessage, - RespondPermissionBatchResponse, RespondPermissionMessage, RespondPermissionResponse, - RestoreSessionMessage, RunMessage, RunResponse, SessionEventNotification, - SetAgentProfileConfigMessage, SetAgentProfileConfigResponse, SetConfigMessage, - SetConfigResponse, SetSessionArchivedMessage, SetSessionArchivedResponse, - SubmitDialogTurnMessage, SubmitTurnMessage, SubmitTurnResponse, UpdateSessionModeMessage, - UpdateSessionModeResponse, UpdateSessionModelMessage, UpdateSessionModelResponse, -}; -use crate::{AppClient, AppServer}; - -/// Projected frontend event forwarded to consumers (websocket connections, -/// Tauri event bridge, ...). -#[derive(Debug, Clone)] -pub struct FrontendEvent { - /// Frontend event name (`agent/event` projection). - pub event: String, - /// Projected payload, ready to serialize for the consumer's wire shape. - pub payload: serde_json::Value, -} - -/// Startup grace for the in-process app-server client connection. -const CLIENT_STARTUP_TIMEOUT: Duration = Duration::from_secs(5); - -/// Client handle for an app-server connection. -/// -/// Cheaply cloneable: each clone shares the connection handle, the event -/// broadcast sender, and the shutdown signal. Use [`AppServerClient::rpc`] -/// (or the typed helpers) to route an agent kernel RPC through the app-server -/// surface, and [`AppServerClient::subscribe_events`] to receive projected -/// runtime events. -/// -/// The connection is parked for the lifetime of this handle: the in-process -/// `connect_task` that drives the `AppClient` connection parks on the shutdown -/// receiver, and the `shutdown_tx` lives in the shared handle so the parked -/// main loop only resumes when [`AppServerClient::shutdown`] is called or the -/// last clone is dropped. Hosts that want the connection to live as long as -/// the process (the Server Host pattern) simply hold a clone and never call -/// `shutdown`. -#[derive(Clone)] -pub struct AppServerClient { - connection: Arc>, - event_tx: broadcast::Sender, - config_event_tx: broadcast::Sender, - shutdown_tx: Arc>>>, -} - -impl AppServerClient { - /// Subscribe to projected frontend events. One receiver per consumer - /// (websocket connection, Tauri bridge, ...); dropping the last receiver - /// does not close the server. - pub fn subscribe_events(&self) -> broadcast::Receiver { - self.event_tx.subscribe() - } - - /// Subscribe to canonical configuration updates from `config/event`. - pub fn subscribe_config_updates(&self) -> broadcast::Receiver { - self.config_event_tx.subscribe() - } - - /// Shut down the in-process app-server client connection. Signals the - /// parked `connect_task` to resume, which lets the connection's background - /// actors (task/outgoing/incoming/responder) unwind and close the - /// transport. Safe to call more than once; subsequent calls are no-ops. - /// Hosts that want the connection to live for the process lifetime simply - /// never call this. - pub async fn shutdown(&self) { - if let Some(tx) = self - .shutdown_tx - .lock() - .ok() - .and_then(|mut guard| guard.take()) - { - let _ = tx.send(()); - } - } - - /// Create an agent session via `agent/createSession`. - pub async fn create_session( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionCreateRequest, - ) -> Result { - let CreateSessionResponse(inner) = self - .rpc(|cx| cx.send_request(CreateSessionMessage(request))) - .await?; - Ok(inner) - } - - /// List agent sessions via `agent/listSessions`. - pub async fn list_sessions( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionListRequest, - ) -> Result> { - let ListSessionsResponse { sessions } = self - .rpc(|cx| cx.send_request(ListSessionsMessage(request))) - .await?; - Ok(sessions) - } - - /// Delete an agent session via `agent/deleteSession`. - pub async fn delete_session( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionDeleteRequest, - ) -> Result<()> { - let DeleteSessionResponse {} = self - .rpc(|cx| cx.send_request(DeleteSessionMessage(request))) - .await?; - Ok(()) - } - - /// Rename a persisted Session through the Runtime Session owner. - pub async fn rename_session( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionRenameRequest, - ) -> Result<()> { - let RenameSessionResponse {} = self - .rpc(|cx| cx.send_request(RenameSessionMessage(request))) - .await?; - Ok(()) - } - - /// Set the persisted archive state through the Runtime Session owner. - pub async fn set_session_archived( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionArchiveStateRequest, - ) -> Result<()> { - let SetSessionArchivedResponse {} = self - .rpc(|cx| cx.send_request(SetSessionArchivedMessage(request))) - .await?; - Ok(()) - } - - /// Update the selected model for an already loaded Session. - pub async fn update_session_model( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionModelUpdateRequest, - ) -> Result<()> { - let UpdateSessionModelResponse {} = self - .rpc(|cx| cx.send_request(UpdateSessionModelMessage(request))) - .await?; - Ok(()) - } - - /// Update the selected mode for an already loaded Session. - pub async fn update_session_mode( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionModeUpdateRequest, - ) -> Result<()> { - let UpdateSessionModeResponse {} = self - .rpc(|cx| cx.send_request(UpdateSessionModeMessage(request))) - .await?; - Ok(()) - } - - /// Fork a Session at its latest persisted turn. - pub async fn fork_session( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionForkRequest, - ) -> Result { - let ForkSessionResponse(result) = self - .rpc(|cx| cx.send_request(ForkSessionMessage(request))) - .await?; - Ok(result) - } - - /// Fork a Session including the selected persisted turn. - pub async fn fork_session_at_turn( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionForkAtTurnRequest, - ) -> Result { - let ForkSessionResponse(result) = self - .rpc(|cx| cx.send_request(ForkSessionAtTurnMessage(request))) - .await?; - Ok(result) - } - - /// Fork a Session immediately before the selected persisted turn. - pub async fn fork_session_before_turn( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionForkBeforeTurnRequest, - ) -> Result { - let ForkSessionResponse(result) = self - .rpc(|cx| cx.send_request(ForkSessionBeforeTurnMessage(request))) - .await?; - Ok(result) - } - - /// Restore a persisted Session into the Runtime owner. - pub async fn restore_session( - &self, - request: bitfun_agent_runtime::sdk::AgentSessionRestoreRequest, - ) -> Result { - let response = self - .rpc(|cx| { - cx.send_request(RestoreSessionMessage { - workspace_path: request.workspace_path, - session_id: request.session_id, - include_internal: request.include_internal, - remote_connection_id: request.remote_connection_id, - remote_ssh_host: request.remote_ssh_host, - }) - }) - .await?; - Ok(response.into()) - } - - /// Submit a turn via `agent/submitTurn`. - pub async fn submit_turn( - &self, - request: bitfun_agent_runtime::sdk::AgentSubmissionRequest, - ) -> Result { - let SubmitTurnResponse(inner) = self - .rpc(|cx| cx.send_request(SubmitTurnMessage(request))) - .await?; - Ok(inner) - } - - /// Submit a dialog turn via `agent/submitDialogTurn`. - /// - /// This is the operation the desktop `start_dialog_turn` command drives and - /// the one web/CLI hosts should map `startDialogTurn`-style calls to: it - /// carries `agentType`/`workspacePath`/`policy`, unlike `submit_turn` which - /// is a bare message into an existing session. The response reports whether - /// the turn started immediately or was queued. - pub async fn submit_dialog_turn( - &self, - body: crate::schema::SubmitDialogTurnBody, - ) -> Result { - self.rpc(|cx| cx.send_request(SubmitDialogTurnMessage(body))) - .await - } - - /// Cancel a turn via `agent/cancelTurn`. - pub async fn cancel_turn( - &self, - request: bitfun_agent_runtime::sdk::AgentTurnCancellationRequest, - ) -> Result { - let CancelTurnResponse(inner) = self - .rpc(|cx| cx.send_request(CancelTurnMessage(request))) - .await?; - Ok(inner) - } - - /// Respond to a permission request via `agent/respondPermission`. - pub async fn respond_permission( - &self, - request_id: &str, - reply: bitfun_agent_runtime::sdk::PermissionReply, - ) -> Result<()> { - let RespondPermissionResponse {} = self - .rpc(|cx| { - cx.send_request(RespondPermissionMessage { - request_id: request_id.to_string(), - reply, - }) - }) - .await?; - Ok(()) - } - - /// Respond to a permission request and all requests sharing its reply via - /// `agent/respondPermissionBatch`. Returns the request ids that shared the - /// resolved reply. - pub async fn respond_permission_batch( - &self, - request_id: &str, - reply: bitfun_agent_runtime::sdk::PermissionReply, - ) -> Result> { - let RespondPermissionBatchResponse { request_ids } = self - .rpc(|cx| { - cx.send_request(RespondPermissionBatchMessage { - request_id: request_id.to_string(), - reply, - }) - }) - .await?; - Ok(request_ids) - } - - /// List pending permission requests via `agent/listPendingPermissionRequests`. - pub async fn list_pending_permission_requests( - &self, - ) -> Result> { - let ListPendingPermissionRequestsResponse { requests } = self - .rpc(|cx| cx.send_request(ListPendingPermissionRequestsMessage {})) - .await?; - Ok(requests) - } - - /// List project permission grants via `agent/listProjectPermissionGrants`. - pub async fn list_project_permission_grants( - &self, - project_id: &str, - ) -> Result> { - let ListProjectPermissionGrantsResponse { grants } = self - .rpc(|cx| { - cx.send_request(ListProjectPermissionGrantsMessage { - project_id: project_id.to_string(), - }) - }) - .await?; - Ok(grants) - } - - /// Remove a project permission grant via `agent/removeProjectPermissionGrant`. - pub async fn remove_project_permission_grant( - &self, - key: bitfun_agent_runtime::sdk::PermissionGrantKey, - ) -> Result { - let RemoveProjectPermissionGrantResponse { removed } = self - .rpc(|cx| cx.send_request(RemoveProjectPermissionGrantMessage(key))) - .await?; - Ok(removed) - } - - /// Clear all permission grants for a project via - /// `agent/clearProjectPermissionGrants`. Returns the count cleared. - pub async fn clear_project_permission_grants(&self, project_id: &str) -> Result { - let ClearProjectPermissionGrantsResponse { cleared } = self - .rpc(|cx| { - cx.send_request(ClearProjectPermissionGrantsMessage { - project_id: project_id.to_string(), - }) - }) - .await?; - Ok(cleared) - } - - /// List the permission audit log for a project via - /// `agent/listProjectPermissionAudit`. - pub async fn list_project_permission_audit( - &self, - project_id: &str, - ) -> Result> { - let ListProjectPermissionAuditResponse { records } = self - .rpc(|cx| { - cx.send_request(ListProjectPermissionAuditMessage { - project_id: project_id.to_string(), - }) - }) - .await?; - Ok(records) - } - - /// Run an agent turn (create-or-existing) via `agent/run`. - pub async fn run(&self, request: RunMessage) -> Result { - self.rpc(|cx| cx.send_request(request)).await - } - - /// Check whether a path is a Git repository via `git/isRepository`. - pub async fn git_is_repository(&self, repository_path: &str) -> Result { - let GitIsRepositoryResponse(value) = self - .rpc(|cx| { - cx.send_request(GitIsRepositoryMessage(GitRepositoryPathRequest { - repository_path: repository_path.to_string(), - })) - }) - .await?; - Ok(value) - } - - /// Get the working-tree status of a Git repository via `git/getStatus`. - pub async fn git_get_status( - &self, - repository_path: &str, - ) -> Result { - let GitGetStatusResponse(status) = self - .rpc(|cx| { - cx.send_request(GitGetStatusMessage(GitRepositoryPathRequest { - repository_path: repository_path.to_string(), - })) - }) - .await?; - Ok(status) - } - - /// List the branches of a Git repository via `git/getBranches`. - pub async fn git_get_branches( - &self, - repository_path: &str, - include_remote: bool, - ) -> Result> { - let GitGetBranchesResponse { branches } = self - .rpc(|cx| { - cx.send_request(GitGetBranchesMessage(GitBranchesRequest { - repository_path: repository_path.to_string(), - include_remote: Some(include_remote), - })) - }) - .await?; - Ok(branches) - } - - /// List all agent profile configs via `config/getAgentProfileConfigs`. - pub async fn get_agent_profile_configs( - &self, - ) -> Result> - { - let GetAgentProfileConfigsResponse { profiles } = self - .rpc(|cx| cx.send_request(GetAgentProfileConfigsMessage {})) - .await?; - Ok(profiles) - } - - /// Get a single agent profile config via `config/getAgentProfileConfig`. - pub async fn get_agent_profile_config( - &self, - agent_id: &str, - ) -> Result { - let GetAgentProfileConfigResponse(view) = self - .rpc(|cx| { - cx.send_request(GetAgentProfileConfigMessage { - agent_id: agent_id.to_string(), - }) - }) - .await?; - Ok(view) - } - - /// List all AI model configs via `config/getModelConfigs`. - pub async fn get_model_configs( - &self, - ) -> Result> { - let GetModelConfigsResponse { models } = self - .rpc(|cx| cx.send_request(GetModelConfigsMessage {})) - .await?; - Ok(models) - } - - /// Read a single config path via `config/getConfig`. Returns the raw JSON - /// value at that path (`None` path reads the whole config tree). A missing - /// path surfaces as an error whose `message` matches the desktop contract - /// (`Failed to get config: Not found: Config path '' not found`) so - /// the frontend `ConfigAPI.getConfig` can swallow it into `undefined`. - pub async fn get_config(&self, path: Option<&str>) -> Result { - let GetConfigResponse(value) = self - .rpc(|cx| { - cx.send_request(GetConfigMessage { - path: path.map(str::to_string), - skip_retry_on_not_found: false, - }) - }) - .await?; - Ok(value) - } - - /// Read multiple config paths in one batch via `config/getConfigs`. - /// Returns a path -> value map; paths are deduped server-side the way the - /// desktop host does. The first not-found path aborts the batch and - /// surfaces the same not-found message shape as [`get_config`]. - pub async fn get_configs( - &self, - paths: &[String], - ) -> Result> { - let GetConfigsResponse { configs } = self - .rpc(|cx| { - cx.send_request(GetConfigsMessage { - paths: paths.to_vec(), - skip_retry_on_not_found: false, - }) - }) - .await?; - Ok(configs) - } - - /// Write a value at a config path via `config/setConfig` (Track B). The - /// handler reaches the global config singleton the Desktop host uses. - pub async fn set_config(&self, path: &str, value: serde_json::Value) -> Result<()> { - let SetConfigResponse {} = self - .rpc(|cx| { - cx.send_request(SetConfigMessage { - path: path.to_string(), - value, - }) - }) - .await?; - Ok(()) - } - - /// Canonicalize and persist one agent profile, returning the effective view. - pub async fn set_agent_profile_config( - &self, - agent_id: &str, - config: serde_json::Value, - ) -> Result { - let SetAgentProfileConfigResponse(view) = self - .rpc(|cx| { - cx.send_request(SetAgentProfileConfigMessage { - agent_id: agent_id.to_string(), - config, - }) - }) - .await?; - Ok(view) - } - - /// Reset one agent profile and return the effective default view. - pub async fn reset_agent_profile_config( - &self, - agent_id: &str, - ) -> Result { - let ResetAgentProfileConfigResponse(view) = self - .rpc(|cx| { - cx.send_request(ResetAgentProfileConfigMessage { - agent_id: agent_id.to_string(), - }) - }) - .await?; - Ok(view) - } - - /// Read the current runtime locale id via `i18n/getCurrentLanguage`. - pub async fn i18n_get_current_language(&self) -> Result { - let I18nGetCurrentLanguageResponse { language } = self - .rpc(|cx| cx.send_request(I18nGetCurrentLanguageMessage {})) - .await?; - Ok(language) - } - - /// Set the runtime locale via `i18n/setLanguage` and sync the global - /// I18nService. Returns the validated locale id that was applied. - pub async fn i18n_set_language(&self, language: &str) -> Result { - let I18nSetLanguageResponse { language } = self - .rpc(|cx| { - cx.send_request(I18nSetLanguageMessage { - language: language.to_string(), - }) - }) - .await?; - Ok(language) - } - - /// Read the i18n config (current/fallback language, autoDetect) via - /// `i18n/getConfig`. - pub async fn i18n_get_config(&self) -> Result { - let response = self - .rpc(|cx| cx.send_request(I18nGetConfigMessage {})) - .await?; - Ok(response) - } - - /// Write the i18n config (writes `currentLanguage` and syncs the I18nService) - /// via `i18n/setConfig`. - pub async fn i18n_set_config( - &self, - current_language: Option<&str>, - fallback_language: Option<&str>, - auto_detect: bool, - ) -> Result<()> { - let I18nSetConfigResponse {} = self - .rpc(|cx| { - cx.send_request(I18nSetConfigMessage { - current_language: current_language.map(str::to_string), - fallback_language: fallback_language.map(str::to_string), - auto_detect, - }) - }) - .await?; - Ok(()) - } - - /// List all supported locales via `i18n/getSupportedLanguages`. - pub async fn i18n_get_supported_languages( - &self, - ) -> Result> { - let I18nGetSupportedLanguagesResponse { locales } = self - .rpc(|cx| cx.send_request(I18nGetSupportedLanguagesMessage {})) - .await?; - Ok(locales) - } - - /// Send a JSON-RPC request through the app-server connection and await its - /// response. Uses the canonical `on_receiving_result` + oneshot pattern - /// (mirrors the app-server round-trip tests) so the calling task is not - /// blocked on the connection driver. - async fn rpc(&self, send: F) -> Result - where - F: FnOnce(&ConnectionTo) -> SentRequest, - R: JsonRpcResponse + Send, - { - let sent = send(&self.connection); - let (tx, rx) = oneshot::channel(); - sent.on_receiving_result(async move |result| { - tx.send(result) - .map_err(|_| agent_client_protocol::Error::internal_error()) - })?; - rx.await - .map_err(|_| agent_client_protocol::Error::internal_error())? - } -} - -/// Connect an app-server client over a host-supplied transport. -/// -/// Drives an [`AppClient`] on `transport`, parks the [`ConnectionTo`] -/// for the lifetime of the returned handle, and fans projected `agent/event` -/// notifications out through the returned [`AppServerClient`]. The host owns the -/// transport and the server half of the connection (for example it spawns -/// [`crate::BitfunAppServer::serve`] on the server half of an -/// [`crate::transport::in_memory_channel_pair`] before calling this). -/// -/// The connection is parked for the lifetime of the host: the shutdown signal -/// is intentionally never sent, so the spawned connection task (which owns the -/// transport) stays alive while the [`AppServerClient`] is in use. -/// -/// # Errors -/// -/// Returns an error if the connection closes or the startup handshake does not -/// complete within the startup grace window. -pub async fn connect( - transport: impl ConnectTo + 'static, -) -> std::result::Result { - // Event fan-out: the client receives `agent/event` notifications forwarded - // by the app-server, projects each envelope to the frontend shape, and - // broadcasts it to consumers. - let (event_tx, _) = broadcast::channel::(1024); - let event_tx_for_task = event_tx.clone(); - let (config_event_tx, _) = broadcast::channel::(256); - let config_event_tx_for_task = config_event_tx.clone(); - - // Park the connection handle through a oneshot, then await a never-sent - // shutdown signal so `connect_with`'s main_fn keeps the connection alive. - let (cx_tx, cx_rx) = oneshot::channel::>(); - let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); - let connect_task = tokio::spawn(async move { - let result = AppClient - .builder() - .name("bitfun-app-client") - .on_receive_notification( - { - let event_tx = event_tx_for_task.clone(); - async move |notification: SessionEventNotification, - _cx: ConnectionTo| { - let SessionEventNotification { - event: envelope, .. - } = notification; - if let Some(projected) = project_agentic_frontend_event(envelope.event) { - let _ = event_tx.send(FrontendEvent { - event: projected.event_name, - payload: projected.payload, - }); - } - Ok(()) - } - }, - agent_client_protocol::on_receive_notification!(), - ) - .on_receive_notification( - async move |notification: ConfigEventNotification, _cx: ConnectionTo| { - let ConfigEventNotification { event, .. } = notification; - let _ = config_event_tx_for_task.send(event); - Ok(()) - }, - agent_client_protocol::on_receive_notification!(), - ) - .on_receive_notification( - { - let event_tx = event_tx_for_task; - async move |notification: PermissionEventNotification, - _cx: ConnectionTo| { - let PermissionEventNotification { event, .. } = notification; - // Project the permission lifecycle event to the frontend - // `permission://event` channel the desktop host uses, so - // consumers can listen on the same name in web and desktop. - if let Ok(payload) = serde_json::to_value(&event) { - let _ = event_tx.send(FrontendEvent { - event: "permission://event".to_string(), - payload, - }); - } - Ok(()) - } - }, - agent_client_protocol::on_receive_notification!(), - ) - .connect_with(transport, async |cx: ConnectionTo| { - let _ = cx_tx.send(cx); - let _ = shutdown_rx.await; - Ok(()) - }) - .await; - if let Err(error) = &result { - log::warn!("App-server client connection ended with error: {:?}", error); - } - }); - - let connection = match tokio::time::timeout(CLIENT_STARTUP_TIMEOUT, cx_rx).await { - Ok(Ok(cx)) => cx, - Ok(Err(_)) => { - connect_task.abort(); - anyhow::bail!("App-server client connection closed before startup completed"); - } - Err(_) => { - connect_task.abort(); - anyhow::bail!("App-server client connection startup timed out"); - } - }; - - // The connection is parked for the lifetime of the host: the `shutdown_tx` - // lives in the returned handle so the parked `connect_task` main loop only - // resumes when the host calls `shutdown` (or drops the last clone). The - // Server Host holds the handle for the process lifetime and never calls - // `shutdown`, so the connection -- and therefore the in-process app-server - // `BitfunAppServer` it is paired with -- stays alive as long as the server - // does. (Previously this was `let _ = shutdown_tx;`, which dropped the - // sender immediately and let the parked main loop resume right after - // `connect` returned, killing the connection -- every subsequent RPC then - // surfaced as `send failed because receiver is gone`.) - Ok(AppServerClient { - connection: Arc::new(connection), - event_tx, - config_event_tx, - shutdown_tx: Arc::new(std::sync::Mutex::new(Some(shutdown_tx))), - }) -} diff --git a/src/crates/interfaces/app-server/src/lib.rs b/src/crates/interfaces/app-server/src/lib.rs index 19ff651f57..929a4da7cb 100644 --- a/src/crates/interfaces/app-server/src/lib.rs +++ b/src/crates/interfaces/app-server/src/lib.rs @@ -1,4 +1,4 @@ -//! BitFun generic JSON-RPC app-server surface. +//! BitFun generic JSON-RPC App Server host surface. //! //! This crate owns a protocol-agnostic JSON-RPC server/client scaffold built on //! [`agent_client_protocol`] using custom roles ([`AppServer`]/[`AppClient`]), @@ -6,12 +6,11 @@ //! own `JsonRpcRequest` / `JsonRpcNotification` types; the crate binds no //! schema method set, unlike [`bitfun_acp`]. //! -//! The optional `agent` module is the Phase 2 wiring: [`BitfunAppServer`] -//! exposes a ready set of agent kernel operations (create / list / delete / -//! submit / run / cancel) over a host-injected [`AgentRuntime`], using the -//! generic `AppServer` role and the schema in [`schema`]. Hosts that want a -//! purely schema-free scaffold can ignore `agent` / `schema` / `server` and -//! register their own message types directly on `AppServer::builder()`. +//! [`BitfunAppServer`] exposes the selected Agent Kernel and management +//! operations over a host-injected runtime. Behavior-light wire contracts are +//! owned by `bitfun-app-server-protocol`; [`schema`] remains a compatibility +//! re-export for existing server-side imports. Typed clients are owned by the +//! separate `bitfun-app-server-client` crate. //! //! # Example //! @@ -35,15 +34,12 @@ //! //! [`bitfun_acp`]: bitfun_acp //! -//! # Crate boundary (preview) +//! # Crate boundary //! //! This crate is an **internal interface crate**, not a versioned public API. //! The server-side surface ([`BitfunAppServer`], [`schema`], [`agent`]) is the -//! production path consumed by the Server Host. The client-side exports -//! ([`AppServerClient`], [`FrontendEvent`], [`connect`]) are test-only -//! utilities -- they have no production consumer and are `#[doc(hidden)]` to -//! avoid implying a stable public SDK. They will be replaced by a proper -//! versioned event envelope and connection protocol in a follow-up. +//! production path consumed by the Server Host. This crate does not own a +//! second client implementation. // Lifted from the default 128: the `AppServer` builder chains one // `ChainedHandler` layer per registered request handler, and with the @@ -55,7 +51,6 @@ #![recursion_limit = "256"] pub mod agent; -pub mod client; pub mod management; pub mod role; pub mod schema; @@ -64,12 +59,6 @@ pub mod transport; pub use agent::BitfunAppRuntime; pub use agent_client_protocol as protocol; -// `connect`, `AppServerClient`, and `FrontendEvent` are test-only utilities -// with no production consumer. They are `#[doc(hidden)]` to avoid implying a -// versioned public client SDK; they will be replaced by a proper versioned -// event envelope in a follow-up. -#[doc(hidden)] -pub use client::{connect, AppServerClient, FrontendEvent}; pub use management::{ AppManagementCapabilities, AppManagementError, AppManagementErrorKind, AppManagementResult, AppManagementService, EXTERNAL_HOOKS_CAPABILITY, EXTERNAL_SOURCES_CAPABILITY, @@ -81,8 +70,7 @@ pub use server::BitfunAppServer; /// Convenience prelude for consumers building an app-server connection. pub mod prelude { pub use crate::{ - agent, client, schema, server, transport, AppClient, AppServer, BitfunAppRuntime, - BitfunAppServer, + agent, schema, server, transport, AppClient, AppServer, BitfunAppRuntime, BitfunAppServer, }; pub use agent_client_protocol::{ Builder, ConnectionTo, Dispatch, Handled, JsonRpcNotification, JsonRpcRequest, diff --git a/src/crates/interfaces/app-server/src/schema/agent.rs b/src/crates/interfaces/app-server/src/schema/agent.rs deleted file mode 100644 index cb9fcfc937..0000000000 --- a/src/crates/interfaces/app-server/src/schema/agent.rs +++ /dev/null @@ -1,234 +0,0 @@ -use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; -use bitfun_agent_runtime::sdk::{ - AgentDialogTurnExecution, AgentDialogTurnRequest, AgentInputAttachment, AgentRunHandle, - AgentRunRequest, AgentSessionCreateRequest, AgentSessionCreateResult, - AgentSessionDeleteRequest, AgentSessionListRequest, AgentSessionSummary, - AgentSubmissionRequest, AgentSubmissionResult, AgentSubmissionSource, - AgentTurnCancellationRequest, AgentTurnCancellationResult, DialogSubmissionPolicy, - DialogSubmitOutcome, SessionSelector, -}; -use serde::{Deserialize, Serialize}; - -/// `agent/createSession` request body (wraps the port request type). -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "agent/createSession", response = CreateSessionResponse)] -pub struct CreateSessionMessage(pub AgentSessionCreateRequest); - -/// `agent/createSession` response body (wraps the port result type). -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -pub struct CreateSessionResponse(pub AgentSessionCreateResult); - -/// `agent/listSessions` request body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "agent/listSessions", response = ListSessionsResponse)] -pub struct ListSessionsMessage(pub AgentSessionListRequest); - -/// `agent/listSessions` response body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct ListSessionsResponse { - pub sessions: Vec, -} - -/// `agent/deleteSession` request body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "agent/deleteSession", response = DeleteSessionResponse)] -pub struct DeleteSessionMessage(pub AgentSessionDeleteRequest); - -/// `agent/deleteSession` response body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct DeleteSessionResponse {} - -/// `agent/submitTurn` request body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "agent/submitTurn", response = SubmitTurnResponse)] -pub struct SubmitTurnMessage(pub AgentSubmissionRequest); - -/// `agent/submitTurn` response body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -pub struct SubmitTurnResponse(pub AgentSubmissionResult); - -/// `agent/submitDialogTurn` request body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "agent/submitDialogTurn", response = SubmitDialogTurnResponse)] -pub struct SubmitDialogTurnMessage(pub SubmitDialogTurnBody); - -/// Wire form of [`AgentDialogTurnRequest`] with an optional `policy`. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde(rename_all = "camelCase")] -pub struct SubmitDialogTurnBody { - pub session_id: String, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub original_message: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub turn_id: Option, - #[serde(default, skip_serializing_if = "AgentDialogTurnExecution::is_standard")] - pub execution: AgentDialogTurnExecution, - pub agent_type: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub workspace_path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub remote_connection_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub remote_ssh_host: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub policy: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub attachments: Vec, - #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] - pub metadata: serde_json::Map, -} - -impl SubmitDialogTurnBody { - /// Build the runtime request, defaulting `policy` to the desktop UI source. - pub fn to_request(self) -> AgentDialogTurnRequest { - AgentDialogTurnRequest { - session_id: self.session_id, - message: self.message, - original_message: self.original_message, - turn_id: self.turn_id, - execution: self.execution, - agent_type: self.agent_type, - workspace_path: self.workspace_path, - remote_connection_id: self.remote_connection_id, - remote_ssh_host: self.remote_ssh_host, - policy: self.policy.unwrap_or_else(|| { - DialogSubmissionPolicy::for_source(AgentSubmissionSource::DesktopUi) - }), - reply_route: None, - prepended_reminders: Vec::new(), - attachments: self.attachments, - metadata: self.metadata, - } - } -} - -/// `agent/submitDialogTurn` response body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde(rename_all = "camelCase", tag = "status")] -pub enum SubmitDialogTurnResponse { - Started { session_id: String, turn_id: String }, - Queued { session_id: String, turn_id: String }, -} - -impl SubmitDialogTurnResponse { - pub fn from_outcome(outcome: DialogSubmitOutcome) -> Self { - match outcome { - DialogSubmitOutcome::Started { - session_id, - turn_id, - } => Self::Started { - session_id, - turn_id, - }, - DialogSubmitOutcome::Queued { - session_id, - turn_id, - } => Self::Queued { - session_id, - turn_id, - }, - } - } -} - -/// `agent/cancelTurn` request body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "agent/cancelTurn", response = CancelTurnResponse)] -pub struct CancelTurnMessage(pub AgentTurnCancellationRequest); - -/// `agent/cancelTurn` response body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -pub struct CancelTurnResponse(pub AgentTurnCancellationResult); - -/// `agent/run` request body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "agent/run", response = RunResponse)] -pub struct RunMessage { - pub session: RunSessionSpec, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub turn_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source: Option, -} - -/// Wire form of [`SessionSelector`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde(rename_all = "camelCase", tag = "kind")] -pub enum RunSessionSpec { - Existing { - session_id: String, - }, - Create { - session_name: String, - agent_type: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - workspace_path: Option, - }, -} - -impl RunSessionSpec { - pub fn to_selector(&self) -> SessionSelector { - match self { - RunSessionSpec::Existing { session_id } => SessionSelector::existing(session_id), - RunSessionSpec::Create { - session_name, - agent_type, - workspace_path, - } => SessionSelector::create(session_name, agent_type, workspace_path.clone()), - } - } -} - -/// `agent/run` response body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde(rename_all = "camelCase")] -pub struct RunResponse { - pub session_id: String, - pub turn_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent_type: Option, - #[serde(default)] - pub accepted: bool, -} - -impl RunResponse { - pub fn from_handle(handle: AgentRunHandle) -> Self { - Self { - session_id: handle.session_id, - turn_id: handle.turn_id, - agent_type: handle.agent_type, - accepted: handle.accepted, - } - } -} - -impl RunMessage { - pub fn to_run_request(&self) -> AgentRunRequest { - let mut request = AgentRunRequest::new(self.session.to_selector(), &self.message); - if let Some(turn_id) = &self.turn_id { - request = request.with_turn_id(turn_id); - } - if let Some(source) = self.source { - request = request.with_source(source); - } - request - } -} diff --git a/src/crates/interfaces/app-server/src/schema/app.rs b/src/crates/interfaces/app-server/src/schema/app.rs deleted file mode 100644 index 80e9b2d6af..0000000000 --- a/src/crates/interfaces/app-server/src/schema/app.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Behavior-light application lifecycle contracts. - -pub use bitfun_app_server_protocol::app::*; diff --git a/src/crates/interfaces/app-server/src/schema/config.rs b/src/crates/interfaces/app-server/src/schema/config.rs deleted file mode 100644 index 20b3ac4c84..0000000000 --- a/src/crates/interfaces/app-server/src/schema/config.rs +++ /dev/null @@ -1,111 +0,0 @@ -use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; -use serde::{Deserialize, Serialize}; - -pub use bitfun_app_server_protocol::config::{ - SaveCloudSpeechConfigMessage, SaveCloudSpeechConfigRequest, SaveCloudSpeechConfigResponse, - SaveCloudSpeechConfigResult, ValidateConfigMessage, ValidateConfigResponse, -}; - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "config/getAgentProfileConfigs", response = GetAgentProfileConfigsResponse)] -pub struct GetAgentProfileConfigsMessage {} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct GetAgentProfileConfigsResponse { - pub profiles: std::collections::HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "config/getAgentProfileConfig", response = GetAgentProfileConfigResponse)] -pub struct GetAgentProfileConfigMessage { - pub agent_id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -pub struct GetAgentProfileConfigResponse(pub bitfun_core::service::config::AgentProfileView); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[request(method = "config/getModelConfigs", response = GetModelConfigsResponse)] -pub struct GetModelConfigsMessage {} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -pub struct GetModelConfigsResponse { - pub models: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "config/getConfig", response = GetConfigResponse)] -#[serde(rename_all = "camelCase")] -pub struct GetConfigMessage { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(default, skip_serializing_if = "skip_if_false")] - pub skip_retry_on_not_found: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -pub struct GetConfigResponse(pub serde_json::Value); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "config/getConfigs", response = GetConfigsResponse)] -#[serde(rename_all = "camelCase")] -pub struct GetConfigsMessage { - pub paths: Vec, - #[serde(default, skip_serializing_if = "skip_if_false")] - pub skip_retry_on_not_found: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct GetConfigsResponse { - pub configs: std::collections::BTreeMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "config/setConfig", response = SetConfigResponse)] -#[serde(rename_all = "camelCase")] -pub struct SetConfigMessage { - pub path: String, - pub value: serde_json::Value, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct SetConfigResponse {} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "config/setAgentProfileConfig", response = SetAgentProfileConfigResponse)] -#[serde(rename_all = "camelCase")] -pub struct SetAgentProfileConfigMessage { - pub agent_id: String, - pub config: serde_json::Value, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct SetAgentProfileConfigResponse(pub bitfun_core::service::config::AgentProfileView); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "config/resetAgentProfileConfig", response = ResetAgentProfileConfigResponse)] -#[serde(rename_all = "camelCase")] -pub struct ResetAgentProfileConfigMessage { - pub agent_id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct ResetAgentProfileConfigResponse(pub bitfun_core::service::config::AgentProfileView); - -fn skip_if_false(value: &bool) -> bool { - !*value -} diff --git a/src/crates/interfaces/app-server/src/schema/events.rs b/src/crates/interfaces/app-server/src/schema/events.rs deleted file mode 100644 index 581399d09e..0000000000 --- a/src/crates/interfaces/app-server/src/schema/events.rs +++ /dev/null @@ -1,127 +0,0 @@ -use agent_client_protocol::JsonRpcNotification; -use serde::{Deserialize, Serialize}; - -pub use bitfun_app_server_protocol::event::{ - AgentEventNotification as SessionEventNotification, ConfigEventNotification, ConfigUpdate, - EventCursor, EventStream, EventStreamState, EventStreamStateNotification, ResyncDirective, - SyncEventsRequest, SyncEventsResponse, -}; - -/// Browser-facing projected runtime or permission event. -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcNotification)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[notification(method = "agent/frontendEvent")] -pub struct FrontendEventNotification { - /// Frontend event name, such as `agentic://session-created`. - pub event: String, - /// Projected payload in the frontend's expected shape. - pub payload: serde_json::Value, -} - -pub(crate) fn config_update_from_owner( - value: bitfun_core::service::config::ConfigUpdateEvent, -) -> ConfigUpdate { - use bitfun_core::service::config::ConfigUpdateEvent; - - match value { - ConfigUpdateEvent::ModelConfigurationUpdated => ConfigUpdate::ModelConfigurationUpdated, - ConfigUpdateEvent::AIModelUpdated { - model_id, - model_name, - } => ConfigUpdate::AiModelUpdated { - model_id, - model_name, - }, - ConfigUpdateEvent::DefaultAIModelUpdated { - model_id, - model_name, - } => ConfigUpdate::DefaultAiModelUpdated { - model_id, - model_name, - }, - ConfigUpdateEvent::AppearanceUpdated { appearance_id } => { - ConfigUpdate::AppearanceUpdated { appearance_id } - } - ConfigUpdateEvent::EditorUpdated => ConfigUpdate::EditorUpdated, - ConfigUpdateEvent::TerminalUpdated => ConfigUpdate::TerminalUpdated, - ConfigUpdateEvent::WorkspaceUpdated => ConfigUpdate::WorkspaceUpdated, - ConfigUpdateEvent::AppUpdated => ConfigUpdate::AppUpdated, - ConfigUpdateEvent::ConfigReloaded => ConfigUpdate::ConfigReloaded, - ConfigUpdateEvent::ReasoningCatalogUpdated => ConfigUpdate::ReasoningCatalogUpdated, - ConfigUpdateEvent::DebugModeConfigUpdated { - new_port, - new_log_path, - } => ConfigUpdate::DebugModeConfigUpdated { - new_port, - new_log_path, - }, - ConfigUpdateEvent::LogLevelUpdated { new_level } => { - ConfigUpdate::LogLevelUpdated { new_level } - } - ConfigUpdateEvent::LoggingSensitiveDiagnosticsUpdated { - include_sensitive_diagnostics, - } => ConfigUpdate::LoggingSensitiveDiagnosticsUpdated { - include_sensitive_diagnostics, - }, - ConfigUpdateEvent::ModelsReconciled { - invalidated_model_ids, - default_models_changed, - func_agent_models_changed, - agent_model_defaults_changed, - } => ConfigUpdate::ModelsReconciled { - invalidated_model_ids, - default_models_changed, - func_agent_models_changed, - agent_model_defaults_changed, - }, - } -} - -#[cfg(test)] -mod tests { - use bitfun_core::service::config::ConfigUpdateEvent; - use serde_json::json; - - #[test] - fn models_reconciled_preserves_all_owner_facts() { - let update = super::config_update_from_owner(ConfigUpdateEvent::ModelsReconciled { - invalidated_model_ids: vec!["model-1".to_string()], - default_models_changed: true, - func_agent_models_changed: false, - agent_model_defaults_changed: true, - }); - - assert_eq!( - serde_json::to_value(update).expect("config update should serialize"), - json!({ - "kind": "modelsReconciled", - "invalidatedModelIds": ["model-1"], - "defaultModelsChanged": true, - "funcAgentModelsChanged": false, - "agentModelDefaultsChanged": true - }) - ); - } - - #[test] - fn reasoning_catalog_update_is_projected() { - let update = super::config_update_from_owner(ConfigUpdateEvent::ReasoningCatalogUpdated); - - assert_eq!( - serde_json::to_value(update).expect("config update should serialize"), - json!({ "kind": "reasoningCatalogUpdated" }) - ); - } -} - -#[cfg(all(test, feature = "ts"))] -mod ts_exports { - use super::ConfigUpdate; - use ts_rs::{Config, TS}; - - #[test] - fn export_upstream_config_update() { - ConfigUpdate::export(&Config::from_env()) - .expect("ConfigUpdate TypeScript export should succeed"); - } -} diff --git a/src/crates/interfaces/app-server/src/schema/git.rs b/src/crates/interfaces/app-server/src/schema/git.rs deleted file mode 100644 index 7a960acc1e..0000000000 --- a/src/crates/interfaces/app-server/src/schema/git.rs +++ /dev/null @@ -1,47 +0,0 @@ -use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "git/isRepository", response = GitIsRepositoryResponse)] -pub struct GitIsRepositoryMessage(pub GitRepositoryPathRequest); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -pub struct GitIsRepositoryResponse(pub bool); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "git/getStatus", response = GitGetStatusResponse)] -pub struct GitGetStatusMessage(pub GitRepositoryPathRequest); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -pub struct GitGetStatusResponse(pub bitfun_core::service::git::GitStatus); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS))] -#[request(method = "git/getBranches", response = GitGetBranchesResponse)] -pub struct GitGetBranchesMessage(pub GitBranchesRequest); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct GitGetBranchesResponse { - pub branches: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde(rename_all = "camelCase")] -pub struct GitRepositoryPathRequest { - pub repository_path: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde(rename_all = "camelCase")] -pub struct GitBranchesRequest { - pub repository_path: String, - #[serde(default)] - pub include_remote: Option, -} diff --git a/src/crates/interfaces/app-server/src/schema/mod.rs b/src/crates/interfaces/app-server/src/schema/mod.rs index 166034b205..8390a263bc 100644 --- a/src/crates/interfaces/app-server/src/schema/mod.rs +++ b/src/crates/interfaces/app-server/src/schema/mod.rs @@ -4,20 +4,16 @@ //! complete contract. Existing consumers can continue importing types from //! `bitfun_app_server::schema` without depending on the internal layout. -mod agent; -mod app; -mod config; -mod events; -mod git; -mod i18n; -mod permission; -mod session; - -pub use agent::*; -pub use app::*; -pub use config::*; -pub use events::*; -pub use git::*; -pub use i18n::*; -pub use permission::*; -pub use session::*; +pub use bitfun_app_server_protocol::agent::*; +pub use bitfun_app_server_protocol::app::*; +pub use bitfun_app_server_protocol::config::*; +pub use bitfun_app_server_protocol::event::{ + AgentEventNotification as SessionEventNotification, ConfigEventNotification, ConfigUpdate, + EventCursor, EventStream, EventStreamState, EventStreamStateNotification, + FrontendEventNotification, PermissionEventNotification, ResyncDirective, SyncEventsRequest, + SyncEventsResponse, +}; +pub use bitfun_app_server_protocol::git::*; +pub use bitfun_app_server_protocol::i18n::*; +pub use bitfun_app_server_protocol::permission::*; +pub use bitfun_app_server_protocol::session::*; diff --git a/src/crates/interfaces/app-server/src/schema/session.rs b/src/crates/interfaces/app-server/src/schema/session.rs deleted file mode 100644 index 9b89b1f716..0000000000 --- a/src/crates/interfaces/app-server/src/schema/session.rs +++ /dev/null @@ -1,209 +0,0 @@ -use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; -use bitfun_agent_runtime::sdk::{ - AgentSessionArchiveStateRequest, AgentSessionForkAtTurnRequest, - AgentSessionForkBeforeTurnRequest, AgentSessionForkRequest, AgentSessionForkResult, - AgentSessionModeUpdateRequest, AgentSessionModelUpdateRequest, AgentSessionRenameRequest, -}; -use serde::{Deserialize, Serialize}; - -macro_rules! empty_response { - ($name:ident) => { - #[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] - #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] - pub struct $name {} - }; -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "session/rename", response = RenameSessionResponse)] -pub struct RenameSessionMessage(pub AgentSessionRenameRequest); - -empty_response!(RenameSessionResponse); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "session/setArchived", response = SetSessionArchivedResponse)] -pub struct SetSessionArchivedMessage(pub AgentSessionArchiveStateRequest); - -empty_response!(SetSessionArchivedResponse); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "session/updateModel", response = UpdateSessionModelResponse)] -pub struct UpdateSessionModelMessage(pub AgentSessionModelUpdateRequest); - -empty_response!(UpdateSessionModelResponse); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "session/updateMode", response = UpdateSessionModeResponse)] -pub struct UpdateSessionModeMessage(pub AgentSessionModeUpdateRequest); - -empty_response!(UpdateSessionModeResponse); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "session/fork", response = ForkSessionResponse)] -pub struct ForkSessionMessage(pub AgentSessionForkRequest); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "session/forkAtTurn", response = ForkSessionResponse)] -pub struct ForkSessionAtTurnMessage(pub AgentSessionForkAtTurnRequest); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "session/forkBeforeTurn", response = ForkSessionResponse)] -pub struct ForkSessionBeforeTurnMessage(pub AgentSessionForkBeforeTurnRequest); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct ForkSessionResponse(pub AgentSessionForkResult); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[request(method = "session/restore", response = RestoreSessionResponse)] -#[serde(rename_all = "camelCase")] -pub struct RestoreSessionMessage { - pub workspace_path: String, - pub session_id: String, - #[serde(default)] - pub include_internal: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub remote_connection_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub remote_ssh_host: Option, -} - -impl From for bitfun_agent_runtime::sdk::AgentSessionRestoreRequest { - fn from(value: RestoreSessionMessage) -> Self { - Self { - workspace_path: value.workspace_path, - session_id: value.session_id, - include_internal: value.include_internal, - remote_connection_id: value.remote_connection_id, - remote_ssh_host: value.remote_ssh_host, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde(rename_all = "camelCase")] -pub struct RestoreSessionResponse { - pub session: bitfun_agent_runtime::sdk::AgentSessionSummary, - pub state: SessionRuntimeState, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde( - tag = "kind", - rename_all = "camelCase", - rename_all_fields = "camelCase" -)] -#[cfg_attr(feature = "ts", ts(rename_all = "camelCase"))] -pub enum SessionRuntimeState { - Idle, - Processing { - current_turn_id: String, - phase: SessionProcessingPhase, - }, - Error { - error: String, - recoverable: bool, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -#[serde(rename_all = "camelCase")] -pub enum SessionProcessingPhase { - Starting, - Compacting, - Thinking, - Streaming, - ToolCalling, - ToolConfirming, -} - -impl From for RestoreSessionResponse { - fn from(value: bitfun_agent_runtime::sdk::AgentSessionRestoreResult) -> Self { - Self { - session: value.session, - state: value.state.into(), - } - } -} - -impl From for SessionRuntimeState { - fn from(value: bitfun_agent_runtime::session_state::SessionState) -> Self { - use bitfun_agent_runtime::session_state::SessionState; - - match value { - SessionState::Idle => Self::Idle, - SessionState::Processing { - current_turn_id, - phase, - } => Self::Processing { - current_turn_id, - phase: phase.into(), - }, - SessionState::Error { error, recoverable } => Self::Error { error, recoverable }, - } - } -} - -impl From for SessionProcessingPhase { - fn from(value: bitfun_agent_runtime::session_state::ProcessingPhase) -> Self { - use bitfun_agent_runtime::session_state::ProcessingPhase; - - match value { - ProcessingPhase::Starting => Self::Starting, - ProcessingPhase::Compacting => Self::Compacting, - ProcessingPhase::Thinking => Self::Thinking, - ProcessingPhase::Streaming => Self::Streaming, - ProcessingPhase::ToolCalling => Self::ToolCalling, - ProcessingPhase::ToolConfirming => Self::ToolConfirming, - } - } -} - -impl From for bitfun_agent_runtime::sdk::AgentSessionRestoreResult { - fn from(value: RestoreSessionResponse) -> Self { - Self { - session: value.session, - state: value.state.into(), - } - } -} - -impl From for bitfun_agent_runtime::session_state::SessionState { - fn from(value: SessionRuntimeState) -> Self { - match value { - SessionRuntimeState::Idle => Self::Idle, - SessionRuntimeState::Processing { - current_turn_id, - phase, - } => Self::Processing { - current_turn_id, - phase: phase.into(), - }, - SessionRuntimeState::Error { error, recoverable } => Self::Error { error, recoverable }, - } - } -} - -impl From for bitfun_agent_runtime::session_state::ProcessingPhase { - fn from(value: SessionProcessingPhase) -> Self { - match value { - SessionProcessingPhase::Starting => Self::Starting, - SessionProcessingPhase::Compacting => Self::Compacting, - SessionProcessingPhase::Thinking => Self::Thinking, - SessionProcessingPhase::Streaming => Self::Streaming, - SessionProcessingPhase::ToolCalling => Self::ToolCalling, - SessionProcessingPhase::ToolConfirming => Self::ToolConfirming, - } - } -} diff --git a/src/crates/interfaces/app-server/src/server.rs b/src/crates/interfaces/app-server/src/server.rs index 4dfeac8806..e83c400110 100644 --- a/src/crates/interfaces/app-server/src/server.rs +++ b/src/crates/interfaces/app-server/src/server.rs @@ -7,6 +7,7 @@ mod event_forwarder; mod fallback; mod handlers; +mod wire; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; diff --git a/src/crates/interfaces/app-server/src/server/event_forwarder.rs b/src/crates/interfaces/app-server/src/server/event_forwarder.rs index 1afbbe5f22..42c3c5bc59 100644 --- a/src/crates/interfaces/app-server/src/server/event_forwarder.rs +++ b/src/crates/interfaces/app-server/src/server/event_forwarder.rs @@ -2,9 +2,8 @@ use crate::agent::BitfunAppRuntime; use crate::management::AppManagementService; use crate::role::AppClient; use crate::schema::{ - config_update_from_owner, ConfigEventNotification, EventStream, EventStreamState, - EventStreamStateNotification, PermissionEventNotification, ResyncDirective, - SessionEventNotification, + ConfigEventNotification, EventStream, EventStreamState, EventStreamStateNotification, + PermissionEventNotification, ResyncDirective, SessionEventNotification, }; use agent_client_protocol::{ConnectionTo, Result}; use bitfun_agent_runtime::sdk::PermissionRequestEvent; @@ -111,7 +110,7 @@ pub(super) async fn run( Some(Ok(event)) => { if let Err(error) = cx.send_notification(ConfigEventNotification { cursor: event_state.next_cursor(EventStream::Config), - event: config_update_from_owner(event), + event: crate::server::wire::config_update(event), }) { log::warn!("App-server config event forwarder failed to send a notification: {:?} -- skipping this event", error); } diff --git a/src/crates/interfaces/app-server/src/server/handlers/agent.rs b/src/crates/interfaces/app-server/src/server/handlers/agent.rs index 5abffa5a5d..fc3d29668e 100644 --- a/src/crates/interfaces/app-server/src/server/handlers/agent.rs +++ b/src/crates/interfaces/app-server/src/server/handlers/agent.rs @@ -12,6 +12,7 @@ use crate::agent::{runtime_call, BitfunAppRuntime}; use crate::management::{AppManagementService, MODES_CAPABILITY}; use crate::role::{AppClient, AppServer}; use crate::schema::*; +use crate::server::wire; pub(in crate::server) fn builder( runtime: Arc, @@ -91,9 +92,9 @@ pub(in crate::server) fn builder( responder.respond_with_result( runtime .runtime() - .submit_dialog_turn(request.0.to_request()) + .submit_dialog_turn(wire::submit_dialog_turn_request(request.0)) .await - .map(SubmitDialogTurnResponse::from_outcome) + .map(wire::submit_dialog_turn_response) .map_err(|err| { BitfunAppRuntime::session_runtime_error(&session_id, err) }), @@ -107,8 +108,8 @@ pub(in crate::server) fn builder( let runtime = runtime.clone(); async move |request: RunMessage, responder, _cx| { let handle = - runtime_call(runtime.runtime().run(request.to_run_request()).await)?; - responder.respond(RunResponse::from_handle(handle)) + runtime_call(runtime.runtime().run(wire::run_request(&request)).await)?; + responder.respond(wire::run_response(handle)) } }, agent_client_protocol::on_receive_request!(), diff --git a/src/crates/interfaces/app-server/src/server/handlers/config.rs b/src/crates/interfaces/app-server/src/server/handlers/config.rs index bbd13d5500..13deb921e4 100644 --- a/src/crates/interfaces/app-server/src/server/handlers/config.rs +++ b/src/crates/interfaces/app-server/src/server/handlers/config.rs @@ -1,6 +1,7 @@ use crate::agent::{bitfun_error, config_get_error}; use crate::role::{AppClient, AppServer}; use crate::schema::*; +use crate::server::wire; use agent_client_protocol::{Builder, HandleDispatchFrom}; pub(in crate::server) fn builder() -> Builder> { @@ -11,7 +12,12 @@ pub(in crate::server) fn builder() -> Builder Builder Builder, _>>() + .map_err(bitfun_core::BitFunError::from) } .await .map(|models| GetModelConfigsResponse { models }) @@ -152,7 +163,7 @@ pub(in crate::server) fn builder() -> Builder Builder Builder> { AppServer @@ -31,7 +32,7 @@ pub(in crate::server) fn builder() -> Builder Builder, @@ -152,9 +152,9 @@ pub(in crate::server) fn builder( responder.respond_with_result( runtime .runtime() - .restore_session(request.into()) + .restore_session(wire::restore_session_request(request)) .await - .map(RestoreSessionResponse::from) + .map(wire::restore_session_response) .map_err(|error| { BitfunAppRuntime::session_runtime_error(&session_id, error) }), @@ -214,7 +214,7 @@ pub(in crate::server) fn builder( responder.respond(SyncSessionResponse { session: restored.session, - state: session_state(restored.state), + state: wire::session_state(restored.state), transcript, workspace_binding, pending_permissions, @@ -223,7 +223,6 @@ pub(in crate::server) fn builder( }, agent_client_protocol::on_receive_request!(), ) - .on_receive_request( { let runtime = runtime.clone(); @@ -418,30 +417,3 @@ fn fallback_workspace_binding(workspace_path: String) -> AgentSessionWorkspaceBi remote_ssh_host: None, } } - -fn session_state(state: SessionState) -> SessionRuntimeState { - match state { - SessionState::Idle => SessionRuntimeState::Idle, - SessionState::Processing { - current_turn_id, - phase, - } => SessionRuntimeState::Processing { - current_turn_id, - phase: processing_phase(phase), - }, - SessionState::Error { error, recoverable } => { - SessionRuntimeState::Error { error, recoverable } - } - } -} - -fn processing_phase(phase: ProcessingPhase) -> SessionProcessingPhase { - match phase { - ProcessingPhase::Starting => SessionProcessingPhase::Starting, - ProcessingPhase::Compacting => SessionProcessingPhase::Compacting, - ProcessingPhase::Thinking => SessionProcessingPhase::Thinking, - ProcessingPhase::Streaming => SessionProcessingPhase::Streaming, - ProcessingPhase::ToolCalling => SessionProcessingPhase::ToolCalling, - ProcessingPhase::ToolConfirming => SessionProcessingPhase::ToolConfirming, - } -} diff --git a/src/crates/interfaces/app-server/src/server/wire.rs b/src/crates/interfaces/app-server/src/server/wire.rs new file mode 100644 index 0000000000..178c3e46fd --- /dev/null +++ b/src/crates/interfaces/app-server/src/server/wire.rs @@ -0,0 +1,381 @@ +//! Server-owned translations between behavior-light wire DTOs and runtime or +//! service-owner types. + +use bitfun_agent_runtime::sdk::{ + AgentRunHandle, AgentRunRequest, AgentSessionRestoreRequest, AgentSessionRestoreResult, + DialogSubmitOutcome, SessionSelector, +}; +use bitfun_agent_runtime::session_state::{ProcessingPhase, SessionState}; +use bitfun_app_server_protocol::agent::{ + RunMessage, RunResponse, RunSessionSpec, SubmitDialogTurnBody, SubmitDialogTurnResponse, +}; +use bitfun_app_server_protocol::config::AgentProfileView; +use bitfun_app_server_protocol::event::ConfigUpdate; +use bitfun_app_server_protocol::git::{ + GitBranch, GitBranchStats, GitFileStatus, GitLinesChanged, GitStatus, +}; +use bitfun_app_server_protocol::session::{ + RestoreSessionMessage, RestoreSessionResponse, SessionProcessingPhase, SessionRuntimeState, +}; +use bitfun_runtime_ports::{AgentDialogTurnRequest, AgentSubmissionSource, DialogSubmissionPolicy}; + +pub(super) fn submit_dialog_turn_request(body: SubmitDialogTurnBody) -> AgentDialogTurnRequest { + body.to_request(DialogSubmissionPolicy::for_source( + AgentSubmissionSource::DesktopUi, + )) +} + +pub(super) fn submit_dialog_turn_response( + outcome: DialogSubmitOutcome, +) -> SubmitDialogTurnResponse { + match outcome { + DialogSubmitOutcome::Started { + session_id, + turn_id, + } => SubmitDialogTurnResponse::Started { + session_id, + turn_id, + }, + DialogSubmitOutcome::Queued { + session_id, + turn_id, + } => SubmitDialogTurnResponse::Queued { + session_id, + turn_id, + }, + } +} + +pub(super) fn run_request(request: &RunMessage) -> AgentRunRequest { + let session = match &request.session { + RunSessionSpec::Existing { session_id } => SessionSelector::existing(session_id), + RunSessionSpec::Create { + session_name, + agent_type, + workspace_path, + } => SessionSelector::create(session_name, agent_type, workspace_path.clone()), + }; + let mut runtime_request = AgentRunRequest::new(session, &request.message); + if let Some(turn_id) = &request.turn_id { + runtime_request = runtime_request.with_turn_id(turn_id); + } + if let Some(source) = request.source { + runtime_request = runtime_request.with_source(source); + } + runtime_request +} + +pub(super) fn run_response(handle: AgentRunHandle) -> RunResponse { + RunResponse { + session_id: handle.session_id, + turn_id: handle.turn_id, + agent_type: handle.agent_type, + accepted: handle.accepted, + } +} + +pub(super) fn restore_session_request( + request: RestoreSessionMessage, +) -> AgentSessionRestoreRequest { + AgentSessionRestoreRequest { + workspace_path: request.workspace_path, + session_id: request.session_id, + include_internal: request.include_internal, + remote_connection_id: request.remote_connection_id, + remote_ssh_host: request.remote_ssh_host, + } +} + +pub(super) fn restore_session_response( + result: AgentSessionRestoreResult, +) -> RestoreSessionResponse { + RestoreSessionResponse { + session: result.session, + state: session_state(result.state), + } +} + +pub(super) fn session_state(value: SessionState) -> SessionRuntimeState { + match value { + SessionState::Idle => SessionRuntimeState::Idle, + SessionState::Processing { + current_turn_id, + phase, + } => SessionRuntimeState::Processing { + current_turn_id, + phase: session_processing_phase(phase), + }, + SessionState::Error { error, recoverable } => { + SessionRuntimeState::Error { error, recoverable } + } + } +} + +fn session_processing_phase(value: ProcessingPhase) -> SessionProcessingPhase { + match value { + ProcessingPhase::Starting => SessionProcessingPhase::Starting, + ProcessingPhase::Compacting => SessionProcessingPhase::Compacting, + ProcessingPhase::Thinking => SessionProcessingPhase::Thinking, + ProcessingPhase::Streaming => SessionProcessingPhase::Streaming, + ProcessingPhase::ToolCalling => SessionProcessingPhase::ToolCalling, + ProcessingPhase::ToolConfirming => SessionProcessingPhase::ToolConfirming, + } +} + +pub(super) fn agent_profile_view( + value: bitfun_core::service::config::AgentProfileView, +) -> AgentProfileView { + AgentProfileView { + profile_id: value.profile_id, + enabled_tools: value.enabled_tools, + default_tools: value.default_tools, + disabled_user_skills: value.disabled_user_skills, + enabled_user_skills: value.enabled_user_skills, + } +} + +pub(super) fn model_config( + value: bitfun_core::service::config::AIModelConfig, +) -> Result { + serde_json::to_value(value) +} + +pub(super) fn git_status(value: bitfun_core::service::git::GitStatus) -> GitStatus { + GitStatus { + staged: value.staged.into_iter().map(git_file_status).collect(), + unstaged: value.unstaged.into_iter().map(git_file_status).collect(), + untracked: value.untracked, + conflicts: value.conflicts, + current_branch: value.current_branch, + ahead: value.ahead, + behind: value.behind, + } +} + +fn git_file_status(value: bitfun_core::service::git::GitFileStatus) -> GitFileStatus { + GitFileStatus { + path: value.path, + status: value.status, + index_status: value.index_status, + workdir_status: value.workdir_status, + } +} + +pub(super) fn git_branch(value: bitfun_core::service::git::GitBranch) -> GitBranch { + GitBranch { + name: value.name, + current: value.current, + remote: value.remote, + upstream: value.upstream, + ahead: value.ahead, + behind: value.behind, + last_commit: value.last_commit, + last_commit_date: value.last_commit_date, + base_branch: value.base_branch, + child_branches: value.child_branches, + merged_branches: value.merged_branches, + branch_type: value.branch_type, + has_conflicts: value.has_conflicts, + can_merge: value.can_merge, + is_stale: value.is_stale, + merge_status: value.merge_status, + stats: value.stats.map(|stats| GitBranchStats { + commit_count: stats.commit_count, + contributor_count: stats.contributor_count, + file_changes: stats.file_changes, + lines_changed: GitLinesChanged { + additions: stats.lines_changed.additions, + deletions: stats.lines_changed.deletions, + }, + activity_score: stats.activity_score, + }), + created_at: value.created_at, + last_activity_at: value.last_activity_at, + tags: value.tags, + description: value.description, + linked_issues: value.linked_issues, + } +} + +pub(super) fn config_update( + value: bitfun_core::service::config::ConfigUpdateEvent, +) -> ConfigUpdate { + use bitfun_core::service::config::ConfigUpdateEvent; + + match value { + ConfigUpdateEvent::ModelConfigurationUpdated => ConfigUpdate::ModelConfigurationUpdated, + ConfigUpdateEvent::AIModelUpdated { + model_id, + model_name, + } => ConfigUpdate::AiModelUpdated { + model_id, + model_name, + }, + ConfigUpdateEvent::DefaultAIModelUpdated { + model_id, + model_name, + } => ConfigUpdate::DefaultAiModelUpdated { + model_id, + model_name, + }, + ConfigUpdateEvent::AppearanceUpdated { appearance_id } => { + ConfigUpdate::AppearanceUpdated { appearance_id } + } + ConfigUpdateEvent::EditorUpdated => ConfigUpdate::EditorUpdated, + ConfigUpdateEvent::TerminalUpdated => ConfigUpdate::TerminalUpdated, + ConfigUpdateEvent::WorkspaceUpdated => ConfigUpdate::WorkspaceUpdated, + ConfigUpdateEvent::AppUpdated => ConfigUpdate::AppUpdated, + ConfigUpdateEvent::ConfigReloaded => ConfigUpdate::ConfigReloaded, + ConfigUpdateEvent::ReasoningCatalogUpdated => ConfigUpdate::ReasoningCatalogUpdated, + ConfigUpdateEvent::DebugModeConfigUpdated { + new_port, + new_log_path, + } => ConfigUpdate::DebugModeConfigUpdated { + new_port, + new_log_path, + }, + ConfigUpdateEvent::LogLevelUpdated { new_level } => { + ConfigUpdate::LogLevelUpdated { new_level } + } + ConfigUpdateEvent::LoggingSensitiveDiagnosticsUpdated { + include_sensitive_diagnostics, + } => ConfigUpdate::LoggingSensitiveDiagnosticsUpdated { + include_sensitive_diagnostics, + }, + ConfigUpdateEvent::ModelsReconciled { + invalidated_model_ids, + default_models_changed, + func_agent_models_changed, + agent_model_defaults_changed, + } => ConfigUpdate::ModelsReconciled { + invalidated_model_ids, + default_models_changed, + func_agent_models_changed, + agent_model_defaults_changed, + }, + } +} + +#[cfg(test)] +mod tests { + use bitfun_app_server_protocol::agent::SubmitDialogTurnBody; + use bitfun_app_server_protocol::session::{SessionProcessingPhase, SessionRuntimeState}; + use bitfun_runtime_ports::{AgentDialogTurnExecution, AgentSubmissionSource}; + + #[test] + fn missing_dialog_policy_defaults_only_at_the_server_boundary() { + let request = super::submit_dialog_turn_request(SubmitDialogTurnBody { + session_id: "session-1".to_string(), + message: "hello".to_string(), + original_message: None, + turn_id: None, + execution: AgentDialogTurnExecution::Standard, + agent_type: "general".to_string(), + workspace_path: None, + remote_connection_id: None, + remote_ssh_host: None, + policy: None, + attachments: Vec::new(), + metadata: serde_json::Map::new(), + }); + + assert_eq!( + request.policy.trigger_source, + AgentSubmissionSource::DesktopUi + ); + assert!(request.reply_route.is_none()); + assert!(request.prepended_reminders.is_empty()); + } + + #[test] + fn runtime_session_state_maps_without_changing_wire_phase_names() { + let state = super::session_state( + bitfun_agent_runtime::session_state::SessionState::Processing { + current_turn_id: "turn-1".to_string(), + phase: bitfun_agent_runtime::session_state::ProcessingPhase::ToolCalling, + }, + ); + + assert!(matches!( + state, + SessionRuntimeState::Processing { + phase: SessionProcessingPhase::ToolCalling, + .. + } + )); + } + + #[test] + fn legacy_model_config_wire_document_is_the_owner_serialization() { + let owner = bitfun_core::service::config::AIModelConfig::default(); + let expected = serde_json::to_value(&owner).expect("owner config should serialize"); + + assert_eq!( + super::model_config(owner).expect("wire projection should serialize"), + expected + ); + } + + #[test] + fn legacy_git_wire_projections_preserve_owner_json() { + let status_json = serde_json::json!({ + "staged": [{ + "path": "src/main.rs", + "status": "modified", + "index_status": "M", + "workdir_status": null + }], + "unstaged": [], + "untracked": ["notes.txt"], + "conflicts": ["Cargo.toml"], + "current_branch": "main", + "ahead": 2, + "behind": 1 + }); + let status = serde_json::from_value(status_json.clone()) + .expect("owner Git status fixture should deserialize"); + assert_eq!( + serde_json::to_value(super::git_status(status)) + .expect("wire Git status should serialize"), + status_json + ); + + let branch_json = serde_json::json!({ + "name": "feature/schema-owner", + "current": true, + "remote": false, + "upstream": "origin/feature/schema-owner", + "ahead": 3, + "behind": 0, + "last_commit": "abc123", + "last_commit_date": "2026-08-14T00:00:00Z", + "base_branch": "main", + "child_branches": ["feature/child"], + "merged_branches": [], + "branch_type": "feature", + "has_conflicts": false, + "can_merge": true, + "is_stale": false, + "merge_status": "clean", + "stats": { + "commit_count": 3, + "contributor_count": 1, + "file_changes": 4, + "lines_changed": { "additions": 20, "deletions": 5 }, + "activity_score": 8 + }, + "created_at": "2026-08-13T00:00:00Z", + "last_activity_at": "2026-08-14T00:00:00Z", + "tags": ["schema"], + "description": "schema owner migration", + "linked_issues": ["#123"] + }); + let branch = serde_json::from_value(branch_json.clone()) + .expect("owner Git branch fixture should deserialize"); + assert_eq!( + serde_json::to_value(super::git_branch(branch)) + .expect("wire Git branch should serialize"), + branch_json + ); + } +} diff --git a/src/crates/interfaces/app-server/tests/agent_kernel.rs b/src/crates/interfaces/app-server/tests/agent_kernel.rs index 1c1ab052e5..858d678117 100644 --- a/src/crates/interfaces/app-server/tests/agent_kernel.rs +++ b/src/crates/interfaces/app-server/tests/agent_kernel.rs @@ -1640,55 +1640,3 @@ struct UnknownAgentResponse; #[allow(dead_code)] fn _document_run_response_shape_in_tests(_r: RunResponse) {} - -/// The Server Host drives the app-server through the real `client::connect` -/// handle (not an inline `AppClient::builder().connect_with` main_fn like the -/// round-trip tests above). `connect` parks its main loop on a shutdown -/// receiver and returns an `AppServerClient` the host holds for the process -/// lifetime. This regression test pins that contract: after `connect` -/// returns, an RPC sent through the returned handle must still reach the -/// server and get a response. A previous version dropped `shutdown_tx` right -/// before returning (`let _ = shutdown_tx;`), which let the parked main loop -/// resume immediately, cancelling the connection's background actors -- every -/// subsequent RPC then surfaced as `send failed because receiver is gone` -/// (from `Task::spawn`'s `unbounded_send` failure). This test fails loud if -/// that regression returns, because `create_session` would error instead of -/// returning the mock session id. -#[tokio::test(flavor = "current_thread")] -async fn client_connect_keeps_connection_alive_after_return() { - let local = LocalSet::new(); - local - .run_until(async { - let (server_transport, client_transport) = transport::in_memory_channel_pair(); - let runtime = build_app_runtime(); - spawn_server(runtime, server_transport); - - let client = bitfun_app_server::connect(client_transport) - .await - .expect("app-server client should connect"); - - // The connect task must still be parked on the shutdown receiver - // here -- otherwise the connection's background actors are gone and - // this RPC surfaces `send failed because receiver is gone`. - let response = client - .create_session(AgentSessionCreateRequest { - session_name: "post-connect session".to_string(), - agent_type: "agentic".to_string(), - workspace_path: None, - project_workspace_path: None, - execution_target: None, - workspace_id: None, - remote_connection_id: None, - remote_ssh_host: None, - model_id: None, - metadata: Default::default(), - }) - .await - .expect("RPC after connect() must succeed -- connection should still be alive"); - assert_eq!(response.session_id, "example-session"); - assert_eq!(response.agent_type, "agentic"); - - client.shutdown().await; - }) - .await; -} diff --git a/src/web-ui/package.json b/src/web-ui/package.json index 510c7dea28..d83720141e 100644 --- a/src/web-ui/package.json +++ b/src/web-ui/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "vite", "dev:force": "vite --force", - "gen:types": "cargo test --package bitfun-app-server-protocol --features ts export -- --nocapture && cargo test --package bitfun-app-server --features ts --no-default-features export -- --nocapture && node scripts/gen-api-barrel.mjs", + "gen:types": "node scripts/gen-api-barrel.mjs --clean && cargo test --locked --package bitfun-app-server-protocol --no-default-features --features ts --lib export -- --nocapture && node scripts/gen-api-barrel.mjs", "build": "vite build", "build:desktop": "vite build --mode desktop", "build:web": "vite build --mode web", diff --git a/src/web-ui/scripts/gen-api-barrel.mjs b/src/web-ui/scripts/gen-api-barrel.mjs index 7f32f0423d..00de698f4f 100644 --- a/src/web-ui/scripts/gen-api-barrel.mjs +++ b/src/web-ui/scripts/gen-api-barrel.mjs @@ -1,17 +1,23 @@ // Generates src/generated/api/index.ts — a barrel re-exporting every // ts-rs-generated type. Run after `cargo test ... export` (gen:types). // Generated files are gitignored; this barrel is regenerated alongside them. -import { readdir, writeFile } from 'node:fs/promises'; +import { mkdir, readdir, rm, writeFile } from 'node:fs/promises'; import { join, basename, extname } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = fileURLToPath(new URL('.', import.meta.url)); const dir = join(here, '..', 'src', 'generated', 'api'); +if (process.argv.includes('--clean')) { + await rm(dir, { recursive: true, force: true }); + await mkdir(dir, { recursive: true }); + process.exit(0); +} + const header = `// GENERATED CODE! DO NOT MODIFY BY HAND! // -// Source: src/crates/interfaces/app-server/src/schema/ (+ upstream contract -// crates), exported via ts-rs (#[ts(export)], run by \`npm run gen:types\`). +// Source: bitfun-app-server-protocol (+ stable contract crates), exported via +// ts-rs (#[ts(export)], run by \`npm run gen:types\`). // This barrel re-exports every generated type so consumers can import from a // single path: \`import type { SubmitDialogTurnBody } from '@/generated/api'\`. // @@ -24,9 +30,25 @@ const files = (await readdir(dir, { withFileTypes: true })) .sort(); const requiredTypes = [ + 'AgentSessionArchiveStateRequest', + 'AgentSessionForkAtTurnRequest', 'ConfigUpdate', + 'ForkSessionResponse', + 'GitBranch', + 'GitRepositoryPathRequest', + 'ListSessionsResponse', + 'PermissionGrant', + 'PermissionReply', + 'RemoveProjectPermissionGrantResponse', + 'ResetAgentProfileConfigMessage', + 'ResetAgentProfileConfigResponse', + 'RunResponse', 'SaveCloudSpeechConfigRequest', 'SaveCloudSpeechConfigResult', + 'SetAgentProfileConfigMessage', + 'SetAgentProfileConfigResponse', + 'SubmitDialogTurnBody', + 'SubmitDialogTurnResponse', ]; const missingTypes = requiredTypes.filter((typeName) => !files.includes(typeName)); if (missingTypes.length > 0) { diff --git a/src/web-ui/src/infrastructure/api/adapters/websocket-adapter.ts b/src/web-ui/src/infrastructure/api/adapters/websocket-adapter.ts index 3d23f6b0de..6351becbd1 100644 --- a/src/web-ui/src/infrastructure/api/adapters/websocket-adapter.ts +++ b/src/web-ui/src/infrastructure/api/adapters/websocket-adapter.ts @@ -27,7 +27,7 @@ const log = createLogger('WebSocketAdapter'); /** * Typed mapping from the frontend's snake_case agent commands to the app-server * JSON-RPC method names, carrying the request/response types from the generated - * schema (`@/generated/api`, source: `bitfun-app-server/src/schema/`). + * schema (`@/generated/api`, source: `bitfun-app-server-protocol`). * * The service layer (`AgentAPI` and friends) speaks Tauri command names * (`create_session`, `start_dialog_turn`, ...) because that is the desktop