summaryrefslogtreecommitdiffhomepage
path: root/notes/plan-user-agents.md
blob: 012dbfb4f7725f986a2bd337f9159a7c0ebcd00b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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.