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 index 0f7bd16552..07a35fa473 100644 --- 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 @@ -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-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 +2026-08-03-per-session-agent-presets.md: ee6303e5f52234d7eaf6768043c92a15e4e5399f +2026-08-03-per-session-agent-presets.zh.md: 5a2e1c3d8d0564f06c75785e1080b53740751355 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 9099207264..d7963f28de 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -373,7 +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.agentPresets` | `core` | [`agent-presets`](../packages/preset/agent-presets) | - | - | - | Discovers preset directories over trusted and user-authored roots and mounts one preset 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 775387ca6f..5542d296f1 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -115,25 +115,25 @@ Source: [`packages/core/agent-loop/src/index.ts:236`](../packages/core/agent-loo Requires: `loader` ```ts config-catalog -/** Plugin config: which profile is the default, and where profiles live. */ +/** Plugin config: which preset is the default, and where presets live. */ export interface Config { - /** Profile id mounted when a caller names none. Missing at mount time fails loud. */ + /** Preset 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. */ +/** One directory scanned for preset subdirectories. */ export interface PresetRoot { - /** Directory holding one subdirectory per profile; a leading `~` expands. */ + /** Directory holding one subdirectory per preset; a leading `~` expands. */ path: string - /** Trust recorded on every profile discovered under this root. */ + /** Trust recorded on every preset 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 + * Where a preset's composition came from. A `system` preset ships with the + * deployment; a `user` preset was authored locally, by a person or by an * agent, and therefore carries the same trust as shell access. */ export type PresetTrust = 'system' | 'user' diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 3030e2c348..0a70b0f69f 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -50,33 +50,33 @@ Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent- 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. +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. ```ts cordis-catalog /** - * Every profile the configured roots currently supply. - * @returns the profiles, first-root-wins per id. + * Every preset the configured roots currently supply. + * @returns the presets, 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. + * 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 /** - * Compose one agent from a profile, installing it under that agent alone. + * 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 profile never yields a half-composed + * the agent creation back, so a broken preset 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. + * @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 ``` diff --git a/docs/module-graph.md b/docs/module-graph.md index 3ad8ef0b7e..d4680deebe 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -220,6 +220,9 @@ flowchart TD subgraph group_mcp["packages/mcp"] pkg_mcp_client["mcp-client"] end + subgraph group_preset["packages/preset"] + pkg_agent_presets["agent-presets"] + end subgraph group_pty["packages/pty"] pkg_pty["pty"] pkg_pty_local["pty-local"] @@ -335,6 +338,9 @@ flowchart TD pkg_credentials --> pkg_invariants pkg_frontend_static --> pkg_host_webserver pkg_frontend_static --> pkg_invariants + pkg_agent_presets --> pkg_invariants + pkg_agent_presets --> pkg_paths + pkg_agent_presets --> pkg_scope pkg_helper --> pkg_brand pkg_helper --> pkg_invariants pkg_helper --> pkg_subprocess @@ -1132,6 +1138,7 @@ flowchart TD | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) | | [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | +| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope) | | [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) | | [`telemetry`](../packages/sdk/telemetry) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index 7980b809d6..3ef2ce6ee1 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: 365659617c97c44dd0f30fbcd3347b6438024eb3 -README.zh.md: 9edabd67ea728e77e2863a32c250675a5b9359f8 +README.md: b736aa5dc9d0e9313d652d40c3f4834456dccbb4 +README.zh.md: 53081b5e8c2d465dc866644eae78bebf0c4fc3a1 diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 55a50977a7..d296b1b519 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -86,15 +86,15 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ methods: [ { signature: 'async list(): Promise', - jsDoc: '/**\n * Every profile the configured roots currently supply.\n * @returns the profiles, first-root-wins per id.\n */', + jsDoc: '/**\n * Every preset the configured roots currently supply.\n * @returns the presets, 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 */', + jsDoc: '/**\n * Resolve one preset by id.\n * @param id - the preset id, or `undefined` for {@link defaultId}.\n * @returns the resolved preset.\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 */', + jsDoc: '/**\n * Compose one agent from a preset, 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 preset never yields a half-composed\n * session.\n * @param agentCtx - the agent\'s scope context.\n * @param id - the preset id, or `undefined` for {@link defaultId}.\n * @returns the preset that was mounted, for the caller to record.\n * @throws when the preset is unknown or its composition is unusable.\n */', }, ], }, diff --git a/packages/preset/README.i18n.yaml b/packages/preset/README.i18n.yaml index b554512392..f0176c1bc6 100644 --- a/packages/preset/README.i18n.yaml +++ b/packages/preset/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/preset/README.md -README.md: e7940642166f81e370e3a328f3097d15fd367151 -README.zh.md: 0767ca5074071e9ef2fa38769d27d8ef2344188e +README.md: 7baac391c224f717b60edeb0de828cb004ab460a +README.zh.md: 4d8c350b2831ae9c506ad7756ae157bf313a1c3e diff --git a/packages/preset/agent-presets/README.i18n.yaml b/packages/preset/agent-presets/README.i18n.yaml index 9106494073..b27e89ec7b 100644 --- a/packages/preset/agent-presets/README.i18n.yaml +++ b/packages/preset/agent-presets/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/preset/agent-presets/README.md -README.md: 6068a68d3c81081074165077a8afa6b42af48d1f -README.zh.md: 9f951f566a51a7b7acb666c7d9ea80061aa45d73 +README.md: 5d66c23f24717d1c30a9e729c7b0528715692d41 +README.zh.md: 9cdcd8b11a8a37d6d789c393e9a1500e4fbfd503 diff --git a/packages/preset/agent-presets/package.json b/packages/preset/agent-presets/package.json index 9dae4e584a..1411542d7c 100644 --- a/packages/preset/agent-presets/package.json +++ b/packages/preset/agent-presets/package.json @@ -21,9 +21,7 @@ "files": [ "lib/index.js", "lib/invariant.js", - "lib/types/**/*.d.ts", - "lib/types/**/*.d.ts.map", - "src" + "lib/types/**/*.d.ts" ], "license": "BSD-3-Clause", "peerDependencies": { diff --git a/packages/preset/agent-presets/src/mount.ts b/packages/preset/agent-presets/src/mount.ts index ba09869cb4..39d55b17e6 100644 --- a/packages/preset/agent-presets/src/mount.ts +++ b/packages/preset/agent-presets/src/mount.ts @@ -60,16 +60,34 @@ export interface PresetMount { const mounts = new Set() /** - * Every preset composition still installed, pruning fibers disposed since the - * last read. Records are dropped lazily rather than through a disposal hook + * Drop every record whose subtree is gone. + * + * Records are pruned by observation rather than through a disposal hook * because a subtree can be torn down by its owning agent, by a failed mount, or * by the whole tree unloading, and a cleared `uid` is what all three share. - * @returns the live mounts. + * + * Pruning therefore has to happen on a path this module owns. Reading is one + * such path, but not a reliable one: the only production reader is the + * invariant companion's service listener, and `dsh-invariants` is a + * development composition — a shipped host never loads it. Mounting is the + * other, and it is the one every session takes, which bounds the set at one + * generation of dead records rather than one per session ever composed. Each + * record would otherwise retain its whole disposed subtree: the fiber holds + * its config, and that config is the key its `EntryTree` is stored under. */ -export function livePresetMounts(): PresetMount[] { +function pruneDisposedMounts(): void { for (const mount of mounts) { if (mount.fiber.uid === null) mounts.delete(mount) } +} + +/** + * Every preset composition still installed, pruning fibers disposed since the + * last read. + * @returns the live mounts. + */ +export function livePresetMounts(): PresetMount[] { + pruneDisposedMounts() return [...mounts] } @@ -167,6 +185,9 @@ export async function mountPreset(agentCtx: Context, preset: AgentPreset): Promi ) } const config: Include.Config = { path: pathToFileURL(preset.path).href } + // Before the record this mount is about to add: every session takes this + // path, so it is what keeps the set bounded on a host that never reads it. + pruneDisposedMounts() const handle = agentCtx.plugin(PresetTree, config) try { await handle.await()