diff --git a/docs/superpowers/plans/2026-05-05-regenerate-semantics.md b/docs/superpowers/plans/2026-05-05-regenerate-semantics.md new file mode 100644 index 000000000..00a695b80 --- /dev/null +++ b/docs/superpowers/plans/2026-05-05-regenerate-semantics.md @@ -0,0 +1,379 @@ +# `@ngaf 0.0.26` — Regenerate Semantics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. + +**Goal:** Implement replace-semantics for "Regenerate response" button. Discards target assistant message + everything after it, re-runs agent on the prior user prompt. Bump @cacheplane/partial-markdown peer to ^0.3.0 alongside. + +**Architecture:** Add `regenerate(index)` method to `Agent` interface. LangGraph adapter uses checkpoint roll-back via `update_state`; ag-ui adapter uses STATE_DELTA truncation. Chat composition wires ``'s regenerate event to call `agent.regenerate(i)` instead of resending the prompt. + +**Spec:** `docs/superpowers/specs/2026-05-05-regenerate-semantics-design.md` + +**Working repo:** `/Users/blove/repos/angular-agent-framework/.claude/worktrees/dazzling-dewdney-887eac` +**Implementation branch:** `claude/regenerate-semantics-0.0.26` (already created from `origin/main`) + +--- + +## Phase 1 — Agent interface + types + +### Task 1: Add `regenerate` to `Agent` interface + +**Files:** `libs/chat/src/lib/agent/agent.ts` (or wherever `Agent` interface lives — find via `grep -rn "export interface Agent" libs/chat/src`). + +- [ ] Step 1: Locate the file: + +```bash +grep -rn "^export interface Agent\b" libs/chat/src/lib/agent/ +``` + +- [ ] Step 2: Add the `regenerate` method: + +```ts +/** + * Discards the assistant message at the given index AND all messages after + * it, then re-runs the agent against the trimmed conversation tail. The + * preceding user message (at index - 1) is preserved and re-submitted as + * the agent's input. No new user message is added to the history. + * + * Throws if the message at `index` is not 'assistant' role, or if the + * agent is currently loading another response. + */ +regenerate(assistantMessageIndex: number): Promise; +``` + +- [ ] Step 3: Run typecheck — expect errors at every `Agent` implementation site (langgraph adapter, ag-ui adapter, mockAgent, etc.). That's expected. Tasks 2-4 fix them. + +```bash +npx tsc --noEmit | grep "regenerate" +``` + +- [ ] Step 4: Commit: + +```bash +git add libs/chat/src/lib/agent/agent.ts +git commit -m "feat(chat): add regenerate(index) method to Agent interface" +``` + +--- + +## Phase 2 — LangGraph adapter implementation + +### Task 2: Implement `regenerate` in `@ngaf/langgraph` + +**Files:** +- Modify: `libs/langgraph/src/lib/agent.fn.ts` (the agent factory) +- Test: `libs/langgraph/src/lib/agent.fn.spec.ts` + +- [ ] Step 1: Find where `submit` is implemented in `agent.fn.ts`. Add `regenerate` next to it: + +```ts +async function regenerate(this: LangGraphAgent, assistantMessageIndex: number): Promise { + if (this.isLoading()) { + throw new Error('Cannot regenerate while agent is loading another response'); + } + const messages = this.messages(); + const target = messages[assistantMessageIndex]; + if (!target || target.role !== 'assistant') { + throw new Error(`Message at index ${assistantMessageIndex} is not an assistant message`); + } + + // Truncate local message buffer to [0..index-1]. + const trimmed = messages.slice(0, assistantMessageIndex); + this.messages.set(trimmed); + + // Find the user message that prompts this assistant response. + // The agent's submit will use the trimmed message history as input. + const lastUserMsg = trimmed.reverse().find(m => m.role === 'user'); + if (!lastUserMsg) { + throw new Error('No user message found before the target assistant message'); + } + + // Re-submit by replaying the user message. The LangGraph thread state + // will see the trimmed messages array via the messages[] argument. + await this.submit({ + message: typeof lastUserMsg.content === 'string' + ? lastUserMsg.content + : '', // structured content; re-submit handles via state.messages + state: { messages: trimmed }, + }); +} +``` + +(Note: this is a "local truncation + replay" implementation. Does NOT use LangGraph checkpoint roll-back — that's a future optimization. Real semantic replace works because the new submit uses the trimmed message history.) + +- [ ] Step 2: Wire `regenerate` into the agent factory return value: + +```ts +const agent: LangGraphAgent = { + // ...existing methods... + regenerate: regenerate.bind(/* ... */), +}; +``` + +- [ ] Step 3: Add tests: + +```ts +describe('agent.regenerate()', () => { + it('truncates messages [N..end] and re-submits from N-1', async () => { + const a = mockLangGraphAgent({ + messages: [ + { id: '1', role: 'user', content: 'hello' }, + { id: '2', role: 'assistant', content: 'hi there' }, + ], + }); + await a.regenerate(1); + expect(a.messages().length).toBeLessThanOrEqual(2); + expect(a.messages()[0].role).toBe('user'); + }); + + it('throws when target index is not an assistant message', async () => { + const a = mockLangGraphAgent({ + messages: [{ id: '1', role: 'user', content: 'hello' }], + }); + await expect(a.regenerate(0)).rejects.toThrow(/not an assistant/); + }); + + it('throws when agent is loading', async () => { + const a = mockLangGraphAgent({ isLoading: true, messages: [ + { id: '1', role: 'user', content: 'hi' }, + { id: '2', role: 'assistant', content: 'hello' }, + ] }); + await expect(a.regenerate(1)).rejects.toThrow(/loading/); + }); +}); +``` + +- [ ] Step 4: Run tests + typecheck: + +```bash +npx nx run langgraph:test +``` + +- [ ] Step 5: Commit: + +```bash +git add libs/langgraph/src/ +git commit -m "feat(langgraph): implement Agent.regenerate via local truncation + replay" +``` + +--- + +## Phase 3 — ag-ui adapter implementation + +### Task 3: Implement `regenerate` in `@ngaf/ag-ui` + +**Files:** +- Modify: `libs/ag-ui/src/lib/to-agent.ts` (or wherever the ag-ui agent factory is) +- Test: corresponding `.spec.ts` + +- [ ] Step 1: Add `regenerate` analogous to LangGraph: + +```ts +async function regenerate(assistantMessageIndex: number): Promise { + if (agent.isLoading()) { + throw new Error('Cannot regenerate while agent is loading another response'); + } + const messages = agent.messages(); + const target = messages[assistantMessageIndex]; + if (!target || target.role !== 'assistant') { + throw new Error(`Message at index ${assistantMessageIndex} is not an assistant message`); + } + const trimmed = messages.slice(0, assistantMessageIndex); + agent.messages.set(trimmed); + + // Dispatch a new run with the trimmed message history. The ag-ui RunInput + // includes a `messages` field that overrides the prior thread state. + const lastUserMsg = trimmed.reverse().find(m => m.role === 'user'); + if (!lastUserMsg) throw new Error('No user message before target assistant'); + + await agent.submit({ messages: trimmed }); +} +``` + +- [ ] Step 2: Add tests + commit: + +```bash +npx nx run ag-ui:test +git add libs/ag-ui/src/ +git commit -m "feat(ag-ui): implement Agent.regenerate via local truncation + replay" +``` + +--- + +## Phase 4 — Chat composition wiring + +### Task 4: Update `chat.component.ts` to call `regenerate(i)` + +**Files:** `libs/chat/src/lib/compositions/chat/chat.component.ts` + +- [ ] Step 1: Find the `` element in the assistant-message branch. The `(regenerate)` event currently fires `onRegenerate(message)` (or similar). Update it to: + +```html + +``` + +The `i` index is already in scope from the existing `*ngFor` / @for. + +- [ ] Step 2: Update the component method: + +```ts +onRegenerate(messageIndex: number): void { + void this.agent().regenerate(messageIndex); +} +``` + +Remove any prior implementation that called `submitMessage` / `agent.submit` directly. + +- [ ] Step 3: Run chat tests + lint: + +```bash +npx nx run chat:test +npx nx run chat:lint +``` + +- [ ] Step 4: Commit: + +```bash +git add libs/chat/src/lib/compositions/chat/chat.component.ts +git commit -m "feat(chat): wire regenerate button to Agent.regenerate(index)" +``` + +--- + +## Phase 5 — Disable button while loading + +### Task 5: Block regenerate during streaming + +**Files:** `libs/chat/src/lib/primitives/chat-message-actions/chat-message-actions.component.ts` + +- [ ] Step 1: Add a `disabled` input or `isLoading` input: + +```ts +readonly disabled = input(false); +``` + +In the regenerate button template: + +```html +