feat(connection): dispatch TypeRT remotes through shared API

This commit is contained in:
imccyu
2026-08-06 16:41:24 +08:00
parent 4eff751058
commit 36516e97b9
20 changed files with 439 additions and 182 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-08-02-typert-remote-method-calls.md
2026-08-02-typert-remote-method-calls.md: c3a7a77c583720c3f967de185a089d374f017d81
2026-08-02-typert-remote-method-calls.zh.md: 9b2fbbd69f1c054cbf6c86f177b743c583be3e8a
2026-08-02-typert-remote-method-calls.md: 61c8f61468621846fa8e8ff78d52313ae805aa17
2026-08-02-typert-remote-method-calls.zh.md: 1e09965d2baba2db35301288f338cef15d947f36

View File

@@ -20,7 +20,7 @@ A business Service declares callable methods with `@Remote` or `@RemoteContext()
The Remote consumer projection contains `.d.ts`, `.d.ts.map`, and `.js` files. The `.d.ts` exposes only methods marked with a Remote decorator and refers to the business package's single public type symbols. The `.d.ts.map` navigates consumer API methods back to their Host business method implementations. The `.js` carries endpoint, parameter, Context, and Zod information for the same contract. At the assembly layer, the Browser Client mounts the required Remote JS contributions onto the Client API Service. The projection and API abstraction remain platform-independent so that a future TUI can reuse them.
`@deepseek-ai/dsh-host-api-gateway`, located at `packages/host/api-gateway`, provides two symmetric faces: its default entry provides Host `ctx.typertGateway`, while its `/client` entry provides consumer-side `ctx.api`. Each side consumes a locally generated `InvocationDescriptor` from the same model; descriptors are not sent over the wire. The Remote data protocol runs over the single Connection/RPC mechanism through an isolated `/api2` channel. The business calling interface does not change when Connection migrates from HTTP to WebSocket.
`@deepseek-ai/dsh-host-api-gateway`, located at `packages/host/api-gateway`, provides two symmetric faces: its default entry provides Host `ctx.typertGateway`, while its `/client` entry provides consumer-side `ctx.api`. Each side consumes a locally generated `InvocationDescriptor` from the same model; descriptors are not sent over the wire. The Remote data protocol runs over Connection's shared `/api` RPC channel. The business calling interface does not change when Connection migrates from HTTP to WebSocket.
## Components and Cordis services
@@ -30,7 +30,7 @@ The Remote consumer projection contains `.d.ts`, `.d.ts.map`, and `.js` files. T
| TypeRT registry | `ctx.typert` | Separately stores reflection for the current environment, imported Remote contributions, lookup providers, and Context providers |
| TypeRT generator/loader | No new business service | Generates three kinds of `lib` artifacts from the Host/Client Programs and registers the current environment's artifacts with `ctx.typert` |
| Host API Gateway's Host face | `ctx.typertGateway` | Associates Host definitions with live Services, decodes parameters, resolves receivers, invokes methods, and encodes results |
| Connection | `ctx.connection` | Exclusively owns the HTTP Server/future WebSocket, RPC envelope, rpcId, serialization, trust, and error transport, while carrying the isolated `/api` and `/api2` channels |
| Connection | `ctx.connection` | Exclusively owns the HTTP Server/future WebSocket, the shared `/api` route, RPC envelope, rpcId, serialization, trust, error transport, TypeRT interception, and legacy API Proxy fallback |
| Host API Gateway's Client face | `ctx.api` | Mounts Remote contributions, materializes root and scoped APIs, and delegates canonical calls to `ctx.connection.rpc` |
| Client Remotes | No new service | Serves as the only Remote facade for Client business code, selecting and mounting `/remote` contributions while exposing the Gateway Client face and the selected API declarations |
| Agent/Session owning packages | Existing domain services | Provide both static interface merges and runtime lookup/Context providers |
@@ -139,7 +139,7 @@ Parameter order comes from the method signature. HTTP fields come from parameter
A LIB codec contains a Zod schema and a canonical `typeSymbol` consisting of "package + public subpath + export name." An SRC codec is marked only as `src-json`. When the Host and consumer run in different JavaScript realms, each holds its own Zod instances, but both sets are generated from the same TypeRT model and symbol keys.
Descriptors exist only in the local registry on each side. The wire carries only the `/api2` channel, endpoint, and `{ args }` payload. The Host uses its descriptor to decode and invoke the method, while the Client uses its corresponding descriptor to encode arguments and validate the result.
Descriptors exist only in the local registry on each side. The wire carries only the `/api` channel, endpoint, and `{ args }` payload. The Host uses its descriptor to decode and invoke the method, while the Client uses its corresponding descriptor to encode arguments and validate the result.
## TypeRT runtime registry
@@ -294,20 +294,20 @@ Client business packages depend only on `@deepseek-ai/dsh-client-remotes/client`
`ctx.api.mount()` registers a contribution with `TypeRT.remotes`, and its disposer is owned by the Cordis fiber that called the method. Duplicate endpoints, conflicting invocation modes for the same namespace and method, or conflicts between a descriptor and an existing type identity fail immediately.
The API Service materializes each `@Remote` descriptor as a real function on the root `api`. The function constructs named `args` in descriptor parameter order, applies the Client's strict codec, and then calls `ctx.connection.rpc.call('/api2', endpoint, { args })`.
The API Service materializes each `@Remote` descriptor as a real function on the root `api`. The function constructs named `args` in descriptor parameter order, applies the Client's strict codec, and then calls `ctx.connection.rpc.call('/api', endpoint, { args })`.
Neither a direct descriptor with `scope` nor a `@RemoteContext` descriptor copies functions into every Agent Scope. The API Service creates one root singleton Cordis Service for each scoped namespace and materializes methods on that Service. When `agent.goals.create()` is called, the Cordis tracker rebinds the Service's `this.ctx` to the current Agent Context. The method then asks the corresponding Context binder for identity from `this.ctx`. A direct scoped projection substitutes this identity at the lookup position named by `scope.wire`; a Context descriptor writes the identity into the receiver's separate wire field. Both issue the same kind of `/api2` call.
Neither a direct descriptor with `scope` nor a `@RemoteContext` descriptor copies functions into every Agent Scope. The API Service creates one root singleton Cordis Service for each scoped namespace and materializes methods on that Service. When `agent.goals.create()` is called, the Cordis tracker rebinds the Service's `this.ctx` to the current Agent Context. The method then asks the corresponding Context binder for identity from `this.ctx`. A direct scoped projection substitutes this identity at the lookup position named by `scope.wire`; a Context descriptor writes the identity into the receiver's separate wire field. Both issue the same kind of `/api` call.
```text
root ctx.api.goals.create(agentId, request)
→ direct descriptor
→ ctx.connection.rpc.call('/api2', 'goals/create', { args })
→ ctx.connection.rpc.call('/api', 'goals/create', { args })
agent.goals.create(request)
→ tracker 将 namespace Service rebind 到 agent Context
→ agent binder 从 caller Context 取得 agentId
→ 用 agentId 补入同一 direct descriptor 的 lookup 参数
→ ctx.connection.rpc.call('/api2', 'goals/create', { args })
→ ctx.connection.rpc.call('/api', 'goals/create', { args })
```
The Root `Context` does not merge the scoped `goals` type; only `AgentContext` gains that property through `RemoteContextApi<'agent'>`. If a caller bypasses the type system and dynamically calls a scoped method from Root, the binder reports an explicit error. If the Client already has a Cordis service with the same name, or two contributions claim the same namespace and method incompatibly, mounting fails instead of overwriting the existing service.
@@ -318,7 +318,7 @@ Generated Remote JS contains only descriptors, symbol keys, and codecs; it does
Remote API is a consumer capability, not a synonym for Browser API. This phase implements only Browser Client contribution mounting, Connection RPC calls, and Agent Scope association.
Remote DTS, Remote JS, `RemoteApi`, `InvocationDescriptor`, the Remote RPC data protocol, and Context binders must not depend on the DOM, Browser module loaders, or HTTP. Through Connection, the Browser Client encodes descriptor-materialized methods as `/api2` RPC calls.
Remote DTS, Remote JS, `RemoteApi`, `InvocationDescriptor`, the Remote RPC data protocol, and Context binders must not depend on the DOM, Browser module loaders, or HTTP. Through Connection, the Browser Client encodes descriptor-materialized methods as `/api` RPC calls.
A future TUI can join the same call abstraction without changing business decorators, Remote maps, or the shape of API calls. The TUI-visible API must still be generated exclusively from `@Remote` and `@RemoteContext`; sharing a process with the Host must not allow it to bypass Remote restrictions and expose Service methods directly.
@@ -363,22 +363,28 @@ ctx.typertGateway.invoke({ namespace, method, args })
`ctx.typertGateway.invoke()` is the carrier-independent Host entry point. It neither creates an rpcId, RPC envelope, nor HTTP response. It returns only the encoded result or raises a Gateway error that the Connection RPC adapter maps for transport.
## The `/api2` call chain
## The shared `/api` call chain
`/api2` is an isolated protocol channel on the single Connection/RPC mechanism, not a transport created by the Gateway. The Gateway registers one local handler with Connection. This phase adds the following general channel capability to the existing HTTP Connection:
Connection owns one `/api` route on the HTTP Server. The Gateway mounts a synchronous endpoint ownership test and the Remote RPC handler into Connection:
```text
ctx.connection.rpc.handle('/api2', (endpoint, payload) => {
const { namespace, method } = parseEndpoint(endpoint)
const { args } = parsePayload(payload)
return ctx.typertGateway.invoke({ namespace, method, args })
})
ctx.connection.rpc.intercept(
'/api',
endpoint => ownsRemoteEndpoint(endpoint),
(endpoint, payload) => {
const { namespace, method } = parseEndpoint(endpoint)
const { args } = parsePayload(payload)
return ctx.typertGateway.invoke({ namespace, method, args })
},
)
```
The Connection Host half obtains a handle from the single HTTP Server and reuses the same RPC bridge, request/response envelope, rpcId, serialization, trust, transport errors, and `RpcError`. Its current physical mapping is:
The Gateway claims an endpoint when the Host registry contains its strict descriptor, remembers a withdrawn strict descriptor, or finds a matching `@Remote` marker on an active SRC Service binding. A claimed endpoint stays in the Gateway after payload decoding, descriptor resolution, or invocation fails; only an endpoint that is not Remote-owned reaches the legacy API Proxy fallback.
The Connection Host half passes one composite FetchHandler to the HTTP bridge. After the bridge creates a standard `Request`, that handler selects either the Gateway RPC FetchHandler or the API Proxy FetchHandler. Both paths reuse the same request/response envelope, rpcId, serialization, trust, transport errors, and `RpcError`. The current physical mapping is:
```text
POST /api2/<namespace>/<method>
POST /api/<namespace>/<method>
```
The Remote payload is a named JSON object, not a positional array, and does not carry an `InvocationDescriptor`. A normal Goal call has this payload slot:
@@ -399,11 +405,12 @@ The complete path is:
```text
ctx.api.goals.create(sessionId, request)
→ Client InvocationDescriptor 编码 { args: { agentId, request } }
→ ctx.connection.rpc.call('/api2', 'goals/create', { args })
→ ctx.connection.rpc.call('/api', 'goals/create', { args })
→ Connection 创建 rpcId 和既有 client-request envelope
→ 当前 carrier 发送 POST /api2/goals/create
→ Connection Host half 执行 trust、反序列化和 RPC 分发
→ /api2 handler 调用 ctx.typertGateway.invoke(...)
→ 当前 carrier 发送 POST /api/goals/create
→ Connection Host half 执行共享 trust,再由 bridge 创建标准 Request
→ 复合 FetchHandler 判断 endpoint ownership 并选择目标 FetchHandler
→ TypeRT interceptor 调用 ctx.typertGateway.invoke(...)
→ Host InvocationDescriptor 解码、lookup、receiver 解析和 Reflect.apply
→ result codec 编码
→ Connection 写入既有 RPC result 并回送相同 rpcId
@@ -412,30 +419,30 @@ ctx.api.goals.create(sessionId, request)
Remote does not define a second-layer `{ ok, value/error }` response. Successful values and Gateway errors use the existing RPC response's `result` directly. The Gateway adapter maps endpoint, schema, lookup, Context, Service, and business-invocation failures to `RpcError`; Connection transports that error.
The Gateway does not handle per-method permissions, caller identity, cancellation, idempotency, or long-lived connection state. This work only extends Connection with general channel registration and invocation capabilities. It does not change existing `/api`, trusted connection, trusted-host, or privileged-method semantics. Connection's WebSocket migration remains separate follow-up work.
The Gateway does not handle per-method permissions, caller identity, cancellation, idempotency, or long-lived connection state. TypeRT endpoints use Connection's trusted-host policy; unclaimed endpoints retain the legacy API Proxy's trust and privileged-method policies. Connection's WebSocket migration remains separate follow-up work.
## Connection and protocol boundaries
The API Service owns Remote contributions, method materialization, Scope binding, and the correspondence between positional parameters and descriptors. The Gateway owns Host descriptors, lookup, Context, and business invocation. Connection only sends `/api2`, the endpoint, and `{ args }` as one RPC call to the target and returns the existing RPC result; it does not understand Goal, Agent, lookup, descriptors, or Client API types.
The API Service owns Remote contributions, method materialization, Scope binding, and the correspondence between positional parameters and descriptors. The Gateway owns Host descriptors, endpoint ownership, lookup, Context, and business invocation. Connection sends `/api`, the endpoint, and `{ args }` as one RPC call to the target and returns the existing RPC result; it does not understand Goal, Agent, lookup, descriptors, or Client API types.
`/api` and `/api2` share one Connection, Server, RPC envelope, and connection lifecycle while remaining separate protocols. When Connection migrates from HTTP to WebSocket, `/api2` naturally changes from a physical path to a logical channel. The Remote payload, business decorators, generated DTS, Remote API types, and Agent Scope programming interface remain unchanged.
The Gateway registers only its ownership matcher and RPC handler with Connection; it does not register an HTTP route. Connection mounts the shared `/api` route into the HTTP Server and gives the bridge one composite FetchHandler; that handler dispatches claimed endpoints to Gateway and unclaimed endpoints to API Proxy. A future Connection transport can preserve this order without changing the Remote payload, business decorators, generated DTS, Remote API types, or Agent Scope programming interface.
## Package boundaries
- `@deepseek-ai/dsh-type-meta`: lightweight protocols for decorators, bindings, lookup, Remote Context, and descriptors.
- TypeRT generator: analyzes Host/Client Programs, generates local faces and Remote consumer projections, and emits canonical symbol/Zod information.
- TypeRT runtime: separately stores the current environment's local reflection and imported Remote contributions.
- `@deepseek-ai/dsh-host-api-gateway`: its default entry associates Host definitions with Services, performs lookup, resolves Context receivers, invokes methods, encodes results, and registers an `/api2` handler with Connection; its `/client` entry mounts Remote contributions, creates strict API methods, and delegates calls to `ctx.connection.rpc`. The entries share the Remote protocol but do not import each other's Cordis interface merges.
- `@deepseek-ai/dsh-host-api-gateway`: its default entry associates Host definitions with Services, claims Remote endpoints, performs lookup, resolves Context receivers, invokes methods, encodes results, and registers an `/api` interceptor with Connection; its `/client` entry mounts Remote contributions, creates strict API methods, and delegates calls to `ctx.connection.rpc`. The entries share the Remote protocol but do not import each other's Cordis interface merges.
- `@deepseek-ai/dsh-client-remotes`: the only Remote facade depended on by Client business code; directly depends on the Gateway Client face, selects `/remote` contributions, and exposes the merged API types to business packages.
- Connection: owns the single HTTP Server/future WebSocket carrier, RPC envelope, rpcId, serialization, trust, and error transport while carrying the isolated `/api` and `/api2` channels.
- Connection: owns the single HTTP Server/future WebSocket carrier, shared `/api` route and composite FetchHandler, API Proxy fallback, RPC envelope, rpcId, serialization, trust, and error transport.
- Business-object packages such as Agent/Session: own lookup, Context providers, canonical ID types, and public type-only entries.
- Business Service packages: declare bindings, Remote methods, and their request/result types, and export the generated `/remote` subpath.
## Initial implementation scope
The first vertical path implements `@deepseek-ai/dsh-goal/remote → Browser Client API → Connection RPC /api2 → Host Gateway → GoalService.remoteExportCreate()` and proves that the same direct descriptor with an Agent lookup supports both `ctx.api.goals.create(agentId, request)` and `agentCtx.goals.create(request)`. The scoped-receiver semantics of `@RemoteContext('agent')` remain a separate mode.
The first vertical path implements `@deepseek-ai/dsh-goal/remote → Browser Client API → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()` and proves that the same direct descriptor with an Agent lookup supports both `ctx.api.goals.create(agentId, request)` and `agentCtx.goals.create(request)`. The scoped-receiver semantics of `@RemoteContext('agent')` remain a separate mode.
This phase implements Connection's general second-channel API and its current HTTP carrier mapping, but not WebSocket migration, the TUI runtime, a TUI carrier, or TUI Agent Scope wiring. This RFC also does not design Permission/Approval state machines, Session event streams, call authorization, cancellation, retries, idempotency, or cross-version protocol compatibility.
This phase implements Connection's shared-channel interceptor and current HTTP carrier mapping, but not WebSocket migration, the TUI runtime, a TUI carrier, or TUI Agent Scope wiring. This RFC also does not design Permission/Approval state machines, Session event streams, call authorization, cancellation, retries, idempotency, or cross-version protocol compatibility.
## Alternatives considered
@@ -455,7 +462,7 @@ This phase implements Connection's general second-channel API and its current HT
**Let a top-level `/remote` import register global state implicitly.** The target Cordis Context may not exist when ESM evaluation occurs, and ownership becomes ambiguous across multiple Contexts, HMR, and disposal. A normal value import therefore returns only a contribution, which the environment assembly explicitly mounts through the API Service.
**Create a separate transport, HTTP route, and response envelope for Remote.** This would duplicate the existing Connection's Server ownership, rpcId, serialization, trust, errors, and future WebSocket lifecycle, requiring two RPC stacks to migrate separately. `/api2` instead reuses the single Connection/RPC mechanism as an isolated protocol channel.
**Create a separate transport, HTTP route, or `/api2` channel for Remote.** This would duplicate or split Connection's Server ownership, rpcId, serialization, trust, errors, and future WebSocket lifecycle. The shared `/api` interceptor instead keeps one physical route and lets Connection preserve API Proxy as the fallback FetchHandler.
## Acceptance criteria
@@ -465,10 +472,10 @@ This phase implements Connection's general second-channel API and its current HT
- After the Client assembly mounts the JS contribution obtained from the same import, TypeRT can reflect endpoint, parameter, result, lookup, Context, and Zod information, and the API Service creates the calling method without a hand-written stub.
- Remote DTS, Remote JS, `RemoteApi`, and the descriptor protocol do not depend on Browser-specific capabilities, and the type model cannot expose unmarked Goal Service methods, preserving the boundary required for future isomorphic TUI integration.
- `agent.goals.*` obtains its call Scope through the Cordis tracker and Context binder. The Root Context has no Agent-only type, and functions are not copied into each Scope.
- `/api2/goals/create` resolves `agentId` to the canonical Agent object, invokes the original Goal Service receiver, and returns the result through the existing RPC result/error mechanism.
- `/api2` and `/api` share the single Connection/RPC carrier while remaining protocol-isolated. Remote neither registers an HTTP Server handle directly nor defines a second response envelope.
- Connection provides general channel registration and invocation capabilities and maps `/api2` to the current HTTP carrier. Existing `/api` behavior and trust semantics remain unchanged.
- This implementation does not change existing `/api`, Connection/trusted connection, Permission/Approval, or Session event stream behavior.
- `/api/goals/create` resolves `agentId` to the canonical Agent object, invokes the original Goal Service receiver, and returns the result through the existing RPC result/error mechanism.
- Gateway mounts into Connection, Connection mounts the single `/api` route into HTTP Server, and Remote defines neither an HTTP route nor a second response envelope.
- Connection's composite FetchHandler dispatches a TypeRT-owned endpoint to Gateway and falls back to API Proxy only when Gateway does not claim it. A withdrawn strict endpoint remains claimed and fails as unavailable.
- Existing API Proxy trust, privileged-method, Permission/Approval, and Session event stream behavior remains unchanged for unclaimed endpoints.
## Risks

View File

@@ -20,7 +20,7 @@ Host 与 Browser Client 使用独立的 TypeScript Program,因为两边会以
Remote 消费端投影同时包含 `.d.ts`、`.d.ts.map` 和 `.js`。`.d.ts` 只暴露被 Remote decorator 标记的方法,并引用业务包唯一的公共类型符号;`.d.ts.map` 把消费端 API 方法导航回 Host 业务方法实现;`.js` 携带同一契约的 endpoint、参数、Context 和 Zod 信息。Browser Client 在 assembly 层把需要的 Remote JS 贡献集中挂到 Client API Service;该投影和 API 抽象保持平台无关,以便未来 TUI 复用。
`@deepseek-ai/dsh-host-api-gateway` 在 `packages/host/api-gateway` 内提供对称的两个 face:默认入口提供 Host `ctx.typertGateway`,`/client` 入口提供消费端 `ctx.api`。两边各自在本地消费由同一模型生成的 `InvocationDescriptor`,descriptor 不通过 wire 发送。Remote 数据协议运行在唯一 Connection/RPC 机制之上,使用独立 `/api2` channel;业务调用界面不随 Connection 从 HTTP 迁移到 WebSocket 而改变。
`@deepseek-ai/dsh-host-api-gateway` 在 `packages/host/api-gateway` 内提供对称的两个 face:默认入口提供 Host `ctx.typertGateway`,`/client` 入口提供消费端 `ctx.api`。两边各自在本地消费由同一模型生成的 `InvocationDescriptor`,descriptor 不通过 wire 发送。Remote 数据协议运行在 Connection 共享的 `/api` RPC channel 上;业务调用界面不随 Connection 从 HTTP 迁移到 WebSocket 而改变。
## 组件和 Cordis 服务
@@ -30,7 +30,7 @@ Remote 消费端投影同时包含 `.d.ts`、`.d.ts.map` 和 `.js`。`.d.ts` 只
| TypeRT registry | `ctx.typert` | 分开保存当前环境 reflection、导入的 Remote contribution、lookup provider 和 Context provider |
| TypeRT generator/loader | 无新增业务服务 | 从 Host/Client Program 生成三类 `lib` 产物,并把当前环境产物注册到 `ctx.typert` |
| Host API Gateway 的 Host face | `ctx.typertGateway` | 关联 Host definition 与活 Service,解码参数、解析 receiver、调用方法和编码结果 |
| Connection | `ctx.connection` | 独占 HTTP Server/未来 WebSocket、RPC envelope、rpcId、序列化、trust 和错误传输,并承载 `/api` 与 `/api2` 两个隔离 channel |
| Connection | `ctx.connection` | 独占 HTTP Server/未来 WebSocket、共享 `/api` route、RPC envelope、rpcId、序列化、trust、错误传输、TypeRT 拦截和旧 API Proxy 回退 |
| Host API Gateway 的 Client face | `ctx.api` | mount Remote contribution,实体化根 API 和 scoped API,把规范调用交给 `ctx.connection.rpc` |
| Client Remotes | 无新增服务 | 作为 Client 业务的唯一 Remote facade,选择并挂载 `/remote` contribution,同时传递 Gateway Client face 和所选 API 的类型声明 |
| Agent/Session owning 包 | 既有领域服务 | 同时提供静态 interface merge 与运行时 lookup/Context provider |
@@ -139,7 +139,7 @@ InvocationDescriptor {
LIB codec 带有 Zod schema 和“package + 公共 subpath + export name”的规范 `typeSymbol`;SRC codec 只标记 `src-json`。Host 和消费端运行在不同 JavaScript realm 时会各自持有 Zod 实例,但这些实例由同一 TypeRT 模型和 symbol key 生成。
descriptor 只存在于两端本地 registry。wire 上只有 `/api2` channel、endpoint 和 `{ args }` payload;Host 用自己的 descriptor 解码和调用,Client 用自己的对应 descriptor 编码参数和验证结果。
descriptor 只存在于两端本地 registry。wire 上只有 `/api` channel、endpoint 和 `{ args }` payload;Host 用自己的 descriptor 解码和调用,Client 用自己的对应 descriptor 编码参数和验证结果。
## TypeRT 运行时 registry
@@ -294,20 +294,20 @@ Client 业务包只引用 `@deepseek-ai/dsh-client-remotes/client`,不直接
`ctx.api.mount()` 把 contribution 注册到 `TypeRT.remotes`,并由调用该方法的 Cordis fiber 持有 disposer。endpoint 重复、同一 namespace/method 模式冲突或 descriptor 与现有类型身份冲突时直接失败。
API Service 把 `@Remote` descriptor 实体化为根 `api` 上的真实函数。函数按 descriptor 的位置参数顺序构造具名 `args`,执行 Client strict codec,然后调用 `ctx.connection.rpc.call('/api2', endpoint, { args })`。
API Service 把 `@Remote` descriptor 实体化为根 `api` 上的真实函数。函数按 descriptor 的位置参数顺序构造具名 `args`,执行 Client strict codec,然后调用 `ctx.connection.rpc.call('/api', endpoint, { args })`。
带 `scope` 的 direct descriptor 和 `@RemoteContext` descriptor 都不为每个 Agent Scope 复制函数。API Service 为每个 scoped namespace 建立一个 root singleton Cordis Service,并在该 Service 上实体化方法;Cordis tracker 在 `agent.goals.create()` 调用时把 Service 的 `this.ctx` rebind 到当前 Agent Context。方法再通过对应 Context binder 从 `this.ctx` 取得 identity。direct scoped 投影用 identity 替代 `scope.wire` 指定的 lookup 位置,Context descriptor 则把 identity 写入 receiver 的独立 wire 字段;两者都发起同一种 `/api2` 调用。
带 `scope` 的 direct descriptor 和 `@RemoteContext` descriptor 都不为每个 Agent Scope 复制函数。API Service 为每个 scoped namespace 建立一个 root singleton Cordis Service,并在该 Service 上实体化方法;Cordis tracker 在 `agent.goals.create()` 调用时把 Service 的 `this.ctx` rebind 到当前 Agent Context。方法再通过对应 Context binder 从 `this.ctx` 取得 identity。direct scoped 投影用 identity 替代 `scope.wire` 指定的 lookup 位置,Context descriptor 则把 identity 写入 receiver 的独立 wire 字段;两者都发起同一种 `/api` 调用。
```text
root ctx.api.goals.create(agentId, request)
→ direct descriptor
→ ctx.connection.rpc.call('/api2', 'goals/create', { args })
→ ctx.connection.rpc.call('/api', 'goals/create', { args })
agent.goals.create(request)
→ tracker 将 namespace Service rebind 到 agent Context
→ agent binder 从 caller Context 取得 agentId
→ 用 agentId 补入同一 direct descriptor 的 lookup 参数
→ ctx.connection.rpc.call('/api2', 'goals/create', { args })
→ ctx.connection.rpc.call('/api', 'goals/create', { args })
```
Root `Context` 不 merge scoped `goals` 类型;只有 `AgentContext` 通过 `RemoteContextApi<'agent'>` 获得该属性。若调用方绕过类型从 Root 动态调用 scoped 方法,binder 明确报错。若 Client 已有同名 Cordis service,或两个 contribution 冲突占用同一 namespace/method,mount 直接失败,不覆盖现有服务。
@@ -318,7 +318,7 @@ Root `Context` 不 merge scoped `goals` 类型;只有 `AgentContext` 通过 `R
Remote API 是消费端能力,不等同于 Browser API。本期只实现 Browser Client 的 contribution 挂载、Connection RPC 调用和 Agent Scope 关联。
Remote DTS、Remote JS、`RemoteApi`、`InvocationDescriptor`、Remote RPC 数据协议和 Context binder 不得依赖 DOM、Browser module loader 或 HTTP。Browser Client 通过 Connection 把 descriptor 实体化的方法编码为 `/api2` RPC 调用。
Remote DTS、Remote JS、`RemoteApi`、`InvocationDescriptor`、Remote RPC 数据协议和 Context binder 不得依赖 DOM、Browser module loader 或 HTTP。Browser Client 通过 Connection 把 descriptor 实体化的方法编码为 `/api` RPC 调用。
未来 TUI 可以在不改变业务 decorator、Remote maps 和 API 调用形状的前提下接入同一调用抽象。届时 TUI 可见的 API 仍只能由 `@Remote` 和 `@RemoteContext` 生成,不能因为它与 Host 同进程就绕过 Remote 限制直接暴露 Service 方法。
@@ -363,22 +363,28 @@ ctx.typertGateway.invoke({ namespace, method, args })
`ctx.typertGateway.invoke()` 是 carrier-independent 的 Host 入口。它不创建 rpcId、RPC envelope 或 HTTP response;它只返回编码结果,或产生由 Connection RPC adapter 映射的 Gateway 错误。
## `/api2` 调用链
## 共享 `/api` 调用链
`/api2` 是唯一 Connection/RPC 机制上的独立协议 channel,不是 Gateway 自建的 transport。Gateway 只向 Connection 注册一个本地 handler;本期在现有 HTTP Connection 中增加这项通用 channel 能力:
Connection 在 HTTP Server 上持有唯一 `/api` route。Gateway 把同步 endpoint ownership 判断和 Remote RPC handler 挂到 Connection:
```text
ctx.connection.rpc.handle('/api2', (endpoint, payload) => {
const { namespace, method } = parseEndpoint(endpoint)
const { args } = parsePayload(payload)
return ctx.typertGateway.invoke({ namespace, method, args })
})
ctx.connection.rpc.intercept(
'/api',
endpoint => ownsRemoteEndpoint(endpoint),
(endpoint, payload) => {
const { namespace, method } = parseEndpoint(endpoint)
const { args } = parsePayload(payload)
return ctx.typertGateway.invoke({ namespace, method, args })
},
)
```
Connection Host half 从唯一 HTTP Server 取得 handle,复用同一 RPC bridge、request/response envelope、rpcId、序列化、trust、transport error 和 `RpcError`。当前物理映射是:
Host registry 中存在 strict descriptor、记录过已撤回的 strict descriptor,或 active SRC Service binding 上存在匹配的 `@Remote` 标记时,Gateway 认领该 endpoint。endpoint 一旦被认领,即使 payload 解码、descriptor 解析或调用失败也继续由 Gateway 返回错误;只有不属于 Remote 的 endpoint 才进入旧 API Proxy 回退。
Connection Host half 把一个复合 FetchHandler 交给 HTTP bridge。bridge 创建标准 `Request` 后,该 handler 再选择 Gateway RPC FetchHandler 或 API Proxy FetchHandler;两条路径复用同一 request/response envelope、rpcId、序列化、trust、transport error 和 `RpcError`。当前物理映射是:
```text
POST /api2/<namespace>/<method>
POST /api/<namespace>/<method>
```
Remote payload 使用具名 JSON 对象,不使用位置数组,也不发送 `InvocationDescriptor`。普通 Goal 调用的 payload slot 是:
@@ -399,11 +405,12 @@ Remote payload 使用具名 JSON 对象,不使用位置数组,也不发送 `
```text
ctx.api.goals.create(sessionId, request)
→ Client InvocationDescriptor 编码 { args: { agentId, request } }
→ ctx.connection.rpc.call('/api2', 'goals/create', { args })
→ ctx.connection.rpc.call('/api', 'goals/create', { args })
→ Connection 创建 rpcId 和既有 client-request envelope
→ 当前 carrier 发送 POST /api2/goals/create
→ Connection Host half 执行 trust、反序列化和 RPC 分发
→ /api2 handler 调用 ctx.typertGateway.invoke(...)
→ 当前 carrier 发送 POST /api/goals/create
→ Connection Host half 执行共享 trust,再由 bridge 创建标准 Request
→ 复合 FetchHandler 判断 endpoint ownership 并选择目标 FetchHandler
→ TypeRT interceptor 调用 ctx.typertGateway.invoke(...)
→ Host InvocationDescriptor 解码、lookup、receiver 解析和 Reflect.apply
→ result codec 编码
→ Connection 写入既有 RPC result 并回送相同 rpcId
@@ -412,30 +419,30 @@ ctx.api.goals.create(sessionId, request)
Remote 不定义第二层 `{ ok, value/error }` response。成功值和 Gateway 错误直接使用既有 RPC response 的 `result`;Gateway adapter 负责把 endpoint、schema、lookup、Context、Service 和业务调用失败映射为 `RpcError`,Connection 负责传输该错误。
Gateway 不处理逐方法权限、调用者身份、取消、幂等或长连接状态。本工作只扩展 Connection 的通用 channel 注册和调用能力,不改变现有 `/api`、trusted connection、trusted-host 或 privileged method 语义;Connection/WebSocket 迁移后续独立完成。
Gateway 不处理逐方法权限、调用者身份、取消、幂等或长连接状态。TypeRT endpoint 使用 Connection 的 trusted-host 策略;未认领 endpoint 保留旧 API Proxy 的 trust 和 privileged-method 策略。Connection/WebSocket 迁移后续独立完成。
## Connection 与协议边界
API Service 负责 Remote contribution、方法实体化、Scope 绑定以及位置参数与 descriptor 的对应。Gateway 负责 Host descriptor、lookup、Context 和业务调用。Connection 只负责把 `/api2`、endpoint 和 `{ args }` 作为一个 RPC 调用发送到目标并返回既有 RPC result;它不理解 Goal、Agent、lookup、descriptor 或 Client API 类型。
API Service 负责 Remote contribution、方法实体化、Scope 绑定以及位置参数与 descriptor 的对应。Gateway 负责 Host descriptor、endpoint ownership、lookup、Context 和业务调用。Connection 把 `/api`、endpoint 和 `{ args }` 作为一个 RPC 调用发送到目标并返回既有 RPC result;它不理解 Goal、Agent、lookup、descriptor 或 Client API 类型。
`/api` 与 `/api2` 共享唯一 Connection、Server、RPC envelope 和连接生命周期,但保持协议隔离。Connection 从 HTTP 迁移到 WebSocket 时,`/api2` 从物理路径自然变成逻辑 channel;Remote payload、业务 decorator、生成的 DTS、Remote API 类型和 Agent Scope 编程界面都不变化。
Gateway 只向 Connection 注册 ownership matcher 和 RPC handler,不注册 HTTP route。Connection 把共享 `/api` route 挂到 HTTP Server,并把一个复合 FetchHandler 交给 bridge;该 handler 将已认领 endpoint 分发给 Gateway,未认领 endpoint 则交给 API Proxy。未来 Connection transport 可以保留相同顺序,而不改变 Remote payload、业务 decorator、生成的 DTS、Remote API 类型或 Agent Scope 编程界面。
## 包边界
- `@deepseek-ai/dsh-type-meta`:轻量 decorator、binding、lookup、Remote Context 和 descriptor 协议。
- TypeRT generator:分析 Host/Client Program,生成本地 face 和 Remote 消费端投影,并生成规范 symbol/Zod 信息。
- TypeRT runtime:分别保存当前环境的 local reflection 与导入的 Remote contribution。
- `@deepseek-ai/dsh-host-api-gateway`:默认入口关联 Host definition 与 Service,执行 lookup、Context receiver 解析、调用和结果编码,并向 Connection 注册 `/api2` handler;`/client` 入口挂载 Remote contribution,创建严格 API 方法,并把调用交给 `ctx.connection.rpc`。两个入口共享 Remote 协议,但不互相导入各自的 Cordis interface merge。
- `@deepseek-ai/dsh-host-api-gateway`:默认入口关联 Host definition 与 Service,认领 Remote endpoint,执行 lookup、Context receiver 解析、调用和结果编码,并向 Connection 注册 `/api` interceptor;`/client` 入口挂载 Remote contribution,创建严格 API 方法,并把调用交给 `ctx.connection.rpc`。两个入口共享 Remote 协议,但不互相导入各自的 Cordis interface merge。
- `@deepseek-ai/dsh-client-remotes`:Client 业务唯一依赖的 Remote facade;直接依赖 Gateway Client face,选择 `/remote` contributions,并向业务包传递合并后的 API 类型。
- Connection:拥有唯一 HTTP Server/未来 WebSocket carrier、RPC envelope、rpcId、序列化、trust 和错误传输,同时承载隔离的 `/api` 与 `/api2` channel。
- Connection:拥有唯一 HTTP Server/未来 WebSocket carrier、共享 `/api` route 与复合 FetchHandler、API Proxy 回退、RPC envelope、rpcId、序列化、trust 和错误传输。
- Agent/Session 等业务对象包:拥有 lookup、Context provider、唯一 ID 类型和纯类型公共出口。
- 业务 Service 包:声明 binding、Remote 方法及其 request/result 类型,并导出生成的 `/remote` 子路径。
## 首期实现范围
第一条纵向链路实现 `@deepseek-ai/dsh-goal/remote → Browser Client API → Connection RPC /api2 → Host Gateway → GoalService.remoteExportCreate()`,并证明同一个带 Agent lookup 的 direct descriptor 同时支持 `ctx.api.goals.create(agentId, request)` 与 `agentCtx.goals.create(request)`。`@RemoteContext('agent')` 的 scoped receiver 语义继续保留为独立模式。
第一条纵向链路实现 `@deepseek-ai/dsh-goal/remote → Browser Client API → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()`,并证明同一个带 Agent lookup 的 direct descriptor 同时支持 `ctx.api.goals.create(agentId, request)` 与 `agentCtx.goals.create(request)`。`@RemoteContext('agent')` 的 scoped receiver 语义继续保留为独立模式。
本期实现 Connection 的通用第二 channel API 及当前 HTTP carrier 映射,但不实现 WebSocket 迁移、TUI runtime、TUI carrier 或 TUI Agent Scope 接线。本 RFC 也不设计 Permission/Approval 状态机、Session 事件流、调用授权、取消、重试、幂等和跨版本协议兼容。
本期实现 Connection 的共享 channel interceptor 及当前 HTTP carrier 映射,但不实现 WebSocket 迁移、TUI runtime、TUI carrier 或 TUI Agent Scope 接线。本 RFC 也不设计 Permission/Approval 状态机、Session 事件流、调用授权、取消、重试、幂等和跨版本协议兼容。
## Alternatives considered
@@ -455,7 +462,7 @@ API Service 负责 Remote contribution、方法实体化、Scope 绑定以及位
**让 `/remote` 的顶层 import 偷偷注册全局状态。** ESM 求值时未必已有目标 Cordis Context,多个 Context、HMR 和 dispose 也无法明确归属,因此普通 value import 只返回 contribution,由环境 assembly 的 API Service 显式挂载。
**为 Remote 新建独立 transport、HTTP route 和响应信封。** 这会复制现有 Connection 的 Server ownership、rpcId、序列化、trust、错误和未来 WebSocket 生命周期,并让两个 RPC 栈分别迁移,因此 `/api2` 作为独立协议 channel 复用唯一 Connection/RPC 机制。
**为 Remote 新建独立 transport、HTTP route 或 `/api2` channel。** 这会复制或拆分 Connection 的 Server ownership、rpcId、序列化、trust、错误和未来 WebSocket 生命周期。共享 `/api` interceptor 保留唯一物理 route,并让 Connection 继续以 API Proxy 作为回退 FetchHandler。
## Acceptance criteria
@@ -465,10 +472,10 @@ API Service 负责 Remote contribution、方法实体化、Scope 绑定以及位
- Client assembly 挂载同一个 import 得到的 JS contribution 后,TypeRT 能反射 endpoint、参数、结果、lookup、Context 和 Zod 信息,API Service 无需手写 stub 即可创建调用方法。
- Remote DTS、Remote JS、`RemoteApi` 和 descriptor 协议不依赖 Browser 专属能力,且类型模型无法暴露未标记的 Goal Service 方法,为未来 TUI 同构接入保留边界。
- `agent.goals.*` 通过 Cordis tracker 和 Context binder 取得调用 Scope,Root Context 不获得 Agent-only 类型,且不为每个 Scope 复制函数。
- `/api2/goals/create` 能把 `agentId` 解析为唯一 Agent 对象,调用原始 Goal Service receiver,并通过既有 RPC result/error 返回结果。
- `/api2` 与 `/api` 共享唯一 Connection/RPC carrier,但保持协议隔离;Remote 不直接注册 HTTP Server handle,也不定义第二套 response envelope。
- Connection 提供通用 channel 注册和调用能力,并把 `/api2` 映射到当前 HTTP carrier;现有 `/api` 行为与 trust 语义保持不变。
- 现有 `/api`、Connection/trusted connection、Permission/Approval 和 Session 事件流行为不因本实现改变。
- `/api/goals/create` 能把 `agentId` 解析为唯一 Agent 对象,调用原始 Goal Service receiver,并通过既有 RPC result/error 返回结果。
- Gateway 挂到 Connection,Connection 把唯一 `/api` route 挂到 HTTP Server;Remote 不定义 HTTP route 或第二套 response envelope。
- Connection 的复合 FetchHandler 将 TypeRT 认领的 endpoint 分发给 Gateway,仅在 Gateway 不认领时回退 API Proxy;已撤回的 strict endpoint 继续被认领并返回 unavailable。
- 未认领 endpoint 保留既有 API Proxy trust、privileged-method、Permission/Approval 和 Session 事件流行为。
## Risks