mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(typert): harden remote reflection boundaries
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/implemented/architecture/2026-08-02-typert-remote-method-calls.md
|
||||
2026-08-02-typert-remote-method-calls.md: 91ab8e44ff8aedf666fe3426b85b54491deb340c
|
||||
2026-08-02-typert-remote-method-calls.zh.md: 73abd53109d871076aa41af39825c80c35ac3f26
|
||||
@@ -1,6 +1,6 @@
|
||||
# Agent Note: TypeRT Gateway Targeted Method Calls
|
||||
|
||||
Status: proposed
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-02-typert-remote-method-calls.zh.md)
|
||||
|
||||
@@ -8,13 +8,13 @@ English | [中文](2026-08-02-typert-remote-method-calls.zh.md)
|
||||
|
||||
The Host API Proxy handles direct method calls, stateful interactions, and Session event streams. These concerns have different lifecycles, routing semantics, and client programming interfaces. Continuing to export all business operations through one package would couple business Services, transport protocols, state machines, and client types.
|
||||
|
||||
This proposal addresses only targeted method calls in which one request produces one result. Stateful interactions such as Permission and Approval, as well as Session event streams, do not use this design and will be designed separately.
|
||||
This decision covers only targeted method calls in which one request produces one result. Stateful interactions such as Permission and Approval, as well as Session event streams, remain separate designs.
|
||||
|
||||
The contract for a direct method call belongs to the business Service that implements it. Business developers should declare only which methods are remotely callable, without also maintaining a central API interface, routing table, parameter conversion table, client stub, and Zod schema.
|
||||
The contract for a direct method call belongs to the business Service that implements it. Business developers declare only which methods are remotely callable, without also maintaining a central API interface, routing table, parameter conversion table, client stub, and Zod schema.
|
||||
|
||||
The Host and Browser Client use separate TypeScript Programs because each side augments the Cordis `Context` type differently. A Remote projection must not import the complete Host declarations into a consumer or depend on Browser-specific types. If the TUI later reuses this programming interface, it must likewise see only methods marked Remote. TUI integration is outside the current scope, but the implementation boundary must preserve this isomorphic reuse.
|
||||
|
||||
## Proposal
|
||||
## Decision
|
||||
|
||||
A business Service declares callable methods with `@Remote` or `@RemoteContext()` and explicitly joins the Gateway through `bindTypeRTGateway()`. TypeRT generates the Host-local reflection artifact and a platform-independent Remote consumer projection from the Host Program. The Client Program continues to generate its own local reflection artifact independently.
|
||||
|
||||
@@ -24,7 +24,7 @@ The Remote consumer projection contains `.d.ts`, `.d.ts.map`, and `.js` files. T
|
||||
|
||||
## Components and Cordis services
|
||||
|
||||
| Component | Cordis service | Responsibility in this proposal |
|
||||
| Component | Cordis service | Responsibility |
|
||||
|---|---|---|
|
||||
| `@deepseek-ai/dsh-type-meta` | Declares only the minimal `ctx.typert` protocol | Decorators, bindings, descriptors, lookup/Context, and the Remote map; no dependency on the compiler, Zod, Connection, or Browser |
|
||||
| TypeRT registry | `ctx.typert` | Separately stores reflection for the current environment, imported Remote contributions, lookup providers, and Context providers |
|
||||
@@ -104,7 +104,7 @@ ctx.typert.lookups.register('agent', {
|
||||
|
||||
The static declaration tells TypeRT that `Agent` corresponds to `SessionId` on the wire. The runtime provider resolves an `agentId` in a request to the currently live `Agent` object. If either side is missing, the LIB build or the earliest resolvable runtime registration fails immediately.
|
||||
|
||||
Lookup objects such as Agent and Session may each occupy only one top-level parameter position. An ordinary JSON request may be passed as another complete parameter, but this proposal does not support `request.agent`, object destructuring, arrays of objects, nested lookups, or searching arbitrary complex structures for IDs.
|
||||
Lookup objects such as Agent and Session may each occupy only one top-level parameter position. An ordinary JSON request may be passed as another complete parameter, but this design does not support `request.agent`, object destructuring, arrays of objects, nested lookups, or searching arbitrary complex structures for IDs.
|
||||
|
||||
Remote Context uses a separate merge-extensible map and provider. The Agent package registers an `agent` Context provider that locates the Agent Context from its wire identity and resolves the Service key named by the descriptor from that Context. The Gateway does not know the internal structure of an Agent Context.
|
||||
|
||||
@@ -150,7 +150,9 @@ ctx.typert.lookups wire ID 到 Host 活对象的 provider
|
||||
ctx.typert.contexts Host Context resolver 与 Client Context binder
|
||||
```
|
||||
|
||||
Every registration returns a disposer owned by the caller's Cordis fiber. The Gateway and API Service read the current snapshot before subscribing to changes, so business Services, generated contributions, providers, and consumers can load in any order. When any dependency is disposed, its related endpoints or methods become unavailable immediately.
|
||||
Every registration returns a disposer owned by the caller's Cordis fiber. Client contribution mounting registers the descriptor set and concrete methods as one owned operation. The Host Gateway resolves descriptors, Services, and providers from current state for every claim and invocation instead of retaining endpoint registrations. Removing a strict definition, Service, or provider therefore makes the corresponding call unavailable without leaving a stale live object.
|
||||
|
||||
The lookup registry retains the stable wire declaration after its live resolver unloads. SRC parsing continues to classify the parameter as a lookup, while invocation fails with `lookup-unavailable`; it never reclassifies the incoming ID as an ordinary JSON business object. Re-registering the same key with different parameter, wire, or canonical type symbols fails for the lifetime of that TypeRT Service.
|
||||
|
||||
The registry's Host root entry has the complete `TypeRTService` interface merge. The registry implementation shared by Host and Client lives in a separate module without environment declarations. The registry's `/client` entry imports only that shared implementation and does not pass through the Host root entry, so it cannot bring Host Cordis declarations into the Client Program.
|
||||
|
||||
@@ -312,11 +314,11 @@ agent.goals.create(request)
|
||||
|
||||
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.
|
||||
|
||||
Generated Remote JS contains only descriptors, symbol keys, and codecs; it does not bundle Host Service implementations. The API Service can create real functions from that data, so this proposal does not depend on a JavaScript Proxy. A Proxy remains an implementation option but is not a source of types or reflection.
|
||||
Generated Remote JS contains only descriptors, symbol keys, and codecs; it does not bundle Host Service implementations. The API Service creates real functions from that data, so the runtime does not depend on a JavaScript Proxy. A Proxy remains an implementation option but is not a source of types or reflection.
|
||||
|
||||
## Cross-environment isomorphism constraints
|
||||
|
||||
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 API is a consumer capability, not a synonym for Browser API. The shipped runtime implements 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 `/api` RPC calls.
|
||||
|
||||
@@ -324,7 +326,7 @@ A future TUI can join the same call abstraction without changing business decora
|
||||
|
||||
TUI runtime mounting, carriers, Agent Scope association, and SRC startup wiring are outside this phase.
|
||||
|
||||
The Web already depends on build artifacts such as `lib/client.js`, so it requires a complete `build:lib` before startup. After the Host Remote contract changes, developers must rebuild the lib and then start or restart the Web. The first phase does not implement incremental watching of the Remote contract.
|
||||
The Web already depends on build artifacts such as `lib/client.js`, so it requires a complete `build:lib` before startup. After the Host Remote contract changes, developers rebuild the lib and then start or restart the Web. Incremental watching of the Remote contract is not implemented.
|
||||
|
||||
## SRC and LIB operating modes
|
||||
|
||||
@@ -340,11 +342,11 @@ At runtime, LIB only loads definitions from `lib`; it does not start the TypeScr
|
||||
|
||||
CI and releases use LIB. Moving all repository coverage to LIB is separate follow-up work and does not block this direct-method-call implementation.
|
||||
|
||||
## Host Gateway registration
|
||||
## Host Gateway resolution
|
||||
|
||||
The Host Gateway observes both TypeRT Remote definitions and the Cordis Service lifecycle. When a Service carrying the `typertGateway` facet and a definition with the same service key are both available, the Gateway registers the definition's endpoints. Their arrival order does not matter.
|
||||
The Host Gateway registers one `/api` interceptor with Connection and does not maintain a second endpoint registry. Its ownership matcher resolves each endpoint from the current TypeRT local registry or scans current Cordis Services for a matching `typertGateway` binding and SRC Remote marker. TypeRT definitions and business Services may therefore arrive in either order.
|
||||
|
||||
At startup, the Gateway reads the current snapshots of TypeRT definitions and the Cordis reflection store before subscribing to registry changes and `internal/service`. It reconciles definitions, live Services, and bindings by service key, and unregisters endpoints when a Service is replaced or disposed. If a definition, lookup provider, or Context provider is removed, dependent endpoints immediately become unavailable; the Gateway neither retains invalid objects nor degrades to invoking methods with raw IDs.
|
||||
Invocation resolves the descriptor, receiver, lookup providers, and Context provider again from current state. A current strict descriptor takes precedence over SRC. After a strict endpoint has appeared, `TypeRTLocalRegistry.hasSeen()` keeps it owned when that descriptor is withdrawn and forbids SRC fallback for the remainder of the registry lifetime; re-registering the strict descriptor restores calls. Removing a Service or provider makes invocation fail explicitly, and the Gateway neither retains invalid objects nor invokes a method with a raw lookup ID.
|
||||
|
||||
An ordinary `@Remote` call retains the original Service instance as receiver. After lookups succeed, the Gateway calls the member identified by `implementation ?? method` with parameters in descriptor order.
|
||||
|
||||
@@ -417,7 +419,7 @@ ctx.api.goals.create(sessionId, request)
|
||||
→ Client result codec 验证并返回 CreateGoalResult
|
||||
```
|
||||
|
||||
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.
|
||||
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 current adapter converts every Gateway and business-invocation failure to the existing `RpcError` envelope with `code: 'internal'`; the Gateway's structured error category remains available only in-process, while the message carries the diagnostic across Connection.
|
||||
|
||||
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.
|
||||
|
||||
@@ -438,11 +440,11 @@ The Gateway registers only its ownership matcher and RPC handler with Connection
|
||||
- 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
|
||||
## Shipped scope and deferred work
|
||||
|
||||
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.
|
||||
The shipped vertical path is `@deepseek-ai/dsh-goal/remote → Browser Client API → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()`. The same direct descriptor with an Agent lookup supports both `ctx.api.goals.create(agentId, request)` and `agentCtx.goals.create(request)`. `@RemoteContext('agent')` remains the distinct scoped-receiver mode.
|
||||
|
||||
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.
|
||||
Connection supplies the shared-channel interceptor and current HTTP carrier mapping. WebSocket migration, the TUI runtime and carrier, TUI Agent Scope wiring, Permission/Approval state machines, Session event streams, call authorization, cancellation, retries, idempotency, and cross-version protocol compatibility remain outside this decision.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -464,26 +466,24 @@ This phase implements Connection's shared-channel interceptor and current HTTP c
|
||||
|
||||
**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
|
||||
## Verification
|
||||
|
||||
- Goal Service retains its existing business method and adds a remote entry point at the end of the class through an explicit `typertGateway` and `@Remote('create') remoteExportCreate(...)`, without maintaining a second route, codec, or Client method list.
|
||||
- One clean `build:lib` generates the Host Remote contract before compiling Host and Client consumers and produces JS, DTS, and a DTS map under the business package's `lib`, importable through `/remote`.
|
||||
- After importing `@deepseek-ai/dsh-goal/remote`, a consumer project gets a strict `api.goals.create(...)` type; without the import, that namespace does not enter its types. Go to Definition on `create` follows the declaration map to the Host Service's `remoteExportCreate` implementation.
|
||||
- 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.
|
||||
- `/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.
|
||||
- Goal Service keeps its existing business method and adds an explicit `typertGateway` plus `@Remote('create') remoteExportCreate(...)`, without a second route, codec, or Client method list.
|
||||
- A clean `build:lib` emits Host and consumer Remote artifacts before Client compilation, including the business package's JS, DTS, and declaration map under `/remote`.
|
||||
- Importing `@deepseek-ai/dsh-goal/remote` adds the strict `api.goals.create(...)` type and declaration navigation to `remoteExportCreate`; omitting that import omits the namespace.
|
||||
- Mounting the same import's JS contribution supplies endpoint, parameter, result, lookup, Context, and Zod reflection and materializes the call without a handwritten stub.
|
||||
- Root and Agent-scoped calls cross the real shared `/api` carrier, resolve `agentId` to the live Agent, invoke the original Goal receiver, and return through the existing RPC envelope.
|
||||
- The Remote artifacts and maps contain only marked methods and no Browser dependency, preserving the same consumer boundary for a future TUI.
|
||||
- Lifecycle tests withdraw and remount descriptors, Services, lookups, Context providers, and Client namespaces; unavailable dependencies fail without stale calls or raw-ID fallback.
|
||||
- Unclaimed endpoints continue through the existing API Proxy path with its trust, privileged-method, Permission/Approval, and Session event-stream behavior unchanged.
|
||||
|
||||
## Risks
|
||||
## Consequences
|
||||
|
||||
Remote API types depend on generated `lib` declarations. Build orchestration must finish the Host contract pass before compiling Host and Client consumers; an incorrect order makes a clean build depend on stale artifacts.
|
||||
|
||||
Source navigation requires a Remote package to publish both its declaration map and the `src` file referenced by the map. If package `files` omits either side, types still compile but consumer navigation stops at the generated DTS. The workspace manifest check must therefore treat both as one publication contract.
|
||||
|
||||
The permissive SRC descriptor does not validate the internal structure of ordinary JSON. After a Host Remote signature changes, the Web and strict type consumers must rebuild the lib; the first phase has no incremental contract watcher.
|
||||
The permissive SRC descriptor does not validate the internal structure of ordinary JSON. After a Host Remote signature changes, the Web and strict type consumers must rebuild the lib because no incremental contract watcher exists.
|
||||
|
||||
Canonical public types require business DTOs to have type-only entries, which may expose packages whose Host types and implementation entries are currently mixed. The build rejects those boundaries instead of copying types to conceal them.
|
||||
|
||||
@@ -494,3 +494,9 @@ Browser and Host each hold their own Zod instances and cannot compare object ide
|
||||
A consumer may import a Remote contract that is not currently mounted on the Host. The types mean "this protocol capability was selected by the consumer," not that a corresponding Service currently exists in the target process; an unavailable endpoint must fail explicitly at runtime.
|
||||
|
||||
Connection's general channel API must suit both the current HTTP carrier and a future WebSocket carrier. If the API exposes `fetch`, an HTTP request, or a route handle to the Gateway/API Service, WebSocket migration will pierce the Remote layer again. Those physical objects must therefore remain internal to Connection.
|
||||
|
||||
Remote endpoints use Connection's `trusted-host` authority. Loopback is accepted by default and LAN callers require an explicit trusted-host configuration, but this layer adds no per-method caller authorization; every trusted host can invoke a mounted Remote endpoint.
|
||||
|
||||
`hasSeen()` favors strict-definition safety over SRC availability. While a strict descriptor is withdrawn, such as during HMR, the Gateway continues to claim the endpoint and reports it unavailable instead of falling back to a weak SRC descriptor. Re-registration restores it; only a TypeRT registry restart forgets the historical strict definition.
|
||||
|
||||
Connection supplies an `AbortSignal`, but Remote business signatures have no cancellation parameter. A client disconnect therefore does not cancel business work; cancellation remains deferred rather than being implied by the transport handler shape.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Agent Note: TypeRT Gateway 定向方法调用
|
||||
|
||||
Status: proposed
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-02-typert-remote-method-calls.md) | 中文
|
||||
|
||||
@@ -8,13 +8,13 @@ Status: proposed
|
||||
|
||||
Host API Proxy 同时承担直接方法调用、带状态交互和 Session 事件流。三者的生命周期、路由语义和客户端编程界面不同,继续共用一个业务导出包会让业务 Service、传输协议、状态机和客户端类型彼此耦合。
|
||||
|
||||
本方案只解决一次请求对应一次结果的定向方法调用。Permission、Approval 等带状态交互以及 Session 事件流不使用本方案,后续分别设计。
|
||||
本决策只涵盖一次请求对应一次结果的定向方法调用。Permission、Approval 等带状态交互以及 Session 事件流仍采用独立设计。
|
||||
|
||||
直接方法调用的契约属于实现该行为的业务 Service。业务开发者应只声明哪些方法可以远程调用,而不应再同步维护中央 API 接口、路由表、参数转换表、客户端 stub 和 Zod schema。
|
||||
直接方法调用的契约属于实现该行为的业务 Service。业务开发者只需声明哪些方法可以远程调用,无需再同步维护中央 API 接口、路由表、参数转换表、客户端 stub 和 Zod schema。
|
||||
|
||||
Host 与 Browser Client 使用独立的 TypeScript Program,因为两边会以不同类型合并同名 Cordis `Context`。Remote 投影不能把完整 Host 声明导入消费端,也不能依赖 Browser 专属类型;未来 TUI 若复用这套编程界面,也只能看到 Remote 标记的方法。本期不实现 TUI 接入,但实现边界不得阻断这种同构复用。
|
||||
|
||||
## Proposal
|
||||
## 决策
|
||||
|
||||
业务 Service 通过 `@Remote` 或 `@RemoteContext()` 声明可调用方法,并通过 `bindTypeRTGateway()` 显式加入 Gateway。TypeRT 从 Host Program 生成 Host 本地反射产物和平台无关的 Remote 消费端投影;Client Program 继续独立生成自己的本地反射产物。
|
||||
|
||||
@@ -24,7 +24,7 @@ Remote 消费端投影同时包含 `.d.ts`、`.d.ts.map` 和 `.js`。`.d.ts` 只
|
||||
|
||||
## 组件和 Cordis 服务
|
||||
|
||||
| 组件 | Cordis 服务 | 本方案中的职责 |
|
||||
| 组件 | Cordis 服务 | 职责 |
|
||||
|---|---|---|
|
||||
| `@deepseek-ai/dsh-type-meta` | 只声明 `ctx.typert` 的最小协议 | decorator、binding、descriptor、lookup/Context 和 Remote map;不依赖 compiler、Zod、Connection 或 Browser |
|
||||
| TypeRT registry | `ctx.typert` | 分开保存当前环境 reflection、导入的 Remote contribution、lookup provider 和 Context provider |
|
||||
@@ -104,7 +104,7 @@ ctx.typert.lookups.register('agent', {
|
||||
|
||||
静态声明让 TypeRT 知道 `Agent` 在 wire 上对应 `SessionId`;运行时 provider 负责把请求中的 `agentId` 解析为当前活的 `Agent` 对象。缺少任一侧时,LIB 构建或最早可解析的运行时注册直接失败。
|
||||
|
||||
Agent、Session 等 lookup 对象只能各自占据一个顶层参数位置。普通 JSON request 可以作为另一个完整参数传入,但本方案不支持 `request.agent`、对象解构、对象数组、嵌套 lookup 或从任意复杂结构中搜索 ID。
|
||||
Agent、Session 等 lookup 对象只能各自占据一个顶层参数位置。普通 JSON request 可以作为另一个完整参数传入,但本设计不支持 `request.agent`、对象解构、对象数组、嵌套 lookup 或从任意复杂结构中搜索 ID。
|
||||
|
||||
Remote Context 使用独立的 merge-extensible map 和 provider。Agent 包注册 `agent` Context provider,负责用 wire identity 找到 Agent Context,并从该 Context 解析 descriptor 指定的 service key;Gateway 不知道 Agent Context 的内部结构。
|
||||
|
||||
@@ -150,7 +150,9 @@ ctx.typert.lookups wire ID 到 Host 活对象的 provider
|
||||
ctx.typert.contexts Host Context resolver 与 Client Context binder
|
||||
```
|
||||
|
||||
每次注册都返回由调用方 Cordis fiber 持有的 disposer。Gateway 和 API Service 先读取当前快照再订阅变化,因此业务 Service、generated contribution、provider 和消费者可以按任意顺序加载;任一依赖 dispose 后,相关 endpoint 或方法立即失效。
|
||||
每次注册都返回由调用方 Cordis fiber 持有的 disposer。挂载 Client contribution 时,descriptor 集与具体方法会作为一项有明确所有者的操作统一注册。Host Gateway 每次认领和调用时都从当前状态解析 descriptor、Service 与提供方,不保留 endpoint 注册。因此移除 strict definition、Service 或提供方会使相应调用不可用,且不会留下陈旧的活对象。
|
||||
|
||||
lookup 注册表会在活 resolver 卸载后保留稳定的 wire 声明。SRC 解析仍会把该参数归类为 lookup,而调用会以 `lookup-unavailable` 失败;系统绝不会把传入的 ID 重新归类为普通 JSON 业务对象。在同一个 TypeRT Service 的生命周期内,以不同参数、wire 或规范类型 symbol 重新注册同一 key 会直接失败。
|
||||
|
||||
Registry 的 Host 根入口拥有完整 `TypeRTService` interface merge;Host 与 Client 共用的 registry 实现位于无环境声明的独立模块。Registry `/client` 入口只引用该共享实现,不经过 Host 根入口,因此不会把 Host Cordis 声明带入 Client Program。
|
||||
|
||||
@@ -312,11 +314,11 @@ agent.goals.create(request)
|
||||
|
||||
Root `Context` 不 merge scoped `goals` 类型;只有 `AgentContext` 通过 `RemoteContextApi<'agent'>` 获得该属性。若调用方绕过类型从 Root 动态调用 scoped 方法,binder 明确报错。若 Client 已有同名 Cordis service,或两个 contribution 冲突占用同一 namespace/method,mount 直接失败,不覆盖现有服务。
|
||||
|
||||
生成的 Remote JS 只包含 descriptor、symbol key 和 codec,不打包 Host Service 实现。API Service 可以据此创建真实函数,因此本方案不依赖 JavaScript Proxy;Proxy 可以作为实现选择,但不会成为类型或反射来源。
|
||||
生成的 Remote JS 只包含 descriptor、symbol key 和 codec,不打包 Host Service 实现。API Service 据此创建真实函数,因此运行时不依赖 JavaScript Proxy;Proxy 可以作为实现选择,但不会成为类型或反射来源。
|
||||
|
||||
## 跨环境同构约束
|
||||
|
||||
Remote API 是消费端能力,不等同于 Browser API。本期只实现 Browser Client 的 contribution 挂载、Connection RPC 调用和 Agent Scope 关联。
|
||||
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 实体化的方法编码为 `/api` RPC 调用。
|
||||
|
||||
@@ -324,7 +326,7 @@ Remote DTS、Remote JS、`RemoteApi`、`InvocationDescriptor`、Remote RPC 数
|
||||
|
||||
TUI 的 runtime 挂载、carrier、Agent Scope 关联和 SRC 启动接线均不属于本期实现。
|
||||
|
||||
Web 本身依赖 `lib/client.js` 等构建产物,因此启动 Web 前要求完整 `build:lib`。Host Remote 契约变化后必须重新执行 lib build,再启动或重启 Web;本方案不在第一阶段实现 Remote contract 的增量 watch。
|
||||
Web 本身依赖 `lib/client.js` 等构建产物,因此启动 Web 前要求完整 `build:lib`。Host Remote 契约变化后,开发者需重新执行 lib build,再启动或重启 Web;系统不实现 Remote contract 的增量 watch。
|
||||
|
||||
## SRC 与 LIB 运行模式
|
||||
|
||||
@@ -340,11 +342,11 @@ LIB 运行时只加载 `lib` 中的 definition,不启动 TypeScript compiler
|
||||
|
||||
CI 和发布运行 LIB。全仓 coverage 全部切换到 LIB 是独立后续工作,不阻塞本次直接方法调用实现。
|
||||
|
||||
## Host Gateway 注册
|
||||
## Host Gateway 解析
|
||||
|
||||
Host Gateway 同时观察 TypeRT Remote definition 和 Cordis Service 生命周期。当某个带 `typertGateway` facet 的 Service 与同 service key 的 definition 都可用时,Gateway 注册其 endpoint;两者到达顺序不影响结果。
|
||||
Host Gateway 向 Connection 注册一个 `/api` interceptor,不维护第二份 endpoint 注册表。ownership matcher 会从当前 TypeRT local 注册表解析各 endpoint,或扫描当前 Cordis Service,查找匹配的 `typertGateway` binding 与 SRC Remote 标记。因此 TypeRT definition 与业务 Service 可以按任意顺序到达。
|
||||
|
||||
Gateway 启动时先读取 TypeRT definition 和 Cordis reflection store 的当前快照,再订阅 registry change 与 `internal/service`。它按 service key reconcile definition、活 Service 和 binding;Service 被替换或 dispose 时撤销对应 endpoint。definition、lookup provider 或 Context provider 撤销时,依赖它们的 endpoint 立即不可调用,不保留失效对象或降级为原始 ID 调用。
|
||||
每次调用都会重新从当前状态解析 descriptor、receiver、lookup 提供方与 Context 提供方。当前 strict descriptor 优先于 SRC。strict endpoint 一旦出现,即使随后撤回对应 descriptor,`TypeRTLocalRegistry.hasSeen()` 仍会在注册表剩余生命周期内保持对它的认领并禁止回退 SRC;重新注册 strict descriptor 即可恢复调用。移除 Service 或提供方会让调用明确失败;Gateway 既不保留失效对象,也不会以原始 lookup ID 调用方法。
|
||||
|
||||
普通 `@Remote` 调用保留原始 Service 实例作为 receiver。lookup 成功后,Gateway 按 descriptor 的参数顺序调用 `implementation ?? method` 指定的成员。
|
||||
|
||||
@@ -417,7 +419,7 @@ ctx.api.goals.create(sessionId, request)
|
||||
→ Client result codec 验证并返回 CreateGoalResult
|
||||
```
|
||||
|
||||
Remote 不定义第二层 `{ ok, value/error }` response。成功值和 Gateway 错误直接使用既有 RPC response 的 `result`;Gateway adapter 负责把 endpoint、schema、lookup、Context、Service 和业务调用失败映射为 `RpcError`,Connection 负责传输该错误。
|
||||
Remote 不定义第二层 `{ ok, value/error }` response。成功值和 Gateway 错误直接使用既有 RPC response 的 `result`。当前 adapter 把所有 Gateway 与业务调用失败转换为既有 `RpcError` envelope,并统一使用 `code: 'internal'`;Gateway 的结构化错误分类仅在进程内保留,诊断信息则通过 message 跨 Connection 传递。
|
||||
|
||||
Gateway 不处理逐方法权限、调用者身份、取消、幂等或长连接状态。TypeRT endpoint 使用 Connection 的 trusted-host 策略;未认领 endpoint 保留旧 API Proxy 的 trust 和 privileged-method 策略。Connection/WebSocket 迁移后续独立完成。
|
||||
|
||||
@@ -438,11 +440,11 @@ Gateway 只向 Connection 注册 ownership matcher 和 RPC handler,不注册 H
|
||||
- Agent/Session 等业务对象包:拥有 lookup、Context provider、唯一 ID 类型和纯类型公共出口。
|
||||
- 业务 Service 包:声明 binding、Remote 方法及其 request/result 类型,并导出生成的 `/remote` 子路径。
|
||||
|
||||
## 首期实现范围
|
||||
## 已交付范围与后续工作
|
||||
|
||||
第一条纵向链路实现 `@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 语义继续保留为独立模式。
|
||||
已交付的纵向链路是 `@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 interceptor 及当前 HTTP carrier 映射,但不实现 WebSocket 迁移、TUI runtime、TUI carrier 或 TUI Agent Scope 接线。本 RFC 也不设计 Permission/Approval 状态机、Session 事件流、调用授权、取消、重试、幂等和跨版本协议兼容。
|
||||
Connection 提供共享 channel interceptor 与当前 HTTP carrier 映射。WebSocket 迁移、TUI runtime 与 carrier、TUI Agent Scope 接线、Permission/Approval 状态机、Session 事件流、调用授权、取消、重试、幂等及跨版本协议兼容均不属于本决策。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -464,26 +466,24 @@ Gateway 只向 Connection 注册 ownership matcher 和 RPC handler,不注册 H
|
||||
|
||||
**为 Remote 新建独立 transport、HTTP route 或 `/api2` channel。** 这会复制或拆分 Connection 的 Server ownership、rpcId、序列化、trust、错误和未来 WebSocket 生命周期。共享 `/api` interceptor 保留唯一物理 route,并让 Connection 继续以 API Proxy 作为回退 FetchHandler。
|
||||
|
||||
## Acceptance criteria
|
||||
## 验证
|
||||
|
||||
- Goal Service 保留既有业务方法,在类末尾通过显式 `typertGateway` 和 `@Remote('create') remoteExportCreate(...)` 新增远程出口,不维护第二份路由、codec 或 Client 方法清单。
|
||||
- 一次干净 `build:lib` 先生成 Host Remote contract,再完成 Host 和 Client 消费端编译,并在业务包 `lib` 下产生可通过 `/remote` 导入的 JS、DTS 和 DTS map。
|
||||
- 导入 `@deepseek-ai/dsh-goal/remote` 后,消费 project 获得严格的 `api.goals.create(...)` 类型;不导入时该 namespace 不进入类型;从 `create` 跳转定义会通过 declaration map 到达 Host Service 的 `remoteExportCreate` 实现。
|
||||
- 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 复制函数。
|
||||
- `/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 事件流行为。
|
||||
- Goal Service 保留既有业务方法,并新增显式 `typertGateway` 与 `@Remote('create') remoteExportCreate(...)`,无需第二条路由、第二份 codec 或 Client 方法清单。
|
||||
- 一次干净的 `build:lib` 会在 Client 编译前生成 Host 与消费方 Remote 产物,包括业务包 `/remote` 下的 JS、DTS 和 declaration map。
|
||||
- 导入 `@deepseek-ai/dsh-goal/remote` 会加入严格的 `api.goals.create(...)` 类型,并可通过 declaration 导航到 `remoteExportCreate`;不导入时不会出现该 namespace。
|
||||
- 挂载同一次 import 得到的 JS contribution 会提供 endpoint、参数、结果、lookup、Context 和 Zod 反射,并在无需手写 stub 的情况下实体化调用。
|
||||
- Root 与 Agent-scoped 调用会经过真实的共享 `/api` carrier,将 `agentId` 解析为活 Agent,调用原始 Goal receiver,并通过既有 RPC envelope 返回。
|
||||
- Remote 产物与 map 仅包含已标记的方法,不依赖 Browser,从而为未来 TUI 保留相同的消费方边界。
|
||||
- 生命周期测试会撤回并重新挂载 descriptor、Service、lookup、Context 提供方和 Client namespace;依赖不可用时,调用会失败,且不会使用陈旧调用或回退原始 ID。
|
||||
- 未认领 endpoint 继续使用既有 API Proxy 路径,其 trust、privileged-method、Permission/Approval 与 Session 事件流行为保持不变。
|
||||
|
||||
## Risks
|
||||
## 后果
|
||||
|
||||
Remote API 类型依赖生成的 `lib` 声明,构建编排必须在 Host 和 Client 消费端编译前完成 contract pass;顺序错误会让干净构建依赖陈旧产物。
|
||||
|
||||
源码导航依赖 Remote package 同时发布 declaration map 和 map 指向的 `src`。package `files` 漏掉任一侧时类型仍可编译,但消费端跳转会停在生成 DTS,因此 workspace manifest 校验必须把两者作为同一发布契约。
|
||||
|
||||
SRC 弱 descriptor 不验证普通 JSON 内部结构。Host Remote 签名变化后,Web 和严格类型消费者必须重新执行 lib build;第一阶段没有增量 contract watch。
|
||||
SRC 弱 descriptor 不验证普通 JSON 内部结构。Host Remote 签名变化后,Web 和严格类型消费方必须重新执行 lib build,因为系统没有增量 contract watcher。
|
||||
|
||||
公共类型唯一性要求业务 DTO 具有纯类型出口,可能暴露现有包中 Host 类型与实现入口混杂的问题。构建会拒绝这些边界,而不是复制类型掩盖问题。
|
||||
|
||||
@@ -494,3 +494,9 @@ Browser 与 Host 各自持有 Zod 实例,不能依赖对象 identity 跨 realm
|
||||
消费端可以导入 Host 当前未挂载的 Remote contract。类型表示“该协议能力已被消费端选择”,不保证目标进程当前存在对应 Service;运行时 endpoint 不可用必须明确失败。
|
||||
|
||||
Connection 的通用 channel API 必须同时适合当前 HTTP carrier 和后续 WebSocket carrier。若接口把 `fetch`、HTTP request 或 route handle 暴露给 Gateway/API Service,WebSocket 迁移会再次穿透 Remote 层,因此这些物理对象必须留在 Connection 内部。
|
||||
|
||||
Remote endpoint 使用 Connection 的 `trusted-host` authority。系统默认接受 loopback;LAN 调用方必须通过显式 trusted-host 配置接入,但本层不增加逐方法调用方授权,因此每个 trusted host 都能调用已挂载的 Remote endpoint。
|
||||
|
||||
`hasSeen()` 优先保障 strict definition 的安全性,而非 SRC 可用性。strict descriptor 撤回时(例如 HMR 期间),Gateway 会继续认领 endpoint 并报告不可用,而不会回退到弱 SRC descriptor。重新注册即可恢复;只有重启 TypeRT 注册表才会忘记历史 strict definition。
|
||||
|
||||
Connection 提供 `AbortSignal`,但 Remote 业务签名没有取消参数。因此 Client 断连不会取消业务工作;取消仍作为后续工作,而不能由 transport handler 的形状暗示已经支持。
|
||||
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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: 61c8f61468621846fa8e8ff78d52313ae805aa17
|
||||
2026-08-02-typert-remote-method-calls.zh.md: 1e09965d2baba2db35301288f338cef15d947f36
|
||||
@@ -308,7 +308,7 @@ export interface ConnectionConfig {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/client/connection/src/index.ts:31`](../packages/client/connection/src/index.ts)
|
||||
Source: [`packages/client/connection/src/index.ts:32`](../packages/client/connection/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-client-hmr`
|
||||
|
||||
|
||||
@@ -2634,7 +2634,7 @@ listPackages(filter: TypertPackageFilter = {}): TypertPackageRecord[]
|
||||
toJSONSchema(key: string, params?: z.core.ToJSONSchemaParams): z.core.JSONSchema.BaseSchema
|
||||
```
|
||||
|
||||
Source: [`packages/typert/registry/src/service.ts:324`](../../packages/typert/registry/src/service.ts)
|
||||
Source: [`packages/typert/registry/src/service.ts:346`](../../packages/typert/registry/src/service.ts)
|
||||
|
||||
## `ctx.typertGateway` — `TypertGatewayService`
|
||||
|
||||
|
||||
@@ -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 docs/core-data-structures/core.md
|
||||
core.md: eb96988abe096455c4f24ac220a6da3f266e690d
|
||||
core.zh.md: 7334b3d3a5bd088f5467a72d7357f87c4c745487
|
||||
core.md: f7cf288715a3aec2f7037f12fc983e3172a77cef
|
||||
core.zh.md: c17fd1335503c95e7f7f6f96cc286f567a8384e6
|
||||
|
||||
@@ -20,6 +20,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
|
||||
| [llm-streaming.md](llm-streaming.md) | the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` seam |
|
||||
| [token-meter.md](token-meter.md) | immutable scalar and positional replay measurements with consumed-log revisions |
|
||||
| [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context |
|
||||
| [typert.md](typert.md) | Remote invocation descriptors, lookup/Context declarations, TypeRT registries, and the Host Gateway/Client API seams |
|
||||
| [goal.md](goal.md) | persisted goal identity, lifecycle snapshots, activation, change records, and round attribution |
|
||||
| [commands.md](commands.md) | the human-command seam: definitions, adapter discovery, direct invocation, results, and parsing views |
|
||||
| [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, execution enclosure, and standalone events |
|
||||
|
||||
@@ -20,6 +20,7 @@ harness 是一个微内核:一个极小的核心加上众多插件。大多数
|
||||
| [llm-streaming.md](llm-streaming.md) | `StreamChunk` 协议格式(wire format)+ 适配器契约(adapter contract)、`BlockAssembler`、`LlmAdapter` seam |
|
||||
| [token-meter.md](token-meter.md) | 不可变的标量与位置回放度量,附带已消费日志修订号 |
|
||||
| [scope.md](scope.md) | 作用域注册标识、dispatch 载体,以及拥有的 `Scope` 上下文 |
|
||||
| [typert.md](typert.md) | Remote 调用 descriptor、lookup/Context 声明、TypeRT 注册表,以及 Host Gateway/Client API seam |
|
||||
| [goal.md](goal.md) | 持久 goal 标识、生命周期快照、激活、变更记录与 Round 归属 |
|
||||
| [commands.md](commands.md) | 人类命令 seam:定义、适配器发现、直接调用、结果与解析视图 |
|
||||
| [session.md](session.md) | 完整的 `SessionEventMap` 变体目录、`TurnTrigger`/`TurnEndReason`、`deriveMessages()`、执行封闭与独立事件 |
|
||||
|
||||
6
docs/core-data-structures/typert.i18n.yaml
Normal file
6
docs/core-data-structures/typert.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 docs/core-data-structures/typert.md
|
||||
typert.md: 9f5c63fc554a43fd0248ed08a64dcff566c83b58
|
||||
typert.zh.md: 2b74c8325a510ba39d134fa6d463dab273239772
|
||||
196
docs/core-data-structures/typert.md
Normal file
196
docs/core-data-structures/typert.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# TypeRT remote calls
|
||||
|
||||
English | [中文](typert.zh.md)
|
||||
|
||||
Types shared by generated Remote artifacts, the Host Gateway, and consumer API assemblies. The [TypeRT Gateway Agent Note](../../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md) owns the architecture and transport decisions; this page records the literal public contracts from [`dsh-type-meta`](../../packages/typert/type-meta/src/types.ts) and [`dsh-host-api-gateway`](../../packages/host/api-gateway/src/types.ts).
|
||||
|
||||
## Lookup and Context declarations
|
||||
|
||||
Business-object packages extend two empty maps through declaration merging. A lookup associates one Host object type with its wire identity; a Context declaration associates one scoped Context kind with its wire identity. Generated descriptors name these keys, while runtime providers supply the live resolution behavior.
|
||||
|
||||
```ts type-equiv
|
||||
/** Merge-extensible Host object lookup declarations. */
|
||||
interface TypeRTLookupMap {}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Merge-extensible scoped Context declarations. */
|
||||
interface TypeRTContextMap {}
|
||||
```
|
||||
|
||||
The registry retains a lookup's wire declaration after its resolver unloads. SRC discovery therefore continues to classify the parameter as a lookup and fails unavailable instead of accepting the wire value as an ordinary business object.
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable wire declaration retained after a lookup provider unloads. */
|
||||
interface TypeRTLookupDefinition {
|
||||
/** Merge-declared lookup key. */
|
||||
readonly key: string
|
||||
/** Source parameter name recognized by the SRC weak parser. */
|
||||
readonly parameter: string
|
||||
/** Wire field replacing the Host object parameter. */
|
||||
readonly wire: string
|
||||
/** Canonical Host type symbol used by strict generation. */
|
||||
readonly hostTypeSymbol: string
|
||||
/** Canonical wire type symbol used by strict generation. */
|
||||
readonly wireTypeSymbol: string
|
||||
}
|
||||
```
|
||||
|
||||
## Invocation descriptors
|
||||
|
||||
An `InvocationDescriptor` is local reflection, not a wire message. Host and consumer builds generate corresponding descriptors; the request sends only the endpoint and named `args`. Strict codecs carry generated schemas, while SRC codecs enforce JSON-safe values without structural type recovery.
|
||||
|
||||
```ts type-equiv
|
||||
/** Codec attached to one invocation parameter or result. */
|
||||
type TypeRTCodec =
|
||||
| {
|
||||
readonly mode: 'strict'
|
||||
readonly typeSymbol: string
|
||||
readonly schema: TypeRTSchema
|
||||
}
|
||||
| {
|
||||
readonly mode: 'src-json'
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One ordered business parameter in a Remote invocation. */
|
||||
interface InvocationParameterDescriptor {
|
||||
/** Source-level parameter name. */
|
||||
readonly name: string
|
||||
/** Required key in the wire `args` object. */
|
||||
readonly wire: string
|
||||
/** Whether the value is JSON or requires a registered Host lookup. */
|
||||
readonly source: 'json' | 'lookup'
|
||||
/** Lookup key when `source` is `lookup`. */
|
||||
readonly lookup?: string
|
||||
/** Boundary codec for the wire representation. */
|
||||
readonly codec: TypeRTCodec
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Carrier-independent description of one exported method invocation. */
|
||||
interface InvocationDescriptor {
|
||||
/** Globally stable generated identity. */
|
||||
readonly id: string
|
||||
/** Cordis service key owning the method. */
|
||||
readonly service: string
|
||||
/** Wire namespace, defaulting to the service key. */
|
||||
readonly namespace: string
|
||||
/** Public instance method name. */
|
||||
readonly method: string
|
||||
/** Service member invoked when the exported method name is an alias. */
|
||||
readonly implementation?: string
|
||||
/** Receiver selection mode. */
|
||||
readonly invocation:
|
||||
| { readonly kind: 'direct' }
|
||||
| {
|
||||
readonly kind: 'context'
|
||||
readonly context: string
|
||||
readonly wire: string
|
||||
readonly codec: TypeRTCodec
|
||||
}
|
||||
/** Optional consuming-Context projection for one direct lookup parameter. */
|
||||
readonly scope?: {
|
||||
/** Context kind whose Client binder supplies the identity. */
|
||||
readonly context: string
|
||||
/** Lookup parameter wire field replaced by the Context identity. */
|
||||
readonly wire: string
|
||||
}
|
||||
/** Ordered business parameters. */
|
||||
readonly parameters: readonly InvocationParameterDescriptor[]
|
||||
/** Codec for the resolved method result. */
|
||||
readonly result: TypeRTCodec
|
||||
/** Source declaration used only for diagnostics. */
|
||||
readonly sourceLocation?: InvocationSourceLocation
|
||||
}
|
||||
```
|
||||
|
||||
## TypeRT registry
|
||||
|
||||
`ctx.typert` separates current-environment descriptors, explicitly selected Remote contributions, live lookup providers, and scoped Context providers. Registrations are Cordis-owned effects and return awaitable disposers.
|
||||
|
||||
```ts type-equiv
|
||||
/** Minimal TypeRT runtime consumed through dependency inversion. */
|
||||
interface TypeRTService {
|
||||
readonly local: TypeRTLocalRegistry
|
||||
readonly remotes: TypeRTRemoteRegistry
|
||||
readonly lookups: TypeRTLookupRegistry
|
||||
readonly contexts: TypeRTContextRegistry
|
||||
}
|
||||
```
|
||||
|
||||
Generated consumer declarations merge direct namespaces into the map inherited by `ClientApi`.
|
||||
|
||||
```ts type-equiv
|
||||
/** Merge-extensible direct namespace surface generated for Client API services. */
|
||||
interface TypeRTRemoteNamespaceMap {}
|
||||
```
|
||||
|
||||
## Host Gateway
|
||||
|
||||
Connection decodes its carrier envelope before calling `ctx.typertGateway`. The request carries exact named wire fields; infrastructure and boundary failures use the Gateway's in-process error taxonomy, although the current RPC adapter folds them into the transport's `internal` error code.
|
||||
|
||||
```ts type-equiv
|
||||
/** One Remote method request after a carrier has decoded its envelope. */
|
||||
interface InvokeRemoteRequest {
|
||||
/** Remote namespace selected by the generated descriptor. */
|
||||
readonly namespace: string
|
||||
/** Exported Service method name. */
|
||||
readonly method: string
|
||||
/** Named wire values; fields must exactly match the descriptor. */
|
||||
readonly args: Readonly<Record<string, unknown>>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable infrastructure and boundary failures emitted before or after business execution. */
|
||||
type TypertGatewayErrorCode =
|
||||
| 'ambiguous-endpoint'
|
||||
| 'arguments-invalid'
|
||||
| 'binding-invalid'
|
||||
| 'context-failed'
|
||||
| 'context-not-found'
|
||||
| 'context-unavailable'
|
||||
| 'definition-unavailable'
|
||||
| 'input-invalid'
|
||||
| 'invocation-unavailable'
|
||||
| 'lookup-failed'
|
||||
| 'lookup-not-found'
|
||||
| 'lookup-unavailable'
|
||||
| 'method-unavailable'
|
||||
| 'provider-mismatch'
|
||||
| 'result-invalid'
|
||||
| 'service-unavailable'
|
||||
| 'signature-invalid'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Host dispatcher consumed by Connection adapters. */
|
||||
interface TypertGateway {
|
||||
/**
|
||||
* Invoke one live Remote method without assuming a carrier or response envelope.
|
||||
* @param request - decoded endpoint and named wire arguments.
|
||||
* @returns the validated business result.
|
||||
* @throws {@link TypertGatewayError} for dispatch, provider, or boundary failures; business errors retain their identity.
|
||||
*/
|
||||
invoke(request: InvokeRemoteRequest): Promise<unknown>
|
||||
}
|
||||
```
|
||||
|
||||
## Consumer API
|
||||
|
||||
`ctx.api` exposes only namespaces contributed by imported `/remote` artifacts. Mounting installs the generated descriptors and concrete root/scoped methods as one fiber-owned operation; no JavaScript Proxy or Host Service type enters the consumer.
|
||||
|
||||
```ts type-equiv
|
||||
/** Typed API service augmented by generated direct Remote namespaces. */
|
||||
interface ClientApi extends TypeRTRemoteNamespaceMap {
|
||||
/**
|
||||
* Mount one generated Host-for-Client contribution in the caller's fiber.
|
||||
* @param contribution - explicitly selected Remote package artifact.
|
||||
* @returns disposer withdrawing descriptors and concrete methods together.
|
||||
*/
|
||||
mount(contribution: TypeRTRemoteContribution): TypeRTDisposer
|
||||
}
|
||||
```
|
||||
196
docs/core-data-structures/typert.zh.md
Normal file
196
docs/core-data-structures/typert.zh.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# TypeRT 远程调用
|
||||
|
||||
[English](typert.md) | 中文
|
||||
|
||||
以下类型由生成的 Remote 产物、Host Gateway 与消费方 API assembly 共用。[TypeRT Gateway Agent Note](../../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md) 负责架构与传输决策;本页记录 [`dsh-type-meta`](../../packages/typert/type-meta/src/types.ts) 和 [`dsh-host-api-gateway`](../../packages/host/api-gateway/src/types.ts) 中公共契约的字面定义。
|
||||
|
||||
## Lookup 与 Context 声明
|
||||
|
||||
业务对象包通过声明合并扩展两个空 map。lookup 将一种 Host 对象类型与其 wire identity 关联;Context 声明将一种 scoped Context 类别与其 wire identity 关联。生成的 descriptor 引用这些 key,运行时提供方则提供活对象解析行为。
|
||||
|
||||
```ts type-equiv
|
||||
/** Merge-extensible Host object lookup declarations. */
|
||||
interface TypeRTLookupMap {}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Merge-extensible scoped Context declarations. */
|
||||
interface TypeRTContextMap {}
|
||||
```
|
||||
|
||||
lookup 的 resolver 卸载后,注册表仍会保留其 wire 声明。因此 SRC 发现过程会继续把该参数归类为 lookup,并因不可用而失败,而不会把 wire 值当作普通业务对象接受。
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable wire declaration retained after a lookup provider unloads. */
|
||||
interface TypeRTLookupDefinition {
|
||||
/** Merge-declared lookup key. */
|
||||
readonly key: string
|
||||
/** Source parameter name recognized by the SRC weak parser. */
|
||||
readonly parameter: string
|
||||
/** Wire field replacing the Host object parameter. */
|
||||
readonly wire: string
|
||||
/** Canonical Host type symbol used by strict generation. */
|
||||
readonly hostTypeSymbol: string
|
||||
/** Canonical wire type symbol used by strict generation. */
|
||||
readonly wireTypeSymbol: string
|
||||
}
|
||||
```
|
||||
|
||||
## 调用 descriptor
|
||||
|
||||
`InvocationDescriptor` 是本地反射信息,不是 wire message。Host 与消费方构建会生成彼此对应的 descriptor;请求只发送 endpoint 与具名 `args`。strict codec 携带生成的 schema,SRC codec 则在不恢复结构类型的前提下强制要求 JSON 安全值。
|
||||
|
||||
```ts type-equiv
|
||||
/** Codec attached to one invocation parameter or result. */
|
||||
type TypeRTCodec =
|
||||
| {
|
||||
readonly mode: 'strict'
|
||||
readonly typeSymbol: string
|
||||
readonly schema: TypeRTSchema
|
||||
}
|
||||
| {
|
||||
readonly mode: 'src-json'
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One ordered business parameter in a Remote invocation. */
|
||||
interface InvocationParameterDescriptor {
|
||||
/** Source-level parameter name. */
|
||||
readonly name: string
|
||||
/** Required key in the wire `args` object. */
|
||||
readonly wire: string
|
||||
/** Whether the value is JSON or requires a registered Host lookup. */
|
||||
readonly source: 'json' | 'lookup'
|
||||
/** Lookup key when `source` is `lookup`. */
|
||||
readonly lookup?: string
|
||||
/** Boundary codec for the wire representation. */
|
||||
readonly codec: TypeRTCodec
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Carrier-independent description of one exported method invocation. */
|
||||
interface InvocationDescriptor {
|
||||
/** Globally stable generated identity. */
|
||||
readonly id: string
|
||||
/** Cordis service key owning the method. */
|
||||
readonly service: string
|
||||
/** Wire namespace, defaulting to the service key. */
|
||||
readonly namespace: string
|
||||
/** Public instance method name. */
|
||||
readonly method: string
|
||||
/** Service member invoked when the exported method name is an alias. */
|
||||
readonly implementation?: string
|
||||
/** Receiver selection mode. */
|
||||
readonly invocation:
|
||||
| { readonly kind: 'direct' }
|
||||
| {
|
||||
readonly kind: 'context'
|
||||
readonly context: string
|
||||
readonly wire: string
|
||||
readonly codec: TypeRTCodec
|
||||
}
|
||||
/** Optional consuming-Context projection for one direct lookup parameter. */
|
||||
readonly scope?: {
|
||||
/** Context kind whose Client binder supplies the identity. */
|
||||
readonly context: string
|
||||
/** Lookup parameter wire field replaced by the Context identity. */
|
||||
readonly wire: string
|
||||
}
|
||||
/** Ordered business parameters. */
|
||||
readonly parameters: readonly InvocationParameterDescriptor[]
|
||||
/** Codec for the resolved method result. */
|
||||
readonly result: TypeRTCodec
|
||||
/** Source declaration used only for diagnostics. */
|
||||
readonly sourceLocation?: InvocationSourceLocation
|
||||
}
|
||||
```
|
||||
|
||||
## TypeRT 注册表
|
||||
|
||||
`ctx.typert` 分开保存当前环境的 descriptor、显式选择的 Remote contribution、活 lookup 提供方与 scoped Context 提供方。各项注册都是由 Cordis 持有的 effect,并返回可等待的 disposer。
|
||||
|
||||
```ts type-equiv
|
||||
/** Minimal TypeRT runtime consumed through dependency inversion. */
|
||||
interface TypeRTService {
|
||||
readonly local: TypeRTLocalRegistry
|
||||
readonly remotes: TypeRTRemoteRegistry
|
||||
readonly lookups: TypeRTLookupRegistry
|
||||
readonly contexts: TypeRTContextRegistry
|
||||
}
|
||||
```
|
||||
|
||||
生成的消费方声明会把 direct namespace 合并到 `ClientApi` 继承的 map 中。
|
||||
|
||||
```ts type-equiv
|
||||
/** Merge-extensible direct namespace surface generated for Client API services. */
|
||||
interface TypeRTRemoteNamespaceMap {}
|
||||
```
|
||||
|
||||
## Host Gateway
|
||||
|
||||
Connection 会先解码 carrier envelope,再调用 `ctx.typertGateway`。请求携带精确的具名 wire 字段;基础设施与边界失败使用 Gateway 的进程内错误分类体系,但当前 RPC 适配器会把这些错误折叠为传输层的 `internal` 错误码。
|
||||
|
||||
```ts type-equiv
|
||||
/** One Remote method request after a carrier has decoded its envelope. */
|
||||
interface InvokeRemoteRequest {
|
||||
/** Remote namespace selected by the generated descriptor. */
|
||||
readonly namespace: string
|
||||
/** Exported Service method name. */
|
||||
readonly method: string
|
||||
/** Named wire values; fields must exactly match the descriptor. */
|
||||
readonly args: Readonly<Record<string, unknown>>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable infrastructure and boundary failures emitted before or after business execution. */
|
||||
type TypertGatewayErrorCode =
|
||||
| 'ambiguous-endpoint'
|
||||
| 'arguments-invalid'
|
||||
| 'binding-invalid'
|
||||
| 'context-failed'
|
||||
| 'context-not-found'
|
||||
| 'context-unavailable'
|
||||
| 'definition-unavailable'
|
||||
| 'input-invalid'
|
||||
| 'invocation-unavailable'
|
||||
| 'lookup-failed'
|
||||
| 'lookup-not-found'
|
||||
| 'lookup-unavailable'
|
||||
| 'method-unavailable'
|
||||
| 'provider-mismatch'
|
||||
| 'result-invalid'
|
||||
| 'service-unavailable'
|
||||
| 'signature-invalid'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Host dispatcher consumed by Connection adapters. */
|
||||
interface TypertGateway {
|
||||
/**
|
||||
* Invoke one live Remote method without assuming a carrier or response envelope.
|
||||
* @param request - decoded endpoint and named wire arguments.
|
||||
* @returns the validated business result.
|
||||
* @throws {@link TypertGatewayError} for dispatch, provider, or boundary failures; business errors retain their identity.
|
||||
*/
|
||||
invoke(request: InvokeRemoteRequest): Promise<unknown>
|
||||
}
|
||||
```
|
||||
|
||||
## 消费方 API
|
||||
|
||||
`ctx.api` 只暴露由已导入 `/remote` 产物贡献的 namespace。挂载会把生成的 descriptor 与具体的 root/scoped 方法作为一项由 fiber 持有的操作统一注册;JavaScript Proxy 与 Host 服务类型都不会进入消费方。
|
||||
|
||||
```ts type-equiv
|
||||
/** Typed API service augmented by generated direct Remote namespaces. */
|
||||
interface ClientApi extends TypeRTRemoteNamespaceMap {
|
||||
/**
|
||||
* Mount one generated Host-for-Client contribution in the caller's fiber.
|
||||
* @param contribution - explicitly selected Remote package artifact.
|
||||
* @returns disposer withdrawing descriptors and concrete methods together.
|
||||
*/
|
||||
mount(contribution: TypeRTRemoteContribution): TypeRTDisposer
|
||||
}
|
||||
```
|
||||
@@ -22,7 +22,7 @@
|
||||
"build:web": "pnpm --filter @deepseek-ai/dsh-frontend run build",
|
||||
"clean": "tsx scripts/clean.ts",
|
||||
"change-scope": "tsx scripts/change-scope.ts",
|
||||
"typecheck": "tsc -b",
|
||||
"typecheck": "npm run build:lib:contracts && tsc -b",
|
||||
"lint": "tsx scripts/run-oxlint.ts .",
|
||||
"lint:fix": "eslint --config eslint.format.config.mjs --fix . && tsx scripts/run-oxlint.ts . --fix",
|
||||
"duplication": "jscpd --config .jscpd.json packages scripts",
|
||||
|
||||
@@ -3097,7 +3097,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'TypertContribution',
|
||||
declaration: 'export interface TypertContribution {\n readonly package: string;\n readonly face: TypertFace;\n readonly schemas: readonly TypertSchema[];\n readonly model: TypertPackageModel;\n readonly invocations?: readonly InvocationDescriptor[];\n}',
|
||||
declaration: 'export interface TypertContribution {\n readonly package: string;\n readonly face: TypertFace;\n readonly schemas: readonly TypertSchema[];\n readonly model: TypertPackageModel;\n readonly invocations: readonly InvocationDescriptor[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'TypeRTDisposer',
|
||||
|
||||
@@ -284,6 +284,7 @@ class ScopedRemoteNamespace extends Service {
|
||||
|
||||
install(descriptor: InvocationDescriptor, projection: ScopedProjection, token: MountToken): void {
|
||||
this.assertMethodAvailable(descriptor.method)
|
||||
if (this.methods.size === 0) this.ownerCtx.set(this.name, this)
|
||||
const method = descriptor.method
|
||||
Object.defineProperty(this, method, {
|
||||
configurable: true,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
type InvocationParameterDescriptor,
|
||||
type TypeRTCodec,
|
||||
type TypeRTGatewayBinding,
|
||||
type TypeRTLookupProvider,
|
||||
} from '@deepseek-ai/dsh-type-meta'
|
||||
import type {
|
||||
InvokeRemoteRequest,
|
||||
@@ -149,6 +148,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
payload: unknown,
|
||||
_signal: AbortSignal,
|
||||
): Promise<ConnectionRpcResult> {
|
||||
// Remote methods have no cancellation parameter yet, so disconnects do not cancel business work.
|
||||
return this.invokeRpc(endpoint, payload)
|
||||
}
|
||||
|
||||
@@ -229,10 +229,8 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const parameters: InvocationParameterDescriptor[] = []
|
||||
const wires = new Set<string>()
|
||||
for (const name of names) {
|
||||
const matches = this.ctx.typert.lookups.keys()
|
||||
.map(key => ({ key, provider: this.ctx.typert.lookups.get(key) }))
|
||||
.filter((entry): entry is { key: string; provider: TypeRTLookupProvider } =>
|
||||
entry.provider?.parameter === name)
|
||||
const matches = this.ctx.typert.lookups.definitions()
|
||||
.filter(definition => definition.parameter === name)
|
||||
if (matches.length > 1) {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
@@ -246,7 +244,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
? { name, wire: name, source: 'json', codec: { mode: 'src-json' } }
|
||||
: {
|
||||
name,
|
||||
wire: match.provider.wire,
|
||||
wire: match.wire,
|
||||
source: 'lookup',
|
||||
lookup: match.key,
|
||||
codec: { mode: 'src-json' },
|
||||
@@ -540,7 +538,7 @@ function decode(
|
||||
field: string,
|
||||
): unknown {
|
||||
try {
|
||||
if (codec.mode === 'strict') return codec.schema.parse(value)
|
||||
if (codec.mode === 'strict') value = codec.schema.parse(value)
|
||||
assertJsonValue(value, new Set())
|
||||
return value
|
||||
} catch (cause) {
|
||||
|
||||
@@ -204,7 +204,13 @@ describe('Client TypeRT API', () => {
|
||||
})
|
||||
|
||||
it('rejects duplicate, live, scoped-service, and Context namespace collisions', async () => {
|
||||
const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>())
|
||||
const call = vi.fn<ConnectionHandle['rpc']['call']>()
|
||||
.mockResolvedValue({ ok: true, value: { renamed: true } })
|
||||
const ctx = await bench(call)
|
||||
const agentCtx = ctx.extend({ fixtureId: 'agent-remounted' }) as FixtureContext
|
||||
ctx.typert.contexts.registerClient('fixture', {
|
||||
identity: candidate => (candidate as Context & { fixtureId?: string }).fixtureId,
|
||||
})
|
||||
const direct = directDescriptor()
|
||||
const context = contextDescriptor()
|
||||
|
||||
@@ -242,6 +248,13 @@ describe('Client TypeRT API', () => {
|
||||
package: '@fixture/multiple-scoped',
|
||||
descriptors: [directDescriptor(), contextDescriptor()],
|
||||
})
|
||||
await expect(agentCtx.goals.rename({ objective: 'remounted' })).resolves.toEqual({ renamed: true })
|
||||
expect(call).toHaveBeenLastCalledWith(
|
||||
'/api',
|
||||
'goals/rename',
|
||||
{ args: { agentId: 'agent-remounted', request: { objective: 'remounted' } } },
|
||||
expect.any(AbortSignal),
|
||||
)
|
||||
await disposeMultipleScoped()
|
||||
})
|
||||
|
||||
|
||||
@@ -370,6 +370,19 @@ describe('TypertGatewayService', () => {
|
||||
})).resolves.toEqual({ agentId: 'agent-1', title: 'ship', scope: 'direct-src' })
|
||||
})
|
||||
|
||||
it('does not downgrade an observed SRC lookup after its provider unloads', async () => {
|
||||
const { ctx, service } = await setup()
|
||||
const dispose = registerAgentLookup(ctx, { id: 'agent-1' })
|
||||
await dispose()
|
||||
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'lookup-unavailable')
|
||||
expect(service.calls).toEqual([])
|
||||
})
|
||||
|
||||
it('derives SRC Remote Context identity and preserves the scoped Proxy receiver', async () => {
|
||||
const { ctx } = await setup()
|
||||
const scoped = ctx.extend({ fixtureScope: 'agent-src' })
|
||||
@@ -657,6 +670,22 @@ describe('TypertGatewayService', () => {
|
||||
}), 'result-invalid')
|
||||
})
|
||||
|
||||
it('rejects non-JSON values after strict codec validation', async () => {
|
||||
const { ctx, service } = await setup()
|
||||
const descriptor = strictOnlyDescriptor()
|
||||
registerStrict(ctx, [{
|
||||
...descriptor,
|
||||
result: strictCodec('@fixture/gateway#UnknownResult', z.unknown()),
|
||||
}])
|
||||
service.nextResult = 1n
|
||||
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'goals',
|
||||
method: 'strictOnly',
|
||||
args: { request: { title: 'ship' } },
|
||||
}), 'result-invalid')
|
||||
})
|
||||
|
||||
it.each([
|
||||
undefined,
|
||||
Number.NaN,
|
||||
|
||||
@@ -1318,6 +1318,8 @@ class FaceAnalyzer {
|
||||
* type evaluator.
|
||||
*/
|
||||
private resolvedRemoteCodecType(authoredType: ts.TypeNode): TypeNodeId {
|
||||
const resolvedType = this.checker.getTypeFromTypeNode(authoredType)
|
||||
this.assertRemoteJsonType(resolvedType, authoredType, new Set(), false)
|
||||
const completed = new Map<ts.Type, TypeNodeId>()
|
||||
const active = new Map<ts.Type, TypeNodeId>()
|
||||
const recursiveDeclarations = new Map<ts.Type, SymbolId>()
|
||||
@@ -1474,7 +1476,107 @@ class FaceAnalyzer {
|
||||
active.delete(type)
|
||||
}
|
||||
}
|
||||
return convert(this.checker.getTypeFromTypeNode(authoredType))
|
||||
return convert(resolvedType)
|
||||
}
|
||||
|
||||
private assertRemoteJsonType(
|
||||
type: ts.Type,
|
||||
site: ts.TypeNode,
|
||||
active: Set<ts.Type>,
|
||||
allowUndefined: boolean,
|
||||
): void {
|
||||
const flags = type.flags
|
||||
if ((flags & ts.TypeFlags.Undefined) !== 0 && allowUndefined) return
|
||||
if ((flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
|
||||
this.fail(site, `Remote boundary contains unconstrained ${this.checker.typeToString(type)} data`)
|
||||
}
|
||||
if ((flags & (ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0) {
|
||||
this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`)
|
||||
}
|
||||
if ((flags & (ts.TypeFlags.StringLike
|
||||
| ts.TypeFlags.NumberLike
|
||||
| ts.TypeFlags.BooleanLike
|
||||
| ts.TypeFlags.Null
|
||||
| ts.TypeFlags.Never)) !== 0) return
|
||||
if (type.isUnion()) {
|
||||
for (const member of type.types) this.assertRemoteJsonType(member, site, active, allowUndefined)
|
||||
return
|
||||
}
|
||||
if (type.isIntersection()) {
|
||||
const material = type.types.filter(member => !this.isRemotePhantomConstraint(member))
|
||||
if (material.length === 0) this.fail(site, 'Remote boundary contains a symbol-only object')
|
||||
for (const member of material) this.assertRemoteJsonType(member, site, active, false)
|
||||
return
|
||||
}
|
||||
if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
|
||||
this.fail(site, 'Remote boundary contains an unresolved type parameter')
|
||||
}
|
||||
if ((flags & ts.TypeFlags.Object) === 0) {
|
||||
this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`)
|
||||
}
|
||||
const symbol = type.getSymbol()
|
||||
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0]
|
||||
if (declaration !== undefined && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration))) {
|
||||
this.fail(site, `Remote boundary contains class instance ${symbol?.name ?? this.checker.typeToString(type)}`)
|
||||
}
|
||||
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
|
||||
this.fail(site, 'Remote boundary contains callable or constructable data')
|
||||
}
|
||||
if (active.has(type)) return
|
||||
active.add(type)
|
||||
try {
|
||||
if (this.checker.isTupleType(type)) {
|
||||
const reference = type as ts.TypeReference
|
||||
const target = reference.target as ts.TupleType
|
||||
const arguments_ = this.checker.getTypeArguments(reference)
|
||||
arguments_.forEach((argument, index) => {
|
||||
const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required
|
||||
this.assertRemoteJsonType(
|
||||
argument,
|
||||
site,
|
||||
active,
|
||||
(elementFlags & ts.ElementFlags.Optional) !== 0,
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
|
||||
const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number)
|
||||
if (element === undefined) this.fail(site, 'Remote boundary array has no element type')
|
||||
this.assertRemoteJsonType(element, site, active, false)
|
||||
return
|
||||
}
|
||||
const properties = this.checker.getPropertiesOfType(type)
|
||||
if (properties.some(property => property.getName().startsWith('__@'))) {
|
||||
this.fail(site, 'Remote boundary contains a symbol-keyed property')
|
||||
}
|
||||
for (const property of properties) {
|
||||
const propertyDeclaration = property.valueDeclaration ?? property.declarations?.[0]
|
||||
const propertyType = this.checker.getTypeOfSymbolAtLocation(property, propertyDeclaration ?? site)
|
||||
this.assertRemoteJsonType(
|
||||
propertyType,
|
||||
site,
|
||||
active,
|
||||
(property.flags & ts.SymbolFlags.Optional) !== 0,
|
||||
)
|
||||
}
|
||||
for (const info of this.checker.getIndexInfosOfType(type)) {
|
||||
if ((info.keyType.flags & ts.TypeFlags.ESSymbolLike) !== 0) {
|
||||
this.fail(site, 'Remote boundary contains a symbol index signature')
|
||||
}
|
||||
this.assertRemoteJsonType(info.type, site, active, false)
|
||||
}
|
||||
} finally {
|
||||
active.delete(type)
|
||||
}
|
||||
}
|
||||
|
||||
private isRemotePhantomConstraint(type: ts.Type): boolean {
|
||||
if ((type.flags & ts.TypeFlags.Unknown) !== 0) return true
|
||||
if ((type.flags & ts.TypeFlags.Any) !== 0 || (type.flags & ts.TypeFlags.Object) === 0) return false
|
||||
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) return false
|
||||
if (this.checker.getIndexInfosOfType(type).length > 0) return false
|
||||
return this.checker.getPropertiesOfType(type).every(property => property.getName().startsWith('__@'))
|
||||
}
|
||||
|
||||
private resolvedCycleReference(
|
||||
|
||||
@@ -284,6 +284,32 @@ export type GenericResult = {
|
||||
expect(() => analyzeRemote(root, false)).toThrow(/non-JSON class parameter Agent requires a TypeRTLookupMap entry/)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['bigint', 'bigint'],
|
||||
['symbol', 'symbol'],
|
||||
['undefined', 'undefined'],
|
||||
['any', 'unconstrained any'],
|
||||
['unknown', 'unconstrained unknown'],
|
||||
])('rejects non-JSON Remote boundary type %s', (type, message) => {
|
||||
const root = copyFixture()
|
||||
editFile(root, 'packages/remote/src/types.ts', source => source.replace(
|
||||
' readonly title: string\n}',
|
||||
` readonly title: string\n readonly invalid: ${type}\n}`,
|
||||
))
|
||||
|
||||
expect(() => analyzeRemote(root, false)).toThrow(new RegExp(message))
|
||||
})
|
||||
|
||||
it('keeps optional JSON object fields valid', () => {
|
||||
const root = copyFixture()
|
||||
editFile(root, 'packages/remote/src/types.ts', source => source.replace(
|
||||
' readonly title: string\n}',
|
||||
' readonly title: string\n readonly note?: string\n}',
|
||||
))
|
||||
|
||||
expect(() => analyzeRemote(root)).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects a Remote Context without a static Context declaration', () => {
|
||||
const root = copyFixture()
|
||||
editFile(root, 'packages/remote/src/index.ts', source => source.replace("@RemoteContext('agent')", "@RemoteContext('missing')"))
|
||||
|
||||
@@ -135,10 +135,8 @@ export function validateTypertManifest(pkgName: string, exported: unknown): Type
|
||||
requireMembers(pkgName, object.members, `object "${object.name as string}"`)
|
||||
requireTypes(pkgName, object.types, `object "${object.name as string}"`)
|
||||
}
|
||||
if (manifest.invocations !== undefined) {
|
||||
for (const value of requireArray(pkgName, manifest.invocations, 'TYPERT.invocations')) {
|
||||
requireInvocation(pkgName, value)
|
||||
}
|
||||
for (const value of requireArray(pkgName, manifest.invocations, 'TYPERT.invocations')) {
|
||||
requireInvocation(pkgName, value)
|
||||
}
|
||||
return manifest as unknown as TypertContribution
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ function typertSource(pkgName: string, entryName: string): string {
|
||||
' face: \'host\',',
|
||||
` schemas: [{ name: '${entryName}', schema: ${entryName} }],`,
|
||||
' model: { services: [], events: [], objects: [] },',
|
||||
' invocations: [],',
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
@@ -262,6 +263,7 @@ describe('typert loader', () => {
|
||||
' face: \'host\',',
|
||||
' schemas: [{ name: \'Pending\', schema: Pending }],',
|
||||
' model: { services: [], events: [], objects: [] },',
|
||||
' invocations: [],',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
@@ -295,7 +297,7 @@ describe('typert loader', () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'dsh-typert-loader-'))
|
||||
await linkZod(root)
|
||||
await writePackage(root, '@fixture/broken', {
|
||||
typertSource: 'export const TYPERT = { package: \'@fixture/broken\', face: \'host\', schemas: [{ name: \'\', schema: {} }], model: { services: [], events: [], objects: [] } }\n',
|
||||
typertSource: 'export const TYPERT = { package: \'@fixture/broken\', face: \'host\', schemas: [{ name: \'\', schema: {} }], model: { services: [], events: [], objects: [] }, invocations: [] }\n',
|
||||
})
|
||||
const ctx = await boot()
|
||||
await ctx.loader.create({ name: '@fixture/broken' })
|
||||
@@ -410,6 +412,7 @@ describe('validateTypertManifest', () => {
|
||||
face: 'host',
|
||||
schemas: [{ name: 'A', schema: zodish }],
|
||||
model: { services: [], events: [], objects: [] },
|
||||
invocations: [],
|
||||
}).schemas).toHaveLength(1)
|
||||
|
||||
expect(() => validateTypertManifest('pkg', undefined)).toThrow('no TYPERT manifest object')
|
||||
@@ -490,12 +493,14 @@ describe('validateTypertManifest', () => {
|
||||
})).toThrow('object has a missing or empty exportName')
|
||||
})
|
||||
|
||||
it('validates strict invocation descriptors and accepts legacy manifests without them', () => {
|
||||
const legacy = completeManifest(zodish)
|
||||
expect(validateTypertManifest('pkg', legacy)).toBe(legacy)
|
||||
it('requires and validates strict invocation descriptors', () => {
|
||||
const base = completeManifest(zodish)
|
||||
const { invocations: _invocations, ...missingInvocations } = base
|
||||
expect(() => validateTypertManifest('pkg', missingInvocations))
|
||||
.toThrow('TYPERT.invocations must be an array')
|
||||
|
||||
const descriptor = strictInvocation()
|
||||
const manifest = { ...legacy, invocations: [descriptor] }
|
||||
const manifest = { ...base, invocations: [descriptor] }
|
||||
expect(validateTypertManifest('pkg', manifest)).toBe(manifest)
|
||||
const scoped = {
|
||||
...descriptor,
|
||||
@@ -508,53 +513,53 @@ describe('validateTypertManifest', () => {
|
||||
codec: strictCodec('pkg#AgentId'),
|
||||
}, ...descriptor.parameters],
|
||||
}
|
||||
expect(validateTypertManifest('pkg', { ...legacy, invocations: [scoped] }).invocations)
|
||||
expect(validateTypertManifest('pkg', { ...base, invocations: [scoped] }).invocations)
|
||||
.toEqual([scoped])
|
||||
|
||||
expect(() => validateTypertManifest('pkg', { ...legacy, invocations: {} }))
|
||||
expect(() => validateTypertManifest('pkg', { ...base, invocations: {} }))
|
||||
.toThrow('TYPERT.invocations must be an array')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...descriptor, invocation: { kind: 'future' } }],
|
||||
})).toThrow('receiver kind must be "direct" or "context"')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...descriptor, result: { mode: 'src-json' } }],
|
||||
})).toThrow('result codec must use a strict codec')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...descriptor, result: { mode: 'strict', typeSymbol: 'pkg#Result', schema: zodish } }],
|
||||
})).toThrow('result codec is not backed by a zod v4 schema')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{
|
||||
...descriptor,
|
||||
parameters: [{ ...descriptor.parameters[0], source: 'future' }],
|
||||
}],
|
||||
})).toThrow('parameter source must be "json" or "lookup"')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{
|
||||
...descriptor,
|
||||
parameters: [{ ...descriptor.parameters[0], source: 'lookup' }],
|
||||
}],
|
||||
})).toThrow('lookup parameter has a missing or empty lookup')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{
|
||||
...descriptor,
|
||||
parameters: [{ ...descriptor.parameters[0], lookup: 'agent' }],
|
||||
}],
|
||||
})).toThrow('JSON parameter declares a lookup')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{
|
||||
...descriptor,
|
||||
parameters: [descriptor.parameters[0], { ...descriptor.parameters[0], name: 'again' }],
|
||||
}],
|
||||
})).toThrow('repeats wire field "request"')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{
|
||||
...descriptor,
|
||||
invocation: {
|
||||
@@ -566,19 +571,19 @@ describe('validateTypertManifest', () => {
|
||||
}],
|
||||
})).toThrow('repeats Context wire field "request"')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...scoped, scope: null }],
|
||||
})).toThrow('scope must be an object')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...scoped, scope: { wire: 'agentId' } }],
|
||||
})).toThrow('scope has a missing or empty context')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...scoped, scope: { context: 'agent' } }],
|
||||
})).toThrow('scope has a missing or empty wire')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{
|
||||
...scoped,
|
||||
invocation: {
|
||||
@@ -590,11 +595,11 @@ describe('validateTypertManifest', () => {
|
||||
}],
|
||||
})).toThrow('Context receiver cannot declare a direct scope projection')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...scoped, scope: { context: 'agent', wire: 'missingId' } }],
|
||||
})).toThrow('must select its only lookup parameter')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{
|
||||
...scoped,
|
||||
parameters: [...scoped.parameters, {
|
||||
@@ -607,11 +612,11 @@ describe('validateTypertManifest', () => {
|
||||
}],
|
||||
})).toThrow('must select its only lookup parameter')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...scoped, scope: { context: 'other', wire: 'agentId' } }],
|
||||
})).toThrow('must select its only lookup parameter')
|
||||
expect(() => validateTypertManifest('pkg', {
|
||||
...legacy,
|
||||
...base,
|
||||
invocations: [{ ...descriptor, sourceLocation: { file: 'src/index.ts', line: 0, column: 1 } }],
|
||||
})).toThrow('sourceLocation.line must be a positive integer')
|
||||
})
|
||||
@@ -646,6 +651,7 @@ function completeManifest(zodish: object) {
|
||||
package: 'pkg',
|
||||
face: 'host',
|
||||
schemas: [{ name: 'Schema', schema: zodish }],
|
||||
invocations: [],
|
||||
model: {
|
||||
services: [{
|
||||
key: 'service',
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
TypeRTHostContextProvider,
|
||||
TypeRTLocalRegistry,
|
||||
TypeRTLookupHost,
|
||||
TypeRTLookupDefinition,
|
||||
TypeRTLookupMap,
|
||||
TypeRTLookupProvider,
|
||||
TypeRTLookupRegistry,
|
||||
@@ -212,6 +213,7 @@ class RemoteStore {
|
||||
|
||||
class LookupStore {
|
||||
private readonly providers = new Map<string, ProviderEntry<TypeRTLookupProvider>>()
|
||||
private readonly definitions = new Map<string, TypeRTLookupDefinition>()
|
||||
private readonly changes: ChangeSource
|
||||
|
||||
constructor(report: ReportObserverError) {
|
||||
@@ -228,6 +230,7 @@ class LookupStore {
|
||||
>,
|
||||
) => this.register(ctx, key, provider),
|
||||
get: key => this.providers.get(key)?.provider,
|
||||
definitions: () => [...this.definitions.values()],
|
||||
keys: () => [...this.providers.keys()],
|
||||
subscribe: listener => this.changes.subscribe(ctx, listener),
|
||||
}
|
||||
@@ -240,10 +243,22 @@ class LookupStore {
|
||||
validateNonempty('lookup Host type symbol', provider.hostTypeSymbol)
|
||||
validateNonempty('lookup wire type symbol', provider.wireTypeSymbol)
|
||||
if (this.providers.has(key)) throw new Error(`typert: lookup "${key}" is already registered`)
|
||||
const definition: TypeRTLookupDefinition = {
|
||||
key,
|
||||
parameter: provider.parameter,
|
||||
wire: provider.wire,
|
||||
hostTypeSymbol: provider.hostTypeSymbol,
|
||||
wireTypeSymbol: provider.wireTypeSymbol,
|
||||
}
|
||||
const known = this.definitions.get(key)
|
||||
if (known !== undefined && !lookupDefinitionEquals(known, definition)) {
|
||||
throw new Error(`typert: lookup "${key}" changed its wire declaration during this registry lifetime`)
|
||||
}
|
||||
const owner = {}
|
||||
const entry: ProviderEntry<TypeRTLookupProvider> = { provider, owner }
|
||||
const { providers, changes } = this
|
||||
const { definitions, providers, changes } = this
|
||||
return ctx.effect(function* () {
|
||||
definitions.set(key, definition)
|
||||
providers.set(key, entry)
|
||||
changes.emit({ kind: 'lookup', key })
|
||||
yield () => {
|
||||
@@ -256,6 +271,13 @@ class LookupStore {
|
||||
}
|
||||
}
|
||||
|
||||
function lookupDefinitionEquals(left: TypeRTLookupDefinition, right: TypeRTLookupDefinition): boolean {
|
||||
return left.parameter === right.parameter
|
||||
&& left.wire === right.wire
|
||||
&& left.hostTypeSymbol === right.hostTypeSymbol
|
||||
&& left.wireTypeSymbol === right.wireTypeSymbol
|
||||
}
|
||||
|
||||
class ContextStore {
|
||||
private readonly hosts = new Map<string, ProviderEntry<TypeRTHostContextProvider>>()
|
||||
private readonly clients = new Map<string, ProviderEntry<TypeRTClientContextBinder>>()
|
||||
@@ -377,7 +399,7 @@ export class TypertRegistry extends Service implements TypeRTService {
|
||||
register(contribution: TypertContribution): TypeRTDisposer {
|
||||
const packageRecord = this.validatePackage(contribution)
|
||||
const schemaRecords = this.validateSchemas(contribution)
|
||||
const invocations = contribution.invocations ?? []
|
||||
const invocations = contribution.invocations
|
||||
this.localStore.validate(invocations)
|
||||
const owner = {}
|
||||
const { schemas, packages, localStore } = this
|
||||
|
||||
@@ -83,12 +83,7 @@ export interface TypertContribution {
|
||||
readonly face: TypertFace
|
||||
readonly schemas: readonly TypertSchema[]
|
||||
readonly model: TypertPackageModel
|
||||
/** Host invocation definitions; absent on artifacts generated before Remote support. */
|
||||
readonly invocations?: readonly InvocationDescriptor[]
|
||||
}
|
||||
|
||||
/** Generated Host contribution with strict Remote invocation definitions. */
|
||||
export interface TypertLocalContribution extends TypertContribution {
|
||||
/** Host invocation definitions, empty when the package exports no Remote methods. */
|
||||
readonly invocations: readonly InvocationDescriptor[]
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ function toolsContribution(schema: z.ZodType = z.object({ name: z.string() })):
|
||||
package: '@deepseek-ai/dsh-tools',
|
||||
face: 'host',
|
||||
schemas: [{ name: 'ToolInput', schema }],
|
||||
invocations: [],
|
||||
model: {
|
||||
services: [{
|
||||
key: 'tools',
|
||||
@@ -329,11 +330,19 @@ describe('TypertRegistry', () => {
|
||||
})
|
||||
|
||||
expect(ctx.typert.lookups.get('fixture')?.resolve('agent-1')).toBe(object)
|
||||
expect(ctx.typert.lookups.definitions()).toEqual([{
|
||||
key: 'fixture',
|
||||
parameter: 'agent',
|
||||
wire: 'agentId',
|
||||
hostTypeSymbol: '@fixture/agent#Agent',
|
||||
wireTypeSymbol: '@fixture/session#SessionId',
|
||||
}])
|
||||
expect(ctx.typert.contexts.getHost('registryFixture')?.resolve('agent-1')).toBe(scoped)
|
||||
expect(ctx.typert.contexts.getClient('registryFixture')?.identity(scoped)).toBe('agent-1')
|
||||
|
||||
await Promise.all([disposeClient(), disposeHost(), disposeLookup()])
|
||||
expect(ctx.typert.lookups.keys()).toEqual([])
|
||||
expect(ctx.typert.lookups.definitions()).toHaveLength(1)
|
||||
expect(ctx.typert.contexts.getHost('registryFixture')).toBeUndefined()
|
||||
expect(ctx.typert.contexts.getClient('registryFixture')).toBeUndefined()
|
||||
})
|
||||
@@ -378,6 +387,15 @@ describe('TypertRegistry', () => {
|
||||
])
|
||||
|
||||
await Promise.all([disposeLookupSubscription(), disposeContextSubscription()])
|
||||
for (const changed of [
|
||||
{ ...lookup, parameter: 'session' },
|
||||
{ ...lookup, wire: 'sessionId' },
|
||||
{ ...lookup, hostTypeSymbol: '@fixture#Session' },
|
||||
{ ...lookup, wireTypeSymbol: '@fixture#SessionId' },
|
||||
]) {
|
||||
expect(() => ctx.typert.lookups.register('fixture', changed))
|
||||
.toThrow('changed its wire declaration during this registry lifetime')
|
||||
}
|
||||
ctx.typert.lookups.register('fixture', lookup)
|
||||
expect(changes).toHaveLength(6)
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ export type {
|
||||
TypeRTHostContextProvider,
|
||||
TypeRTLocalRegistry,
|
||||
TypeRTLookup,
|
||||
TypeRTLookupDefinition,
|
||||
TypeRTLookupHost,
|
||||
TypeRTLookupMap,
|
||||
TypeRTLookupProvider,
|
||||
|
||||
@@ -189,6 +189,20 @@ export interface TypeRTLookupProvider<Host = unknown, Wire = unknown> {
|
||||
resolve(id: Wire): Host | undefined
|
||||
}
|
||||
|
||||
/** Stable wire declaration retained after a lookup provider unloads. */
|
||||
export interface TypeRTLookupDefinition {
|
||||
/** Merge-declared lookup key. */
|
||||
readonly key: string
|
||||
/** Source parameter name recognized by the SRC weak parser. */
|
||||
readonly parameter: string
|
||||
/** Wire field replacing the Host object parameter. */
|
||||
readonly wire: string
|
||||
/** Canonical Host type symbol used by strict generation. */
|
||||
readonly hostTypeSymbol: string
|
||||
/** Canonical wire type symbol used by strict generation. */
|
||||
readonly wireTypeSymbol: string
|
||||
}
|
||||
|
||||
/** Host resolver for one scoped Remote Context kind. */
|
||||
export interface TypeRTHostContextProvider<Wire = unknown> {
|
||||
/** Wire field carrying the Context identity. */
|
||||
@@ -291,6 +305,8 @@ export interface TypeRTLookupRegistry {
|
||||
* @returns the live provider, or `undefined` when absent.
|
||||
*/
|
||||
get(key: string): TypeRTLookupProvider | undefined
|
||||
/** @returns lookup declarations observed during this TypeRT Service lifetime. */
|
||||
definitions(): readonly TypeRTLookupDefinition[]
|
||||
/** @returns a snapshot of registered provider keys. */
|
||||
keys(): readonly string[]
|
||||
/**
|
||||
|
||||
@@ -1494,6 +1494,66 @@
|
||||
"doc": "docs/core-data-structures/settings.md",
|
||||
"symbol": "SettingsPathOp",
|
||||
"source": "packages/settings/settings/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "TypeRTLookupMap",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "TypeRTContextMap",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "TypeRTLookupDefinition",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "TypeRTCodec",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "InvocationParameterDescriptor",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "InvocationDescriptor",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "TypeRTService",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "TypeRTRemoteNamespaceMap",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "InvokeRemoteRequest",
|
||||
"source": "packages/host/api-gateway/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "TypertGatewayErrorCode",
|
||||
"source": "packages/host/api-gateway/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "TypertGateway",
|
||||
"source": "packages/host/api-gateway/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/typert.md",
|
||||
"symbol": "ClientApi",
|
||||
"source": "packages/host/api-gateway/src/client/index.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user