Files
openmaic/OpenMAIC/lib/agent/DESIGN-regenerate-scene.md
2026-08-16 14:58:47 +08:00

215 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MAIC Editor Agent — `regenerate_scene` (next-release capability widening)
- Date: 2026-06-21
- Base branch: `feat/maic-editor-agent-v0`
- Status: design approved, pending spec review → implementation plan
## 1. Background
The MAIC Editor Agent v0 (`feat/maic-editor-agent-v0`) stands up a server-side
`pi` Agent that streams `AgentEvent`s to the editor sidebar over SSE. Its
capability surface is deliberately one tool — `regenerate_scene_actions` — gated
by a tool allowlist (`V0_ALLOWLIST`). The design philosophy is **capability =
allowlist**: widening capability means adding tools to the allowlist, never
hardcoding a workflow.
Two limitations make v0 a demo rather than a usable editing assistant:
1. The agent can only regenerate **narration/actions**, not slide **content**.
The system prompt hard-refuses any content edit; the user must hand-edit the
canvas themselves.
2. The agent never **sees** the current scene. Scene context (outline / content)
is injected into the tool's deps (`getSceneContext`) but is invisible to the
model. So even `regenerate_scene_actions` is effectively a blind re-roll — the
model cannot translate "this slide is too dense" into a precise instruction
because it does not know what is on the slide.
## 2. Goal
Reach **basic user usability** in the next release by letting the agent
**read** a slide and then **regenerate the whole slide (content + narration)
under a natural-language instruction**, with a safe apply/restore story.
## 3. Scope decisions (locked)
| Decision | Choice | Rationale |
|---|---|---|
| Capability class | **Regeneration only** — no add / delete / reorder / insert | Structural ops are one click away on the canvas + NavRail; the agent's differentiated value is generation, not clicking structural buttons. Keeps allowlist + blast radius tight. |
| Regeneration granularity | **Whole page** (content + actions) in one tool | Simplest mental model — "redo this slide". Reuses the standard `generateSceneContent` → `generateSceneActions` steps. |
| Steerability | **Instruction-driven** (every regeneration accepts an optional NL instruction) | Without an instruction, regeneration is a slot machine, not a tool. This is the usability unlock. |
| Apply model | **Apply directly + explicit "还原到重生成前" button** on the tool card | Snapshots the pre-regenerate scene; does not rely on the user remembering Ctrl+Z; lighter than a full preview/approve surface. |
| Scene-type scope | **slide only** | Most pages are slides; lowest risk; matches the restrained v0 posture. Other types: the agent honestly says "not supported yet". |
| Agent ↔ content access | **`read_scene_content` tool** (read-then-act) | The agent reads the slide to reason and craft a precise instruction. Read-only is zero-risk and yields free Q&A ("what's on this slide?"). |
### Explicit non-goals (next release)
- No new-slide / delete / reorder / duplicate capability.
- No element-level or region-select local editing (a separate inline local-edit
track, out of scope here).
- No quiz / interactive / PBL regeneration.
- No multi-scene / deck-wide operations.
- No preview/diff surface beyond the restore button.
## 4. Architecture
The next-release allowlist grows from 1 tool to 3:
| Tool | Kind | Purpose |
|---|---|---|
| `read_scene_content` | read (new) | Return the current slide's content + outline for a `sceneId` so the model can reason, answer questions, and distil a precise regenerate instruction. |
| `regenerate_scene` | write (new) | Regenerate the whole slide (content + actions) for a `sceneId`, using **trusted injected content as baseline** + the agent's NL instruction → `generateSceneContent` + `generateSceneActions`. slide-only. |
| `regenerate_scene_actions` | write (existing) | Unchanged. Regenerate narration/actions only. |
### 4.1 Trust boundary (carries v0's "model is not a data source" rule)
- `read_scene_content` surfaces content **to the model's reasoning context**, on
demand, pulled from the same client-injected `sceneContextMap` the route
already receives. This replaces "pre-stuff everything" with "pull what you
need" — strictly more token-efficient.
- `regenerate_scene` **executes** against the trusted injected content resolved
by `sceneId` (never against content the model retyped). The model contributes
only `{ sceneId?, instruction? }`. The instruction is NL intent, not content.
So: the model reads content to **write a good instruction**; the generation
consumes content from the **trusted source**. Both paths are needed.
## 5. Components & changes
### 5.1 `read_scene_content` tool — `lib/agent/tools/read-scene-content.ts` (new)
- Factory `makeReadSceneContentTool(deps)` mirroring the
`makeRegenerateSceneActionsTool` shape.
- Args (typebox): `{ sceneId?: string }` — defaults to the active scene.
- `execute`: resolves `getSceneContext(sceneId)` and returns a compact,
model-readable projection of `{ title, type, outline, content }`. For
non-slide types it still returns a readable summary (read is safe for all
types even though regenerate is slide-only).
- Reuses the existing `SceneContext` deps; no new data plumbing.
### 5.2 `regenerate_scene` tool — `lib/agent/tools/regenerate-scene.ts` (new)
- Factory `makeRegenerateSceneTool({ aiCall, getSceneContext })`, sibling to
`regenerate-scene-actions.ts`.
- Args (typebox): `{ sceneId?: string, instruction?: string }`.
- `execute`:
1. Resolve trusted context (`outline`, `allOutlines`, current `content`,
`stageId`) via `getSceneContext`.
2. **Guard**: if `outline.type !== 'slide'`, return a typed refusal result
(the model relays "only slides are supported yet"). No generation.
3. Call the content pipeline (see 5.4) with the current content as baseline +
`instruction` as `editDirective`, then regenerate actions to match.
4. Return `{ content, actions }` in `details`, same `tool_execution_end`
contract as `regenerate_scene_actions`, so the client applies them.
### 5.3 Registry / allowlist — `lib/agent/tools/registry.ts`
- `ToolsetDeps` unchanged shape (same `SceneContext` deps reused by all three).
- `buildToolset` returns `[read_scene_content, regenerate_scene,
regenerate_scene_actions]`.
- `V0_ALLOWLIST` → `new Set(['read_scene_content', 'regenerate_scene',
'regenerate_scene_actions'])`.
### 5.4 Generation pipeline — `@openmaic/generation`
`generateSceneContent` already threads a `languageDirective` into its prompt;
this is the template for the new inputs. Add to `SceneContentOptions`:
- `editDirective?: string` — the agent's NL instruction, woven into the slide
content prompt the same way `languageDirective` is.
- `baselineContent?: GeneratedSlideContent` — the current slide content, fed as
the edit baseline so content-specific instructions ("drop the 2nd bullet",
"sharpen the title") operate on the real slide rather than re-rolling from
outline. When absent, behaviour is today's outline-based generation.
Only the **slide** branch of `generateSceneContent` consumes the baseline in the
next release; other branches ignore it (slide-only scope).
### 5.5 Server route — `app/api/agent/edit/route.ts`
- Wire the two new tools through `buildToolset` (no new request fields needed —
`sceneContextMap` already carries everything).
- The existing `aiCall` (resolved `maic-agent` model) is shared by all tools.
### 5.6 System prompt — `lib/agent/runtime/build-agent.ts`
Rewrite the capability-boundary paragraph from "you can ONLY regenerate
narration" to:
- You can **read** the current slide (`read_scene_content`) to understand it and
answer questions about it.
- You can **regenerate the whole slide** (content + narration) for **slide-type**
scenes, following the user's instruction (`regenerate_scene`); you can also
regenerate **only the narration** (`regenerate_scene_actions`).
- You still CANNOT: add / delete / reorder / duplicate slides; edit quiz,
interactive (GenUI) or whiteboard content; regenerate non-slide scenes — say
so honestly and suggest the user edits those on the canvas.
- Regeneration rebuilds the slide wholesale; it cannot guarantee specific
existing elements/cues survive unchanged.
### 5.7 Client — `components/edit/AgentPanel/*` + `lib/agent/client/*`
- New `regenerate-scene-tool-ui.tsx` tool card (sibling to the existing
`regenerate-tool-ui.tsx`): on `tool_execution_end`,
1. **snapshot** the scene's pre-regenerate `{ content, actions }` into the
card's local state,
2. apply the new `{ content, actions }` into `useStageStore`,
3. render a **"还原到重生成前"** button that re-applies the snapshot.
- `read_scene_content` needs no special card — its result feeds the model's next
turn; the assistant reply renders normally.
- `use-agent-runtime` apply logic generalised to dispatch by tool name
(regenerate_scene → content+actions; regenerate_scene_actions → actions only).
## 6. Data flow (happy path)
```
user: "这页太满了,精简成3个要点"
→ agent calls read_scene_content(activeScene)
→ tool returns {title, outline, content(7 bullets)}
→ agent reasons, calls regenerate_scene(sceneId, instruction:
"精简为3点,保留 X/Y/Z,删掉细节举例")
→ tool: guard slide ✓; generateSceneContent(outline, aiCall,
{baselineContent: current, editDirective: instruction})
→ generateSceneActions(...) ; returns {content, actions}
→ client tool card: snapshot old → apply new → show 还原 button
→ agent reply: "已精简为3个要点,旁白也对齐了。"
```
## 7. Guardrails
- Master gate unchanged: `isMaicEditorEnabled()` 404s the route when off.
- `beforeToolCall` allowlist gate now admits the 3 tools; everything else denied.
- `regenerate_scene` hard-refuses non-slide types inside `execute`.
- Quota: unchanged v0 unlimited stub (shape aligned to the planned quota hook);
regeneration is the expensive path, so this is where the quota hook will bite.
- Concurrency / superseded-run guards from v0 (stop-then-resend) apply unchanged.
## 8. Testing
Mirror the existing `tests/lib/agent/**` coverage:
- `read-scene-content.test.ts`: returns trusted projection; defaults to active
scene; safe on non-slide types.
- `regenerate-scene.test.ts`: slide → calls content+actions with baseline +
editDirective; non-slide → typed refusal, no generation; model-supplied
content is ignored (trust boundary).
- `scene-generator` content test: `editDirective` + `baselineContent` reach the
slide prompt; absent → today's behaviour unchanged (regression guard).
- `use-agent-runtime` client test: regenerate_scene applies content+actions and
the restore snapshot round-trips.
- allowlist test updated to expect the 3-tool set.
## 9. Risks & open points
- **Baseline-edit prompt quality**: feeding current content + instruction into
the slide prompt is the one non-trivial prompt change. Needs eval on a few
representative instructions (精简 / 加例子 / 改标题 / 删某点) before release.
- **Wholesale replace surprises**: even with the restore button, a user who
hand-edited then asked for an unrelated regeneration loses those edits until
they hit restore. Acceptable for "basic usability"; preview/approve is the
later upgrade.
- **Token growth**: `read_scene_content` puts slide content into the model
context. Single-slide content is small; do not let the agent read all siblings
by default.
- **Scene-type creep**: quiz/interactive are intentionally deferred; resist
widening the slide-only guard in this release.