Files
deepseek-harness/packages/host/apiproxy/README.zh.md
imccyu 01ecb43ebc docs: state the Host-face rule for the browser e2e and settle the follow-ups
apps/web/tests/README.md records why these e2e type-check in the Host aggregate
and why importing a Client package there pulls its project tree into the Host
build graph, with mirroring as the standing answer. The Agent Note drops the
directory-picker face split (assessed and declined) and the grep-level gate in
favour of that README.

docs: regenerate the catalogs and retarget the moved declarations

The forwarded-event change moved three owner packages' cordis `Events`
declarations and their branded types into client-safe `./types` modules, and
the settings-scope split moves the shell spec into ui-settings-general. Point
the type-equivalence manifest and the affected Agent Note at those homes,
register the new `remote/*` event scope and the `ctx.settingsScope` service in
the catalog partition, and re-run the generators.

`$on` joins the documented `TypeRTClientRemote` surface, and the two Agent Note
fences that quote a bare member signature are marked `ignore-check`: they are
declaration fragments, not compilable units.

refactor(client): make ui-settings the settings domain's base layer

The settings-namespace transport lived in client/runtime, where every feature
could value-import it because runtime is a platform module. It belongs to the
settings domain, but moving it into ui-settings as a shared function fails
twice: the client bundle purity gate forbids cross-plugin value imports, and
ui-settings reached ui-sidebar for its shell, so any feature depending on it
closed a cycle through ui-layout and ui-theme.

Both halves move. `ctx.settingsScope` is now a cordis service — the
collaboration shape the purity gate prescribes, and the service proxy binds
`this.ctx` to the caller, so a bound scope's disposer belongs to the calling
fiber. The shell ui-settings used to own (the `sidebar.settings` occupant, its
navigation, and the nav-row projection) moves to ui-settings-general, which
already owns the chrome and the General section. What stays in ui-settings is
what carries no `ui-*` dependency: the scope service and the canonical settings
slot types, `settings.general.item` included. That type was parked in the locale
package precisely because the declarer was unreachable without a cycle; every
registrant now depends on this base layer, so it comes home.

The scope CONTRACT stays in client/runtime: a feature service accepts a scope
through its own signature without depending on the surface that binds it.

The forwarded settings invalidation replaces the deleted client-side
`settings/changed` event, so the transport reads `ctx.remote.$on`. It reaches
`$on` through the gateway's Client half plus the allowlist's type-only subpath
rather than api-remotes' Client face: that face imports a Host-tsdown-generated
artifact, and this package is reachable from the Host build graph through its
callers.

refactor(client): reach the settings transport through ctx.settingsScope

Every feature that owns a preference row switches from value-importing a shared
binder to the settings domain's service, and declares the two injections that
binding needs: `settingsScope` for the transport and `remote` for the forwarded
invalidation it subscribes to on the caller's own context.

The rows stay with the features that own the preferences — Language with locale,
Appearance with ui-theme, Composer Enter with ui-conversation. Only their route
to the transport changes, so no settings surface moves and no feature gains a
dependency on the shell.

The `settings.general.item` slot type now arrives from ui-settings, the base
layer every registrant already depends on, which retires the re-export outlet
ui-theme kept and the parked declaration in the locale package.

client/runtime drops its settings-form and schemastery dependencies with the
transport that used them.

test(client): bind the settings transport in the specs that boot a preference row

Every bench that activates a plugin owning a preference row now supplies the two
services that plugin injects: the forwarded-event port and the scope service.
Specs that exercise no settings path get the minimal doubles; the ones that do
drive their refresh chains through `remote/host-event`, the same signal
client/runtime republishes from a forwarded frame, replacing the deleted
client-side `settings/changed` event.

Also fixes a publication defect the built-invariant gate catches once it runs:
api-remotes' invariant companion shared the allowlist module with the package
index, so rolldown hoisted it into a third chunk beside the two bundled entries
— a file the mechanically derived publication list does not carry, leaving an
installed companion unable to import it. The companion now reads the allowlist
through this package's own published `./types` subpath, which the bundle keeps
external, so each entry stays self-contained.

The dynamic-subscription cast in apiproxy is gone: after the vendored cordis
rescope, `on` accepts the rest-parameter handler directly, and the allowlist's
shape assertion still carries the safety argument.

fix(client): carry the settings-scope move across the release manifests

Rebasing onto the publishable release set replaced every manifest's dependency
block, so the packages this change touches restate their additions in the
workspace-protocol form: the base layer's own transport dependencies, and the
`ui-settings` plus `remote` edges each preference-row owner now needs.

ui-settings-general takes clsx with the shell it received, and client/runtime
drops the settings-form and schemastery dependencies that left with the
transport.

fix(api-gateway): give each $on subscription its own registration and containment

Two defects in the forwarded-event subscription table, both raised in review:

A set keyed on listener identity stored one entry when two callers subscribed the
same function object to the same event, so the first frame reached it once instead
of twice and either disposer silenced the surviving registration. Subscriptions are
now records addressed by registration, which is what "the disposer belongs to the
calling fiber" requires.

A listener declared void may still be `async`, and the synchronous `try/catch`
could not see its rejection: the promise was dropped and surfaced as an unhandled
rejection outside the documented containment. Delivery now attaches a rejection
handler when a listener returns a promise, so both failure modes are logged and
isolated alike.

Delivery also iterates a snapshot, so a listener that subscribes or disposes during
a frame no longer changes who receives that frame, and production matches the
TestRemote double instead of relying on live Set iteration order.

Both fixes are pinned by tests that fail against the previous implementation. The
double gains its own spec for the `$mount` refusal and the unsubscribed-name drop —
per-file coverage reaches it — plus a note that it propagates a throwing listener
where production contains one, so no spec mistakes it for the containment guarantee.

Three prose corrections: `assertJsonArgs` states where its throw actually surfaces
(the emitter's listener containment, not load or emit time), the browser e2e README
names every standing Client import rather than claiming one exception, and two
comments and a test title state the forwarded event instead of the deleted
client-side one.

refactor(remote): deliver forwarded frames through ctx.remote.$dispatch

The carrier used to relay each decoded frame over an internal
`remote/host-event` cordis event so the delivery port could stay off the Remote
contract. The relay was the wrong shape twice over: it put a client-face event
into a scan whose subject is the Host vocabulary, forcing a walk exemption for
something that is not a Host event at all, and it made a direct handoff between
two Client plugins look like a broadcast any plugin participates in.

`TypeRTClientRemote` now carries both roles of one surface — consumers subscribe
with `$on`, and whoever owns the Host frame sink hands frames over with
`$dispatch` — so client/runtime calls the Remote service directly and the event
declaration is gone. A cordis service method is the collaboration shape the
client bundle purity gate prescribes, and it needs no relay to satisfy it.

The trade is that the handoff is now developer-visible: any plugin holding
`ctx.remote` can synthesize a forwarded event. That is the exposure the relay
already had — `ctx.emit` was equally reachable — stated in the contract instead
of hidden behind a private subscriber.

runtime reaches `ctx.remote` through the gateway's Client face rather than
api-remotes': that face imports a Host-tsdown-generated artifact, and this
project sits in the Host build graph.

refactor(api-remotes): keep the allowlist value out of types.ts

`src/types.ts` carries only types by package convention, but it held the
forwarded-event array, so the type-only subpath published runtime code. The
array moves to `src/remote-events.ts` and `types.ts` derives its projection from
it; both compiler faces list both files, so the Host forwarding loop and the
consumer key face still read one declaration and the package's exports are
unchanged.

The invariant companion returns to an empty installer. Its dispatch-shape check
was the only reason the companion imported the allowlist, which made the two
bundled entries share a module: rolldown hoisted it into a third chunk that the
mechanically derived publication list does not carry, so an installed companion
could not import it. Dropping the check retires that coupling along with the
subpath-import and bundle-external workarounds it needed, and the shape the
check enforced at runtime is the part the Host face's `TypeRTForwardableEvent`
assertion already refuses at compile time.

test(ui-task): bind the locale plugin's new injections in its bench

The bench boots the real locale plugin, which now injects the settings-scope
service and the forwarded-event port, so it stayed pending and left `ctx.locale`
undefined. Supplies both doubles like the other benches that boot a plugin
owning a preference row.

docs: close the documentation gates for the forwarded-event surface

Regenerates the two graph catalogs and re-records every bilingual pair this
branch edited. Several pairs needed real work beyond the record:

- The generators write only the English side, so the Chinese sides of
  `event-producer-consumer` and `module-graph` had drifted: the former still
  listed the three deleted client-face events and pointed at declaration sites
  this branch moved into `types.ts` modules, and the latter carried a stale
  dependency graph.
- `TypeRTClientRemote`'s documented declaration gains `$dispatch` on both sides.
- The pairing contract requires both sides to link the same target, so the
  apiproxy README and the design note now link the English note from both
  languages, and the note's code blocks are byte-identical across the pair
  (a translated comment inside a fence counts as divergence).
- `apps/web/tests/README.md` gains its Chinese counterpart; the browser e2e lane
  documents a discipline reviewers apply, so it belongs in the bilingual corpus
  rather than in the pairing exemption list.
- Four fences in the design note are marked `ignore-check`: each quotes a member
  signature, a union arm, or a snippet that names symbols it does not import, so
  none is a compilable unit.

docs(agent-note): transition the forwarded-event note to implemented

The design shipped in this PR, so the pair moves into `implemented/` and takes
that folder's skeleton: `## Proposal` becomes a present-tense `## Decision`,
and `## Acceptance criteria` plus `## Risks` fold into `## Verification` (what
pins the behavior) and `## Consequences` (what the shipped shape costs).

Facts that moved after the proposal are corrected rather than preserved: the
allowlist value now lives in `remote-events.ts` beside a type-only `types.ts`,
the delivery port is `$dispatch` rather than an internal cordis event, and the
invariant companion is an explained empty installer. `Verification` states the
two `$on` defects the review found — independent registration identity and
async-rejection containment — since those are now the properties tests pin.

Supersession is partial, so five active notes stay active and gain a
cross-link each: `web-config-plane`, `web-client-session-scope`,
`config-plane-boundaries`, `versioned-gui-welcome-onboarding`, and
`permission-default-for-new-sessions` each described a frame this change
replaced. Only the mechanism sentence is annotated; every conclusion those
notes own is untouched, and `host/models-changed` remains apiproxy's own
derived frame in all of them.

Also pins the disposer's idempotence: calling one `$on` disposer twice must not
splice a surviving twin registration out from under its owner.

fix: docs

fix: test
2026-08-11 19:25:41 +08:00

28 KiB
Raw Blame History

@deepseek-ai/dsh-host-apiproxy

English | 中文

所有客户端共用的 API 网关由三部分组成TypeScript API 约定(src/api/,不依赖 Node可从浏览器导入、fetch 载体对(src/fetch/:宿主侧的 toFetchHandler,以及客户端侧的 AbstractApiClient 与平台子类)和宿主侧实现(src/api-proxy.tscreateApiProxy 加上默认导出的 ApiProxyService 网关插件,其配置为 {nativeOpen?, sessionExportCompressionLevel?},提供 ctx.apiProxy。该包不注册任何路由HTTP 等载体自行包装 ctx.apiProxy。随发行版交付的 Web 组合位于 packages/bundle/web-app/cordis.patch.yml,其默认 Agent智能体模型选择属于 base 组合包中的 @deepseek-ai/dsh-agent-default-model

共享 Agent 默认值(agent-default-model Settings 分节)

ApiProxyService 消费 ctx.agentDefaultModel;它不持有提供方/模型配置或 Settings 分节。共享服务在 agent-default-model 下注册 {provider, model, reasoningEffort?}base 组合包的组合条目是底层,settings.yaml 把用户选择叠加其上。

会话每次访问时都按三级解析模型选择:本进程内作出的选择,其次是该会话日志中最新的 request/header,最后是这个默认值。已经跑过一轮的会话从自己的日志推导选择,空白会话则能观察到创建之后保存的默认值。

session.selectModel 会把接受的切换保存为部署默认值;没有单独的选择动作。它存储已解析的 ModelSelection,包括适配器实体化的默认推理强度。完整分节写入会在所选模型没有推理强度时清除已存值。存储失败只记日志,不会撤销会话选择。没有设置提供方的部署保留组合条目,切换只对当前会话生效。

Settings 分节中的 reasoningEffort 在 agent-default-model 插件配置中刻意没有对应字段seam 按字段把用户层合并到组合条目之上,因此缺席的键无法覆盖已有键,组合层中的推理强度会在以后选择没有推理强度的模型时继续存在。推理强度的部署默认值属于按模型解析的适配器 profile。

存储的选择独立于目录成员关系。默认值指向不可用的提供方时,它仍会作为会话的 current 送到 session.models,让选择器请求用户重新选择,而不是静默选用其他模型。反过来,适配器也可以服务其目录中未公布的模型。

约定层(/api

协议消息组成一个四象限可辨识联合:发起方 × 请求/响应,与物理通道解耦。四种消息分别是 ClientRequestPOST /api/<method> 的请求体)、ServerResponse(该 POST 的响应体)、ServerRequestSSE 帧)和 ClientResponsePOST /api/respond 的请求体)。响应始终回显对应请求的 rpcId,绝不签发新值。方法的参数与返回值结构只存在于领域接口签名(SessionsApiHostApiEventsApi)中;RpcMethodMap 注册方法,其他所有位置均通过 RequestPayload<K>ResponseValue<K> 派生。Zod schema 以 satisfies z.ZodType<Wire<T>> 锚定类型,并分两层解析:先解析信封,再解析业务载荷,随后按方法分发。业务错误由 RpcResult 的错误分支承载(RpcErrorDetailsMap 封闭错误码集合HTTP 状态只表达载体层结果。每个 /api POST 都必须声明 application/json 媒体类型——否则在分发前即以 415 拒绝,因此跨站「简单请求」(浏览器不经 CORS 预检就会发出)永远无法盲目执行有副作用的方法。

分层与协议决策记录在 GUI 分层与 RPC 协议 RFC中;浏览器侧消费架构记录在 Web 客户端架构 RFC中。

首个回答认领待处理请求之前,系统会对照该请求校验问题响应。多选题的回答项可以同时携带 selected 中的请求选项标签与非空 custom 文本单选题的回答项必须二选一。标签重复、标签未知、id 不匹配、批次不完整以及自定义文本为空都会以 bad-response 拒绝。

session.history 会读取已附加 Session 的内存状态,或通过持久化检查冷日志,而不会恢复或发布 agent然后按追加来源的消息边界分页maxMessages 统计以追加方式进入 surface 的 user/messageassistant/message 事件因此仅供模型使用的替换副本不占用配额。每一页仍是一段连续的原始事件区间从而让压缩compaction的仅日志 compact/summary 记录与引用它的替换留在同一页。

session.history 的尾页(不带 beforeSeq)额外携带一个可选的 projections 块——ctx.sessionProjections@deepseek-ai/dsh-session-projection)上每个已注册单元的水位线快照,asOfSeq = 这些值共同反映到的最后一个事件 seq空日志为 -1)。网关还订阅注册表的变更流,为每个状态发生变化的单元生成一个 session/projection mux 帧({sessionId, key, value, seq}——实时推送状态,绝不入日志;客户端按 seq 高者胜维护一个按会话的通用值仓)。载体不持有任何领域知识(每个值在注册表内部已过其单元自己的 schema协议 schema 对 values/value 保持宽松loadOlder 页永不携带该块,未装注册表的组合则两个面都不提供。

会话日志导出是宿主侧的下载面,不是 RPCGET /api/session.export?sessionId=…&includeDescendants=true 流式返回一个 ZIP其中每个文件都是会话存储工件的逐字原文持久化后端的 readRaw——按物理编码解码的确切持久化字节,绝非从解析后事件重建),根会话放在其原始基础文件名下,每个子代理后代放在 subagents/<id>/ 下,每个被任何包含的日志引用的图片放在 media/<attachmentId>.<ext> 下(从附件存储读取并校验;共享图片只出现一次)。每个实时根会话或后代都会在读取原始工件前立即通过权威的 SessionStore.flush 持久性屏障;冷会话没有需要 flush 的内存工作。压缩在宿主侧使用 fflate 流式 Zip API 和已验证的 sessionExportCompressionLevel 09默认 6使部署可以在 CPU延迟与归档大小之间取舍响应边生成边分块写出宿主从不把整个归档放进单个缓冲区。响应队列达到 64 KiB 字节高水位后,生产会等待 Consumer pull 恢复正容量fflate 的同步回调最多只会让该界限多出一次有界输入 push 的输出。请求中止或响应 body 取消会停止血缘与工件工作、终止活跃压缩器,并继续按取消传播,而不会变成 HTTP 500。它要求同时挂载持久化、session-query 与附件服务:任一缺失应答 500持久化后端不提供每会话原始工件时应答 501根会话缺失时应答 404后代缺少存储工件或引用的图片无法读取则整个流失败fail-loud绝不静默少导出。端点由传输层挂载ApiProxy.downloads.sessionLog 实现它。

会话标题与其他所有领域一样搭乘这对通用投影机制——历史尾页的 projections 块外加 title 键下的 session/projection 帧。标题不会加入 session.list;冷会话在其中仍只有元数据,直到打开或恢复操作附加其日志。session.rename 接受用户显式标题(冷会话先恢复),委托给 ctx.sessionTitle.rename——被接受的 session/title 事件将标题钉住、不再被自动生成覆盖——并返回规范化后的标题及其事件 seq让 client 在推送帧到达前就结算自己的 title 投影格;规范化后为空的标题返回 title-invalid

session.fork 将可选事件锚点映射到该锚点处或其后的首个 turn/end,使消息操作可包含该消息所在的完整轮次。锚点省略或超过末尾时,选择最后一个已完成轮次;若锚点已在日志中,而其所在轮次仍开放,则返回 fork-unavailable不会向较早位置裁剪。发布后的子会话会先继承源会话的种子历史、cwd、日志中最新的 ModelSelection 及谱系,再加入源 Workspace。如果附加到 Workspace 失败,workspace-attach-failed 会携带已发布的子会话 id供客户端对账。SessionStore fork 决策记录了为何锚点要映射到该 turn/end

会话模型选择属于会话领域约定。session.models 将当前 ModelSelection 与按提供方分组的建议性模型、精确模型的推理reasoning元数据和逐提供方查询失败记录分开返回。该选择可能不在这些分组中也绝不会作为合成行注入客户端可以提示用户作出另一项选择而无需把目录变成路由白名单。session.selectModel 校验由适配器持有的可选推理强度,并指定下次组装提示词时使用的完整选择。目录成员关系不构成校验:适配器可以解析未列出的模型,而不可用的提供方或不受支持的推理强度会返回 model-unavailablesession.models 还会报告 routable,即当前是否有适配器为所选提供方提供服务。该值刻意不从分组推导,因为适配器可以服务未公布的模型。session.prompt 会依据同一事实,在开启轮次之前以 model-unavailable 拒绝;客户端禁用 composer 只是提示性设计,这个方法始终可被调用。

待处理的 queued 输入属于实时控制平面约定,而非对话历史。网关根据持久 agent/inbox/spliced 变更派生完整的 next-turn 队列,并在每次变更后及重连时广播权威 session/queue 快照;待处理的 next-step steering中途引导不进入此 Web 投影。在 next-step 内,用户来源的消息携带 steering placement而注入上下文审批通知、任务完成、附加快照携带 context,领取前不对外呈现。面向单条消息的 agent/inbox/insertedclaimeddiscarded 通知仍供生命周期观察方使用,但不用于构建队列视图。session.updateQueue 通过 MessageId 寻址单个项;编辑和移除经已挂载 Agent 的 Inbox.splice() 修改队列。claim 的纯删除 splice 会在 pre-step 准入前赢得竞态,因此之后的操作返回 queue-item-not-foundsession.cancel 仅中止活动轮次并保留待处理 inbox 工作;取消达到完全停稳且结束中的轮次完成 flush 后AgentLoop 按 FIFO 顺序认领下一条可唤醒消息,浏览器绝不重发或提升它。队列操作绝不恢复冷会话,客户端也绝不根据轮次或状态事件推断某项已退出队列。

后台任务沿用同一种实时推送姿态。当组合中有 ctx.tasks 时,网关订阅它的变更订阅,并在注册表每一次改变某个会话可见内容的提交后——注册、转入 stopping、结算以及 owner 销毁时的移除——广播一份完整的 session/tasks 快照,另外为每个已经有任务的会话发送订阅 baseline没有 baseline 即表示空集;把集合清空的那次变更仍然发送 [])。带 owner 的变更通过那个确切的 Agent 读取,因此推送在其 scope 拆除期间依然正确baseline 读 ctx.agents.get(sessionId),对没有活体 Agent 的会话只得到无主任务,且绝不恢复冷会话。无主变更向每一个已订阅会话扇出,因为无主任务对所有调用方可见。线路上的 TaskView 丢弃 ownerSessionreportedoutputLimitBytes:第一个由帧自身的 sessionId 携带,另外两个分别是内部通知位和模型呈现策略。没有该注册表的组合不发出这类帧。

Workspace 列表与 Session 列表是相互独立的重连基线。workspace.create({ path }) 会接纳已有的规范目录,并允许由 basename 派生的标题重复。workspace.delete 只移除 Workspace 注册记录,session.create 接受可选的预分配 Session idhost/workspace-changedhost/workspace-removedhost/session-added 则以任意到达顺序携带已提交的增量。workspace.archiveSession 向注册表级全局归档集合添加一个会话,并应答完整的更新后集合;workspace.list 携带该集合作为重连基线,host/archived-sessions-changed 在每次持久变更后推送完整快照。归档只把会话从各分组视图中隐藏,不触碰其日志和 workspace 记账;既非实时也未持久化的会话以 session-not-found 失败。删除注册记录会保留目录和会话日志;相关 Session 仍留在 session.list 中,并进入 Ungrouped。SessionSummary.blankhost/session-added 帧携带派生的零事件位:客户端隐藏空白会话并按 workspace 复用它们,在首个 host/session-status(running:true) 时翻转 blank并以 session.list 作为重连权威;冷会话摘要永远不是空白:惰性持久化让从未追加过事件的会话根本不出现在 list() 中。

session.search 是以 session.list 所列会话为范围的有界内容搜索投影。网关向可选的 ctx.sessionQuery 服务请求全局排序后的当前内容视图中的 user、assistant 和 steering 匹配项,并持续消费该结果流,直到获得至多 20 个可见会话snippet 对及一个前瞻项;返回前仍会依据从列表推导的授权集合重新校验每个命中。提供方分页初始请求 20 个命中;如果第一页请求因这一上限被拒绝,网关会依次探测 10、5、2、1并在续传和陈旧世代重启中沿用探测所得的页面大小。返回的 snippet 最多包含 240 个 Unicode 码点,响应 schema 则会在每个客户端边界独立强制执行该上限。将授权集合保留在宿主内存中,可在不削弱可见性或排序的前提下避开有效大型语料库的 SQLite 变量上限。

陈旧的续传会丢弃该提供方尝试中的所有部分结果、去重条目和游标,然后依据最初从列表推导的可见性快照从第一页重新开始,但不会丢弃探测所得的提供方页面大小。上限探测与陈旧重试共用最多 100 次提供方调用的限制(因此最多检查 2,000 个命中);如果某页命中数超过其请求的上限、续传游标重复,或用尽该调用预算后结果流仍未耗尽,都会直接返回 internal 业务错误,不返回部分结果。载体请求信号可取消持久化列表枚举、冷会话摘要收集和每一次搜索调用;即使同时收到上限拒绝或陈旧拒绝,也以取消为准。部署若未挂载该服务,或索引/查询故障无法恢复,也会返回 internal 业务错误,以便客户端保留仅基于元数据的匹配项。

目录选择委托给组合的 ctx.directoryPicker 后端(目录选择 seam);调用组合能力 kind 之外的方法会以 directory-picker-unavailable 失败(客户端不需要广播——组合的选择器包自己的 client half 渲染匹配的交互)。在 native 下,host.pickDirectory 打开一个原生选择器并返回选中路径(取消为 null);该方法需等待用户完成操作,不使用默认的 30 秒一元调用超时,而调用方与连接的中止仍会传播至原生进程。在 browse 下,host.listDirectory 返回一个按名称排序的目录层级,携带面包屑祖先链、home 锚点与宿主判定的 hidden 标志(不带路径即家目录),host.createDirectory 创建一个经校验的子段;后端的类型化失败 1:1 映射为 directory-unreadabledirectory-existsdirectory-create-failed 错误码。浏览器载体的前缀级信任栅栏dsh-client-connection像覆盖其他所有 /api 请求一样覆盖上述全部方法。

host.openPath 会用操作系统的默认应用打开一个文件系统路径macOS 为 openWindows 为 Invoke-Item,桌面 Linux 为 xdg-open)。对于 .html.htm.xhtml.svgmacOS 和桌面 Linux 会优先使用能够确定的默认浏览器无法确定时回退到上述应用交接。WSL 会通过 wslpath -w 转换每个 Linux 路径,并将所得 Windows/UNC 路径交给 Windows Invoke-Item,浏览器可渲染的文档也不例外,而非假定存在 Linux 桌面文件关联。浏览器载体对其施加与 host.pickDirectory 相同的回环、同源限制。

agentPreset.list 领域向浏览器暴露部署的 preset 名单,使其在开启会话时能够提供选择;每一行携带它的 trustuser preset 的权限恰好等于它所引用的插件)、它是否为当前默认值,以及——当该 preset 无法组装会话时——一条 broken 原因:损坏的目录仍占着它的 id界面必须能展示并删除它而不是把它端出来然后在会话启动时失败。未组装任何 preset 的部署返回空名单而非错误,因为共用宿主组装本身就是一种有效部署。agentPreset.select 用另一个 preset 重组某个会话的 agent且仅在会话空白时允许一旦跑过任何轮次那段历史就是在该 preset 的工具下产生的,替换会留下无法执行的已记录 tool call此时返回 agent-preset-locked。agent 与会话都不销毁——只替换组装,且替换失败会恢复原来的组装。

agentPreset.readcopyopenDocumentremove 负责管理组装本身。read 返回文本连同它的 trust,供只读查看器使用。创作只有复制一种写入:copy 接收 { from, agentPreset, name? }——两个由 Host 对照自身根目录解析的 id 加一个可选显示名——并整目录复制来源,因此组装文本不经过传输层,副本与其来源同等可加载;不可约束或已被占用的 id 回答 agent-preset-invalidremove 对随附 preset 回答 agent-preset-read-onlyopenDocument 把一个本地创作 preset 的目录交给平台打开器——请求只携带 id、绝不携带路径因此没有任何浏览器载荷能选中任意文件系统目标部署没有原生打开器时回答 { opened: false, path } 供界面以文本展示,随附 preset 与 remove 一样被拒绝,而网关的 nativeOpen 配置可在平台探测(canOpenNativePath)失真处钉死该能力。这四个方法在 dsh-client-connection 中被固定在环回地址:组装指明了一个会话所运行的插件,因此读取它是侦察,而 copy/remove/openDocument 管理名单并驱动宿主桌面。listselect 保持为普通方法——名单只携带 id 与信任级别,每个 preset 选择器都需要它;而选择一个 preset 并不比 session.create 自带的 agentPreset 多给任何能力,何况默认 preset 本就带着 bash。list 报告两个不含路径的能力标志:authorable,即部署是否配置了可供复制新 preset 的根目录;hasDocument,即 openDocument 会原生打开、还是回答一个路径。

command.*skill.* 领域向客户端暴露宿主命令注册表和 skill技能目录。每个方法都通过 sessionId 寻址一个会话的 Agent被服务的会话必有 Agentcommand.* 经由与 session.* 相同的路径恢复冷会话,而 skill.list 从会话头解析项目根目录,不触碰 Agent 注册表)。skill.list 服务于 composer 的菜单:它返回每一个用户可调用的 skill 及其 modelInvocable 标志,让菜单能够标出仅限用户(disable-model-invocation)的条目——斜杠手势是这类条目唯一的调用路径。列表是 skill 领域唯一的 RPC——调用本身就是一次普通的 session.promptdsh-tool-skill 会在 pre-step 边界识别其中以空白为界的 /name token并以注入的 <skill_content> 上下文作答因此所有入口Web、TUI 与 ACPAgent Client Protocol共享同一条确定性路径手动键入的文本也走该路径且没有专设的调用协议。command.execute 在宿主侧运行一条斜杠命令行,语义为纯准入:响应报告该行是否解析到处理器,并在解析到时回带铸造的生命周期 commandId(将本次确认与流节点关联);结局经由持久落账并在 mux 流广播的 command/run/command/done 生命周期事件对承载。命令处理器运行超过 30 秒的传输健康时限仍属正常,因此 command.execute 仅携带调用方/连接取消信号;该信号可取消正在运行的处理器。commands/change 搭乘转发事件帧作为注册表级目录失效信号:客户端重新拉取 command.list 而不是做差分。host/session-preset-changed 是它按会话粒度的对应物,由落账的 agent-preset/selected 提交点成帧:重组空会话的 agent 只是重新挂接其 scope不产生任何注册因此该会话组成所决定的两份目录command.listskill.list)都会失效,却没有任何注册表变化来宣告它。

settings.*credentials.*llm.* 领域是配置页协议。settings 领域服务于已注册可配置提供方所指向的 namespacectx.llm.listConfigurableProviders()),并额外服务于一份小型、显式的 allowlist——Web 偏好 localepermissionui-conversationui-theme,以及产品持有的 ui-onboarding;仅新增一项 Settings 注册,绝不会使其可被远程读取或写入。其他任何 namespace 都只会得到 settings-not-exposed——未注册的 namespace 得到的是同一个答复,因此没有调用方能靠逐个探测把注册表枚举出来。settings.describe 为每个已暴露 namespace 提供其序列化 schemastery schema、脱敏后的分层值resolved/base/user——字段出现在 user 中即标记其被用户覆盖)、secrets 槽位列表、该分节的 revision,以及布尔型 hasDocument 能力标志。浏览器不会收到 Host 路径:无路径参数的 settings.openDocument 会请求提供方准备文档,再把由 Host 解析出的结果交给原生打开器,因此任何浏览器载荷都无法选择任意文件系统目标。settings.update/settings.replace 写入用户层;settings.mutate 则在已存分节上施加路径 opset/unset),这是持有脱敏视图的客户端的删除路径——据此重建分节再整体替换,会删掉协议从未回传过的那些机密。任何写入都可携带 expectedRevision;陈旧的期望值会以 settings-conflict 连同两个 revision 作答,而不是覆盖先落地的那个写方,其余每种 seam 拒绝则折叠为 settings-rejected。secret 角色的值绝不在任何一层搭乘任何响应secret 只沿一个方向跨越协议——在 update/mutate 载荷或 credentials.set 之内。credentials.describe 返回不含值的视图(configured/source/writablecredentials.set/credentials.unset 则把被遮蔽引用的拒绝映射为 credential-rejectedllm.providers 把可配置提供方目录与存活路由合并(休眠条目携带 active: false;未声明的存活路由追加在后,不带 settings 地址),llm.models 则是与会话无关的目录。llm.discoverModels 询问页面尚在起草的提供方端点:settingsNs 选出懂得读取该列表的适配器家族,端点、协议与密钥则来自表单而非存储。它什么都不写——回复是候选,只有随后的 settings.mutate 才决定路由服务什么——因此其 apiKey 是 secret 可以搭乘的第三个载荷(另两个是 settings.update/mutatecredentials.set且绝不被存储或回显。host 从不存储或回传它;与另两者一样,它确实会搭乘客户端的出站信封,subscribeEnvelopes() 的观察者能看到——为该 tap 做脱敏是整个配置面的改动,而非本方法一家的事。每一种拒绝(无人服务的 namespace、没有可读列表的协议、不可达端点、被拒凭据都折叠为 model-discovery-failed其消息是适配器自己的文本details 点名被询问的端点,绝不点名所提供的凭据。失效通知让每个面无需轮询即保持收敛。settings/document-updatedcredentials/updated 搭乘原样转发事件帧(见下),因此解析值未变的原始设置变更同样能到达客户端,凭据失效通知也仍然只带引用名、绝不带值。host/models-changed 仍是本包自有的派生帧:它由 llm/adapters-updated 和可配置提供方 namespace 的变更触发,因为该提供方的设置正承载着它的目录与端点;localepermissionui-conversationui-themeui-onboarding 变更只会发出自身的转发 settings 失效通知。浏览器载体把整个配置面(含读取与原生操作:settings.describe/openDocument/update/replace/mutatecredentials.describe/set/unset)限制为仅接受来自回环地址的同源请求——即 host.pickDirectory 所在的特权集合。未装 settings 或凭据 provider 的组合会以指名缺失插件、包含解决建议的 internal 错误应答这些领域。

载体层(/client + 根路径)

AbstractApiClient 持有全部协议不变量:签发 rpcId、包装解包信封、Zod 解析、SSE 帧解码、一元请求超时,以及按微任务批处理的信封观测(subscribeEnvelopes);平台子类只提供 doFetch 传输环节。InProcessApiClienttoFetchHandler(api) 为基础,仍是同构接点:它运行完整的协议序列化与校验路径而不经过网络,供需要该路径的调用方和载体测试使用。产品的 dsh --profile headless 是直连 core 的入口,不挂载本包。

模型体验

无。该包定义客户端与宿主间的 wire 约定和载体,其中没有任何内容会进入模型请求。

KV Cache 影响

无;该包既不组装也不发送提供方请求。

已知限制与暂缓事项

  • 转发的 Remote 事件寄居在这套 legacy 帧联合里host/remote-event 住在 HostFrame 中,是为了让投递路径复用现有宿主流、不必新开第三条下行通道,因此读起来像是本包拥有 Remote 事件契约。并非如此:名单归 dsh-api-remotes,消费端动词是 ctx.remote.$on。将来宿主流整体搬离本包时,该帧随之搬走,消费端契约不受影响(原委)。
  • 待处理交互状态位于宿主侧wire 使用 POST /api/respondRpcReceiptsrc/api-proxy.ts 中的表只处理问题,不包含审批条目。
  • 预留 seam 不进入 RpcMethodMapprompt.mode: 'inject'task.list 和描述字段 hostInstanceId 都是已记录的预留项;模型发现使用 llm.models。未知方法会在信封解析时直接失败,而不会返回「尚未实现」错误码。
  • 没有协议版本字段:客户端与宿主一同发布;只有出现独立发布的客户端后,host.describe 才会增加版本协商字段。
  • 搜索失败会包含提供方诊断信息:网关是单用户本地服务。将其暴露给多名用户的载体必须用可安全公开的诊断信息替代内部搜索细节。
  • Linux 原生选择器依赖桌面工具:在 native 能力下Zenity 和 KDialog 均未安装时,host.pickDirectory 会给出包含解决建议的错误提示;组合层面的回退是 browse 后端(见 native 后端 README)。
  • 冷会话的 updatedAt 会把一次单纯的拾起算作写入(仅逐文件后端):已附加投影排除了 session/end-seed 边界,因为接手一个会话不算活动;但冷会话的 updatedAt 取自其日志文件的 mtime而每一次持久写入都会刷新它包括这条边界。agentFor() 会在首次触碰时恢复一个冷会话,因此在客户端里仅仅打开一个会话就会写入它。这只适用于 locate() 能解析出逐会话产物的场景,即 JSONLSQLite 返回 undefined,因此它的冷会话回退到 createdAt,偏差方向相反——偏旧而不是偏新——且与这条边界无关。于是一个被触碰过却没有在里面工作过的会话,在重新附加之前会按晚于其最后一次真实活动的时间排序。要把两者区分开需要读取日志,而这恰恰是 mtime 路径存在的目的;在索引中存储一个最后活动字段可以从源头修好它,范围见最后活动索引 Agent Note