diff options
Diffstat (limited to 'notes/plan-user-agents.md')
| -rw-r--r-- | notes/plan-user-agents.md | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/notes/plan-user-agents.md b/notes/plan-user-agents.md new file mode 100644 index 0000000..012dbfb --- /dev/null +++ b/notes/plan-user-agents.md @@ -0,0 +1,206 @@ +# Implementation Plan: User Agents + +## Summary + +Two changes rolled into one: + +1. **`agent` becomes required** on `summon` — all spawned agents must use a definition +2. **New `top_level` mode** — spawns independent "user agent" tabs, gated by a new `perm_user_agent` permission + +A **user agent** is a top-level, independent tab spawned by an AI agent via the `summon` tool. Unlike **subagents** (child tabs owned by a parent), user agents appear as first-class tabs — persistent, independent lifecycle, no parent. They are fire-and-forget: the spawning agent gets an `agent_id` back but cannot `retrieve` the result. + +User agents **must** be spawned from a non-subagent agent definition (e.g. `default`). The definition controls their tools, models, working directory, and system prompt. Tools are still intersected with the spawning agent's own tools (can't escalate). A new `perm_user_agent` permission gates access to this capability, surfaced as a separate checkbox in the Tool Permissions UI. + +--- + +## Current Architecture (for context) + +Today, the `summon` tool creates **subagent tabs**: + +- They have a `parentTabId` linking them to the spawning tab +- They show in a **bottom row** under the parent tab in the tab bar +- They're **non-persistent** by default (italic, faded) until promoted +- Their **tools are restricted** — intersected with the parent's tool set (can't escalate) +- Their **working directory** must be within the parent's working directory +- They have **completion tracking** (`completionPromise`) — the parent blocks on `retrieve` until the child finishes +- The `agent` parameter is currently optional — agents can be spawned ad-hoc with just a `tools` list, inheriting the parent's model + +### Comparison table + +| Property | Subagent (current & updated) | User Agent (new) | +|---|---|---| +| `parentTabId` | set to parent | `null` | +| `persistent` | `false` (promoted on click) | `true` | +| Tab bar position | Bottom row under parent | Top row with user tabs | +| Tab lifecycle | Closed when parent closes | Independent | +| Retrievable | Yes, via `retrieve` tool | No — fire-and-forget | +| Working directory | Must be within parent's dir | Any dir (from definition or default) | +| Completion tracking | Yes (`completionPromise`) | No | +| Agent definition | Required | Required (`is_subagent !== true`) | +| Models | From agent definition | From agent definition | + +--- + +## Summon tool — new parameter shape + +``` +summon({ + task: string, // required — what to do + agent: string, // required — agent definition slug (was optional) + top_level?: boolean, // optional — spawn as user agent (only in schema if perm_user_agent enabled) + tools?: string[], // optional — override tools (intersected with spawning agent's tools) + background?: boolean, // optional — for subagents only (user agents are always fire-and-forget) + working_directory?: string // optional — override the definition's cwd +}) +``` + +### Tools resolution + +- **`tools` omitted**: agent definition's tools ∩ spawning agent's tools +- **`tools` provided**: provided tools ∩ spawning agent's tools + +Models always come from the agent definition. + +--- + +## Changes by file + +### 1. `packages/core/src/tools/summon.ts` + +**Schema changes:** + +- `agent` — change from `.optional()` to required +- `top_level` — new `z.boolean().optional()`, **only included in the schema when `userAgentEnabled` is `true`** +- `tools` — stays optional. Description updated: "Defaults to the agent definition's tools. Intersected with the spawning agent's tools." +- `background` — stays optional, ignored when `top_level: true` +- `working_directory` — stays optional + +**Factory signature change:** + +```ts +createSummonTool( + defaultWorkingDirectory: string, + callbacks: SummonCallbacks, + availableSubagents: AvailableAgent[], // is_subagent === true + availableUserAgents: AvailableAgent[], // is_subagent !== true + agentDirs: string[], + userAgentEnabled: boolean, // new — controls whether top_level param + user agent catalog exist +) +``` + +**`SummonCallbacks.spawn` interface:** + +- Add `topLevel?: boolean` to the spawn options object + +**`buildAgentsCatalog` update:** + +The catalog in the tool description is built conditionally: + +- **When `userAgentEnabled` is `false`**: only show the subagents group: + ``` + Available agents: + - programmer: Programmer — Implements code from a given plan + - flash: Flash — A cheap subagent + ``` + +- **When `userAgentEnabled` is `true`**: show two labeled groups: + ``` + Subagents (spawned as child tabs): + - programmer: Programmer — Implements code from a given plan + - flash: Flash — A cheap subagent + + User agents (spawned as independent top-level tabs, requires top_level=true): + - default: Default — Default agent with all tools enabled + ``` + +**`toAvailableAgents` → split into two functions:** + +- Rename existing to `toAvailableSubagents()` — keeps `is_subagent === true` filter +- New `toAvailableUserAgents()` — filters `is_subagent !== true` + +**Execute logic when `top_level: true`:** + +- Always return immediately with `agent_id` (fire-and-forget, ignore `background`) +- Call `callbacks.spawn(...)` with `topLevel: true` + +--- + +### 2. `packages/core/src/index.ts` + +- Re-export renamed `toAvailableSubagents` and new `toAvailableUserAgents` + +--- + +### 3. `packages/api/src/agent-manager.ts` + +**`getOrCreateAgentForTab` — permission reading & tool construction:** + +- Read new setting: `const permUserAgent = getSetting("perm_user_agent") === "allow"` +- Include `permUserAgent` in the `permKey` cache-invalidation string +- Load user agent definitions via `toAvailableUserAgents(...)` +- Pass `userAgentEnabled: permUserAgent` and `availableUserAgents` to `createSummonTool` + +**`spawnChildAgent` — when `topLevel: true`:** + +- **`parentTabId`**: pass `null` to `createTab()` and the `tab-created` event +- **Working directory**: use the agent definition's `cwd` if set, otherwise the global default (`DISPATCH_WORKING_DIR` / `process.cwd()`). **No containment check** against parent's directory. +- **Tools**: from definition (or `tools` param if provided), intersected with spawning agent's tools. Same intersection logic as subagents — can't escalate. +- **Models**: from the agent definition's `models` array +- **No completion tracking**: skip `completionPromise` / `completionResolve` setup. Leave them `undefined`. + +**`getChildResult` guard:** + +- If the tab has no `completionPromise` and status is `running`, return error: `"This is a user agent (top-level tab) and cannot be retrieved. User agents are fire-and-forget."` + +--- + +### 4. `packages/frontend/src/lib/components/ToolPermissions.svelte` + +Add a new entry to the `toolPermissions` array: + +```ts +{ + id: "user_agent", + label: "Spawn user agents", + description: "Allow the AI to open new independent top-level tabs" +} +``` + +--- + +### 5. `packages/frontend/src/lib/settings.svelte.ts` + +Add `user_agent: false` to the default `toolPerms` and `savedToolPerms` objects. + +--- + +## Files NOT changing + +| File | Why | +|---|---| +| `packages/frontend/src/lib/components/TabBar.svelte` | Already renders `parentTabId === null` tabs in top row as persistent | +| `packages/frontend/src/lib/tabs.svelte.ts` (`tab-created` handler) | Already sets `persistent: parentTabId == null` | +| `packages/core/src/tools/retrieve.ts` | Unchanged — the retrieve guard lives in AgentManager | +| `packages/core/src/agents/loader.ts` | `is_subagent` already exists and distinguishes the two types | +| DB schema | The `settings` table is key-value, no migration needed | +| Agent definition TOML format | `is_subagent` already exists | + +--- + +## Complete file change list + +| File | Change | +|---|---| +| `packages/core/src/tools/summon.ts` | `agent` required, conditional `top_level` param, two-group catalog (subagents-only when no perm), `toAvailableSubagents()` + `toAvailableUserAgents()`, spawn interface update | +| `packages/core/src/index.ts` | Re-export new/renamed functions | +| `packages/api/src/agent-manager.ts` | Read `perm_user_agent`, pass to factory, handle `topLevel` in spawn, retrieve guard | +| `packages/frontend/src/lib/components/ToolPermissions.svelte` | Add "Spawn user agents" checkbox | +| `packages/frontend/src/lib/settings.svelte.ts` | Add `user_agent` default | + +--- + +## Risks / edge cases + +- **Nested user agents**: A user agent could itself have `perm_user_agent` and spawn more user agents. This is allowed and works naturally since user agents are independent tabs with no parent chain. +- **Agent definition with no models**: Should not happen in practice — the Agent Builder UI requires at least one model entry. But if it does, the spawn will fail at the model-resolution step with a clear error. +- **Retrieve on user agent**: Guarded in `getChildResult` — returns an error message explaining user agents are fire-and-forget. |
