docs: add sub-agent activity WebUI design spec

This commit is contained in:
xiaoxixi 2026-08-13 16:29:19 +08:00
parent 5501c539fc
commit 0813eb4e6d

View File

@ -0,0 +1,126 @@
# Sub-Agent Activity WebUI Design
## Goal
Restructure the WebUI around sub-agent activity while moving sub-agent *definition* management into the settings page. Concretely:
1. Move sub-agent definition management (CRUD, enable/disable, role prompt) out of the `agents` navigation item into a new "子代理" tab on the settings page, and enlarge the role-prompt editing area.
2. Turn the `agents` navigation item into an activity monitor that shows running sub-agents and historical run results.
3. Add a read-only, chat-like detail view for a single sub-agent run, including a full, incrementally streamed message transcript.
4. Reduce the `tasks` page to scheduled-task information only; move the background-task listing into the sub-agent activity page.
## Non-goals
- No changes to sub-agent orchestration semantics (delegation, budgets, signals, run admission).
- No WebUI routing framework; the detail view is an in-page sub-view.
- No persistence of the main agent's chat history changes; the transcript work is scoped to sub-agent runs.
## Backend
### Schema (v9)
Add a new `agent_run_messages` table for incrementally streamed sub-agent transcripts:
```sql
CREATE TABLE IF NOT EXISTS agent_run_messages (
id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
seq INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
reasoning_content TEXT,
tool_call_id TEXT,
tool_name TEXT,
tool_calls_json TEXT,
created_at INTEGER NOT NULL,
FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_agent_run_messages_run_seq ON agent_run_messages(run_id, seq);
```
- Bump `SCHEMA_VERSION` from `8` to `9` in `src/storage/mod.rs`.
- Add the table DDL to `src/storage/agent_run.rs::AGENT_SCHEMA_STATEMENTS`.
- **Version-gate the existing agent-table drops.** `migrate_schema` currently drops `agent_runs`/`agent_inbox_events`/`agent_run_groups` unconditionally whenever `current < SCHEMA_VERSION`; on a v8→v9 upgrade this would destroy run history. Gate those drops (and the `background_tasks` drop) on `current < 8` — the batch-group removal is a pre-v8 concern — so a v8→v9 upgrade only adds the new table and preserves existing runs. When the drops do run (pre-v8), drop `agent_run_messages` before `agent_runs` so foreign-key enforcement never blocks the implicit row delete.
- Update the v8 schema test that hardcodes `assert_eq!(version, 8)` (`fresh_database_creates_schema_v8_agent_tables`) to v9 and rename it accordingly.
We deliberately do **not** add a transcript column to `agent_runs`. Incremental append rows in `agent_run_messages` are the single source of truth for transcripts.
### Incremental capture
- Add an optional sink to `AgentLoop`:
- field `transcript_sink: Option<tokio::sync::mpsc::UnboundedSender<ChatMessage>>`
- builder `with_transcript_sink(sender) -> Self`
- at every site where a message is appended to `emitted_messages` (assistant messages and tool result messages), forward a clone to the sink. Convert `append_steering_messages` from an associated function to a method so it can forward too (sub-agents never use steering, but the sink must be consistent for future use).
- `src/agent/sub_agent.rs::execute_resolved`:
- create an `mpsc::unbounded_channel`
- pass the sender via `build_sub_agent_with_provider``with_transcript_sink`
- spawn a writer task that owns the receiver and a `seq` counter; for each message it strips `provider_state` (set to `None`) and does `INSERT INTO agent_run_messages`
- restructure so the sender is dropped and the writer task is awaited in **every** exit path, including the `tokio::select!` cancellation arm that currently returns early without `process_with_context` returning; only after the writer drains does the terminal commit run
- the writer uses `self.storage` (the manager already holds `Option<Arc<Storage>>`); if storage is absent, the writer becomes a no-op
The task prompt is not duplicated into the table; the detail view renders `run.task` as the leading user bubble.
### Storage API
Add to `src/storage/agent_run.rs`:
- `append_agent_run_message(run_id, seq, &message, now) -> Result<()>` — single-row insert.
- `list_agent_run_messages(run_id, limit) -> Result<Vec<AgentRunMessageRecord>>` — ordered by `seq`. The transcript is naturally bounded: one run emits at most a handful of messages per tool iteration and iterations are capped by the definition's `limits.max_iterations` (default 99); default `limit` of `10_000` is a generous ceiling, not a pagination contract. `get_agent_run` uses this same default limit when loading the transcript.
Two distinct types to avoid a storage/protocol collision:
- `AgentRunMessageRecord` (storage, in `src/storage/agent_run.rs`): carries the raw columns — `id, run_id, seq, role, content, reasoning_content, tool_call_id, tool_name, tool_calls_json, created_at`.
- `AgentTranscriptMessage` (protocol, in `src/protocol.rs`): the serialized shape — same fields except `tool_calls_json` is parsed into `Vec<providers::ToolCall>`; a `From<AgentRunMessageRecord>` impl performs the parse.
### HTTP API
- `GET /api/agent-runs/{id}` currently returns `{ "run": AgentRunView }`. Extend the response to `{ "run": ..., "session_id": ..., "transcript": [ ... ] }` where:
- `session_id` is `run.root_session_id` (the `AgentRunView` deliberately omits it, so it is added at this endpoint's response level)
- `transcript` is the ordered list of `AgentTranscriptMessage` rows (exposing `reasoning_content` but never `provider_state`)
- `GET /api/agent-runs/{id}/events` is unchanged (signals/completions).
- `GET /api/tasks` is unchanged and already lists all runs in any status; the activity page consumes it.
## Frontend
### Settings page (`SettingsPage.svelte`)
- Add a "子代理定义" tab to the existing vertical `Tabs` (named to disambiguate from the "子代理" activity nav item). Move the definition list and the editor modal from `AgentsPage.svelte` here verbatim, then:
- enlarge the role-prompt `textarea` (`min-height` ~360px, full-width, monospace)
- widen the editor modal (`min(920px, 94vw)`) and put the role-prompt field on its own row
- keep the existing API calls (`/api/agents`, `/api/agents/options`, POST/DELETE) unchanged
### Sub-agent activity page (`AgentsPage.svelte`, rewritten)
- List view:
- "活动中" section: runs in `queued`/`running`/`waiting_children` status, with a pulse indicator, `agent_id`, prompt excerpt, `mode`/`depth`, and elapsed time.
- "历史活动" section: terminal runs (`completed`/`failed`/`timed_out`/`cancelled`/`interrupted`), newest first, with `StatusBadge`, `agent_id`, prompt excerpt, `tool_calls_count`/`iterations`, timestamps, and a "详情" action.
- Poll `GET /api/tasks?limit=200` every 5s while mounted.
- Detail view (in-page sub-view, back button, read-only):
- metadata header: `agent_id`, `StatusBadge`, `provider/model`, `mode`, `depth`, `tool_calls_count`, `iterations`, `session_id`, `started_at`/`finished_at`, duration, `parent_run_id` if present
- leading "task" bubble from `run.task`
- transcript messages rendered like the chat page: `Markdown` for content, collapsible reasoning block, `ToolCallCard` for tool calls. Pair each assistant message's `tool_calls` with its `tool`-role result by `tool_call_id` (mirroring `ChatPage`'s `toolResult()`), so calls and results remain independently collapsible.
- signal cards from `GET /api/agent-runs/{id}/events` (same rendering as the chat page's agent-event cards)
- final `error` card on failure
- while `status` is non-terminal, poll `GET /api/agent-runs/{id}` + `/events` every 2s to stream the growing transcript; stop polling on terminal status
- No composer, no input, no interactive actions other than navigation/collapse.
### Tasks page (`TasksPage.svelte`)
- Remove the "后台任务" tab and the `Tabs` wrapper; keep only the scheduled-job list (jobs + runs dots), which already comes from `/api/jobs` and `/api/jobs/{id}/runs`.
### Navigation (`App.svelte`)
- Update the `agents` page description to reflect activity monitoring ("查看活动中的子代理与历史运行"); keep the nav label "子代理".
- Update the `tasks` page description to scheduled tasks only ("管理定时任务").
## Error handling
- Missing/empty transcript: detail view renders the task bubble + metadata + error (or a "无转录" placeholder for failed runs).
- Storage write failures in the transcript writer are logged and the run still commits its terminal status; a broken transcript never fails the run.
- API auth/5xx reuse the existing `api()` error handling and `notify` toast path.
## Testing
- Rust: v9 migration from a v8 database **preserves existing `agent_runs` rows** (version-gated drops), and the pre-v8 legacy rebuild path still works; `append_agent_run_message` + `list_agent_run_messages` round-trip with `seq` ordering; `provider_state` is stripped from persisted transcript rows; sink drains on success, timeout, and cancellation before terminal commit (including the early-return cancellation arm); `get_agent_run` returns the transcript and `session_id`; `AgentRunView` list responses remain transcript-free; the v8 hardcoded schema-version assertion is updated to v9.
- Run `cargo test --lib` and `cargo clippy --all-targets --all-features -- -D warnings`.
- Frontend: `cd webui && npm run check && npm run build`, then `cargo build` to verify the `OUT_DIR` embedding path. No external runtime dependency; the browser stays on `/ws` and the existing HTTP endpoints.