Merge pull request #3293 from deepseek-harness/worktree-apiremote
refactor(api): converge the ctx.remote programming surface
This commit is contained in:
commit
f27021c9b6
338 files changed
+4816
-4148
No files matched your search
+2
-2
@@ -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/implemented/architecture/2026-08-02-typert-remote-method-calls.md
|
||||
2026-08-02-typert-remote-method-calls.md: b95e3f0dec56287cbec2586921477284d0489a40
|
||||
2026-08-02-typert-remote-method-calls.zh.md: 50f04fd44a06ae3914998f0337fef09c75fe707c
|
||||
2026-08-02-typert-remote-method-calls.md: 9102d04266626a8692e1d3ec5cb8f529d8c34d13
|
||||
2026-08-02-typert-remote-method-calls.zh.md: bf9ec58b28c1e1e833d92d76737890d49f0487a3
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-08-02-typert-remote-method-calls.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
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.
|
||||
The Host API Proxy handled direct method calls, stateful interactions, and Session event streams in one package. 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 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.
|
||||
|
||||
@@ -22,7 +22,7 @@ The Remote consumer projection contains `.d.ts`, `.d.ts.map`, and `.js` files. T
|
||||
|
||||
`@deepseek-ai/dsh-api-gateway`, located at `packages/api/gateway`, provides two symmetric faces: its default entry provides Host `ctx.typertGateway`, while its `/client` entry provides consumer-side `ctx.remote`. 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.
|
||||
|
||||
`@deepseek-ai/dsh-api-remotes`, located at `packages/api/remotes`, is the BFF layer above the Gateway. Its Host entry owns Agent/Session identity resolution and Typert lookup configuration; its `/client` entry selects the generated Remote contributions exposed by the application. The Client entry consumes the shared `TypertClientRemote` contract through Cordis rather than importing the concrete Gateway implementation.
|
||||
`@deepseek-ai/dsh-api-remotes`, located at `packages/api/remotes`, is the BFF layer above the Gateway. Its Host entry registers the application's forwarded Cordis event source and the Host facts carried by generation readiness; its `/client` entry selects the generated Remote contributions exposed by the application. The Client entry consumes the shared `TypertClientRemote` contract through Cordis rather than importing the concrete Gateway implementation.
|
||||
|
||||
## Components and Cordis services
|
||||
|
||||
@@ -32,7 +32,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` |
|
||||
| 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, the shared `/api` route, RPC envelope, rpcId, serialization, trust, error transport, Typert interception, and legacy API Proxy fallback |
|
||||
| Connection | `ctx.connection` | Exclusively owns the HTTP Server/future WebSocket, the shared `/api` route, RPC envelope, rpcId, serialization, trust, error transport, Typert interception, and owner-registered exact Fetch routes on the same channel |
|
||||
| API Gateway's Client face | `ctx.remote`, `ctx.remote.<namespace>` | Mounts Remote contributions, materializes each namespace as a traced `remote.<namespace>` child Service, and delegates canonical calls to `ctx.connection.rpc` |
|
||||
| API Remotes | No new service | Owns Host Agent/Session lookup policy and serves as the only Client business facade, selecting and mounting `/remote` contributions while exposing the selected API declarations |
|
||||
| Agent/Session owning packages | Existing domain services | Provide both static interface merges and runtime lookup/Context providers |
|
||||
@@ -162,9 +162,9 @@ ctx.typert.contexts Host Context resolvers and Client Context binders
|
||||
|
||||
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 caches only the set of SRC-owned endpoint names and discards it whenever the Cordis Service set changes; it retains no descriptor, Service, or provider. Invocation resolves all live objects from current state, so removing a strict definition, Service, or provider 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 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 `gateway/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.
|
||||
|
||||
Business-object and scoped-Context packages own stable declarations and default resolvers through `lookups.register()` and `contexts.registerHost()`; Host composition supplies effect-scoped asynchronous policies through `lookups.configure()` and `contexts.configureHost()`. Configuration may precede provider registration, but does not by itself make an identity available without a live provider; unloading the configuration restores the provider's default resolver. API Remotes creates the shared `agentFor()` resolver for `agent` and `session` lookups and the `agent` Host Context: live Agents are reused, ordinary cold sessions are resumed automatically, concurrent resumes are deduplicated by Session ID, and the subagent ownership fence returns the existing `agent-busy`. The standard Web API Proxy supplies its Agent defaults and scope setup and consumes that resolver for legacy methods. The `session` lookup returns the resolved Agent's Session, while the `agent` Host Context returns its Context, so all three projections share one resume lifecycle.
|
||||
Business-object and scoped-Context packages own stable declarations and default resolvers through `lookups.register()` and `contexts.registerHost()`; Host composition supplies effect-scoped asynchronous policies through `lookups.configure()` and `contexts.configureHost()`. Configuration may precede provider registration, but does not by itself make an identity available without a live provider; unloading the configuration restores the provider's default resolver. The Session Controller's `ApiSessionAgentController` configures one shared resolver for the `agent` and `session` lookups and the `agent` Host Context: live Agents are reused, ordinary cold sessions are resumed automatically, concurrent resumes are deduplicated by Session ID, and the subagent ownership fence returns `session/agent-busy`. The `session` lookup returns the resolved Agent's Session, while the `agent` Host Context returns its Context, so all three projections share one resume lifecycle.
|
||||
|
||||
The registry's Host root entry has the complete `TypertRegistryContract` 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.
|
||||
|
||||
@@ -254,7 +254,7 @@ interface TypertRemoteNamespace$676f616c73 {
|
||||
agentId: SessionId,
|
||||
request: CreateGoalRequest,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<CreateGoalResult>
|
||||
) => Promise<RemoteResult<CreateGoalResult>>
|
||||
}
|
||||
|
||||
interface TypertRemoteMap {
|
||||
@@ -262,7 +262,7 @@ interface TypertRemoteMap {
|
||||
agentId: SessionId,
|
||||
request: CreateGoalRequest,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<CreateGoalResult>
|
||||
) => Promise<RemoteResult<CreateGoalResult>>
|
||||
}
|
||||
|
||||
interface TypertRemoteNamespaceMap {
|
||||
@@ -273,7 +273,7 @@ interface TypertRemoteScopeMap {
|
||||
'agent:goals/create': (
|
||||
request: CreateGoalRequest,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<CreateGoalResult>
|
||||
) => Promise<RemoteResult<CreateGoalResult>>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -288,7 +288,9 @@ agentCtx.remote.goals.create(request)
|
||||
|
||||
The Agent Scope supplies its own `SessionId` automatically. A `@Remote` method with an `agent` lookup can therefore generate both root and scoped consumer signatures. A `@RemoteScope('agent')` method also omits a separate Scope identity, but generates only the scoped signature. The root `Context` exposes direct namespaces through `ctx.remote`, while `AgentContext.remote` intersects that direct surface with the scoped surface. A future TUI must preserve the same distinction.
|
||||
|
||||
`TypertClientRemote` remains platform-independent, and the Browser Client exposes it as `ctx.remote`. If a future TUI reuses this type, it must likewise access it through a dedicated Remote object and Agent Scope rather than treating the Host `Context` as a broader Service collection. Public Service methods without Remote markers do not enter the Remote maps.
|
||||
Every generated method resolves to `Promise<RemoteResult<T>>`: a call reports its outcome in the result's `ok` branch instead of rejecting, and only an assembly fault (arity, an unmounted method, a missing Context adapter) still throws. A consumer branches on `result.ok`, and reads `result.error.code` when it must distinguish failures; the failure vocabulary itself is [one Remote failure class plus a merged code table](2026-08-28-ctx-remote-failure-vocabulary.md).
|
||||
|
||||
`TypertClientRemote` remains platform-independent, and the Browser Client exposes it as `ctx.remote`. If a future TUI reuses this type, it must likewise access it through a dedicated Remote object and Agent Scope rather than treating the Host `Context` as a broader Service collection. Public Service methods without Remote markers do not enter the Remote maps. Beside the generated namespaces, the Gateway's client face adds `$mount`, `$on`, `$stream`, and `$host` — the last exposing the connection's fixed Host facts (`home`, `isLoopback`) as plain reads, so a consumer never injects the carrier to learn them.
|
||||
|
||||
## Client Typert and the API Gateway Client face
|
||||
|
||||
@@ -361,7 +363,7 @@ CI and releases use LIB. Moving all repository coverage to LIB is separate follo
|
||||
|
||||
## Host Gateway resolution
|
||||
|
||||
The Host Gateway registers one `/api` interceptor with Connection and does not maintain a second endpoint registry. Its ownership matcher checks the current Typert local registry first, then consults an invalidation-aware set populated by scanning current Cordis Services for `typertGateway` bindings and SRC Remote markers. A Cordis Service change discards the set, so Typert definitions and business Services may arrive in either order without making legacy `/api` traffic rescan every Service on each request or letting arbitrary request paths grow the cache.
|
||||
The Host Gateway registers one `/api` interceptor with Connection and does not maintain a second endpoint registry. Its ownership matcher checks the current Typert local registry first, then consults an invalidation-aware set populated by scanning current Cordis Services for `typertGateway` bindings and SRC Remote markers. A Cordis Service change discards the set, so Typert definitions and business Services may arrive in either order without rescanning every Service on each request or letting arbitrary request paths grow the cache.
|
||||
|
||||
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.
|
||||
|
||||
@@ -399,9 +401,9 @@ ctx.connection.rpc.intercept(
|
||||
)
|
||||
```
|
||||
|
||||
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 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; an endpoint that neither an exact Fetch route nor the Gateway claims answers 404.
|
||||
|
||||
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:
|
||||
The Connection Host half passes one composite FetchHandler to the HTTP bridge. After the bridge creates a standard `Request`, that handler matches the pathname against the exact Fetch routes owners registered on the channel, then against the channel's single interceptor — the Gateway — and answers 404 when neither claims it. Every path on the channel reuses the same request/response envelope, rpcId, serialization, trust, and error transport, and a failure carries the shared `{ code, message, details }` data. The current physical mapping is:
|
||||
|
||||
```text
|
||||
POST /api/<namespace>/<method>
|
||||
@@ -438,15 +440,15 @@ ctx.remote.goals.create(sessionId, request, signal?)
|
||||
→ 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 adapter converts ordinary Gateway and business-invocation failures to the existing `RpcError` envelope with `code: 'internal'`; an existing RPC error carried by a resolver in `TypertLookupFailure` is returned unchanged, preserving stable error codes for cold-resume failures and ownership fences. The Gateway's structured error category remains available only in-process, while the message carries the diagnostic across Connection.
|
||||
Remote does not define a second-layer `{ ok, value/error }` response on the wire. Successful values and failures use the existing RPC response's `result` directly, and the failure branch carries the shared `{ code, message, details }` data. Owners, resolvers, and the Gateway all raise one class, `RemoteError`, whose code comes from the merged `RemoteErrorDetailsMap`: the Host encodes a structurally identified `RemoteError` onto the wire unchanged — including the Gateway's own `gateway/*` assembly codes and a resolver's `session/not-found` or `session/agent-busy` — and folds only an unclassified throw into `gateway/internal`, keeping its diagnostic in the message. The Client face rebuilds an instance for the `RemoteResult` error branch, so `throw result.error` keeps throw semantics. [The failure-vocabulary Agent Note](2026-08-28-ctx-remote-failure-vocabulary.md) owns the code table, its ownership rules, and why discrimination reads `code` instead of `instanceof`.
|
||||
|
||||
The Gateway does not handle per-method permissions, caller identity, idempotency, or long-lived connection state. It only propagates cooperative cancellation from Connection into explicitly cancellation-aware business methods. 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.
|
||||
The Gateway does not handle per-method permissions, caller identity, idempotency, or long-lived connection state. It only propagates cooperative cancellation from Connection into explicitly cancellation-aware business methods. Every request on the shared channel, Typert endpoint or exact Fetch route alike, passes Connection's browser authentication and trusted-host policy before dispatch; the Gateway adds no second policy. Connection's WebSocket migration remains separate follow-up work.
|
||||
|
||||
## Connection and protocol boundaries
|
||||
|
||||
The Client Remote Service owns Remote contributions, namespace Service 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 Remote types.
|
||||
|
||||
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.
|
||||
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 an exact registered path to its route owner, a claimed endpoint to the Gateway, and anything else to 404. 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
|
||||
|
||||
@@ -454,19 +456,19 @@ The Gateway registers only its ownership matcher and RPC handler with Connection
|
||||
- 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-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 Remote namespace Services and 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-api-remotes`: the BFF layer; owns the Host Agent/Session resolver, selects Client `/remote` contributions, and exposes the merged Remote types to business packages through the shared `TypertClientRemote` contract.
|
||||
- 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.
|
||||
- `@deepseek-ai/dsh-api-remotes`: the BFF layer; registers the application's forwarded Cordis event source and the Host home carried by generation readiness, selects Client `/remote` contributions, and exposes the merged Remote types to business packages through the shared `TypertClientRemote` contract.
|
||||
- Connection: owns the single HTTP Server/future WebSocket carrier, the shared `/api` route and its composite FetchHandler, owner-registered exact Fetch routes, the 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.
|
||||
- API Proxy Host composition: supplies Web Agent defaults and scope setup to API Remotes and consumes the same `agentFor()` for legacy methods.
|
||||
- `@deepseek-ai/dsh-api-session-controller`: configures the shared `agent`/`session` lookup and `agent` Host Context resolver, so every Remote endpoint that accepts one of those objects shares one resume and ownership-fence policy.
|
||||
- Business Service packages: declare bindings, Remote methods, and their request/result types, and export the generated `/remote` subpath.
|
||||
|
||||
## Shipped scope and deferred work
|
||||
|
||||
The shipped vertical path is `@deepseek-ai/dsh-goal/remote → Browser Client Remote → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()`. The same direct descriptor with an Agent lookup supports both `ctx.remote.goals.create(agentId, request)` and `agentCtx.remote.goals.create(request)`. Ordinary cold sessions are resumed through `agentFor()` during lookup, while subagent-owned identities retain the existing `agent-busy` fence; `@RemoteScope('agent')` remains the distinct scoped-receiver mode.
|
||||
The shipped vertical path is `@deepseek-ai/dsh-goal/remote → Browser Client Remote → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()`. The same direct descriptor with an Agent lookup supports both `ctx.remote.goals.create(agentId, request)` and `agentCtx.remote.goals.create(request)`. Ordinary cold sessions are resumed by the shared lookup resolver, while subagent-owned identities retain the `session/agent-busy` fence; `@RemoteScope('agent')` remains the distinct scoped-receiver mode.
|
||||
|
||||
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, retries, idempotency, and cross-version protocol compatibility remain outside this decision.
|
||||
|
||||
The package topology is `api/remotes → api/gateway → client/connection → host/webserver`. Connection and WebServer retain their existing paths in this change; moving them later to `api/connection` and `api/webserver` changes package placement rather than these service boundaries. The legacy API Proxy likewise remains under `host/apiproxy` as the fallback for methods not yet migrated to Remote.
|
||||
The package topology is `api/remotes → api/gateway → client/connection → host/webserver`. Connection and WebServer retain their existing paths in this change; moving them later to `api/connection` and `api/webserver` changes package placement rather than these service boundaries.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -486,7 +488,7 @@ The package topology is `api/remotes → api/gateway → client/connection → h
|
||||
|
||||
**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 Client Remote Service.
|
||||
|
||||
**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.
|
||||
**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 compose it from owner-registered exact Fetch routes and the channel's single interceptor.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -496,11 +498,11 @@ The package topology is `api/remotes → api/gateway → client/connection → h
|
||||
- Importing `@deepseek-ai/dsh-goal/remote` adds the strict `ctx.remote.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.
|
||||
- Agent and Session lookups share a single in-flight cold-session resume; ordinary cold sessions receive restored objects, while both cold and live subagent identities return `agent-busy` before business invocation.
|
||||
- Agent and Session lookups share a single in-flight cold-session resume; ordinary cold sessions receive restored objects, while both cold and live subagent identities return `session/agent-busy` before business invocation.
|
||||
- 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.
|
||||
- Cancellation tests cover strict generation, SRC final-name recognition, Client signal fusion, Connection-to-Gateway propagation, and Host injection outside wire `args`.
|
||||
- Unclaimed endpoints continue through the existing API Proxy path with its trust, privileged-method, Permission/Approval, and Session event-stream behavior unchanged.
|
||||
- A request that matches neither an exact Fetch route nor a claimed Remote endpoint answers 404 on the same channel, while a withdrawn route stops being served.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
+25
-23
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
Host API Proxy 同时承担直接方法调用、带状态交互和 Session 事件流。三者的生命周期、路由语义和客户端编程界面不同,继续共用一个业务导出包会让业务 Service、传输协议、状态机和客户端类型彼此耦合。
|
||||
Host API Proxy 当时在一个包里同时承担直接方法调用、带状态交互和 Session 事件流。三者的生命周期、路由语义和客户端编程界面不同,继续共用一个业务导出包会让业务 Service、传输协议、状态机和客户端类型彼此耦合。
|
||||
|
||||
本决策只涵盖一次请求对应一次结果的定向方法调用。Permission、Approval 等带状态交互以及 Session 事件流仍采用独立设计。
|
||||
|
||||
@@ -22,7 +22,7 @@ Remote 消费端投影同时包含 `.d.ts`、`.d.ts.map` 和 `.js`。`.d.ts` 只
|
||||
|
||||
`@deepseek-ai/dsh-api-gateway` 位于 `packages/api/gateway`,提供对称的两个 face:默认入口提供 Host `ctx.typertGateway`,`/client` 入口提供消费端 `ctx.remote`。两边各自在本地消费由同一模型生成的 `InvocationDescriptor`,descriptor 不通过 wire 发送。Remote 数据协议运行在 Connection 共享的 `/api` RPC channel 上;业务调用界面不随 Connection 从 HTTP 迁移到 WebSocket 而改变。
|
||||
|
||||
`@deepseek-ai/dsh-api-remotes` 位于 `packages/api/remotes`,是 Gateway 上层的 BFF 层。其 Host 入口负责 Agent/Session 身份解析与 Typert lookup 配置;`/client` 入口选择应用对外暴露的生成 Remote contribution。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 约定,而不导入具体 Gateway 实现。
|
||||
`@deepseek-ai/dsh-api-remotes` 位于 `packages/api/remotes`,是 Gateway 上层的 BFF 层。其 Host 入口注册本应用转发的 Cordis 事件源与随 generation readiness 携带的 Host 事实;`/client` 入口选择应用对外暴露的生成 Remote contribution。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 约定,而不导入具体 Gateway 实现。
|
||||
|
||||
## 组件和 Cordis 服务
|
||||
|
||||
@@ -32,7 +32,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` |
|
||||
| API Gateway 的 Host face | `ctx.typertGateway` | 关联 Host definition 与活 Service,解码参数、解析 receiver、调用方法和编码结果 |
|
||||
| Connection | `ctx.connection` | 独占 HTTP Server/未来 WebSocket、共享 `/api` route、RPC envelope、rpcId、序列化、trust、错误传输、Typert 拦截和旧 API Proxy 回退 |
|
||||
| Connection | `ctx.connection` | 独占 HTTP Server/未来 WebSocket、共享 `/api` route、RPC envelope、rpcId、序列化、trust、错误传输、Typert 拦截,以及各 owner 在同一 channel 上注册的精确 Fetch route |
|
||||
| API Gateway 的 Client face | `ctx.remote`、`ctx.remote.<namespace>` | mount Remote contribution,把每个 namespace 实体化为可追踪的 `remote.<namespace>` 子 Service,并把规范调用交给 `ctx.connection.rpc` |
|
||||
| API Remotes | 无新增服务 | 负责 Host Agent/Session lookup 策略,并作为 Client 业务的唯一 facade,选择并挂载 `/remote` contribution,同时暴露所选 API 声明 |
|
||||
| Agent/Session owning 包 | 既有领域服务 | 同时提供静态 interface merge 与运行时 lookup/Context provider |
|
||||
@@ -162,9 +162,9 @@ ctx.typert.contexts Host Context resolvers and Client Context binders
|
||||
|
||||
每次注册都返回由调用方 Cordis fiber 持有的 disposer。挂载 Client contribution 时,descriptor 集与具体方法会作为一项有明确所有者的操作统一注册。Host Gateway 只缓存 SRC 所认领的 endpoint 名称集合,并在 Cordis Service 集合发生变化时整体丢弃该集合;它不保留 descriptor、Service 或提供方。调用时会从当前状态解析所有活对象,因此移除 strict definition、Service 或提供方会使相应调用不可用,且不会留下陈旧的活对象。
|
||||
|
||||
lookup 注册表会在活 resolver 卸载后保留稳定的 wire 声明。SRC 解析仍会把该参数归类为 lookup,而调用会以 `lookup-unavailable` 失败;系统绝不会把传入的 ID 重新归类为普通 JSON 业务对象。在同一个 Typert Service 的生命周期内,以不同参数、wire 或规范类型 symbol 重新注册同一 key 会直接失败。
|
||||
lookup 注册表会在活 resolver 卸载后保留稳定的 wire 声明。SRC 解析仍会把该参数归类为 lookup,而调用会以 `gateway/lookup-unavailable` 失败;系统绝不会把传入的 ID 重新归类为普通 JSON 业务对象。在同一个 Typert Service 的生命周期内,以不同参数、wire 或规范类型 symbol 重新注册同一 key 会直接失败。
|
||||
|
||||
业务对象包和 scoped Context 包通过 `lookups.register()` 与 `contexts.registerHost()` 拥有稳定声明和默认 resolver;Host 组合通过 `lookups.configure()` 与 `contexts.configureHost()` 提供 effect-scoped 异步策略。配置可以先于 provider 注册,但没有活 provider 时不会单独形成可用身份;配置卸载后恢复 provider 默认 resolver。API Remotes 为 `agent`、`session` lookup 和 `agent` Host Context 创建共享的 `agentFor()` resolver:live Agent 直接复用,普通冷会话自动恢复,并发恢复按 Session ID 去重,subagent ownership fence 则返回既有 `agent-busy`。标准 Web API Proxy 提供 Agent 默认值和 scope 设置,并让旧方法使用该 resolver。`session` lookup 返回解析所得 Agent 的 Session,`agent` Host Context 返回其 Context,因此三种投影共用一个恢复生命周期。
|
||||
业务对象包和 scoped Context 包通过 `lookups.register()` 与 `contexts.registerHost()` 拥有稳定声明和默认 resolver;Host 组合通过 `lookups.configure()` 与 `contexts.configureHost()` 提供 effect-scoped 异步策略。配置可以先于 provider 注册,但没有活 provider 时不会单独形成可用身份;配置卸载后恢复 provider 默认 resolver。Session Controller 的 `ApiSessionAgentController` 为 `agent`、`session` lookup 和 `agent` Host Context 配置同一个共享 resolver:live Agent 直接复用,普通冷会话自动恢复,并发恢复按 Session ID 去重,subagent ownership fence 则返回 `session/agent-busy`。`session` lookup 返回解析所得 Agent 的 Session,`agent` Host Context 返回其 Context,因此三种投影共用一个恢复生命周期。
|
||||
|
||||
Registry 的 Host 根入口拥有完整 `TypertRegistryContract` interface merge;Host 与 Client 共用的 registry 实现位于无环境声明的独立模块。Registry `/client` 入口只引用该共享实现,不经过 Host 根入口,因此不会把 Host Cordis 声明带入 Client Program。
|
||||
|
||||
@@ -254,7 +254,7 @@ interface TypertRemoteNamespace$676f616c73 {
|
||||
agentId: SessionId,
|
||||
request: CreateGoalRequest,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<CreateGoalResult>
|
||||
) => Promise<RemoteResult<CreateGoalResult>>
|
||||
}
|
||||
|
||||
interface TypertRemoteMap {
|
||||
@@ -262,7 +262,7 @@ interface TypertRemoteMap {
|
||||
agentId: SessionId,
|
||||
request: CreateGoalRequest,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<CreateGoalResult>
|
||||
) => Promise<RemoteResult<CreateGoalResult>>
|
||||
}
|
||||
|
||||
interface TypertRemoteNamespaceMap {
|
||||
@@ -273,7 +273,7 @@ interface TypertRemoteScopeMap {
|
||||
'agent:goals/create': (
|
||||
request: CreateGoalRequest,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<CreateGoalResult>
|
||||
) => Promise<RemoteResult<CreateGoalResult>>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -288,7 +288,9 @@ agentCtx.remote.goals.create(request)
|
||||
|
||||
Agent Scope 自动提供自己的 `SessionId`。因此带 `agent` lookup 的 `@Remote` 方法可以同时生成 root 和 scoped 两种消费端签名;`@RemoteScope('agent')` 方法也省略独立的 Scope identity,但只生成 scoped 签名。根 `Context` 通过 `ctx.remote` 暴露 direct namespace,`AgentContext.remote` 则把该 direct surface 与 scoped surface 取交集。未来 TUI 复用时必须维持相同区分。
|
||||
|
||||
`TypertClientRemote` 保持平台无关,Browser Client 通过 `ctx.remote` 暴露它。未来 TUI 若复用该类型,也必须通过专用 Remote 对象和 Agent Scope 使用它,不能把 Host `Context` 当成更宽的 Service 集合;未标记的 public Service 方法不会进入 Remote maps。
|
||||
每个生成方法都解析为 `Promise<RemoteResult<T>>`:调用把结果报告在 `ok` 分支里而不是 reject,只有装配故障(arity、未挂载的方法、缺失的 Context adapter)仍然抛出。消费方按 `result.ok` 分支,需要区分失败时读 `result.error.code`;失败词汇本身是[单一 Remote 失败类加一张合并码表](2026-08-28-ctx-remote-failure-vocabulary.zh.md)。
|
||||
|
||||
`TypertClientRemote` 保持平台无关,Browser Client 通过 `ctx.remote` 暴露它。未来 TUI 若复用该类型,也必须通过专用 Remote 对象和 Agent Scope 使用它,不能把 Host `Context` 当成更宽的 Service 集合;未标记的 public Service 方法不会进入 Remote maps。除生成的 namespace 之外,Gateway client face 还提供 `$mount`、`$on`、`$stream` 与 `$host`——最后这项把连接的固定 Host 事实(`home`、`isLoopback`)作为普通值读取暴露,消费方无需为此注入载体。
|
||||
|
||||
## Client Typert 与 API Gateway Client face
|
||||
|
||||
@@ -361,7 +363,7 @@ CI 和发布运行 LIB。全仓 coverage 全部切换到 LIB 是独立后续工
|
||||
|
||||
## Host Gateway 解析
|
||||
|
||||
Host Gateway 向 Connection 注册一个 `/api` interceptor,不维护第二份 endpoint 注册表。ownership matcher 会先检查当前 Typert local 注册表,再查询一份可失效的集合;该集合通过扫描当前 Cordis Service 中的 `typertGateway` binding 与 SRC Remote 标记生成。Cordis Service 发生变化时会整体丢弃该集合,因此 Typert definition 与业务 Service 可以按任意顺序到达,同时既不会让旧 API Proxy 的 `/api` 流量在每次请求时重新扫描所有 Service,也不会因任意请求路径而扩大缓存。
|
||||
Host Gateway 向 Connection 注册一个 `/api` interceptor,不维护第二份 endpoint 注册表。ownership matcher 会先检查当前 Typert local 注册表,再查询一份可失效的集合;该集合通过扫描当前 Cordis Service 中的 `typertGateway` binding 与 SRC Remote 标记生成。Cordis Service 发生变化时会整体丢弃该集合,因此 Typert definition 与业务 Service 可以按任意顺序到达,同时既不会在每次请求时重新扫描所有 Service,也不会因任意请求路径而扩大缓存。
|
||||
|
||||
每次调用都会重新从当前状态解析 descriptor、receiver、lookup 提供方与 Context 提供方。当前 strict descriptor 优先于 SRC。strict endpoint 一旦出现,即使随后撤回对应 descriptor,`TypertLocalRegistry.hasSeen()` 仍会在注册表剩余生命周期内保持对它的认领并禁止回退 SRC;重新注册 strict descriptor 即可恢复调用。移除 Service 或提供方会让调用明确失败;Gateway 既不保留失效对象,也不会以原始 lookup ID 调用方法。
|
||||
|
||||
@@ -399,9 +401,9 @@ ctx.connection.rpc.intercept(
|
||||
)
|
||||
```
|
||||
|
||||
Host registry 中存在 strict descriptor、记录过已撤回的 strict descriptor,或 active SRC Service binding 上存在匹配的 `@Remote` 标记时,Gateway 认领该 endpoint。endpoint 一旦被认领,即使 payload 解码、descriptor 解析或调用失败也继续由 Gateway 返回错误;只有不属于 Remote 的 endpoint 才进入旧 API Proxy 回退。
|
||||
Host registry 中存在 strict descriptor、记录过已撤回的 strict descriptor,或 active SRC Service binding 上存在匹配的 `@Remote` 标记时,Gateway 认领该 endpoint。endpoint 一旦被认领,即使 payload 解码、descriptor 解析或调用失败也继续由 Gateway 返回错误;既不匹配精确 Fetch route、也不被 Gateway 认领的 endpoint 返回 404。
|
||||
|
||||
Connection Host half 把一个复合 FetchHandler 交给 HTTP bridge。bridge 创建标准 `Request` 后,该 handler 再选择 Gateway RPC FetchHandler 或 API Proxy FetchHandler;两条路径复用同一 request/response envelope、rpcId、序列化、trust、transport error 和 `RpcError`。当前物理映射是:
|
||||
Connection Host half 把一个复合 FetchHandler 交给 HTTP bridge。bridge 创建标准 `Request` 后,该 handler 先用 pathname 匹配各 owner 在该 channel 上注册的精确 Fetch route,再匹配该 channel 唯一的 interceptor——即 Gateway——两者都不认领时返回 404。该 channel 上的每条路径复用同一 request/response envelope、rpcId、序列化、trust 与错误传输,失败则携带共享的 `{ code, message, details }` 数据。当前物理映射是:
|
||||
|
||||
```text
|
||||
POST /api/<namespace>/<method>
|
||||
@@ -438,15 +440,15 @@ ctx.remote.goals.create(sessionId, request, signal?)
|
||||
→ Client result codec 验证并返回 CreateGoalResult
|
||||
```
|
||||
|
||||
Remote 不定义第二层 `{ ok, value/error }` response。成功值和 Gateway 错误直接使用既有 RPC response 的 `result`。adapter 把普通 Gateway 与业务调用失败转换为既有 `RpcError` envelope,并统一使用 `code: 'internal'`;resolver 通过 `TypertLookupFailure` 携带的既有 RPC error 则原样返回,使冷恢复失败和 ownership fence 保持稳定错误码。Gateway 的结构化错误分类仅在进程内保留,诊断信息则通过 message 跨 Connection 传递。
|
||||
Remote 不在 wire 上定义第二层 `{ ok, value/error }` response。成功值与失败都直接使用既有 RPC response 的 `result`,失败分支携带共享的 `{ code, message, details }` 数据。owner、resolver 与 Gateway 抛的都是同一个类 `RemoteError`,其码来自合并后的 `RemoteErrorDetailsMap`:Host 把结构识别出的 `RemoteError` 原样编码上 wire——包括 Gateway 自己的 `gateway/*` 装配码,以及 resolver 的 `session/not-found`、`session/agent-busy`——只把未归类的 throw 折成 `gateway/internal`,并把诊断串留在 message 里。Client face 为 `RemoteResult` 的错误分支重建实例,因此 `throw result.error` 的 throw 语义成立。[失败词汇 Agent Note](2026-08-28-ctx-remote-failure-vocabulary.zh.md) 持有码表、落点规则,以及为什么判别读 `code` 而不用 `instanceof`。
|
||||
|
||||
Gateway 不处理逐方法权限、调用者身份、幂等或长连接状态。它只把 Connection 的协作式取消传播给显式支持取消的业务方法。Typert endpoint 使用 Connection 的 trusted-host 策略;未认领 endpoint 保留旧 API Proxy 的 trust 和 privileged-method 策略。Connection/WebSocket 迁移后续独立完成。
|
||||
Gateway 不处理逐方法权限、调用者身份、幂等或长连接状态。它只把 Connection 的协作式取消传播给显式支持取消的业务方法。共享 channel 上的每个请求——无论是 Typert endpoint 还是精确 Fetch route——都先过 Connection 的浏览器认证与 trusted-host 策略再分发;Gateway 不叠加第二套策略。Connection/WebSocket 迁移后续独立完成。
|
||||
|
||||
## Connection 与协议边界
|
||||
|
||||
Client Remote Service 负责 Remote contribution、namespace Service 实体化、Scope 绑定以及位置参数与 descriptor 的对应。Gateway 负责 Host descriptor、endpoint ownership、lookup、Context 和业务调用。Connection 把 `/api`、endpoint 和 `{ args }` 作为一个 RPC 调用发送到目标并返回既有 RPC result;它不理解 Goal、Agent、lookup、descriptor 或 Client Remote 类型。
|
||||
|
||||
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 编程界面。
|
||||
Gateway 只向 Connection 注册 ownership matcher 和 RPC handler,不注册 HTTP route。Connection 把共享 `/api` route 挂到 HTTP Server,并把一个复合 FetchHandler 交给 bridge;该 handler 把精确注册路径分发给它的 route owner、把已认领 endpoint 分发给 Gateway,其余一律 404。未来 Connection transport 可以保留相同顺序,而不改变 Remote payload、业务 decorator、生成的 DTS、Remote API 类型或 Agent Scope 编程界面。
|
||||
|
||||
## 包边界
|
||||
|
||||
@@ -454,19 +456,19 @@ Gateway 只向 Connection 注册 ownership matcher 和 RPC handler,不注册 H
|
||||
- Typert generator:分析 Host/Client Program,生成本地 face 和 Remote 消费端投影,并生成规范 symbol/Zod 信息。
|
||||
- Typert runtime:分别保存当前环境的 local reflection 与导入的 Remote contribution。
|
||||
- `@deepseek-ai/dsh-api-gateway`:默认入口关联 Host definition 与 Service,认领 Remote endpoint,执行 lookup、Context receiver 解析、调用和结果编码,并向 Connection 注册 `/api` interceptor;`/client` 入口挂载 Remote contribution,创建严格 Remote namespace Service 和方法,并把调用交给 `ctx.connection.rpc`。两个入口共享 Remote 协议,但不互相导入各自的 Cordis interface merge。
|
||||
- `@deepseek-ai/dsh-api-remotes`:BFF 层;负责 Host Agent/Session resolver,选择 Client `/remote` contribution,并通过共享的 `TypertClientRemote` 约定向业务包暴露合并后的 Remote 类型。
|
||||
- Connection:拥有唯一 HTTP Server/未来 WebSocket carrier、共享 `/api` route 与复合 FetchHandler、API Proxy 回退、RPC envelope、rpcId、序列化、trust 和错误传输。
|
||||
- `@deepseek-ai/dsh-api-remotes`:BFF 层;注册本应用转发的 Cordis 事件源与随 generation readiness 携带的 Host home,选择 Client `/remote` contribution,并通过共享的 `TypertClientRemote` 约定向业务包暴露合并后的 Remote 类型。
|
||||
- Connection:拥有唯一 HTTP Server/未来 WebSocket carrier、共享 `/api` route 与其复合 FetchHandler、各 owner 注册的精确 Fetch route、RPC envelope、rpcId、序列化、trust 和错误传输。
|
||||
- Agent/Session 等业务对象包:拥有 lookup、Context provider、唯一 ID 类型和纯类型公共出口。
|
||||
- API Proxy Host 组合:向 API Remotes 提供 Web Agent 默认值和 scope 设置,并让旧方法使用同一个 `agentFor()`。
|
||||
- `@deepseek-ai/dsh-api-session-controller`:配置共享的 `agent`/`session` lookup 与 `agent` Host Context resolver,因此每个接收这些对象的 Remote endpoint 共用同一套恢复与 ownership fence 策略。
|
||||
- 业务 Service 包:声明 binding、Remote 方法及其 request/result 类型,并导出生成的 `/remote` 子路径。
|
||||
|
||||
## 已交付范围与后续工作
|
||||
|
||||
已交付的纵向链路是 `@deepseek-ai/dsh-goal/remote → Browser Client Remote → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()`。同一个带 Agent lookup 的 direct descriptor 同时支持 `ctx.remote.goals.create(agentId, request)` 与 `agentCtx.remote.goals.create(request)`。普通冷会话在 lookup 时通过 `agentFor()` 恢复,subagent-owned identity 保持既有 `agent-busy` fence;`@RemoteScope('agent')` 仍是独立的 scoped receiver 模式。
|
||||
已交付的纵向链路是 `@deepseek-ai/dsh-goal/remote → Browser Client Remote → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()`。同一个带 Agent lookup 的 direct descriptor 同时支持 `ctx.remote.goals.create(agentId, request)` 与 `agentCtx.remote.goals.create(request)`。普通冷会话在 lookup 时由该共享 resolver 恢复,subagent-owned identity 保持 `session/agent-busy` fence;`@RemoteScope('agent')` 仍是独立的 scoped receiver 模式。
|
||||
|
||||
Connection 提供共享 channel interceptor 与当前 HTTP carrier 映射。WebSocket 迁移、TUI runtime 与 carrier、TUI Agent Scope 接线、Permission/Approval 状态机、Session 事件流、调用授权、重试、幂等及跨版本协议兼容均不属于本决策。
|
||||
|
||||
包拓扑为 `api/remotes → api/gateway → client/connection → host/webserver`。Connection 与 WebServer 在本次变更中保留既有路径;后续将它们移到 `api/connection` 和 `api/webserver` 只会改变包位置,不会改变这些服务边界。旧 API Proxy 同样保留在 `host/apiproxy` 下,作为尚未迁移到 Remote 的方法的回退路径。
|
||||
包拓扑为 `api/remotes → api/gateway → client/connection → host/webserver`。Connection 与 WebServer 在本次变更中保留既有路径;后续将它们移到 `api/connection` 和 `api/webserver` 只会改变包位置,不会改变这些服务边界。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -486,7 +488,7 @@ Connection 提供共享 channel interceptor 与当前 HTTP carrier 映射。WebS
|
||||
|
||||
**让 `/remote` 的顶层 import 偷偷注册全局状态。** ESM 求值时未必已有目标 Cordis Context,多个 Context、HMR 和 dispose 也无法明确归属,因此普通 value import 只返回 contribution,由环境 assembly 的 Client Remote Service 显式挂载。
|
||||
|
||||
**为 Remote 新建独立 transport、HTTP route 或 `/api2` channel。** 这会复制或拆分 Connection 的 Server ownership、rpcId、序列化、trust、错误和未来 WebSocket 生命周期。共享 `/api` interceptor 保留唯一物理 route,并让 Connection 继续以 API Proxy 作为回退 FetchHandler。
|
||||
**为 Remote 新建独立 transport、HTTP route 或 `/api2` channel。** 这会复制或拆分 Connection 的 Server ownership、rpcId、序列化、trust、错误和未来 WebSocket 生命周期。共享 `/api` interceptor 保留唯一物理 route,并让 Connection 用各 owner 注册的精确 Fetch route 与该 channel 唯一的 interceptor 组合出它。
|
||||
|
||||
## 验证
|
||||
|
||||
@@ -496,11 +498,11 @@ Connection 提供共享 channel interceptor 与当前 HTTP carrier 映射。WebS
|
||||
- 导入 `@deepseek-ai/dsh-goal/remote` 会加入严格的 `ctx.remote.goals.create(...)` 类型,并可通过 declaration 导航到 `remoteExportCreate`;不导入时不会出现该 namespace。
|
||||
- 挂载同一次 import 得到的 JS contribution 会提供 endpoint、参数、结果、lookup、Context 和 Zod 反射,并在无需手写 stub 的情况下实体化调用。
|
||||
- Root 与 Agent-scoped 调用会经过真实的共享 `/api` carrier,将 `agentId` 解析为活 Agent,调用原始 Goal receiver,并通过既有 RPC envelope 返回。
|
||||
- Agent 与 Session lookup 会共享同一次并发冷恢复;普通冷会话得到恢复后的对象,冷态或 live subagent identity 均在业务调用前返回 `agent-busy`。
|
||||
- Agent 与 Session lookup 会共享同一次并发冷恢复;普通冷会话得到恢复后的对象,冷态或 live subagent identity 均在业务调用前返回 `session/agent-busy`。
|
||||
- Remote 产物与 map 仅包含已标记的方法,不依赖 Browser,从而为未来 TUI 保留相同的消费方边界。
|
||||
- 生命周期测试会撤回并重新挂载 descriptor、Service、lookup、Context 提供方和 Client namespace;依赖不可用时,调用会失败,且不会使用陈旧调用或回退原始 ID。
|
||||
- 取消测试覆盖严格生成、SRC 末位参数名识别、Client signal 合并、Connection 到 Gateway 的传播,以及 Host 在 wire `args` 之外的注入。
|
||||
- 未认领 endpoint 继续使用既有 API Proxy 路径,其 trust、privileged-method、Permission/Approval 与 Session 事件流行为保持不变。
|
||||
- 既不匹配精确 Fetch route、也不属于已认领 Remote endpoint 的请求在同一 channel 上返回 404,而已撤回的 route 随即停止服务。
|
||||
|
||||
## 后果
|
||||
|
||||
|
||||
+2
-2
@@ -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/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md
|
||||
2026-08-10-unary-apiproxy-remote-migration.md: b98c7ee95b61ec00a5cab3106e812a6f17fc0a15
|
||||
2026-08-10-unary-apiproxy-remote-migration.zh.md: 74bca8fd72f3e41075f5a44eba116fe127343bb2
|
||||
2026-08-10-unary-apiproxy-remote-migration.md: ee93276b08e204b8c10c22c9fbb890a73691c5a9
|
||||
2026-08-10-unary-apiproxy-remote-migration.zh.md: 63eb30a1c079c4f5beb3c6aa328ae31f9dbc51b2
|
||||
+2
-2
@@ -31,10 +31,10 @@ Simple unary operations live on their natural business Remote owner. The busines
|
||||
| `skill.list` | `skills/list` | `SessionSkillCatalog` observes the Session and its recorded preset, uses a live Agent only when one already exists, and never activates an Agent for listing. |
|
||||
| `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` supplies the Session Controller's established Agent lookup to the provider; cold lookup behavior remains unchanged. |
|
||||
| `host.openPath` | `session/openWorkspacePath` | The Session-aware Client resolves relative paths against the known workspace before `SessionController` hands them to the native opener. |
|
||||
| `host.describe` | `$events` ready frame plus capability queries | API Remotes sends the Host home with generation readiness; Settings and Session controllers report their native-open capabilities when the corresponding page appears. Unused process metadata is not sent. |
|
||||
| `host.describe` | `$events` ready frame plus capability queries | API Remotes sends the Host home with generation readiness, and consumers read it as a plain value through `ctx.remote.$host.home` beside `$host.isLoopback`; Settings and Session controllers report their native-open capabilities when the corresponding page appears. Unused process metadata is not sent. |
|
||||
| `session.export` | `GET`/`HEAD /api/session.export` | `session-log-export` registers an exact Connection Fetch route and streams the ZIP without a JSON Remote envelope. |
|
||||
|
||||
The shared Agent and Session resolver remains the authority for endpoints that accept those objects. It provides the same live reuse, cold restoration, concurrent deduplication, preset setup, persistence failures, and subagent ownership fence that legacy API Proxy calls used. `TypertLookupFailure` preserves resolver-owned RPC errors instead of collapsing them into `internal`.
|
||||
The shared Agent and Session resolver remains the authority for endpoints that accept those objects. It provides the same live reuse, cold restoration, concurrent deduplication, preset setup, persistence failures, and subagent ownership fence that legacy API Proxy calls used. The resolver raises a `RemoteError` carrying its own code — `session/not-found` or `session/agent-busy` — and the Gateway encodes that code, message, and details onto the wire unchanged, so a lookup refusal stays distinguishable from `gateway/internal` ([failure vocabulary](2026-08-28-ctx-remote-failure-vocabulary.md)).
|
||||
|
||||
The native path implementation lives in `@deepseek-ai/dsh-native-command`. Settings controllers select Host-owned targets, while Session-aware Clients resolve workspace paths before calling `SessionController`; the utility only performs platform detection, WSL translation, browser preference, text-editor intent, and shell-free command execution.
|
||||
|
||||
|
||||
+2
-2
@@ -31,10 +31,10 @@ Host API Proxy 曾在业务 Service、API Proxy interface、Zod schema、路由
|
||||
| `skill.list` | `skills/list` | `SessionSkillCatalog` 观察 Session 及其记录的 preset,仅在 live Agent 已存在时使用它,列表查询绝不激活 Agent。 |
|
||||
| `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` 向 provider 提供 Session Controller 的既有 Agent lookup;冷 lookup 行为保持不变。 |
|
||||
| `host.openPath` | `session/openWorkspacePath` | Session-aware Client 先基于已知 workspace 解析相对路径,再由 `SessionController` 交给原生打开器。 |
|
||||
| `host.describe` | `$events` ready frame 与 capability 查询 | API Remotes 随 generation readiness 发送 Host home;Settings 与 Session controller 在对应页面显示时报告各自的原生打开能力。不发送无人使用的进程元数据。 |
|
||||
| `host.describe` | `$events` ready frame 与 capability 查询 | API Remotes 随 generation readiness 发送 Host home,消费方通过 `ctx.remote.$host.home` 与并列的 `$host.isLoopback` 以普通值读取;Settings 与 Session controller 在对应页面显示时报告各自的原生打开能力。不发送无人使用的进程元数据。 |
|
||||
| `session.export` | `GET`/`HEAD /api/session.export` | `session-log-export` 注册精确的 Connection Fetch 路由,并在没有 JSON Remote envelope 的情况下流式传输 ZIP。 |
|
||||
|
||||
共享 Agent 与 Session resolver 仍是接收这些对象的 endpoint 的权威。它提供与旧 API Proxy 调用相同的 live 复用、冷恢复、并发去重、preset setup、持久化失败与 subagent ownership fence。`TypertLookupFailure` 保留 resolver 持有的 RPC error,而不把它们归并为 `internal`。
|
||||
共享 Agent 与 Session resolver 仍是接收这些对象的 endpoint 的权威。它提供与旧 API Proxy 调用相同的 live 复用、冷恢复、并发去重、preset setup、持久化失败与 subagent ownership fence。resolver 抛出携带自有码的 `RemoteError`——`session/not-found` 或 `session/agent-busy`——Gateway 把该码、message 与 details 原样编码上 wire,因此 lookup 拒绝与 `gateway/internal` 始终可区分([失败词汇](2026-08-28-ctx-remote-failure-vocabulary.zh.md))。
|
||||
|
||||
原生路径实现在 `@deepseek-ai/dsh-native-command` 中。Settings controller 选择 Host 持有的目标,Session-aware Client 则在调用 `SessionController` 前解析 workspace 路径;该工具仅负责平台探测、WSL 转换、浏览器偏好、文本编辑器意图与无 shell 命令执行。
|
||||
|
||||
|
||||
+6
@@ -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-28-ctx-remote-failure-vocabulary.md
|
||||
2026-08-28-ctx-remote-failure-vocabulary.md: fe8cafb6116d73797e1dae07fb28fe52d42c9285
|
||||
2026-08-28-ctx-remote-failure-vocabulary.zh.md: 6b75aae454fb9e4d7c4622525220d1949c45404d
|
||||
@@ -0,0 +1,88 @@
|
||||
# Agent Note: One Remote failure vocabulary for ctx.remote
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-28-ctx-remote-failure-vocabulary.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Every Remote owner package maintained its own failure surface: an `XxxErrorDetailsMap` interface, an `XxxError` union derived from it, and an exit mapping function that translated domain error classes (`UnknownPresetError`, `PresetMountError`, `SessionTitleInvalidError`, and their peers) into a wire failure value. `@deepseek-ai/dsh-typert-protocol` carried two failure classes at once — `TypertRemoteFailure` for a failure an owner reported and `TypertLookupFailure` for one a lookup resolver produced — while `@deepseek-ai/dsh-client-connection` kept a second typed view, `RpcErrorDetailsMap`, that hardcoded domain codes such as `agent-preset-not-found` and `session-not-found` into the carrier.
|
||||
|
||||
One code therefore existed in three places: the owner's table, the carrier's typed view, and whatever union or cast a consumer wrote to narrow it (`result.error as SessionError`). Adding a domain code meant editing all three, and relaying another domain's code meant copying that code into your own table — `SessionErrorDetailsMap` had absorbed five foreign codes this way, across `agent-preset-*`, `subagent-*`, and `workspace-not-found`.
|
||||
|
||||
Failure information was flattened in two places as well. All 17 of the Gateway's own assembly failures (an unmounted method, an ambiguous endpoint, a lookup provider mismatch, a result that fails its codec) reached the wire as `code: 'internal'`, so a client could not separate an assembly fault from a business refusal; owners defensively pre-folded unrelated exceptions into their own domain codes, so a genuine Host bug arrived at the caller as a plausible-looking domain failure.
|
||||
|
||||
Fixed Host facts bypassed `ctx.remote` too: the Host home came from `(ctx.get('connection') as ConnectionHandle).generation.getSnapshot()?.host.home`, so every page that needed one fixed fact injected the carrier and understood its generation store.
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-typert-protocol` exports one failure class, `RemoteError<Code>`: a real `Error` carrying readonly `code` and `details`, the structural marker `isDSHRemoteError`, and standard `ErrorOptions` (`cause` holds in-process only). The correspondence between codes and details lives in one merge-extensible `RemoteErrorDetailsMap`; `RemoteFailure` is the code-distributed union of instances, and `RemoteResult<T>` keeps its shape.
|
||||
|
||||
```text
|
||||
export class RemoteError<Code extends RemoteErrorCode = RemoteErrorCode> extends Error {
|
||||
readonly isDSHRemoteError: true = true
|
||||
constructor(readonly code: Code, message: string,
|
||||
readonly details: RemoteErrorDetailsMap[Code], options?: ErrorOptions)
|
||||
}
|
||||
export type RemoteFailure = { [C in RemoteErrorCode]: RemoteError<C> }[RemoteErrorCode]
|
||||
export type RemoteResult<T> = { ok: true; value: T } | { ok: false; error: RemoteFailure }
|
||||
```
|
||||
|
||||
A failure point throws directly: `throw new RemoteError(code, message, details)`. A domain builds no error-class family and writes no exit mapping function; only the "classify any provider exception" case keeps one `catch`, and inside it `throw new RemoteError(code, messageOf(error), details, { cause: error })`. An existing exception class that an in-process flow still consumes (`ApiSessionCwdConflict` and its peers) stays as a non-exported private class and converts to a `RemoteError` in one line at the exit.
|
||||
|
||||
A code is a `<domain>/<reason>` string: `session/not-found`, `gateway/cancelled`, `workspace/invalid-path`, `agent-preset/locked`. The prefix follows the wire-namespace style, so the code itself says who owns it, and relaying another domain's code no longer needs an awkward unprefixed name.
|
||||
|
||||
## Code ownership
|
||||
|
||||
A code has exactly one declaration site, and the site follows from both who produces it and who can see the declaration — declaration merging only applies where the augmenting file enters the current program, so the home must be a package every producer already sees:
|
||||
|
||||
- **Carrier codes**: `gateway/bad-request`, `gateway/cancelled`, and `gateway/internal` are declared by the protocol and reachable everywhere.
|
||||
- **Gateway assembly codes**: the 17 `gateway/*` codes are declared in `packages/api/gateway/src/remote-error-codes.ts` with the uniform `TypertGatewayFaultDetails { endpoint, field? }` details; that module is face-neutral and each face imports it, so both programs see the same entries.
|
||||
- **Produced by several packages**: when two or more packages throw the same code, the declaration lands in the lowest layer both already depend on. `session/not-found` lands in `@deepseek-ai/dsh-session` (session-controller and workspace-controller both depend on it), and `workspace/not-found` lands in `@deepseek-ai/dsh-workspace` (no dependency edge exists between the two API packages, so the capability package is their only shared layer).
|
||||
- **Single producer**: a code only one package throws lands in that producer. `subagent/not-found` and `agent-preset/conflict` therefore live in session-controller — it is their only thrower in the repository, and neither the subagent nor the agent-presets table declares them.
|
||||
|
||||
What two domains share is validation logic, not a code. `session/invalid-time-zone` and `subagent/invalid-time-zone` are two codes each declared and thrown by its own domain, and both endpoints canonicalize through `canonicalClientTimeZone()` from `@deepseek-ai/dsh-util-time`; no client branches on this code, so splitting it costs nothing while merging it would recreate the reachability problem.
|
||||
|
||||
## Discrimination by code
|
||||
|
||||
Discrimination always reads `code` and never uses `instanceof`. Client and Host are separately bundled programs, and a worker transport bundles the page half once more, so several copies of the same class exist and prototype identity across copies does not hold. The mechanism layer reads the structural marker plus a string `code` through the protocol's `remoteErrorOf(value)`, and the Gateway client face additionally exports `isRemoteFailure(error)` for a consumer's catch site; both read those fields, never the class — the test does not even require `instanceof Error`, because an Error thrown in another realm fails that too.
|
||||
|
||||
Business code usually needs neither function: the `ok: false` branch of `RemoteResult` is already a typed `RemoteFailure`, so `if (result.error.code === 'session/not-found')` narrows `details` to that code's shape with no cast. A site that must propagate the failure writes `throw result.error` — it is a real `Error`, with a working stack and `message`.
|
||||
|
||||
The client plane does not construct `RemoteError`; the one exception is the Gateway's own client face, which rebuilds an instance from wire data in `invoke()` and folds carrier throws at stream boundaries into the same vocabulary. A test double that needs a failure value takes `RemoteError` from `@deepseek-ai/dsh-client-test-runtime` instead of making a client package import the protocol as a value. Assertions match the code (plus details fields where they matter) with `toMatchObject`: `RemoteError` is an `Error`, its own-key set differs from the former literal, and `toEqual` fails on it.
|
||||
|
||||
## Fixed Host facts
|
||||
|
||||
`ctx.remote.$host` exposes two fixed facts: `home: string | undefined` and `isLoopback: boolean`. It is a getter on the Client Remote service reading the connection handle captured at service construction — `home` comes from the ready frame in the generation snapshot (`undefined` before ready), `isLoopback` from the carrier. There is no store, no subscription, and no generation counter.
|
||||
|
||||
Refresh after a reconnect rides the existing signal: the Client Remote emits `connection/reset` when it connects, and a consumer that must re-read listens for that or for its own domain's remote event rather than turning `$host` into a subscribable object. Consumers therefore no longer inject `connection`: the `@deepseek-ai/dsh-client-connection` consumer allowlist shrinks to hmr, frontend-static, bundle/web-app, session-log-export, webworker-runtime, and the gateway and api-remotes assemblies.
|
||||
|
||||
## What the wire carries
|
||||
|
||||
The envelope is unchanged: the wire still carries `{ code, message, details }` data, and `RemoteError` is each side's in-process carrier for it. On the Host, `rpcFailure()` collapses to two branches — a structurally identified `RemoteError` is encoded as-is, everything else folds into `gateway/internal` — and carrier-signal cancellation uses the same vocabulary (the `RemoteInvocationCancelled` class is deleted, and its four throw points raise `RemoteError('gateway/cancelled', …)`).
|
||||
|
||||
Three wire-visible behaviors follow. The Gateway's 17 assembly codes travel as themselves, so a client can handle "method not mounted" separately from a business refusal. Owners do not pre-fold unrelated exceptions: an unclassified throw reaches the Gateway, which folds it into `gateway/internal` once and keeps the diagnostic chain in `message`. A client unary call aborted by its caller answers `gateway/cancelled`, matching the code the Host would have produced even when the local throw wins the race against the wire round-trip.
|
||||
|
||||
The carrier keeps only the open wire shape. `ConnectionRpcFailure` and `ConnectionRpcResult` in `@deepseek-ai/dsh-client-connection` carry no domain-code knowledge, and its `transportError()` produces `gateway/internal`; the only home for the typed view is now the protocol's `RemoteFailure`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A `RemoteFault` error-class family per domain.** Giving each domain (or each code) its own `Error` subclass reads as more object-oriented, but it splits one fact — the code — across class identity and a field, and cross-realm discrimination has to fall back to the field anyway. Class identity then becomes pure overhead: every domain maintains a subclass, exports it, and explains it in prose, while consumers still branch on `code`. One class plus one code table trades that weight for a single declaration line.
|
||||
|
||||
**`attempt` / `unwrap` / `remoteFailureOf` wrappers at call sites.** A wrapper saves one `if` per call site, but it turns `RemoteResult` from the canonical shape into "first pass it through a library function," and both styles then coexist indefinitely; `unwrap` additionally turns "failure is a normal result" back into an exception flow, against the Remote face's contract of never rejecting. The `remoteErrorOf` that survives serves the mechanism layer and test assertions only — business code holds either a typed `result.error` or a failure it threw itself.
|
||||
|
||||
**A `host/updated` event with a subscribed `$host` store.** A subscription would refresh automatically when the Host home changes, but `home` and `isLoopback` are fixed for the lifetime of one connection, so a store, generation, and subscription lifecycle would tax every page that only wants one read. Reconnection already has a signal (`connection/reset`) and business invalidation rides each domain's remote event, so fixed facts stay plain reads.
|
||||
|
||||
**Putting local, non-wire failures in the code table.** ui-goal's `no-current-goal` never crosses a process boundary; admitting it would mix entries only one client package cares about into a shared vocabulary and would suggest it has wire semantics. Local failures keep their own local types, and the code table describes the Remote vocabulary alone.
|
||||
|
||||
## Consequences
|
||||
|
||||
Adding a domain code is one declaration merge plus one throw: no mapping function, error class, and carrier typed view to keep in step. The cost is that the home now requires a judgment — it must be reachable from every producer — and that judgment only surfaces once a second producer appears; `workspace/not-found` moved from workspace-controller to the capability package exactly that way, which also gave `@deepseek-ai/dsh-workspace` a type-only protocol dependency.
|
||||
|
||||
Prefixing the code strings changes the wire strings wholesale, so codes embedded in connection fixtures, assertions on both the Host and Client sides, and spec-local declarations all move in one pass. The pre-release stance accepts that single cut; the same rename after a release would need a compatibility window.
|
||||
|
||||
The type of `details` follows from the code, so a code-and-details mismatch is rejected at compile time. The other face of that is every throw site having to supply the code's required detail fields: the protocol makes `issues` optional on `gateway/bad-request` precisely so a business validation point with no codec issues still writes `{}`.
|
||||
|
||||
`RemoteError` is an `Error`, so it keeps `message` and `cause` through any logger and through `errorChain()`; but `cause` holds only in-process, and the wire carries exactly `code`, `message`, and `details`. Cross-realm discrimination always reads the structural marker, and any new transport (a worker, a bundle split) must carry that marker or an equivalent marker frame across, or failure values degrade into plain `Error`s.
|
||||
|
||||
Consumer signatures for Remote methods are uniformly `Promise<RemoteResult<T>>`, matching the generated projection described in [the method-call surface](2026-08-02-typert-remote-method-calls.md); the ledger for the unary endpoints is [the unary endpoint migration](2026-08-10-unary-apiproxy-remote-migration.md).
|
||||
@@ -0,0 +1,88 @@
|
||||
# Agent Note: One Remote failure vocabulary for ctx.remote
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-28-ctx-remote-failure-vocabulary.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
每个 Remote owner 包各自维护一套失败面:一个 `XxxErrorDetailsMap` 接口、由它派生的 `XxxError` union,以及一个出口映射函数,把域内错误类(`UnknownPresetError`、`PresetMountError`、`SessionTitleInvalidError` 等)翻译成 wire 失败值。`@deepseek-ai/dsh-typert-protocol` 同时携带两个失败类——owner 主动上报用 `TypertRemoteFailure`,lookup resolver 产生的用 `TypertLookupFailure`——而 `@deepseek-ai/dsh-client-connection` 又保留了第二份 typed 视图 `RpcErrorDetailsMap`,把 `agent-preset-not-found`、`session-not-found` 这类域码硬编码进载体层。
|
||||
|
||||
于是一个码同时存在三处:owner 的表、载体的 typed 视图、以及消费方为窄化而写的 union 或 cast(`result.error as SessionError`)。新增一个域码要改三处,跨域转述一个别人的码则要把对方的码复制进自己的表——`SessionErrorDetailsMap` 就收编了 `agent-preset-*`、`subagent-*`、`workspace-not-found` 五个他域码。
|
||||
|
||||
失败信息也在两处被压平。Gateway 自己的 17 个装配失败(未挂载的方法、歧义 endpoint、lookup provider 不匹配、结果未过 codec 等)一律以 `code: 'internal'` 上 wire,client 无法把装配 bug 与业务拒绝区分开;owner 又出于防御把无关异常预折成自己的域码,于是一个真正的 Host bug 会以一个看起来合理的域失败到达调用方。
|
||||
|
||||
Host 固定事实同样绕过了 `ctx.remote`:Host home 取自 `(ctx.get('connection') as ConnectionHandle).generation.getSnapshot()?.host.home`,任何只需要一条固定事实的页面都得注入载体并理解它的 generation store。
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-typert-protocol` 导出唯一的失败类 `RemoteError<Code>`:一个真 `Error`,带只读 `code` 与 `details`、结构标记 `isDSHRemoteError`,以及标准 `ErrorOptions`(`cause` 只在进程内有效)。码与 details 的对应关系收进一张 merge-extensible 的 `RemoteErrorDetailsMap`;`RemoteFailure` 是按码分布的实例 union,`RemoteResult<T>` 形状不变。
|
||||
|
||||
```text
|
||||
export class RemoteError<Code extends RemoteErrorCode = RemoteErrorCode> extends Error {
|
||||
readonly isDSHRemoteError: true = true
|
||||
constructor(readonly code: Code, message: string,
|
||||
readonly details: RemoteErrorDetailsMap[Code], options?: ErrorOptions)
|
||||
}
|
||||
export type RemoteFailure = { [C in RemoteErrorCode]: RemoteError<C> }[RemoteErrorCode]
|
||||
export type RemoteResult<T> = { ok: true; value: T } | { ok: false; error: RemoteFailure }
|
||||
```
|
||||
|
||||
失败点直接 `throw new RemoteError(code, message, details)`。域内不再建错误类家族,也不再写出口映射函数;只有「把任意 provider 异常归类」这一种场景保留一个 `catch`,并在其中 `throw new RemoteError(code, messageOf(error), details, { cause: error })`。进程内仍需消费的既有异常类(`ApiSessionCwdConflict` 等)保留为不导出的私有类,在出口一行转成 `RemoteError`。
|
||||
|
||||
码是 `<语义域>/<理由>` 形式的字符串:`session/not-found`、`gateway/cancelled`、`workspace/invalid-path`、`agent-preset/locked`。前缀与 wire namespace 同风格,读者从码本身就能看出它属于谁,跨域转述时也不再需要一个别扭的无前缀名。
|
||||
|
||||
## Code ownership
|
||||
|
||||
一个码只有一个声明处,落点由「谁生产它」和「声明对谁可达」共同决定——声明合并只在增补文件进入当前 program 时生效,所以正家必须是每个生产者都能看见的包:
|
||||
|
||||
- **载体码**:`gateway/bad-request`、`gateway/cancelled`、`gateway/internal` 由 protocol 声明,人人可达。
|
||||
- **Gateway 装配码**:17 个 `gateway/*` 由 `packages/api/gateway/src/remote-error-codes.ts` 声明,details 统一为 `TypertGatewayFaultDetails { endpoint, field? }`;该模块 face-neutral,Host 与 Client 两面各自 import,因此两个 program 看到同一批条目。
|
||||
- **跨包共产**:两个及以上不同包抛同一个码时,声明落到双方都已依赖的最低层。`session/not-found` 落 `@deepseek-ai/dsh-session`(session-controller 与 workspace-controller 都依赖它),`workspace/not-found` 落 `@deepseek-ai/dsh-workspace`(session-controller 与 workspace-controller 之间没有依赖边,能力包是唯一共同下层)。
|
||||
- **单一生产者**:只有一个包抛的码落生产者包。`subagent/not-found` 与 `agent-preset/conflict` 因此落 session-controller——全仓只有它抛这两个码,subagent 与 agent-presets 的码表里都没有它们。
|
||||
|
||||
共享的是校验逻辑,不是码。`session/invalid-time-zone` 与 `subagent/invalid-time-zone` 是两个域各自声明、各自抛出的两个码,两个端点共用 `@deepseek-ai/dsh-util-time` 的 `canonicalClientTimeZone()` 做规范化;client 对这个码没有分支语义,拆码的成本是零,而合成一个码就会重新制造可达性问题。
|
||||
|
||||
## Discrimination by code
|
||||
|
||||
判别一律读 `code`,从不用 `instanceof`。Client 与 Host 是两个独立打包的 program,worker 传输还会把页面侧再分一次包,因此同一个类会存在多份副本,跨副本的原型链身份不成立。机制层用 protocol 的 `remoteErrorOf(value)` 读结构标记加一个字符串 `code`,Gateway client face 另外导出 `isRemoteFailure(error)` 供消费方在 catch 里判别;两者都只看这两个字段、不看类——连 `instanceof Error` 都不要求,因为另一个 realm 抛出的 Error 同样通不过它。
|
||||
|
||||
业务代码通常连这两个函数都不需要:`RemoteResult` 的 `ok: false` 分支已经是类型化的 `RemoteFailure`,`if (result.error.code === 'session/not-found')` 就把 `details` 窄化到该码的形状,无需 cast。需要向上抛的站点直接 `throw result.error`——它是真 `Error`,栈与 `message` 都成立。
|
||||
|
||||
client 面不构造 `RemoteError`:唯一例外是 Gateway 的 client face 本身,它在 `invoke()` 里按 wire 数据重建实例、在流边界把载体 throw 折进同一词汇。测试替身要构造失败值时从 `@deepseek-ai/dsh-client-test-runtime` 取 `RemoteError`,而不是让 client 包值引入 protocol。断言用 `toMatchObject` 判 code(必要时加 details 字段):`RemoteError` 是 `Error`,own key 集合与旧字面量不同,`toEqual` 会失败。
|
||||
|
||||
## Fixed Host facts
|
||||
|
||||
`ctx.remote.$host` 暴露两条固定事实:`home: string | undefined` 与 `isLoopback: boolean`。它是 Client Remote service 上的 getter,读的是 service 构造期取得的 connection 句柄——`home` 来自 generation 快照的 ready frame(ready 之前是 `undefined`),`isLoopback` 来自载体。没有 store、没有订阅、没有 generation 计数器。
|
||||
|
||||
重连后的刷新走既有信号:Client Remote 在连上时 emit `connection/reset`,需要重取的消费方监听它或各域自己的 remote event,而不是让 `$host` 变成一个可订阅对象。因此消费方不再注入 `connection`:`@deepseek-ai/dsh-client-connection` 的消费白名单收缩到 hmr、frontend-static、bundle/web-app、session-log-export、webworker-runtime、gateway 与 api-remotes 装配。
|
||||
|
||||
## What the wire carries
|
||||
|
||||
envelope 不变:wire 上仍是 `{ code, message, details }` 数据,`RemoteError` 是两端各自的进程内载体。Host 侧 `rpcFailure()` 收敛为两分支——结构识别出的 `RemoteError` 原样编码,其余折成 `gateway/internal`;载体信号取消也走同一词汇(`RemoteInvocationCancelled` 类整体删除,四个 throw 点改抛 `RemoteError('gateway/cancelled', …)`)。
|
||||
|
||||
三条 wire 可见行为随之确定。Gateway 的 17 个装配码按语义上 wire,client 因此能把「方法未挂载」与「业务拒绝」分开处理。owner 不预折无关异常:未归类的 throw 交给 Gateway 折一次 `gateway/internal`,诊断串保留在 `message` 里。client 一元调用被调用方 abort 时答 `gateway/cancelled`,即使本地 throw 抢在 wire 往返之前赢得竞争,也与 Host 会给出的码一致。
|
||||
|
||||
载体层只保留开放的 wire 形状。`@deepseek-ai/dsh-client-connection` 的 `ConnectionRpcFailure`/`ConnectionRpcResult` 不含任何域码知识,其 `transportError()` 产出 `gateway/internal`;typed 视图的正家从此只有 protocol 的 `RemoteFailure`。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**每域一套 `RemoteFault` 错误类家族。** 让每个域(或每个码)有自己的 `Error` 子类,看起来更 OO,但它把「码」这一条信息拆成了类身份加字段两处,跨 realm 又只能退回判字段——于是类身份成为纯粹的负担:每个域要维护子类、导出它、在文档里解释它,而消费方仍然只能判 code。单类加一张码表把这份重量换成了一行声明。
|
||||
|
||||
**在调用点加 `attempt` / `unwrap` / `remoteFailureOf` 包装函数。** 包装能让调用点少写一个 `if`,但它把 `RemoteResult` 这个 canonical 形状变成了「先过一层库函数」,两种风格会长期并存;`unwrap` 还会把「失败是正常结果」重新变成异常流,与 Remote 面不 reject 的契约背道而驰。被保留的 `remoteErrorOf` 只服务机制层与测试断言,业务代码拿到的要么是已类型化的 `result.error`、要么是自己抛的,不需要它。
|
||||
|
||||
**`host/updated` 事件加订阅式 `$host` store。** 订阅能在 Host home 变化时自动刷新,但 home 与 isLoopback 在一条连接内是固定事实,为它引入 store、generation 与订阅生命周期,等于让每个只想读一次的页面都承担一套状态管理。重连是已有信号(`connection/reset`),业务失效走各域 remote event,固定事实保持普通值读取。
|
||||
|
||||
**把不上 wire 的本地失败也纳入码表。** 例如 ui-goal 的 `no-current-goal`:它从不跨进程,纳入码表会让共享词汇混入只有一个 client 包关心的条目,还会误导读者以为它有 wire 语义。本地失败保持各自的本地类型,码表只描述 Remote 词汇。
|
||||
|
||||
## Consequences
|
||||
|
||||
新增一个域码是一处 declaration merging 加一个 throw:不再有映射函数、错误类、载体 typed 视图三处联动。代价是落点需要判断——正家必须对每个生产者可达,而这条判断只有在真的出现第二个生产者时才显现;`workspace/not-found` 就是这样从 workspace-controller 迁到能力包的,并为此给 `@deepseek-ai/dsh-workspace` 加了一条 type-only 的 protocol 依赖。
|
||||
|
||||
码字符串带前缀后,wire 字符串整体变化,connection fixture 内嵌的码、host 与 client 两侧断言、spec 本地 declare 一次性同步。发布前阶段接受这次一波切;发布后同样的改名需要一个兼容期。
|
||||
|
||||
`details` 的类型由码决定,因此码与 details 的搭配错误在编译期就被拒。反面是每个抛点都要给全 details 的必填字段:protocol 把 `gateway/bad-request` 的 `issues` 设为可选,正是为了让没有 codec issues 的业务校验点仍然只写 `{}`。
|
||||
|
||||
`RemoteError` 是 `Error`,所以它进任何日志与 `errorChain()` 都保留 `message` 与 `cause`;但 `cause` 只在进程内成立,wire 上只有 `code`、`message`、`details` 三个字段。跨 realm 的判别永远读结构标记,任何新增的传输(worker、bundle 分片)都必须把标记或等价的 marker 帧带过去,否则失败值会退化为普通 `Error`。
|
||||
|
||||
Remote 方法的消费端签名统一为 `Promise<RemoteResult<T>>`,与[方法调用面](2026-08-02-typert-remote-method-calls.zh.md)描述的生成投影一致;一元调用的迁移账本见[一元端点迁移](2026-08-10-unary-apiproxy-remote-migration.zh.md)。
|
||||
@@ -148,7 +148,7 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
sessionId: SessionId(await createSession('default-model-refusal')),
|
||||
mode: 'queue',
|
||||
content: [{ type: 'text', text: 'hi' }],
|
||||
}, new AbortController().signal)).rejects.toMatchObject({ failure: { code: 'model-unavailable' } })
|
||||
}, new AbortController().signal)).rejects.toMatchObject({ code: 'session/model-unavailable' })
|
||||
|
||||
// The way out stays open. Locking the model seat with everything else
|
||||
// would leave the composer asking for the one thing it prevents.
|
||||
|
||||
@@ -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/api-gateway.md
|
||||
api-gateway.md: 43b00bcff3da0adb1d53ac25534b65366bc42611
|
||||
api-gateway.zh.md: bb2711220fd75ef0119e1188c22a0af5ad4169a8
|
||||
api-gateway.md: 92d1bb5f0103fd4e2424992de9445f4768346f91
|
||||
api-gateway.zh.md: 3c3d95a5c125c592baf21579fb724101708b5cf8
|
||||
+1
-1
@@ -124,7 +124,7 @@ The Connection performs the unified trust check for `/api` before the HTTP bridg
|
||||
|
||||
For every call, the Gateway resolves the descriptor and live service from the current registries instead of caching business objects. It requires the fields in `args` to match the descriptor exactly, validates wire values with codecs, resolves objects or receivers through registered lookup or Context providers, invokes the service method targeted by the binding, and validates the return value. A missing provider, unknown identity, binding mismatch, missing or extra argument, schema failure, or missing method fails before entering or after leaving business code.
|
||||
|
||||
The lookup provider's `register()` supplies both the stable declaration and the default resolver; `configure()` supplies a resolver owned by Host composition that may execute asynchronously and is scoped to an effect lifetime. Configuration may precede provider mounting; without a provider, invocation still fails with `lookup-unavailable`, and unloading the configuration restores the provider's default policy. The Session Controller owns the standard `agentFor()` semantics for `agent` and `session`: it reuses a live Agent, automatically resumes ordinary cold sessions, deduplicates concurrent resumes, and rejects identities owned by subagent routing; the `session` lookup returns that Agent's Session. Resume failures and ownership fences pass through unchanged as existing RPC errors rather than being collapsed into the Gateway's `internal` error.
|
||||
The lookup provider's `register()` supplies both the stable declaration and the default resolver; `configure()` supplies a resolver owned by Host composition that may execute asynchronously and is scoped to an effect lifetime. Configuration may precede provider mounting; without a provider, invocation still fails with `gateway/lookup-unavailable`, and unloading the configuration restores the provider's default policy. The Session Controller owns the standard resolver semantics for `agent` and `session`: it reuses a live Agent, automatically resumes ordinary cold sessions, deduplicates concurrent resumes, and rejects identities owned by subagent routing; the `session` lookup returns that Agent's Session. A resume failure and an ownership fence raise a `RemoteError` carrying their own code, `session/not-found` or `session/agent-busy`, which the Gateway encodes onto the wire unchanged; only an unclassified throw folds into `gateway/internal`.
|
||||
|
||||
Unloading a Client contribution removes its descriptors and concrete methods together, aborts its in-flight calls, and makes stale method handles retained by external code reject further calls. A strict endpoint withdrawn on the Host also does not degrade to SRC inference, preventing a hot unload from silently weakening validation.
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ Connection 在 HTTP bridge 之前执行 `/api` 的统一信任检查,再在共
|
||||
|
||||
Gateway 每次调用都从当前注册表解析描述符和实时服务,不缓存业务对象。它要求 `args` 的字段集合与描述符完全一致,先用 codec 校验 wire 值,再通过注册的 lookup 或 Context 提供方解析对象或接收者,最后调用 binding 指向的服务方法并校验返回值。缺少提供方、identity 未命中、binding 不一致、参数缺失或多余、schema 失败和方法不存在都会在进入业务代码前或离开业务代码后失败。
|
||||
|
||||
lookup 提供方的 `register()` 同时提供稳定声明和默认 resolver;`configure()` 提供由 Host 组合拥有、可异步执行且受 effect 生命周期约束的 resolver。配置可以先于提供方挂载;没有提供方时调用仍以 `lookup-unavailable` 失败,配置卸载后则恢复提供方默认策略。Session Controller 负责 `agent` 与 `session` 的标准 `agentFor()` 语义:复用 live Agent,自动恢复普通冷会话,对并发恢复去重,并拒绝由 subagent routing 拥有的 identity;`session` lookup 返回该 Agent 的 Session。恢复失败和 ownership fence 通过既有 RPC error 原样返回,不折叠为 Gateway 的 `internal` 错误。
|
||||
lookup 提供方的 `register()` 同时提供稳定声明和默认 resolver;`configure()` 提供由 Host 组合拥有、可异步执行且受 effect 生命周期约束的 resolver。配置可以先于提供方挂载;没有提供方时调用仍以 `gateway/lookup-unavailable` 失败,配置卸载后则恢复提供方默认策略。Session Controller 负责 `agent` 与 `session` 的标准 resolver 语义:复用 live Agent,自动恢复普通冷会话,对并发恢复去重,并拒绝由 subagent routing 拥有的 identity;`session` lookup 返回该 Agent 的 Session。恢复失败与 ownership fence 抛出携带自有码的 `RemoteError`(`session/not-found` 或 `session/agent-busy`),Gateway 原样编码上 wire;只有未归类的 throw 才折成 `gateway/internal`。
|
||||
|
||||
Client 卸载一个贡献时会一起移除描述符和具体方法,中止其进行中的调用,并使外部仍持有的陈旧方法句柄拒绝继续调用。Host 上已经注册过的严格 endpoint 被撤回后也不会降级到 SRC 推断,以免热卸载悄然降低校验强度。
|
||||
|
||||
|
||||
@@ -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/config-catalog.md
|
||||
config-catalog.md: 3c3699228bf5d4c0f49f8134d7c342f452de92c9
|
||||
config-catalog.zh.md: 0d96acf23a302d7365e6ebdb4aa3fe7cba4af400
|
||||
config-catalog.md: a48163a2ea9b04c99e2322620e207cc0a5aa3b88
|
||||
config-catalog.zh.md: bb662eecf45d72eb605a93663e9177cab10fcc34
|
||||
@@ -157,7 +157,7 @@ export interface PresetRoot {
|
||||
export type PresetTrust = 'system' | 'user'
|
||||
```
|
||||
|
||||
Source: [`packages/preset/agent-presets/src/preset.ts:54`](../packages/preset/agent-presets/src/preset.ts)
|
||||
Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts)
|
||||
|
||||
<a id="deepseek-aidsh-agent-spine-demo"></a>
|
||||
|
||||
@@ -290,7 +290,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/api/gateway/src/index.ts:117`](../packages/api/gateway/src/index.ts)
|
||||
Source: [`packages/api/gateway/src/index.ts:118`](../packages/api/gateway/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-api-session-controller"></a>
|
||||
|
||||
@@ -322,7 +322,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/api/settings-controller/src/index.ts:41`](../packages/api/settings-controller/src/index.ts)
|
||||
Source: [`packages/api/settings-controller/src/index.ts:37`](../packages/api/settings-controller/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-attachment-local"></a>
|
||||
|
||||
@@ -3549,5 +3549,6 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
|
||||
- `@deepseek-ai/dsh-typert-protocol` ([`packages/typert/protocol/src/index.ts`](../packages/typert/protocol/src/index.ts))
|
||||
- `@deepseek-ai/dsh-typert-registry` ([`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts))
|
||||
- `@deepseek-ai/dsh-util-crypto` ([`packages/util/crypto/src/index.ts`](../packages/util/crypto/src/index.ts))
|
||||
- `@deepseek-ai/dsh-util-time` ([`packages/util/time/src/index.ts`](../packages/util/time/src/index.ts))
|
||||
- `@deepseek-ai/dsh-util-workspace-path` ([`packages/util/workspace-path/src/index.ts`](../packages/util/workspace-path/src/index.ts))
|
||||
- `@deepseek-ai/dsh-win32-process` ([`packages/subprocess/win32-process/src/index.ts`](../packages/subprocess/win32-process/src/index.ts))
|
||||
@@ -159,7 +159,7 @@ export interface PresetRoot {
|
||||
export type PresetTrust = 'system' | 'user'
|
||||
```
|
||||
|
||||
来源:[`packages/preset/agent-presets/src/preset.ts:54`](../packages/preset/agent-presets/src/preset.ts)
|
||||
来源:[`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts)
|
||||
|
||||
<a id="deepseek-aidsh-agent-spine-demo"></a>
|
||||
|
||||
@@ -292,7 +292,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/api/gateway/src/index.ts:117`](../packages/api/gateway/src/index.ts)
|
||||
来源:[`packages/api/gateway/src/index.ts:118`](../packages/api/gateway/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-api-session-controller"></a>
|
||||
|
||||
@@ -324,7 +324,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/api/settings-controller/src/index.ts:41`](../packages/api/settings-controller/src/index.ts)
|
||||
来源:[`packages/api/settings-controller/src/index.ts:37`](../packages/api/settings-controller/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-attachment-local"></a>
|
||||
|
||||
@@ -3550,5 +3550,6 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-typert-protocol`([`packages/typert/protocol/src/index.ts`](../packages/typert/protocol/src/index.ts))
|
||||
- `@deepseek-ai/dsh-typert-registry`([`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts))
|
||||
- `@deepseek-ai/dsh-util-crypto`([`packages/util/crypto/src/index.ts`](../packages/util/crypto/src/index.ts))
|
||||
- `@deepseek-ai/dsh-util-time`([`packages/util/time/src/index.ts`](../packages/util/time/src/index.ts))
|
||||
- `@deepseek-ai/dsh-util-workspace-path`([`packages/util/workspace-path/src/index.ts`](../packages/util/workspace-path/src/index.ts))
|
||||
- `@deepseek-ai/dsh-win32-process`([`packages/subprocess/win32-process/src/index.ts`](../packages/subprocess/win32-process/src/index.ts))
|
||||
@@ -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/cookbook/adding-a-remote-api.md
|
||||
adding-a-remote-api.md: 0f89101e9bc3ff961d465c109f033150e93e04f6
|
||||
adding-a-remote-api.zh.md: 0c5a57b539ecd98cb88790260a3fb8e7b41afff6
|
||||
@@ -0,0 +1,197 @@
|
||||
# Cookbook: adding a Remote API
|
||||
|
||||
English | [中文](adding-a-remote-api.zh.md)
|
||||
|
||||
Adding or changing a `ctx.remote` endpoint takes the five steps on this page: declare the method, declare its failures, register it on the package, consume it on the Client, and test it. Decorator semantics, lookup resolution, the generation pipeline, and the `/api` route are the mechanism and belong to the [API Gateway reference](../api-gateway.md); this page gives the action for each step and the conventions it must satisfy. Why the programming interface looks like this is in the [Typert Remote method calls Agent Note](../../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md), and why a failure is one `RemoteError` plus a code table is in the [failure vocabulary Agent Note](../../.agents/notes/implemented/architecture/2026-08-28-ctx-remote-failure-vocabulary.md).
|
||||
|
||||
## 1. Declare the API
|
||||
|
||||
The owner is a Host-side Cordis service: extend `TypertRemoteService` so the service key and the wire namespace are bound together, then mark the exposed methods with `@Remote`. Mark the business method itself when its signature already satisfies the wire conventions; write a `remoteExport*` adapter only when the shape has to change (adding `signal`, reordering parameters, exporting another name), and let that adapter call the unrenamed business method. Lookup objects (`Agent`, `Session`) may only occupy top-level parameter positions, and a method that supports cooperative cancellation takes `signal: AbortSignal` as its final parameter.
|
||||
|
||||
```ts
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
/** One stored note as a Client reads it. */
|
||||
export interface NoteRow {
|
||||
readonly noteId: string
|
||||
readonly title: string
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
notesController: NotesController
|
||||
}
|
||||
}
|
||||
|
||||
export class NotesController extends TypertRemoteService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'notesController', { namespace: 'notes' })
|
||||
}
|
||||
|
||||
/**
|
||||
* @param agent - lookup parameter the Gateway resolves from its wire identity.
|
||||
* @param signal - carrier cancellation, always the final parameter.
|
||||
* @returns the notes this Agent's session owns.
|
||||
*/
|
||||
@Remote('list')
|
||||
async remoteExportList(agent: Agent, signal: AbortSignal): Promise<NoteRow[]> {
|
||||
return await this.list(agent, signal)
|
||||
}
|
||||
|
||||
/** The in-process API the adapter above delegates to, unchanged by it. */
|
||||
async list(agent: Agent, signal: AbortSignal): Promise<NoteRow[]> {
|
||||
signal.throwIfAborted()
|
||||
return await Promise.resolve([{ noteId: `${agent.id}-1`, title: 'draft' }])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Declare the failures
|
||||
|
||||
A Remote failure is one class, `RemoteError`: merge the domain codes into `RemoteErrorDetailsMap` through declaration merging and `throw new RemoteError(code, message, details)` at the failure point. Do not build a family of domain error classes, and do not write an exit-mapping function; an exception unrelated to this endpoint is not pre-classified, because the Gateway folds it into `gateway/internal`. Write a `catch` only to classify an arbitrary provider exception as one domain code, and attach the original exception as `cause`.
|
||||
|
||||
A code reads `<domain>/<reason>`, and its declaration has four placement rules:
|
||||
|
||||
- One producer only: declare it in the producing package, next to the throw.
|
||||
- Several packages produce it: declare it in the lowest domain package both depend on (`session/not-found` in `core/session`, `workspace/not-found` in `dsh-workspace`).
|
||||
- The carrier codes `gateway/bad-request`, `gateway/cancelled`, and `gateway/internal` are declared in protocol, and the Gateway infrastructure codes in gateway — use them, never copy them.
|
||||
- A local failure that never crosses the wire stays out of the code table; express it with the caller's own type.
|
||||
|
||||
```ts
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
/** No stored note carries that id. */
|
||||
'note/not-found': { readonly noteId: string }
|
||||
/** The store refused an otherwise valid write. */
|
||||
'note/rejected': { readonly noteId: string }
|
||||
}
|
||||
}
|
||||
|
||||
declare const stored: ReadonlyMap<string, string>
|
||||
declare function persist(noteId: string, title: string): Promise<void>
|
||||
|
||||
export async function rename(noteId: string, title: string): Promise<void> {
|
||||
if (!stored.has(noteId)) {
|
||||
throw new RemoteError('note/not-found', `no note "${noteId}"`, { noteId })
|
||||
}
|
||||
try {
|
||||
await persist(noteId, title)
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
throw new RemoteError('note/rejected', message, { noteId }, { cause: error })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Register it on the package
|
||||
|
||||
`@Remote` must live in a Loader entry plugin package; when the owner is an abstract seam, the controller goes in the matching package under `packages/api/`. The manifest gains the two generated entries and the protocol peer dependency, while on the Client side the `@deepseek-ai/dsh-api-remotes` assembly mounts the contribution and re-exports the type vocabulary that consumers need. Which generated artifact each entry points at, and how the generation pipeline is ordered, are in the [API Gateway reference](../api-gateway.md).
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
"./typert": { "types": "./lib/typert.host.d.ts", "default": "./lib/typert.host.js" },
|
||||
"./remote": { "types": "./lib/typert.remote-client.d.ts", "default": "./lib/typert.remote-client.js" }
|
||||
},
|
||||
"peerDependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:^" },
|
||||
"devDependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:^" }
|
||||
}
|
||||
```
|
||||
|
||||
Rerun `pnpm run build:lib` after changing a signature, the code table, the namespace, or an export name, because that is what hands the Client its new declarations and codecs; changing only an implementation body needs no regeneration.
|
||||
|
||||
## 4. Consume it on the Client
|
||||
|
||||
The calling plugin declares both `remote` and `remote.<namespace>` in its `inject`, and the call site writes `ctx.remote.<namespace>.<method>(...)` directly: no `Pick<ClientRemote, …>` narrowing, no hand-written method signature, no wire relay object. The result is a `RemoteResult<T>`, so branch on `if (!result.ok)` in place and discriminate by `code` rather than `instanceof` — a code branch narrows `details` on its own. An exception-flow site writes `throw result.error` (it is a real Error); whoever catches it uses `isRemoteFailure` to tell a Remote failure from a local defect and rethrows the defect. Do not write a defensive catch: a Remote call does not reject, and an assembly mistake should crash.
|
||||
|
||||
Fixed Host facts come from `ctx.remote.$host`: `home` and `isLoopback` are plain reads with no subscription and no generation counter, and `home` is `undefined` until the first ready frame. Refresh after a reconnect through `ctx.on('connection/reset')` or a domain's own remote event. When the caller aborts a unary call, the outcome is `gateway/cancelled` on the error branch rather than a throw.
|
||||
|
||||
```ts ignore-check
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
|
||||
export const inject = ['remote', 'remote.notes']
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
/** Store-side read: the error branch is handled where the code is meaningful. */
|
||||
export async function noteTitles(): Promise<readonly string[]> {
|
||||
const result = await ctx.remote.notes.list()
|
||||
if (!result.ok) {
|
||||
if (result.error.code === 'note/not-found') return []
|
||||
throw result.error
|
||||
}
|
||||
return result.value.map(row => row.title)
|
||||
}
|
||||
|
||||
/** Action-side: a Remote failure becomes copy; a local fault keeps crashing. */
|
||||
export async function renderTitles(): Promise<string> {
|
||||
try {
|
||||
return (await noteTitles()).join(', ')
|
||||
} catch (error: unknown) {
|
||||
if (!isRemoteFailure(error)) throw error
|
||||
return `unavailable (${error.code})`
|
||||
}
|
||||
}
|
||||
|
||||
/** Fixed Host facts as plain reads. */
|
||||
export function hostLabel(): string {
|
||||
const { home, isLoopback } = ctx.remote.$host
|
||||
return home ?? (isLoopback ? 'local host' : 'remote host')
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Test it
|
||||
|
||||
On the owner side, assert the code that was thrown: recover the failure with `remoteErrorOf` after catching, then compare `code` and the details fields you care about with `toMatchObject` — never deep-compare the error object with `toEqual`, and never assert `instanceof`.
|
||||
|
||||
```ts
|
||||
import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
declare function rename(noteId: string, title: string): Promise<void>
|
||||
|
||||
it('refuses an unknown note before writing', async () => {
|
||||
const failure = await rename('n-404', 'fresh title').catch((error: unknown) => error)
|
||||
|
||||
expect(remoteErrorOf(failure)).toMatchObject({
|
||||
code: 'note/not-found',
|
||||
details: { noteId: 'n-404' },
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
A Client-side double returns real instances: take the `RemoteError` and `TestRemote` value imports from `@deepseek-ai/dsh-client-test-runtime`, because a value import from the `api-remotes` facade would load the unbuilt assembly chain. `TestRemote.$host` is a plain field a spec assigns directly.
|
||||
|
||||
```ts ignore-check
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { RemoteError, TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
it('renders the failure code the Host reported', async () => {
|
||||
const ctx = new Context()
|
||||
const remote = new TestRemote(ctx, {
|
||||
notes: {
|
||||
list: () => Promise.resolve({
|
||||
ok: false as const,
|
||||
error: new RemoteError('note/not-found', 'no note "n-404"', { noteId: 'n-404' }),
|
||||
}),
|
||||
},
|
||||
})
|
||||
remote.$host = { home: '/home/fixture', isLoopback: true }
|
||||
|
||||
await expect(ctx.remote.notes.list()).resolves.toMatchObject({ error: { code: 'note/not-found' } })
|
||||
})
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
1. `pnpm run build:lib`: mandatory once a signature, the code table, the namespace, or an export name changed, because it produces the Client declarations and codecs.
|
||||
2. `pnpm run typecheck`: both the Host and the Client program, where a code merged into an unreachable package turns red.
|
||||
3. Run both sides' specs by name: `npx vitest run <owner spec> <client spec>`.
|
||||
4. Add a recorded-session snapshot when the endpoint reaches a product-visible surface, per the [testing policy](../testing.md).
|
||||
@@ -0,0 +1,197 @@
|
||||
# 实操手册:新增一个 Remote API
|
||||
|
||||
[English](adding-a-remote-api.md) | 中文
|
||||
|
||||
新增或改动一个 `ctx.remote` 端点按本页五步走:声明方法、声明失败、在包上注册、在 Client 消费、写测试。decorator 语义、lookup 解析、生成管线与 `/api` 路由属于机制,由 [API Gateway 参考](../api-gateway.zh.md)负责;本页给的是每一步的动作与必须遵守的约定。为什么是这套编程面,见 [Typert Remote 方法调用 Agent Note](../../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md);为什么失败面是单个 `RemoteError` 加一张码表,见[失败词汇 Agent Note](../../.agents/notes/implemented/architecture/2026-08-28-ctx-remote-failure-vocabulary.zh.md)。
|
||||
|
||||
## 1. 声明 API
|
||||
|
||||
owner 是一个 Host 侧 Cordis 服务:继承 `TypertRemoteService` 把 service 键与 wire namespace 一起绑定,再用 `@Remote` 标注对外暴露的方法。业务方法的签名若已符合 wire 约定就直接标注它本身;只有形态需要调整(补 `signal`、换参数顺序、换导出名)才写一个 `remoteExport*` adapter,由它调用不改名的业务方法。lookup 对象(`Agent`、`Session`)只能占顶层参数位,支持协作式取消的方法把 `signal: AbortSignal` 放在最后一位。
|
||||
|
||||
```ts
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
/** One stored note as a Client reads it. */
|
||||
export interface NoteRow {
|
||||
readonly noteId: string
|
||||
readonly title: string
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
notesController: NotesController
|
||||
}
|
||||
}
|
||||
|
||||
export class NotesController extends TypertRemoteService {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'notesController', { namespace: 'notes' })
|
||||
}
|
||||
|
||||
/**
|
||||
* @param agent - lookup parameter the Gateway resolves from its wire identity.
|
||||
* @param signal - carrier cancellation, always the final parameter.
|
||||
* @returns the notes this Agent's session owns.
|
||||
*/
|
||||
@Remote('list')
|
||||
async remoteExportList(agent: Agent, signal: AbortSignal): Promise<NoteRow[]> {
|
||||
return await this.list(agent, signal)
|
||||
}
|
||||
|
||||
/** The in-process API the adapter above delegates to, unchanged by it. */
|
||||
async list(agent: Agent, signal: AbortSignal): Promise<NoteRow[]> {
|
||||
signal.throwIfAborted()
|
||||
return await Promise.resolve([{ noteId: `${agent.id}-1`, title: 'draft' }])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 声明失败
|
||||
|
||||
Remote 失败只有一个类 `RemoteError`:域码经 declaration merging 进 `RemoteErrorDetailsMap`,失败点直接 `throw new RemoteError(code, message, details)`。不要建域异常类家族,也不要写出口映射函数;与本端点无关的异常不预先归类,Gateway 会兜底折成 `gateway/internal`。只有"把任意 provider 异常归为一个域码"这一种场景才写 `catch`,并把原始异常挂在 `cause` 上。
|
||||
|
||||
码名是 `<域>/<理由>`,声明落点四条:
|
||||
|
||||
- 只有一个生产者:声明落生产者包,紧挨抛出点。
|
||||
- 多个包共同生产:落双方共同依赖的最低层域包(`session/not-found` 在 `core/session`,`workspace/not-found` 在 `dsh-workspace`)。
|
||||
- 载体码 `gateway/bad-request`、`gateway/cancelled`、`gateway/internal` 已在 protocol 声明,Gateway 基础设施码已在 gateway 声明——直接用,不要复制。
|
||||
- 不上 wire 的本地失败不进码表,用调用方自己的类型表达。
|
||||
|
||||
```ts
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
/** No stored note carries that id. */
|
||||
'note/not-found': { readonly noteId: string }
|
||||
/** The store refused an otherwise valid write. */
|
||||
'note/rejected': { readonly noteId: string }
|
||||
}
|
||||
}
|
||||
|
||||
declare const stored: ReadonlyMap<string, string>
|
||||
declare function persist(noteId: string, title: string): Promise<void>
|
||||
|
||||
export async function rename(noteId: string, title: string): Promise<void> {
|
||||
if (!stored.has(noteId)) {
|
||||
throw new RemoteError('note/not-found', `no note "${noteId}"`, { noteId })
|
||||
}
|
||||
try {
|
||||
await persist(noteId, title)
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
throw new RemoteError('note/rejected', message, { noteId }, { cause: error })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 在包上注册
|
||||
|
||||
`@Remote` 必须落在一个 Loader entry 插件包里;owner 是抽象 seam 时把控制器放进 `packages/api/` 下的对应包。包清单要补两个生成入口与 protocol 的 peer 依赖,Client 侧则由 `@deepseek-ai/dsh-api-remotes` 的 assembly 挂载该贡献并按需转口类型词汇。两个入口分别指向哪个生成产物、生成管线如何排序,见 [API Gateway 参考](../api-gateway.zh.md)。
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
"./typert": { "types": "./lib/typert.host.d.ts", "default": "./lib/typert.host.js" },
|
||||
"./remote": { "types": "./lib/typert.remote-client.d.ts", "default": "./lib/typert.remote-client.js" }
|
||||
},
|
||||
"peerDependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:^" },
|
||||
"devDependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:^" }
|
||||
}
|
||||
```
|
||||
|
||||
改动了签名、码表、namespace 或导出名之后重跑 `pnpm run build:lib`,Client 才拿得到新的声明与 codec;只改实现体不需要重新生成。
|
||||
|
||||
## 4. 在 Client 消费
|
||||
|
||||
调用插件在 `inject` 里同时声明 `remote` 与 `remote.<namespace>`,调用点直写 `ctx.remote.<namespace>.<method>(...)`:不要用 `Pick<ClientRemote, …>` 窄化、不要手写方法签名、不要造 wire 中转对象。结果是 `RemoteResult<T>`,就地 `if (!result.ok)` 分支,判 `code` 而不是 `instanceof`——code 分支会自动窄化 `details`。异常流的站点写 `throw result.error`(它是真 Error);接住它的上层用 `isRemoteFailure` 区分 Remote 失败与本地缺陷,本地缺陷继续往上抛。不要写防御性 catch:Remote 调用不 reject,装配错误就该炸。
|
||||
|
||||
Host 的固定事实读 `ctx.remote.$host`:`home` 与 `isLoopback` 是普通值读取,没有订阅也没有 generation 计数器,`home` 在第一帧 ready 之前是 `undefined`;重连后的刷新走 `ctx.on('connection/reset')` 或各域自己的 remote 事件。调用方 abort 掉一次一元调用时,结果落在错误分支上的 `gateway/cancelled`,而不是抛出。
|
||||
|
||||
```ts ignore-check
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
|
||||
export const inject = ['remote', 'remote.notes']
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
/** Store-side read: the error branch is handled where the code is meaningful. */
|
||||
export async function noteTitles(): Promise<readonly string[]> {
|
||||
const result = await ctx.remote.notes.list()
|
||||
if (!result.ok) {
|
||||
if (result.error.code === 'note/not-found') return []
|
||||
throw result.error
|
||||
}
|
||||
return result.value.map(row => row.title)
|
||||
}
|
||||
|
||||
/** Action-side: a Remote failure becomes copy; a local fault keeps crashing. */
|
||||
export async function renderTitles(): Promise<string> {
|
||||
try {
|
||||
return (await noteTitles()).join(', ')
|
||||
} catch (error: unknown) {
|
||||
if (!isRemoteFailure(error)) throw error
|
||||
return `unavailable (${error.code})`
|
||||
}
|
||||
}
|
||||
|
||||
/** Fixed Host facts as plain reads. */
|
||||
export function hostLabel(): string {
|
||||
const { home, isLoopback } = ctx.remote.$host
|
||||
return home ?? (isLoopback ? 'local host' : 'remote host')
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 测试
|
||||
|
||||
owner 侧断言抛出的码:捕获后用 `remoteErrorOf` 取出失败,再用 `toMatchObject` 比对 `code` 与需要的 `details` 字段——不要用 `toEqual` 深比对错误对象,也不要断言 `instanceof`。
|
||||
|
||||
```ts
|
||||
import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
declare function rename(noteId: string, title: string): Promise<void>
|
||||
|
||||
it('refuses an unknown note before writing', async () => {
|
||||
const failure = await rename('n-404', 'fresh title').catch((error: unknown) => error)
|
||||
|
||||
expect(remoteErrorOf(failure)).toMatchObject({
|
||||
code: 'note/not-found',
|
||||
details: { noteId: 'n-404' },
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Client 侧的替身返回真实例:`RemoteError` 与 `TestRemote` 的值 import 一律取自 `@deepseek-ai/dsh-client-test-runtime`,因为从 `api-remotes` facade 值 import 会拉起尚未构建的装配链。`TestRemote.$host` 是普通字段,spec 直接赋值即可。
|
||||
|
||||
```ts ignore-check
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { RemoteError, TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
it('renders the failure code the Host reported', async () => {
|
||||
const ctx = new Context()
|
||||
const remote = new TestRemote(ctx, {
|
||||
notes: {
|
||||
list: () => Promise.resolve({
|
||||
ok: false as const,
|
||||
error: new RemoteError('note/not-found', 'no note "n-404"', { noteId: 'n-404' }),
|
||||
}),
|
||||
},
|
||||
})
|
||||
remote.$host = { home: '/home/fixture', isLoopback: true }
|
||||
|
||||
await expect(ctx.remote.notes.list()).resolves.toMatchObject({ error: { code: 'note/not-found' } })
|
||||
})
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
1. `pnpm run build:lib`:签名、码表、namespace 或导出名变过就必须重跑,Client 声明与 codec 由它产出。
|
||||
2. `pnpm run typecheck`:Host 与 Client 两个 program 都过一遍,码表的 merge 落点错了会在这里红。
|
||||
3. 点名跑两侧 spec:`npx vitest run <owner spec> <client spec>`。
|
||||
4. 端点属于产品可见面时补一条录制会话快照,规则见[测试策略](../testing.zh.md)。
|
||||
@@ -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/event-producer-consumer.md
|
||||
event-producer-consumer.md: 24ed4b92daae6d06a862618e398605fce7063f4f
|
||||
event-producer-consumer.zh.md: af440aa4155b65a211b7fdc9bce232a264bbe3a7
|
||||
event-producer-consumer.md: 53ed72ca93702ad06d63d3fb3b80ab6aba05baaa
|
||||
event-producer-consumer.zh.md: 8f4b12a8e7dbea96b735320997a5a0a2a4a8a545
|
||||
@@ -8,7 +8,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:92`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:80`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:166`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:175`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:297`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
@@ -21,11 +21,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:537`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:517`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:544`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:523`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:530`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:521`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:501`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:528`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:507`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:514`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
| 事件 | 模式 | 声明位置 | 派发方 | 监听方 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:92`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:80`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:166`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:175`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:297`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
@@ -23,11 +23,11 @@
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:537`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:517`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:544`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:523`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:530`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:521`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:501`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:528`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:507`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:514`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
|
||||
|
||||
@@ -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/module-graph.md
|
||||
module-graph.md: 90c0f2061d27fa6410dd867edb94089440fab006
|
||||
module-graph.zh.md: fb3d7c7f6a22b9ff2f6b41ed9745c050aa483759
|
||||
module-graph.md: 5c8d6a4f4c9eff9f2ddee548ecd9003e7a5eda8b
|
||||
module-graph.zh.md: e507a11e6e3e1c220c7947a76cda16645e79512f
|
||||
+16
-12
@@ -16,6 +16,7 @@ flowchart TD
|
||||
pkg_output_retention["output-retention"]
|
||||
pkg_timeout["timeout"]
|
||||
pkg_util_crypto["util-crypto"]
|
||||
pkg_util_time["util-time"]
|
||||
pkg_util_workspace_path["util-workspace-path"]
|
||||
end
|
||||
subgraph group_llm["packages/llm"]
|
||||
@@ -365,6 +366,7 @@ flowchart TD
|
||||
pkg_output_retention --> pkg_invariants
|
||||
pkg_timeout --> pkg_invariants
|
||||
pkg_util_crypto --> pkg_invariants
|
||||
pkg_util_time --> pkg_invariants
|
||||
pkg_util_workspace_path --> pkg_invariants
|
||||
pkg_deepseek_llm_api_extensions --> pkg_invariants
|
||||
pkg_scope --> pkg_invariants
|
||||
@@ -556,6 +558,7 @@ flowchart TD
|
||||
pkg_workspace --> pkg_session_persistence
|
||||
pkg_workspace --> pkg_storage
|
||||
pkg_workspace --> pkg_storage_domain
|
||||
pkg_workspace --> pkg_typert_protocol
|
||||
pkg_llm_deepseek --> pkg_anonymous_user_id
|
||||
pkg_llm_deepseek --> pkg_atomic_write
|
||||
pkg_llm_deepseek --> pkg_attachment
|
||||
@@ -1087,6 +1090,7 @@ flowchart TD
|
||||
pkg_webhook --> pkg_workspace
|
||||
pkg_subagent --> pkg_agent
|
||||
pkg_subagent --> pkg_agent_presets
|
||||
pkg_subagent --> pkg_attachment
|
||||
pkg_subagent --> pkg_brand
|
||||
pkg_subagent --> pkg_invariants
|
||||
pkg_subagent --> pkg_jobs
|
||||
@@ -1103,6 +1107,7 @@ flowchart TD
|
||||
pkg_subagent --> pkg_tools
|
||||
pkg_subagent --> pkg_typert_protocol
|
||||
pkg_subagent --> pkg_user_approval
|
||||
pkg_subagent --> pkg_util_time
|
||||
pkg_session_query_sqlite --> pkg_invariants
|
||||
pkg_session_query_sqlite --> pkg_session
|
||||
pkg_session_query_sqlite --> pkg_session_persistence
|
||||
@@ -1253,6 +1258,7 @@ flowchart TD
|
||||
pkg_api_session_controller --> pkg_subagent
|
||||
pkg_api_session_controller --> pkg_typert_protocol
|
||||
pkg_api_session_controller --> pkg_typert_registry
|
||||
pkg_api_session_controller --> pkg_util_time
|
||||
pkg_api_session_controller --> pkg_util_workspace_path
|
||||
pkg_api_session_controller --> pkg_workspace
|
||||
pkg_api_workspace_controller --> pkg_api_gateway
|
||||
@@ -1350,7 +1356,6 @@ flowchart TD
|
||||
pkg_subagent_dsh_sdk --> pkg_subagent
|
||||
pkg_subagent_dsh_sdk --> pkg_subprocess
|
||||
pkg_client_ui_settings --> pkg_api_remotes
|
||||
pkg_client_ui_settings --> pkg_client_connection
|
||||
pkg_client_ui_settings --> pkg_invariants
|
||||
pkg_client_ui_settings --> pkg_settings
|
||||
pkg_client_locale --> pkg_api_remotes
|
||||
@@ -1370,7 +1375,6 @@ flowchart TD
|
||||
pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_settings
|
||||
pkg_client_ui_settings_plugin_inventory --> pkg_invariants
|
||||
pkg_client_ui_settings_plugins --> pkg_api_remotes
|
||||
pkg_client_ui_settings_plugins --> pkg_client_connection
|
||||
pkg_client_ui_settings_plugins --> pkg_client_locale
|
||||
pkg_client_ui_settings_plugins --> pkg_client_ui_renderer
|
||||
pkg_client_ui_settings_plugins --> pkg_client_ui_settings
|
||||
@@ -1389,7 +1393,6 @@ flowchart TD
|
||||
pkg_client_ui_layout --> pkg_client_ui_theme
|
||||
pkg_client_ui_layout --> pkg_invariants
|
||||
pkg_cordis_client_runner --> pkg_api_remotes
|
||||
pkg_cordis_client_runner --> pkg_client_connection
|
||||
pkg_cordis_client_runner --> pkg_client_modules
|
||||
pkg_cordis_client_runner --> pkg_client_ui_renderer
|
||||
pkg_cordis_client_runner --> pkg_client_ui_theme
|
||||
@@ -1503,7 +1506,6 @@ flowchart TD
|
||||
pkg_client_ui_schedule --> pkg_invariants
|
||||
pkg_client_ui_schedule --> pkg_schedule
|
||||
pkg_client_ui_settings_general --> pkg_api_remotes
|
||||
pkg_client_ui_settings_general --> pkg_client_connection
|
||||
pkg_client_ui_settings_general --> pkg_client_locale
|
||||
pkg_client_ui_settings_general --> pkg_client_ui_renderer
|
||||
pkg_client_ui_settings_general --> pkg_client_ui_session
|
||||
@@ -1711,6 +1713,7 @@ flowchart TD
|
||||
pkg_client_test_runtime --> pkg_invariants
|
||||
pkg_client_test_runtime --> pkg_session
|
||||
pkg_client_test_runtime --> pkg_subagent
|
||||
pkg_client_test_runtime --> pkg_typert_protocol
|
||||
pkg_client_ui_skill --> pkg_api_remotes
|
||||
pkg_client_ui_skill --> pkg_api_session_controller
|
||||
pkg_client_ui_skill --> pkg_client_connection
|
||||
@@ -1743,6 +1746,7 @@ flowchart TD
|
||||
| [`output-retention`](../packages/util/output-retention) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`util-crypto`](../packages/util/crypto) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`util-time`](../packages/util/time) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`util-workspace-path`](../packages/util/workspace-path) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | `llm` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
@@ -1821,7 +1825,7 @@ flowchart TD
|
||||
| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) |
|
||||
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
|
||||
@@ -1909,7 +1913,7 @@ flowchart TD
|
||||
| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||
@@ -1927,7 +1931,7 @@ flowchart TD
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
@@ -1941,14 +1945,14 @@ flowchart TD
|
||||
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`util-crypto`](../packages/util/crypto), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`api-workspace-controller`](../packages/api/workspace-controller), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`schedule`](../packages/schedule/schedule), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
@@ -1961,7 +1965,7 @@ flowchart TD
|
||||
| [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session) |
|
||||
| [`client-ui-schedule`](../packages/client/ui-schedule) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`schedule`](../packages/schedule/schedule) |
|
||||
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
|
||||
| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
@@ -1979,6 +1983,6 @@ flowchart TD
|
||||
| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
|
||||
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-tool`](../packages/client/ui-tool), [`cordis-client-runner`](../packages/extensions/cordis-client-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
+16
-12
@@ -18,6 +18,7 @@ flowchart TD
|
||||
pkg_output_retention["output-retention"]
|
||||
pkg_timeout["timeout"]
|
||||
pkg_util_crypto["util-crypto"]
|
||||
pkg_util_time["util-time"]
|
||||
pkg_util_workspace_path["util-workspace-path"]
|
||||
end
|
||||
subgraph group_llm["packages/llm"]
|
||||
@@ -367,6 +368,7 @@ flowchart TD
|
||||
pkg_output_retention --> pkg_invariants
|
||||
pkg_timeout --> pkg_invariants
|
||||
pkg_util_crypto --> pkg_invariants
|
||||
pkg_util_time --> pkg_invariants
|
||||
pkg_util_workspace_path --> pkg_invariants
|
||||
pkg_deepseek_llm_api_extensions --> pkg_invariants
|
||||
pkg_scope --> pkg_invariants
|
||||
@@ -558,6 +560,7 @@ flowchart TD
|
||||
pkg_workspace --> pkg_session_persistence
|
||||
pkg_workspace --> pkg_storage
|
||||
pkg_workspace --> pkg_storage_domain
|
||||
pkg_workspace --> pkg_typert_protocol
|
||||
pkg_llm_deepseek --> pkg_anonymous_user_id
|
||||
pkg_llm_deepseek --> pkg_atomic_write
|
||||
pkg_llm_deepseek --> pkg_attachment
|
||||
@@ -1089,6 +1092,7 @@ flowchart TD
|
||||
pkg_webhook --> pkg_workspace
|
||||
pkg_subagent --> pkg_agent
|
||||
pkg_subagent --> pkg_agent_presets
|
||||
pkg_subagent --> pkg_attachment
|
||||
pkg_subagent --> pkg_brand
|
||||
pkg_subagent --> pkg_invariants
|
||||
pkg_subagent --> pkg_jobs
|
||||
@@ -1105,6 +1109,7 @@ flowchart TD
|
||||
pkg_subagent --> pkg_tools
|
||||
pkg_subagent --> pkg_typert_protocol
|
||||
pkg_subagent --> pkg_user_approval
|
||||
pkg_subagent --> pkg_util_time
|
||||
pkg_session_query_sqlite --> pkg_invariants
|
||||
pkg_session_query_sqlite --> pkg_session
|
||||
pkg_session_query_sqlite --> pkg_session_persistence
|
||||
@@ -1255,6 +1260,7 @@ flowchart TD
|
||||
pkg_api_session_controller --> pkg_subagent
|
||||
pkg_api_session_controller --> pkg_typert_protocol
|
||||
pkg_api_session_controller --> pkg_typert_registry
|
||||
pkg_api_session_controller --> pkg_util_time
|
||||
pkg_api_session_controller --> pkg_util_workspace_path
|
||||
pkg_api_session_controller --> pkg_workspace
|
||||
pkg_api_workspace_controller --> pkg_api_gateway
|
||||
@@ -1352,7 +1358,6 @@ flowchart TD
|
||||
pkg_subagent_dsh_sdk --> pkg_subagent
|
||||
pkg_subagent_dsh_sdk --> pkg_subprocess
|
||||
pkg_client_ui_settings --> pkg_api_remotes
|
||||
pkg_client_ui_settings --> pkg_client_connection
|
||||
pkg_client_ui_settings --> pkg_invariants
|
||||
pkg_client_ui_settings --> pkg_settings
|
||||
pkg_client_locale --> pkg_api_remotes
|
||||
@@ -1372,7 +1377,6 @@ flowchart TD
|
||||
pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_settings
|
||||
pkg_client_ui_settings_plugin_inventory --> pkg_invariants
|
||||
pkg_client_ui_settings_plugins --> pkg_api_remotes
|
||||
pkg_client_ui_settings_plugins --> pkg_client_connection
|
||||
pkg_client_ui_settings_plugins --> pkg_client_locale
|
||||
pkg_client_ui_settings_plugins --> pkg_client_ui_renderer
|
||||
pkg_client_ui_settings_plugins --> pkg_client_ui_settings
|
||||
@@ -1391,7 +1395,6 @@ flowchart TD
|
||||
pkg_client_ui_layout --> pkg_client_ui_theme
|
||||
pkg_client_ui_layout --> pkg_invariants
|
||||
pkg_cordis_client_runner --> pkg_api_remotes
|
||||
pkg_cordis_client_runner --> pkg_client_connection
|
||||
pkg_cordis_client_runner --> pkg_client_modules
|
||||
pkg_cordis_client_runner --> pkg_client_ui_renderer
|
||||
pkg_cordis_client_runner --> pkg_client_ui_theme
|
||||
@@ -1505,7 +1508,6 @@ flowchart TD
|
||||
pkg_client_ui_schedule --> pkg_invariants
|
||||
pkg_client_ui_schedule --> pkg_schedule
|
||||
pkg_client_ui_settings_general --> pkg_api_remotes
|
||||
pkg_client_ui_settings_general --> pkg_client_connection
|
||||
pkg_client_ui_settings_general --> pkg_client_locale
|
||||
pkg_client_ui_settings_general --> pkg_client_ui_renderer
|
||||
pkg_client_ui_settings_general --> pkg_client_ui_session
|
||||
@@ -1713,6 +1715,7 @@ flowchart TD
|
||||
pkg_client_test_runtime --> pkg_invariants
|
||||
pkg_client_test_runtime --> pkg_session
|
||||
pkg_client_test_runtime --> pkg_subagent
|
||||
pkg_client_test_runtime --> pkg_typert_protocol
|
||||
pkg_client_ui_skill --> pkg_api_remotes
|
||||
pkg_client_ui_skill --> pkg_api_session_controller
|
||||
pkg_client_ui_skill --> pkg_client_connection
|
||||
@@ -1745,6 +1748,7 @@ flowchart TD
|
||||
| [`output-retention`](../packages/util/output-retention) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`util-crypto`](../packages/util/crypto) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`util-time`](../packages/util/time) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`util-workspace-path`](../packages/util/workspace-path) | `util` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | `llm` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
@@ -1823,7 +1827,7 @@ flowchart TD
|
||||
| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) |
|
||||
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
|
||||
@@ -1911,7 +1915,7 @@ flowchart TD
|
||||
| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||
@@ -1929,7 +1933,7 @@ flowchart TD
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
@@ -1943,14 +1947,14 @@ flowchart TD
|
||||
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`util-crypto`](../packages/util/crypto), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`api-workspace-controller`](../packages/api/workspace-controller), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`schedule`](../packages/schedule/schedule), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
@@ -1963,7 +1967,7 @@ flowchart TD
|
||||
| [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session) |
|
||||
| [`client-ui-schedule`](../packages/client/ui-schedule) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`schedule`](../packages/schedule/schedule) |
|
||||
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
|
||||
| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
@@ -1981,6 +1985,6 @@ flowchart TD
|
||||
| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
|
||||
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-tool`](../packages/client/ui-tool), [`cordis-client-runner`](../packages/extensions/cordis-client-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
@@ -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/subsystems/core.md
|
||||
core.md: dbe12dfc9bfdafe0f59d7e52eefc5695b1c8d063
|
||||
core.zh.md: 50fd2e4e76ee72653c02c2ffeca06042191aa9c4
|
||||
core.md: 2f81b9d3456edae88a4f777b376bc5394c32a14e
|
||||
core.zh.md: e4afda0c3c57acab49dbcfe3f8ce8cb3f08c3d23
|
||||
@@ -488,8 +488,8 @@ async read(id: string): Promise<string>
|
||||
* One preset's composition text with the roster row it belongs to.
|
||||
* @param agentPreset - the preset id.
|
||||
* @returns the composition beside its trust and published metadata.
|
||||
* @throws {TypertRemoteFailure} `bad-request` for an empty id, or
|
||||
* `agent-preset-not-found` when no configured root supplies it.
|
||||
* @throws {RemoteError} `gateway/bad-request` for an empty id, or
|
||||
* `agent-preset/not-found` when no configured root supplies it.
|
||||
*/
|
||||
@Remote('read') async readDocument(agentPreset: string): Promise<AgentPresetDocument>
|
||||
|
||||
@@ -516,8 +516,8 @@ async copy(from: string, id: string, name?: string): Promise<void>
|
||||
* @param id - the new preset id.
|
||||
* @param name - the copy's optional display name.
|
||||
* @returns once the copy is stored.
|
||||
* @throws {TypertRemoteFailure} with the corresponding stable preset code
|
||||
* and details when the copy is refused.
|
||||
* @throws {RemoteError} with the corresponding stable preset code and
|
||||
* details when the copy is refused.
|
||||
*/
|
||||
@Remote('copy') async remoteExportCopy(from: string, id: string, name?: string): Promise<void>
|
||||
|
||||
@@ -533,8 +533,8 @@ async remove(id: string): Promise<void>
|
||||
* Delete one preset through the Remote API.
|
||||
* @param id - the preset id.
|
||||
* @returns once the preset is deleted.
|
||||
* @throws {TypertRemoteFailure} with the corresponding stable preset code
|
||||
* and details when deletion is refused.
|
||||
* @throws {RemoteError} with the corresponding stable preset code and
|
||||
* details when deletion is refused.
|
||||
*/
|
||||
@Remote('deletePreset') async remoteExportDelete(id: string): Promise<void>
|
||||
|
||||
@@ -585,8 +585,8 @@ async recompose(agentCtx: Context, id: string): Promise<AgentPreset>
|
||||
* @param agent - the session's live agent, resolved from the wire identity.
|
||||
* @param agentPreset - the preset to compose the agent from instead.
|
||||
* @returns the preset id that was recorded.
|
||||
* @throws {TypertRemoteFailure} with `bad-request`, `agent-preset-locked`,
|
||||
* `agent-preset-not-found`, or `agent-preset-invalid` when refused.
|
||||
* @throws {RemoteError} with `gateway/bad-request`, `agent-preset/locked`,
|
||||
* `agent-preset/not-found`, or `agent-preset/invalid` when refused.
|
||||
*/
|
||||
@Remote('select') async select(agent: Agent, agentPreset: string): Promise<string>
|
||||
|
||||
|
||||
@@ -498,8 +498,8 @@ async read(id: string): Promise<string>
|
||||
* One preset's composition text with the roster row it belongs to.
|
||||
* @param agentPreset - the preset id.
|
||||
* @returns the composition beside its trust and published metadata.
|
||||
* @throws {TypertRemoteFailure} `bad-request` for an empty id, or
|
||||
* `agent-preset-not-found` when no configured root supplies it.
|
||||
* @throws {RemoteError} `gateway/bad-request` for an empty id, or
|
||||
* `agent-preset/not-found` when no configured root supplies it.
|
||||
*/
|
||||
@Remote('read') async readDocument(agentPreset: string): Promise<AgentPresetDocument>
|
||||
|
||||
@@ -526,8 +526,8 @@ async copy(from: string, id: string, name?: string): Promise<void>
|
||||
* @param id - the new preset id.
|
||||
* @param name - the copy's optional display name.
|
||||
* @returns once the copy is stored.
|
||||
* @throws {TypertRemoteFailure} with the corresponding stable preset code
|
||||
* and details when the copy is refused.
|
||||
* @throws {RemoteError} with the corresponding stable preset code and
|
||||
* details when the copy is refused.
|
||||
*/
|
||||
@Remote('copy') async remoteExportCopy(from: string, id: string, name?: string): Promise<void>
|
||||
|
||||
@@ -543,8 +543,8 @@ async remove(id: string): Promise<void>
|
||||
* Delete one preset through the Remote API.
|
||||
* @param id - the preset id.
|
||||
* @returns once the preset is deleted.
|
||||
* @throws {TypertRemoteFailure} with the corresponding stable preset code
|
||||
* and details when deletion is refused.
|
||||
* @throws {RemoteError} with the corresponding stable preset code and
|
||||
* details when deletion is refused.
|
||||
*/
|
||||
@Remote('deletePreset') async remoteExportDelete(id: string): Promise<void>
|
||||
|
||||
@@ -595,8 +595,8 @@ async recompose(agentCtx: Context, id: string): Promise<AgentPreset>
|
||||
* @param agent - the session's live agent, resolved from the wire identity.
|
||||
* @param agentPreset - the preset to compose the agent from instead.
|
||||
* @returns the preset id that was recorded.
|
||||
* @throws {TypertRemoteFailure} with `bad-request`, `agent-preset-locked`,
|
||||
* `agent-preset-not-found`, or `agent-preset-invalid` when refused.
|
||||
* @throws {RemoteError} with `gateway/bad-request`, `agent-preset/locked`,
|
||||
* `agent-preset/not-found`, or `agent-preset/invalid` when refused.
|
||||
*/
|
||||
@Remote('select') async select(agent: Agent, agentPreset: string): Promise<string>
|
||||
|
||||
|
||||
@@ -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/subsystems/credentials.md
|
||||
credentials.md: 60cd388fc18fe239b9ee5207d77d3b5561b7a3b3
|
||||
credentials.zh.md: 5ca47fac6c36c14eb1ee0aa8a6cb6a8b5e3eb9c4
|
||||
credentials.md: 5ae53a19140f423e1c52334c40f25fca7d3ed754
|
||||
credentials.zh.md: b6d5d8eb3960edd68a96a32a41c591e61f2c8b5e
|
||||
@@ -227,9 +227,10 @@ Host service backing the generated `ctx.remote.credentials` namespace. It carrie
|
||||
* Describe several references for one configuration surface. Batched because
|
||||
* a settings page describes every reference its rows name at once, and one
|
||||
* round trip keeps those rows from settling separately.
|
||||
* @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar rejects the whole call as `bad-request`.
|
||||
* @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar
|
||||
* rejects the whole call as `gateway/bad-request`.
|
||||
* @returns one view per requested name, keyed by that name.
|
||||
* @throws TypertRemoteFailure when the request is invalid or no credential provider is mounted.
|
||||
* @throws RemoteError when the request is invalid or no credential provider is mounted.
|
||||
*/
|
||||
@Remote async describe(refs: string[]): Promise<Record<string, CredentialInfo>>
|
||||
|
||||
@@ -238,14 +239,14 @@ Host service backing the generated `ctx.remote.credentials` namespace. It carrie
|
||||
* this direction only: no read path returns it.
|
||||
* @param ref - reference name to store under.
|
||||
* @param value - the non-empty secret value.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async set(ref: string, value: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Remove one reference from a configuration surface.
|
||||
* @param ref - reference name to remove.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async unset(ref: string): Promise<void>
|
||||
```
|
||||
|
||||
@@ -227,9 +227,10 @@ Host service backing the generated `ctx.remote.credentials` namespace. It carrie
|
||||
* Describe several references for one configuration surface. Batched because
|
||||
* a settings page describes every reference its rows name at once, and one
|
||||
* round trip keeps those rows from settling separately.
|
||||
* @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar rejects the whole call as `bad-request`.
|
||||
* @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar
|
||||
* rejects the whole call as `gateway/bad-request`.
|
||||
* @returns one view per requested name, keyed by that name.
|
||||
* @throws TypertRemoteFailure when the request is invalid or no credential provider is mounted.
|
||||
* @throws RemoteError when the request is invalid or no credential provider is mounted.
|
||||
*/
|
||||
@Remote async describe(refs: string[]): Promise<Record<string, CredentialInfo>>
|
||||
|
||||
@@ -238,14 +239,14 @@ Host service backing the generated `ctx.remote.credentials` namespace. It carrie
|
||||
* this direction only: no read path returns it.
|
||||
* @param ref - reference name to store under.
|
||||
* @param value - the non-empty secret value.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async set(ref: string, value: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Remove one reference from a configuration surface.
|
||||
* @param ref - reference name to remove.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async unset(ref: string): Promise<void>
|
||||
```
|
||||
|
||||
@@ -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/subsystems/llm-streaming.md
|
||||
llm-streaming.md: e37356d91e75334987b6fc707744f2376df1b2fc
|
||||
llm-streaming.zh.md: 05f24bed6beff9508db88e60af7b259eda33f474
|
||||
llm-streaming.md: 6867ae292d77474bcedc1466ae0ce6b1fc1c92d3
|
||||
llm-streaming.zh.md: b75e24f2e9010b4fb08c035f14bc4e91dc3971ef
|
||||
@@ -929,7 +929,7 @@ async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, sig
|
||||
* @param request - endpoint, protocol, and one-shot credential to use.
|
||||
* @param signal - caller cancellation supplied by the Remote carrier.
|
||||
* @returns advertised models in endpoint order.
|
||||
* @throws TypertRemoteFailure with `model-discovery-failed` when discovery refuses or fails.
|
||||
* @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
|
||||
*/
|
||||
@Remote('discoverModels') async remoteDiscoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, signal: AbortSignal, ): Promise<LlmDiscoveredModel[]>
|
||||
|
||||
|
||||
@@ -935,7 +935,7 @@ async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, sig
|
||||
* @param request - endpoint, protocol, and one-shot credential to use.
|
||||
* @param signal - caller cancellation supplied by the Remote carrier.
|
||||
* @returns advertised models in endpoint order.
|
||||
* @throws TypertRemoteFailure with `model-discovery-failed` when discovery refuses or fails.
|
||||
* @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
|
||||
*/
|
||||
@Remote('discoverModels') async remoteDiscoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, signal: AbortSignal, ): Promise<LlmDiscoveredModel[]>
|
||||
|
||||
|
||||
@@ -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/subsystems/session.md
|
||||
session.md: f3c246f7a77386a088f0559d233031bdb8d997f8
|
||||
session.zh.md: bc706a11e9b504f6806cbf7c1beb67b972fb4f75
|
||||
session.md: d06932171b6fcf0df3470d0236a0e8b0f5aa6c21
|
||||
session.zh.md: fc8c4ec53b875614a082777eaa109f1a2514268e
|
||||
@@ -660,7 +660,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
|
||||
* @param request - path after best-effort Session workspace resolution.
|
||||
* @param signal - caller lifetime; abort terminates the native command.
|
||||
* @returns confirmation after the native opener accepts the path.
|
||||
* @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails.
|
||||
* @throws RemoteError when the request is invalid, cancelled, or the opener fails.
|
||||
*/
|
||||
@Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise<SessionOpenWorkspacePathValue>
|
||||
|
||||
|
||||
@@ -664,7 +664,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
|
||||
* @param request - path after best-effort Session workspace resolution.
|
||||
* @param signal - caller lifetime; abort terminates the native command.
|
||||
* @returns confirmation after the native opener accepts the path.
|
||||
* @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails.
|
||||
* @throws RemoteError when the request is invalid, cancelled, or the opener fails.
|
||||
*/
|
||||
@Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise<SessionOpenWorkspacePathValue>
|
||||
|
||||
|
||||
@@ -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/subsystems/settings.md
|
||||
settings.md: 3215de191b4ef8fecb373d280c8bc8e4a89bbc7e
|
||||
settings.zh.md: 0c28ef381ee6ea8e59da64717575fbf80c8a2e5f
|
||||
settings.md: 1a83e76d908ff19ca0f3abb3b21e10cae0d0685f
|
||||
settings.zh.md: af204ccfbda419996686ba691ee57e884c18e631
|
||||
@@ -262,14 +262,14 @@ Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/sett
|
||||
|
||||
### `ctx.settingsController` — `SettingsController`
|
||||
|
||||
Host service backing the generated `ctx.remote.settings` namespace. Every remote read uses `redactSecrets: true`, so a `role('secret')` field cannot ride a response. Writes expose the settings service's merge, replacement, and path-addressed operations, and classify every provider refusal as `settings-conflict` or `settings-rejected` with the service's message.
|
||||
Host service backing the generated `ctx.remote.settings` namespace. Every remote read uses `redactSecrets: true`, so a `role('secret')` field cannot ride a response. Writes expose the settings service's merge, replacement, and path-addressed operations, and classify every provider refusal as `settings/conflict` or `settings/rejected` with the service's message.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Describe every registered namespace for a configuration page: redacted
|
||||
* layered values plus the serialized schema the page renders its form from.
|
||||
* @returns provider writability, local-document presence, and one view per namespace.
|
||||
* @throws TypertRemoteFailure when no settings provider is mounted.
|
||||
* @throws RemoteError when no settings provider is mounted.
|
||||
*/
|
||||
@Remote describe(): SettingsDescribeValue
|
||||
|
||||
@@ -285,7 +285,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* @param patch - fields to merge into the user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
@@ -295,7 +295,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* @param section - complete replacement user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
@@ -307,7 +307,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* @param ops - the edits to apply, in order.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
@@ -315,7 +315,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* Materialize the provider-owned settings document and open it in a native text editor.
|
||||
* @param signal - caller lifetime; abort terminates preparation or the native command.
|
||||
* @returns confirmation after the native opener accepts the document.
|
||||
* @throws TypertRemoteFailure when no document exists, preparation fails, or opening fails.
|
||||
* @throws RemoteError when no document exists, preparation fails, or opening fails.
|
||||
*/
|
||||
@Remote async openSettingsDocument(signal: AbortSignal): Promise<SettingsDocumentOpenValue>
|
||||
|
||||
@@ -324,7 +324,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* @param agentPreset - preset id resolved against Host-owned roots.
|
||||
* @param signal - caller lifetime; abort terminates the native command.
|
||||
* @returns an opened confirmation or the resolved directory for text display.
|
||||
* @throws TypertRemoteFailure when the preset is missing, read-only, invalid, or cannot be opened.
|
||||
* @throws RemoteError when the preset is missing, read-only, invalid, or cannot be opened.
|
||||
*/
|
||||
@Remote async openAgentPresetDirectory( agentPreset: string, signal: AbortSignal, ): Promise<AgentPresetDirectoryOpenValue>
|
||||
```
|
||||
|
||||
@@ -262,14 +262,14 @@ Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/sett
|
||||
|
||||
### `ctx.settingsController` — `SettingsController`
|
||||
|
||||
Host service backing the generated `ctx.remote.settings` namespace. Every remote read uses `redactSecrets: true`, so a `role('secret')` field cannot ride a response. Writes expose the settings service's merge, replacement, and path-addressed operations, and classify every provider refusal as `settings-conflict` or `settings-rejected` with the service's message.
|
||||
Host service backing the generated `ctx.remote.settings` namespace. Every remote read uses `redactSecrets: true`, so a `role('secret')` field cannot ride a response. Writes expose the settings service's merge, replacement, and path-addressed operations, and classify every provider refusal as `settings/conflict` or `settings/rejected` with the service's message.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Describe every registered namespace for a configuration page: redacted
|
||||
* layered values plus the serialized schema the page renders its form from.
|
||||
* @returns provider writability, local-document presence, and one view per namespace.
|
||||
* @throws TypertRemoteFailure when no settings provider is mounted.
|
||||
* @throws RemoteError when no settings provider is mounted.
|
||||
*/
|
||||
@Remote describe(): SettingsDescribeValue
|
||||
|
||||
@@ -285,7 +285,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* @param patch - fields to merge into the user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
@@ -295,7 +295,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* @param section - complete replacement user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
@@ -307,7 +307,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* @param ops - the edits to apply, in order.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
@@ -315,7 +315,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* Materialize the provider-owned settings document and open it in a native text editor.
|
||||
* @param signal - caller lifetime; abort terminates preparation or the native command.
|
||||
* @returns confirmation after the native opener accepts the document.
|
||||
* @throws TypertRemoteFailure when no document exists, preparation fails, or opening fails.
|
||||
* @throws RemoteError when no document exists, preparation fails, or opening fails.
|
||||
*/
|
||||
@Remote async openSettingsDocument(signal: AbortSignal): Promise<SettingsDocumentOpenValue>
|
||||
|
||||
@@ -324,7 +324,7 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
|
||||
* @param agentPreset - preset id resolved against Host-owned roots.
|
||||
* @param signal - caller lifetime; abort terminates the native command.
|
||||
* @returns an opened confirmation or the resolved directory for text display.
|
||||
* @throws TypertRemoteFailure when the preset is missing, read-only, invalid, or cannot be opened.
|
||||
* @throws RemoteError when the preset is missing, read-only, invalid, or cannot be opened.
|
||||
*/
|
||||
@Remote async openAgentPresetDirectory( agentPreset: string, signal: AbortSignal, ): Promise<AgentPresetDirectoryOpenValue>
|
||||
```
|
||||
|
||||
@@ -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/subsystems/skills.md
|
||||
skills.md: 18eb4d3289cec5c7807feab4229096b5799d4168
|
||||
skills.zh.md: aa167373cb9736cf24d7bf20dc965e68568826be
|
||||
skills.md: 8224b91290c110d52d739cd85b3fc7d641f9a7ea
|
||||
skills.zh.md: 018bd85d74ba40717c741d6600f991f3c6d36d4c
|
||||
@@ -258,7 +258,7 @@ Host service backing `ctx.remote.skills` without activating a cold Agent.
|
||||
* @param request - Session identity whose cwd and preset select the catalog view.
|
||||
* @param signal - caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics.
|
||||
* @returns user-invocable skill metadata without loading skill bodies.
|
||||
* @throws TypertRemoteFailure when the Session cannot be inspected or no registry can serve it.
|
||||
* @throws RemoteError when the Session cannot be inspected or no registry can serve it.
|
||||
*/
|
||||
@Remote async list(request: SkillListRequest, signal: AbortSignal): Promise<SkillListValue>
|
||||
```
|
||||
|
||||
@@ -258,7 +258,7 @@ Host service backing `ctx.remote.skills` without activating a cold Agent.
|
||||
* @param request - Session identity whose cwd and preset select the catalog view.
|
||||
* @param signal - caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics.
|
||||
* @returns user-invocable skill metadata without loading skill bodies.
|
||||
* @throws TypertRemoteFailure when the Session cannot be inspected or no registry can serve it.
|
||||
* @throws RemoteError when the Session cannot be inspected or no registry can serve it.
|
||||
*/
|
||||
@Remote async list(request: SkillListRequest, signal: AbortSignal): Promise<SkillListValue>
|
||||
```
|
||||
|
||||
@@ -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/subsystems/subagent.md
|
||||
subagent.md: dfadbbdee28607c929e629a20c18af6b55f4f51e
|
||||
subagent.zh.md: d4ffe30d69aff404733a6e8acbc2d1a19e2276d3
|
||||
subagent.md: cedbc8544cb86db9feda4b0a2bcf5a65cf99bfe9
|
||||
subagent.zh.md: 4eabe673b0e750e0b3ed5480299b93a377886b13
|
||||
+10
-10
@@ -657,9 +657,9 @@ listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<Subagen
|
||||
* @param parentSessionId - parent session whose direct children are listed.
|
||||
* @param signal - carrier cancellation forwarded to Session queries.
|
||||
* @returns the catalog view for that parent.
|
||||
* @throws {TypertRemoteFailure} `bad-request` for an empty parent id,
|
||||
* `cancelled` for an aborted read, `subagent-projections-unavailable` when
|
||||
* the deployment has no projection registry, otherwise `internal`.
|
||||
* @throws {RemoteError} `gateway/bad-request` for an empty parent id,
|
||||
* `gateway/cancelled` for an aborted read, `subagent/projections-unavailable` when
|
||||
* the deployment has no projection registry, otherwise `gateway/internal`.
|
||||
*/
|
||||
@Remote('list') async remoteExportList(parentSessionId: SessionId, signal: AbortSignal): Promise<SubagentCatalog>
|
||||
|
||||
@@ -672,10 +672,10 @@ listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<Subagen
|
||||
* @param request - durable address, minted identity, content, and optional browser zone.
|
||||
* @param signal - carrier cancellation, owning the call until inbox acceptance.
|
||||
* @returns the accepted message's inbox identity.
|
||||
* @throws {TypertRemoteFailure} `bad-request`, `invalid-time-zone`,
|
||||
* `subagent-parent-unavailable`, `subagent-not-resumable`,
|
||||
* `subagent-unauthorized`, `subagent-delivery-unavailable`, `cancelled`, or
|
||||
* `internal`.
|
||||
* @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-unsupported`,
|
||||
* `subagent/invalid-time-zone`, `subagent/parent-unavailable`,
|
||||
* `subagent/not-resumable`, `subagent/unauthorized`,
|
||||
* `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.
|
||||
*/
|
||||
@Remote('prompt') async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt>
|
||||
|
||||
@@ -689,9 +689,9 @@ listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<Subagen
|
||||
* @param parentSessionId - durable direct parent whose authority is claimed.
|
||||
* @param mode - required continuable-address discriminator.
|
||||
* @returns acknowledgement that the cancel signal was admitted, not that the target is quiescent.
|
||||
* @throws {TypertRemoteFailure} `bad-request` for an empty id,
|
||||
* `subagent-unauthorized` when the address does not own the live target,
|
||||
* otherwise `internal`.
|
||||
* @throws {RemoteError} `gateway/bad-request` for an empty id,
|
||||
* `subagent/unauthorized` when the address does not own the live target,
|
||||
* otherwise `gateway/internal`.
|
||||
*/
|
||||
@Remote('interruptByParent') interruptByParent( childSessionId: SessionId, parentSessionId: SessionId, mode: 'continuable', ): SubagentInterruptReceipt
|
||||
|
||||
|
||||
@@ -661,9 +661,9 @@ listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<Subagen
|
||||
* @param parentSessionId - parent session whose direct children are listed.
|
||||
* @param signal - carrier cancellation forwarded to Session queries.
|
||||
* @returns the catalog view for that parent.
|
||||
* @throws {TypertRemoteFailure} `bad-request` for an empty parent id,
|
||||
* `cancelled` for an aborted read, `subagent-projections-unavailable` when
|
||||
* the deployment has no projection registry, otherwise `internal`.
|
||||
* @throws {RemoteError} `gateway/bad-request` for an empty parent id,
|
||||
* `gateway/cancelled` for an aborted read, `subagent/projections-unavailable` when
|
||||
* the deployment has no projection registry, otherwise `gateway/internal`.
|
||||
*/
|
||||
@Remote('list') async remoteExportList(parentSessionId: SessionId, signal: AbortSignal): Promise<SubagentCatalog>
|
||||
|
||||
@@ -676,10 +676,10 @@ listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<Subagen
|
||||
* @param request - durable address, minted identity, content, and optional browser zone.
|
||||
* @param signal - carrier cancellation, owning the call until inbox acceptance.
|
||||
* @returns the accepted message's inbox identity.
|
||||
* @throws {TypertRemoteFailure} `bad-request`, `invalid-time-zone`,
|
||||
* `subagent-parent-unavailable`, `subagent-not-resumable`,
|
||||
* `subagent-unauthorized`, `subagent-delivery-unavailable`, `cancelled`, or
|
||||
* `internal`.
|
||||
* @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-unsupported`,
|
||||
* `subagent/invalid-time-zone`, `subagent/parent-unavailable`,
|
||||
* `subagent/not-resumable`, `subagent/unauthorized`,
|
||||
* `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.
|
||||
*/
|
||||
@Remote('prompt') async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt>
|
||||
|
||||
@@ -693,9 +693,9 @@ listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<Subagen
|
||||
* @param parentSessionId - durable direct parent whose authority is claimed.
|
||||
* @param mode - required continuable-address discriminator.
|
||||
* @returns acknowledgement that the cancel signal was admitted, not that the target is quiescent.
|
||||
* @throws {TypertRemoteFailure} `bad-request` for an empty id,
|
||||
* `subagent-unauthorized` when the address does not own the live target,
|
||||
* otherwise `internal`.
|
||||
* @throws {RemoteError} `gateway/bad-request` for an empty id,
|
||||
* `subagent/unauthorized` when the address does not own the live target,
|
||||
* otherwise `gateway/internal`.
|
||||
*/
|
||||
@Remote('interruptByParent') interruptByParent( childSessionId: SessionId, parentSessionId: SessionId, mode: 'continuable', ): SubagentInterruptReceipt
|
||||
|
||||
|
||||
@@ -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/subsystems/typert.md
|
||||
typert.md: 20734175cc6b853ae7eeb18e6d5fc91a97cdbe0d
|
||||
typert.zh.md: e900011f24a5a232ab8764501594b563f320e090
|
||||
typert.md: 0f3d2b1afdc1b7713402abc5885dc9551e16ac14
|
||||
typert.zh.md: a3c06489707e18433ed9018363c57ec76732e606
|
||||
+18
-18
@@ -139,7 +139,7 @@ interface TypertRemoteNamespaceMap {}
|
||||
|
||||
## Host Gateway
|
||||
|
||||
Connection decodes its carrier envelope before calling `ctx.typertGateway`. The request carries exact named wire fields and the carrier's cancellation signal separately; infrastructure and boundary failures use the Gateway's in-process error taxonomy, ordinary exceptions are folded by the RPC adapter into the transport's `internal` error code, and existing RPC errors carried by lookup policy through `TypertLookupFailure` are returned unchanged.
|
||||
Connection decodes its carrier envelope before calling `ctx.typertGateway`. The request carries exact named wire fields and the carrier's cancellation signal separately; infrastructure and boundary failures ride `TypertGatewayError`, whose `gateway/*` codes are ordinary `RemoteError` codes, so the RPC adapter passes every structurally identified `RemoteError` through with its code and details intact and folds only unrecognized exceptions into `gateway/internal`.
|
||||
|
||||
```ts type-equiv
|
||||
/** One Remote method request after a carrier has decoded its envelope. */
|
||||
@@ -158,23 +158,23 @@ interface InvokeRemoteRequest {
|
||||
```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'
|
||||
| 'gateway/ambiguous-endpoint'
|
||||
| 'gateway/arguments-invalid'
|
||||
| 'gateway/binding-invalid'
|
||||
| 'gateway/context-failed'
|
||||
| 'gateway/context-not-found'
|
||||
| 'gateway/context-unavailable'
|
||||
| 'gateway/definition-unavailable'
|
||||
| 'gateway/input-invalid'
|
||||
| 'gateway/invocation-unavailable'
|
||||
| 'gateway/lookup-failed'
|
||||
| 'gateway/lookup-not-found'
|
||||
| 'gateway/lookup-unavailable'
|
||||
| 'gateway/method-unavailable'
|
||||
| 'gateway/provider-mismatch'
|
||||
| 'gateway/result-invalid'
|
||||
| 'gateway/service-unavailable'
|
||||
| 'gateway/signature-invalid'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
|
||||
@@ -139,7 +139,7 @@ interface TypertRemoteNamespaceMap {}
|
||||
|
||||
## Host Gateway
|
||||
|
||||
Connection 会先解码 carrier envelope,再调用 `ctx.typertGateway`。请求将精确的具名 wire 字段与 carrier 的取消 signal 分开携带;基础设施与边界失败使用 Gateway 的进程内错误分类体系,普通异常由 RPC 适配器归并为传输层的 `internal` 错误码,lookup 策略通过 `TypertLookupFailure` 携带的既有 RPC error 则原样返回。
|
||||
Connection 会先解码 carrier envelope,再调用 `ctx.typertGateway`。请求将精确的具名 wire 字段与 carrier 的取消 signal 分开携带;基础设施与边界失败由 `TypertGatewayError` 承载,其 `gateway/*` 码就是普通的 `RemoteError` 码,因此 RPC 适配器会把每个经结构识别的 `RemoteError` 连同其 code 与 details 原样放行,只把无法识别的异常归并为 `gateway/internal`。
|
||||
|
||||
```ts type-equiv
|
||||
/** One Remote method request after a carrier has decoded its envelope. */
|
||||
@@ -158,23 +158,23 @@ interface InvokeRemoteRequest {
|
||||
```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'
|
||||
| 'gateway/ambiguous-endpoint'
|
||||
| 'gateway/arguments-invalid'
|
||||
| 'gateway/binding-invalid'
|
||||
| 'gateway/context-failed'
|
||||
| 'gateway/context-not-found'
|
||||
| 'gateway/context-unavailable'
|
||||
| 'gateway/definition-unavailable'
|
||||
| 'gateway/input-invalid'
|
||||
| 'gateway/invocation-unavailable'
|
||||
| 'gateway/lookup-failed'
|
||||
| 'gateway/lookup-not-found'
|
||||
| 'gateway/lookup-unavailable'
|
||||
| 'gateway/method-unavailable'
|
||||
| 'gateway/provider-mismatch'
|
||||
| 'gateway/result-invalid'
|
||||
| 'gateway/service-unavailable'
|
||||
| 'gateway/signature-invalid'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
|
||||
@@ -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 packages/api/gateway/README.md
|
||||
README.md: d10eed46d5534a2459995bd687942d799f009c7b
|
||||
README.zh.md: 310b44cfe633758089771cda4040679c09373ec2
|
||||
README.md: 509f1108a8f940b93b9555399ec59c26325fe4b8
|
||||
README.zh.md: 116969c9f04d7791475ff63d8b4602e96c2aaa5b
|
||||
@@ -28,7 +28,7 @@ Two-sided Typert RPC endpoint for Host and Client Cordis environments. The Host
|
||||
|
||||
Strict mode reads generated invocation descriptors from `ctx.typert.local`. Lookup parameters use the currently active resolver in `ctx.typert.lookups`: the business package registers the stable declaration and default policy, while Host composition can override resolution behavior with effect-scoped `configure()`; `@RemoteScope` resolves its receiver through a registered Host Context adapter. SRC mode is a development fallback for endpoints that have never had a strict definition; it parses simple parameter names and accepts only JSON-safe values for non-lookup parameters. Withdrawing an observed strict definition fails instead of weakening validation.
|
||||
|
||||
The Host entry registers a trusted-host interceptor on Connection's shared `/api` FetchHandler. Connection passes this composite handler through its HTTP bridge; the handler dispatches claimed endpoints to Gateway and returns 404 for unclaimed requests unless an exact Fetch route owns them. Direct `invoke()` calls preserve business errors; `TypertGatewayError` distinguishes failures owned by dispatch, binding, providers, lookup, Context, arguments, and codecs. A resolver may use `TypertLookupFailure` to carry an existing RPC error, preserving its original error code for policy rejections such as cold-resume failures or ownership fences.
|
||||
The Host entry registers a trusted-host interceptor on Connection's shared `/api` FetchHandler. Connection passes this composite handler through its HTTP bridge; the handler dispatches claimed endpoints to Gateway and returns 404 for unclaimed requests unless an exact Fetch route owns them. Direct `invoke()` calls preserve business errors; `TypertGatewayError` is a `RemoteError` subclass whose `gateway/*` codes name the failures owned by dispatch, binding, providers, lookup, Context, arguments, and codecs. A resolver that refuses on policy grounds — a cold-resume failure or an ownership fence — throws its own `RemoteError`, and the code it chose reaches the caller unchanged.
|
||||
|
||||
A cancellation-aware Remote method declares `signal: AbortSignal` as its final Host parameter. The signal is descriptor metadata rather than a wire argument: Connection supplies it to the Gateway, and the Gateway injects it after decoded business parameters. SRC recognizes the reserved final name, while strict generation additionally requires the global `AbortSignal` type.
|
||||
|
||||
@@ -43,7 +43,11 @@ Host composition can register one application event source through `registerRemo
|
||||
|
||||
Each unary call validates positional inputs, constructs the descriptor's exact named `args`, and sends it through `ctx.connection.rpc.call('/api', endpoint, ...)`. A generated stream method returns an `AsyncIterable` and opens one logical stream through an in-process Connection carrier when available, otherwise through the shared Gateway WebSocket. Generated cancellation-aware methods accept a final optional `AbortSignal`; the Client combines it with the contribution mount lifetime before invoking the carrier. Unary results and every stream item are validated before reaching application code. Withdrawing a contribution removes its descriptors and methods together, aborts in-flight calls and streams, and makes retained method handles reject.
|
||||
|
||||
`ctx.remote.$stream()` returns a single-consumer `RemoteStream` spanning physical carrier generations. It permits one immediate retry while the Host remains available, otherwise waits for the next connected Host generation, and annotates each item with its physical generation. The domain consumer validates and accepts each generation's opening value; business and protocol failures remain terminal. `RemoteSnapshotStream` adds one opening snapshot followed by deltas. `RemoteJournalStream` adds follow-before-page opening, pagination, reconnect catch-up, and gap repair over domain-defined inclusive entry ranges; it removes complete duplicates and rejects gaps, inverted ranges, and partial overlaps. Disposing any stream cancels its requests and resolves after the active iterator is fully stopped.
|
||||
Every unary call resolves to `RemoteResult<T>` — `{ ok: true, value }` or `{ ok: false, error }` — and never rejects for a carrier problem: this face folds an offline carrier into the error branch and answers `gateway/cancelled` when the caller's signal aborts, so no consumer wraps a call to recover one. Only an assembly fault still rejects: wrong arity, an unmounted method, a withdrawn contribution, a missing Context adapter. `error` is a live `RemoteError` instance, so `throw result.error` keeps throw semantics, and `isRemoteFailure(value)` is the one predicate a consumer needs — a caught value it accepts carries a Host code, and anything it rejects is a local fault the caller should let crash.
|
||||
|
||||
`ctx.remote.$host` reads the fixed Host facts as plain values: `home` (undefined until the first ready frame) and `isLoopback`. It is not a store — no subscription, no generation counter — so a consumer that must react to reconnection listens for `connection/reset` instead of polling it.
|
||||
|
||||
`ctx.remote.$stream()` returns a single-consumer `RemoteStream` spanning physical carrier generations. It permits one immediate retry while the Host remains available, otherwise waits for the next connected Host generation, and annotates each item with its physical generation. The domain consumer validates and accepts each generation's opening value; business and protocol failures remain terminal. Every terminal failure leaves this face as a `RemoteError`, including exhausted carrier retries and a generation that ends before its opening value, so a stream consumer discriminates the same way a unary caller does. `RemoteStreamCarrierError` names a retryable physical loss and reaches a domain only as the `carrierFailed` callback argument, never as a terminal outcome. `RemoteSnapshotStream` adds one opening snapshot followed by deltas. `RemoteJournalStream` adds follow-before-page opening, pagination, reconnect catch-up, and gap repair over domain-defined inclusive entry ranges; it removes complete duplicates and rejects gaps, inverted ranges, and partial overlaps. Disposing any stream cancels its requests and resolves after the active iterator is fully stopped.
|
||||
|
||||
`ctx.remote.$on()` subscribes to one forwarded Host event. Its legal keys are exactly the Host assembly's forwarding selection, and the listener type is the owning package's own Cordis `Events` declaration, so no second signature can drift from it. Each subscription belongs to the calling fiber and disappears with it. The Client Remote service registers the `$events` pump as a Connection generation source when it activates, whether any `$on` listener exists. Browsers use Remote mux, while in-process compositions use `connection.rpc.open`; the opening `ready` item establishes a Connection generation and supplies its Host facts. Carrier failure, Remote stream failure, unexpected normal completion, a non-ready opening item, or a malformed event item ends that generation and lets Connection reopen it after backoff. Ordinary notifications run in registration order and isolate listener failures. Agent-scoped waterfalls let a listener return a result, call `next()`, or reject; Gateway returns that outcome through the existing HTTP unary carrier.
|
||||
|
||||
@@ -62,7 +66,7 @@ No direct effect; invoked business Services own any model-visible result.
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- The Connection adapter maps ordinary dispatch failures and business exceptions to the RPC `internal` code with empty details; lookup-policy errors carried by `TypertLookupFailure` are returned unchanged. Structured `TypertGatewayError` categories remain available only to same-process callers.
|
||||
- The Connection adapter answers `gateway/internal` with empty details for dispatch failures and unclassified exceptions; a `RemoteError` thrown by an owner or by Gateway itself crosses the wire with its own code, message, and details. Its `cause` chain and the `TypertGatewayError` subclass identity survive only for same-process callers.
|
||||
- SRC mode supports unique identifier parameters without destructuring, defaults, or rest parameters. It validates JSON safety rather than generated business types and never infers optional fields.
|
||||
- Only strict generated contributions can mount on the Client face. SRC markers have no Client codec or type projection.
|
||||
- `$stream()` supervises carrier replacement but does not infer replay semantics; each domain owns its resume cursor or replacement-baseline validation and normal-end classification. Connection generations reopen the internal `$events` stream; one-way notifications are not replayed, while pending scoped waterfalls retain their event id across replay.
|
||||
|
||||
@@ -28,7 +28,7 @@ kind: "package-reference"
|
||||
|
||||
严格模式从 `ctx.typert.local` 读取生成的调用描述符。查找参数使用 `ctx.typert.lookups` 中当前有效的 resolver:业务包注册稳定声明与默认策略,Host 组合可用 effect-scoped `configure()` 覆盖解析行为;`@RemoteScope` 则通过已注册的 Host Context adapter 解析其接收者。SRC 模式是开发阶段的回退路径,适用于从未具备严格定义的端点;它解析简单参数名,并且只允许非查找参数使用可安全表示为 JSON 的值。已观测到的严格定义一旦撤回,系统会直接报错,而不会降低校验强度。
|
||||
|
||||
Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridge;handler 将已认领 endpoint 分发给 Gateway,未认领且没有精确 Fetch 路由负责的请求返回 404。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 可区分分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。resolver 可以用 `TypertLookupFailure` 携带既有 RPC error,使冷恢复失败或 ownership fence 等策略拒绝保持原错误码。
|
||||
Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridge;handler 将已认领 endpoint 分发给 Gateway,未认领且没有精确 Fetch 路由负责的请求返回 404。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 是 `RemoteError` 的子类,其 `gateway/*` 码命名了分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。因策略而拒绝的 resolver——冷恢复失败或 ownership fence——抛出自己的 `RemoteError`,它选定的码原样到达调用方。
|
||||
|
||||
支持取消的 Remote 方法会把 `signal: AbortSignal` 声明为最后一个 Host 参数。signal 是 descriptor 元数据,而不是 wire 参数:Connection 将它提供给 Gateway,Gateway 则在已解码的业务参数之后注入它。SRC 识别这个保留的末位参数名,严格生成还要求它具有全局 `AbortSignal` 类型。
|
||||
|
||||
@@ -43,7 +43,11 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
|
||||
|
||||
每次一元调用都会校验位置参数,构造与描述符完全匹配的具名 `args`,再通过 `ctx.connection.rpc.call('/api', endpoint, ...)` 发送。生成的流方法返回 `AsyncIterable`,并在进程内 Connection 载体可用时通过它打开逻辑流,否则通过共享的 Gateway WebSocket 打开。生成的支持取消的方法接受最后一个可选 `AbortSignal`;Client 会在调用载体前将它与贡献项的挂载生命周期合并。一元结果和每个流项都经过校验后才会交给应用代码。撤回贡献项会同时移除其描述符和方法、中止正在进行的调用与流,并使外部仍持有的方法句柄在调用时返回拒绝。
|
||||
|
||||
`ctx.remote.$stream()` 返回跨越多个物理载体代次的单消费方 `RemoteStream`。Host 仍在线时,它允许一次立即重试;Host 离线时,它等待下一代连接,并为每个流项标注物理代次。领域消费方校验并接受各代次的 opening value;业务与协议错误仍然终止流。`RemoteSnapshotStream` 在此之上规定每代由一个 opening snapshot 和后续 delta 组成。`RemoteJournalStream` 基于领域提供的 entry 闭区间提供 follow-before-page、分页、重连追赶与缺口修复;它丢弃完整重复项,并拒绝缺口、倒置区间和部分重叠。dispose 任一种 stream 都会取消其请求,并在活动 iterator 完全停止后完成。
|
||||
每次一元调用都解析为 `RemoteResult<T>`——`{ ok: true, value }` 或 `{ ok: false, error }`——且绝不因载体问题 reject:本面把断线载体折入错误分支,调用方 signal 中止时答以 `gateway/cancelled`,因此没有消费方需要包一层来兜载体失败。只有装配故障仍会 reject:参数个数不符、方法未挂载、贡献已撤下、缺少 Context adapter。`error` 是活的 `RemoteError` 实例,所以 `throw result.error` 保持 throw 语义;而 `isRemoteFailure(value)` 是消费方唯一需要的谓词——它认下的捕获值带着 Host 码,它拒绝的一律是本地故障,调用方应当让其崩掉。
|
||||
|
||||
`ctx.remote.$host` 以普通值读取固定的 Host 事实:`home`(首个 ready 帧之前为 undefined)与 `isLoopback`。它不是 store——没有订阅、没有代次计数——所以需要响应重连的消费方去监听 `connection/reset`,而不是轮询它。
|
||||
|
||||
`ctx.remote.$stream()` 返回跨越多个物理载体代次的单消费方 `RemoteStream`。Host 仍在线时,它允许一次立即重试;Host 离线时,它等待下一代连接,并为每个流项标注物理代次。领域消费方校验并接受各代次的 opening value;业务与协议错误仍然终止流。一切终态失败离开本面时都是 `RemoteError`,包括重试耗尽和在 opening value 之前就结束的代次,因此流消费方与一元调用方用同一种方式判别。`RemoteStreamCarrierError` 命名的是可重试的物理丢失,它只作为 `carrierFailed` 回调参数到达领域,绝不作为终态结果。`RemoteSnapshotStream` 在此之上规定每代由一个 opening snapshot 和后续 delta 组成。`RemoteJournalStream` 基于领域提供的 entry 闭区间提供 follow-before-page、分页、重连追赶与缺口修复;它丢弃完整重复项,并拒绝缺口、倒置区间和部分重叠。dispose 任一种 stream 都会取消其请求,并在活动 iterator 完全停止后完成。
|
||||
|
||||
`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属发起调用的 fiber,并随该 fiber 一起消失。Client Remote 服务激活时就把 `$events` pump 注册为 Connection generation source,因此即使当前无 `$on` 订阅,它也会在 Connection 循环启动时打开。浏览器使用 Remote mux,进程内组合使用 `connection.rpc.open`;opening `ready` 项建立 Connection generation 并提供 Host 信息。物理 carrier 失败、Remote stream error、意外正常结束、非 ready 首项或畸形事件项都会终止该 generation,由 Connection 退避后重开。普通通知按注册顺序运行并隔离 listener 失败;Agent-scoped waterfall 允许 listener 返回结果、调用 `next()` 或拒绝,Gateway 再通过现有 HTTP 一元载体回送该结果。
|
||||
|
||||
@@ -62,7 +66,7 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- Connection 适配器将普通分发故障和业务异常映射为 RPC 的 `internal` 代码,且不附带详细信息;`TypertLookupFailure` 携带的 lookup 策略错误会原样返回。结构化的 `TypertGatewayError` 类别仅供同进程调用方使用。
|
||||
- Connection 适配器对分发故障与未归类异常答以 `gateway/internal`,且不附带详细信息;拥有方或 Gateway 自己抛出的 `RemoteError` 带着自有码、message 与 details 过线。其 `cause` 链与 `TypertGatewayError` 子类身份只对同进程调用方留存。
|
||||
- SRC 模式仅支持名称唯一的标识符参数,不支持解构、默认值或剩余参数。它只校验值能否安全表示为 JSON,不校验生成的业务类型,也绝不会推断可选字段。
|
||||
- Client 侧只能挂载严格模式生成的贡献项。SRC 标记不具备 Client 编解码器或类型投影。
|
||||
- `$stream()` 监督载体替换,但不推断回放语义;各领域自行拥有恢复 cursor 或替换 baseline 的校验,以及正常结束的分类。Connection generation 会重开内部 `$events`;单向通知不会重放,仍处于 pending 的 scoped waterfall 则沿用同一个 event id 重放。
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
import { Service } from '@deepseek-ai/cordis'
|
||||
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
export type { TypertGatewayFaultDetails } from '../remote-error-codes.ts'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {
|
||||
ConnectionHandle,
|
||||
@@ -13,6 +15,7 @@ import type {
|
||||
InvocationDescriptor,
|
||||
TypertClientEventListener,
|
||||
TypertClientRemote,
|
||||
RemoteFailure,
|
||||
RemoteResult,
|
||||
TypertCodec,
|
||||
TypertDisposer,
|
||||
@@ -21,7 +24,6 @@ import type {
|
||||
} from '@deepseek-ai/dsh-typert-protocol'
|
||||
import {
|
||||
RemoteStreamCarrierError,
|
||||
RemoteStreamError,
|
||||
RemoteStreamMuxClient,
|
||||
} from './stream-client.ts'
|
||||
import { ClientRemoteEvents } from './remote-events.ts'
|
||||
@@ -30,7 +32,7 @@ import {
|
||||
type RemoteStreamOptions,
|
||||
} from './remote-stream.ts'
|
||||
|
||||
export { RemoteStreamCarrierError, RemoteStreamError } from './stream-client.ts'
|
||||
export { RemoteStreamCarrierError } from './stream-client.ts'
|
||||
export { RemoteJournalStream } from './journal-stream.ts'
|
||||
export type {
|
||||
RemoteJournalChange,
|
||||
@@ -104,6 +106,20 @@ export interface ClientRemote extends TypertClientRemote {
|
||||
* @returns a single-consumer stream annotated with physical generation ids.
|
||||
*/
|
||||
$stream<Item>(options: RemoteStreamOptions<Item>): RemoteStream<Item>
|
||||
/**
|
||||
* Fixed Host facts as plain reads: no store, no subscription, no generation
|
||||
* counter. `home` stays undefined until the first ready frame and reflects
|
||||
* the latest one afterwards.
|
||||
*/
|
||||
readonly $host: RemoteHostFacts
|
||||
}
|
||||
|
||||
/** The fixed Host facts exposed on `ctx.remote.$host`. */
|
||||
export interface RemoteHostFacts {
|
||||
/** Host home directory from the ready frame, undefined before it. */
|
||||
readonly home: string | undefined
|
||||
/** Whether the carrier connects to the local Host. */
|
||||
readonly isLoopback: boolean
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
@@ -128,6 +144,7 @@ class ClientRemoteService extends Service implements ClientRemote {
|
||||
private readonly ownerCtx: Context
|
||||
private readonly connection: ConnectionHandle
|
||||
private readonly namespaces = new Map<string, RemoteNamespaceHandle>()
|
||||
private hostFacts: RemoteHostFacts | undefined
|
||||
private readonly streams = new RemoteStreamMuxClient()
|
||||
private readonly events: ClientRemoteEvents
|
||||
private mutations = Promise.resolve()
|
||||
@@ -166,6 +183,17 @@ class ClientRemoteService extends Service implements ClientRemote {
|
||||
return new RemoteStream(this.connection, options)
|
||||
}
|
||||
|
||||
get $host(): RemoteHostFacts {
|
||||
// Identity-stable: readers (useSyncExternalStore snapshots, memo inputs)
|
||||
// compare by reference, so a fresh object is minted only when the fact
|
||||
// itself changed. isLoopback is fixed for the page lifetime.
|
||||
const home = this.connection.generation.getSnapshot()?.host.home
|
||||
if (this.hostFacts === undefined || this.hostFacts.home !== home) {
|
||||
this.hostFacts = { home, isLoopback: this.connection.isLoopback }
|
||||
}
|
||||
return this.hostFacts
|
||||
}
|
||||
|
||||
async $mount(contribution: TypertRemoteContribution): ReturnType<TypertClientRemote['$mount']> {
|
||||
const callerCtx = this.ctx
|
||||
const owned = callerCtx.effect(async () => {
|
||||
@@ -410,11 +438,14 @@ class ClientRemoteService extends Service implements ClientRemote {
|
||||
try {
|
||||
const result = await connection.rpc.call('/api', endpoint, { args: prepared.args }, prepared.signal)
|
||||
if (!mountActive(token)) return withdrawn(endpoint)
|
||||
if (!result.ok) return { ok: false, error: result.error }
|
||||
if (!result.ok) return { ok: false, error: rebuiltFailure(result.error) }
|
||||
return { ok: true, value: result.value }
|
||||
} catch (error) {
|
||||
// Carrier throws (offline or abort) are outcomes of the call, not assembly
|
||||
// faults, so they join the same error branch.
|
||||
// faults, so they join the same error branch. A caller-aborted call is a
|
||||
// cancellation even when the local throw wins the race against the wire
|
||||
// round-trip, so it gets the same code the Host would have produced.
|
||||
if (prepared.signal.aborted) return cancelledFailure(endpoint, error)
|
||||
return carrierFailure(endpoint, error)
|
||||
}
|
||||
}
|
||||
@@ -697,8 +728,36 @@ function carrierFailure(endpoint: string, error: unknown): Extract<RemoteResult<
|
||||
return internalFailure(`client api: ${endpoint} failed: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
||||
function cancelledFailure(endpoint: string, cause: unknown): Extract<RemoteResult<never>, { readonly ok: false }> {
|
||||
return {
|
||||
ok: false,
|
||||
error: new RemoteError('gateway/cancelled', `client api: Remote invocation "${endpoint}" was aborted`, {}, { cause }),
|
||||
}
|
||||
}
|
||||
|
||||
function internalFailure(message: string): Extract<RemoteResult<never>, { readonly ok: false }> {
|
||||
return { ok: false, error: { code: 'internal', message, details: {} } }
|
||||
return { ok: false, error: new RemoteError('gateway/internal', message, {}) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a caught value is a Remote failure this face delivered or threw.
|
||||
* The one consumer-facing discrimination point: marked instances carry their
|
||||
* Host code; anything else is a local fault the caller should let crash.
|
||||
* @param error - a caught value.
|
||||
* @returns true when the value narrows to RemoteFailure.
|
||||
*/
|
||||
export function isRemoteFailure(error: unknown): error is RemoteFailure {
|
||||
return remoteErrorOf(error) !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the wire failure as a local RemoteError instance so the error branch
|
||||
* carries a real Error and `throw result.error` keeps throw semantics. The code
|
||||
* is passed through verbatim without runtime validation: a code outside this
|
||||
* Client's merged map still surfaces as-is, so a newer Host stays readable.
|
||||
*/
|
||||
function rebuiltFailure(error: { code: string; message: string; details: object }): RemoteFailure {
|
||||
return new RemoteError(error.code as never, error.message, error.details as never)
|
||||
}
|
||||
|
||||
type MarkedConnectionStreamFailure = Error & {
|
||||
@@ -715,7 +774,7 @@ async function *normalizeConnectionStream(source: AsyncIterable<unknown>): Async
|
||||
if (!(error instanceof Error)) throw error
|
||||
const marker = (error as MarkedConnectionStreamFailure).dshRemoteStreamFailure
|
||||
if (marker?.kind === 'remote') {
|
||||
throw new RemoteStreamError(marker.code, error.message, marker.details)
|
||||
throw new RemoteError(marker.code as never, error.message, marker.details as never)
|
||||
}
|
||||
if (marker?.kind === 'carrier') {
|
||||
throw new RemoteStreamCarrierError(error.message, { cause: error })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Cursor, page, and live-tail coordination over a reconnecting Remote stream. */
|
||||
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteStreamCarrierError } from './stream-client.ts'
|
||||
import type {
|
||||
RemoteStream,
|
||||
@@ -7,6 +8,11 @@ import type {
|
||||
RemoteStreamOptions,
|
||||
} from './remote-stream.ts'
|
||||
|
||||
/** Host-side stream protocol violation, marked so consumers surface it as an error state. */
|
||||
function protocolViolation(message: string): RemoteError<'gateway/internal'> {
|
||||
return new RemoteError('gateway/internal', message, {})
|
||||
}
|
||||
|
||||
/** Transport-neutral opening snapshot or journal entry. */
|
||||
export type RemoteJournalFrame<Entry, Cursor, Page> =
|
||||
| { readonly type: 'opened'; readonly cursor: Cursor; readonly page: Page }
|
||||
@@ -100,7 +106,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
open: signal => this.follow(this.initialRequest, signal),
|
||||
ended: accepted => accepted
|
||||
? new RemoteStreamCarrierError(`${options.name} ended without a terminal result`)
|
||||
: new Error(
|
||||
: protocolViolation(
|
||||
`${this.hasResumeCursor ? 'resumed ' : ''}${options.name} ended before its opening cursor`,
|
||||
),
|
||||
...(options.carrierFailed === undefined
|
||||
@@ -153,7 +159,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
const iterator = this.stream[Symbol.asyncIterator]()
|
||||
try {
|
||||
const first = await this.takeNext(iterator)
|
||||
if (first.done) throw new Error(`${this.options.name} ended before its opening cursor`)
|
||||
if (first.done) throw protocolViolation(`${this.options.name} ended before its opening cursor`)
|
||||
this.replaceGeneration(first.value, false)
|
||||
this.opened = true
|
||||
this.done = this.consume(iterator)
|
||||
@@ -182,7 +188,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
if (tail !== undefined && before !== undefined
|
||||
&& !this.options.follows(this.options.last(tail), before)) {
|
||||
this.options.publish({ type: 'prepend', page, entries: [], hasMore: false })
|
||||
throw new Error(`${this.options.name} history page is discontinuous`)
|
||||
throw protocolViolation(`${this.options.name} history page is discontinuous`)
|
||||
}
|
||||
const first = accepted[0]
|
||||
if (first !== undefined) this.firstCursor = this.options.first(first)
|
||||
@@ -228,7 +234,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
continue
|
||||
}
|
||||
if (item.value.type === 'opened') {
|
||||
throw new Error(`${this.options.name} emitted more than one opening cursor`)
|
||||
throw protocolViolation(`${this.options.name} emitted more than one opening cursor`)
|
||||
}
|
||||
await this.acceptEntry(item.value.entry, item, iterator)
|
||||
}
|
||||
@@ -250,12 +256,12 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
resumed: boolean,
|
||||
): { readonly cursor: Cursor; readonly page: Page } {
|
||||
if (item.value.type !== 'opened') {
|
||||
throw new Error(`${resumed ? 'resumed ' : ''}${this.options.name} emitted an entry before its opening cursor`)
|
||||
throw protocolViolation(`${resumed ? 'resumed ' : ''}${this.options.name} emitted an entry before its opening cursor`)
|
||||
}
|
||||
const cursor = item.value.cursor
|
||||
if (resumed && this.lastCursor !== undefined
|
||||
&& this.options.compare(cursor, this.lastCursor) < 0) {
|
||||
throw new Error(
|
||||
throw protocolViolation(
|
||||
`${this.options.name} resumed at a cursor behind the last applied entry`,
|
||||
)
|
||||
}
|
||||
@@ -290,7 +296,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
const last = this.lastCursor as Cursor
|
||||
if (this.options.compare(cursor, last) <= 0) return
|
||||
if (this.options.compare(first, last) <= 0) {
|
||||
throw new Error(`${this.options.name} emitted a partially overlapping entry`)
|
||||
throw protocolViolation(`${this.options.name} emitted a partially overlapping entry`)
|
||||
}
|
||||
if (!this.options.follows(last, first)) {
|
||||
const request = this.repairPageRequest()
|
||||
@@ -350,7 +356,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
target = this.maxCursor(requiredCursor, queued)
|
||||
}
|
||||
if (entries === undefined || this.options.compare(this.tailCursor(entries), target) < 0) {
|
||||
throw new Error(`${this.options.name} page did not reach its opening cursor`)
|
||||
throw protocolViolation(`${this.options.name} page did not reach its opening cursor`)
|
||||
}
|
||||
const first = entries[0]
|
||||
/* v8 ignore next -- a successful positive-cursor replacement page cannot be empty. */
|
||||
@@ -400,12 +406,12 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
if (result.type === 'next-error') throw result.error
|
||||
if (result.value.done) {
|
||||
signal.throwIfAborted()
|
||||
throw new Error(`${this.options.name} ended while reading its replacement page`)
|
||||
throw protocolViolation(`${this.options.name} ended while reading its replacement page`)
|
||||
}
|
||||
const item = result.value.value
|
||||
if (item.generation !== generation) return { type: 'superseded', item }
|
||||
if (item.value.type === 'opened') {
|
||||
throw new Error(`${this.options.name} emitted more than one opening cursor`)
|
||||
throw protocolViolation(`${this.options.name} emitted more than one opening cursor`)
|
||||
}
|
||||
queued.push(item.value.entry)
|
||||
}
|
||||
@@ -426,12 +432,12 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
}
|
||||
if (next.done) {
|
||||
this.stream.signal.throwIfAborted()
|
||||
throw new Error(`${this.options.name} ended while replacing an aborted page generation`)
|
||||
throw protocolViolation(`${this.options.name} ended while replacing an aborted page generation`)
|
||||
}
|
||||
const item = next.value
|
||||
if (item.generation !== generation) return { type: 'superseded', item }
|
||||
if (item.value.type === 'opened') {
|
||||
throw new Error(`${this.options.name} emitted more than one opening cursor`)
|
||||
throw protocolViolation(`${this.options.name} emitted more than one opening cursor`)
|
||||
}
|
||||
pending = this.nextResult(iterator)
|
||||
}
|
||||
@@ -450,7 +456,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
const last = this.options.last(entry)
|
||||
if (this.options.compare(last, tail) <= 0) continue
|
||||
if (this.options.compare(first, tail) <= 0) {
|
||||
throw new Error(`${this.options.name} replacement contains a partially overlapping entry`)
|
||||
throw protocolViolation(`${this.options.name} replacement contains a partially overlapping entry`)
|
||||
}
|
||||
if (!this.options.follows(tail, first)) return undefined
|
||||
entries.push(entry)
|
||||
@@ -516,7 +522,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
for (const entry of iterator) {
|
||||
const range = this.entryRange(entry)
|
||||
if (!this.options.follows(previousRange.last, range.first)) {
|
||||
throw new Error(`${this.options.name} page contains discontinuous entries`)
|
||||
throw protocolViolation(`${this.options.name} page contains discontinuous entries`)
|
||||
}
|
||||
previousRange = range
|
||||
}
|
||||
@@ -526,7 +532,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
const first = this.options.first(entry)
|
||||
const last = this.options.last(entry)
|
||||
if (this.options.compare(first, last) > 0) {
|
||||
throw new Error(`${this.options.name} entry has an inverted cursor range`)
|
||||
throw protocolViolation(`${this.options.name} entry has an inverted cursor range`)
|
||||
}
|
||||
return { first, last }
|
||||
}
|
||||
@@ -534,7 +540,7 @@ export abstract class RemoteJournalStream<Page, Entry, Cursor, PageRequest = voi
|
||||
private assertPageThrough(page: Page, through: Cursor): void {
|
||||
const tail = this.tailCursor(this.options.entries(page))
|
||||
if (this.options.compare(tail, through) !== 0) {
|
||||
throw new Error(`${this.options.name} page did not end at its requested cursor`)
|
||||
throw protocolViolation(`${this.options.name} page did not end at its requested cursor`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Reconnecting lifecycle for one single-consumer Remote stream. */
|
||||
|
||||
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { RemoteStreamCarrierError } from './stream-client.ts'
|
||||
|
||||
@@ -128,7 +129,7 @@ export class RemoteStream<Item> implements AsyncIterable<RemoteStreamItem<Item>>
|
||||
} catch (error) {
|
||||
if (isAborted(this.lifetime.signal)) return
|
||||
if (revision !== this.revision) continue
|
||||
if (!(error instanceof RemoteStreamCarrierError)) throw error
|
||||
if (!(error instanceof RemoteStreamCarrierError)) throw terminalStreamFailure(error)
|
||||
this.options.carrierFailed?.(error)
|
||||
if (revision !== this.revision) continue
|
||||
attempt++
|
||||
@@ -137,7 +138,7 @@ export class RemoteStream<Item> implements AsyncIterable<RemoteStreamItem<Item>>
|
||||
} catch (retryError) {
|
||||
if (isAborted(this.lifetime.signal)) return
|
||||
if (revision !== this.revision) continue
|
||||
throw retryError
|
||||
throw terminalStreamFailure(retryError)
|
||||
}
|
||||
} finally {
|
||||
this.generationAbort = undefined
|
||||
@@ -195,6 +196,22 @@ async function waitForRemoteStreamRetry(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a terminal escape before it crosses the stream boundary: consumers
|
||||
* discriminate failures by code, so an unmarked throw reads as a local bug.
|
||||
* Marked failures pass through verbatim. The carrier class never escapes as a
|
||||
* terminal outcome — it stays the retry-internal signal fed to `carrierFailed`
|
||||
* and the `ended(true)` retry trigger.
|
||||
*/
|
||||
function terminalStreamFailure(error: unknown): Error {
|
||||
return remoteErrorOf(error) ?? new RemoteError(
|
||||
'gateway/internal',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
{},
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
|
||||
function isAborted(signal: AbortSignal): boolean {
|
||||
return signal.aborted
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/** Baseline-and-delta protocol layered over a reconnecting Remote stream. */
|
||||
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { RemoteStream } from './remote-stream.ts'
|
||||
|
||||
/** Host-side stream protocol violation, marked so consumers surface it as an error state. */
|
||||
function protocolViolation(message: string): RemoteError<'gateway/internal'> {
|
||||
return new RemoteError('gateway/internal', message, {})
|
||||
}
|
||||
|
||||
/** Domain operations for one snapshot stream. */
|
||||
export interface RemoteSnapshotStreamOptions<Snapshot, Delta> {
|
||||
/** Diagnostic stream name used in protocol failures. */
|
||||
@@ -69,7 +75,7 @@ export class RemoteSnapshotStream<Snapshot, Delta> {
|
||||
}
|
||||
if (this.options.isSnapshot(item.value)) {
|
||||
if (snapshotSeen) {
|
||||
throw new Error(`${this.options.name} emitted more than one opening snapshot`)
|
||||
throw protocolViolation(`${this.options.name} emitted more than one opening snapshot`)
|
||||
}
|
||||
this.options.replace(item.value)
|
||||
snapshotSeen = true
|
||||
@@ -77,7 +83,7 @@ export class RemoteSnapshotStream<Snapshot, Delta> {
|
||||
continue
|
||||
}
|
||||
if (!snapshotSeen) {
|
||||
throw new Error(`${this.options.name} emitted an update before its opening snapshot`)
|
||||
throw protocolViolation(`${this.options.name} emitted an update before its opening snapshot`)
|
||||
}
|
||||
this.options.update(item.value)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
/** Browser owner for the Gateway multiplexed Remote stream socket. */
|
||||
|
||||
import {
|
||||
@@ -14,25 +15,6 @@ const RECONNECT_FACTOR = 2
|
||||
const RECONNECT_MAX_MS = 10_000
|
||||
|
||||
/** One Host-reported Remote stream failure. */
|
||||
export class RemoteStreamError extends Error {
|
||||
/** Stable carrier or Gateway error category. */
|
||||
readonly code: string
|
||||
/** Host-provided structured failure context. */
|
||||
readonly details: object
|
||||
|
||||
/**
|
||||
* @param code - stable Gateway or business error category.
|
||||
* @param message - Host-provided failure description.
|
||||
* @param details - Host-provided structured failure context.
|
||||
*/
|
||||
constructor(code: string, message: string, details: object) {
|
||||
super(message)
|
||||
this.name = 'RemoteStreamError'
|
||||
this.code = code
|
||||
this.details = details
|
||||
}
|
||||
}
|
||||
|
||||
/** Physical Remote stream socket failure that may be retried by a domain transport. */
|
||||
export class RemoteStreamCarrierError extends Error {
|
||||
/**
|
||||
@@ -105,7 +87,7 @@ export class RemoteStreamMuxClient {
|
||||
}
|
||||
terminal = true
|
||||
if (frame.type === 'error') {
|
||||
throw new RemoteStreamError(frame.error.code, frame.error.message, frame.error.details)
|
||||
throw new RemoteError(frame.error.code as never, frame.error.message, frame.error.details as never)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
|
||||
import type { WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
export type { TypertGatewayFaultDetails } from './remote-error-codes.ts'
|
||||
import {
|
||||
RemoteError,
|
||||
remoteErrorOf,
|
||||
remoteMethods,
|
||||
TypertLookupFailure,
|
||||
TypertRemoteFailure,
|
||||
type InvocationDescriptor,
|
||||
type InvocationParameterDescriptor,
|
||||
type TypertCodec,
|
||||
@@ -123,10 +124,12 @@ interface ResolvedConfig extends Config {
|
||||
readonly websocketHeartbeatIntervalMs: number
|
||||
}
|
||||
|
||||
/** Dispatch failure produced outside the invoked business method. */
|
||||
export class TypertGatewayError extends Error {
|
||||
/** Machine-readable failure category. */
|
||||
readonly code: TypertGatewayErrorCode
|
||||
/**
|
||||
* Dispatch failure produced outside the invoked business method. Rides the
|
||||
* shared Remote failure vocabulary, so its code crosses the wire instead of
|
||||
* folding to `internal`.
|
||||
*/
|
||||
export class TypertGatewayError extends RemoteError<TypertGatewayErrorCode> {
|
||||
/** Canonical `<namespace>/<method>` endpoint. */
|
||||
readonly endpoint: string
|
||||
/** Affected wire field when the failure is field-specific. */
|
||||
@@ -145,26 +148,18 @@ export class TypertGatewayError extends Error {
|
||||
message: string,
|
||||
options: GatewayErrorOptions = {},
|
||||
) {
|
||||
super(`typert gateway: ${endpoint}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause })
|
||||
super(
|
||||
code,
|
||||
`typert gateway: ${endpoint}: ${message}`,
|
||||
{ endpoint, ...options.field === undefined ? {} : { field: options.field } },
|
||||
options.cause === undefined ? undefined : { cause: options.cause },
|
||||
)
|
||||
this.name = 'TypertGatewayError'
|
||||
this.code = code
|
||||
this.endpoint = endpoint
|
||||
this.field = options.field
|
||||
}
|
||||
}
|
||||
|
||||
/** Business invocation lost its carrier cancellation race. */
|
||||
class RemoteInvocationCancelled extends Error {
|
||||
/**
|
||||
* @param endpoint - canonical Remote endpoint.
|
||||
* @param cause - business rejection observed after carrier cancellation.
|
||||
*/
|
||||
constructor(endpoint: string, cause: unknown) {
|
||||
super(`Remote invocation "${endpoint}" was aborted`, { cause })
|
||||
this.name = 'RemoteInvocationCancelled'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve strict generated definitions or conservative SRC markers against
|
||||
* current Cordis Services and Typert providers.
|
||||
@@ -303,7 +298,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const prepared = await this.prepareInvocation(request)
|
||||
if (prepared.descriptor.mode === 'stream') {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
'gateway/signature-invalid',
|
||||
prepared.endpoint,
|
||||
'stream Remote methods must be opened through the stream carrier',
|
||||
)
|
||||
@@ -312,7 +307,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
try {
|
||||
return await Reflect.apply(prepared.method, prepared.receiver, prepared.args) as unknown
|
||||
} catch (error) {
|
||||
if (request.signal?.aborted === true) throw new RemoteInvocationCancelled(prepared.endpoint, error)
|
||||
if (request.signal?.aborted === true) throw remoteCancelled(prepared.endpoint, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -326,7 +321,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const prepared = await this.prepareInvocation(request)
|
||||
if (prepared.descriptor.mode !== 'stream') {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
'gateway/signature-invalid',
|
||||
prepared.endpoint,
|
||||
'unary Remote methods cannot be opened through the stream carrier',
|
||||
)
|
||||
@@ -335,12 +330,12 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
try {
|
||||
source = Reflect.apply(prepared.method, prepared.receiver, prepared.args) as unknown
|
||||
} catch (error) {
|
||||
if (request.signal?.aborted === true) throw new RemoteInvocationCancelled(prepared.endpoint, error)
|
||||
if (request.signal?.aborted === true) throw remoteCancelled(prepared.endpoint, error)
|
||||
throw error
|
||||
}
|
||||
if (!isIterable(source)) {
|
||||
throw new TypertGatewayError(
|
||||
'result-invalid',
|
||||
'gateway/result-invalid',
|
||||
prepared.endpoint,
|
||||
'stream Remote method did not return Iterable or AsyncIterable',
|
||||
{ field: 'result' },
|
||||
@@ -400,7 +395,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
|| !isPlainObject(payload.args)
|
||||
|| Reflect.ownKeys(payload.args).length !== 0) {
|
||||
throw new TypertGatewayError(
|
||||
'arguments-invalid',
|
||||
'gateway/arguments-invalid',
|
||||
REMOTE_EVENT_STREAM_ENDPOINT,
|
||||
'forwarded Remote event stream requires an empty args object',
|
||||
)
|
||||
@@ -408,7 +403,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const registration = this.remoteEvents
|
||||
if (registration === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'service-unavailable',
|
||||
'gateway/service-unavailable',
|
||||
REMOTE_EVENT_STREAM_ENDPOINT,
|
||||
'forwarded Remote event source is unavailable',
|
||||
)
|
||||
@@ -611,7 +606,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const receiver = receiverContext.get(descriptor.service) as unknown
|
||||
if (!isObject(receiver)) {
|
||||
throw new TypertGatewayError(
|
||||
'service-unavailable',
|
||||
'gateway/service-unavailable',
|
||||
endpoint,
|
||||
`active Service ${JSON.stringify(descriptor.service)} is unavailable`,
|
||||
)
|
||||
@@ -624,7 +619,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const method = Reflect.get(receiver, implementation) as unknown
|
||||
if (typeof method !== 'function') {
|
||||
throw new TypertGatewayError(
|
||||
'method-unavailable',
|
||||
'gateway/method-unavailable',
|
||||
endpoint,
|
||||
`active Service ${JSON.stringify(descriptor.service)} has no callable method ${JSON.stringify(implementation)}`,
|
||||
)
|
||||
@@ -637,7 +632,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
if (strict !== undefined) return strict
|
||||
if (this.ctx.typert.local.hasSeen(endpoint)) {
|
||||
throw new TypertGatewayError(
|
||||
'definition-unavailable',
|
||||
'gateway/definition-unavailable',
|
||||
endpoint,
|
||||
'its strict definition was withdrawn and SRC fallback is forbidden',
|
||||
)
|
||||
@@ -661,11 +656,11 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
candidates.push(this.srcDescriptor(binding, marker, method, endpoint))
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
throw new TypertGatewayError('invocation-unavailable', endpoint, 'no active Remote method exports this endpoint')
|
||||
throw new TypertGatewayError('gateway/invocation-unavailable', endpoint, 'no active Remote method exports this endpoint')
|
||||
}
|
||||
if (candidates.length > 1) {
|
||||
throw new TypertGatewayError(
|
||||
'ambiguous-endpoint',
|
||||
'gateway/ambiguous-endpoint',
|
||||
endpoint,
|
||||
`multiple active Services export this endpoint: ${candidates.map(candidate => candidate.service).sort().join(', ')}`,
|
||||
)
|
||||
@@ -683,7 +678,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const signalIndex = names.indexOf('signal')
|
||||
if (signalIndex >= 0 && signalIndex !== names.length - 1) {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
'gateway/signature-invalid',
|
||||
endpoint,
|
||||
'SRC cancellation parameter signal must be the final parameter',
|
||||
{ field: 'signal' },
|
||||
@@ -700,7 +695,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
.filter(definition => definition.parameter === name)
|
||||
if (matches.length > 1) {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
'gateway/signature-invalid',
|
||||
endpoint,
|
||||
`parameter ${JSON.stringify(name)} matches multiple lookup providers`,
|
||||
{ field: name },
|
||||
@@ -718,7 +713,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
}
|
||||
if (wires.has(parameter.wire)) {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
'gateway/signature-invalid',
|
||||
endpoint,
|
||||
`multiple parameters use wire field ${JSON.stringify(parameter.wire)}`,
|
||||
{ field: parameter.wire },
|
||||
@@ -733,14 +728,14 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const provider = this.ctx.typert.contexts.getHost(marker.invocation.context)
|
||||
if (provider === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'context-unavailable',
|
||||
'gateway/context-unavailable',
|
||||
endpoint,
|
||||
`Context provider ${JSON.stringify(marker.invocation.context)} is unavailable`,
|
||||
)
|
||||
}
|
||||
if (wires.has(provider.wire)) {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
'gateway/signature-invalid',
|
||||
endpoint,
|
||||
`Context identity conflicts with wire field ${JSON.stringify(provider.wire)}`,
|
||||
{ field: provider.wire },
|
||||
@@ -778,7 +773,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const provider = this.ctx.typert.contexts.getHost(invocation.context)
|
||||
if (provider === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'context-unavailable',
|
||||
'gateway/context-unavailable',
|
||||
endpoint,
|
||||
`Context provider ${JSON.stringify(invocation.context)} is unavailable`,
|
||||
)
|
||||
@@ -786,7 +781,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
if (provider.wire !== invocation.wire
|
||||
|| (invocation.codec.mode === 'strict' && provider.wireTypeSymbol !== invocation.codec.typeSymbol)) {
|
||||
throw new TypertGatewayError(
|
||||
'provider-mismatch',
|
||||
'gateway/provider-mismatch',
|
||||
endpoint,
|
||||
`Context provider ${JSON.stringify(invocation.context)} does not match its strict definition`,
|
||||
{ field: invocation.wire },
|
||||
@@ -797,9 +792,9 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
try {
|
||||
context = await provider.resolve(identity)
|
||||
} catch (cause) {
|
||||
if (cause instanceof TypertLookupFailure) throw cause
|
||||
if (remoteErrorOf(cause) !== undefined) throw cause
|
||||
throw new TypertGatewayError(
|
||||
'context-failed',
|
||||
'gateway/context-failed',
|
||||
endpoint,
|
||||
`Context provider ${JSON.stringify(invocation.context)} failed`,
|
||||
{ cause, field: invocation.wire },
|
||||
@@ -807,7 +802,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
}
|
||||
if (context === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'context-not-found',
|
||||
'gateway/context-not-found',
|
||||
endpoint,
|
||||
`Context provider ${JSON.stringify(invocation.context)} did not resolve the requested identity`,
|
||||
{ field: invocation.wire },
|
||||
@@ -832,7 +827,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
/* v8 ignore next -- registry validation rejects strict descriptors without a key, and SRC derivation always supplies one. */
|
||||
if (key === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'lookup-unavailable',
|
||||
'gateway/lookup-unavailable',
|
||||
endpoint,
|
||||
`lookup parameter ${JSON.stringify(parameter.name)} has no provider key`,
|
||||
{ field: parameter.wire },
|
||||
@@ -841,7 +836,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
const provider = this.ctx.typert.lookups.get(key)
|
||||
if (provider === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'lookup-unavailable',
|
||||
'gateway/lookup-unavailable',
|
||||
endpoint,
|
||||
`lookup provider ${JSON.stringify(key)} is unavailable`,
|
||||
{ field: parameter.wire },
|
||||
@@ -850,7 +845,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
if (provider.wire !== parameter.wire
|
||||
|| (parameter.codec.mode === 'strict' && provider.wireTypeSymbol !== parameter.codec.typeSymbol)) {
|
||||
throw new TypertGatewayError(
|
||||
'provider-mismatch',
|
||||
'gateway/provider-mismatch',
|
||||
endpoint,
|
||||
`lookup provider ${JSON.stringify(key)} does not match its strict definition`,
|
||||
{ field: parameter.wire },
|
||||
@@ -860,9 +855,9 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
try {
|
||||
resolved = await provider.resolve(value)
|
||||
} catch (cause) {
|
||||
if (cause instanceof TypertLookupFailure) throw cause
|
||||
if (remoteErrorOf(cause) !== undefined) throw cause
|
||||
throw new TypertGatewayError(
|
||||
'lookup-failed',
|
||||
'gateway/lookup-failed',
|
||||
endpoint,
|
||||
`lookup provider ${JSON.stringify(key)} failed`,
|
||||
{ cause, field: parameter.wire },
|
||||
@@ -870,7 +865,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
|
||||
}
|
||||
if (resolved === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'lookup-not-found',
|
||||
'gateway/lookup-not-found',
|
||||
endpoint,
|
||||
`lookup provider ${JSON.stringify(key)} did not resolve the requested identity`,
|
||||
{ field: parameter.wire },
|
||||
@@ -978,11 +973,11 @@ async function *cancellableStream(
|
||||
let rejectAbort: ((error: unknown) => void) | undefined
|
||||
const aborted = new Promise<never>((_resolve, reject) => { rejectAbort = reject })
|
||||
const onAbort = (): void => {
|
||||
rejectAbort?.(new RemoteInvocationCancelled(endpoint, signal.reason))
|
||||
rejectAbort?.(remoteCancelled(endpoint, signal.reason))
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
try {
|
||||
if (signal.aborted) throw new RemoteInvocationCancelled(endpoint, signal.reason)
|
||||
if (signal.aborted) throw remoteCancelled(endpoint, signal.reason)
|
||||
while (true) {
|
||||
const next = await Promise.race([Promise.resolve(iterator.next()), aborted])
|
||||
if (next.done === true) return
|
||||
@@ -994,23 +989,20 @@ async function *cancellableStream(
|
||||
}
|
||||
}
|
||||
|
||||
/** Carrier-signal cancellation as the shared failure vocabulary expresses it. */
|
||||
function remoteCancelled(endpoint: string, cause: unknown): RemoteError<'gateway/cancelled'> {
|
||||
return new RemoteError('gateway/cancelled', `Remote invocation "${endpoint}" was aborted`, {}, { cause })
|
||||
}
|
||||
|
||||
function rpcFailure(error: unknown): ConnectionRpcResult {
|
||||
if (error instanceof RemoteInvocationCancelled) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { code: 'cancelled', message: error.message, details: {} },
|
||||
}
|
||||
}
|
||||
if (error instanceof TypertLookupFailure) {
|
||||
return { ok: false, error: error.failure as ConnectionRpcError }
|
||||
}
|
||||
if (error instanceof TypertRemoteFailure) {
|
||||
return { ok: false, error: error.failure }
|
||||
const remote = remoteErrorOf(error)
|
||||
if (remote !== undefined) {
|
||||
return { ok: false, error: { code: remote.code, message: remote.message, details: remote.details } }
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'internal',
|
||||
code: 'gateway/internal',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
details: {},
|
||||
},
|
||||
@@ -1035,7 +1027,7 @@ function validateBinding(
|
||||
const value = Reflect.get(original, 'typertRemote') as unknown
|
||||
if (value === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'binding-invalid',
|
||||
'gateway/binding-invalid',
|
||||
endpoint,
|
||||
`Service ${JSON.stringify(serviceKey)} has no visible typertRemote binding`,
|
||||
)
|
||||
@@ -1059,7 +1051,7 @@ function readBinding(
|
||||
|| typeof Reflect.get(value, 'namespace') !== 'string'
|
||||
|| (namespace !== undefined && Reflect.get(value, 'namespace') !== namespace)) {
|
||||
throw new TypertGatewayError(
|
||||
'binding-invalid',
|
||||
'gateway/binding-invalid',
|
||||
endpoint,
|
||||
`Service ${JSON.stringify(serviceKey)} has an inconsistent typertRemote binding`,
|
||||
)
|
||||
@@ -1087,7 +1079,7 @@ function methodParameterNames(service: object, method: string, endpoint: string)
|
||||
}
|
||||
if (implementation === undefined) {
|
||||
throw new TypertGatewayError(
|
||||
'method-unavailable',
|
||||
'gateway/method-unavailable',
|
||||
endpoint,
|
||||
`Remote marker has no prototype method ${JSON.stringify(method)}`,
|
||||
)
|
||||
@@ -1110,7 +1102,7 @@ function methodParameterNames(service: object, method: string, endpoint: string)
|
||||
|
||||
function invalidSignature(endpoint: string, method: string): never {
|
||||
throw new TypertGatewayError(
|
||||
'signature-invalid',
|
||||
'gateway/signature-invalid',
|
||||
endpoint,
|
||||
`SRC method ${JSON.stringify(method)} must use unique identifier parameters without destructuring, defaults, or rest`,
|
||||
)
|
||||
@@ -1122,7 +1114,7 @@ function assertExactArguments(
|
||||
endpoint: string,
|
||||
): void {
|
||||
if (!isPlainObject(args)) {
|
||||
throw new TypertGatewayError('arguments-invalid', endpoint, 'args must be a plain object')
|
||||
throw new TypertGatewayError('gateway/arguments-invalid', endpoint, 'args must be a plain object')
|
||||
}
|
||||
const expected = new Set(descriptor.parameters.map(parameter => parameter.wire))
|
||||
if (descriptor.invocation.kind === 'context') expected.add(descriptor.invocation.wire)
|
||||
@@ -1141,7 +1133,7 @@ function assertExactArguments(
|
||||
const clauses: string[] = []
|
||||
if (missing.length > 0) clauses.push(`missing ${missing.map(key => JSON.stringify(key)).join(', ')}`)
|
||||
if (extra.length > 0) clauses.push(`unexpected ${extra.map(key => JSON.stringify(String(key))).join(', ')}`)
|
||||
throw new TypertGatewayError('arguments-invalid', endpoint, `args fields do not match the descriptor: ${clauses.join('; ')}`)
|
||||
throw new TypertGatewayError('gateway/arguments-invalid', endpoint, `args fields do not match the descriptor: ${clauses.join('; ')}`)
|
||||
}
|
||||
|
||||
function decode(
|
||||
@@ -1160,7 +1152,7 @@ function decode(
|
||||
return value
|
||||
} catch (cause) {
|
||||
throw new TypertGatewayError(
|
||||
'input-invalid',
|
||||
'gateway/input-invalid',
|
||||
endpoint,
|
||||
`wire field ${JSON.stringify(field)} failed boundary validation`,
|
||||
{ cause, field },
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Gateway infrastructure failure codes merged into the shared Remote failure
|
||||
* vocabulary. Face-neutral: the Host face and the Client face each import this
|
||||
* module so both programs see the same map entries.
|
||||
*/
|
||||
|
||||
/** Wire details every Gateway infrastructure failure carries. */
|
||||
export interface TypertGatewayFaultDetails {
|
||||
/** Canonical `<namespace>/<method>` endpoint. */
|
||||
readonly endpoint: string
|
||||
/** Affected wire field when the failure is field-specific. */
|
||||
readonly field?: string
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
'gateway/ambiguous-endpoint': TypertGatewayFaultDetails
|
||||
'gateway/arguments-invalid': TypertGatewayFaultDetails
|
||||
'gateway/binding-invalid': TypertGatewayFaultDetails
|
||||
'gateway/context-failed': TypertGatewayFaultDetails
|
||||
'gateway/context-not-found': TypertGatewayFaultDetails
|
||||
'gateway/context-unavailable': TypertGatewayFaultDetails
|
||||
'gateway/definition-unavailable': TypertGatewayFaultDetails
|
||||
'gateway/input-invalid': TypertGatewayFaultDetails
|
||||
'gateway/invocation-unavailable': TypertGatewayFaultDetails
|
||||
'gateway/lookup-failed': TypertGatewayFaultDetails
|
||||
'gateway/lookup-not-found': TypertGatewayFaultDetails
|
||||
'gateway/lookup-unavailable': TypertGatewayFaultDetails
|
||||
'gateway/method-unavailable': TypertGatewayFaultDetails
|
||||
'gateway/provider-mismatch': TypertGatewayFaultDetails
|
||||
'gateway/result-invalid': TypertGatewayFaultDetails
|
||||
'gateway/service-unavailable': TypertGatewayFaultDetails
|
||||
'gateway/signature-invalid': TypertGatewayFaultDetails
|
||||
}
|
||||
}
|
||||
@@ -99,23 +99,23 @@ export interface TypertGatewayWireStream {
|
||||
|
||||
/** Stable infrastructure and boundary failures emitted before or after business execution. */
|
||||
export 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'
|
||||
| 'gateway/ambiguous-endpoint'
|
||||
| 'gateway/arguments-invalid'
|
||||
| 'gateway/binding-invalid'
|
||||
| 'gateway/context-failed'
|
||||
| 'gateway/context-not-found'
|
||||
| 'gateway/context-unavailable'
|
||||
| 'gateway/definition-unavailable'
|
||||
| 'gateway/input-invalid'
|
||||
| 'gateway/invocation-unavailable'
|
||||
| 'gateway/lookup-failed'
|
||||
| 'gateway/lookup-not-found'
|
||||
| 'gateway/lookup-unavailable'
|
||||
| 'gateway/method-unavailable'
|
||||
| 'gateway/provider-mismatch'
|
||||
| 'gateway/result-invalid'
|
||||
| 'gateway/service-unavailable'
|
||||
| 'gateway/signature-invalid'
|
||||
|
||||
/** Host dispatcher consumed by Connection adapters. */
|
||||
export interface TypertGateway {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import {
|
||||
RemoteStreamCarrierError,
|
||||
@@ -106,11 +107,42 @@ describe('RemoteStream', () => {
|
||||
{ terminal: repeated },
|
||||
], carrierFailed)
|
||||
|
||||
await expect(stream[Symbol.asyncIterator]().next()).rejects.toBe(repeated)
|
||||
await expect(stream[Symbol.asyncIterator]().next()).rejects.toMatchObject({
|
||||
isDSHRemoteError: true,
|
||||
code: 'gateway/internal',
|
||||
message: 'isolated retry failed',
|
||||
details: {},
|
||||
cause: repeated,
|
||||
})
|
||||
expect(carrierFailed).toHaveBeenNthCalledWith(1, first)
|
||||
expect(carrierFailed).toHaveBeenNthCalledWith(2, repeated)
|
||||
})
|
||||
|
||||
it('folds a non-Error terminal escape into a marked gateway/internal failure', async () => {
|
||||
const stream = new RemoteStream(hostSource(true).connection, {
|
||||
name: 'fixture stream',
|
||||
open: () => ({
|
||||
[Symbol.asyncIterator]: (): AsyncIterator<string> => ({
|
||||
next: vi.fn<() => Promise<IteratorResult<string>>>().mockRejectedValue('generation exploded'),
|
||||
}),
|
||||
}),
|
||||
ended: () => new Error('fixture stream ended'),
|
||||
})
|
||||
|
||||
await expect(stream[Symbol.asyncIterator]().next()).rejects.toMatchObject({
|
||||
isDSHRemoteError: true,
|
||||
code: 'gateway/internal',
|
||||
message: 'generation exploded',
|
||||
})
|
||||
})
|
||||
|
||||
it('passes a marked Remote failure through the terminal boundary verbatim', async () => {
|
||||
const failure = new RemoteError('gateway/internal', 'host stream failed', {})
|
||||
const stream = supervisor(hostSource(true).connection, [{ terminal: failure }])
|
||||
|
||||
await expect(stream[Symbol.asyncIterator]().next()).rejects.toBe(failure)
|
||||
})
|
||||
|
||||
it('waits for a replacement Host generation after observing unavailability', async () => {
|
||||
let available = false
|
||||
let listener: (() => void) | undefined
|
||||
|
||||
@@ -12,9 +12,16 @@ import {
|
||||
type InvocationDescriptor,
|
||||
type TypertContextMap,
|
||||
type TypertContextWire,
|
||||
TypertRemoteFailure,
|
||||
RemoteError,
|
||||
} from '@deepseek-ai/dsh-typert-protocol'
|
||||
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
'fixture/rejected': { readonly retryable: boolean }
|
||||
'fixture/broken': { readonly count: bigint }
|
||||
}
|
||||
}
|
||||
import { provideBrowserCredentials } from './browser-credentials.ts'
|
||||
import TypertGatewayService, {
|
||||
TypertGatewayError,
|
||||
@@ -118,16 +125,12 @@ class FeedService extends Service {
|
||||
|
||||
@Remote({ mode: 'stream' })
|
||||
reject(): Iterable<string> {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'fixture-rejected', message: 'fixture rejected the stream', details: { retryable: false },
|
||||
})
|
||||
throw new RemoteError('fixture/rejected', 'fixture rejected the stream', { retryable: false })
|
||||
}
|
||||
|
||||
@Remote({ mode: 'stream' })
|
||||
rejectWithNonJsonDetails(): Iterable<string> {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'fixture-broken', message: 'fixture emitted invalid details', details: { count: 1n },
|
||||
})
|
||||
throw new RemoteError('fixture/broken', 'fixture emitted invalid details', { count: 1n })
|
||||
}
|
||||
|
||||
unary(label: string): string {
|
||||
@@ -262,7 +265,7 @@ describe('Typert Remote streams', () => {
|
||||
}))).resolves.toEqual([1n])
|
||||
await expect(ctx.typertGateway.stream({
|
||||
namespace: 'feed', method: 'missing', args: {},
|
||||
})).rejects.toMatchObject({ code: 'result-invalid' })
|
||||
})).rejects.toMatchObject({ code: 'gateway/result-invalid' })
|
||||
|
||||
await expect(collect(await ctx.typertGateway.stream({
|
||||
namespace: 'feed', method: 'src', args: { label: 'c' },
|
||||
@@ -286,10 +289,10 @@ describe('Typert Remote streams', () => {
|
||||
const { ctx } = await setup(false)
|
||||
await expect(ctx.typertGateway.invoke({
|
||||
namespace: 'feed', method: 'sync', args: { label: 'a' },
|
||||
})).rejects.toMatchObject({ code: 'signature-invalid' } satisfies Partial<TypertGatewayError>)
|
||||
})).rejects.toMatchObject({ code: 'gateway/signature-invalid' } satisfies Partial<TypertGatewayError>)
|
||||
await expect(ctx.typertGateway.stream({
|
||||
namespace: 'feed', method: 'unary', args: { label: 'a' },
|
||||
})).rejects.toMatchObject({ code: 'signature-invalid' } satisfies Partial<TypertGatewayError>)
|
||||
})).rejects.toMatchObject({ code: 'gateway/signature-invalid' } satisfies Partial<TypertGatewayError>)
|
||||
})
|
||||
|
||||
it('uses the configured WebSocket heartbeat interval', { timeout: 1_000 }, async () => {
|
||||
@@ -345,13 +348,13 @@ describe('Typert Remote streams', () => {
|
||||
{ type: 'end', streamId: 'invalid' },
|
||||
])
|
||||
expect(frames.find(frame => frame.streamId === 'non-json')).toMatchObject({
|
||||
type: 'error', error: { code: 'internal' },
|
||||
type: 'error', error: { code: 'gateway/internal' },
|
||||
})
|
||||
expect(frames.find(frame => frame.streamId === 'rejected')).toEqual({
|
||||
type: 'error',
|
||||
streamId: 'rejected',
|
||||
error: {
|
||||
code: 'fixture-rejected',
|
||||
code: 'fixture/rejected',
|
||||
message: 'fixture rejected the stream',
|
||||
details: { retryable: false },
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { Context, Service } from '@deepseek-ai/cordis'
|
||||
import type { Fiber } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
apply as applyConnection,
|
||||
type ConnectionGeneration,
|
||||
type ConnectionGenerationSource,
|
||||
type ConnectionHandle,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
@@ -22,7 +24,6 @@ import type { ClientRemote } from '../src/client/index.ts'
|
||||
import { apply, inject, RemoteStream } from '../src/client/index.ts'
|
||||
import {
|
||||
RemoteStreamCarrierError,
|
||||
RemoteStreamError,
|
||||
RemoteStreamMuxClient,
|
||||
} from '../src/client/stream-client.ts'
|
||||
|
||||
@@ -548,6 +549,41 @@ describe('Client Remote transport readiness', () => {
|
||||
await client.dispose()
|
||||
})
|
||||
|
||||
it('reports Host facts as plain reads and keeps them through Connection withdrawal', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(TypertRegistry)
|
||||
const generation = new GenerationHarness()
|
||||
const live: { snapshot: ConnectionGeneration | undefined } = { snapshot: undefined }
|
||||
const handle = {
|
||||
isLoopback: true,
|
||||
generation: { getSnapshot: () => live.snapshot, subscribe: () => () => {} },
|
||||
rpc: {
|
||||
call: vi.fn<ConnectionHandle['rpc']['call']>(),
|
||||
open: () => unexpectedInProcessStream(),
|
||||
},
|
||||
registerGenerationSource: generation.register,
|
||||
start: () => ({ stop: () => {} }),
|
||||
} as unknown as ConnectionHandle
|
||||
const withdraw = ctx.provide('connection', handle)
|
||||
const client = ctx.plugin({ inject, apply })
|
||||
await client
|
||||
const remote = ctx.remote
|
||||
|
||||
const beforeReady = remote.$host
|
||||
expect(beforeReady).toEqual({ home: undefined, isLoopback: true })
|
||||
expect(remote.$host).toBe(beforeReady)
|
||||
|
||||
live.snapshot = { id: 1, host: { home: '/hosts/primary' } }
|
||||
const afterReady = remote.$host
|
||||
expect(afterReady).toEqual({ home: '/hosts/primary', isLoopback: true })
|
||||
expect(afterReady).not.toBe(beforeReady)
|
||||
expect(remote.$host).toBe(afterReady)
|
||||
|
||||
withdraw()
|
||||
expect(ctx.get('connection')).toBeUndefined()
|
||||
expect(remote.$host).toBe(afterReady)
|
||||
})
|
||||
|
||||
it('starts after Loader settlement and stops the owned loop on disposal', async () => {
|
||||
const readiness = deferredReadiness()
|
||||
const { client, start, stop } = await loaderReadinessBench(readiness.promise)
|
||||
@@ -634,10 +670,10 @@ describe('Client Typert API', () => {
|
||||
expect(ctx.get('remote.probe')).toBeUndefined()
|
||||
expect(ctx.get('probe')).toBe(businessProbe)
|
||||
expect(ctx.typert.remotes.list()).toEqual([])
|
||||
await expect(retained?.('agent-1', { objective: 'ship' })).resolves.toEqual({
|
||||
await expect(retained?.('agent-1', { objective: 'ship' })).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'internal',
|
||||
code: 'gateway/internal',
|
||||
message: 'client api: Remote method probe/create is no longer mounted',
|
||||
details: {},
|
||||
},
|
||||
@@ -1079,10 +1115,10 @@ describe('Client Typert API', () => {
|
||||
await dispose()
|
||||
resolveCall({ ok: true, value: { ref: 'goal-1' } })
|
||||
|
||||
await expect(invocation).resolves.toEqual({
|
||||
await expect(invocation).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'internal',
|
||||
code: 'gateway/internal',
|
||||
message: 'client api: Remote method probe/create is no longer mounted',
|
||||
details: {},
|
||||
},
|
||||
@@ -1235,14 +1271,14 @@ describe('Client Typert API', () => {
|
||||
})
|
||||
|
||||
it('delivers an RPC failure in the error branch with the Host error verbatim', async () => {
|
||||
const rpcError = { code: 'internal' as const, message: 'host failed', details: {} }
|
||||
const rpcError = { code: 'gateway/internal' as const, message: 'host failed', details: {} }
|
||||
const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>().mockResolvedValue({ ok: false, error: rpcError }))
|
||||
await ctx.remote.$mount({ package: '@fixture/probe', descriptors: [directDescriptor()] })
|
||||
|
||||
const outcome = await ctx.remote.probe.create('agent-1', { objective: 'ship' })
|
||||
expect(outcome.ok).toBe(false)
|
||||
if (outcome.ok) throw new Error('expected the Client API invocation to report a failure')
|
||||
expect(outcome.error).toBe(rpcError)
|
||||
expect(outcome.error).toMatchObject(rpcError)
|
||||
})
|
||||
|
||||
it('folds a transport throw into the error branch', async () => {
|
||||
@@ -1250,10 +1286,10 @@ describe('Client Typert API', () => {
|
||||
.mockRejectedValue(new Error('carrier offline')))
|
||||
await ctx.remote.$mount({ package: '@fixture/probe', descriptors: [directDescriptor()] })
|
||||
|
||||
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' })).resolves.toEqual({
|
||||
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' })).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'internal',
|
||||
code: 'gateway/internal',
|
||||
message: 'client api: probe/create failed: carrier offline',
|
||||
details: {},
|
||||
},
|
||||
@@ -1265,16 +1301,52 @@ describe('Client Typert API', () => {
|
||||
.mockRejectedValue('carrier exploded'))
|
||||
await ctx.remote.$mount({ package: '@fixture/probe', descriptors: [directDescriptor()] })
|
||||
|
||||
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' })).resolves.toEqual({
|
||||
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' })).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'internal',
|
||||
code: 'gateway/internal',
|
||||
message: 'client api: probe/create failed: carrier exploded',
|
||||
details: {},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('classifies a carrier throw under a caller-aborted signal as gateway/cancelled', async () => {
|
||||
const controller = new AbortController()
|
||||
const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>().mockImplementation(async () => {
|
||||
controller.abort()
|
||||
throw new Error('carrier aborted mid-flight')
|
||||
}))
|
||||
await ctx.remote.$mount({ package: '@fixture/probe', descriptors: [directDescriptor()] })
|
||||
|
||||
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' }, controller.signal))
|
||||
.resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'gateway/cancelled',
|
||||
message: 'client api: Remote invocation "probe/create" was aborted',
|
||||
details: {},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a carrier throw under an unaborted caller signal in the internal branch', async () => {
|
||||
const controller = new AbortController()
|
||||
const ctx = await bench(vi.fn<ConnectionHandle['rpc']['call']>()
|
||||
.mockRejectedValue(new Error('carrier offline')))
|
||||
await ctx.remote.$mount({ package: '@fixture/probe', descriptors: [directDescriptor()] })
|
||||
|
||||
await expect(ctx.remote.probe.create('agent-1', { objective: 'ship' }, controller.signal))
|
||||
.resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'gateway/internal',
|
||||
message: 'client api: probe/create failed: carrier offline',
|
||||
details: {},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('owns each $on subscription in the calling fiber', async () => {
|
||||
const { ctx, client, carrier } = await eventBench()
|
||||
const seen: string[] = []
|
||||
@@ -1472,7 +1544,7 @@ describe('Client Typert API', () => {
|
||||
it('fails the Connection generation when a result RPC is rejected', async () => {
|
||||
const call = vi.fn<ConnectionHandle['rpc']['call']>().mockResolvedValue({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: 'fixture result rejected', details: {} },
|
||||
error: { code: 'gateway/internal', message: 'fixture result rejected', details: {} },
|
||||
})
|
||||
const { client, carrier, run } = await eventBench(call)
|
||||
|
||||
@@ -1554,7 +1626,7 @@ describe('Client Typert API', () => {
|
||||
})
|
||||
target.remote.$on('fixture/approval', () => Promise.reject(rejection))
|
||||
|
||||
carrier.emit(approvalFrame('event-rejected', 'agent-rejected', 'cancelled'))
|
||||
carrier.emit(approvalFrame('event-rejected', 'agent-rejected', 'gateway/cancelled'))
|
||||
|
||||
await vi.waitFor(() => { expect(call).toHaveBeenCalledTimes(1) })
|
||||
expect(call).toHaveBeenCalledWith(
|
||||
@@ -1884,7 +1956,7 @@ describe('Client Typert API', () => {
|
||||
{
|
||||
name: 'Host failure',
|
||||
stop: (carrier: RemoteEventCarrier) => {
|
||||
carrier.fail(new RemoteStreamError('internal', 'fixture Host failed', {}))
|
||||
carrier.fail(new RemoteError('gateway/internal', 'fixture Host failed', {}))
|
||||
},
|
||||
message: 'fixture Host failed',
|
||||
},
|
||||
@@ -2037,14 +2109,14 @@ describe('Client Typert API', () => {
|
||||
failure: Object.assign(new Error('fixture Host rejected the stream'), {
|
||||
dshRemoteStreamFailure: {
|
||||
kind: 'remote' as const,
|
||||
code: 'fixture-rejected',
|
||||
code: 'fixture/rejected',
|
||||
details: { retry: false },
|
||||
},
|
||||
}),
|
||||
assert: (error: unknown) => {
|
||||
expect(error).toBeInstanceOf(RemoteStreamError)
|
||||
expect(error).toBeInstanceOf(RemoteError)
|
||||
expect(error).toMatchObject({
|
||||
code: 'fixture-rejected',
|
||||
code: 'fixture/rejected',
|
||||
message: 'fixture Host rejected the stream',
|
||||
details: { retry: false },
|
||||
})
|
||||
@@ -2122,14 +2194,14 @@ describe('Client Typert API', () => {
|
||||
type: 'error',
|
||||
streamId: failedOpen.streamId,
|
||||
error: {
|
||||
code: 'lookup-unavailable',
|
||||
code: 'gateway/lookup-unavailable',
|
||||
message: 'fixture stream failed',
|
||||
details: { lookup: 'missing' },
|
||||
},
|
||||
})
|
||||
await expect(failedItem).rejects.toMatchObject({
|
||||
name: 'RemoteStreamError',
|
||||
code: 'lookup-unavailable',
|
||||
name: 'RemoteError',
|
||||
code: 'gateway/lookup-unavailable',
|
||||
message: 'fixture stream failed',
|
||||
details: { lookup: 'missing' },
|
||||
})
|
||||
|
||||
@@ -9,8 +9,8 @@ import type { WebServer, WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
||||
import {
|
||||
bindTypertRemote,
|
||||
Remote,
|
||||
RemoteError,
|
||||
RemoteScope,
|
||||
TypertLookupFailure,
|
||||
type InvocationDescriptor,
|
||||
type TypertContext,
|
||||
type TypertLookup,
|
||||
@@ -37,6 +37,10 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface TypertContextMap {
|
||||
gatewayFixture: TypertContext<string>
|
||||
}
|
||||
|
||||
interface RemoteErrorDetailsMap {
|
||||
'session/agent-busy': { readonly reason: string }
|
||||
}
|
||||
}
|
||||
|
||||
const emptyModel: TypertContribution['model'] = {
|
||||
@@ -452,7 +456,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'lookup-unavailable')
|
||||
}), 'gateway/lookup-unavailable')
|
||||
expect(service.calls).toEqual([])
|
||||
})
|
||||
|
||||
@@ -485,7 +489,7 @@ describe('TypertGatewayService', () => {
|
||||
})).resolves.toBe('land')
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'other', method: 'absent', args: {},
|
||||
}), 'invocation-unavailable')
|
||||
}), 'gateway/invocation-unavailable')
|
||||
})
|
||||
|
||||
it('rejects SRC wire collisions and unavailable Context providers', async () => {
|
||||
@@ -496,14 +500,14 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'colliding-wire',
|
||||
method: 'run',
|
||||
args: { agentId: 'agent-1' },
|
||||
}), 'signature-invalid')
|
||||
}), 'gateway/signature-invalid')
|
||||
|
||||
const missing = await setup()
|
||||
await expectCode(missing.ctx.typertGateway.invoke({
|
||||
namespace: 'goals',
|
||||
method: 'rename',
|
||||
args: { agentId: 'agent-1', request: { title: 'land' } },
|
||||
}), 'context-unavailable')
|
||||
}), 'gateway/context-unavailable')
|
||||
|
||||
const contextCollision = await setupGateway()
|
||||
await contextCollision.plugin(ContextWireService)
|
||||
@@ -512,7 +516,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'context-wire',
|
||||
method: 'run',
|
||||
args: { agentId: 'agent-1' },
|
||||
}), 'signature-invalid')
|
||||
}), 'gateway/signature-invalid')
|
||||
})
|
||||
|
||||
it('re-reads Service and providers on every strict invocation', async () => {
|
||||
@@ -526,7 +530,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'lookup-unavailable')
|
||||
}), 'gateway/lookup-unavailable')
|
||||
|
||||
registerAgentLookup(ctx, agent)
|
||||
await serviceFiber.dispose()
|
||||
@@ -534,7 +538,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'service-unavailable')
|
||||
}), 'gateway/service-unavailable')
|
||||
})
|
||||
|
||||
it('re-reads and contains Context providers', async () => {
|
||||
@@ -548,7 +552,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'rename',
|
||||
args: { agentId: 'agent-1', request: { title: 'land' } },
|
||||
}), 'context-unavailable')
|
||||
}), 'gateway/context-unavailable')
|
||||
|
||||
ctx.typert.contexts.registerHost('gatewayFixture', {
|
||||
...contextProvider(scoped),
|
||||
@@ -558,13 +562,13 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'rename',
|
||||
args: { agentId: 'agent-1', request: { title: 'land' } },
|
||||
}), 'context-failed')
|
||||
}), 'gateway/context-failed')
|
||||
expect(error.cause).toEqual(new Error('provider failed'))
|
||||
})
|
||||
|
||||
it('preserves a Host Context policy rejection for the active RPC adapter', async () => {
|
||||
const { ctx } = await setup()
|
||||
const rejection = new TypertLookupFailure({ code: 'agent-busy', message: 'owned', details: { reason: 'subagent' } })
|
||||
const rejection = new RemoteError('session/agent-busy', 'owned', { reason: 'subagent' })
|
||||
ctx.typert.contexts.registerHost('gatewayFixture', {
|
||||
...contextProvider(ctx.extend()),
|
||||
resolve: async () => { throw rejection },
|
||||
@@ -590,7 +594,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'rename',
|
||||
args: { agentId: 'agent-1', request: { title: 'land' } },
|
||||
}), 'provider-mismatch')
|
||||
}), 'gateway/provider-mismatch')
|
||||
await mismatch()
|
||||
|
||||
ctx.typert.contexts.registerHost('gatewayFixture', {
|
||||
@@ -601,7 +605,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'rename',
|
||||
args: { agentId: 'agent-1', request: { title: 'land' } },
|
||||
}), 'context-not-found')
|
||||
}), 'gateway/context-not-found')
|
||||
})
|
||||
|
||||
it('contains lookup provider failures and missing identities', async () => {
|
||||
@@ -615,7 +619,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'lookup-failed')
|
||||
}), 'gateway/lookup-failed')
|
||||
expect(failure.cause).toEqual(new Error('lookup failed'))
|
||||
await throwing()
|
||||
|
||||
@@ -627,7 +631,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'lookup-not-found')
|
||||
}), 'gateway/lookup-not-found')
|
||||
await missing()
|
||||
|
||||
ctx.typert.lookups.register('gatewayFixture', {
|
||||
@@ -650,7 +654,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'passthrough',
|
||||
args: { value: 'would pass through SRC' },
|
||||
}), 'definition-unavailable')
|
||||
}), 'gateway/definition-unavailable')
|
||||
})
|
||||
|
||||
it('seeds the no-downgrade guard from definitions present before Gateway startup', async () => {
|
||||
@@ -665,7 +669,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'passthrough',
|
||||
args: { value: 'would pass through SRC' },
|
||||
}), 'definition-unavailable')
|
||||
}), 'gateway/definition-unavailable')
|
||||
})
|
||||
|
||||
it('retains the no-downgrade guard across Gateway Service reloads', async () => {
|
||||
@@ -684,7 +688,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'passthrough',
|
||||
args: { value: 'would pass through SRC' },
|
||||
}), 'definition-unavailable')
|
||||
}), 'gateway/definition-unavailable')
|
||||
})
|
||||
|
||||
it('rejects ambiguous SRC endpoints independently of reflection order', async () => {
|
||||
@@ -696,7 +700,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'shared',
|
||||
method: 'run',
|
||||
args: { value: 'ship' },
|
||||
}), 'ambiguous-endpoint')
|
||||
}), 'gateway/ambiguous-endpoint')
|
||||
expect(error.message).toContain('firstShared, secondShared')
|
||||
})
|
||||
|
||||
@@ -714,7 +718,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: testCase.namespace,
|
||||
method: 'run',
|
||||
args: testCase.args,
|
||||
}), 'signature-invalid')
|
||||
}), 'gateway/signature-invalid')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -728,7 +732,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'signature-invalid')
|
||||
}), 'gateway/signature-invalid')
|
||||
})
|
||||
|
||||
it('requires exact wire fields before invoking business code', async () => {
|
||||
@@ -739,17 +743,17 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { request: { title: 'ship' } },
|
||||
}), 'arguments-invalid')
|
||||
}), 'gateway/arguments-invalid')
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' }, optional: true },
|
||||
}), 'arguments-invalid')
|
||||
}), 'gateway/arguments-invalid')
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: [] as unknown as Record<string, unknown>,
|
||||
}), 'arguments-invalid')
|
||||
}), 'gateway/arguments-invalid')
|
||||
expect(service.calls).toEqual([])
|
||||
})
|
||||
|
||||
@@ -761,7 +765,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'strictOnly',
|
||||
args: { request: { title: 1 } },
|
||||
}), 'input-invalid')
|
||||
}), 'gateway/input-invalid')
|
||||
|
||||
service.nextResult = { title: 1 }
|
||||
await expect(ctx.typertGateway.invoke({
|
||||
@@ -799,7 +803,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'passthrough',
|
||||
args: { value },
|
||||
}), 'input-invalid')
|
||||
}), 'gateway/input-invalid')
|
||||
})
|
||||
|
||||
it('admits an omitted SRC field and hands the Host method undefined', async () => {
|
||||
@@ -823,7 +827,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'passthrough',
|
||||
args: { value: cyclic },
|
||||
}), 'input-invalid')
|
||||
}), 'gateway/input-invalid')
|
||||
|
||||
const result = new Date(0)
|
||||
service.nextResult = result
|
||||
@@ -855,7 +859,7 @@ describe('TypertGatewayService', () => {
|
||||
for (const value of [sparseWithExtra, symbolArray, symbolObject, hidden, accessor]) {
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'goals', method: 'passthrough', args: { value },
|
||||
}), 'input-invalid')
|
||||
}), 'gateway/input-invalid')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -871,7 +875,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'create',
|
||||
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
||||
}), 'provider-mismatch')
|
||||
}), 'gateway/provider-mismatch')
|
||||
})
|
||||
|
||||
it('validates binding identity and active method availability', async () => {
|
||||
@@ -881,7 +885,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'wrong-binding',
|
||||
method: 'run',
|
||||
args: { value: 'ship' },
|
||||
}), 'binding-invalid')
|
||||
}), 'gateway/binding-invalid')
|
||||
|
||||
await ctx.plugin(GoalService)
|
||||
registerStrict(ctx, [{ ...passthroughDescriptor(), method: 'missing' }])
|
||||
@@ -889,7 +893,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'missing',
|
||||
args: { value: 'ship' },
|
||||
}), 'method-unavailable')
|
||||
}), 'gateway/method-unavailable')
|
||||
})
|
||||
|
||||
it('requires a visible binding and supports explicitly provided plain Services', async () => {
|
||||
@@ -904,7 +908,7 @@ describe('TypertGatewayService', () => {
|
||||
}])
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'no-binding', method: 'run', args: { value: 'ship' },
|
||||
}), 'binding-invalid')
|
||||
}), 'gateway/binding-invalid')
|
||||
|
||||
const plain: {
|
||||
typertRemote?: ReturnType<typeof bindTypertRemote>
|
||||
@@ -941,7 +945,7 @@ describe('TypertGatewayService', () => {
|
||||
try {
|
||||
await expectCode(ctx.typertGateway.invoke({
|
||||
namespace: 'missing-method', method: 'run', args: { value: 'ship' },
|
||||
}), 'method-unavailable')
|
||||
}), 'gateway/method-unavailable')
|
||||
} finally {
|
||||
Object.defineProperty(MissingMethodService.prototype, 'run', descriptor)
|
||||
}
|
||||
@@ -965,7 +969,7 @@ describe('TypertGatewayService', () => {
|
||||
namespace: 'goals',
|
||||
method: 'absent',
|
||||
args: {},
|
||||
}), 'invocation-unavailable')
|
||||
}), 'gateway/invocation-unavailable')
|
||||
})
|
||||
|
||||
it('mounts a shared /api interceptor through an optional Connection and returns existing RPC results', async () => {
|
||||
@@ -1002,7 +1006,7 @@ describe('TypertGatewayService', () => {
|
||||
const invalid = await handler('goals/create', { invalid: true }, signal)
|
||||
expect(invalid).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'internal' },
|
||||
error: { code: 'gateway/internal' },
|
||||
})
|
||||
if (invalid.ok) throw new Error('invalid Remote payload unexpectedly succeeded')
|
||||
expect(invalid.error.message).toMatch(/exactly one plain-object args field/)
|
||||
@@ -1018,13 +1022,13 @@ describe('TypertGatewayService', () => {
|
||||
|
||||
for (const endpoint of ['goals', '/create', 'goals/', 'goals/create/extra']) {
|
||||
const result = await handler(endpoint, { args: {} }, signal)
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'internal' } })
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
|
||||
if (result.ok) throw new Error('invalid Remote endpoint unexpectedly succeeded')
|
||||
expect(result.error.message).toContain('invalid Remote endpoint')
|
||||
}
|
||||
for (const payload of [null, [], { args: {}, extra: true }, { only: true }, { args: null }, { args: [] }]) {
|
||||
const result = await handler('goals/create', payload, signal)
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'internal' } })
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
|
||||
if (result.ok) throw new Error('invalid Remote payload unexpectedly succeeded')
|
||||
expect(result.error.message).toContain('plain-object args field')
|
||||
}
|
||||
@@ -1036,7 +1040,7 @@ describe('TypertGatewayService', () => {
|
||||
new AbortController().signal,
|
||||
)).resolves.toEqual({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: 'non-error failure', details: {} },
|
||||
error: { code: 'gateway/internal', message: 'non-error failure', details: {} },
|
||||
})
|
||||
|
||||
// A business rejection observed while the carrier signal is already aborted
|
||||
@@ -1051,7 +1055,7 @@ describe('TypertGatewayService', () => {
|
||||
)).resolves.toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'cancelled',
|
||||
code: 'gateway/cancelled',
|
||||
message: 'Remote invocation "goals/fail" was aborted',
|
||||
details: {},
|
||||
},
|
||||
@@ -1075,7 +1079,7 @@ describe('TypertGatewayService', () => {
|
||||
args: { clientId: 'missing-client', eventId: 'missing', outcome: { kind: 'next' } },
|
||||
}
|
||||
const inactive = await handler('$events/result', result, new AbortController().signal)
|
||||
expect(inactive).toMatchObject({ ok: false, error: { code: 'internal' } })
|
||||
expect(inactive).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
|
||||
if (inactive.ok) throw new Error('inactive Remote event result unexpectedly succeeded')
|
||||
expect(inactive.error.message).toContain('identifies no active event stream')
|
||||
|
||||
@@ -1098,7 +1102,7 @@ describe('TypertGatewayService', () => {
|
||||
|
||||
for (const payload of [null, [], {}, { other: {} }]) {
|
||||
const invalid = await handler('$events/result', payload, carrier.signal)
|
||||
expect(invalid).toMatchObject({ ok: false, error: { code: 'internal' } })
|
||||
expect(invalid).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
|
||||
if (invalid.ok) throw new Error('invalid Remote event result payload unexpectedly succeeded')
|
||||
expect(invalid.error.message).toContain('requires exactly one plain-object args field')
|
||||
}
|
||||
@@ -1122,13 +1126,13 @@ describe('TypertGatewayService', () => {
|
||||
await ctx.plugin(GoalService)
|
||||
registerStrict(ctx, [createDescriptor()])
|
||||
const failure = {
|
||||
code: 'agent-busy',
|
||||
code: 'session/agent-busy',
|
||||
message: 'session is owned by subagent routing',
|
||||
details: { reason: 'use subagent delivery for this child session' },
|
||||
}
|
||||
ctx.typert.lookups.register('gatewayFixture', {
|
||||
...agentLookup({ id: 'agent-1' }),
|
||||
resolve: () => { throw new TypertLookupFailure(failure) },
|
||||
resolve: () => { throw new RemoteError('session/agent-busy', failure.message, failure.details) },
|
||||
})
|
||||
const handler = rawConnection(ctx).handler
|
||||
if (handler === undefined) throw new Error('fixture Connection did not retain the /api interceptor')
|
||||
@@ -1225,7 +1229,7 @@ describe('TypertGatewayService', () => {
|
||||
rpcId: 'rpc-invalid',
|
||||
result: {
|
||||
ok: false,
|
||||
error: { code: 'internal' },
|
||||
error: { code: 'gateway/internal' },
|
||||
},
|
||||
})
|
||||
expect(JSON.stringify(invalidBody)).toContain('plain-object args field')
|
||||
@@ -1249,7 +1253,7 @@ describe('TypertGatewayService', () => {
|
||||
rpcId: 'rpc-withdrawn',
|
||||
result: {
|
||||
ok: false,
|
||||
error: { code: 'internal' },
|
||||
error: { code: 'gateway/definition-unavailable' },
|
||||
},
|
||||
})
|
||||
expect(JSON.stringify(withdrawnBody)).toContain('strict definition was withdrawn')
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"src/client/remote-stream.ts",
|
||||
"src/client/snapshot-stream.ts",
|
||||
"src/client/stream-client.ts",
|
||||
"src/remote-error-codes.ts",
|
||||
"src/stream-protocol.ts"
|
||||
],
|
||||
"references": [
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"files": [
|
||||
"src/index.ts",
|
||||
"src/invariant.ts",
|
||||
"src/remote-error-codes.ts",
|
||||
"src/stream-protocol.ts",
|
||||
"src/stream-server.ts",
|
||||
"src/types.ts"
|
||||
|
||||
@@ -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 packages/api/remotes/README.md
|
||||
README.md: 7f855bd1cd37c0799f10cadfaf2167fe34f7ea40
|
||||
README.zh.md: edad8d735dd914e44bcf283336f78b243e3715d9
|
||||
README.md: 228bd53024a61c935e84d6939a7e8ac2873cd7ea
|
||||
README.zh.md: c8581bd72909602a1bb5509455dc5ed2a195b66f
|
||||
@@ -29,6 +29,8 @@ Two-sided BFF for Host Remote capabilities selected by this application. The Hos
|
||||
|
||||
The Client assembly mounts Commands, credentials, settings, Goal, dynamic Cordis, file and Session references, read-only Host plugin inventory, message feedback, Session Controller, and Workspace Controller contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
|
||||
|
||||
This facade is also the front door for the wire type vocabulary a Client package names. It re-exports, type-only, the Remote failure vocabulary (`RemoteResult`, `RemoteFailure`, `RemoteErrorCode`, `RemoteErrorDetailsMap`), the Host facts (`RemoteHostFacts`), and each selected domain's client-safe payload types, so a Client feature package imports one specifier instead of reaching into `dsh-typert-protocol`, the Gateway, or an owner's Host entry. Two kinds of package deliberately skip this door: the api-layer packages this assembly itself selects — importing it back would close a dependency cycle — and their tests, which take the failure vocabulary from `dsh-typert-protocol` directly. A UI package's tests instead take the `RemoteError` constructor from [`dsh-client-test-runtime`](../../test-support/client-runtime/README.md).
|
||||
|
||||
This package owns no physical transport or Host service discovery. It projects the application selection into generated Remote contributions and an independent Host event source per Client; API Gateway owns endpoints, carriers, cancellation, and reconnection. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
|
||||
|
||||
-----
|
||||
|
||||
@@ -29,6 +29,8 @@ kind: "package-reference"
|
||||
|
||||
Client 组合挂载 Commands、凭据、settings、Goal、动态 Cordis、文件与 Session 引用、只读 Host 插件清单、消息反馈、Session Controller 和 Workspace Controller contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
|
||||
|
||||
本 facade 同时是 Client 包指称 wire 类型词汇的正门。它以 type-only 方式转出 Remote 失败词汇(`RemoteResult`、`RemoteFailure`、`RemoteErrorCode`、`RemoteErrorDetailsMap`)、Host 事实(`RemoteHostFacts`),以及各已选领域的浏览器安全载荷类型,因此 Client 功能包只 import 一个 specifier,不必伸手进 `dsh-typert-protocol`、Gateway 或某个拥有方的 Host 入口。有两类包刻意不走这道门:本装配自己选中的 api 层包——反向 import 会形成依赖环——以及它们的测试,后者直接从 `dsh-typert-protocol` 取失败词汇。UI 包的测试则从 [`dsh-client-test-runtime`](../../test-support/client-runtime/README.zh.md) 取 `RemoteError` 构造器。
|
||||
|
||||
本包不拥有物理传输或 Host 服务发现。它只把应用选择投影为生成的 Remote contribution 和唯一的 Host Cordis event source;API Gateway 负责 endpoint、carrier、取消与重连。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
|
||||
|
||||
-----
|
||||
|
||||
@@ -56,7 +56,7 @@ export type {} from '@deepseek-ai/dsh-api-session-controller/types'
|
||||
export type {
|
||||
ConnectionHandle, ConnectionSinks, ContentBlock,
|
||||
MessageId,
|
||||
RpcError, RpcId, RpcRequest, RpcResponse, RpcResult, SessionId,
|
||||
RpcId, RpcRequest, RpcResponse, RpcResult, SessionId,
|
||||
StreamChunk,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
export type {} from '@deepseek-ai/dsh-api-gateway/client'
|
||||
@@ -112,7 +112,7 @@ export type {
|
||||
} from '@deepseek-ai/dsh-settings/types'
|
||||
// Provider registry and discovery vocabulary for the llm namespace.
|
||||
export type {
|
||||
LlmConfigurableProvider, LlmDiscoveredModel, LlmModelDiscoveryError,
|
||||
LlmConfigurableProvider, LlmDiscoveredModel,
|
||||
LlmModelDiscoveryRequest, LlmProviderInfo,
|
||||
} from '@deepseek-ai/dsh-llm/types'
|
||||
// Reference-discovery result vocabulary for the fileReferences and
|
||||
@@ -120,21 +120,14 @@ export type {
|
||||
export type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
|
||||
export type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types'
|
||||
|
||||
/** Failure vocabulary exposed by the assembled Client data layer. */
|
||||
export type ClientFailure =
|
||||
| import('@deepseek-ai/dsh-client-connection/client').RpcError
|
||||
| import('@deepseek-ai/dsh-agent-presets/types').AgentPresetError
|
||||
| import('@deepseek-ai/dsh-api-session-controller/types').SessionError
|
||||
| import('@deepseek-ai/dsh-api-settings-controller/types').CredentialError
|
||||
| import('@deepseek-ai/dsh-api-settings-controller/types').SettingsError
|
||||
| import('@deepseek-ai/dsh-llm/types').LlmModelDiscoveryError
|
||||
| import('@deepseek-ai/dsh-subagent/client').SubagentControlError
|
||||
| import('@deepseek-ai/dsh-api-workspace-controller/types').WorkspaceError
|
||||
|
||||
/** Success or failure returned by Client operations spanning both API families. */
|
||||
export type ClientResult<T> =
|
||||
| { readonly ok: true; readonly value: T }
|
||||
| { readonly ok: false; readonly error: ClientFailure }
|
||||
// The Remote failure vocabulary, re-exported so business packages keep naming
|
||||
// this assembly alone. Types only: a value export would make spec imports load
|
||||
// this module's owner /remote artifacts; specs take RemoteError from
|
||||
// dsh-client-test-runtime instead.
|
||||
export type {
|
||||
RemoteErrorCode, RemoteErrorDetailsMap, RemoteFailure, RemoteResult,
|
||||
} from '@deepseek-ai/dsh-typert-protocol'
|
||||
export type { RemoteHostFacts } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-title": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-time": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
@@ -137,6 +138,7 @@
|
||||
"@deepseek-ai/dsh-storage-domain": "workspace:^",
|
||||
"@deepseek-ai/dsh-storage-json": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-time": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-crypto": "workspace:^",
|
||||
|
||||
@@ -11,9 +11,9 @@ import type {} from '@deepseek-ai/dsh-agent-presets'
|
||||
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
|
||||
import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type {} from '@deepseek-ai/dsh-typert-registry'
|
||||
import type { ModelSelection, SessionError } from './types.ts'
|
||||
import type { ModelSelection } from './types.ts'
|
||||
|
||||
/** Cold Session identity absent from persistence. */
|
||||
export class ApiSessionNotFound extends Error {}
|
||||
@@ -57,10 +57,7 @@ export class ApiSessionPresetConflict extends Error {
|
||||
}
|
||||
|
||||
/** Failures produced while resolving one ordinary Session identity to its live Agent. */
|
||||
export type ApiSessionAgentError = Extract<
|
||||
SessionError,
|
||||
{ readonly code: 'session-not-found' | 'agent-busy' | 'internal' }
|
||||
>
|
||||
export type ApiSessionAgentError = RemoteError<'session/not-found' | 'session/agent-busy' | 'gateway/internal'>
|
||||
|
||||
/** Result of resolving one ordinary Session identity to its live Agent. */
|
||||
export type ApiSessionAgentResult =
|
||||
@@ -97,11 +94,11 @@ export function hasApiSessionSubagentOwner(
|
||||
* @returns a stable Session-domain failure.
|
||||
*/
|
||||
export function apiSessionSubagentOwnershipError(sessionId: SessionId): ApiSessionAgentError {
|
||||
return {
|
||||
code: 'agent-busy',
|
||||
message: `session "${sessionId}" is owned by subagent routing`,
|
||||
details: { reason: 'use subagent delivery for this child session' },
|
||||
}
|
||||
return new RemoteError(
|
||||
'session/agent-busy',
|
||||
`session "${sessionId}" is owned by subagent routing`,
|
||||
{ reason: 'use subagent delivery for this child session' },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,17 +142,17 @@ export class ApiSessionAgentController {
|
||||
constructor(private readonly ctx: Context) {
|
||||
ctx.typert.lookups.configure('agent', async (sessionId: SessionId) => {
|
||||
const found = await this.resolveAgent(sessionId)
|
||||
if ('error' in found) throw new TypertLookupFailure(found.error)
|
||||
if ('error' in found) throw found.error
|
||||
return found.agent
|
||||
})
|
||||
ctx.typert.lookups.configure('session', async (sessionId: SessionId) => {
|
||||
const found = await this.resolveAgent(sessionId)
|
||||
if ('error' in found) throw new TypertLookupFailure(found.error)
|
||||
if ('error' in found) throw found.error
|
||||
return found.agent.session
|
||||
})
|
||||
ctx.typert.contexts.configureHost('agent', async (sessionId: SessionId) => {
|
||||
const found = await this.resolveAgent(sessionId)
|
||||
if ('error' in found) throw new TypertLookupFailure(found.error)
|
||||
if ('error' in found) throw found.error
|
||||
return found.agent.ctx
|
||||
})
|
||||
}
|
||||
@@ -198,13 +195,7 @@ export class ApiSessionAgentController {
|
||||
return { agent: await resume }
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ApiSessionNotFound) {
|
||||
return {
|
||||
error: {
|
||||
code: 'session-not-found',
|
||||
message: error.message,
|
||||
details: { sessionId },
|
||||
},
|
||||
}
|
||||
return { error: new RemoteError('session/not-found', error.message, { sessionId }) }
|
||||
}
|
||||
if (error instanceof ApiSessionSubagentOwnership) {
|
||||
return { error: apiSessionSubagentOwnershipError(error.sessionId) }
|
||||
@@ -216,11 +207,11 @@ export class ApiSessionAgentController {
|
||||
return { error: apiSessionSubagentOwnershipError(sessionId) }
|
||||
}
|
||||
return {
|
||||
error: {
|
||||
code: 'internal',
|
||||
message: `resume failed for session "${sessionId}": ${String(error)}`,
|
||||
details: {},
|
||||
},
|
||||
error: new RemoteError(
|
||||
'gateway/internal',
|
||||
`resume failed for session "${sessionId}": ${String(error)}`,
|
||||
{},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/** Client operation results spanning the Session and subagent Remote calls. */
|
||||
|
||||
import type { RpcError } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SubagentControlError } from '@deepseek-ai/dsh-subagent/client'
|
||||
import type { SessionError } from '../../types.ts'
|
||||
|
||||
/** Failure surfaced by the Client Session object layer. */
|
||||
export type ClientFailure = RpcError | SessionError | SubagentControlError
|
||||
|
||||
/** Success or failure returned by a Client Session operation. */
|
||||
export type ClientResult<T> =
|
||||
| { readonly ok: true; readonly value: T }
|
||||
| { readonly ok: false; readonly error: ClientFailure }
|
||||
|
||||
/**
|
||||
* Fold a rejected carrier operation into the Client Session failure vocabulary.
|
||||
* @param error - rejection from a Remote or local carrier call.
|
||||
* @returns the failure branch of a Client Session result.
|
||||
*/
|
||||
export function transportResult<T>(error: unknown): ClientResult<T> {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'internal',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
details: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
|
||||
import type { PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts'
|
||||
import type { ClientResult } from './result.ts'
|
||||
import type { PendingSubmissionImage, SessionSnapshot } from './snapshot.ts'
|
||||
|
||||
/**
|
||||
@@ -84,7 +83,7 @@ export interface ISession {
|
||||
mode: 'queue' | 'steer',
|
||||
signal?: AbortSignal,
|
||||
requestId?: SessionRequestId,
|
||||
): Promise<ClientResult<{ accepted: true }>>
|
||||
): Promise<RemoteResult<{ accepted: true }>>
|
||||
/**
|
||||
* Resolve one durable image referenced by this session.
|
||||
* @param attachmentId - opaque id found in the folded session log.
|
||||
@@ -92,27 +91,27 @@ export interface ISession {
|
||||
*/
|
||||
readAttachment(
|
||||
attachmentId: AttachmentIdType,
|
||||
): Promise<ClientResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>>
|
||||
): Promise<RemoteResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>>
|
||||
/**
|
||||
* Apply one edit, remove, or strict steer action to a still-pending queue occurrence.
|
||||
* @param itemId - agent-owned inbox occurrence identity.
|
||||
* @param action - requested queue operation.
|
||||
* @returns acceptance, or a business/transport error.
|
||||
*/
|
||||
updateQueue(itemId: MessageId, action: QueueAction): Promise<ClientResult<{ accepted: true }>>
|
||||
updateQueue(itemId: MessageId, action: QueueAction): Promise<RemoteResult<{ accepted: true }>>
|
||||
/**
|
||||
* Cancel the running turn. Pending queued work remains and resumes in FIFO
|
||||
* order after the Host reaches cancellation quiescence.
|
||||
* @returns acceptance, or the business error.
|
||||
*/
|
||||
cancel(): Promise<ClientResult<{ accepted: true }>>
|
||||
cancel(): Promise<RemoteResult<{ accepted: true }>>
|
||||
/**
|
||||
* Rename this session (explicit user title; pins it against automatic
|
||||
* regeneration).
|
||||
* @param title - raw title text (the host normalizes acceptance).
|
||||
* @returns the normalized accepted title and its event seq, or the business error.
|
||||
*/
|
||||
rename(title: string): Promise<ClientResult<{ title: string; seq: number }>>
|
||||
rename(title: string): Promise<RemoteResult<{ title: string; seq: number }>>
|
||||
/**
|
||||
* Extend the history window backwards (older messages pagination).
|
||||
* @returns completion; failures land in snapshot.openState/loadingOlder.
|
||||
|
||||
@@ -8,10 +8,10 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { AgentContext } from '../scope.ts'
|
||||
import type { SessionSearchResultItem } from '../sessions/manager.ts'
|
||||
import type { SessionBinding, SessionListState } from '../sessions/service.ts'
|
||||
import type { ClientResult } from './result.ts'
|
||||
import type { SessionFace } from './session.ts'
|
||||
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface ISessions {
|
||||
search(
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<ClientResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>>
|
||||
): Promise<RemoteResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>>
|
||||
/**
|
||||
* Fork a session from a completed-turn prefix of the source; on resolution
|
||||
* the child is in the list store and `open()` can target it.
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
|
||||
import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SessionRequestId } from '../../types.ts'
|
||||
import type { ClientFailure } from './result.ts'
|
||||
|
||||
/** One transient inbox occurrence from the authoritative queue snapshot. */
|
||||
export interface QueuedMessage {
|
||||
@@ -53,7 +53,7 @@ export type OpenState = 'cold' | 'loading' | 'open' | 'error'
|
||||
/** Send/stop failure surfaced by Session consumers. */
|
||||
export interface PromptError {
|
||||
readonly op: 'send' | 'stop'
|
||||
readonly error: ClientFailure
|
||||
readonly error: RemoteFailure
|
||||
}
|
||||
|
||||
/** Immutable Session lifecycle and control snapshot. */
|
||||
@@ -70,7 +70,7 @@ export interface SessionSnapshot {
|
||||
} | null
|
||||
readonly removed: boolean
|
||||
readonly openState: OpenState
|
||||
readonly openError: ClientFailure | null
|
||||
readonly openError: RemoteFailure | null
|
||||
readonly hasMore: boolean
|
||||
readonly loadingOlder: boolean
|
||||
readonly promptError: PromptError | null
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-agent/types'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { createSessionControlStream } from './transport.ts'
|
||||
import { ClientSessions } from './sessions/service.ts'
|
||||
import type { SessionRemotes } from './sessions/remotes.ts'
|
||||
@@ -13,7 +12,6 @@ export {
|
||||
SessionEventStream,
|
||||
SESSION_SEARCH_RESULT_LIMIT,
|
||||
SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS,
|
||||
sessionStreamFailure,
|
||||
} from './transport.ts'
|
||||
export type {
|
||||
ClientSessionPageRequest,
|
||||
@@ -66,7 +64,6 @@ export type {
|
||||
QueuedMessage,
|
||||
SessionSnapshot,
|
||||
} from './contract/snapshot.ts'
|
||||
export type { ClientFailure, ClientResult } from './contract/result.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
@@ -75,9 +72,8 @@ declare module '@deepseek-ai/cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Required wire, Remote, and Context projection services. */
|
||||
/** Required Remote and Context projection services. */
|
||||
export const inject = [
|
||||
'connection',
|
||||
'typert',
|
||||
'remote',
|
||||
'remote.commands',
|
||||
@@ -90,7 +86,6 @@ export const inject = [
|
||||
* @param ctx - Client Cordis context.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const remotes = ctx.remote as unknown as SessionRemotes
|
||||
const sessions = new ClientSessions(ctx, remotes)
|
||||
ctx.remote.$on('api-session/added', (summary) => { sessions.handleSessionAdded(summary) })
|
||||
@@ -111,7 +106,7 @@ export function apply(ctx: Context): void {
|
||||
})
|
||||
control.start()
|
||||
ctx.on('connection/reset', () => { sessions.handleConnected() })
|
||||
if (connection.generation.getSnapshot() !== undefined) sessions.handleConnected()
|
||||
if (ctx.remote.$host.home !== undefined) sessions.handleConnected()
|
||||
ctx.typert.contexts.registerClient('agent', {
|
||||
identity: candidate => sessions.scopeOf(candidate),
|
||||
resolve: sessionId => sessions.resolveAgentScope(sessionId),
|
||||
|
||||
@@ -9,13 +9,12 @@ import type {
|
||||
SessionControlBaseline,
|
||||
SessionControlFrame,
|
||||
SessionQueuedItem,
|
||||
SessionError,
|
||||
SessionSummary,
|
||||
SessionJob as JobView,
|
||||
} from '../../types.ts'
|
||||
import { mergeOrderedBaseline } from '../ordered-baseline.ts'
|
||||
import type { ClientFailure, ClientResult } from '../contract/result.ts'
|
||||
import { transportResult } from '../contract/result.ts'
|
||||
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SessionListEntry, TitledSessionSummary } from './lineage.ts'
|
||||
import { flattenLineage } from './lineage.ts'
|
||||
// Type-only merge edge: the title domain's client-namespace outlet declares
|
||||
@@ -51,7 +50,7 @@ export interface SessionListSnapshot {
|
||||
state: 'idle' | 'loading' | 'error'
|
||||
/** Arrival lifecycle (see {@link SessionListPhase}); `state` stays the pull-activity axis. */
|
||||
phase: SessionListPhase
|
||||
error: ClientFailure | null
|
||||
error: RemoteFailure | null
|
||||
subagentsByParent: Readonly<Record<SessionId, SubagentCatalogSnapshot>>
|
||||
/** Background jobs per session; an absent key is an empty set. */
|
||||
jobsBySession: Readonly<Record<SessionId, readonly JobView[]>>
|
||||
@@ -63,7 +62,7 @@ export type SubagentCatalogSnapshot = Omit<SubagentCatalog, 'parentAvailable'> &
|
||||
/** Absent until the first successful catalog read. */
|
||||
readonly parentAvailable?: boolean
|
||||
state: 'loading' | 'ready' | 'error'
|
||||
error: ClientFailure | null
|
||||
error: RemoteFailure | null
|
||||
}
|
||||
|
||||
function catalogAvailability(parentAvailable: boolean | undefined): {
|
||||
@@ -112,7 +111,7 @@ export class SessionManager {
|
||||
private listState: 'idle' | 'loading' | 'error' = 'idle'
|
||||
/** Arrival phase; the pending → ready edge fires on the first successful pull (see SessionListPhase). */
|
||||
private listPhase: SessionListPhase = 'pending'
|
||||
private listError: ClientFailure | null = null
|
||||
private listError: RemoteFailure | null = null
|
||||
private listInflight: Promise<void> | null = null
|
||||
/** Mutations arriving after a list request starts are replayed over its response. */
|
||||
private listMutations: SessionListMutation[] | null = null
|
||||
@@ -366,7 +365,7 @@ export class SessionManager {
|
||||
this.notifier.markDirty()
|
||||
const operation = (async () => {
|
||||
try {
|
||||
const result = toSessionResult(await this.remote.subagents.list(parentSessionId))
|
||||
const result = await this.remote.subagents.list(parentSessionId)
|
||||
if (result.ok) {
|
||||
const parentAvailable = this.catalogInflight.get(parentSessionId)?.parentAvailableOverride
|
||||
?? result.value.parentAvailable
|
||||
@@ -395,7 +394,7 @@ export class SessionManager {
|
||||
})
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const folded = transportResult<never>(error)
|
||||
if (!isRemoteFailure(error)) throw error
|
||||
this.catalogs.set(parentSessionId, {
|
||||
entries: this.withCatalogMutations(
|
||||
previous?.entries ?? [], expandableRows, activityRows,
|
||||
@@ -405,7 +404,7 @@ export class SessionManager {
|
||||
?? previous?.parentAvailable,
|
||||
),
|
||||
state: 'error',
|
||||
error: folded.ok ? null : folded.error,
|
||||
error,
|
||||
})
|
||||
} finally {
|
||||
this.catalogInflight.delete(parentSessionId)
|
||||
@@ -457,7 +456,7 @@ export class SessionManager {
|
||||
this.notifier.markDirty()
|
||||
this.listInflight = (async () => {
|
||||
try {
|
||||
const result = toSessionResult(await this.remote.session.list({}))
|
||||
const result = await this.remote.session.list({})
|
||||
if (result.ok) {
|
||||
const baseline: SessionSummary[] = this.listPhase === 'pending'
|
||||
? [...result.value.items]
|
||||
@@ -506,10 +505,9 @@ export class SessionManager {
|
||||
this.listError = result.error
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isRemoteFailure(error)) throw error
|
||||
this.listState = 'error'
|
||||
const folded = transportResult<never>(error)
|
||||
/* v8 ignore next -- the `? null` arm is unreachable: transportResult always returns ok:false. */
|
||||
this.listError = folded.ok ? null : folded.error
|
||||
this.listError = error
|
||||
} finally {
|
||||
this.listMutations = null
|
||||
this.listInflight = null
|
||||
@@ -529,19 +527,15 @@ export class SessionManager {
|
||||
async search(
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<ClientResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>> {
|
||||
try {
|
||||
const result = toSessionResult(await this.remote.session.search({ query }, signal))
|
||||
if (!result.ok) return result
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
items: [...result.value.items],
|
||||
hasMore: result.value.hasMore,
|
||||
},
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return transportResult(error)
|
||||
): Promise<RemoteResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>> {
|
||||
const result = await this.remote.session.search({ query }, signal)
|
||||
if (!result.ok) return result
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
items: [...result.value.items],
|
||||
hasMore: result.value.hasMore,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,36 +552,32 @@ export class SessionManager {
|
||||
cwd?: string
|
||||
sessionId?: SessionId
|
||||
} = {},
|
||||
): Promise<ClientResult<{ sessionId: SessionId }>> {
|
||||
try {
|
||||
const shared = opts.sessionId === undefined ? {} : { sessionId: opts.sessionId }
|
||||
const payload = opts.workspaceId !== undefined
|
||||
? { workspaceId: opts.workspaceId, ...shared }
|
||||
: { ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }), ...shared }
|
||||
const result = toSessionResult(await this.remote.session.create(payload))
|
||||
if (result.ok) {
|
||||
): Promise<RemoteResult<{ sessionId: SessionId }>> {
|
||||
const shared = opts.sessionId === undefined ? {} : { sessionId: opts.sessionId }
|
||||
const payload = opts.workspaceId !== undefined
|
||||
? { workspaceId: opts.workspaceId, ...shared }
|
||||
: { ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }), ...shared }
|
||||
const result = await this.remote.session.create(payload)
|
||||
if (result.ok) {
|
||||
this.recordMutation({ kind: 'upsert', summary: {
|
||||
sessionId: result.value.sessionId, updatedAt: Date.now(), running: false, blank: true,
|
||||
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
||||
} })
|
||||
} else {
|
||||
const publishedSessionId = workspaceAttachSessionId(result.error)
|
||||
// Publication precedes attachment. The error's id is a real Session,
|
||||
// so expose it immediately as Ungrouped while the caller keeps the
|
||||
// prompt buffer and decides whether to retry attachment.
|
||||
if (publishedSessionId !== undefined) {
|
||||
this.recordMutation({ kind: 'upsert', summary: {
|
||||
sessionId: result.value.sessionId, updatedAt: Date.now(), running: false, blank: true,
|
||||
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
||||
sessionId: publishedSessionId,
|
||||
updatedAt: Date.now(),
|
||||
running: false,
|
||||
blank: true,
|
||||
} })
|
||||
} else {
|
||||
const publishedSessionId = workspaceAttachSessionId(result.error)
|
||||
// Publication precedes attachment. The error's id is a real Session,
|
||||
// so expose it immediately as Ungrouped while the caller keeps the
|
||||
// prompt buffer and decides whether to retry attachment.
|
||||
if (publishedSessionId !== undefined) {
|
||||
this.recordMutation({ kind: 'upsert', summary: {
|
||||
sessionId: publishedSessionId,
|
||||
updatedAt: Date.now(),
|
||||
running: false,
|
||||
blank: true,
|
||||
} })
|
||||
}
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
return transportResult(error)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -601,27 +591,23 @@ export class SessionManager {
|
||||
*/
|
||||
async fork(
|
||||
opts: { sessionId: SessionId; atSeq?: number },
|
||||
): Promise<ClientResult<{ sessionId: SessionId }>> {
|
||||
try {
|
||||
const source = this.summaries.find(s => s.sessionId === opts.sessionId)
|
||||
const result = toSessionResult(await this.remote.session.fork({
|
||||
sessionId: opts.sessionId,
|
||||
...opts.atSeq === undefined ? {} : { atSeq: opts.atSeq },
|
||||
}))
|
||||
const childId = result.ok
|
||||
? result.value.sessionId
|
||||
: workspaceAttachSessionId(result.error)
|
||||
if (childId !== undefined) {
|
||||
this.recordMutation({ kind: 'upsert', summary: {
|
||||
sessionId: childId, updatedAt: Date.now(), running: false, blank: false,
|
||||
parentSessionId: opts.sessionId,
|
||||
...(source?.cwd !== undefined ? { cwd: source.cwd } : {}),
|
||||
} })
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
return transportResult(error)
|
||||
): Promise<RemoteResult<{ sessionId: SessionId }>> {
|
||||
const source = this.summaries.find(s => s.sessionId === opts.sessionId)
|
||||
const result = await this.remote.session.fork({
|
||||
sessionId: opts.sessionId,
|
||||
...opts.atSeq === undefined ? {} : { atSeq: opts.atSeq },
|
||||
})
|
||||
const childId = result.ok
|
||||
? result.value.sessionId
|
||||
: workspaceAttachSessionId(result.error)
|
||||
if (childId !== undefined) {
|
||||
this.recordMutation({ kind: 'upsert', summary: {
|
||||
sessionId: childId, updatedAt: Date.now(), running: false, blank: false,
|
||||
parentSessionId: opts.sessionId,
|
||||
...(source?.cwd !== undefined ? { cwd: source.cwd } : {}),
|
||||
} })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1010,13 +996,6 @@ function applyMutation(summaries: readonly SessionSummary[], mutation: SessionLi
|
||||
}
|
||||
|
||||
/** Temporary source-plane bridge while the Host contract and client project build independently. */
|
||||
function workspaceAttachSessionId(error: ClientFailure): SessionId | undefined {
|
||||
return error.code === 'workspace-attach-failed' ? error.details.sessionId : undefined
|
||||
}
|
||||
|
||||
/** Narrow a generated Session Remote failure to its service-owned error vocabulary. */
|
||||
function toSessionResult<T>(
|
||||
result: import('@deepseek-ai/dsh-typert-protocol').RemoteResult<T>,
|
||||
): ClientResult<T> {
|
||||
return result.ok ? result : { ok: false, error: result.error as SessionError }
|
||||
function workspaceAttachSessionId(error: RemoteFailure): SessionId | undefined {
|
||||
return error.code === 'session/workspace-attach-failed' ? error.details.sessionId : undefined
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/t
|
||||
import {
|
||||
createSnapshotStore, type SnapshotStore,
|
||||
} from '@deepseek-ai/dsh-client-store'
|
||||
import type { ClientFailure, ClientResult } from '../contract/result.ts'
|
||||
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SessionEventSource } from '../contract/events.ts'
|
||||
import type { SessionFace } from '../contract/session.ts'
|
||||
import type { AgentContext, ISessions } from '../contract/sessions.ts'
|
||||
@@ -101,7 +101,7 @@ export class SessionCreateError extends Error {
|
||||
* @param requestedSessionId - caller-preallocated id used for later stream/list reconciliation.
|
||||
*/
|
||||
constructor(
|
||||
readonly rpcError: ClientFailure,
|
||||
readonly rpcError: RemoteFailure,
|
||||
readonly requestedSessionId: SessionId | undefined,
|
||||
) {
|
||||
super(`session create failed: ${rpcError.code}: ${rpcError.message}`)
|
||||
@@ -117,7 +117,7 @@ export class SessionForkError extends Error {
|
||||
* @param sourceSessionId - the session the fork was cut from.
|
||||
*/
|
||||
constructor(
|
||||
readonly rpcError: ClientFailure,
|
||||
readonly rpcError: RemoteFailure,
|
||||
readonly sourceSessionId: SessionId,
|
||||
) {
|
||||
super(`session fork failed: ${rpcError.code}: ${rpcError.message}`)
|
||||
@@ -335,7 +335,7 @@ export class ClientSessions implements ISessions {
|
||||
search(
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<ClientResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>> {
|
||||
): Promise<RemoteResult<{ items: SessionSearchResultItem[]; hasMore: boolean }>> {
|
||||
return this.manager.search(query, signal)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-atta
|
||||
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
|
||||
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import {
|
||||
SessionEventStream,
|
||||
sessionStreamFailure,
|
||||
} from '../transport.ts'
|
||||
import { SessionEventStream } from '../transport.ts'
|
||||
import type { SessionJournalChange } from '../transport.ts'
|
||||
import type {
|
||||
PromptContentPart,
|
||||
@@ -18,10 +15,7 @@ import type {
|
||||
SessionControlFrame,
|
||||
SessionQueuedItem,
|
||||
SessionRequestId,
|
||||
SessionError,
|
||||
} from '../../types.ts'
|
||||
import type { ClientFailure, ClientResult } from '../contract/result.ts'
|
||||
import { transportResult } from '../contract/result.ts'
|
||||
import type {
|
||||
BeginSubmissionInput, PendingSubmissionRetirement, SessionFace, SubmissionHandle,
|
||||
} from '../contract/session.ts'
|
||||
@@ -33,7 +27,8 @@ import type {
|
||||
SessionEventLikeEntry, SessionLiveEventEntry,
|
||||
} from '../contract/events.ts'
|
||||
import { Notifier } from './notifier.ts'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SessionRemotes } from './remotes.ts'
|
||||
import { ProjectionValueStore } from './projection-store.ts'
|
||||
import type { ProjectionsBaseline } from './projection-store.ts'
|
||||
@@ -77,7 +72,7 @@ export class Session implements SessionFace {
|
||||
private baseSeq = 0
|
||||
private hasMore = false
|
||||
private openState: OpenState = 'cold'
|
||||
private openError: ClientFailure | null = null
|
||||
private openError: RemoteFailure | null = null
|
||||
private openPromise: Promise<void> | null = null
|
||||
/** Bumped by stream replacement to invalidate an in-flight doOpen. Stale
|
||||
* passes drop all writes once the generation moves on. */
|
||||
@@ -214,7 +209,7 @@ export class Session implements SessionFace {
|
||||
mode: 'queue' | 'steer',
|
||||
signal?: AbortSignal,
|
||||
requestId?: SessionRequestId,
|
||||
): Promise<ClientResult<{ accepted: true }>> {
|
||||
): Promise<RemoteResult<{ accepted: true }>> {
|
||||
this.promptError = null
|
||||
this.lastAgentError = null
|
||||
// Synchronous, before the first await: the blank → engaging edge must be
|
||||
@@ -223,52 +218,26 @@ export class Session implements SessionFace {
|
||||
this.promptAttempted = true
|
||||
if (this.blankBit) this.firstPromptPendingTurn = true
|
||||
this.notifier.markDirty()
|
||||
let result: ClientResult<{ accepted: true }>
|
||||
try {
|
||||
if (this.address === undefined) {
|
||||
const clientTimeZone = resolvedClientTimeZone()
|
||||
result = toSessionResult(await this.remote.session.prompt({
|
||||
requestId: requestId ?? randomUUID() as SessionRequestId,
|
||||
sessionId: this.sessionId,
|
||||
mode,
|
||||
content,
|
||||
clientTimeZone,
|
||||
}, signal))
|
||||
} else if (this.address.mode === 'one-shot') {
|
||||
result = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'subagent-not-resumable',
|
||||
message: 'one-shot subagent conversations are read-only',
|
||||
details: { childSessionId: this.address.childSessionId },
|
||||
},
|
||||
}
|
||||
} else {
|
||||
if (content.some(part => part.type === 'image')) {
|
||||
result = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'attachment-error',
|
||||
message: 'Image input is unavailable for subagent continuations.',
|
||||
details: { reason: 'SUBAGENT_IMAGE_UNSUPPORTED' },
|
||||
},
|
||||
}
|
||||
} else {
|
||||
const routed = toSessionResult(await this.remote.subagents.prompt({
|
||||
requestId: randomUUID() as SessionRequestId,
|
||||
parentSessionId: this.address.parentSessionId,
|
||||
childSessionId: this.address.childSessionId,
|
||||
mode: this.address.mode,
|
||||
content: content.flatMap(part => part.type === 'text'
|
||||
? [{ type: 'text' as const, text: part.text }]
|
||||
: []),
|
||||
clientTimeZone: resolvedClientTimeZone(),
|
||||
}, signal))
|
||||
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
result = transportResult(error)
|
||||
let result: RemoteResult<{ accepted: true }>
|
||||
if (this.address === undefined) {
|
||||
const clientTimeZone = resolvedClientTimeZone()
|
||||
result = await this.remote.session.prompt({
|
||||
requestId: requestId ?? randomUUID() as SessionRequestId,
|
||||
sessionId: this.sessionId,
|
||||
mode,
|
||||
content,
|
||||
clientTimeZone,
|
||||
}, signal)
|
||||
} else {
|
||||
const routed = await this.remote.subagents.prompt({
|
||||
requestId: randomUUID() as SessionRequestId,
|
||||
parentSessionId: this.address.parentSessionId,
|
||||
childSessionId: this.address.childSessionId,
|
||||
mode: 'continuable',
|
||||
content,
|
||||
clientTimeZone: resolvedClientTimeZone(),
|
||||
}, signal)
|
||||
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
|
||||
}
|
||||
if (!result.ok) {
|
||||
if (requestId !== undefined) this.retireFailedSubmission(requestId)
|
||||
@@ -299,66 +268,38 @@ export class Session implements SessionFace {
|
||||
*/
|
||||
async readAttachment(
|
||||
attachmentId: AttachmentIdType,
|
||||
): Promise<ClientResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>> {
|
||||
try {
|
||||
const result = await this.remote.session.attachment({
|
||||
sessionId: this.sessionId,
|
||||
attachmentId,
|
||||
})
|
||||
if (!result.ok) return toSessionResult(result)
|
||||
const binary = atob(result.value.data)
|
||||
const data = Uint8Array.from(binary, char => char.charCodeAt(0))
|
||||
return { ok: true, value: { attachment: result.value.attachment, data } }
|
||||
} catch (error) {
|
||||
return transportResult(error)
|
||||
}
|
||||
): Promise<RemoteResult<{ attachment: ImageAttachmentRef; data: Uint8Array }>> {
|
||||
const result = await this.remote.session.attachment({
|
||||
sessionId: this.sessionId,
|
||||
attachmentId,
|
||||
})
|
||||
if (!result.ok) return result
|
||||
const binary = atob(result.value.data)
|
||||
const data = Uint8Array.from(binary, char => char.charCodeAt(0))
|
||||
return { ok: true, value: { attachment: result.value.attachment, data } }
|
||||
}
|
||||
|
||||
/** Apply one operation to a still-pending queue occurrence. */
|
||||
async updateQueue(itemId: MessageId, action: QueueAction): Promise<ClientResult<{ accepted: true }>> {
|
||||
try {
|
||||
return toSessionResult(await this.remote.session.updateQueue({ sessionId: this.sessionId, itemId, action }))
|
||||
} catch (error) {
|
||||
return transportResult(error)
|
||||
}
|
||||
async updateQueue(itemId: MessageId, action: QueueAction): Promise<RemoteResult<{ accepted: true }>> {
|
||||
return this.remote.session.updateQueue({ sessionId: this.sessionId, itemId, action })
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the active turn while the Host preserves pending inbox work; failures
|
||||
* land in promptError (same error-strip display slot). A continuable
|
||||
* subagent address routes through `subagents.interruptByParent`, whose durable
|
||||
* parent-address authority works without a live parent Agent; a one-shot
|
||||
* address stays uncancellable (the UI offers no stop action, so this arm is
|
||||
* defensive).
|
||||
* land in promptError (same error-strip display slot). A subagent address
|
||||
* routes through `subagents.interruptByParent`, whose durable parent-address
|
||||
* authority works without a live parent Agent.
|
||||
* @returns the cancel result.
|
||||
*/
|
||||
async cancel(): Promise<ClientResult<{ accepted: true }>> {
|
||||
async cancel(): Promise<RemoteResult<{ accepted: true }>> {
|
||||
const address = this.address
|
||||
if (address !== undefined && address.mode === 'one-shot') {
|
||||
const result: ClientResult<{ accepted: true }> = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'subagent-delivery-unavailable',
|
||||
message: 'subagent activation cancellation is unavailable',
|
||||
details: { childSessionId: address.childSessionId },
|
||||
},
|
||||
}
|
||||
this.promptError = { op: 'stop', error: result.error }
|
||||
this.notifier.markDirty()
|
||||
return result
|
||||
}
|
||||
let result: ClientResult<{ accepted: true }>
|
||||
try {
|
||||
result = address !== undefined
|
||||
? toSessionResult(await this.remote.subagents.interruptByParent(
|
||||
address.childSessionId,
|
||||
address.parentSessionId,
|
||||
address.mode,
|
||||
))
|
||||
: toSessionResult(await this.remote.session.cancel({ sessionId: this.sessionId }))
|
||||
} catch (error) {
|
||||
result = transportResult(error)
|
||||
}
|
||||
const result = address !== undefined
|
||||
? await this.remote.subagents.interruptByParent(
|
||||
address.childSessionId,
|
||||
address.parentSessionId,
|
||||
'continuable',
|
||||
)
|
||||
: await this.remote.session.cancel({ sessionId: this.sessionId })
|
||||
if (!result.ok) {
|
||||
this.promptError = { op: 'stop', error: result.error }
|
||||
this.notifier.markDirty()
|
||||
@@ -375,14 +316,10 @@ export class Session implements SessionFace {
|
||||
* @param title - raw title text (the host normalizes acceptance).
|
||||
* @returns the rename result (normalized accepted title + title event seq).
|
||||
*/
|
||||
async rename(title: string): Promise<ClientResult<{ title: string; seq: number }>> {
|
||||
try {
|
||||
const result = toSessionResult(await this.remote.session.rename({ sessionId: this.sessionId, title }))
|
||||
if (result.ok) this.projections.apply('title', result.value.title, result.value.seq)
|
||||
return result
|
||||
} catch (error) {
|
||||
return transportResult(error)
|
||||
}
|
||||
async rename(title: string): Promise<RemoteResult<{ title: string; seq: number }>> {
|
||||
const result = await this.remote.session.rename({ sessionId: this.sessionId, title })
|
||||
if (result.ok) this.projections.apply('title', result.value.title, result.value.seq)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,7 +327,7 @@ export class Session implements SessionFace {
|
||||
* admission semantics (the host executor durably logs the lifecycle;
|
||||
* outcomes render as flow nodes, never as a response echo).
|
||||
* @param line - the full command line, leading slash included.
|
||||
* @returns the admission result, or the error branch on transport failure.
|
||||
* @returns the admission result.
|
||||
*/
|
||||
async command(line: string): Promise<RemoteResult<{ matched: boolean }>> {
|
||||
const result = await this.remote.commands.execute(this.sessionId, line, [])
|
||||
@@ -420,7 +357,7 @@ export class Session implements SessionFace {
|
||||
try {
|
||||
await events.prepend({ beforeSeq: this.baseSeq, maxMessages: PAGE_MESSAGES })
|
||||
} catch (error) {
|
||||
if (sessionStreamFailure(error) === undefined) {
|
||||
if (!isRemoteFailure(error)) {
|
||||
console.error('[session-controller] loadOlder failed:', error)
|
||||
}
|
||||
} finally {
|
||||
@@ -600,9 +537,10 @@ export class Session implements SessionFace {
|
||||
this.openState = 'open'
|
||||
} catch (error) {
|
||||
if (generation !== this.openGeneration || this.events !== events) return
|
||||
if (!isRemoteFailure(error)) throw error
|
||||
this.events = undefined
|
||||
this.openState = 'error'
|
||||
this.openError = openFailure(error)
|
||||
this.openError = error
|
||||
} finally {
|
||||
if (generation === this.openGeneration) this.notifier.markDirty()
|
||||
}
|
||||
@@ -714,11 +652,12 @@ export class Session implements SessionFace {
|
||||
/** Publish a terminal background failure only while this stream still owns the Session. */
|
||||
private failEventStream(events: SessionEventStream, generation: number, error: unknown): void {
|
||||
if (generation !== this.openGeneration || this.events !== events) return
|
||||
if (!isRemoteFailure(error)) throw error
|
||||
this.openGeneration++
|
||||
this.events = undefined
|
||||
this.openPromise = null
|
||||
this.openState = 'error'
|
||||
this.openError = openFailure(error)
|
||||
this.openError = error
|
||||
void events.dispose()
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
@@ -774,17 +713,3 @@ function imageRefsIn(content: unknown): readonly ImageAttachmentRef[] {
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
/** Convert a terminal Session stream failure to the Client error vocabulary. */
|
||||
function openFailure(error: unknown): ClientFailure {
|
||||
const failure = sessionStreamFailure(error)
|
||||
if (failure !== undefined) return failure as SessionError
|
||||
const folded = transportResult<never>(error)
|
||||
/* v8 ignore next -- transportResult never returns an ok result. */
|
||||
if (folded.ok) throw new Error('transportResult returned an unexpected success')
|
||||
return folded.error
|
||||
}
|
||||
/** Narrow a generated Session Remote failure to its service-owned error vocabulary. */
|
||||
function toSessionResult<T>(result: RemoteResult<T>): ClientResult<T> {
|
||||
return result.ok ? result : { ok: false, error: result.error as SessionError }
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
/** Session-specific adapters for Gateway-owned Remote stream lifecycles. */
|
||||
|
||||
import type {} from '@deepseek-ai/dsh-api-session-controller/remote'
|
||||
import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import {
|
||||
RemoteJournalStream,
|
||||
RemoteSnapshotStream,
|
||||
RemoteStreamCarrierError,
|
||||
RemoteStreamError,
|
||||
type ClientRemote,
|
||||
type RemoteJournalChange,
|
||||
type RemoteJournalFrame,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
historyRecordLastSeq,
|
||||
} from './sessions/history-records.ts'
|
||||
import type { SessionEventLikeEntry, SessionLiveEventEntry } from './contract/events.ts'
|
||||
import type { SessionRemotes } from './sessions/remotes.ts'
|
||||
|
||||
export {
|
||||
SESSION_SEARCH_RESULT_LIMIT,
|
||||
@@ -61,7 +61,11 @@ function toSessionJournalChange(
|
||||
return { ...change, entries: historyEntries(change.entries) }
|
||||
case 'append': {
|
||||
if (change.entry.type !== 'event') {
|
||||
throw new Error('session live stream emitted a packed history record')
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
'session live stream emitted a packed history record',
|
||||
{},
|
||||
)
|
||||
}
|
||||
return {
|
||||
type: 'append',
|
||||
@@ -80,8 +84,6 @@ export type SessionControlStream = RemoteSnapshotStream<
|
||||
SessionControlDeltaFrame
|
||||
>
|
||||
|
||||
type SessionStreamRemote = Pick<ClientRemote, '$stream' | 'session'>
|
||||
|
||||
/** Domain sinks used by the Host-wide Session control stream. */
|
||||
export interface SessionControlStreamOptions {
|
||||
/** Apply a complete baseline or one later update. */
|
||||
@@ -109,7 +111,7 @@ export interface SessionEventStreamOptions {
|
||||
* @returns an unstarted stream owned by the Client Session runtime.
|
||||
*/
|
||||
export function createSessionControlStream(
|
||||
remote: SessionStreamRemote,
|
||||
remote: SessionRemotes,
|
||||
options: SessionControlStreamOptions,
|
||||
): SessionControlStream {
|
||||
const stream = remote.$stream<SessionControlFrame>({
|
||||
@@ -142,7 +144,7 @@ export class SessionEventStream extends RemoteJournalStream<
|
||||
* @param options - Session event-window destinations.
|
||||
*/
|
||||
constructor(
|
||||
private readonly remote: SessionStreamRemote,
|
||||
private readonly remote: SessionRemotes,
|
||||
private readonly address: SessionAddress,
|
||||
options: SessionEventStreamOptions,
|
||||
) {
|
||||
@@ -198,13 +200,7 @@ export class SessionEventStream extends RemoteJournalStream<
|
||||
{ address: this.address, throughSeq, ...request },
|
||||
signal,
|
||||
)
|
||||
if (!result.ok) {
|
||||
throw new RemoteStreamError(
|
||||
result.error.code,
|
||||
result.error.message,
|
||||
result.error.details,
|
||||
)
|
||||
}
|
||||
if (!result.ok) throw result.error
|
||||
return result.value
|
||||
}
|
||||
|
||||
@@ -215,13 +211,3 @@ export class SessionEventStream extends RemoteJournalStream<
|
||||
return request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover a Host Session failure from a Remote stream terminal error.
|
||||
* @param error - value thrown while opening or consuming a Session stream.
|
||||
* @returns the Host failure, or `undefined` for carrier and local failures.
|
||||
*/
|
||||
export function sessionStreamFailure(error: unknown): RemoteFailure | undefined {
|
||||
if (!(error instanceof RemoteStreamError)) return undefined
|
||||
return { code: error.code, message: error.message, details: error.details }
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent, ModelSelection as AgentModelSelection } from '@deepseek-ai/dsh-agent'
|
||||
import { PresetMountError, UnknownPresetError } from '@deepseek-ai/dsh-agent-presets'
|
||||
import { AttachmentError, admitEncodedImages } from '@deepseek-ai/dsh-attachment'
|
||||
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import {
|
||||
@@ -14,7 +13,8 @@ import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionHeader, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
|
||||
import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title'
|
||||
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time'
|
||||
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { Workspace } from '@deepseek-ai/dsh-workspace'
|
||||
import {
|
||||
ApiSessionAgentController,
|
||||
@@ -71,14 +71,14 @@ export class SessionCommandController {
|
||||
*/
|
||||
async create(request: SessionCreateRequest): Promise<SessionCreateValue> {
|
||||
if (request.workspaceId !== undefined && request.cwd !== undefined) {
|
||||
reject('bad-request', 'session.create accepts workspaceId or cwd, not both', {})
|
||||
throw new RemoteError('gateway/bad-request', 'session.create accepts workspaceId or cwd, not both', {})
|
||||
}
|
||||
const sessionId = request.sessionId ?? SessionId(`session-${randomUUID()}`)
|
||||
let workspace: Workspace | undefined
|
||||
if (request.workspaceId !== undefined) {
|
||||
workspace = this.ctx.workspaceRegistry.get(request.workspaceId)
|
||||
if (workspace === undefined) {
|
||||
reject('workspace-not-found', `workspace "${request.workspaceId}" not found`, {
|
||||
throw new RemoteError('workspace/not-found', `workspace "${request.workspaceId}" not found`, {
|
||||
workspaceId: request.workspaceId,
|
||||
})
|
||||
}
|
||||
@@ -99,8 +99,8 @@ export class SessionCommandController {
|
||||
try {
|
||||
await workspace.attachSession(sessionId)
|
||||
} catch (error) {
|
||||
reject(
|
||||
'workspace-attach-failed',
|
||||
throw new RemoteError(
|
||||
'session/workspace-attach-failed',
|
||||
`session "${sessionId}" was created but could not attach to workspace "${workspace.id}": ${String(error)}`,
|
||||
{ sessionId, workspaceId: workspace.id },
|
||||
)
|
||||
@@ -143,9 +143,9 @@ export class SessionCommandController {
|
||||
}
|
||||
return { selected: { ...selected } }
|
||||
} catch (error) {
|
||||
if (error instanceof TypertRemoteFailure) throw error
|
||||
reject(
|
||||
'model-unavailable',
|
||||
if (remoteErrorOf(error) !== undefined) throw error
|
||||
throw new RemoteError(
|
||||
'session/model-unavailable',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
{ provider: request.provider, model: request.model },
|
||||
)
|
||||
@@ -162,17 +162,17 @@ export class SessionCommandController {
|
||||
const agent = await this.resolveAgent(request.sessionId)
|
||||
const titles = this.ctx.get('sessionTitle')
|
||||
if (titles === undefined) {
|
||||
reject('internal', 'renaming is unavailable: this deployment mounts no session-title service', {})
|
||||
throw new RemoteError('gateway/internal', 'renaming is unavailable: this deployment mounts no session-title service', {})
|
||||
}
|
||||
try {
|
||||
const accepted = titles.rename(agent.session, request.title)
|
||||
return { title: accepted.title, seq: accepted.eventSeq }
|
||||
} catch (error) {
|
||||
if (error instanceof SessionTitleInvalidError) {
|
||||
reject('title-invalid', error.message, { sessionId: request.sessionId })
|
||||
throw new RemoteError('session/title-invalid', error.message, { sessionId: request.sessionId })
|
||||
}
|
||||
reject(
|
||||
'internal',
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
`failed to rename session "${request.sessionId}": ${String(error)}`,
|
||||
{},
|
||||
)
|
||||
@@ -187,7 +187,7 @@ export class SessionCommandController {
|
||||
async fork(request: SessionForkRequest): Promise<SessionForkValue> {
|
||||
if (request.atSeq !== undefined
|
||||
&& (!Number.isInteger(request.atSeq) || request.atSeq < 0)) {
|
||||
reject('bad-request', 'atSeq must be a non-negative integer', {})
|
||||
throw new RemoteError('gateway/bad-request', 'atSeq must be a non-negative integer', {})
|
||||
}
|
||||
let observed: SessionObservation
|
||||
try {
|
||||
@@ -195,12 +195,12 @@ export class SessionCommandController {
|
||||
} catch (error) {
|
||||
if (error instanceof SessionQueryError
|
||||
&& error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
|
||||
reject('session-not-found', `session "${request.sessionId}" not found`, {
|
||||
throw new RemoteError('session/not-found', `session "${request.sessionId}" not found`, {
|
||||
sessionId: request.sessionId,
|
||||
})
|
||||
}
|
||||
reject(
|
||||
'internal',
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
`fork source unavailable for session "${request.sessionId}": ${String(error)}`,
|
||||
{},
|
||||
)
|
||||
@@ -216,8 +216,8 @@ export class SessionCommandController {
|
||||
? source.events.findLast(event => event.type === 'turn/end')
|
||||
: undefined)
|
||||
if (boundary === undefined) {
|
||||
reject(
|
||||
'fork-unavailable',
|
||||
throw new RemoteError(
|
||||
'session/fork-unavailable',
|
||||
atSeq !== undefined && atSeq <= lastSeq
|
||||
? `session "${request.sessionId}" has not completed the turn containing event ${String(atSeq)}`
|
||||
: `session "${request.sessionId}" has no completed turn to fork from`,
|
||||
@@ -230,8 +230,8 @@ export class SessionCommandController {
|
||||
try {
|
||||
workspace = await this.forkWorkspace(source.header)
|
||||
} catch (error) {
|
||||
reject(
|
||||
'internal',
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
`failed to resolve fork workspace for session "${request.sessionId}": ${String(error)}`,
|
||||
{},
|
||||
)
|
||||
@@ -255,8 +255,8 @@ export class SessionCommandController {
|
||||
setup: composition.setup,
|
||||
})
|
||||
} catch (error) {
|
||||
reject(
|
||||
'internal',
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
`failed to fork session "${request.sessionId}": ${String(error)}`,
|
||||
{},
|
||||
)
|
||||
@@ -265,8 +265,8 @@ export class SessionCommandController {
|
||||
try {
|
||||
await workspace.attachSession(childId)
|
||||
} catch (error) {
|
||||
reject(
|
||||
'workspace-attach-failed',
|
||||
throw new RemoteError(
|
||||
'session/workspace-attach-failed',
|
||||
`session "${childId}" was forked but could not attach to workspace "${workspace.id}": ${String(error)}`,
|
||||
{ sessionId: childId, workspaceId: workspace.id },
|
||||
)
|
||||
@@ -285,8 +285,8 @@ export class SessionCommandController {
|
||||
? undefined
|
||||
: canonicalClientTimeZone(request.clientTimeZone)
|
||||
if (request.clientTimeZone !== undefined && clientTimeZone === undefined) {
|
||||
reject(
|
||||
'invalid-time-zone',
|
||||
throw new RemoteError(
|
||||
'session/invalid-time-zone',
|
||||
'clientTimeZone must be UTC or a valid IANA Area/Location name',
|
||||
{ value: request.clientTimeZone },
|
||||
)
|
||||
@@ -294,8 +294,8 @@ export class SessionCommandController {
|
||||
const agent = await this.resolveAgent(request.sessionId)
|
||||
const selection = this.agents.selectionFor(agent).current
|
||||
if (!routeServed(this.ctx, selection.provider)) {
|
||||
reject(
|
||||
'model-unavailable',
|
||||
throw new RemoteError(
|
||||
'session/model-unavailable',
|
||||
`no adapter serves provider "${selection.provider}"; select a model for this session`,
|
||||
{ provider: selection.provider, model: selection.model },
|
||||
)
|
||||
@@ -312,8 +312,8 @@ export class SessionCommandController {
|
||||
const current = this.agents.selectionFor(agent).current
|
||||
const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model)
|
||||
if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
|
||||
reject(
|
||||
'attachment-error',
|
||||
throw new RemoteError(
|
||||
'session/attachment-invalid',
|
||||
`Model "${current.model}" does not support image input.`,
|
||||
{ reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' },
|
||||
)
|
||||
@@ -324,11 +324,11 @@ export class SessionCommandController {
|
||||
if (request.mode === 'steer') agent.steer(message)
|
||||
else agent.followup(message)
|
||||
} catch (error) {
|
||||
if (error instanceof TypertRemoteFailure) throw error
|
||||
if (remoteErrorOf(error) !== undefined) throw error
|
||||
if (error instanceof AttachmentError) {
|
||||
reject('attachment-error', error.message, { reason: error.code })
|
||||
throw new RemoteError('session/attachment-invalid', error.message, { reason: error.code })
|
||||
}
|
||||
reject('agent-busy', 'prompt rejected', { reason: String(error) })
|
||||
throw new RemoteError('session/agent-busy', 'prompt rejected', { reason: String(error) })
|
||||
}
|
||||
return { accepted: true }
|
||||
}
|
||||
@@ -346,18 +346,18 @@ export class SessionCommandController {
|
||||
source = await this.readSessionState(request.sessionId)
|
||||
} catch (error) {
|
||||
if (error instanceof ApiSessionNotFound) {
|
||||
reject('session-not-found', error.message, { sessionId: request.sessionId })
|
||||
throw new RemoteError('session/not-found', error.message, { sessionId: request.sessionId })
|
||||
}
|
||||
reject(
|
||||
'internal',
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
`attachment authorization unavailable for session "${request.sessionId}": ${String(error)}`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
const ref = referencedImage(source.events, String(request.attachmentId))
|
||||
if (ref === undefined) {
|
||||
reject(
|
||||
'attachment-error',
|
||||
throw new RemoteError(
|
||||
'session/attachment-invalid',
|
||||
'Image is not referenced by this session.',
|
||||
{ reason: 'ATTACHMENT_NOT_REFERENCED' },
|
||||
)
|
||||
@@ -370,9 +370,9 @@ export class SessionCommandController {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AttachmentError) {
|
||||
reject('attachment-error', error.message, { reason: error.code })
|
||||
throw new RemoteError('session/attachment-invalid', error.message, { reason: error.code })
|
||||
}
|
||||
reject('internal', 'Unable to read image attachment.', {})
|
||||
throw new RemoteError('gateway/internal', 'Unable to read image attachment.', {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,18 +384,18 @@ export class SessionCommandController {
|
||||
updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue {
|
||||
if (request.action.kind === 'edit'
|
||||
&& request.action.content.some(block => block.type !== 'text')) {
|
||||
reject(
|
||||
'attachment-error',
|
||||
throw new RemoteError(
|
||||
'session/attachment-invalid',
|
||||
'queue edits accept text content only',
|
||||
{ reason: 'QUEUE_EDIT_NON_TEXT' },
|
||||
)
|
||||
}
|
||||
const agent = this.ctx.agents.get(request.sessionId)
|
||||
if (agent !== undefined && hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
||||
rejectFailure(apiSessionSubagentOwnershipError(request.sessionId))
|
||||
throw apiSessionSubagentOwnershipError(request.sessionId)
|
||||
}
|
||||
if (agent === undefined) {
|
||||
reject('queue-item-not-found', 'queued item is no longer pending', { itemId: request.itemId })
|
||||
throw new RemoteError('session/queue-item-not-found', 'queued item is no longer pending', { itemId: request.itemId })
|
||||
}
|
||||
const nextTurn = agent.inbox.nextTurn.find(message => message.id === request.itemId)
|
||||
const nextStep = agent.inbox.nextStep.find(message => message.id === request.itemId)
|
||||
@@ -403,11 +403,11 @@ export class SessionCommandController {
|
||||
? nextStep === undefined ? undefined : { target: 'next-step' as const, message: nextStep }
|
||||
: { target: 'next-turn' as const, message: nextTurn }
|
||||
if (located === undefined) {
|
||||
reject('queue-item-not-found', 'queued item is no longer pending', { itemId: request.itemId })
|
||||
throw new RemoteError('session/queue-item-not-found', 'queued item is no longer pending', { itemId: request.itemId })
|
||||
}
|
||||
const { target, message } = located
|
||||
if (request.action.kind === 'steer' && (target !== 'next-turn' || agent.status !== 'running')) {
|
||||
reject('steer-unavailable', 'current turn no longer accepts steering', { itemId: request.itemId })
|
||||
throw new RemoteError('session/steer-unavailable', 'current turn no longer accepts steering', { itemId: request.itemId })
|
||||
}
|
||||
if (request.action.kind === 'edit') {
|
||||
agent.inbox.replace(request.itemId, freezeMessage<UserMessage>({
|
||||
@@ -429,14 +429,14 @@ export class SessionCommandController {
|
||||
cancel(request: SessionCancelRequest): SessionCancelValue {
|
||||
const agent = this.ctx.agents.get(request.sessionId)
|
||||
if (agent === undefined) {
|
||||
reject(
|
||||
'session-not-found',
|
||||
throw new RemoteError(
|
||||
'session/not-found',
|
||||
`session "${request.sessionId}" not found (not attached)`,
|
||||
{ sessionId: request.sessionId },
|
||||
)
|
||||
}
|
||||
if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
||||
rejectFailure(apiSessionSubagentOwnershipError(request.sessionId))
|
||||
throw apiSessionSubagentOwnershipError(request.sessionId)
|
||||
}
|
||||
agent.cancel({ kind: 'user' }, { keepInbox: true })
|
||||
return { accepted: true }
|
||||
@@ -444,41 +444,30 @@ export class SessionCommandController {
|
||||
|
||||
private async resolveAgent(sessionId: SessionId): Promise<Agent> {
|
||||
const found = await this.agents.resolveAgent(sessionId)
|
||||
if ('error' in found) rejectFailure(found.error)
|
||||
if ('error' in found) throw found.error
|
||||
return found.agent
|
||||
}
|
||||
|
||||
private rejectCreation(sessionId: SessionId, error: unknown): never {
|
||||
if (remoteErrorOf(error) !== undefined) throw error
|
||||
if (error instanceof ApiSessionPresetConflict) {
|
||||
reject('agent-preset-conflict', error.message, {
|
||||
throw new RemoteError('agent-preset/conflict', error.message, {
|
||||
sessionId: error.sessionId,
|
||||
requestedPreset: error.requestedPreset,
|
||||
...(error.existingPreset === undefined ? {} : { existingPreset: error.existingPreset }),
|
||||
})
|
||||
}
|
||||
if (error instanceof UnknownPresetError) {
|
||||
reject('agent-preset-not-found', error.message, {
|
||||
agentPreset: error.presetId,
|
||||
available: [...error.available],
|
||||
})
|
||||
}
|
||||
if (error instanceof PresetMountError) {
|
||||
reject('agent-preset-invalid', error.message, {
|
||||
agentPreset: error.presetId,
|
||||
reason: error.reason,
|
||||
})
|
||||
}
|
||||
if (error instanceof ApiSessionCwdConflict) {
|
||||
reject('session-conflict', error.message, {
|
||||
throw new RemoteError('session/conflict', error.message, {
|
||||
sessionId: error.sessionId,
|
||||
requestedCwd: error.requestedCwd,
|
||||
...(error.existingCwd === undefined ? {} : { existingCwd: error.existingCwd }),
|
||||
})
|
||||
}
|
||||
if (error instanceof ApiSessionSubagentOwnership) {
|
||||
rejectFailure(apiSessionSubagentOwnershipError(error.sessionId))
|
||||
throw apiSessionSubagentOwnershipError(error.sessionId)
|
||||
}
|
||||
reject('internal', `failed to create session "${sessionId}": ${String(error)}`, {})
|
||||
throw new RemoteError('gateway/internal', `failed to create session "${sessionId}": ${String(error)}`, {})
|
||||
}
|
||||
|
||||
private async readSessionState(sessionId: SessionId): Promise<SessionReadState> {
|
||||
@@ -503,14 +492,6 @@ export class SessionCommandController {
|
||||
}
|
||||
}
|
||||
|
||||
function rejectFailure(error: { readonly code: string; readonly message: string; readonly details: object }): never {
|
||||
throw new TypertRemoteFailure(error)
|
||||
}
|
||||
|
||||
function reject(code: string, message: string, details: object): never {
|
||||
throw new TypertRemoteFailure({ code, message, details })
|
||||
}
|
||||
|
||||
async function durablePromptContent(
|
||||
ctx: Context,
|
||||
content: readonly SessionPromptRequest['content'][number][],
|
||||
@@ -580,18 +561,6 @@ function referencedImage(
|
||||
return undefined
|
||||
}
|
||||
|
||||
const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/
|
||||
|
||||
function canonicalClientTimeZone(value: string): string | undefined {
|
||||
if (value.length === 0 || value.trim() !== value
|
||||
|| (value !== 'UTC' && !IANA_TIME_ZONE.test(value))) return undefined
|
||||
try {
|
||||
return new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function routeServed(ctx: Context, provider: string): boolean {
|
||||
return ctx.llm.listProviders().some(entry => entry.id === provider)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { isChunkRow, packChunkRuns, type ChunkRow } from '@deepseek-ai/dsh-sessi
|
||||
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type {
|
||||
SessionAddress,
|
||||
SessionChunkRun,
|
||||
@@ -55,15 +55,15 @@ export class SessionHistoryController {
|
||||
const sourceLog = source.events
|
||||
const sourceCursor = sourceLog.at(-1)?.seq ?? -1
|
||||
if (request.throughSeq > sourceCursor) {
|
||||
reject(
|
||||
'bad-request',
|
||||
throw new RemoteError(
|
||||
'gateway/bad-request',
|
||||
`session page through seq ${String(request.throughSeq)} is past cursor ${String(sourceCursor)}`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
/* v8 ignore next -- Session and persistence validation guarantee a dense zero-based event prefix. */
|
||||
if (request.throughSeq >= 0 && sourceLog[request.throughSeq]?.seq !== request.throughSeq) {
|
||||
reject('internal', `session log does not contain through seq ${String(request.throughSeq)}`, {})
|
||||
throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(request.throughSeq)}`, {})
|
||||
}
|
||||
const page = paginate(
|
||||
sourceLog,
|
||||
@@ -155,7 +155,7 @@ export class SessionHistoryController {
|
||||
}
|
||||
if (item.seq < nextSeq) continue
|
||||
if (item.seq !== nextSeq) {
|
||||
reject('internal', `session event stream skipped seq ${String(nextSeq)}`, {})
|
||||
throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(nextSeq)}`, {})
|
||||
}
|
||||
nextSeq++
|
||||
yield entryFor(item)
|
||||
@@ -211,22 +211,22 @@ function projectionBlock(
|
||||
|
||||
function validatePageRequest(request: SessionPageRequest): void {
|
||||
if (!Number.isSafeInteger(request.throughSeq) || request.throughSeq < -1) {
|
||||
reject('bad-request', 'throughSeq must be an integer greater than or equal to -1', {})
|
||||
throw new RemoteError('gateway/bad-request', 'throughSeq must be an integer greater than or equal to -1', {})
|
||||
}
|
||||
if (request.beforeSeq !== undefined
|
||||
&& (!Number.isSafeInteger(request.beforeSeq) || request.beforeSeq < 0)) {
|
||||
reject('bad-request', 'beforeSeq must be a non-negative safe integer', {})
|
||||
throw new RemoteError('gateway/bad-request', 'beforeSeq must be a non-negative safe integer', {})
|
||||
}
|
||||
if (request.maxMessages !== undefined
|
||||
&& (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) {
|
||||
reject('bad-request', 'maxMessages must be a positive safe integer', {})
|
||||
throw new RemoteError('gateway/bad-request', 'maxMessages must be a positive safe integer', {})
|
||||
}
|
||||
}
|
||||
|
||||
function validateFollowRequest(request: SessionFollowRequest): void {
|
||||
if (request.maxMessages !== undefined
|
||||
&& (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) {
|
||||
reject('bad-request', 'maxMessages must be a positive safe integer', {})
|
||||
throw new RemoteError('gateway/bad-request', 'maxMessages must be a positive safe integer', {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,34 +241,34 @@ function validateAddress(
|
||||
): void {
|
||||
if (address.kind === 'session') {
|
||||
if (header.origin === 'subagent') {
|
||||
reject('agent-busy', 'subagent Sessions require their durable parent address', {
|
||||
throw new RemoteError('session/agent-busy', 'subagent Sessions require their durable parent address', {
|
||||
reason: 'use subagent delivery for this child session',
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (header.origin !== 'subagent' || header.parentSession !== address.parentSessionId) {
|
||||
reject('subagent-unauthorized', 'subagent does not belong to the supplied parent', {
|
||||
throw new RemoteError('subagent/unauthorized', 'subagent does not belong to the supplied parent', {
|
||||
childSessionId: address.childSessionId,
|
||||
})
|
||||
}
|
||||
const identity = projections?.values.subagent
|
||||
if (identity === null) {
|
||||
reject('subagent-catalog-diagnostic', 'subagent descriptor is corrupt', {
|
||||
throw new RemoteError('subagent/catalog-diagnostic', 'subagent descriptor is corrupt', {
|
||||
parentSessionId: address.parentSessionId,
|
||||
childSessionId: address.childSessionId,
|
||||
reason: 'corrupt',
|
||||
})
|
||||
}
|
||||
if (identity === undefined || identity.seq < (header.seedLength ?? 0)) {
|
||||
reject('subagent-catalog-diagnostic', 'subagent descriptor is unavailable', {
|
||||
throw new RemoteError('subagent/catalog-diagnostic', 'subagent descriptor is unavailable', {
|
||||
parentSessionId: address.parentSessionId,
|
||||
childSessionId: address.childSessionId,
|
||||
reason: 'unsupported',
|
||||
})
|
||||
}
|
||||
if (identity.mode !== address.mode) {
|
||||
reject('subagent-unauthorized', 'subagent mode does not match the supplied address', {
|
||||
throw new RemoteError('subagent/unauthorized', 'subagent mode does not match the supplied address', {
|
||||
childSessionId: address.childSessionId,
|
||||
})
|
||||
}
|
||||
@@ -276,18 +276,14 @@ function validateAddress(
|
||||
|
||||
function rejectNotFound(address: SessionAddress): never {
|
||||
if (address.kind === 'session') {
|
||||
reject('session-not-found', `session "${address.sessionId}" not found`, { sessionId: address.sessionId })
|
||||
throw new RemoteError('session/not-found', `session "${address.sessionId}" not found`, { sessionId: address.sessionId })
|
||||
}
|
||||
reject('subagent-not-found', 'subagent is unavailable', {
|
||||
throw new RemoteError('subagent/not-found', 'subagent is unavailable', {
|
||||
parentSessionId: address.parentSessionId,
|
||||
childSessionId: address.childSessionId,
|
||||
})
|
||||
}
|
||||
|
||||
function reject(code: string, message: string, details: object): never {
|
||||
throw new TypertRemoteFailure({ code, message, details })
|
||||
}
|
||||
|
||||
function paginate(
|
||||
events: readonly SessionEvent[],
|
||||
beforeSeq: number | undefined,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { errorChain } from '@deepseek-ai/dsh-llm'
|
||||
import { canOpenNativePath, openNativePath } from '@deepseek-ai/dsh-native-command'
|
||||
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
|
||||
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import {
|
||||
ApiSessionAgentController,
|
||||
inspectApiSession,
|
||||
@@ -264,7 +264,7 @@ export class SessionController extends TypertRemoteService {
|
||||
* @param request - path after best-effort Session workspace resolution.
|
||||
* @param signal - caller lifetime; abort terminates the native command.
|
||||
* @returns confirmation after the native opener accepts the path.
|
||||
* @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails.
|
||||
* @throws RemoteError when the request is invalid, cancelled, or the opener fails.
|
||||
*/
|
||||
@Remote('openWorkspacePath')
|
||||
async openWorkspacePath(
|
||||
@@ -272,27 +272,23 @@ export class SessionController extends TypertRemoteService {
|
||||
signal: AbortSignal,
|
||||
): Promise<SessionOpenWorkspacePathValue> {
|
||||
if (request.path.length === 0) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'bad-request',
|
||||
message: 'session.openWorkspacePath requires a non-empty path',
|
||||
details: {},
|
||||
})
|
||||
throw new RemoteError(
|
||||
'gateway/bad-request',
|
||||
'session.openWorkspacePath requires a non-empty path',
|
||||
{},
|
||||
)
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
try {
|
||||
await this.openPath(request.path, signal)
|
||||
return { opened: true }
|
||||
} catch (error: unknown) {
|
||||
if (signal.aborted) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'cancelled', message: 'path open was aborted', details: {},
|
||||
})
|
||||
}
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'internal',
|
||||
message: `path open failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
details: {},
|
||||
})
|
||||
if (signal.aborted) throw new RemoteError('gateway/cancelled', 'path open was aborted', {})
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
`path open failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-
|
||||
import type {} from '@deepseek-ai/dsh-session-projection'
|
||||
import type {} from '@deepseek-ai/dsh-session-projection-cache'
|
||||
import { SessionQueryError, type SessionSearchCursor } from '@deepseek-ai/dsh-session-query'
|
||||
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
SESSION_SEARCH_RESULT_LIMIT,
|
||||
@@ -226,8 +226,8 @@ export class ApiSessionList {
|
||||
signal.throwIfAborted()
|
||||
const provider = this.ctx.get('sessionQuery')
|
||||
if (provider === undefined) {
|
||||
reject(
|
||||
'internal',
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
'session search is unavailable: this deployment does not mount @deepseek-ai/dsh-session-query',
|
||||
{},
|
||||
)
|
||||
@@ -317,9 +317,9 @@ export class ApiSessionList {
|
||||
} catch (error: unknown) {
|
||||
signal.throwIfAborted()
|
||||
if (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_ABORTED') {
|
||||
reject('cancelled', 'session search was aborted', {})
|
||||
throw new RemoteError('gateway/cancelled', 'session search was aborted', {})
|
||||
}
|
||||
reject('internal', `session search failed: ${String(error)}`, {})
|
||||
throw new RemoteError('gateway/internal', `session search failed: ${String(error)}`, {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,25 +351,21 @@ export class ApiSessionList {
|
||||
function normalizeSearchQuery(query: string): string {
|
||||
const normalized = query.trim()
|
||||
if (normalized.length === 0) {
|
||||
reject('bad-request', 'session search query must not be empty', {})
|
||||
throw new RemoteError('gateway/bad-request', 'session search query must not be empty', {})
|
||||
}
|
||||
if (normalized.length > SESSION_SEARCH_QUERY_MAX_CHARS) {
|
||||
reject(
|
||||
'bad-request',
|
||||
throw new RemoteError(
|
||||
'gateway/bad-request',
|
||||
`session search query must contain at most ${SESSION_SEARCH_QUERY_MAX_CHARS} UTF-16 code units`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
if (normalized.includes('\0')) {
|
||||
reject('bad-request', 'session search query must not contain NUL', {})
|
||||
throw new RemoteError('gateway/bad-request', 'session search query must not contain NUL', {})
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function reject(code: string, message: string, details: object): never {
|
||||
throw new TypertRemoteFailure({ code, message, details })
|
||||
}
|
||||
|
||||
function updatedAt(header: SessionHeader, metadata: SessionListMetadata | undefined): number {
|
||||
return Math.max(header.createdAt, metadata?.lastPromptAt ?? 0)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { SessionQueryError } from '@deepseek-ai/dsh-session-query'
|
||||
import { isUserInvocable } from '@deepseek-ai/dsh-skill'
|
||||
import type { ScopeKey } from '@deepseek-ai/dsh-scope'
|
||||
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SkillListRequest, SkillListValue } from './types.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
@@ -30,7 +30,7 @@ export class SessionSkillCatalog extends TypertRemoteService {
|
||||
* @param request - Session identity whose cwd and preset select the catalog view.
|
||||
* @param signal - caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics.
|
||||
* @returns user-invocable skill metadata without loading skill bodies.
|
||||
* @throws TypertRemoteFailure when the Session cannot be inspected or no registry can serve it.
|
||||
* @throws RemoteError when the Session cannot be inspected or no registry can serve it.
|
||||
*/
|
||||
@Remote
|
||||
async list(request: SkillListRequest, signal: AbortSignal): Promise<SkillListValue> {
|
||||
@@ -48,19 +48,16 @@ export class SessionSkillCatalog extends TypertRemoteService {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SessionQueryError
|
||||
&& error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
|
||||
throw failure(
|
||||
'session-not-found',
|
||||
`session "${sessionId}" not found`,
|
||||
{ sessionId },
|
||||
)
|
||||
throw new RemoteError('session/not-found', `session "${sessionId}" not found`, { sessionId })
|
||||
}
|
||||
throw failure(
|
||||
'internal',
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
`session "${sessionId}" could not be inspected: ${String(error)}`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
if (cwd === undefined) {
|
||||
throw failure('internal', `session "${sessionId}" has no project cwd`)
|
||||
throw new RemoteError('gateway/internal', `session "${sessionId}" has no project cwd`, {})
|
||||
}
|
||||
|
||||
const live = this.ctx.agents.get(sessionId)
|
||||
@@ -68,9 +65,10 @@ export class SessionSkillCatalog extends TypertRemoteService {
|
||||
const scoped = live === undefined ? undefined : presets?.serviceFor(live, 'skills')
|
||||
const skillRegistry = scoped ?? this.ctx.get('skills')
|
||||
if (skillRegistry === undefined) {
|
||||
throw failure(
|
||||
'internal',
|
||||
throw new RemoteError(
|
||||
'gateway/internal',
|
||||
'skill registry is absent: neither this session\'s agent preset nor the host composition mounts @deepseek-ai/dsh-skill',
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +84,7 @@ export class SessionSkillCatalog extends TypertRemoteService {
|
||||
})),
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
throw failure('internal', `skill listing failed: ${String(error)}`)
|
||||
throw new RemoteError('gateway/internal', `skill listing failed: ${String(error)}`, {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,13 +106,4 @@ export class SessionSkillCatalog extends TypertRemoteService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Build one stable Remote failure with optional typed details. */
|
||||
function failure(
|
||||
code: 'session-not-found' | 'internal',
|
||||
message: string,
|
||||
details: { readonly sessionId: SessionId } | Record<never, never> = {},
|
||||
): TypertRemoteFailure {
|
||||
return new TypertRemoteFailure({ code, message, details })
|
||||
}
|
||||
|
||||
export default SessionSkillCatalog
|
||||
@@ -174,55 +174,39 @@ export const SESSION_SEARCH_RESULT_LIMIT = 20
|
||||
/** Maximum search snippet length in Unicode code points. */
|
||||
export const SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS = 240
|
||||
|
||||
/** Error details returned by Session Remote methods. */
|
||||
export interface SessionErrorDetailsMap {
|
||||
'bad-request': Record<never, never>
|
||||
cancelled: Record<never, never>
|
||||
'session-not-found': { readonly sessionId: SessionId }
|
||||
'model-unavailable': { readonly provider: string; readonly model: string }
|
||||
'session-conflict': {
|
||||
readonly sessionId: SessionId
|
||||
readonly requestedCwd: string
|
||||
readonly existingCwd?: string
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
'session/model-unavailable': { readonly provider: string; readonly model: string }
|
||||
'session/conflict': {
|
||||
readonly sessionId: SessionId
|
||||
readonly requestedCwd: string
|
||||
readonly existingCwd?: string
|
||||
}
|
||||
'session/agent-busy': { readonly reason: string }
|
||||
'session/invalid-time-zone': { readonly value: string }
|
||||
'session/workspace-attach-failed': { readonly sessionId: SessionId; readonly workspaceId: string }
|
||||
'agent-preset/conflict': {
|
||||
readonly sessionId: SessionId
|
||||
readonly requestedPreset: string
|
||||
readonly existingPreset?: string
|
||||
}
|
||||
'session/attachment-invalid': { readonly reason: string }
|
||||
'session/queue-item-not-found': { readonly itemId: MessageId }
|
||||
'session/steer-unavailable': { readonly itemId: MessageId }
|
||||
'session/title-invalid': { readonly sessionId: SessionId }
|
||||
'session/fork-unavailable': { readonly sessionId: SessionId }
|
||||
'subagent/not-found': {
|
||||
readonly parentSessionId: SessionId
|
||||
readonly childSessionId: SessionId
|
||||
}
|
||||
'subagent/catalog-diagnostic': {
|
||||
readonly parentSessionId: SessionId
|
||||
readonly childSessionId: SessionId
|
||||
readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
|
||||
}
|
||||
}
|
||||
'invalid-time-zone': { readonly value: string }
|
||||
'workspace-attach-failed': { readonly sessionId: SessionId; readonly workspaceId: string }
|
||||
'workspace-not-found': { readonly workspaceId: string }
|
||||
'agent-preset-conflict': {
|
||||
readonly sessionId: SessionId
|
||||
readonly requestedPreset: string
|
||||
readonly existingPreset?: string
|
||||
}
|
||||
'agent-preset-not-found': { readonly agentPreset: string; readonly available: readonly string[] }
|
||||
'agent-preset-invalid': { readonly agentPreset: string; readonly reason: string }
|
||||
'agent-busy': { readonly reason: string }
|
||||
'attachment-error': { readonly reason: string }
|
||||
'queue-item-not-found': { readonly itemId: MessageId }
|
||||
'steer-unavailable': { readonly itemId: MessageId }
|
||||
'title-invalid': { readonly sessionId: SessionId }
|
||||
'fork-unavailable': { readonly sessionId: SessionId }
|
||||
'subagent-not-found': {
|
||||
readonly parentSessionId: SessionId
|
||||
readonly childSessionId: SessionId
|
||||
}
|
||||
'subagent-catalog-diagnostic': {
|
||||
readonly parentSessionId: SessionId
|
||||
readonly childSessionId: SessionId
|
||||
readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
|
||||
}
|
||||
'subagent-unauthorized': { readonly childSessionId: SessionId }
|
||||
internal: Record<never, never>
|
||||
}
|
||||
|
||||
/** Session business failure returned without throwing a carrier error. */
|
||||
export type SessionError = {
|
||||
[Code in keyof SessionErrorDetailsMap]: {
|
||||
readonly code: Code
|
||||
readonly message: string
|
||||
readonly details: SessionErrorDetailsMap[Code]
|
||||
}
|
||||
}[keyof SessionErrorDetailsMap]
|
||||
|
||||
/** Session-addressed request for the human-invocable skill catalog. */
|
||||
export interface SkillListRequest {
|
||||
readonly sessionId: SessionId
|
||||
|
||||
@@ -8,7 +8,6 @@ import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-presets'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
|
||||
import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
@@ -154,7 +153,7 @@ describe('ApiSession Agent lookup and recovery', () => {
|
||||
header: header('observed-without-cwd', null),
|
||||
} as SessionObservation
|
||||
await expect(agents.resolveObservedAgent(invalid)).resolves.toMatchObject({
|
||||
error: { code: 'session-not-found' },
|
||||
error: { code: 'session/not-found' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -170,7 +169,7 @@ describe('ApiSession Agent lookup and recovery', () => {
|
||||
if (host === undefined) throw new Error('Agent Context resolver was not registered')
|
||||
|
||||
await expect(host.resolve(live.id)).resolves.toBe(live.ctx)
|
||||
await expect(host.resolve(SessionId('missing'))).rejects.toBeInstanceOf(TypertLookupFailure)
|
||||
await expect(host.resolve(SessionId('missing'))).rejects.toMatchObject({ code: 'session/not-found' })
|
||||
})
|
||||
|
||||
it('returns raced ordinary Agents and ownership failures after resume throws', async () => {
|
||||
@@ -200,7 +199,7 @@ describe('ApiSession Agent lookup and recovery', () => {
|
||||
throw new Error('raced child publication')
|
||||
})
|
||||
await expect(child.agents.resolveAgent(childMeta.id)).resolves.toMatchObject({
|
||||
error: { code: 'agent-busy' },
|
||||
error: { code: 'session/agent-busy' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -211,7 +210,7 @@ describe('ApiSession Agent lookup and recovery', () => {
|
||||
inspect: vi.fn(),
|
||||
})
|
||||
await expect(missing.agents.resolveAgent(SessionId('missing'))).resolves.toMatchObject({
|
||||
error: { code: 'session-not-found' },
|
||||
error: { code: 'session/not-found' },
|
||||
})
|
||||
|
||||
const failed = await harness()
|
||||
@@ -222,7 +221,7 @@ describe('ApiSession Agent lookup and recovery', () => {
|
||||
})
|
||||
vi.spyOn(failed.ctx.agents, 'resume').mockRejectedValue(new Error('factory unavailable'))
|
||||
await expect(failed.agents.resolveAgent(meta.id)).resolves.toMatchObject({
|
||||
error: { code: 'internal', message: expect.stringContaining('factory unavailable') as string },
|
||||
error: { code: 'gateway/internal', message: expect.stringContaining('factory unavailable') as string },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -408,7 +407,7 @@ describe('ApiSession create or adoption', () => {
|
||||
mount: () => Promise.resolve(),
|
||||
} as never)
|
||||
await expect(child.agents.resolveAgent(childMeta.id)).resolves.toMatchObject({
|
||||
error: { code: 'agent-busy' },
|
||||
error: { code: 'session/agent-busy' },
|
||||
})
|
||||
|
||||
const conflict = await harness()
|
||||
|
||||
@@ -63,12 +63,14 @@ async function mount(initialGeneration?: ConnectionGeneration): Promise<Bench> {
|
||||
registerGenerationSource: () => () => {},
|
||||
start: () => ({ stop: () => {} }),
|
||||
}
|
||||
ctx.reflect.provide('connection', connection)
|
||||
ctx.reflect.provide('remote', {
|
||||
...remote,
|
||||
$stream: <Item>(options: RemoteStreamOptions<Item>) => (
|
||||
new RemoteStream(connection, options)
|
||||
),
|
||||
get $host() {
|
||||
return { home: generation?.host.home, isLoopback: connection.isLoopback }
|
||||
},
|
||||
$on: (event: string, listener: RemoteListener) => {
|
||||
const eventListeners = listeners.get(event) ?? new Set<RemoteListener>()
|
||||
eventListeners.add(listener)
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
MutableSessionEventSource, type SessionLiveEventEntry,
|
||||
} from '../src/client/contract/events.ts'
|
||||
import { transportResult } from '../src/client/contract/result.ts'
|
||||
|
||||
function entry(seq: number): SessionLiveEventEntry {
|
||||
return {
|
||||
@@ -76,14 +75,4 @@ describe('Client Session contracts', () => {
|
||||
expect(iterate).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('folds Error and non-Error carrier rejections into Client failures', () => {
|
||||
expect(transportResult(new Error('transport unavailable'))).toEqual({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: 'transport unavailable', details: {} },
|
||||
})
|
||||
expect(transportResult(404)).toEqual({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: '404', details: {} },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
|
||||
import { PresetMountError } from '@deepseek-ai/dsh-agent-presets'
|
||||
import type {} from '@deepseek-ai/dsh-agent-presets'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { Workspace, WorkspaceId } from '@deepseek-ai/dsh-workspace'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
@@ -14,7 +15,7 @@ import { SessionCommandController } from '../src/commands.ts'
|
||||
import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts'
|
||||
|
||||
async function expectFailure(operation: Promise<unknown>, code: string): Promise<void> {
|
||||
await expect(operation).rejects.toMatchObject({ failure: { code } })
|
||||
await expect(operation).rejects.toMatchObject({ code })
|
||||
}
|
||||
|
||||
function controllerAgents(overrides: object = {}): ApiSessionAgentController {
|
||||
@@ -76,7 +77,7 @@ describe('Session creation failures', () => {
|
||||
)
|
||||
await expectFailure(missingController.create({
|
||||
workspaceId: 'missing' as WorkspaceId,
|
||||
}), 'workspace-not-found')
|
||||
}), 'workspace/not-found')
|
||||
await missing.fiber.dispose()
|
||||
|
||||
const failed = await baseContext()
|
||||
@@ -97,26 +98,30 @@ describe('Session creation failures', () => {
|
||||
await expectFailure(failedController.create({
|
||||
sessionId: SessionId('workspace-session'),
|
||||
workspaceId: workspace.id,
|
||||
}), 'workspace-attach-failed')
|
||||
}), 'session/workspace-attach-failed')
|
||||
await failed.fiber.dispose()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
error: new PresetMountError('broken', 'invalid composition'),
|
||||
code: 'agent-preset-invalid',
|
||||
error: new RemoteError(
|
||||
'agent-preset/invalid',
|
||||
'agent-presets: preset "broken" failed to mount: invalid composition',
|
||||
{ agentPreset: 'broken', reason: 'invalid composition' },
|
||||
),
|
||||
code: 'agent-preset/invalid',
|
||||
},
|
||||
{
|
||||
error: new ApiSessionCwdConflict(SessionId('cwd-less'), '/requested', undefined),
|
||||
code: 'session-conflict',
|
||||
code: 'session/conflict',
|
||||
},
|
||||
{
|
||||
error: new ApiSessionCwdConflict(SessionId('wrong-cwd'), '/requested', '/stored'),
|
||||
code: 'session-conflict',
|
||||
code: 'session/conflict',
|
||||
},
|
||||
{
|
||||
error: new Error('factory unavailable'),
|
||||
code: 'internal',
|
||||
code: 'gateway/internal',
|
||||
},
|
||||
])('maps $code creation failures', async ({ error, code }) => {
|
||||
const ctx = await baseContext()
|
||||
@@ -140,7 +145,7 @@ describe('Session creation failures', () => {
|
||||
await expectFailure(controller.create({
|
||||
workspaceId: 'workspace-1' as WorkspaceId,
|
||||
cwd: '/workspace',
|
||||
}), 'bad-request')
|
||||
}), 'gateway/bad-request')
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -179,7 +184,7 @@ describe('Session fork failures', () => {
|
||||
)
|
||||
await expectFailure(unavailableController.fork({
|
||||
sessionId: SessionId('missing'),
|
||||
}), 'session-not-found')
|
||||
}), 'session/not-found')
|
||||
await withoutPersistence.fiber.dispose()
|
||||
|
||||
const missing = await baseContext()
|
||||
@@ -191,7 +196,7 @@ describe('Session fork failures', () => {
|
||||
const missingController = new SessionCommandController(missing, controllerAgents(), '/default')
|
||||
await expectFailure(missingController.fork({
|
||||
sessionId: SessionId('missing'),
|
||||
}), 'session-not-found')
|
||||
}), 'session/not-found')
|
||||
await missing.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -201,7 +206,7 @@ describe('Session fork failures', () => {
|
||||
vi.spyOn(ctx.sessionQuery, 'observeSession').mockRejectedValue(new Error('storage offline'))
|
||||
const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
|
||||
|
||||
await expectFailure(controller.fork({ sessionId: SessionId('unreadable') }), 'internal')
|
||||
await expectFailure(controller.fork({ sessionId: SessionId('unreadable') }), 'gateway/internal')
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -211,7 +216,7 @@ describe('Session fork failures', () => {
|
||||
const source = ctx.sessions.create(SessionId('empty-source'))
|
||||
const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
|
||||
|
||||
await expectFailure(controller.fork({ sessionId: source.id }), 'fork-unavailable')
|
||||
await expectFailure(controller.fork({ sessionId: source.id }), 'session/fork-unavailable')
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -225,7 +230,7 @@ describe('Session fork failures', () => {
|
||||
origin: 'subagent',
|
||||
})
|
||||
const lineageController = new SessionCommandController(lineage, controllerAgents(), '/default')
|
||||
await expectFailure(lineageController.fork({ sessionId: child.id }), 'internal')
|
||||
await expectFailure(lineageController.fork({ sessionId: child.id }), 'gateway/internal')
|
||||
await lineage.fiber.dispose()
|
||||
|
||||
const creation = await baseContext()
|
||||
@@ -233,7 +238,7 @@ describe('Session fork failures', () => {
|
||||
const source = completedSession(creation, 'creation-source', '/workspace')
|
||||
vi.spyOn(creation.agents, 'create').mockRejectedValue(new Error('factory failed'))
|
||||
const creationController = new SessionCommandController(creation, controllerAgents(), '/default')
|
||||
await expectFailure(creationController.fork({ sessionId: source.id }), 'internal')
|
||||
await expectFailure(creationController.fork({ sessionId: source.id }), 'gateway/internal')
|
||||
await creation.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -251,7 +256,7 @@ describe('Session fork failures', () => {
|
||||
)
|
||||
const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
|
||||
|
||||
await expectFailure(controller.fork({ sessionId: source.id }), 'workspace-attach-failed')
|
||||
await expectFailure(controller.fork({ sessionId: source.id }), 'session/workspace-attach-failed')
|
||||
const options = create.mock.calls[0]?.[0]
|
||||
if (options === undefined) throw new Error('Agent creation was not attempted')
|
||||
expect(options.meta).not.toHaveProperty('cwd')
|
||||
|
||||
@@ -56,7 +56,7 @@ async function commandHarness(): Promise<{
|
||||
}
|
||||
|
||||
async function expectFailure(operation: Promise<unknown>, code: string): Promise<void> {
|
||||
await expect(operation).rejects.toMatchObject({ failure: { code } })
|
||||
await expect(operation).rejects.toMatchObject({ code })
|
||||
}
|
||||
|
||||
describe('Session queue commands', () => {
|
||||
@@ -79,21 +79,21 @@ describe('Session queue commands', () => {
|
||||
},
|
||||
}],
|
||||
},
|
||||
})), 'attachment-error')
|
||||
})), 'session/attachment-invalid')
|
||||
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
|
||||
sessionId: SessionId('missing'), itemId: queued.id, action: { kind: 'remove' },
|
||||
})), 'queue-item-not-found')
|
||||
})), 'session/queue-item-not-found')
|
||||
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
|
||||
sessionId: agent.id, itemId: MessageId('missing'), action: { kind: 'remove' },
|
||||
})), 'queue-item-not-found')
|
||||
})), 'session/queue-item-not-found')
|
||||
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
|
||||
sessionId: agent.id, itemId: nextStep.id, action: { kind: 'steer' },
|
||||
})), 'steer-unavailable')
|
||||
})), 'session/steer-unavailable')
|
||||
|
||||
Object.assign(agent, { status: 'idle' })
|
||||
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
|
||||
sessionId: agent.id, itemId: queued.id, action: { kind: 'steer' },
|
||||
})), 'steer-unavailable')
|
||||
})), 'session/steer-unavailable')
|
||||
expect(controller.updateQueue({
|
||||
sessionId: agent.id,
|
||||
itemId: queued.id,
|
||||
@@ -114,7 +114,7 @@ describe('Session queue commands', () => {
|
||||
|
||||
await expectFailure(Promise.resolve().then(() => controller.cancel({
|
||||
sessionId: SessionId('missing'),
|
||||
})), 'session-not-found')
|
||||
})), 'session/not-found')
|
||||
expect(controller.cancel({ sessionId: agent.id })).toEqual({ accepted: true })
|
||||
expect(cancel).toHaveBeenCalledWith({ kind: 'user' }, { keepInbox: true })
|
||||
await ctx.fiber.dispose()
|
||||
@@ -209,7 +209,7 @@ describe('Session attachment authorization', () => {
|
||||
)
|
||||
await expectFailure(noPersistenceController.attachment({
|
||||
sessionId: SessionId('missing'), attachmentId: AttachmentId('att'),
|
||||
}), 'session-not-found')
|
||||
}), 'session/not-found')
|
||||
|
||||
const missing = new Context()
|
||||
await missing.plugin(SessionStore)
|
||||
@@ -225,7 +225,7 @@ describe('Session attachment authorization', () => {
|
||||
)
|
||||
await expectFailure(missingController.attachment({
|
||||
sessionId: SessionId('missing'), attachmentId: 'att' as never,
|
||||
}), 'session-not-found')
|
||||
}), 'session/not-found')
|
||||
|
||||
for (const thrown of [
|
||||
new AttachmentError('stored image is unavailable', 'ATTACHMENT_NOT_FOUND'),
|
||||
@@ -239,7 +239,7 @@ describe('Session attachment authorization', () => {
|
||||
await expectFailure(fixture.controller.attachment({
|
||||
sessionId: fixture.sessionId,
|
||||
attachmentId: ref.attachmentId,
|
||||
}), thrown instanceof AttachmentError ? 'attachment-error' : 'internal')
|
||||
}), thrown instanceof AttachmentError ? 'session/attachment-invalid' : 'gateway/internal')
|
||||
await fixture.ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
@@ -257,7 +257,7 @@ describe('Session attachment authorization', () => {
|
||||
|
||||
await expectFailure(controller.attachment({
|
||||
sessionId: SessionId('unreadable'), attachmentId: AttachmentId('att'),
|
||||
}), 'internal')
|
||||
}), 'gateway/internal')
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import SessionController from '../src/index.ts'
|
||||
import type { ApiSessionAgentController } from '../src/agent.ts'
|
||||
@@ -124,7 +125,7 @@ describe('SessionController facade', () => {
|
||||
if (outcome === 'success') resolve.mockResolvedValue({ agent: live })
|
||||
else if (outcome === 'domain-error') {
|
||||
resolve.mockResolvedValue({
|
||||
error: { code: 'internal', message: 'activation unavailable', details: {} },
|
||||
error: new RemoteError('gateway/internal', 'activation unavailable', {}),
|
||||
})
|
||||
} else {
|
||||
resolve.mockRejectedValue(new Error('activation crashed'))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// deferred-controlled timing). Session streams are hand pumps: pushFollow/pushControl.
|
||||
import type {
|
||||
MessageId,
|
||||
RpcError, RpcResponse, SessionId, SessionSearchItem,
|
||||
SessionId, SessionSearchItem,
|
||||
SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt,
|
||||
WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
@@ -24,10 +24,8 @@ import type { WorkspaceFollowFrame } from '@deepseek-ai/dsh-api-workspace-contro
|
||||
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import {
|
||||
RemoteStream,
|
||||
RemoteStreamError,
|
||||
type RemoteStreamOptions,
|
||||
} from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionRemotes } from '../src/client/sessions/remotes.ts'
|
||||
import { historyRecordLastSeq } from '../src/client/sessions/history-records.ts'
|
||||
|
||||
@@ -72,28 +70,21 @@ export function deferred<T>(): Deferred<T> {
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
let nextRpc = 0
|
||||
|
||||
export function ok<T>(value: T): RpcResponse<T> {
|
||||
return { rpcId: RpcId(`fake-${nextRpc++}`), result: { ok: true, value } }
|
||||
}
|
||||
|
||||
export function err<T>(error: RpcError): RpcResponse<T> {
|
||||
return { rpcId: RpcId(`fake-${nextRpc++}`), result: { ok: false, error } }
|
||||
}
|
||||
|
||||
/** Successful generated Remote result for programmable domain fakes. */
|
||||
export function remoteOk<T>(value: T): RemoteResult<T> {
|
||||
/**
|
||||
* Successful generated Remote result for programmable domain fakes.
|
||||
* @param value - the value the Host answers with.
|
||||
* @returns the success branch of a Remote result.
|
||||
*/
|
||||
export function ok<T>(value: T): RemoteResult<T> {
|
||||
return { ok: true, value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed generated Remote result carrying an owner's own failure vocabulary,
|
||||
* which the carrier's closed RPC code set does not contain.
|
||||
* Failed generated Remote result carrying the owner's declared failure.
|
||||
* @param error - the owner-declared failure.
|
||||
* @returns the failure branch of a Remote result.
|
||||
*/
|
||||
export function remoteErr<T>(error: RemoteFailure): RemoteResult<T> {
|
||||
export function err<T>(error: RemoteFailure): RemoteResult<T> {
|
||||
return { ok: false, error }
|
||||
}
|
||||
|
||||
@@ -129,11 +120,11 @@ export class FakeApiClient {
|
||||
readonly followStarts: SessionId[] = []
|
||||
|
||||
// Programmable slots (defaults answer OK-empty); reassign per case.
|
||||
onList: (payload: unknown) => Promise<RpcResponse<{ items: never[] }>> = () => Promise.resolve(ok({ items: [] }))
|
||||
onSearch: (payload: unknown) => Promise<RpcResponse<{ items: SessionSearchItem[]; hasMore: boolean }>> =
|
||||
onList: (payload: unknown) => Promise<RemoteResult<{ items: never[] }>> = () => Promise.resolve(ok({ items: [] }))
|
||||
onSearch: (payload: unknown) => Promise<RemoteResult<{ items: SessionSearchItem[]; hasMore: boolean }>> =
|
||||
() => Promise.resolve(ok({ items: [], hasMore: false }))
|
||||
onCreate: (payload: unknown) => Promise<RpcResponse<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-new' as SessionId }))
|
||||
onSelectModel: (payload: SessionSelectModelRequest) => Promise<RpcResponse<SessionSelectModelValue>> =
|
||||
onCreate: (payload: unknown) => Promise<RemoteResult<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-new' as SessionId }))
|
||||
onSelectModel: (payload: SessionSelectModelRequest) => Promise<RemoteResult<SessionSelectModelValue>> =
|
||||
payload => Promise.resolve(ok({
|
||||
selected: {
|
||||
provider: payload.provider,
|
||||
@@ -143,19 +134,19 @@ export class FakeApiClient {
|
||||
: { reasoningEffort: payload.reasoningEffort }),
|
||||
},
|
||||
}))
|
||||
onRename: (payload: unknown) => Promise<RpcResponse<{ title: string; seq: number }>> = () => Promise.resolve(ok({ title: 'fk-renamed', seq: 0 }))
|
||||
onFork: (payload: unknown) => Promise<RpcResponse<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-fork' as SessionId }))
|
||||
onRename: (payload: unknown) => Promise<RemoteResult<{ title: string; seq: number }>> = () => Promise.resolve(ok({ title: 'fk-renamed', seq: 0 }))
|
||||
onFork: (payload: unknown) => Promise<RemoteResult<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-fork' as SessionId }))
|
||||
onHistory: (payload: { sessionId: SessionId; throughSeq?: number; beforeSeq?: number; maxMessages?: number })
|
||||
=> Promise<RpcResponse<SessionPage & { readonly projections?: SessionProjectionBaseline }>> =
|
||||
=> Promise<RemoteResult<SessionPage & { readonly projections?: SessionProjectionBaseline }>> =
|
||||
() => Promise.resolve(ok({ records: [], hasMore: false }))
|
||||
|
||||
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onAttachment: (payload: unknown) => Promise<RpcResponse<{ attachment: { attachmentId: never; mediaType: 'image/png'; bytes: number; width: number; height: number }; data: string }>> =
|
||||
onPrompt: (payload: unknown) => Promise<RemoteResult<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onAttachment: (payload: unknown) => Promise<RemoteResult<{ attachment: { attachmentId: never; mediaType: 'image/png'; bytes: number; width: number; height: number }; data: string }>> =
|
||||
() => Promise.resolve(ok({ attachment: { attachmentId: 'a' as never, mediaType: 'image/png', bytes: 1, width: 1, height: 1 }, data: 'AA==' }))
|
||||
onUpdateQueue: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onCancel: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onUpdateQueue: (payload: unknown) => Promise<RemoteResult<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onCancel: (payload: unknown) => Promise<RemoteResult<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onOpenWorkspacePath: (payload: unknown) => Promise<RemoteResult<{ opened: true }>> =
|
||||
() => Promise.resolve(remoteOk({ opened: true as const }))
|
||||
() => Promise.resolve(ok({ opened: true as const }))
|
||||
|
||||
private readonly followConns = new Map<SessionId, ValueStreamConn<SessionFollowFrame>[]>()
|
||||
private readonly controlConns: ValueStreamConn<SessionControlFrame>[] = []
|
||||
@@ -174,30 +165,30 @@ export class FakeApiClient {
|
||||
lastSearchSignal: AbortSignal | undefined
|
||||
|
||||
onSubagentList: (payload: unknown) => Promise<RemoteResult<SubagentCatalog>>
|
||||
= () => Promise.resolve(remoteOk({ entries: [], parentAvailable: true }))
|
||||
= () => Promise.resolve(ok({ entries: [], parentAvailable: true }))
|
||||
onSubagentPrompt: (payload: unknown) => Promise<RemoteResult<SubagentPromptReceipt>>
|
||||
= () => Promise.resolve(remoteOk({ messageId: 'fake-message' as MessageId }))
|
||||
= () => Promise.resolve(ok({ messageId: 'fake-message' as MessageId }))
|
||||
|
||||
onSubagentInterrupt: (payload: unknown) => Promise<RemoteResult<SubagentInterruptReceipt>>
|
||||
= () => Promise.resolve(remoteOk({ accepted: true as const }))
|
||||
= () => Promise.resolve(ok({ accepted: true as const }))
|
||||
|
||||
onWorkspaceCreate: (payload: unknown) => Promise<RemoteResult<{ workspace: WorkspaceView; created: boolean }>> =
|
||||
() => Promise.resolve(remoteOk({ workspace: fakeWorkspace('fk-ws'), created: true }))
|
||||
() => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws'), created: true }))
|
||||
|
||||
onWorkspaceRename: (payload: unknown) => Promise<RemoteResult<{ workspace: WorkspaceView }>> =
|
||||
() => Promise.resolve(remoteOk({ workspace: fakeWorkspace('fk-ws') }))
|
||||
() => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws') }))
|
||||
|
||||
onWorkspaceDelete: (payload: unknown) => Promise<RemoteResult<{ deleted: true }>> =
|
||||
() => Promise.resolve(remoteOk({ deleted: true }))
|
||||
() => Promise.resolve(ok({ deleted: true }))
|
||||
|
||||
onWorkspaceInsertBefore: (payload: unknown) => Promise<RemoteResult<{ workspaceIds: WorkspaceId[] }>> =
|
||||
() => Promise.resolve(remoteOk({ workspaceIds: [] }))
|
||||
() => Promise.resolve(ok({ workspaceIds: [] }))
|
||||
|
||||
onWorkspaceInsertSessionBefore: (payload: unknown) => Promise<RemoteResult<{ workspace: WorkspaceView }>> =
|
||||
() => Promise.resolve(remoteOk({ workspace: fakeWorkspace('fk-ws') }))
|
||||
() => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws') }))
|
||||
|
||||
onWorkspaceArchiveSession: (payload: unknown) => Promise<RemoteResult<{ archivedSessionIds: SessionId[] }>> =
|
||||
payload => Promise.resolve(remoteOk({ archivedSessionIds: [(payload as { sessionId: SessionId }).sessionId] }))
|
||||
payload => Promise.resolve(ok({ archivedSessionIds: [(payload as { sessionId: SessionId }).sessionId] }))
|
||||
|
||||
/** Remote namespaces bound to this fake's programmable unary slots and stream pumps. */
|
||||
sessionRemotes(): RuntimeRemotes {
|
||||
@@ -209,8 +200,8 @@ export class FakeApiClient {
|
||||
execute: () => Promise.resolve({ ok: true, value: undefined }),
|
||||
},
|
||||
session: {
|
||||
canOpenWorkspacePath: () => Promise.resolve(remoteOk(true)),
|
||||
list: payload => this.remoteResult('session.list', payload, this.onList(payload)),
|
||||
canOpenWorkspacePath: () => Promise.resolve(ok(true)),
|
||||
list: payload => this.record('session.list', payload, this.onList(payload)),
|
||||
modelCatalog: () => Promise.resolve({
|
||||
ok: true,
|
||||
value: {
|
||||
@@ -222,20 +213,20 @@ export class FakeApiClient {
|
||||
}),
|
||||
search: (payload, signal) => {
|
||||
this.lastSearchSignal = signal
|
||||
return this.remoteResult('session.search', payload, this.onSearch(payload))
|
||||
return this.record('session.search', payload, this.onSearch(payload))
|
||||
},
|
||||
create: payload => this.remoteResult('session.create', payload, this.onCreate(payload)),
|
||||
selectModel: payload => this.remoteResult(
|
||||
create: payload => this.record('session.create', payload, this.onCreate(payload)),
|
||||
selectModel: payload => this.record(
|
||||
'session.selectModel',
|
||||
payload,
|
||||
this.onSelectModel(payload),
|
||||
),
|
||||
rename: payload => this.remoteResult('session.rename', payload, this.onRename(payload)),
|
||||
fork: payload => this.remoteResult('session.fork', payload, this.onFork(payload)),
|
||||
prompt: payload => this.remoteResult('session.prompt', payload, this.onPrompt(payload)),
|
||||
attachment: payload => this.remoteResult('session.attachment', payload, this.onAttachment(payload)),
|
||||
updateQueue: payload => this.remoteResult('session.updateQueue', payload, this.onUpdateQueue(payload)),
|
||||
cancel: payload => this.remoteResult('session.cancel', payload, this.onCancel(payload)),
|
||||
rename: payload => this.record('session.rename', payload, this.onRename(payload)),
|
||||
fork: payload => this.record('session.fork', payload, this.onFork(payload)),
|
||||
prompt: payload => this.record('session.prompt', payload, this.onPrompt(payload)),
|
||||
attachment: payload => this.record('session.attachment', payload, this.onAttachment(payload)),
|
||||
updateQueue: payload => this.record('session.updateQueue', payload, this.onUpdateQueue(payload)),
|
||||
cancel: payload => this.record('session.cancel', payload, this.onCancel(payload)),
|
||||
openWorkspacePath: payload => this.record(
|
||||
'session.openWorkspacePath',
|
||||
payload,
|
||||
@@ -333,21 +324,13 @@ export class FakeApiClient {
|
||||
return response
|
||||
}
|
||||
|
||||
private async remoteResult<T>(
|
||||
method: string,
|
||||
payload: unknown,
|
||||
response: Promise<RpcResponse<T>>,
|
||||
): Promise<RemoteResult<T>> {
|
||||
return (await this.record(method, payload, response)).result
|
||||
}
|
||||
|
||||
private page(request: SessionPageRequest): Promise<RemoteResult<SessionPage>> {
|
||||
return this.fetchPage(request)
|
||||
}
|
||||
|
||||
private async fetchPage(
|
||||
request: SessionPageRequest,
|
||||
response?: Promise<RpcResponse<SessionPage>>,
|
||||
response?: Promise<RemoteResult<SessionPage>>,
|
||||
): Promise<RemoteResult<SessionPage>> {
|
||||
const sessionId = addressSessionId(request.address)
|
||||
const payload = request.address.kind === 'session'
|
||||
@@ -366,7 +349,7 @@ export class FakeApiClient {
|
||||
...request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages },
|
||||
}
|
||||
const method = request.address.kind === 'session' ? 'session.history' : 'subagent.history'
|
||||
const result = await this.remoteResult(method, payload, response ?? this.onHistory({
|
||||
const result = await this.record(method, payload, response ?? this.onHistory({
|
||||
sessionId,
|
||||
throughSeq: request.throughSeq,
|
||||
...request.beforeSeq === undefined ? {} : { beforeSeq: request.beforeSeq },
|
||||
@@ -398,14 +381,8 @@ export class FakeApiClient {
|
||||
sessionId,
|
||||
maxMessages: request.maxMessages ?? 50,
|
||||
})
|
||||
if (!response.result.ok) {
|
||||
throw new RemoteStreamError(
|
||||
response.result.error.code,
|
||||
response.result.error.message,
|
||||
response.result.error.details,
|
||||
)
|
||||
}
|
||||
const page = response.result.value
|
||||
if (!response.ok) throw response.error
|
||||
const page = response.value
|
||||
const tail = page.records.at(-1)
|
||||
const cursor = this.followCursor ?? (tail === undefined ? -1 : historyRecordLastSeq(tail))
|
||||
yield {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SessionControlFrame } from '@deepseek-ai/dsh-api-session-controller/types'
|
||||
import type {} from '@deepseek-ai/dsh-session-title/client'
|
||||
import { SessionManager } from '../src/client/sessions/manager.ts'
|
||||
import { FakeApiClient, deferred, err, fakeRemote, ok, remoteErr, remoteOk } from './fake-api.client.ts'
|
||||
import { FakeApiClient, deferred, err, fakeRemote, ok } from './fake-api.client.ts'
|
||||
import { entries, plainTurn } from './event-script.client.ts'
|
||||
|
||||
const S1 = 'fk-m1' as SessionId
|
||||
@@ -92,10 +93,10 @@ describe('list lifecycle', () => {
|
||||
|
||||
it('keeps the error in the list snapshot on failure', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.resolve(err({ code: 'internal', message: 'boom', details: {} }))
|
||||
api.onList = () => Promise.resolve(err(new RemoteError('gateway/internal', 'boom', {})))
|
||||
const manager = new SessionManager(fakeRemote(api))
|
||||
await manager.refreshList()
|
||||
expect(manager.getListSnapshot()).toMatchObject({ state: 'error', error: { code: 'internal' } })
|
||||
expect(manager.getListSnapshot()).toMatchObject({ state: 'error', error: { code: 'gateway/internal' } })
|
||||
// A failed pull does not step the arrival phase: still pending.
|
||||
expect(manager.getListSnapshot().phase).toBe('pending')
|
||||
})
|
||||
@@ -108,7 +109,7 @@ describe('list lifecycle', () => {
|
||||
expect(manager.getListSnapshot().phase).toBe('ready')
|
||||
// Sticky across later failures: the pull-activity axis reports the error,
|
||||
// the arrival phase holds.
|
||||
api.onList = () => Promise.resolve(err({ code: 'internal', message: 'down', details: {} }))
|
||||
api.onList = () => Promise.resolve(err(new RemoteError('gateway/internal', 'down', {})))
|
||||
await manager.refreshList()
|
||||
expect(manager.getListSnapshot()).toMatchObject({ state: 'error', phase: 'ready' })
|
||||
// And across an empty re-pull (empty-with-ready = truly no sessions).
|
||||
@@ -227,25 +228,18 @@ describe('search', () => {
|
||||
expect(api.lastSearchSignal).toBe(signal)
|
||||
})
|
||||
|
||||
it('preserves business errors and folds transport failures', async () => {
|
||||
it('preserves business errors and propagates a non-Remote throw', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(fakeRemote(api))
|
||||
api.onSearch = () => Promise.resolve(err({
|
||||
code: 'internal',
|
||||
message: 'index unavailable',
|
||||
details: {},
|
||||
}))
|
||||
api.onSearch = () => Promise.resolve(err(new RemoteError('gateway/internal', 'index unavailable', {})))
|
||||
const signal = new AbortController().signal
|
||||
await expect(manager.search('first', signal)).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: 'index unavailable' },
|
||||
error: { code: 'gateway/internal', message: 'index unavailable' },
|
||||
})
|
||||
|
||||
api.onSearch = () => Promise.reject(new Error('wire down'))
|
||||
await expect(manager.search('second', signal)).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: 'wire down' },
|
||||
})
|
||||
await expect(manager.search('second', signal)).rejects.toThrow('wire down')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -279,7 +273,7 @@ describe('subagent catalogs', () => {
|
||||
summary(S1),
|
||||
summary(S2, { parentSessionId: S1, origin: 'subagent' }),
|
||||
] as never[] }))
|
||||
api.onSubagentList = () => Promise.resolve(remoteOk({
|
||||
api.onSubagentList = () => Promise.resolve(ok({
|
||||
entries: [{
|
||||
kind: 'child', id: S2, mode: 'continuable', label: 'worker',
|
||||
activity: 'running', hasChildren: false,
|
||||
@@ -375,7 +369,7 @@ describe('subagent catalogs', () => {
|
||||
it('marks a loaded parent row expandable only for a direct subagent publication', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const root = 'fk-root' as SessionId
|
||||
api.onSubagentList = () => Promise.resolve(remoteOk({
|
||||
api.onSubagentList = () => Promise.resolve(ok({
|
||||
entries: [
|
||||
{
|
||||
kind: 'child', id: S1, mode: 'continuable', label: 'parent',
|
||||
@@ -413,7 +407,7 @@ describe('subagent catalogs', () => {
|
||||
manager.handleSessionAdded(summary('fk-grandchild' as SessionId, {
|
||||
parentSessionId: S1, origin: 'subagent',
|
||||
}))
|
||||
response.resolve(remoteOk({
|
||||
response.resolve(ok({
|
||||
entries: [{
|
||||
kind: 'child', id: S1, mode: 'continuable', label: 'parent',
|
||||
activity: 'inactive', hasChildren: false,
|
||||
@@ -426,7 +420,7 @@ describe('subagent catalogs', () => {
|
||||
{ kind: 'child', id: S1, hasChildren: true },
|
||||
])
|
||||
|
||||
api.onSubagentList = () => Promise.resolve(remoteOk({
|
||||
api.onSubagentList = () => Promise.resolve(ok({
|
||||
entries: [{
|
||||
kind: 'child', id: S1, mode: 'continuable', label: 'parent',
|
||||
activity: 'inactive', hasChildren: false,
|
||||
@@ -449,7 +443,7 @@ describe('subagent catalogs', () => {
|
||||
|
||||
manager.handleSessionStatus(S1, false)
|
||||
manager.handleSessionStatus(S2, true)
|
||||
response.resolve(remoteOk({
|
||||
response.resolve(ok({
|
||||
entries: [
|
||||
{
|
||||
kind: 'child', id: S1, mode: 'continuable', label: 'stopped',
|
||||
@@ -472,7 +466,7 @@ describe('subagent catalogs', () => {
|
||||
|
||||
it('marks a detached catalog child inactive without requiring a selected address', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onSubagentList = () => Promise.resolve(remoteOk({
|
||||
api.onSubagentList = () => Promise.resolve(ok({
|
||||
entries: [{
|
||||
kind: 'child', id: S2, mode: 'continuable', label: 'worker',
|
||||
activity: 'running', hasChildren: false,
|
||||
@@ -498,8 +492,8 @@ describe('subagent catalogs', () => {
|
||||
|
||||
const refresh = manager.refreshSubagents(root)
|
||||
expect(manager.refreshSubagents(root)).toBe(refresh)
|
||||
api.onSubagentList = () => Promise.resolve(remoteOk({ entries: [], parentAvailable: true }))
|
||||
first.resolve(remoteOk({ entries: [], parentAvailable: true }))
|
||||
api.onSubagentList = () => Promise.resolve(ok({ entries: [], parentAvailable: true }))
|
||||
first.resolve(ok({ entries: [], parentAvailable: true }))
|
||||
await refresh
|
||||
|
||||
expect(api.callsOf('subagents.list')).toHaveLength(1)
|
||||
@@ -524,7 +518,7 @@ describe('subagent catalogs', () => {
|
||||
manager.handleSessionAdded(summary(S2, { parentSessionId: root }))
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
api.onSubagentList = () => second.promise
|
||||
first.resolve(remoteOk({
|
||||
first.resolve(ok({
|
||||
entries: [{
|
||||
kind: 'child', id: S1, mode: 'continuable', label: 'older',
|
||||
activity: 'inactive', hasChildren: false,
|
||||
@@ -533,7 +527,7 @@ describe('subagent catalogs', () => {
|
||||
}))
|
||||
await refresh
|
||||
// The trailing pull is already in flight (kicked synchronously in finally).
|
||||
second.resolve(remoteOk({
|
||||
second.resolve(ok({
|
||||
entries: [
|
||||
{
|
||||
kind: 'child', id: S1, mode: 'continuable', label: 'older',
|
||||
@@ -571,7 +565,7 @@ describe('subagent catalogs', () => {
|
||||
api.onSubagentList = () => first.promise
|
||||
const manager = new SessionManager(fakeRemote(api))
|
||||
const refresh = manager.refreshSubagents(root)
|
||||
first.resolve(remoteOk({ entries: [child()] as never[], parentAvailable: true }))
|
||||
first.resolve(ok({ entries: [child()] as never[], parentAvailable: true }))
|
||||
await refresh
|
||||
manager.selectSubagent({ parentSessionId: root, childSessionId: S2, mode: 'continuable' })
|
||||
|
||||
@@ -583,12 +577,12 @@ describe('subagent catalogs', () => {
|
||||
manager.handleSessionRemoved(root)
|
||||
const trailing = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => trailing.promise
|
||||
mid.resolve(remoteOk({ entries: [child()] as never[], parentAvailable: true }))
|
||||
mid.resolve(ok({ entries: [child()] as never[], parentAvailable: true }))
|
||||
await midRefresh
|
||||
expect(manager.getListSnapshot().subagentsByParent[root]?.parentAvailable).toBe(false)
|
||||
expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: false })
|
||||
|
||||
trailing.resolve(remoteErr({ code: 'internal', message: 'trailing pull failed', details: {} }))
|
||||
trailing.resolve(err(new RemoteError('gateway/internal', 'trailing pull failed', {})))
|
||||
await vi.waitFor(() => {
|
||||
expect(manager.getListSnapshot().subagentsByParent[root]).toMatchObject({
|
||||
state: 'error',
|
||||
@@ -605,7 +599,7 @@ describe('subagent catalogs', () => {
|
||||
it('invalidates catalog availability when the owning parent is removed', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const root = 'fk-root' as SessionId
|
||||
api.onSubagentList = () => Promise.resolve(remoteOk({
|
||||
api.onSubagentList = () => Promise.resolve(ok({
|
||||
entries: [{
|
||||
kind: 'child', id: S2, mode: 'continuable', label: 'worker',
|
||||
activity: 'inactive', hasChildren: false,
|
||||
@@ -625,12 +619,11 @@ describe('subagent catalogs', () => {
|
||||
})
|
||||
|
||||
describe('remaining branches', () => {
|
||||
it('refreshList folds a transport throw into the error state', async () => {
|
||||
it('refreshList propagates a non-Remote throw', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.reject(new Error('list wire down'))
|
||||
const manager = new SessionManager(fakeRemote(api))
|
||||
await manager.refreshList()
|
||||
expect(manager.getListSnapshot()).toMatchObject({ state: 'error', error: { code: 'internal', message: 'list wire down' } })
|
||||
await expect(manager.refreshList()).rejects.toThrow('list wire down')
|
||||
})
|
||||
|
||||
it('refreshList pushes running bits down to already-instantiated sessions', async () => {
|
||||
@@ -652,36 +645,32 @@ describe('remaining branches', () => {
|
||||
await manager.create({ cwd: '/tmp/w' }) // same id returned: no duplicate row
|
||||
expect(manager.getListSnapshot().items).toHaveLength(1)
|
||||
api.onCreate = () => Promise.reject(new Error('create wire down'))
|
||||
expect(await manager.create()).toMatchObject({ ok: false, error: { code: 'internal' } })
|
||||
await expect(manager.create()).rejects.toThrow('create wire down')
|
||||
// Business error passes through untouched.
|
||||
api.onCreate = () => Promise.resolve(err({ code: 'internal', message: 'no', details: {} }))
|
||||
api.onCreate = () => Promise.resolve(err(new RemoteError('gateway/internal', 'no', {})))
|
||||
expect(await manager.create()).toMatchObject({ ok: false })
|
||||
})
|
||||
|
||||
it('publishes a real Ungrouped summary from workspace-attach-failed', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onCreate = () => Promise.resolve(err({
|
||||
code: 'workspace-attach-failed',
|
||||
message: 'published but unattached',
|
||||
details: { sessionId: S1, workspaceId: 'w1' },
|
||||
} as never))
|
||||
api.onCreate = () => Promise.resolve(err(new RemoteError('session/workspace-attach-failed', 'published but unattached', {
|
||||
sessionId: S1, workspaceId: 'w1',
|
||||
})))
|
||||
const manager = new SessionManager(fakeRemote(api))
|
||||
const result = await manager.create({ workspaceId: 'w1' as never, sessionId: S1 })
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'workspace-attach-failed' } })
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'session/workspace-attach-failed' } })
|
||||
expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({ sessionId: S1 })])
|
||||
expect(manager.getListSnapshot().items[0]).not.toHaveProperty('cwd')
|
||||
})
|
||||
|
||||
it('reconciles a fork child published before workspace attachment fails', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onFork = () => Promise.resolve(err({
|
||||
code: 'workspace-attach-failed',
|
||||
message: 'forked but unattached',
|
||||
details: { sessionId: S2, workspaceId: 'w1' },
|
||||
} as never))
|
||||
api.onFork = () => Promise.resolve(err(new RemoteError('session/workspace-attach-failed', 'forked but unattached', {
|
||||
sessionId: S2, workspaceId: 'w1',
|
||||
})))
|
||||
const manager = new SessionManager(fakeRemote(api))
|
||||
const result = await manager.fork({ sessionId: S1 })
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'workspace-attach-failed' } })
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'session/workspace-attach-failed' } })
|
||||
expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({
|
||||
sessionId: S2,
|
||||
parentSessionId: S1,
|
||||
@@ -693,8 +682,8 @@ describe('remaining branches', () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onCreate = () => Promise.reject(new Error('response lost'))
|
||||
const manager = new SessionManager(fakeRemote(api))
|
||||
const failed = await manager.create({ workspaceId: 'w1' as never, sessionId: S1 })
|
||||
expect(failed).toMatchObject({ ok: false, error: { message: 'response lost' } })
|
||||
await expect(manager.create({ workspaceId: 'w1' as never, sessionId: S1 }))
|
||||
.rejects.toThrow('response lost')
|
||||
expect(manager.getListSnapshot().items).toEqual([])
|
||||
|
||||
manager.handleSessionAdded(summary(S1, { blank: true, cwd: '/w/one' }))
|
||||
@@ -790,8 +779,8 @@ describe('connected generation', () => {
|
||||
|
||||
manager.handleConnected()
|
||||
expect(manager.get(S2).getSnapshot().subagent).toEqual({ address })
|
||||
parent.resolve(remoteOk({ entries: [], parentAvailable: true }))
|
||||
child.resolve(remoteOk({ entries: [], parentAvailable: true }))
|
||||
parent.resolve(ok({ entries: [], parentAvailable: true }))
|
||||
child.resolve(ok({ entries: [], parentAvailable: true }))
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(api.callsOf('session.list')).toHaveLength(1)
|
||||
|
||||
@@ -13,7 +13,6 @@ import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import { SessionHistoryController } from '@deepseek-ai/dsh-api-session-controller/src/history.ts'
|
||||
import { subagentIdentityProjectionDefinition } from '@deepseek-ai/dsh-subagent/src/projection.ts'
|
||||
import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
||||
import { createUserMessage, MessageId } from '@deepseek-ai/dsh-llm'
|
||||
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
||||
@@ -493,17 +492,13 @@ describe('Remote Agent and Session lookup policy', () => {
|
||||
const sessionLookup = ctx.typert.lookups.get('session')
|
||||
if (agentLookup === undefined || sessionLookup === undefined) throw new Error('core lookup providers were not mounted')
|
||||
const ownershipFailure = {
|
||||
failure: {
|
||||
code: 'agent-busy',
|
||||
details: { reason: 'use subagent delivery for this child session' },
|
||||
},
|
||||
code: 'session/agent-busy',
|
||||
details: { reason: 'use subagent delivery for this child session' },
|
||||
}
|
||||
|
||||
const coldFailure = Promise.resolve(agentLookup.resolve(coldId))
|
||||
const liveFailure = Promise.resolve(sessionLookup.resolve(liveSession.id))
|
||||
await expect(coldFailure).rejects.toBeInstanceOf(TypertLookupFailure)
|
||||
await expect(coldFailure).rejects.toMatchObject(ownershipFailure)
|
||||
await expect(liveFailure).rejects.toBeInstanceOf(TypertLookupFailure)
|
||||
await expect(liveFailure).rejects.toMatchObject(ownershipFailure)
|
||||
expect(resume).not.toHaveBeenCalled()
|
||||
expect(inspect).toHaveBeenCalledOnce()
|
||||
@@ -576,14 +571,14 @@ describe('subagent ownership fence', () => {
|
||||
expect(prompt.ok).toBe(false)
|
||||
if (!prompt.ok) {
|
||||
expect(prompt.error).toMatchObject({
|
||||
code: 'agent-busy',
|
||||
code: 'session/agent-busy',
|
||||
details: { reason: 'use subagent delivery for this child session' },
|
||||
})
|
||||
}
|
||||
|
||||
const create = await remote.create(request({ sessionId, cwd: '/proj' }))
|
||||
expect(create.ok).toBe(false)
|
||||
if (!create.ok) expect(create.error.code).toBe('agent-busy')
|
||||
if (!create.ok) expect(create.error.code).toBe('session/agent-busy')
|
||||
expect(resume).not.toHaveBeenCalled()
|
||||
expect(ctx.agents.get(sessionId)).toBeUndefined()
|
||||
expect(inspect).toHaveBeenCalledTimes(3)
|
||||
@@ -626,7 +621,7 @@ describe('subagent ownership fence', () => {
|
||||
}))
|
||||
expect(resume).toHaveBeenCalledTimes(1)
|
||||
expect(prompt.ok).toBe(false)
|
||||
if (!prompt.ok) expect(prompt.error.code).toBe('internal')
|
||||
if (!prompt.ok) expect(prompt.error.code).toBe('gateway/internal')
|
||||
})
|
||||
|
||||
it('rejects origin-marked and runtime-owned live children from generic controls', async () => {
|
||||
@@ -661,7 +656,7 @@ describe('subagent ownership fence', () => {
|
||||
|
||||
const stopped = await remote.cancel(request({ sessionId: originChild.id }))
|
||||
expect(stopped.ok).toBe(false)
|
||||
if (!stopped.ok) expect(stopped.error.code).toBe('agent-busy')
|
||||
if (!stopped.ok) expect(stopped.error.code).toBe('session/agent-busy')
|
||||
expect(cancel).not.toHaveBeenCalled()
|
||||
|
||||
const queued = await remote.updateQueue(request({
|
||||
@@ -670,7 +665,7 @@ describe('subagent ownership fence', () => {
|
||||
action: { kind: 'remove' },
|
||||
}))
|
||||
expect(queued.ok).toBe(false)
|
||||
if (!queued.ok) expect(queued.error.code).toBe('agent-busy')
|
||||
if (!queued.ok) expect(queued.error.code).toBe('session/agent-busy')
|
||||
expect(updateInbox).not.toHaveBeenCalled()
|
||||
|
||||
const selection = await remote.selectModel(request({
|
||||
@@ -679,11 +674,11 @@ describe('subagent ownership fence', () => {
|
||||
model: 'm',
|
||||
}))
|
||||
expect(selection.ok).toBe(false)
|
||||
if (!selection.ok) expect(selection.error.code).toBe('agent-busy')
|
||||
if (!selection.ok) expect(selection.error.code).toBe('session/agent-busy')
|
||||
|
||||
const create = await remote.create(request({ sessionId: originChild.id, cwd: '/proj' }))
|
||||
expect(create.ok).toBe(false)
|
||||
if (!create.ok) expect(create.error.code).toBe('agent-busy')
|
||||
if (!create.ok) expect(create.error.code).toBe('session/agent-busy')
|
||||
|
||||
expect(ctx.agents.get(originChild.id)).toBe(originChild)
|
||||
})
|
||||
@@ -770,10 +765,10 @@ describe('subagent ownership fence', () => {
|
||||
content: [{ type: 'text' as const, text: 'invalid zone' }],
|
||||
clientTimeZone,
|
||||
}))
|
||||
expect(invalid).toEqual({
|
||||
expect(invalid).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'invalid-time-zone',
|
||||
code: 'session/invalid-time-zone',
|
||||
message: 'clientTimeZone must be UTC or a valid IANA Area/Location name',
|
||||
details: { value: clientTimeZone },
|
||||
},
|
||||
@@ -801,7 +796,7 @@ describe('degenerate composition (no persistence, no factory)', () => {
|
||||
})
|
||||
expect(response.ok).toBe(false)
|
||||
if (!response.ok) {
|
||||
expect(response.error.code).toBe('session-not-found')
|
||||
expect(response.error.code).toBe('session/not-found')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -821,7 +816,7 @@ describe('degenerate composition (no persistence, no factory)', () => {
|
||||
throughSeq: -1,
|
||||
})
|
||||
expect(response.ok).toBe(false)
|
||||
if (!response.ok) expect(response.error.code).toBe('session-not-found')
|
||||
if (!response.ok) expect(response.error.code).toBe('session/not-found')
|
||||
expect(inspect).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -850,7 +845,7 @@ describe('sessions.prompt synchronous rejection', () => {
|
||||
}))
|
||||
expect(response.ok).toBe(false)
|
||||
if (!response.ok) {
|
||||
expect(response.error.code).toBe('agent-busy')
|
||||
expect(response.error.code).toBe('session/agent-busy')
|
||||
expect(response.error.message).toBe('prompt rejected')
|
||||
expect(response.error.details).toEqual({
|
||||
reason: 'Error: agent "session-throwing" lifecycle disposed',
|
||||
@@ -891,7 +886,7 @@ describe('sessions.prompt synchronous rejection', () => {
|
||||
expect(selection.ok).toBe(false)
|
||||
if (!selection.ok) {
|
||||
expect(selection.error).toMatchObject({
|
||||
code: 'agent-busy',
|
||||
code: 'session/agent-busy',
|
||||
details: { reason: 'use subagent delivery for this child session' },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ describe('sessions.fork', () => {
|
||||
|
||||
for (const atSeq of [-1, 0.5]) {
|
||||
await expect(proxy.fork(request({ sessionId: sid('missing'), atSeq })))
|
||||
.resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } })
|
||||
.resolves.toMatchObject({ ok: false, error: { code: 'gateway/bad-request' } })
|
||||
}
|
||||
expect(ctx.sessions.list()).toEqual([])
|
||||
await ctx.fiber.dispose()
|
||||
@@ -246,7 +246,7 @@ describe('sessions.fork', () => {
|
||||
const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: anchor }))
|
||||
expect(response).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'fork-unavailable', details: { sessionId: source.id } },
|
||||
error: { code: 'session/fork-unavailable', details: { sessionId: source.id } },
|
||||
})
|
||||
if (!response.ok) expect(response.error.message).toMatch(/has not completed/)
|
||||
await ctx.fiber.dispose()
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts'
|
||||
import { ApiSessionAgentController } from '../src/agent.ts'
|
||||
import { buildModelCatalog } from '../src/catalog.ts'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { createSessionTestRemote } from './test-remote.ts'
|
||||
|
||||
function request<P>(payload: P): P {
|
||||
@@ -110,11 +110,7 @@ async function harness(logged?: {
|
||||
'Remote Rejected',
|
||||
[],
|
||||
undefined,
|
||||
new TypertRemoteFailure({
|
||||
code: 'fixture-rejected',
|
||||
message: 'fixture rejected the selection',
|
||||
details: { provider: 'remote-rejected' },
|
||||
}),
|
||||
new RemoteError('gateway/internal', 'fixture rejected the selection', {}),
|
||||
))
|
||||
ctx.llm.registerAdapter(['empty'], new CatalogAdapter('Empty Provider', []))
|
||||
ctx.llm.registerAdapter(['duplicate'], new CatalogAdapter('Duplicate Provider', [
|
||||
@@ -225,7 +221,7 @@ describe('Web session model selection', () => {
|
||||
}))
|
||||
expect(denied).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'attachment-error', details: { reason: 'TOO_MANY_IMAGES' } },
|
||||
error: { code: 'session/attachment-invalid', details: { reason: 'TOO_MANY_IMAGES' } },
|
||||
})
|
||||
expect(saveImage).toHaveBeenCalledTimes(2)
|
||||
await ctx.fiber.dispose()
|
||||
@@ -294,7 +290,7 @@ describe('Web session model selection', () => {
|
||||
}))
|
||||
expect(denied).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'attachment-error', details: { reason: 'ATTACHMENT_NOT_REFERENCED' } },
|
||||
error: { code: 'session/attachment-invalid', details: { reason: 'ATTACHMENT_NOT_REFERENCED' } },
|
||||
})
|
||||
expect(readImage).toHaveBeenCalledOnce()
|
||||
await ctx.fiber.dispose()
|
||||
@@ -423,7 +419,7 @@ describe('Web session model selection', () => {
|
||||
expect(unsupported).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'model-unavailable',
|
||||
code: 'session/model-unavailable',
|
||||
message: 'provider "deepseek-official" model "private-preview" does not support reasoning effort "medium"',
|
||||
},
|
||||
})
|
||||
@@ -433,10 +429,10 @@ describe('Web session model selection', () => {
|
||||
provider: 'missing',
|
||||
model: 'model',
|
||||
}))
|
||||
expect(rejected).toEqual({
|
||||
expect(rejected).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'model-unavailable',
|
||||
code: 'session/model-unavailable',
|
||||
message: 'no adapter registered for provider "missing"',
|
||||
details: { provider: 'missing', model: 'model' },
|
||||
},
|
||||
@@ -445,12 +441,12 @@ describe('Web session model selection', () => {
|
||||
sessionId,
|
||||
provider: 'remote-rejected',
|
||||
model: 'model',
|
||||
}))).toEqual({
|
||||
}))).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'fixture-rejected',
|
||||
code: 'gateway/internal',
|
||||
message: 'fixture rejected the selection',
|
||||
details: { provider: 'remote-rejected' },
|
||||
details: {},
|
||||
},
|
||||
})
|
||||
expect(currentSelection(ctx, sessionId))
|
||||
@@ -561,7 +557,7 @@ describe('Web session model selection', () => {
|
||||
}))
|
||||
expect(refused).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'model-unavailable', details: { provider: 'deleted-gateway', model: 'deleted-model' } },
|
||||
error: { code: 'session/model-unavailable', details: { provider: 'deleted-gateway', model: 'deleted-model' } },
|
||||
})
|
||||
const unavailableCatalog = await buildModelCatalog(ctx)
|
||||
expect(unavailableCatalog.routableProviders.includes(currentSelection(ctx, sessionId).provider)).toBe(false)
|
||||
@@ -621,9 +617,7 @@ describe('Web session model selection', () => {
|
||||
saveImages: () => {
|
||||
if (saveMode === 'error') return Promise.reject(new Error('image store offline'))
|
||||
if (saveMode === 'remote') {
|
||||
return Promise.reject(new TypertRemoteFailure({
|
||||
code: 'fixture-rejected', message: 'fixture rejected', details: {},
|
||||
}))
|
||||
return Promise.reject(new RemoteError('gateway/internal', 'fixture rejected', {}))
|
||||
}
|
||||
return Promise.resolve([savedRef])
|
||||
},
|
||||
@@ -643,7 +637,7 @@ describe('Web session model selection', () => {
|
||||
sessionId, mode: 'queue', content: [image],
|
||||
}))).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'attachment-error', details: { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' } },
|
||||
error: { code: 'session/attachment-invalid', details: { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' } },
|
||||
})
|
||||
|
||||
expectValue(await remote.selectModel(request({
|
||||
@@ -653,17 +647,17 @@ describe('Web session model selection', () => {
|
||||
sessionId, mode: 'queue', content: [{ ...image, data: '' }],
|
||||
}))).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'attachment-error', details: { reason: 'INVALID_IMAGE_BASE64' } },
|
||||
error: { code: 'session/attachment-invalid', details: { reason: 'INVALID_IMAGE_BASE64' } },
|
||||
})
|
||||
|
||||
saveMode = 'error'
|
||||
expect(await remote.prompt(promptRequest({
|
||||
sessionId, mode: 'queue', content: [image],
|
||||
}))).toMatchObject({ ok: false, error: { code: 'agent-busy' } })
|
||||
}))).toMatchObject({ ok: false, error: { code: 'session/agent-busy' } })
|
||||
saveMode = 'remote'
|
||||
expect(await remote.prompt(promptRequest({
|
||||
sessionId, mode: 'queue', content: [image],
|
||||
}))).toMatchObject({ ok: false, error: { code: 'fixture-rejected' } })
|
||||
}))).toMatchObject({ ok: false, error: { code: 'gateway/internal', message: 'fixture rejected' } })
|
||||
saveMode = 'success'
|
||||
expectValue(await remote.prompt(promptRequest({ sessionId, mode: 'queue', content: [image] })))
|
||||
expect(followup).toHaveBeenCalledOnce()
|
||||
@@ -681,13 +675,13 @@ describe('Web session model selection', () => {
|
||||
expect(await remote.selectModel(request({
|
||||
sessionId, provider: 'metadata-broken', model: 'broken',
|
||||
}))).toMatchObject({
|
||||
ok: false, error: { code: 'model-unavailable', message: 'reasoning metadata offline' },
|
||||
ok: false, error: { code: 'session/model-unavailable', message: 'reasoning metadata offline' },
|
||||
})
|
||||
expect(await remote.selectModel(request({
|
||||
sessionId, provider: 'string-error', model: 'broken',
|
||||
}))).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'model-unavailable', message: 'string selection failure' },
|
||||
error: { code: 'session/model-unavailable', message: 'string selection failure' },
|
||||
})
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('session/openWorkspacePath', () => {
|
||||
})
|
||||
|
||||
await expect(remote.openWorkspacePath({ path: '' }))
|
||||
.resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } })
|
||||
.resolves.toMatchObject({ ok: false, error: { code: 'gateway/bad-request' } })
|
||||
expect(openPath).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -105,13 +105,13 @@ describe('session/openWorkspacePath', () => {
|
||||
await expect(remote.openWorkspacePath({ path: 'result.html' }))
|
||||
.resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: 'path open failed: desktop unavailable' },
|
||||
error: { code: 'gateway/internal', message: 'path open failed: desktop unavailable' },
|
||||
})
|
||||
|
||||
const aborted = new AbortController()
|
||||
aborted.abort(new Error('cancelled'))
|
||||
aborted.abort(new Error('gateway/cancelled'))
|
||||
await expect(remote.openWorkspacePath({ path: 'result.html' }, aborted.signal))
|
||||
.resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } })
|
||||
.resolves.toMatchObject({ ok: false, error: { code: 'gateway/cancelled' } })
|
||||
})
|
||||
|
||||
it('classifies opener cancellation and non-Error failures', async () => {
|
||||
@@ -119,7 +119,7 @@ describe('session/openWorkspacePath', () => {
|
||||
const aborted = new AbortController()
|
||||
const openPath = vi.fn()
|
||||
.mockImplementationOnce(async () => {
|
||||
aborted.abort(new Error('cancelled'))
|
||||
aborted.abort(new Error('gateway/cancelled'))
|
||||
throw new Error('opening stopped')
|
||||
})
|
||||
.mockRejectedValueOnce('desktop unavailable')
|
||||
@@ -130,11 +130,11 @@ describe('session/openWorkspacePath', () => {
|
||||
})
|
||||
|
||||
await expect(controller.openWorkspacePath({ path: 'first.html' }, aborted.signal))
|
||||
.rejects.toMatchObject({ failure: { code: 'cancelled' } })
|
||||
.rejects.toMatchObject({ code: 'gateway/cancelled' })
|
||||
await expect(controller.openWorkspacePath({
|
||||
path: 'second.html',
|
||||
}, new AbortController().signal)).rejects.toMatchObject({
|
||||
failure: { code: 'internal', message: 'path open failed: desktop unavailable' },
|
||||
code: 'gateway/internal', message: 'path open failed: desktop unavailable',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { Session } from '../src/client/sessions/session.ts'
|
||||
import type { PendingSubmissionRetirement } from '../src/client/contract/session.ts'
|
||||
import type { SessionQueuedItem, SessionRequestId } from '../src/types.ts'
|
||||
@@ -98,7 +99,7 @@ describe('beginSubmission', () => {
|
||||
describe('prompt-coupled retirement', () => {
|
||||
it('a rejected identified prompt retires its echo immediately alongside promptError', async () => {
|
||||
const { api, session } = makeSession()
|
||||
api.onPrompt = () => Promise.resolve(err({ code: 'agent-busy', message: '忙', details: { reason: 'busy' } }))
|
||||
api.onPrompt = () => Promise.resolve(err(new RemoteError('session/agent-busy', '忙', { reason: 'busy' })))
|
||||
const retirements: PendingSubmissionRetirement[] = []
|
||||
const handle = session.beginSubmission({
|
||||
text: '失败的',
|
||||
@@ -121,7 +122,7 @@ describe('prompt-coupled retirement', () => {
|
||||
|
||||
it('an unidentified prompt failure leaves registered echoes alone', async () => {
|
||||
const { api, session } = makeSession()
|
||||
api.onPrompt = () => Promise.resolve(err({ code: 'agent-busy', message: '忙', details: { reason: 'busy' } }))
|
||||
api.onPrompt = () => Promise.resolve(err(new RemoteError('session/agent-busy', '忙', { reason: 'busy' })))
|
||||
session.beginSubmission({ text: '还在', images: [] })
|
||||
await session.prompt([{ type: 'text', text: '另一个' }], 'queue')
|
||||
expect(session.getSnapshot().pendingSubmissions).toHaveLength(1)
|
||||
|
||||
Loaded 100 of 338 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user