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.