# Constrained Demo Agent Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build a standard AI-app-shaped chat/event surface for presentation mode, backed by one deterministic prepared recipe and ready for a future Vercel AI SDK driver. **Architecture:** Add a small `demo/agent/` module that owns chat-compatible message parts, finite tool descriptors, a prepared recipe driver, and a React hook. The prepared driver emits the same event shape that a future AI SDK stream can emit, including workflow tools and presentation tools such as selecting the interrupt node. **Tech Stack:** React 19, TypeScript, Vitest, existing console RPC operation path, existing demo timeline models. Do not add Vercel AI SDK or model-provider dependencies in this slice. ## Global Constraints - Keep the prepared demo deterministic; no API key, model provider, or LLM call is required. - Chat must be standard AI-app-shaped: user messages, assistant text parts, tool-call parts, tool-result parts, error parts, and disabled/preset input affordance. - Separate workflow tools from presentation tools. Workflow tools produce operation evidence; presentation tools mutate only stage focus. - The future AI SDK seam must be explicit, but `ai` / `@ai-sdk/react` are out of scope for this slice. - Effect may be used later at the driver/tool boundary, especially server-side. This browser slice must not add Effect ceremony to render components. - The visual style is not the goal of this slice; preserve existing presentation styling unless a minimal class is needed for new chat parts. - Copy must say "prepared recipe" or "constrained demo agent", not "autonomous agent". --- ## File Structure Create: - `web/apps/console/src/demo/agent/events.ts` — chat message/event types and constructors. - `web/apps/console/src/demo/agent/tools.ts` — finite workflow/presentation tool descriptors. - `web/apps/console/src/demo/agent/recipes.ts` — `prepare-thesis-report` recipe metadata and scripted steps. - `web/apps/console/src/demo/agent/preparedRecipeDriver.ts` — deterministic async event driver over replay/live timeline operations. - `web/apps/console/src/demo/agent/useDemoAgent.ts` — React hook that owns agent messages and presentation actions. - `web/apps/console/src/demo/agent/*.test.ts` — focused tests for event shape, tool allow-list, driver order, and hook reset behavior. Modify: - `web/apps/console/src/presentation/OperatorChat.tsx` — render message parts instead of fixed static copy. - `web/apps/console/src/presentation/PresentationRoute.tsx` — create/use the agent hook and apply emitted presentation actions. - `web/apps/console/src/presentation/PresentationStage.tsx` or child props as needed — receive selected node/evidence state from agent actions. - `web/apps/console/src/presentation/presentation.css` — minimal styles for tool-call/tool-result parts. - `web/apps/console/src/presentation/OperatorChat.test.tsx` and `PresentationRoute.test.tsx` — update tests for event-driven chat and stage action. - `web/README.md` and `docs/current_roadmap.md` — document the constrained demo agent slice. --- ### Task 1: Agent Event And Tool Contracts **Files:** - Create: `web/apps/console/src/demo/agent/events.ts` - Create: `web/apps/console/src/demo/agent/tools.ts` - Test: `web/apps/console/src/demo/agent/events.test.ts` - Test: `web/apps/console/src/demo/agent/tools.test.ts` **Interfaces:** - Produces: - `AgentMessage` - `AgentMessagePart` - `AgentToolName` - `PresentationToolAction` - `agentTextMessage(...)` - `agentToolCallPart(...)` - `agentToolResultPart(...)` - `presentationActionPart(...)` - `isAllowedAgentToolName(...)` - [ ] **Step 1: Write event contract tests** Create `web/apps/console/src/demo/agent/events.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { agentTextMessage, agentToolCallPart, agentToolResultPart, presentationActionPart, } from "./events.js"; describe("agent events", () => { it("creates a standard assistant text message", () => { const message = agentTextMessage("m1", "assistant", "I will use a prepared recipe."); expect(message).toEqual({ id: "m1", role: "assistant", parts: [{ type: "text", text: "I will use a prepared recipe." }], }); }); it("creates tool call, tool result, and presentation action parts", () => { expect(agentToolCallPart("c1", "startPreparedReportRun", { deploymentId: "lda_report_case_study.default" })).toEqual({ type: "tool-call", call: { id: "c1", name: "startPreparedReportRun", input: { deploymentId: "lda_report_case_study.default" }, }, }); expect(agentToolResultPart("c1", "startPreparedReportRun", "success", { runId: "run_1" })).toEqual({ type: "tool-result", result: { callId: "c1", name: "startPreparedReportRun", status: "success", output: { runId: "run_1" }, }, }); expect(presentationActionPart({ type: "selectWorkflowNode", nodeId: "review_issues" })).toEqual({ type: "presentation-action", action: { type: "selectWorkflowNode", nodeId: "review_issues" }, }); }); }); ``` - [ ] **Step 2: Write tool allow-list tests** Create `web/apps/console/src/demo/agent/tools.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { AGENT_TOOLS, isAllowedAgentToolName } from "./tools.js"; describe("agent tools", () => { it("separates workflow tools from presentation tools", () => { expect(AGENT_TOOLS.inspectDeployment.kind).toBe("workflow"); expect(AGENT_TOOLS.startPreparedReportRun.kind).toBe("workflow"); expect(AGENT_TOOLS.resumeIssueReview.kind).toBe("workflow"); expect(AGENT_TOOLS.readRunTrace.kind).toBe("workflow"); expect(AGENT_TOOLS.selectWorkflowNode.kind).toBe("presentation"); expect(AGENT_TOOLS.openEvidence.kind).toBe("presentation"); }); it("rejects unknown tool names", () => { expect(isAllowedAgentToolName("selectWorkflowNode")).toBe(true); expect(isAllowedAgentToolName("readFile")).toBe(false); expect(isAllowedAgentToolName("authorArbitraryWorkflow")).toBe(false); }); }); ``` - [ ] **Step 3: Run tests to verify they fail** Run: ```bash pnpm --dir web --filter @lda/console test -- events tools ``` Expected: FAIL because the new modules do not exist. - [ ] **Step 4: Implement event and tool contracts** Create `web/apps/console/src/demo/agent/tools.ts`: ```ts export type WorkflowToolName = | "inspectDeployment" | "startPreparedReportRun" | "resumeIssueReview" | "readRunTrace"; export type PresentationToolName = | "selectWorkflowNode" | "focusOperation" | "openEvidence" | "showTraceFrame" | "setBeat"; export type AgentToolName = WorkflowToolName | PresentationToolName; export type AgentToolDescriptor = { readonly name: AgentToolName; readonly kind: "workflow" | "presentation"; readonly description: string; }; export const AGENT_TOOLS = { inspectDeployment: { name: "inspectDeployment", kind: "workflow", description: "Inspect the prepared report deployment.", }, startPreparedReportRun: { name: "startPreparedReportRun", kind: "workflow", description: "Start the prepared report workflow run.", }, resumeIssueReview: { name: "resumeIssueReview", kind: "workflow", description: "Resume the typed issue-review interrupt.", }, readRunTrace: { name: "readRunTrace", kind: "workflow", description: "Read trace frames for the completed report run.", }, selectWorkflowNode: { name: "selectWorkflowNode", kind: "presentation", description: "Focus a workflow graph node in the presentation.", }, focusOperation: { name: "focusOperation", kind: "presentation", description: "Focus an operation event in the presentation.", }, openEvidence: { name: "openEvidence", kind: "presentation", description: "Open evidence for an operation event.", }, showTraceFrame: { name: "showTraceFrame", kind: "presentation", description: "Focus a trace frame in the presentation.", }, setBeat: { name: "setBeat", kind: "presentation", description: "Move the presentation to a named beat.", }, } satisfies Record; export const isAllowedAgentToolName = (name: string): name is AgentToolName => Object.hasOwn(AGENT_TOOLS, name); ``` Create `web/apps/console/src/demo/agent/events.ts`: ```ts import type { AgentToolName } from "./tools.js"; export type AgentRole = "user" | "assistant"; export type PresentationToolAction = | { readonly type: "selectWorkflowNode"; readonly nodeId: string } | { readonly type: "focusOperation"; readonly eventId: string } | { readonly type: "openEvidence"; readonly eventId: string } | { readonly type: "showTraceFrame"; readonly frameIndex: number } | { readonly type: "setBeat"; readonly beatId: string }; export type AgentToolCall = { readonly id: string; readonly name: AgentToolName; readonly input: unknown; }; export type AgentToolResult = { readonly callId: string; readonly name: AgentToolName; readonly status: "success" | "failure"; readonly output: unknown; }; export type AgentMessagePart = | { readonly type: "text"; readonly text: string } | { readonly type: "tool-call"; readonly call: AgentToolCall } | { readonly type: "tool-result"; readonly result: AgentToolResult } | { readonly type: "presentation-action"; readonly action: PresentationToolAction } | { readonly type: "error"; readonly message: string }; export type AgentMessage = { readonly id: string; readonly role: AgentRole; readonly parts: ReadonlyArray; }; export const agentTextMessage = (id: string, role: AgentRole, text: string): AgentMessage => ({ id, role, parts: [{ type: "text", text }], }); export const agentToolCallPart = ( id: string, name: AgentToolName, input: unknown, ): AgentMessagePart => ({ type: "tool-call", call: { id, name, input }, }); export const agentToolResultPart = ( callId: string, name: AgentToolName, status: "success" | "failure", output: unknown, ): AgentMessagePart => ({ type: "tool-result", result: { callId, name, status, output }, }); export const presentationActionPart = (action: PresentationToolAction): AgentMessagePart => ({ type: "presentation-action", action, }); ``` - [ ] **Step 5: Run tests** Run: ```bash pnpm --dir web --filter @lda/console test -- events tools ``` Expected: PASS. - [ ] **Step 6: Commit** ```bash git add web/apps/console/src/demo/agent/events.ts web/apps/console/src/demo/agent/tools.ts web/apps/console/src/demo/agent/events.test.ts web/apps/console/src/demo/agent/tools.test.ts git commit -m "feat: define demo agent event contract" ``` --- ### Task 2: Prepared Recipe Metadata And Replay Driver **Files:** - Create: `web/apps/console/src/demo/agent/recipes.ts` - Create: `web/apps/console/src/demo/agent/preparedRecipeDriver.ts` - Test: `web/apps/console/src/demo/agent/preparedRecipeDriver.test.ts` **Interfaces:** - Consumes: - `AgentMessage` and message-part helpers from Task 1. - `DemoEvent` from `web/apps/console/src/demo/timeline/models.ts`. - `loadCanonicalDemoRecording()` from `web/apps/console/src/demo/timeline/replay.ts`. - Produces: - `PREPARE_THESIS_REPORT_RECIPE` - `runPreparedRecipeReplay(): AsyncIterable` - [ ] **Step 1: Write replay driver tests** Create `web/apps/console/src/demo/agent/preparedRecipeDriver.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { runPreparedRecipeReplay } from "./preparedRecipeDriver.js"; const collect = async (events: AsyncIterable): Promise> => { const collected: T[] = []; for await (const event of events) collected.push(event); return collected; }; describe("prepared recipe driver", () => { it("emits a standard chat sequence for the replay recipe", async () => { const messages = await collect(runPreparedRecipeReplay()); expect(messages[0]?.role).toBe("user"); expect(messages.some((message) => message.parts.some((part) => part.type === "tool-call" && part.call.name === "startPreparedReportRun"), )).toBe(true); expect(messages.some((message) => message.parts.some((part) => part.type === "presentation-action" && part.action.type === "selectWorkflowNode"), )).toBe(true); expect(messages.at(-1)?.parts.some((part) => part.type === "text" && part.text.includes("run evidence"), )).toBe(true); }); it("does not emit unknown tool calls", async () => { const messages = await collect(runPreparedRecipeReplay()); const toolNames = messages.flatMap((message) => message.parts.flatMap((part) => part.type === "tool-call" ? [part.call.name] : []), ); expect(toolNames).toEqual([ "inspectDeployment", "startPreparedReportRun", "selectWorkflowNode", "resumeIssueReview", "readRunTrace", "openEvidence", ]); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash pnpm --dir web --filter @lda/console test -- preparedRecipeDriver ``` Expected: FAIL because driver files do not exist. - [ ] **Step 3: Implement recipe metadata** Create `web/apps/console/src/demo/agent/recipes.ts`: ```ts import { LDA_REPORT_DEPLOYMENT_ID } from "../ldaReportDemoConfig.js"; import type { AgentToolName } from "./tools.js"; export type PreparedRecipeStep = { readonly id: string; readonly narration: string; readonly toolName: AgentToolName | null; }; export type PreparedRecipe = { readonly id: "prepare-thesis-report"; readonly title: string; readonly userPrompt: string; readonly deploymentId: string; readonly steps: ReadonlyArray; }; export const PREPARE_THESIS_REPORT_RECIPE: PreparedRecipe = { id: "prepare-thesis-report", title: "Prepare thesis readiness report", userPrompt: "Prepare the thesis readiness report.", deploymentId: LDA_REPORT_DEPLOYMENT_ID, steps: [ { id: "select-recipe", narration: "I found a prepared workflow recipe for the thesis readiness report.", toolName: null, }, { id: "inspect-deployment", narration: "I will inspect the prepared deployment before starting a run.", toolName: "inspectDeployment", }, { id: "start-run", narration: "I will start the prepared workflow run.", toolName: "startPreparedReportRun", }, { id: "focus-interrupt", narration: "Let's zoom into the typed issue-review interrupt.", toolName: "selectWorkflowNode", }, { id: "resume", narration: "I will resume the run with the selected issues.", toolName: "resumeIssueReview", }, { id: "trace", narration: "I will read the run trace as evidence.", toolName: "readRunTrace", }, { id: "open-evidence", narration: "I will open the evidence linked to the trace call.", toolName: "openEvidence", }, ], }; ``` - [ ] **Step 4: Implement replay driver** Create `web/apps/console/src/demo/agent/preparedRecipeDriver.ts`: ```ts import { loadCanonicalDemoRecording } from "../timeline/replay.js"; import { agentTextMessage, agentToolCallPart, agentToolResultPart, presentationActionPart, type AgentMessage, } from "./events.js"; import { PREPARE_THESIS_REPORT_RECIPE } from "./recipes.js"; export async function* runPreparedRecipeReplay(): AsyncIterable { const recording = loadCanonicalDemoRecording(); const deploymentId = PREPARE_THESIS_REPORT_RECIPE.deploymentId; const runStart = recording.events.find((event) => event.stage === "run_start"); const resume = recording.events.find((event) => event.stage === "run_resume"); const trace = recording.events.find((event) => event.stage === "trace_read"); const runId = runStart?.resultingIds.runId ?? "recorded-run"; yield agentTextMessage("recipe-user", "user", PREPARE_THESIS_REPORT_RECIPE.userPrompt); yield agentTextMessage("recipe-selected", "assistant", PREPARE_THESIS_REPORT_RECIPE.steps[0]!.narration); yield { id: "inspect-deployment", role: "assistant", parts: [ agentToolCallPart("inspect-deployment-call", "inspectDeployment", { deploymentId }), agentToolResultPart("inspect-deployment-call", "inspectDeployment", "success", { deploymentId }), ], }; yield { id: "start-run", role: "assistant", parts: [ agentToolCallPart("start-run-call", "startPreparedReportRun", { deploymentId }), agentToolResultPart("start-run-call", "startPreparedReportRun", "success", { runId, eventId: runStart?.id ?? null, }), ], }; yield { id: "focus-interrupt", role: "assistant", parts: [ agentToolCallPart("select-interrupt-call", "selectWorkflowNode", { nodeId: "review_issues" }), presentationActionPart({ type: "selectWorkflowNode", nodeId: "review_issues" }), agentToolResultPart("select-interrupt-call", "selectWorkflowNode", "success", { nodeId: "review_issues" }), ], }; yield { id: "resume-run", role: "assistant", parts: [ agentToolCallPart("resume-run-call", "resumeIssueReview", { runId }), agentToolResultPart("resume-run-call", "resumeIssueReview", "success", { runId, eventId: resume?.id ?? null, }), ], }; yield { id: "read-trace", role: "assistant", parts: [ agentToolCallPart("read-trace-call", "readRunTrace", { runId }), agentToolResultPart("read-trace-call", "readRunTrace", "success", { runId, eventId: trace?.id ?? null, }), ], }; yield { id: "open-evidence", role: "assistant", parts: [ agentToolCallPart("open-evidence-call", "openEvidence", { eventId: trace?.id ?? "trace" }), presentationActionPart({ type: "openEvidence", eventId: trace?.id ?? "trace" }), agentToolResultPart("open-evidence-call", "openEvidence", "success", { eventId: trace?.id ?? "trace" }), ], }; yield agentTextMessage( "summary", "assistant", `The prepared recipe completed with run evidence for ${runId}.`, ); } ``` - [ ] **Step 5: Run tests** Run: ```bash pnpm --dir web --filter @lda/console test -- preparedRecipeDriver ``` Expected: PASS. - [ ] **Step 6: Commit** ```bash git add web/apps/console/src/demo/agent/recipes.ts web/apps/console/src/demo/agent/preparedRecipeDriver.ts web/apps/console/src/demo/agent/preparedRecipeDriver.test.ts git commit -m "feat: add prepared demo recipe driver" ``` --- ### Task 3: Agent Hook And Presentation Actions **Files:** - Create: `web/apps/console/src/demo/agent/useDemoAgent.ts` - Test: `web/apps/console/src/demo/agent/useDemoAgent.test.tsx` **Interfaces:** - Consumes: - `runPreparedRecipeReplay()` from Task 2. - `PresentationToolAction` from Task 1. - Produces: - `useDemoAgent()` - `DemoAgentController` - [ ] **Step 1: Write hook tests** Create `web/apps/console/src/demo/agent/useDemoAgent.test.tsx`: ```tsx import { renderHook, act, waitFor } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { useDemoAgent } from "./useDemoAgent.js"; describe("useDemoAgent", () => { it("runs the prepared replay recipe and collects messages", async () => { const { result } = renderHook(() => useDemoAgent()); await act(async () => { await result.current.startPreparedReplay(); }); await waitFor(() => { expect(result.current.messages.length).toBeGreaterThan(3); }); expect(result.current.phase).toBe("completed"); expect(result.current.messages[0]?.role).toBe("user"); }); it("records presentation actions from the recipe", async () => { const { result } = renderHook(() => useDemoAgent()); await act(async () => { await result.current.startPreparedReplay(); }); expect(result.current.pendingActions).toContainEqual({ type: "selectWorkflowNode", nodeId: "review_issues" }); }); it("reset clears messages and actions", async () => { const { result } = renderHook(() => useDemoAgent()); await act(async () => { await result.current.startPreparedReplay(); }); act(() => result.current.reset()); expect(result.current.messages).toEqual([]); expect(result.current.pendingActions).toEqual([]); expect(result.current.phase).toBe("idle"); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash pnpm --dir web --filter @lda/console test -- useDemoAgent ``` Expected: FAIL because `useDemoAgent.ts` does not exist. - [ ] **Step 3: Implement hook** Create `web/apps/console/src/demo/agent/useDemoAgent.ts`: ```ts import { useCallback, useState } from "react"; import { runPreparedRecipeReplay } from "./preparedRecipeDriver.js"; import type { AgentMessage, PresentationToolAction } from "./events.js"; export type DemoAgentPhase = "idle" | "running" | "completed" | "failed"; export type DemoAgentController = { readonly phase: DemoAgentPhase; readonly messages: ReadonlyArray; readonly pendingActions: ReadonlyArray; readonly startPreparedReplay: () => Promise; readonly clearPendingActions: () => void; readonly reset: () => void; }; const collectActions = (message: AgentMessage): ReadonlyArray => message.parts.flatMap((part) => part.type === "presentation-action" ? [part.action] : []); export const useDemoAgent = (): DemoAgentController => { const [phase, setPhase] = useState("idle"); const [messages, setMessages] = useState>([]); const [pendingActions, setPendingActions] = useState>([]); const reset = useCallback(() => { setPhase("idle"); setMessages([]); setPendingActions([]); }, []); const clearPendingActions = useCallback(() => { setPendingActions([]); }, []); const startPreparedReplay = useCallback(async () => { setPhase("running"); setMessages([]); setPendingActions([]); try { for await (const message of runPreparedRecipeReplay()) { setMessages((current) => [...current, message]); const actions = collectActions(message); if (actions.length > 0) { setPendingActions((current) => [...current, ...actions]); } } setPhase("completed"); } catch (error) { const message = error instanceof Error ? error.message : String(error); setMessages((current) => [ ...current, { id: "agent-failure", role: "assistant", parts: [{ type: "error", message }] }, ]); setPhase("failed"); } }, []); return { phase, messages, pendingActions, startPreparedReplay, clearPendingActions, reset, }; }; ``` - [ ] **Step 4: Run tests** Run: ```bash pnpm --dir web --filter @lda/console test -- useDemoAgent ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add web/apps/console/src/demo/agent/useDemoAgent.ts web/apps/console/src/demo/agent/useDemoAgent.test.tsx git commit -m "feat: add demo agent hook" ``` --- ### Task 4: Render Standard Chat Parts **Files:** - Modify: `web/apps/console/src/presentation/OperatorChat.tsx` - Test: `web/apps/console/src/presentation/OperatorChat.test.tsx` - Modify: `web/apps/console/src/presentation/presentation.css` **Interfaces:** - Consumes: - `AgentMessage` from Task 1. - Produces: - `OperatorChat` accepts `messages?: ReadonlyArray` while preserving a safe fallback. - [ ] **Step 1: Write chat rendering tests** Create or update `web/apps/console/src/presentation/OperatorChat.test.tsx`: ```tsx import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { OperatorChat } from "./OperatorChat.js"; import type { PresentationState } from "./presentation-state.js"; import type { AgentMessage } from "../demo/agent/events.js"; const state: PresentationState = { beat: "intro", selectedNodeId: null, chatMode: "full", evidenceMode: "hidden", playbackMode: "replay", }; describe("OperatorChat", () => { it("renders standard agent message parts", () => { const messages: ReadonlyArray = [ { id: "u1", role: "user", parts: [{ type: "text", text: "Prepare the report." }] }, { id: "a1", role: "assistant", parts: [ { type: "text", text: "I will use the prepared recipe." }, { type: "tool-call", call: { id: "call-1", name: "selectWorkflowNode", input: { nodeId: "review_issues" } }, }, { type: "tool-result", result: { callId: "call-1", name: "selectWorkflowNode", status: "success", output: { nodeId: "review_issues" } }, }, ], }, ]; render(); expect(screen.getByText("Prepare the report.")).toBeInTheDocument(); expect(screen.getByText("I will use the prepared recipe.")).toBeInTheDocument(); expect(screen.getByText(/tool call/i)).toBeInTheDocument(); expect(screen.getByText(/selectWorkflowNode/i)).toBeInTheDocument(); expect(screen.getByText(/tool result/i)).toBeInTheDocument(); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash pnpm --dir web --filter @lda/console test -- OperatorChat ``` Expected: FAIL because `OperatorChat` does not accept `messages`. - [ ] **Step 3: Implement chat renderer** Update `web/apps/console/src/presentation/OperatorChat.tsx`: ```tsx import type { AgentMessage, AgentMessagePart } from "../demo/agent/events.js"; import type { PresentationState } from "./presentation-state.js"; type OperatorChatProps = { readonly state: PresentationState; readonly messages?: ReadonlyArray; }; const fallbackMessages = (state: PresentationState): ReadonlyArray => [ { id: "fallback-user", role: "user", parts: [{ type: "text", text: "Prepare the thesis readiness report." }] }, { id: "fallback-system", role: "assistant", parts: [ { type: "text", text: "Found prepared workflow recipe: lda_report_case_study." }, { type: "text", text: state.playbackMode === "replay" ? "Replay mode is active. Live execution is available when connected." : "Live execution is active. Operations are being sent to the connected workflow server.", }, ], }, ]; const renderPart = (part: AgentMessagePart, index: number) => { switch (part.type) { case "text": return

{part.text}

; case "tool-call": return (
Tool call {part.call.name}
); case "tool-result": return (
Tool result {part.result.name} {part.result.status}
); case "presentation-action": return (
Presentation action {part.action.type}
); case "error": return

{part.message}

; } }; export const OperatorChat = ({ state, messages }: OperatorChatProps) => { const visibleMessages = messages && messages.length > 0 ? messages : fallbackMessages(state); return ( ); }; ``` Append minimal styles to `web/apps/console/src/presentation/presentation.css`: ```css .chat-tool-part { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.35rem; padding: 0.35rem 0.45rem; border: 1px solid color-mix(in oklch, var(--presentation-line), transparent 25%); border-radius: 0.45rem; font-size: 0.78rem; } .chat-tool-part span { color: var(--presentation-muted); text-transform: uppercase; letter-spacing: 0.08em; } .chat-tool-part--presentation { border-color: color-mix(in oklch, var(--presentation-accent), transparent 25%); } .chat-error { color: var(--presentation-red); } ``` - [ ] **Step 4: Run tests** Run: ```bash pnpm --dir web --filter @lda/console test -- OperatorChat ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add web/apps/console/src/presentation/OperatorChat.tsx web/apps/console/src/presentation/OperatorChat.test.tsx web/apps/console/src/presentation/presentation.css git commit -m "feat: render demo agent chat parts" ``` --- ### Task 5: Wire Agent Actions Into Presentation Route **Files:** - Modify: `web/apps/console/src/presentation/PresentationRoute.tsx` - Modify: `web/apps/console/src/presentation/PresentationStage.tsx` - Test: `web/apps/console/src/presentation/PresentationRoute.test.tsx` **Interfaces:** - Consumes: - `useDemoAgent()` from Task 3. - `PresentationToolAction` from Task 1. - Produces: - `/present` can start the prepared replay agent. - `selectWorkflowNode` action focuses `review_issues`. - Chat receives `agent.messages`. - [ ] **Step 1: Write route integration test** Update `web/apps/console/src/presentation/PresentationRoute.test.tsx` with this test: ```tsx it("runs the prepared agent and applies the interrupt node action", async () => { render(); await userEvent.click(screen.getByRole("button", { name: /run prepared agent/i })); expect(await screen.findByText(/prepared workflow recipe/i)).toBeInTheDocument(); expect(await screen.findByText(/selectWorkflowNode/i)).toBeInTheDocument(); expect(await screen.findByRole("dialog", { name: /issue review/i })).toBeInTheDocument(); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash pnpm --dir web --filter @lda/console test -- PresentationRoute ``` Expected: FAIL because no prepared-agent button/action wiring exists. - [ ] **Step 3: Wire the hook and action application** In `PresentationRoute.tsx`: - import `useDemoAgent`; - create `const agent = useDemoAgent();`; - add a small button near existing presentation controls with label `Run prepared agent`; - pass `messages={agent.messages}` into `OperatorChat`; - add an effect that consumes `agent.pendingActions`; - for `selectWorkflowNode`, dispatch or set local selected node state so `NodeSpotlight` opens for the node; - call `agent.clearPendingActions()` after applying actions. Use this action helper shape inside the component: ```ts useEffect(() => { for (const action of agent.pendingActions) { if (action.type === "selectWorkflowNode") { dispatch({ type: "select_node", nodeId: action.nodeId }); } if (action.type === "setBeat") { dispatch({ type: "jump_hash", hash: `#${action.beatId}` }); } } if (agent.pendingActions.length > 0) { agent.clearPendingActions(); } }, [agent.pendingActions, agent.clearPendingActions]); ``` If `presentationReducer` does not currently expose `select_node`, add that action with this behavior: ```ts case "select_node": return { ...state, selectedNodeId: action.nodeId }; ``` - [ ] **Step 4: Run route tests** Run: ```bash pnpm --dir web --filter @lda/console test -- PresentationRoute presentation-state ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add web/apps/console/src/presentation/PresentationRoute.tsx web/apps/console/src/presentation/PresentationRoute.test.tsx web/apps/console/src/presentation/presentation-state.ts web/apps/console/src/presentation/presentation-state.test.ts git commit -m "feat: wire prepared agent into presentation" ``` --- ### Task 6: Documentation And Full Validation **Files:** - Modify: `web/README.md` - Modify: `docs/current_roadmap.md` - Move after implementation: `docs/superpowers/plans/2026-07-03-constrained-demo-agent.md` -> `docs/historical/superpowers/plans/2026-07-03-constrained-demo-agent.md` **Interfaces:** - Consumes all prior tasks. - Produces documented usage and archived plan. - [ ] **Step 1: Update `web/README.md`** Add a short section under presentation/demo docs: ```md ### Constrained Demo Agent `/present` includes a prepared agent recipe for the thesis readiness report. The recipe is deterministic: it emits standard chat message parts, workflow tool calls, and presentation tool actions without requiring a model provider key. The current prepared recipe can: - identify the `lda_report_case_study.default` workflow deployment; - show tool calls for run start, resume, and trace read; - focus the `review_issues` interrupt node through a presentation tool action; - open evidence linked to the run trace. This is intentionally not a general autonomous planner. A future server-side Vercel AI SDK driver can feed the same message-part interface. ``` - [ ] **Step 2: Update `docs/current_roadmap.md`** Change item 8 from planned to completed and include links to this plan and the design spec. - [ ] **Step 3: Move the plan to historical** Run: ```bash git mv docs/superpowers/plans/2026-07-03-constrained-demo-agent.md docs/historical/superpowers/plans/2026-07-03-constrained-demo-agent.md ``` - [ ] **Step 4: Run focused and full web validation** Run: ```bash pnpm --dir web --filter @lda/console test -- agent OperatorChat PresentationRoute pnpm --dir web test pnpm --dir web typecheck pnpm --dir web build git diff --check ``` Expected: - focused tests pass; - full web tests pass; - typecheck passes; - build passes, allowing the existing Vite chunk-size warning; - `git diff --check` has no whitespace errors. - [ ] **Step 5: Commit** ```bash git add web/README.md docs/current_roadmap.md docs/historical/superpowers/plans/2026-07-03-constrained-demo-agent.md git commit -m "docs: document constrained demo agent" ``` --- ## Self-Review Checklist - Spec coverage: the plan covers standard chat parts, workflow tools, presentation tools, replay, live seam, AI SDK future seam, Effect boundary, tests, and docs. - Dependency control: the plan does not add Vercel AI SDK, model providers, or Effect dependencies to the console. - Type consistency: `AgentMessage`, `AgentMessagePart`, `PresentationToolAction`, `AgentToolName`, and `DemoAgentController` names are introduced before use. - Scope control: styling is minimal; no generic chat composer, provider UI, or persistent agent sessions are included.