diff --git a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml
new file mode 100644
index 0000000000..0f7bd16552
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml
@@ -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-03-per-session-agent-presets.md
+2026-08-03-per-session-agent-presets.md: 408e5a52b15efde162fa1bc6ae1e9ede8a7f0d98
+2026-08-03-per-session-agent-presets.zh.md: a06aea8d7e89c77d374c06908c10a9b0e5029548
diff --git a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
new file mode 100644
index 0000000000..ee6303e5f5
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
@@ -0,0 +1,46 @@
+# Agent Note: A session's agent is composed from a preset cordis.yml
+
+Status: implemented
+
+English | [中文](2026-08-03-per-session-agent-presets.zh.md)
+
+## Problem
+
+One `dsh` process serves many sessions, but the composition that decides what an agent *is* — its tools, persona, prompt sections, delegation backends — is fixed for the whole process by the `cordis.yml` the launcher booted. A deployment that wants a benchmark-minimal agent beside a full coding agent has to run two processes, and the shipped workaround (`apps/cli/config/core-web.cordis.yml`, a `--config` overlay that disables tool rows) changes every session at once.
+
+The obvious reading of "let a session pick its composition" is that the loader needs a new tier. It does not. [`dsh-tools`](../../../../packages/core/tools/README.md) and [`dsh-system-prompt`](../../../../packages/core/system-prompt/README.md) already file registrations into the calling context's scope layer, and [the agent is a registration scope](2026-07-08-agent-scope-contexts.md). What was missing is a way to point a whole `cordis.yml` at one agent's scope.
+
+## Decision
+
+A **preset** is a directory holding one `agent.cordis.yml`. The agent factory's `setup(agentCtx)` mounts it as a Cordis `include` subtree plugged into that agent's scope context. Entry contexts chain to the context a subtree was plugged into, so every registration inside the preset lands in that agent's layer and unwinds with the agent. No registry gains a tier, and no session already running is touched.
+
+Composition splits into two planes, decided by what must be shared rather than by what feels agent-related:
+
+| Plane | Instances | Contents |
+|---|---|---|
+| Host | one | The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), cross-session facilities (persistence, query, projections, storage, settings, credentials, telemetry), and the web host |
+| Agent | one per session | What a single agent contributes to those registries: tool plugins, persona and prompt sections, delegation backends, compaction policy |
+
+Model routing stays out of presets. `installAgentLlmTarget` is already the per-agent seam for provider, model, and reasoning effort, and an LLM adapter mounted inside a preset would never be resolved by `agent-loop`, which lives in the host plane.
+
+Mounting is per-session by default. Measured cost for a twelve-row composition is ~3ms and ~600KB per session, so isolation is the cheaper default than any sharing scheme, and a preset authored by a user or by an agent then has the smallest possible blast radius. A preset that genuinely owns an expensive singleton opts into sharing with Cordis's own `isolate` vocabulary: a named realm label is process-global, so two subtrees naming the same label resolve one instance.
+
+## Consequences
+
+**A directly-plugged subtree is invisible to the boot audit.** It never links itself to an `Entry`, so it is absent from `ctx.loader.entries()` and `assertEntriesActivated` cannot see it. The mount audits its own rows instead, reading the tree through an `Include` subclass that publishes it.
+
+**A preset may not publish into the root service realm.** Such a service is process-global rather than per-session, so the second session mounting the same preset collides with the first — and the collision surfaces as an unhandled rejection that `setup` never observes, leaving a half-composed agent that looks healthy. The mount rejects it instead, and the package invariant re-checks on every service notification because a row publishing from a timer or an asynchronous continuation would escape a one-shot audit.
+
+**Failure rolls the agent back.** `setup` runs before publication, so a rejected mount fails `ctx.agents.create()` and leaves nothing behind. This is why `setup` is the one supported call site.
+
+**Fiber membership is object identity, not `uid`.** A `uid` is a per-registry counter, so fibers in two different roots collide on it; comparing by `uid` made one runtime's subtree answer for a service published in another. `ctx.plugin()` returns a thenable `Object.create(fiber)` wrapper that is never identical to the fiber in a parent chain, so the subtree captures its own fiber during construction.
+
+**The preset id is model-visible and must be logged.** It determines the tool set and prompt, so a resumed session has to restore the same composition; recording it is a session fact, not runtime state.
+
+## Alternatives considered
+
+**Add a preset tier to the scoped registries.** `ScopedLayers.merge()` combines the global layer with exactly one exact-scope layer. A middle tier would let many sessions share one mounted composition, but it changes `dsh-scope` and every scope-aware registry to save a cost measured in milliseconds, and it gives a preset's registrations a lifetime no agent owns.
+
+**Make the agent's scope key the preset.** Sessions on one preset would share a layer for free, but per-agent registrations — `installAgentLlmTarget`, per-agent tool restrictions — would then collide across sessions.
+
+**Run each preset as a child process.** [`subagent-dsh-sdk`](../../../../packages/subagent/subagent-dsh-sdk/README.md) already proves a full child harness works, and isolation would be absolute. It also means proxying streaming, approvals, and projections per session, which is a transport project rather than a composition one.
diff --git a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md
new file mode 100644
index 0000000000..5a2e1c3d8d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md
@@ -0,0 +1,46 @@
+# Agent Note:会话的 agent 由一份 preset cordis.yml 组装而成
+
+Status: implemented
+
+[English](2026-08-03-per-session-agent-presets.md) | 中文
+
+## 问题
+
+一个 `dsh` 进程服务多个会话,但决定 agent(智能体)究竟是什么的那套组装——它的工具、人设、提示词段落、委派后端——由启动器所引导的 `cordis.yml` 一次性固定给整个进程。若某个部署希望一个 benchmark 精简 agent 与一个完整编码 agent 并存,就必须跑两个进程;而现有的变通方案(`apps/cli/config/core-web.cordis.yml`,一个用来禁用工具行的 `--config` 覆盖层)会一次性改变所有会话。
+
+对"让会话自选组装"最直觉的理解,是 loader 需要新增一层。其实不需要。[`dsh-tools`](../../../../packages/core/tools/README.md) 与 [`dsh-system-prompt`](../../../../packages/core/system-prompt/README.md) 本就按调用方上下文的 scope 分层归档注册,而且 [agent 本身就是一个注册 scope](2026-07-08-agent-scope-contexts.md)。此前缺的只是一种把整份 `cordis.yml` 指向某一个 agent scope 的办法。
+
+## 决策
+
+**preset** 是一个目录,其中放置一份 `agent.cordis.yml`。agent 工厂的 `setup(agentCtx)` 把它作为 Cordis `include` 子树,挂载到该 agent 的 scope 上下文之下。entry 上下文沿原型链连到子树被挂载时所在的上下文,因此 preset 内部的每一次注册都落进该 agent 的分层,并随 agent 一起卸载。没有任何注册表新增分层,也没有任何已在运行的会话被触及。
+
+组装划分为两个平面,依据是什么必须共享,而不是什么感觉上与 agent 有关:
+
+| 平面 | 实例数 | 内容 |
+|---|---|---|
+| 宿主 | 一份 | 注册表本身(`tools`、`systemPrompt`、`agents`、`agent-loop`、`sessions`)、跨会话设施(持久化、查询、投影、存储、设置、凭据、遥测),以及 web 宿主 |
+| agent | 每会话一份 | 单个 agent 对这些注册表的贡献:工具插件、人设与提示词段落、委派后端、压缩策略 |
+
+模型路由不进 preset。`installAgentLlmTarget` 已经是 provider、model 与 reasoning effort 的按 agent 可替换点;而挂在 preset 内部的 LLM 适配器永远不会被 `agent-loop` 解析到,因为后者位于宿主平面。
+
+挂载默认按会话进行。实测一份十二行组装每会话约 3ms、约 600KB,因此隔离比任何共享方案都更划算;而由用户或 agent 写出的 preset 也因此拥有尽可能小的影响面。确实自带昂贵单例的 preset,可以用 Cordis 自身的 `isolate` 词汇显式选择共享:命名 realm 的 label 是进程级全局的,因此两棵子树只要写同一个 label 就解析到同一个实例。
+
+## 后果
+
+**直接挂载的子树对启动审计不可见。** 它不会把自己关联到 `Entry`,因此不在 `ctx.loader.entries()` 中,`assertEntriesActivated` 也看不到它。改由挂载过程自行校验各行,通过一个会公开自身 tree 的 `Include` 子类读取。
+
+**preset 不得把服务发布进根 realm。** 这类服务是进程级全局而非按会话的,因此第二个挂载同一 preset 的会话会与第一个相撞——而这次相撞表现为 `setup` 永远观察不到的未处理 rejection,留下一个看起来健康、实则组装到一半的 agent。挂载改为直接拒绝它;本包的运行时不变量还会在每次服务通知时复查,因为从定时器或异步续体中发布的行会绕过一次性审计。
+
+**失败会让 agent 回滚。** `setup` 在发布之前运行,因此挂载被拒绝会让 `ctx.agents.create()` 失败且不留残留。这正是 `setup` 是唯一受支持调用点的原因。
+
+**fiber 归属判定用对象同一性,而非 `uid`。** `uid` 是按 registry 计数的序号,因此两个不同根下的 fiber 会在它上面撞号;按 `uid` 比较曾导致一个运行时的子树为另一个运行时中发布的服务背锅。`ctx.plugin()` 返回的是 thenable 的 `Object.create(fiber)` 包装对象,与父链中出现的 fiber 永远不同一,因此子树在构造时捕获自己的 fiber。
+
+**preset id 对模型可见,必须写入日志。** 它决定工具集与提示词,因此被恢复的会话必须还原同一份组装;记录它属于会话事实,而非运行时状态。
+
+## 考虑过的替代方案
+
+**在 scope 注册表中新增 preset 分层。** `ScopedLayers.merge()` 把全局层与恰好一个精确 scope 层合并。新增中间层可以让多个会话共用一份已挂载的组装,但它要改动 `dsh-scope` 及每个 scope 感知的注册表,换来的只是毫秒级的开销节省,而且会让 preset 的注册获得一个没有任何 agent 拥有的生命周期。
+
+**把 agent 的 scope 键设为 preset。** 同一 preset 上的会话就能免费共享一层,但按 agent 的注册——`installAgentLlmTarget`、按 agent 的工具限制——会跨会话相撞。
+
+**把每个 preset 作为子进程运行。** [`subagent-dsh-sdk`](../../../../packages/subagent/subagent-dsh-sdk/README.md) 已经证明完整的子 harness 可行,隔离性也会是绝对的。但这同时意味着要按会话代理流式输出、审批与投影,那是一个传输层项目,而非组装问题。
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index cb24cee7d5..9099207264 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -80,6 +80,8 @@ flowchart LR
svc_userInteraction["ctx.userInteraction Human question/answer seam"]
pkg_plan_mode["plan-mode"]
svc_planMode["ctx.planMode Plan collaboration state"]
+ pkg_agent_presets["agent-presets"]
+ svc_agentPresets["ctx.agentPresets Per-session agent composition"]
pkg_commands["commands"]
svc_commands["ctx.commands Human command registry"]
pkg_session_projection["session-projection"]
@@ -168,6 +170,7 @@ flowchart LR
pkg_acp --> svc_approval
pkg_agent --> svc_agents
pkg_agent_loop --> svc_agentLoop
+ pkg_agent_presets --> svc_agentPresets
pkg_approval --> svc_approval
pkg_bash --> svc_bash
pkg_bash_env --> svc_bashEnv
@@ -370,6 +373,7 @@ flowchart LR
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. |
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | - | [`tool-ask-user`](../packages/ui/tool-ask-user) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions. |
+| `ctx.agentPresets` | `core` | [`agent-presets`](../packages/preset/agent-presets) | - | - | - | Discovers profile directories over trusted and user-authored roots and mounts one profile cordis.yml under an agent scope during creation, rejecting a row that never activates or that publishes into the root service realm. |
| `ctx.commands` | `core` | [`commands`](../packages/ui/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. |
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session-projection/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session-title/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. |
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session-projection/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. |
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 0963f4d490..775387ca6f 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -110,6 +110,37 @@ Depends on: [`AgentOptions`](core-data-structures/core.md) · [`SessionId`](core
Source: [`packages/core/agent-loop/src/index.ts:236`](../packages/core/agent-loop/src/index.ts)
+## `@deepseek-ai/dsh-agent-presets`
+
+Requires: `loader`
+
+```ts config-catalog
+/** Plugin config: which profile is the default, and where profiles live. */
+export interface Config {
+ /** Profile id mounted when a caller names none. Missing at mount time fails loud. */
+ default: string
+ /** Scanned roots in precedence order; an earlier root wins a duplicate id. */
+ roots: PresetRoot[]
+}
+
+/** One directory scanned for profile subdirectories. */
+export interface PresetRoot {
+ /** Directory holding one subdirectory per profile; a leading `~` expands. */
+ path: string
+ /** Trust recorded on every profile discovered under this root. */
+ trust: PresetTrust
+}
+
+/**
+ * Where a profile's composition came from. A `system` profile ships with the
+ * deployment; a `user` profile was authored locally, by a person or by an
+ * agent, and therefore carries the same trust as shell access.
+ */
+export type PresetTrust = 'system' | 'user'
+```
+
+Source: [`packages/preset/agent-presets/src/types.ts:29`](../packages/preset/agent-presets/src/types.ts)
+
## `@deepseek-ai/dsh-agent-spine-demo`
```ts config-catalog
diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md
index 44a44a139e..3030e2c348 100644
--- a/docs/cordis-catalog/services.md
+++ b/docs/cordis-catalog/services.md
@@ -46,6 +46,43 @@ Types: [Agent](../core-data-structures/core.md) · [AgentOptions](../core-data-s
Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts)
+## `ctx.agentPresets` — `AgentPresets`
+
+Registry over the deployment's agent presets.
+
+Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a profile authored while the process runs is visible immediately, and a profile deleted underneath a picker disappears from the next read.
+
+```ts cordis-catalog
+/**
+ * Every profile the configured roots currently supply.
+ * @returns the profiles, first-root-wins per id.
+ */
+async list(): Promise
+
+/**
+ * Resolve one profile by id.
+ * @param id - the profile id, or `undefined` for {@link defaultId}.
+ * @returns the resolved profile.
+ * @throws when no configured root supplies that id.
+ */
+async resolve(id?: string): Promise
+
+/**
+ * Compose one agent from a profile, installing it under that agent alone.
+ *
+ * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls
+ * the agent creation back, so a broken profile never yields a half-composed
+ * session.
+ * @param agentCtx - the agent's scope context.
+ * @param id - the profile id, or `undefined` for {@link defaultId}.
+ * @returns the profile that was mounted, for the caller to record.
+ * @throws when the profile is unknown or its composition is unusable.
+ */
+async mount(agentCtx: Context, id?: string): Promise
+```
+
+Source: [`packages/preset/agent-presets/src/index.ts:36`](../../packages/preset/agent-presets/src/index.ts)
+
## `ctx.agents` — `AgentRegistry`
Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index 23794b5c9d..a805b4530c 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -66,6 +66,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `credentials/changed` | `runtime` (`emit`) | `ui-models` |
| `internal/dispatch` | - | [`commands`](../packages/ui/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workflow`](../packages/workflow/workflow) |
| `internal/plugin` | - | `hmr`, `loader`, `modules`, `webserver` |
+| `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets) |
| `internal/status` | - | [`agent`](../packages/core/agent) |
| `locale/change` | `locale` (`emit`) | `locale` |
| `models/changed` | `runtime` (`emit`) | `ui-models` |
diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml
index e721814e79..7980b809d6 100644
--- a/packages/README.i18n.yaml
+++ b/packages/README.i18n.yaml
@@ -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/README.md
-README.md: dec4d71ca2d323fe05f918dd3bf4709cfa01878e
-README.zh.md: 9596dfe8bf8d2d6144ffe7820886342707dd3009
+README.md: 365659617c97c44dd0f30fbcd3347b6438024eb3
+README.zh.md: 9edabd67ea728e77e2863a32c250675a5b9359f8
diff --git a/packages/README.md b/packages/README.md
index dec4d71ca2..b736aa5dc9 100644
--- a/packages/README.md
+++ b/packages/README.md
@@ -31,6 +31,7 @@ Packages live at `packages///`; groups are containers, while names r
| [`spill/`](spill/README.md) | Spill capability family: storage seam, local impl, tool-result spill policy | Product — stable surface |
| [`todo/`](todo/README.md) | The model-facing `todo_write` tool | Product — stable surface |
| [`plan/`](plan/README.md) | Plan collaboration state with a direct entry command and reviewed exit | Product — stable surface |
+| [`preset/`](preset/README.md) | Per-session agent composition from preset `cordis.yml` files | Product — stable surface |
| [`timeout/`](timeout/README.md) | Tool-call `tools/execute` deadline enforcement | Product — stable surface |
| [`guard/`](guard/README.md) | Loop-hygiene advisory repeat-call reminders | Product — stable surface |
| [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | Product — stable surface |
diff --git a/packages/README.zh.md b/packages/README.zh.md
index 9596dfe8bf..53081b5e8c 100644
--- a/packages/README.zh.md
+++ b/packages/README.zh.md
@@ -31,6 +31,7 @@
| [`spill/`](spill/README.md) | 溢出能力系列:存储 seam、本地实现、工具结果溢出策略 | 产品:稳定表面 |
| [`todo/`](todo/README.md) | 面向模型的 `todo_write` 工具 | 产品:稳定表面 |
| [`plan/`](plan/README.md) | Plan 协作状态,提供直接进入命令与经评审的退出 | 产品:稳定表面 |
+| [`preset/`](preset/README.md) | 由 preset `cordis.yml` 按会话组装 agent | 产品:稳定表面 |
| [`timeout/`](timeout/README.md) | 工具调用 `tools/execute` 截止时间强制执行 | 产品:稳定表面 |
| [`guard/`](guard/README.md) | 循环卫生建议性重复调用提醒 | 产品:稳定表面 |
| [`bundle/`](bundle/README.md) | 可安装的 `dsh --profile` 补丁层 | 产品:稳定表面 |
diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts
index 8a75107eea..55a50977a7 100644
--- a/packages/cordis/tool-cordis/src/api-catalog.ts
+++ b/packages/cordis/tool-cordis/src/api-catalog.ts
@@ -80,6 +80,24 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
],
},
+ {
+ key: 'agentPresets',
+ summary: 'Registry over the deployment\'s agent presets.',
+ methods: [
+ {
+ signature: 'async list(): Promise',
+ jsDoc: '/**\n * Every profile the configured roots currently supply.\n * @returns the profiles, first-root-wins per id.\n */',
+ },
+ {
+ signature: 'async resolve(id?: string): Promise',
+ jsDoc: '/**\n * Resolve one profile by id.\n * @param id - the profile id, or `undefined` for {@link defaultId}.\n * @returns the resolved profile.\n * @throws when no configured root supplies that id.\n */',
+ },
+ {
+ signature: 'async mount(agentCtx: Context, id?: string): Promise',
+ jsDoc: '/**\n * Compose one agent from a profile, installing it under that agent alone.\n *\n * Call from the agent factory\'s `setup(agentCtx)`; a rejection there rolls\n * the agent creation back, so a broken profile never yields a half-composed\n * session.\n * @param agentCtx - the agent\'s scope context.\n * @param id - the profile id, or `undefined` for {@link defaultId}.\n * @returns the profile that was mounted, for the caller to record.\n * @throws when the profile is unknown or its composition is unusable.\n */',
+ },
+ ],
+ },
{
key: 'agents',
summary: 'Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain.',
@@ -1601,6 +1619,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'AgentOptions',
declaration: 'export interface AgentOptions {\n provider?: string;\n model?: string;\n maxTokens?: number;\n}',
},
+ {
+ name: 'AgentPreset',
+ declaration: 'export interface AgentPreset {\n readonly id: string;\n readonly trust: PresetTrust;\n readonly path: string;\n}',
+ },
{
name: 'AgentSetup',
declaration: 'export type AgentSetup = (agentCtx: Context) => AgentSetupCommit | Promise | void;',
@@ -2193,6 +2215,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'PresetSpec',
declaration: 'export interface PresetSpec {\n sandbox: SandboxMode;\n approval: ApprovalPolicy;\n name?: string;\n description?: string;\n}',
},
+ {
+ name: 'PresetTrust',
+ declaration: 'export type PresetTrust = \'system\' | \'user\';',
+ },
{
name: 'ProjectionChangeListener',
declaration: 'export type ProjectionChangeListener = (session: Session, key: Extract, value: unknown, seq: number) => void;',
diff --git a/packages/preset/README.i18n.yaml b/packages/preset/README.i18n.yaml
new file mode 100644
index 0000000000..b554512392
--- /dev/null
+++ b/packages/preset/README.i18n.yaml
@@ -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 packages/preset/README.md
+README.md: e7940642166f81e370e3a328f3097d15fd367151
+README.zh.md: 0767ca5074071e9ef2fa38769d27d8ef2344188e
diff --git a/packages/preset/README.md b/packages/preset/README.md
new file mode 100644
index 0000000000..7baac391c2
--- /dev/null
+++ b/packages/preset/README.md
@@ -0,0 +1,13 @@
+# preset/ — per-session agent composition
+
+English | [中文](README.zh.md)
+
+An **agent preset** is a directory holding one `agent.cordis.yml`. Mounting it under an agent's scope context gives that session its own tools and prompt sections while every other live session keeps its own, so one process can run several differently composed agents at once.
+
+| Package | Role | ctx key |
+|---|---|---|
+| `agent-presets/` | Preset vocabulary, filesystem discovery over trusted and user-authored roots, and the guarded per-agent mount | `ctx.agentPresets` |
+
+The composition split this group assumes: registries and cross-session facilities are process singletons and stay in the host composition, while a preset carries what one agent contributes to them. A preset that names a row publishing a process-global service is rejected at mount rather than allowed to collide with the next session.
+
+Design: [the per-session agent-preset note](../../.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md).
diff --git a/packages/preset/README.zh.md b/packages/preset/README.zh.md
new file mode 100644
index 0000000000..4d8c350b28
--- /dev/null
+++ b/packages/preset/README.zh.md
@@ -0,0 +1,13 @@
+# preset/:按会话组装 agent
+
+[English](README.md) | 中文
+
+**agent preset** 是一个目录,其中放置一份 `agent.cordis.yml`。把它挂载到某个 agent(智能体)的 scope 上下文之下,该会话就获得自己的工具与提示词段落,而其他在运行的会话各自保持不变,因此一个进程可以同时运行多个组装方式不同的 agent。
+
+| 包 | 职责 | ctx 键 |
+|---|---|---|
+| `agent-presets/` | preset 词汇、在受信任目录与用户自建目录上的文件系统发现,以及带校验的按 agent 挂载 | `ctx.agentPresets` |
+
+本组假定的组装划分是:注册表与跨会话设施是进程单例,留在宿主组装中;preset 只承载单个 agent 对它们的贡献。若 preset 中某一行发布了进程级全局服务,挂载时即被拒绝,而不是留到与下一个会话相撞。
+
+设计详见 [按会话组装 agent preset 的 Agent Note](../../.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md)。
diff --git a/packages/preset/agent-presets/README.i18n.yaml b/packages/preset/agent-presets/README.i18n.yaml
new file mode 100644
index 0000000000..9106494073
--- /dev/null
+++ b/packages/preset/agent-presets/README.i18n.yaml
@@ -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 packages/preset/agent-presets/README.md
+README.md: 6068a68d3c81081074165077a8afa6b42af48d1f
+README.zh.md: 9f951f566a51a7b7acb666c7d9ea80061aa45d73
diff --git a/packages/preset/agent-presets/README.md b/packages/preset/agent-presets/README.md
new file mode 100644
index 0000000000..5d66c23f24
--- /dev/null
+++ b/packages/preset/agent-presets/README.md
@@ -0,0 +1,62 @@
+# dsh-agent-presets
+
+English | [中文](README.zh.md)
+
+Per-session agent composition. A **preset** is a directory holding one `agent.cordis.yml`; mounting it under an agent's scope context gives that one session its own tools, prompt sections, and other model-facing contributions, while every other live session keeps its own.
+
+The mechanism is entirely Cordis: entry contexts chain to the context a subtree was plugged into, and both [`dsh-tools`](../../core/tools/README.md) and [`dsh-system-prompt`](../../core/system-prompt/README.md) file registrations into the calling context's scope layer. Mounting a composition under `agent.ctx` therefore makes it that agent's alone, and unwinds it with the agent, without any new layering in those registries.
+
+## Service: `AgentPresets` (ctx key: `agentPresets`)
+
+Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call, so a preset authored while the process runs is visible immediately and a deleted one disappears from the next read.
+
+- `ctx.agentPresets.defaultId: string` The preset id mounted when a caller names none.
+- `ctx.agentPresets.list(): Promise` Every preset the configured roots currently supply, earlier root winning a duplicate id.
+- `ctx.agentPresets.resolve(id?): Promise` One preset by id, defaulting to `defaultId`. Throws naming the available ids when no root supplies it.
+- `ctx.agentPresets.mount(agentCtx, id?): Promise` Compose one agent from a preset and return the preset that was mounted, for the caller to record.
+
+`AgentPreset` carries `id` (the directory name), `trust` (`system` or `user`, from the root it was found under), and `path` (the absolute composition file).
+
+### Where to call `mount()`
+
+The agent factory's `setup(agentCtx)` hook is the one supported call site. Only there is the composition installed while the agent is still unpublished, so a rejected mount rolls the whole creation back rather than leaving a half-composed session. The subtree is owned by `agentCtx`'s fiber, so it unwinds with the agent and the caller receives no disposer.
+
+## Config
+
+| Field | Default | Meaning |
+|---|---|---|
+| `default` | required | Preset id mounted when a caller names none |
+| `roots` | `[]` | Scanned directories in precedence order; each supplies `path` (a leading `~` expands) and `trust` (defaults to `user`) |
+
+An absent root supplies no presets rather than failing: the user root does not exist until the first locally authored preset, and naming a default no root supplies already fails loud at resolution.
+
+## What a mount rejects
+
+A directly-plugged subtree is absent from `ctx.loader.entries()`, so no boot audit covers it. `mount()` therefore proves the result usable itself, and rejects three things.
+
+**An unscoped target.** Mounting into a context that carries no agent scope would register the preset's tools globally, for every agent in the process.
+
+**A row that never became usable.** The loader already rejects a row whose module failed to import or whose plugin threw; what remains is a row still waiting for a service the composition never supplies, which the audit names.
+
+**A row that published a service into the root realm.** Such a service is process-global rather than per-session, so the second session mounting the same preset collides with the first. A preset that genuinely owns a service puts it behind an `isolate` realm — entry-local for one session's private instance, or a shared label when several sessions should share one — or the service belongs in the host composition instead.
+
+The package invariant re-checks that last rule on every service notification, because a row that publishes from a timer or an asynchronous continuation would escape the one-shot audit.
+
+## Trust
+
+Presets are compositions, so a preset is exactly as privileged as the plugins it names. A `user` preset — authored by a person or by an agent — carries the same trust as shell access; the `trust` field exists so consumers can present that difference, not to enforce it.
+
+## Model Experience
+
+Indirectly, through the plugins a mounted composition registers, which own every tool schema and prompt section the preset makes visible to its one agent.
+
+#### KV Cache effect
+
+Prefix-stable for the life of an agent: a composition is installed once, before the agent is published and therefore before its first request, and is never re-read while the agent runs. Choosing a different preset for a new session establishes a different prefix for that session alone and cannot invalidate reuse for any session already running.
+
+## Known Limitations and Deferred Work
+
+- **A preset cannot be changed on a live agent** — the mount happens once during creation, so switching a running session's composition would mean unwinding its subtree mid-turn, dropping tools the model may already have called. Changing the default affects only sessions created afterwards.
+- **Display names are the directory id** — a preset carries no manifest, so pickers and settings surfaces show the id until a consumer needs richer metadata.
+- **`isolate` realms cannot be expressed across rows without `cordis:group`** — an entry-local realm works on a single row, but grouping a provider with its consumers under one shared realm needs the group builtin, which `dsh-app-boot` does not register.
+- **Root scans are not watched** — every read hits the filesystem instead, which keeps the roster fresh but puts one `readdir` per root on each `list()`.
diff --git a/packages/preset/agent-presets/README.zh.md b/packages/preset/agent-presets/README.zh.md
new file mode 100644
index 0000000000..9cdcd8b11a
--- /dev/null
+++ b/packages/preset/agent-presets/README.zh.md
@@ -0,0 +1,62 @@
+# dsh-agent-presets
+
+[English](README.md) | 中文
+
+按会话组装 agent(智能体)。**preset** 是一个目录,其中放置一份 `agent.cordis.yml`;把它挂载到某个 agent 的 scope 上下文之下,该会话就拥有自己的工具、提示词段落以及其他面向模型的贡献,而其他在运行的会话各自保持不变。
+
+其机制完全来自 Cordis:entry 上下文沿原型链连到子树被挂载时所在的上下文,而 [`dsh-tools`](../../core/tools/README.md) 与 [`dsh-system-prompt`](../../core/system-prompt/README.md) 本就按调用方上下文的 scope 分层归档注册。因此把一份组装挂到 `agent.ctx` 之下,它就只属于该 agent,并随 agent 一起卸载,无需在这些注册表中新增任何分层。
+
+## 服务:`AgentPresets`(ctx 键:`agentPresets`)
+
+发现过程不做缓存:`list()` 与 `resolve()` 每次调用都重新读取各个根目录,因此进程运行期间新写的 preset 立即可见,被删除的 preset 也会在下一次读取时消失。
+
+- `ctx.agentPresets.defaultId: string` 调用方未指定时挂载的 preset id。
+- `ctx.agentPresets.list(): Promise` 当前各根目录提供的全部 preset;id 重复时靠前的根目录胜出。
+- `ctx.agentPresets.resolve(id?): Promise` 按 id 取一个 preset,缺省取 `defaultId`。没有任何根目录提供该 id 时抛错,并列出可用 id。
+- `ctx.agentPresets.mount(agentCtx, id?): Promise` 用一个 preset 组装一个 agent,并返回所挂载的 preset 供调用方记录。
+
+`AgentPreset` 携带 `id`(目录名)、`trust`(`system` 或 `user`,取自它所在的根目录)以及 `path`(组装文件的绝对路径)。
+
+### 应在何处调用 `mount()`
+
+agent 工厂的 `setup(agentCtx)` 钩子是唯一受支持的调用点。只有在那里,组装是在 agent 尚未发布时装入的,因此挂载被拒绝会让整次创建回滚,而不会留下一个组装到一半的会话。子树归 `agentCtx` 的 fiber 所有,随 agent 一起卸载,调用方无需持有 disposer。
+
+## 配置
+
+| 字段 | 默认值 | 含义 |
+|---|---|---|
+| `default` | 必填 | 调用方未指定时挂载的 preset id |
+| `roots` | `[]` | 按优先级排列的扫描目录;每项提供 `path`(开头的 `~` 会展开)与 `trust`(默认为 `user`) |
+
+根目录不存在时视为不提供任何 preset,而非失败:用户根目录在写出第一个本地 preset 之前并不存在,而指定了没有任何根目录提供的默认值,在解析时本就会明确报错。
+
+## 挂载会拒绝什么
+
+直接挂载的子树不会出现在 `ctx.loader.entries()` 中,因此没有任何启动审计能覆盖它。`mount()` 因此自行校验结果可用,并拒绝三种情况。
+
+**目标上下文没有 scope。** 挂载到不带 agent scope 的上下文,会把该 preset 的工具注册成全局的,作用于进程内每一个 agent。
+
+**某一行始终未进入可用状态。** 模块导入失败或插件抛错的行,loader 已经会拒绝;剩下的情况是某一行仍在等待该组装从未提供的服务,审计会指名这种情况。
+
+**某一行把服务发布进了根 realm。** 这类服务是进程级全局而非按会话的,因此第二个挂载同一 preset 的会话会与第一个相撞。确实需要自带服务的 preset,应把它放在 `isolate` realm 之后——用 entry 本地 realm 得到该会话私有的实例,或用共享 label 让多个会话共用一个——否则该服务应改放进宿主组装。
+
+最后一条规则由本包的运行时不变量在每次服务通知时复查,因为从定时器或异步续体中发布的行会绕过一次性审计。
+
+## 信任
+
+preset 就是组装,因此一个 preset 的权限恰好等于它所引用的插件。`user` preset——无论由人还是由 agent 写出——与 shell 访问权限同级;`trust` 字段的存在是为了让消费方呈现这一差异,而不是用来强制隔离。
+
+## Model Experience
+
+Indirectly, through the plugins a mounted composition registers, which own every tool schema and prompt section the preset makes visible to its one agent.
+
+#### KV Cache effect
+
+在一个 agent 的整个生命周期内保持前缀稳定:组装只装入一次,发生在 agent 发布之前、因而也在它的首个请求之前,且在 agent 运行期间不再重新读取。为新会话选择不同的 preset,只会为该会话建立不同的前缀,无法让任何已在运行的会话失去缓存复用。
+
+## Known Limitations and Deferred Work
+
+- **无法在存活的 agent 上更换 preset** —— 挂载只在创建时发生一次,因此切换运行中会话的组装意味着要在轮次进行途中卸载其子树,抽走模型可能已经调用的工具。更改默认值只影响此后创建的会话。
+- **展示名称就是目录 id** —— preset 不携带 manifest,因此选择器与设置界面在有消费方需要更丰富的元数据之前,只显示 id。
+- **跨多行的 `isolate` realm 需要 `cordis:group` 才能表达** —— 单行可用 entry 本地 realm,但要把一个提供方与它的消费方归入同一个共享 realm,需要 group 内建插件,而 `dsh-app-boot` 并未注册它。
+- **根目录扫描不做监听** —— 每次读取都实际访问文件系统,这让名单保持新鲜,但每次 `list()` 会对每个根目录产生一次 `readdir`。
diff --git a/packages/preset/agent-presets/package.json b/packages/preset/agent-presets/package.json
new file mode 100644
index 0000000000..9dae4e584a
--- /dev/null
+++ b/packages/preset/agent-presets/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@deepseek-ai/dsh-agent-presets",
+ "description": "Per-session agent composition from preset cordis.yml files for the DeepSeek Harness",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "main": "lib/index.js",
+ "types": "lib/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/types/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./invariant": {
+ "types": "./lib/types/invariant.d.ts",
+ "default": "./lib/invariant.js"
+ },
+ "./src/*": "./src/*",
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "lib/index.js",
+ "lib/invariant.js",
+ "lib/types/**/*.d.ts",
+ "lib/types/**/*.d.ts.map",
+ "src"
+ ],
+ "license": "BSD-3-Clause",
+ "peerDependencies": {
+ "@cordisjs/plugin-include": "^1.0.4",
+ "@cordisjs/plugin-loader": "^1.0.0-rc.5",
+ "@deepseek-ai/dsh-invariants": "^0.0.1",
+ "@deepseek-ai/dsh-paths": "^0.0.1",
+ "@deepseek-ai/dsh-scope": "^0.0.1",
+ "cordis": "^4.0.0-rc.7"
+ },
+ "dependencies": {
+ "schemastery": "^3.18.0"
+ },
+ "devDependencies": {
+ "@cordisjs/plugin-include": "workspace:^",
+ "@cordisjs/plugin-loader": "workspace:^",
+ "@deepseek-ai/dsh-agent": "workspace:^",
+ "@deepseek-ai/dsh-agent-loop": "workspace:^",
+ "@deepseek-ai/dsh-invariants": "workspace:^",
+ "@deepseek-ai/dsh-llm": "workspace:^",
+ "@deepseek-ai/dsh-paths": "workspace:^",
+ "@deepseek-ai/dsh-scope": "workspace:^",
+ "@deepseek-ai/dsh-session": "workspace:^",
+ "@deepseek-ai/dsh-system-prompt": "workspace:^",
+ "@deepseek-ai/dsh-tools": "workspace:^",
+ "cordis": "^4.0.0-rc.7"
+ }
+}
diff --git a/packages/preset/agent-presets/src/discovery.ts b/packages/preset/agent-presets/src/discovery.ts
new file mode 100644
index 0000000000..2b3af0aa11
--- /dev/null
+++ b/packages/preset/agent-presets/src/discovery.ts
@@ -0,0 +1,75 @@
+/**
+ * Filesystem discovery of agent presets. A preset is a directory holding
+ * {@link COMPOSITION_FILE}; the directory name is the preset id. Discovery
+ * re-reads the roots on every call so a preset authored while the process is
+ * running is visible without a restart.
+ * @module @deepseek-ai/dsh-agent-presets/discovery
+ */
+
+import { readdir, stat } from 'node:fs/promises'
+import { join, resolve } from 'node:path'
+import { expandHomePath } from '@deepseek-ai/dsh-paths'
+import type { AgentPreset, PresetRoot } from './types.ts'
+
+/** The composition file that makes a directory a preset. */
+export const COMPOSITION_FILE = 'agent.cordis.yml'
+
+/**
+ * Whether `path` names an existing regular file.
+ * @param path - absolute path to test.
+ * @returns true when the path resolves to a file.
+ */
+async function isFile(path: string): Promise {
+ try {
+ return (await stat(path)).isFile()
+ } catch {
+ // Any stat failure — absent, unreadable, a dangling link — means this
+ // directory does not present a composition, which is not an error: the
+ // directory simply is not a preset.
+ return false
+ }
+}
+
+/**
+ * Scan one root for preset directories.
+ *
+ * An absent root yields no presets rather than throwing: the user root does
+ * not exist until the first locally authored preset, and naming a default
+ * that no root supplies already fails loud at resolution.
+ * @param root - the directory and the trust its presets inherit.
+ * @returns the root's presets ordered by id.
+ */
+export async function scanRoot(root: PresetRoot): Promise {
+ const dir = resolve(expandHomePath(root.path))
+ let children
+ try {
+ children = await readdir(dir, { withFileTypes: true })
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
+ throw new Error(`agent-presets: cannot read preset root ${dir}: ${String(error)}`, { cause: error })
+ }
+ const found: AgentPreset[] = []
+ for (const child of children) {
+ if (!child.isDirectory()) continue
+ const path = join(dir, child.name, COMPOSITION_FILE)
+ if (!await isFile(path)) continue
+ found.push({ id: child.name, trust: root.trust, path })
+ }
+ return found.sort((left, right) => left.id.localeCompare(right.id))
+}
+
+/**
+ * Scan every root in precedence order.
+ * @param roots - roots in precedence order; an earlier root wins a duplicate id.
+ * @returns every discovered preset, first-root-wins per id.
+ */
+export async function discoverPresets(roots: readonly PresetRoot[]): Promise {
+ const byId = new Map()
+ for (const root of roots) {
+ for (const preset of await scanRoot(root)) {
+ if (byId.has(preset.id)) continue
+ byId.set(preset.id, preset)
+ }
+ }
+ return [...byId.values()]
+}
diff --git a/packages/preset/agent-presets/src/index.ts b/packages/preset/agent-presets/src/index.ts
new file mode 100644
index 0000000000..778b0d7275
--- /dev/null
+++ b/packages/preset/agent-presets/src/index.ts
@@ -0,0 +1,100 @@
+/**
+ * Agent presets: each session composes its model-facing plugin set from one
+ * preset `cordis.yml` mounted under that agent's scope context.
+ *
+ * This package owns the preset vocabulary, filesystem discovery, and the
+ * guarded mount. It does not decide when an agent is created — the agent
+ * factory's `setup(agentCtx)` hook is the one supported call site, because
+ * only there is the composition installed while the agent is still
+ * unpublished, so a rejected mount rolls the whole creation back.
+ * @module @deepseek-ai/dsh-agent-presets
+ */
+
+import { Context, Service } from 'cordis'
+import z from 'schemastery'
+import { discoverPresets } from './discovery.ts'
+import { mountPreset } from './mount.ts'
+import type { AgentPreset, Config } from './types.ts'
+
+export { COMPOSITION_FILE, discoverPresets, scanRoot } from './discovery.ts'
+export { inactiveRows, leakedServices, livePresetMounts, mountPreset, type PresetMount } from './mount.ts'
+export type { AgentPreset, Config, PresetRoot, PresetTrust } from './types.ts'
+
+declare module 'cordis' {
+ interface Context {
+ agentPresets: AgentPresets
+ }
+}
+
+/**
+ * Registry over the deployment's agent presets.
+ *
+ * Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every
+ * call so a preset authored while the process runs is visible immediately,
+ * and a preset deleted underneath a picker disappears from the next read.
+ */
+export class AgentPresets extends Service {
+ static inject = ['loader']
+
+ /** Runtime schema for the preset roster. */
+ static Config = z.object({
+ default: z.string().required(),
+ roots: z.array(z.object({
+ path: z.string().required(),
+ trust: z.union(['system', 'user'] as const).default('user'),
+ })).default([]),
+ }) as z
+
+ constructor(ctx: Context, public config: Config) {
+ super(ctx, 'agentPresets')
+ }
+
+ /** The preset id mounted when a caller names none. */
+ get defaultId(): string {
+ return this.config.default
+ }
+
+ /**
+ * Every preset the configured roots currently supply.
+ * @returns the presets, first-root-wins per id.
+ */
+ async list(): Promise {
+ return await discoverPresets(this.config.roots)
+ }
+
+ /**
+ * Resolve one preset by id.
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
+ * @returns the resolved preset.
+ * @throws when no configured root supplies that id.
+ */
+ async resolve(id?: string): Promise {
+ const wanted = id ?? this.config.default
+ const presets = await this.list()
+ const found = presets.find(preset => preset.id === wanted)
+ if (found === undefined) {
+ const known = presets.map(preset => preset.id).join(', ')
+ throw new Error(`agent-presets: preset "${wanted}" not found (available: ${known || 'none'})`)
+ }
+ return found
+ }
+
+ /**
+ * Compose one agent from a preset, installing it under that agent alone.
+ *
+ * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls
+ * the agent creation back, so a broken preset never yields a half-composed
+ * session.
+ * @param agentCtx - the agent's scope context.
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
+ * @returns the preset that was mounted, for the caller to record.
+ * @throws when the preset is unknown or its composition is unusable.
+ */
+ async mount(agentCtx: Context, id?: string): Promise {
+ const preset = await this.resolve(id)
+ await mountPreset(agentCtx, preset)
+ return preset
+ }
+}
+
+export default AgentPresets
diff --git a/packages/preset/agent-presets/src/invariant.ts b/packages/preset/agent-presets/src/invariant.ts
new file mode 100644
index 0000000000..7a08eab7f1
--- /dev/null
+++ b/packages/preset/agent-presets/src/invariant.ts
@@ -0,0 +1,48 @@
+/**
+ * Package-owned invariant companion for `@deepseek-ai/dsh-agent-presets`.
+ * @module @deepseek-ai/dsh-agent-presets/invariant
+ */
+
+import type { Context } from 'cordis'
+import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
+// Imported through the package name, not `./mount.ts`: a module shared between
+// the two build entry points becomes a third chunk that the published `files`
+// list does not carry, which `verify-built-package-invariants` rejects.
+import { leakedServices, livePresetMounts } from '@deepseek-ai/dsh-agent-presets'
+
+const PACKAGE_NAME = '@deepseek-ai/dsh-agent-presets'
+
+/** Cordis companion plugin name. */
+export const name = 'agent-presets-invariant'
+/** Service required before the companion can reserve package ownership. */
+export const inject = ['invariants']
+
+/**
+ * Assert that no installed preset composition reaches the root service realm.
+ *
+ * `mountPreset` proves this once, when the subtree settles. A row that
+ * publishes later — from a timer, or an asynchronous continuation after its
+ * plugin returned — would escape that one-shot audit, so re-check every live
+ * mount whenever a service registration changes.
+ */
+const install: InvariantInstaller = (ctx, fail) => {
+ ctx.on('internal/service', function (this: Context, name) {
+ for (const mount of livePresetMounts()) {
+ const leaked = leakedServices(ctx, mount.fiber)
+ if (leaked.length === 0) continue
+ fail(
+ `preset "${mount.presetId}" published process-global service(s) [${leaked.join(', ')}] `
+ + `after its mount was audited (observed while notifying "${name}") — `
+ + 'a preset service must sit behind an `isolate` realm or move to the host composition',
+ )
+ }
+ }, { global: true })
+}
+
+/**
+ * Register this package's invariant companion.
+ * @param ctx - Cordis context carrying the invariant service.
+ * @returns the installed registration's disposer after setup succeeds.
+ */
+export const apply = (ctx: Context): Promise<() => void> =>
+ Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
diff --git a/packages/preset/agent-presets/src/mount.ts b/packages/preset/agent-presets/src/mount.ts
new file mode 100644
index 0000000000..ba09869cb4
--- /dev/null
+++ b/packages/preset/agent-presets/src/mount.ts
@@ -0,0 +1,205 @@
+/**
+ * Mount one preset composition under an agent's scope context, then prove the
+ * result is usable before the agent is published.
+ *
+ * The scope context is what makes the composition per-session: entry contexts
+ * chain to the context the subtree was plugged into, so every `ctx.tools`
+ * and `ctx.systemPrompt` registration inside the preset files into that
+ * agent's layer and unwinds with it. Two guards make that safe. A row that
+ * never reached a usable state is rejected, because a directly-plugged subtree
+ * is absent from `ctx.loader.entries()` and no boot audit covers it. A row that
+ * published a service into the ROOT realm is rejected, because such a service
+ * is process-global rather than per-session and the second session mounting the
+ * same preset collides with the first.
+ * @module @deepseek-ai/dsh-agent-presets/mount
+ */
+
+import { pathToFileURL } from 'node:url'
+import { Context, type Fiber } from 'cordis'
+import { Include } from '@cordisjs/plugin-include'
+import type { EntryTree } from '@cordisjs/plugin-loader'
+import { scopeOf } from '@deepseek-ai/dsh-scope'
+import type { AgentPreset } from './types.ts'
+
+/** What one mounted subtree publishes about itself for the audit to read. */
+interface MountedTree {
+ /** The rows the composition created. */
+ readonly tree: EntryTree
+ /**
+ * The subtree's own fiber. Captured here rather than taken from
+ * `ctx.plugin()`, which hands back a thenable `Object.create(fiber)` wrapper
+ * that is never identical to the fiber appearing in a parent chain.
+ */
+ readonly fiber: Fiber
+}
+
+/**
+ * Subtrees captured by config identity. A subtree plugged directly (rather than
+ * created as a loader entry) never links itself to an `Entry`, so this is the
+ * only handle to the rows it created; config objects are minted per mount, so
+ * concurrent mounts cannot collide.
+ */
+const mounted = new WeakMap