Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui

recompose becomes a parent re-link: the new preset's standing mount is
ensured BEFORE the link moves, so a failed switch leaves the agent exactly as
it was — the unmount-then-restore dance (and unmountPresetFor with it) is
gone, and the restore-failure test now asserts the agent KEEPS its tools with
the source directory deleted, because the standing mount is not the file.
This commit is contained in:
Yichen Jiang
2026-08-08 18:52:45 +08:00
24 changed files with 600 additions and 185 deletions

View File

@@ -68,14 +68,16 @@ async list(): Promise<AgentPreset[]>
async resolve(id?: string): Promise<AgentPreset>
/**
* Compose one agent from a preset, installing it under that agent alone.
* Compose one agent from a preset: ensure the preset's standing mount, then
* parent the agent's scope key to it so the mount's registrations and
* listeners cover this agent.
*
* 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.
* @returns the preset that was composed, for the caller to record.
* @throws when the preset is unknown or its composition is unusable.
*/
async mount(agentCtx: Context, id?: string): Promise<AgentPreset>
@@ -98,26 +100,42 @@ async mount(agentCtx: Context, id?: string): Promise<AgentPreset>
serviceFor<K extends string & keyof Context>(agent: { ctx: Context }, name: K): Context[K] | undefined
/**
* Replace the composition installed for one agent.
* Re-link one agent to a different preset's standing composition.
*
* Only valid while the agent has produced nothing: swapping tools mid
* conversation would leave logged tool calls the new composition cannot make.
* The CALLER owns that check — this method does not read session history.
* conversation would leave logged tool calls the new composition cannot
* make. The CALLER owns that check — this method does not read session
* history.
*
* The swap is unmount-then-mount because two compositions cannot coexist:
* both would register the same tool names into one layer. A failed mount
* therefore restores the previous composition rather than leaving the agent
* with nothing.
* The swap is a parent re-link, not an unmount: standing mounts are shared
* and permanent, so the old composition stays for its other agents and the
* new one is ensured BEFORE the link moves. An unknown or unusable preset
* therefore throws with the agent exactly as it was — there is no torn-down
* state to restore.
* @param agentCtx - the agent's scope context.
* @param id - the preset to compose the agent from instead.
* @returns the preset now installed.
* @throws when the preset is unknown or its composition is unusable; the
* previous composition is restored first.
* @throws when the preset is unknown or its composition is unusable.
*/
async recompose(agentCtx: Context, id: string): Promise<AgentPreset>
/**
* The standing scope key of one preset, for a host reader with no agent.
*
* A cold transcript read resolves tool presenters against the composition
* the session recorded, and the standing mount makes that possible without
* resuming anything: ensuring the mount composes plugins but starts no
* agent, no session, and no turn.
* @param id - the preset id, or `undefined` for {@link defaultId}.
* @returns the standing scope key readers pass as a registry view scope.
* @throws when the preset is unknown or its composition is unusable.
*/
async standingKeyFor(id?: string): Promise<ScopeKey>
```
Source: [`packages/preset/agent-presets/src/index.ts:57`](../../packages/preset/agent-presets/src/index.ts)
Types: [ScopeKey](../core-data-structures/scope.md)
Source: [`packages/preset/agent-presets/src/index.ts:68`](../../packages/preset/agent-presets/src/index.ts)
## `ctx.agents` — `AgentRegistry`