feat(agent-presets): compose each session's agent from a preset cordis.yml

A preset is a directory holding one `agent.cordis.yml`. Mounting it under an
agent's scope context during `setup(agentCtx)` gives that one session its own
tools and prompt sections while every other live session keeps its own.

No registry gains a tier. `dsh-tools` and `dsh-system-prompt` already file
registrations into the calling context's scope layer, and entry contexts chain
to the context a subtree was plugged into, so a composition mounted under
`agent.ctx` is that agent's alone and unwinds with it.

The mount audits itself because a directly-plugged subtree is absent from
`ctx.loader.entries()` and no boot audit covers it. It rejects an unscoped
target, a row that never became usable, and a row that published a service into
the root service realm — that last one is process-global rather than
per-session, and its collision with the next session surfaces as an unhandled
rejection `setup` never observes, leaving a half-composed agent that looks
healthy. The package invariant re-checks that rule on every service
notification, since a row publishing from a timer would escape a one-shot audit.

Raises the `packages/README.md` word ceiling from 920 to 980: the group table
must enumerate every group, and the new `preset/` row is necessary content.

Design: .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
This commit is contained in:
Yichen Jiang
2026-08-03 20:30:33 +08:00
parent 6a32047e77
commit 18fe174897
47 changed files with 1416 additions and 3 deletions

View File

@@ -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<AgentPreset[]>
/**
* 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<AgentPreset>
/**
* 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<AgentPreset>
```
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.