playbook-subagent-insert-design.md6.5 KBView on GitHub # Playbook editor — "Subagent" insert (/ and #)
Add a **Subagent** option to the playbook editor's `/` and `#` menus that creates a new
subagent **instruction document**, drops a `<ref>` to it at the cursor, and opens its
page — Notion `/page` style.
## Architecture principle
A subagent is a **passive instruction document** at
`user/playbooks/{aopId}/subagents/{name}.md`. It carries **no trigger of its own**. Its
timing comes entirely from the playbook `<trigger>` block whose `<ref id="…"/>` points at
it:
```xml
<trigger type="before-meeting" minutes="60">
<ref id="uuid-meeting-prep"/>
</trigger>
```
The `<trigger>` element (the `TriggerNode` "outer box" in the editor) owns the timing;
the subagent is a child reference. Scheduling reads the compiled
`documents.metadata.playbook_manifest` / `compiled_playbook` (before-meeting via
`scheduleBeforeMeetingAutomations`, cron via `getPlaybookCronConfigs`) — **not** the
`aop_agents` table (that path was removed). So creating a subagent means writing
instructions only; the user sets timing by referencing it inside a trigger block.
(Display-only caveat: `getSubagentDocsForAop` derives a `triggerType` from the **filename**
— `daily-agenda`→cron, `meeting-prep`→before-meeting, else event — used solely to group the
conversation-view agent list. It does not affect execution.)
## Current state
- The `/` and `#` menus already offer Slack / Trigger / iMessage / MCP via shared helpers
in [playbookExtensions.ts](../modules/documents/playbook/playbookExtensions.ts)
(`insertTrigger`, `insertIntegration`, `createPlaybookSlashCommands`,
`onHashMentionSelect`). Hash options live in
[HashMentionList.tsx](../modules/documents/playbook/HashMentionList.tsx).
- The canonical playbook reference is a `fileLink` node (`<ref id="documentId"/>`) — see
`serialize-playbook-xml.ts` `case 'fileLink'` and `FileLinkSuggestion.ts`.
- No non-seed mutation creates a subagent doc. `documents.create` only writes
`user/files/`; `aopAgents.upsert` writes a legacy `aop_agents` row + `agent-instructions`
doc, which `getSubagentDocsForAop` ignores.
## Changes
### Backend
**New mutation `aop.createSubagent`** ([aop.ts](../../server/src/trpc/routes/aop.ts)):
- Input `{ name, scope: 'user' | 'org', aopId?, orgAopId?, description? }` — no trigger
(timing lives on the trigger block).
- **Scope picks the folder:** `user` → `userSubagentPath(aopId, slug)` written with the
session `userId`; `org` → `orgSubagentPath(orgAopId, slug)` written with `userId: null`.
For org scope the `orgAopId` is taken from input, else resolved from the linked user AOP
(`agentOperatingProcedures.orgAopId`) — mirroring `saveCompositePlaybook`. Org scope is
authorised by requiring the org PLAYBOOK to exist in the caller's org.
- Verify AOP ownership; `slug = slugify(name)`; reject if a subagent at that path exists.
- `writeDocument({ documentType: DOCUMENT, mode:'upsert', content: frontmatter(name,
description) + starter body })`.
- Return `{ documentId, path, slug, name, scope }`. No `aop_agents` row; the doc's own id is
the agent id (`getSubagentDocsForAop` falls back to `row.id`).
- `aop.getCompositePlaybook` now also returns `orgAopId` so the composite editor can pass it.
### Frontend
- **Menus.** `subagent` in `HASH_MENTION_OPTIONS` (Bot icon) + a `Subagent` item in
`createPlaybookSlashCommands`, both routing to an `onCreateSubagent(editor, range)`
option threaded through `createPlaybookExtensions` (like `onOpenReference`).
- **Scope detection.** `scopeAtCursor(editor)` walks up to the nearest `scopeSection` and
reads its `scope` attr (`org`/`user`); the single-scope editors fall back to `baseScope`
(composite: `user`; playground single-doc: `isOrgAop`). The hook passes `scope` +
`aopId`/`orgAopId` to the dialog so the new subagent lands in the correct folder.
- **Create dialog.** [SubagentCreateDialog.tsx](../modules/documents/playbook/SubagentCreateDialog.tsx)
asks for a **name only** + [useSubagentCreation.tsx](../modules/documents/playbook/useSubagentCreation.tsx)
hook, mounted by `PlaybookDocument` and `CompositePlaybookDocument`. On submit it calls
`aop.createSubagent`, inserts a `<ref>` (`fileLink`) via `insertSubagentRef`, persists the
playbook via the editor's `save` (so the ref isn't lost / the unsaved-changes guard
doesn't block), then navigates to the new doc (`/brain?documentId={id}`) in the **same
tab**. If the cursor is already inside a `<trigger>` block the ref drops in there;
otherwise it wraps the ref in a fresh trigger (default `any`/event, picker open) so the
subagent isn't an orphan ref that shows in the conversation roster but never fires.
## Critical files
- `apps/server/src/trpc/routes/aop.ts` — `createSubagent` mutation.
- `apps/server/src/services/documents/convention-paths.ts` — `userSubagentPath`.
- `apps/server/src/services/aop/aop-agents.ts` — `getSubagentDocsForAop` (filename-derived
display triggerType; unchanged).
- `apps/mail/modules/documents/playbook/playbookExtensions.ts` — menu wiring +
`insertSubagentRef`.
- `apps/mail/modules/documents/playbook/{SubagentCreateDialog,useSubagentCreation}.tsx`.
- `apps/mail/modules/documents/playbook/{PlaybookDocument,CompositePlaybookDocument}.tsx`.
## Phased implementation
### Phase 1 — Backend
- [x] `aop.createSubagent` mutation — writes the instruction doc only, returns
`documentId`/`slug`.
### Phase 2 — Menu plumbing
- [x] `onCreateSubagent` option on `createPlaybookExtensions` /
`createCompositePlaybookExtensions`; route `#subagent` and `/Subagent` to it.
- [x] `subagent` in `HASH_MENTION_OPTIONS` and `createPlaybookSlashCommands` (Bot icon, top
group of `/`).
### Phase 3 — Create dialog + ref + navigation
- [x] `SubagentCreateDialog` (name only) + `useSubagentCreation` hook in both editors;
submit → mutation → open page in a new tab → insert `<ref>` (auto-wrapped in a trigger
when the cursor isn't already inside one, via `insertSubagentRef` / `isInsideTrigger`).
- [ ] Verify in-app: (a) inside a trigger block, `/` → Subagent → name → ref lands in that
block; (b) outside any trigger → a fresh trigger is created around the ref with its picker
open; the page opens in a new tab and the playbook compiles with the subagent referenced.
## Verification
- `pnpm --filter @zero/server exec vitest run src/services/playbook` — resolver tests pass.
- `pnpm types` clean for touched files.
- Manual: create a subagent inside a `before-meeting` trigger block; confirm the compiled
manifest schedules it and execution fires the referenced doc.