From 37b80842d5f6e98cf170a3e57db34406322abdc5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 20:30:53 -0700 Subject: [PATCH 01/12] docs: phase-2 design for finishing chat-runtime decoupling Defines the ChatAgentWithHistory sub-contract, ChatCheckpoint neutral shape, file-movement plan (three compositions back to @cacheplane/chat), and public-API delta. Replay/fork stay as composition outputs. Co-Authored-By: Claude Opus 4.7 --- ...26-04-21-chat-decoupling-phase-2-design.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-21-chat-decoupling-phase-2-design.md diff --git a/docs/superpowers/specs/2026-04-21-chat-decoupling-phase-2-design.md b/docs/superpowers/specs/2026-04-21-chat-decoupling-phase-2-design.md new file mode 100644 index 000000000..ada8d2bd8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-21-chat-decoupling-phase-2-design.md @@ -0,0 +1,106 @@ +# Chat Decoupling Phase-2 Design + +## Goal + +Complete the chat-runtime decoupling started in Phase-1 by removing the last `AgentRef` dependencies from the LangGraph-hosted compositions (`ChatDebugComponent`, `ChatTimelineComponent`, `ChatTimelineSliderComponent`). They become runtime-neutral consumers of a new `ChatAgentWithHistory` sub-contract and move back to `@cacheplane/chat`. + +## Motivation + +Phase-1 broke the circular `@cacheplane/chat ↔ @cacheplane/langgraph` build-graph edge by migrating all primitives and most compositions to the runtime-neutral `ChatAgent` contract. Three compositions were relocated to `@cacheplane/langgraph` because they still read `AgentRef.history()` / `ThreadState` and the Phase-1 scope didn't cover extending the `ChatAgent` contract. + +Phase-2 finishes the story: a non-LangGraph adapter (AG-UI, custom runtime, etc.) that exposes a history-like surface can power the same debug/timeline UI. The compositions live where they belong — in the chat library. + +## Architecture + +Add a sub-contract in `@cacheplane/chat`: + +```ts +export interface ChatCheckpoint { + id?: string; // adapter-opaque ID + label?: string; // human label (e.g., next node name) + values: Record; // state snapshot +} + +export interface ChatAgentWithHistory extends ChatAgent { + history: Signal; +} +``` + +`toChatAgent(ref)` in `@cacheplane/langgraph` widens its return type to `ChatAgentWithHistory` and translates `ThreadState[]` → `ChatCheckpoint[]` (`next[0]`→`label`, `checkpoint.checkpoint_id`→`id`, `values` passthrough). + +The three compositions stop depending on `AgentRef` / `ThreadState` and read only the neutral sub-contract. + +## Sub-Contract Design + +**Read-only.** Replay/fork stay as composition outputs emitting the opaque `ChatCheckpoint.id`. The parent app calls whatever runtime-specific API it wants. This preserves the current division of labor and avoids making the contract opinionated about time-travel semantics. + +**Extends, not embeds.** `ChatAgentWithHistory extends ChatAgent`. A plain `ChatAgent` never gains a `history` field — compositions that need history take the richer type explicitly. This keeps the capability visible in the type system and prevents adapters that can't supply history from silently returning empty arrays. + +**Minimal neutral shape.** `ChatCheckpoint` has only the fields actually read by the compositions today. No `extra` passthrough — runtime-neutrality is strict. If future compositions need adapter-specific data, we'll grow the neutral shape or add sibling sub-contracts rather than leaking opaque state. + +## File Movement + +**Move from `libs/langgraph/src/lib/` → `libs/chat/src/lib/`:** + +| Current | New | +|---|---| +| `primitives/chat-timeline/chat-timeline.component.ts` | `primitives/chat-timeline/chat-timeline.component.ts` | +| `primitives/chat-timeline/chat-timeline.component.spec.ts` | `primitives/chat-timeline/chat-timeline.component.spec.ts` | +| `compositions/chat-timeline-slider/chat-timeline-slider.component.ts` | `compositions/chat-timeline-slider/chat-timeline-slider.component.ts` | +| `compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts` | `compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts` | +| `compositions/chat-debug/chat-debug.component.ts` | `compositions/chat-debug/chat-debug.component.ts` | +| `compositions/chat-debug/chat-debug.component.spec.ts` | `compositions/chat-debug/chat-debug.component.spec.ts` | +| `compositions/chat-debug/debug-timeline.component.ts` | (same) | +| `compositions/chat-debug/debug-detail.component.ts` | (same) | +| `compositions/chat-debug/debug-controls.component.ts` | (same) | +| `compositions/chat-debug/debug-summary.component.ts` | (same) | +| `compositions/chat-debug/debug-checkpoint-card.component.ts` | (same) | +| `compositions/chat-debug/debug-state-inspector.component.ts` | (same) | +| `compositions/chat-debug/debug-state-diff.component.ts` | (same) | +| `compositions/chat-debug/debug-utils.ts` | (same, rewired to `ChatCheckpoint`) | +| `compositions/chat-debug/state-diff.ts` | (same) | + +`debug-utils.ts` simplifies: `toDebugCheckpoint(cp: ChatCheckpoint, i)` reads `cp.label ?? \`Step ${i+1}\`` and `cp.id` directly. `extractStateValues` takes `ChatCheckpoint | undefined` and returns `cp?.values ?? {}`. + +## Public API + +**`@cacheplane/chat/public-api.ts`:** +- Add: `ChatAgentWithHistory`, `ChatCheckpoint` (types); `ChatTimelineComponent`, `ChatTimelineSliderComponent`, `ChatDebugComponent` (components). + +**`@cacheplane/langgraph/public-api.ts`:** +- Remove: `ChatTimelineComponent`, `ChatTimelineSliderComponent`, `ChatDebugComponent`, and the debug sub-component exports added in Phase-1. +- Keep: `toChatAgent` (now typed to return `ChatAgentWithHistory`). + +## Cockpit Consumers + +**Affected apps** (per Phase-1 grep): `cockpit/chat/debug/angular`, `cockpit/chat/timeline/angular`. + +Today they pass `[ref]="stream"` to the debug/timeline compositions. After Phase-2 they pass `[agent]="chatAgent"` where `chatAgent: ChatAgentWithHistory = toChatAgent(stream)` — already assigned in Phase-1 code for the chat primitives. Net change: template-only. + +The `replayRequested` / `forkRequested` output handlers in those apps remain runtime-specific (they call LangGraph's checkpoint APIs on `stream`). No API surface change there. + +## Testing & Mocks + +- `mockChatAgent()` in `@cacheplane/chat/testing` grows an optional `history?: ChatCheckpoint[]`. When supplied, the return type widens to `ChatAgentWithHistory`. +- New `runChatAgentWithHistoryConformance(label, factory)` helper layers on top of the existing `runChatAgentConformance` — asserts `ChatAgent` contract plus `history()` emission and shape. +- `toChatAgent` gains a unit test covering the `ThreadState` → `ChatCheckpoint` translation (next→label, checkpoint_id→id, values passthrough, empty/partial states). +- Composition specs that currently use `createMockAgentRef` switch to `mockChatAgent({ history: [...] })`. + +## Dep Graph + +No change: `langgraph → chat` remains one-way. `nx graph` verification is part of the final build-check step. + +## Out of Scope + +- Replay/fork methods on the sub-contract (rejected — parent-handled outputs are sufficient). +- `extra` passthrough on `ChatCheckpoint` (rejected — runtime-neutrality is strict). +- Moving `toChatAgent` itself (stays in `@cacheplane/langgraph`). +- Cockpit app refactors beyond the template-one-liner switch. + +## When to Revisit + +Triggers that would reopen these decisions: + +- A second adapter (AG-UI, custom) needs to power `chat-debug` or `chat-timeline` — likely fine under current design; validate on first attempt. +- A composition appears that genuinely needs adapter-specific fields — reopen the `extra` passthrough decision before adding it. +- Parent apps start duplicating replay/fork handler code across runtimes — reopen whether replay/fork belongs on the contract. From 3d15188398f67efc2241b28652306a89a78e54a2 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:10:14 -0700 Subject: [PATCH 02/12] docs: phase-2 implementation plan 9 tasks: add sub-contract types, extend mock/conformance, widen toChatAgent, move three compositions back to @cacheplane/chat, rebind cockpit consumers, verify dep graph. Co-Authored-By: Claude Opus 4.7 --- .../2026-04-21-chat-decoupling-phase-2.md | 1017 +++++++++++++++++ 1 file changed, 1017 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-21-chat-decoupling-phase-2.md diff --git a/docs/superpowers/plans/2026-04-21-chat-decoupling-phase-2.md b/docs/superpowers/plans/2026-04-21-chat-decoupling-phase-2.md new file mode 100644 index 000000000..933a60db3 --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-chat-decoupling-phase-2.md @@ -0,0 +1,1017 @@ +# Chat Decoupling Phase-2 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:** Finish the chat-runtime decoupling by moving `ChatTimelineComponent`, `ChatTimelineSliderComponent`, and `ChatDebugComponent` back to `@cacheplane/chat` and retargeting them from `AgentRef` to a new `ChatAgentWithHistory` sub-contract. + +**Architecture:** Add `ChatCheckpoint` + `ChatAgentWithHistory` to `@cacheplane/chat`. Widen `toChatAgent` return type and have it translate `ThreadState[]` → `ChatCheckpoint[]`. Move the three compositions back to chat, retargeting all `ref()` reads to the neutral sub-contract. + +**Tech Stack:** Angular 21 (signals, zoneless-compatible), RxJS, Nx, Vitest, ng-packagr. + +**Spec:** `docs/superpowers/specs/2026-04-21-chat-decoupling-phase-2-design.md` + +--- + +## File Structure + +### New in `@cacheplane/chat` + +- `libs/chat/src/lib/agent/chat-checkpoint.ts` — `ChatCheckpoint` interface +- `libs/chat/src/lib/agent/chat-agent-with-history.ts` — `ChatAgentWithHistory` interface +- `libs/chat/src/lib/testing/chat-agent-with-history-conformance.ts` — conformance helper + +### Moved into `@cacheplane/chat` (from `@cacheplane/langgraph`) + +- `libs/chat/src/lib/primitives/chat-timeline/` (2 files) +- `libs/chat/src/lib/compositions/chat-timeline-slider/` (2 files) +- `libs/chat/src/lib/compositions/chat-debug/` (15 files) + +### Modified in `@cacheplane/chat` + +- `libs/chat/src/lib/agent/index.ts` — export new types +- `libs/chat/src/public-api.ts` — export new types + moved components +- `libs/chat/src/lib/testing/mock-chat-agent.ts` — add optional `history` option + +### Modified in `@cacheplane/langgraph` + +- `libs/langgraph/src/lib/to-chat-agent.ts` — widen return to `ChatAgentWithHistory`, translate history +- `libs/langgraph/src/lib/to-chat-agent.spec.ts` — add translation test +- `libs/langgraph/src/public-api.ts` — drop moved-out component exports + +### Modified in cockpit + +- `cockpit/chat/debug/angular/src/app/debug.component.ts` — `[ref]` → `[agent]` +- `cockpit/chat/timeline/angular/src/app/timeline.component.ts` — `[ref]` → `[agent]` + +--- + +### Task 1: Add `ChatCheckpoint` + `ChatAgentWithHistory` types + +**Files:** +- Create: `libs/chat/src/lib/agent/chat-checkpoint.ts` +- Create: `libs/chat/src/lib/agent/chat-agent-with-history.ts` +- Modify: `libs/chat/src/lib/agent/index.ts` +- Modify: `libs/chat/src/public-api.ts` + +- [ ] **Step 1: Add `ChatCheckpoint`** + +```ts +// libs/chat/src/lib/agent/chat-checkpoint.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +/** + * Runtime-neutral snapshot of a point in an agent's execution history. + * + * Consumed by time-travel / debug UIs. `id` is adapter-opaque — UIs emit + * it back to the parent app on replay/fork, and the parent app dispatches + * to the underlying runtime. + */ +export interface ChatCheckpoint { + /** Adapter-opaque checkpoint identifier (e.g. LangGraph checkpoint_id). */ + id?: string; + /** Human-friendly label for the checkpoint (e.g. next node name). */ + label?: string; + /** State snapshot at this checkpoint. */ + values: Record; +} +``` + +- [ ] **Step 2: Add `ChatAgentWithHistory`** + +```ts +// libs/chat/src/lib/agent/chat-agent-with-history.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import type { Signal } from '@angular/core'; +import type { ChatAgent } from './chat-agent'; +import type { ChatCheckpoint } from './chat-checkpoint'; + +/** + * Extends ChatAgent with a required `history` signal. + * + * Compositions that need time-travel / checkpoint data (chat-timeline, + * chat-debug) take this richer contract. Adapters that cannot supply + * history should return plain ChatAgent instead of stubbing an empty array. + */ +export interface ChatAgentWithHistory extends ChatAgent { + history: Signal; +} +``` + +- [ ] **Step 3: Re-export from `agent/index.ts`** + +```ts +// libs/chat/src/lib/agent/index.ts — add: +export type { ChatCheckpoint } from './chat-checkpoint'; +export type { ChatAgentWithHistory } from './chat-agent-with-history'; +``` + +- [ ] **Step 4: Re-export from `public-api.ts`** + +Add `ChatCheckpoint, ChatAgentWithHistory` to the existing `export type { ... } from './lib/agent';` block. + +- [ ] **Step 5: Run chat lint + test + build** + +```bash +npx nx run-many -t lint,test,build -p chat +``` +Expected: all pass. + +- [ ] **Step 6: Commit** + +```bash +git add libs/chat/src/lib/agent/chat-checkpoint.ts \ + libs/chat/src/lib/agent/chat-agent-with-history.ts \ + libs/chat/src/lib/agent/index.ts \ + libs/chat/src/public-api.ts +git commit -m "feat(chat): add ChatAgentWithHistory sub-contract and ChatCheckpoint" +``` + +--- + +### Task 2: Extend `mockChatAgent` with optional `history` + +**Files:** +- Modify: `libs/chat/src/lib/testing/mock-chat-agent.ts` +- Create: `libs/chat/src/lib/testing/mock-chat-agent.spec.ts` (if not present; else extend) + +- [ ] **Step 1: Write failing test** + +```ts +// libs/chat/src/lib/testing/mock-chat-agent.spec.ts — add: +import { describe, it, expect } from 'vitest'; +import { mockChatAgent } from './mock-chat-agent'; +import type { ChatAgentWithHistory } from '../agent'; + +describe('mockChatAgent with history', () => { + it('exposes history signal when history option supplied', () => { + const agent = mockChatAgent({ history: [{ id: 'c1', label: 'start', values: {} }] }); + const withHistory = agent as ChatAgentWithHistory; + expect(typeof withHistory.history).toBe('function'); + expect(withHistory.history()).toEqual([{ id: 'c1', label: 'start', values: {} }]); + }); + + it('omits history when option absent', () => { + const agent = mockChatAgent({}); + expect((agent as Partial).history).toBeUndefined(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +npx nx test chat --testNamePattern="mockChatAgent with history" +``` +Expected: FAIL (history option not yet accepted). + +- [ ] **Step 3: Extend `MockChatAgentOptions` and `MockChatAgent`** + +In `libs/chat/src/lib/testing/mock-chat-agent.ts`: + +Imports — add `ChatCheckpoint`: +```ts +import type { + ChatAgent, ChatMessage, ChatStatus, ChatToolCall, + ChatInterrupt, ChatSubagent, ChatSubmitInput, ChatSubmitOptions, + ChatCheckpoint, +} from '../agent'; +``` + +Add to `MockChatAgent` interface: +```ts + history?: WritableSignal; +``` + +Add to `MockChatAgentOptions`: +```ts + history?: ChatCheckpoint[]; +``` + +In the `mockChatAgent` function body, after the `subagents` block: +```ts + const history = opts.history + ? signal(opts.history) + : undefined; +``` + +In the returned object spread: +```ts + ...(history ? { history } : {}), +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +npx nx test chat --testNamePattern="mockChatAgent with history" +``` +Expected: PASS. + +- [ ] **Step 5: Run full chat test suite** + +```bash +npx nx test chat +``` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add libs/chat/src/lib/testing/mock-chat-agent.ts \ + libs/chat/src/lib/testing/mock-chat-agent.spec.ts +git commit -m "feat(chat): extend mockChatAgent with optional history" +``` + +--- + +### Task 3: Add `runChatAgentWithHistoryConformance` helper + +**Files:** +- Create: `libs/chat/src/lib/testing/chat-agent-with-history-conformance.ts` +- Modify: `libs/chat/src/public-api.ts` + +- [ ] **Step 1: Create conformance helper** + +```ts +// libs/chat/src/lib/testing/chat-agent-with-history-conformance.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { describe, it, expect } from 'vitest'; +import type { ChatAgentWithHistory, ChatCheckpoint } from '../agent'; +import { runChatAgentConformance } from './chat-agent-conformance'; + +/** + * Conformance suite for ChatAgentWithHistory implementations. + * + * Runs the base ChatAgent conformance suite, then verifies the history + * signal is present and returns an array of ChatCheckpoint-shaped entries. + */ +export function runChatAgentWithHistoryConformance( + label: string, + factory: (seed?: { history?: ChatCheckpoint[] }) => ChatAgentWithHistory, +): void { + runChatAgentConformance(label, () => factory()); + + describe(`${label} — history`, () => { + it('exposes a history signal', () => { + const agent = factory(); + expect(typeof agent.history).toBe('function'); + expect(Array.isArray(agent.history())).toBe(true); + }); + + it('reflects seeded checkpoints', () => { + const seed: ChatCheckpoint[] = [ + { id: 'c1', label: 'Step 1', values: { foo: 1 } }, + { id: 'c2', label: 'Step 2', values: { foo: 2 } }, + ]; + const agent = factory({ history: seed }); + const entries = agent.history(); + expect(entries).toHaveLength(2); + expect(entries[0].id).toBe('c1'); + expect(entries[1].values).toEqual({ foo: 2 }); + }); + }); +} +``` + +- [ ] **Step 2: Export from public-api** + +Add to `libs/chat/src/public-api.ts` (testing section): +```ts +export { runChatAgentWithHistoryConformance } from './lib/testing/chat-agent-with-history-conformance'; +``` + +- [ ] **Step 3: Run chat build to verify exports typecheck** + +```bash +npx nx build chat +``` +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add libs/chat/src/lib/testing/chat-agent-with-history-conformance.ts \ + libs/chat/src/public-api.ts +git commit -m "test(chat): add ChatAgentWithHistory conformance helper" +``` + +--- + +### Task 4: Widen `toChatAgent` return type + translate history + +**Files:** +- Modify: `libs/langgraph/src/lib/to-chat-agent.ts` +- Modify: `libs/langgraph/src/lib/to-chat-agent.spec.ts` + +- [ ] **Step 1: Write failing test** + +Add to `libs/langgraph/src/lib/to-chat-agent.spec.ts` inside the existing describe block: + +```ts + it('translates ThreadState history into ChatCheckpoint[]', () => { + TestBed.runInInjectionContext(() => { + const ref = stubAgentRef({ + history: signal([ + { values: { step: 1 }, next: ['nodeA'], checkpoint: { checkpoint_id: 'ck1' } }, + { values: { step: 2 }, next: [], checkpoint: { checkpoint_id: 'ck2' } }, + { values: { step: 3 }, next: ['nodeC'], checkpoint: undefined }, + ] as any), + }); + const chat = toChatAgent(ref); + expect(chat.history()).toEqual([ + { id: 'ck1', label: 'nodeA', values: { step: 1 } }, + { id: 'ck2', label: undefined, values: { step: 2 } }, + { id: undefined, label: 'nodeC', values: { step: 3 } }, + ]); + }); + }); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +npx nx test langgraph --testNamePattern="translates ThreadState history" +``` +Expected: FAIL (`chat.history is not a function` or equivalent). + +- [ ] **Step 3: Update `toChatAgent` implementation** + +In `libs/langgraph/src/lib/to-chat-agent.ts`: + +Update imports: +```ts +import type { + ChatAgentWithHistory, ChatCheckpoint, ChatCustomEvent, + ChatMessage, ChatRole, ChatStatus, ChatToolCall, ChatToolCallStatus, + ChatInterrupt, ChatSubagent, ChatSubmitInput, ChatSubmitOptions, +} from '@cacheplane/chat'; +``` + +Change signature: +```ts +export function toChatAgent(ref: AgentRef): ChatAgentWithHistory { +``` + +Inside the function, before `return {`, add: +```ts + const history = computed(() => + ref.history().map(toChatCheckpoint), + ); +``` + +Add `history` to the returned object: +```ts + return { + messages, + status, + isLoading: ref.isLoading, + error: ref.error, + toolCalls, + state, + interrupt, + subagents, + customEvents$, + history, + submit: /* unchanged */, + stop: /* unchanged */, + }; +``` + +At bottom of file, add: +```ts +function toChatCheckpoint(state: ThreadState): ChatCheckpoint { + return { + id: state.checkpoint?.checkpoint_id ?? undefined, + label: state.next?.[0] ?? undefined, + values: isRecord(state.values) ? state.values : {}, + }; +} + +function isRecord(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v); +} +``` + +Import `ThreadState`: +```ts +import type { AgentRef, CustomStreamEvent, SubagentStreamRef, ThreadState } from './agent.types'; +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +npx nx test langgraph --testNamePattern="translates ThreadState history" +``` +Expected: PASS. + +- [ ] **Step 5: Run full langgraph test + build** + +```bash +npx nx run-many -t lint,test,build -p langgraph +``` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add libs/langgraph/src/lib/to-chat-agent.ts \ + libs/langgraph/src/lib/to-chat-agent.spec.ts +git commit -m "feat(langgraph): widen toChatAgent to ChatAgentWithHistory, translate history" +``` + +--- + +### Task 5: Move `chat-timeline` primitive to `@cacheplane/chat` + +**Files:** +- Create: `libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.ts` +- Create: `libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts` +- Delete: `libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.ts` +- Delete: `libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts` +- Modify: `libs/chat/src/public-api.ts` — export `ChatTimelineComponent` +- Modify: `libs/langgraph/src/public-api.ts` — drop `ChatTimelineComponent` + +- [ ] **Step 1: Write the new (runtime-neutral) chat-timeline test** + +```ts +// libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { describe, it, expect } from 'vitest'; +import { TestBed } from '@angular/core/testing'; +import { Component } from '@angular/core'; +import { ChatTimelineComponent } from './chat-timeline.component'; +import { mockChatAgent } from '../../testing/mock-chat-agent'; + +@Component({ + standalone: true, + imports: [ChatTimelineComponent], + template: ` + + {{ i }}:{{ state.label }} + + `, +}) +class HostComponent { + agent = mockChatAgent({ + history: [{ id: 'a', label: 'nodeA', values: {} }, { id: 'b', label: 'nodeB', values: {} }], + }) as any; +} + +describe('ChatTimelineComponent', () => { + it('renders a template for each checkpoint', () => { + const fixture = TestBed.createComponent(HostComponent); + fixture.detectChanges(); + const text = fixture.nativeElement.textContent.replace(/\s+/g, ''); + expect(text).toBe('0:nodeA1:nodeB'); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +npx nx test chat --testNamePattern="ChatTimelineComponent" +``` +Expected: FAIL (component not in this lib yet). + +- [ ] **Step 3: Create the retargeted component** + +```ts +// libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, computed, contentChild, input, output, + TemplateRef, ChangeDetectionStrategy, +} from '@angular/core'; +import { NgTemplateOutlet } from '@angular/common'; +import type { ChatAgentWithHistory, ChatCheckpoint } from '../../agent'; + +@Component({ + selector: 'chat-timeline', + standalone: true, + imports: [NgTemplateOutlet], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @for (cp of history(); track $index) { + @if (templateRef()) { + + } + } + `, +}) +export class ChatTimelineComponent { + readonly agent = input.required(); + + readonly checkpointSelected = output(); + + readonly templateRef = contentChild(TemplateRef); + + readonly history = computed(() => this.agent().history()); + + selectCheckpoint(cp: ChatCheckpoint): void { + this.checkpointSelected.emit(cp); + } +} +``` + +- [ ] **Step 4: Delete old files in langgraph** + +```bash +rm libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.ts \ + libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts +rmdir libs/langgraph/src/lib/primitives/chat-timeline +rmdir libs/langgraph/src/lib/primitives # if now empty +``` + +- [ ] **Step 5: Update public APIs** + +In `libs/chat/src/public-api.ts`, add in the primitives section: +```ts +export { ChatTimelineComponent } from './lib/primitives/chat-timeline/chat-timeline.component'; +``` + +In `libs/langgraph/src/public-api.ts`, remove the `ChatTimelineComponent` export. + +- [ ] **Step 6: Run test to verify it passes** + +```bash +npx nx test chat --testNamePattern="ChatTimelineComponent" +``` +Expected: PASS. + +- [ ] **Step 7: Build both libs** + +```bash +npx nx run-many -t lint,test,build -p chat,langgraph +``` +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add libs/chat/src/lib/primitives/chat-timeline/ \ + libs/chat/src/public-api.ts \ + libs/langgraph/src/public-api.ts +git add -u libs/langgraph/src/lib/primitives # picks up deletions +git commit -m "refactor(chat,langgraph): move chat-timeline primitive to chat, retarget to ChatAgentWithHistory" +``` + +--- + +### Task 6: Move `chat-timeline-slider` composition to `@cacheplane/chat` + +**Files:** +- Create: `libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts` +- Create: `libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts` +- Delete: `libs/langgraph/src/lib/compositions/chat-timeline-slider/*` +- Modify: `libs/chat/src/public-api.ts`, `libs/langgraph/src/public-api.ts` + +- [ ] **Step 1: Write the new test** + +```ts +// libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { describe, it, expect } from 'vitest'; +import { TestBed } from '@angular/core/testing'; +import { Component, viewChild } from '@angular/core'; +import { ChatTimelineSliderComponent } from './chat-timeline-slider.component'; +import { mockChatAgent } from '../../testing/mock-chat-agent'; + +@Component({ + standalone: true, + imports: [ChatTimelineSliderComponent], + template: ``, +}) +class HostComponent { + agent = mockChatAgent({ + history: [ + { id: 'ck1', label: 'nodeA', values: {} }, + { id: 'ck2', label: 'nodeB', values: {} }, + ], + }) as any; + lastReplay: string | undefined; + slider = viewChild.required(ChatTimelineSliderComponent); +} + +describe('ChatTimelineSliderComponent', () => { + it('lists each checkpoint', () => { + const fixture = TestBed.createComponent(HostComponent); + fixture.detectChanges(); + const text = fixture.nativeElement.textContent; + expect(text).toContain('2 checkpoint'); + expect(text).toContain('ck1'); + expect(text).toContain('ck2'); + }); + + it('emits replayRequested with checkpoint id', () => { + const fixture = TestBed.createComponent(HostComponent); + fixture.detectChanges(); + fixture.componentInstance.slider().replay({ id: 'ck2', values: {} } as any); + expect(fixture.componentInstance.lastReplay).toBe('ck2'); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +npx nx test chat --testNamePattern="ChatTimelineSliderComponent" +``` +Expected: FAIL (component missing). + +- [ ] **Step 3: Create retargeted component** + +```ts +// libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, computed, input, output, signal, + ChangeDetectionStrategy, +} from '@angular/core'; +import type { ChatAgentWithHistory, ChatCheckpoint } from '../../agent'; + +@Component({ + selector: 'chat-timeline-slider', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+

Timeline

+ {{ history().length }} checkpoint(s) +
+ + @if (history().length === 0) { +

No checkpoints yet.

+ } + +
+ @for (cp of history(); track $index; let i = $index) { +
+ + {{ i + 1 }} + + +
+

+ {{ cp.label ?? 'Step ' + (i + 1) }} +

+ @if (cp.id) { +

{{ cp.id }}

+ } +
+ +
+ + +
+
+ } +
+
+ `, +}) +export class ChatTimelineSliderComponent { + readonly agent = input.required(); + + readonly selectedIndex = signal(-1); + + readonly history = computed(() => this.agent().history()); + + readonly replayRequested = output(); + readonly forkRequested = output(); + + replay(cp: ChatCheckpoint): void { + if (cp.id) this.replayRequested.emit(cp.id); + } + + fork(cp: ChatCheckpoint, index: number): void { + this.selectedIndex.set(index); + if (cp.id) this.forkRequested.emit(cp.id); + } +} +``` + +- [ ] **Step 4: Delete old files** + +```bash +rm libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts \ + libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts +rmdir libs/langgraph/src/lib/compositions/chat-timeline-slider +``` + +- [ ] **Step 5: Update public APIs** + +In `libs/chat/src/public-api.ts`, add in the compositions section: +```ts +export { ChatTimelineSliderComponent } from './lib/compositions/chat-timeline-slider/chat-timeline-slider.component'; +``` + +Remove from `libs/langgraph/src/public-api.ts`. + +- [ ] **Step 6: Verify test passes** + +```bash +npx nx test chat --testNamePattern="ChatTimelineSliderComponent" +``` +Expected: PASS. + +- [ ] **Step 7: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-timeline-slider/ \ + libs/chat/src/public-api.ts \ + libs/langgraph/src/public-api.ts +git add -u libs/langgraph/src/lib/compositions +git commit -m "refactor(chat,langgraph): move chat-timeline-slider to chat, retarget to ChatAgentWithHistory" +``` + +--- + +### Task 7: Move `chat-debug` composition tree and rewire to `ChatCheckpoint` + +**Files:** +- Create: `libs/chat/src/lib/compositions/chat-debug/` (15 files, moved) +- Delete: `libs/langgraph/src/lib/compositions/chat-debug/*` +- Modify: public-apis + +- [ ] **Step 1: Copy files into chat** + +```bash +mkdir -p libs/chat/src/lib/compositions/chat-debug +cp libs/langgraph/src/lib/compositions/chat-debug/*.{ts} \ + libs/chat/src/lib/compositions/chat-debug/ +``` + +- [ ] **Step 2: Rewire `debug-utils.ts`** + +```ts +// libs/chat/src/lib/compositions/chat-debug/debug-utils.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import type { ChatCheckpoint } from '../../agent'; +import type { DebugCheckpoint } from './debug-checkpoint-card.component'; + +export function toDebugCheckpoint(cp: ChatCheckpoint, index: number): DebugCheckpoint { + return { + node: cp.label ?? `Step ${index + 1}`, + checkpointId: cp.id, + }; +} + +export function extractStateValues(cp: ChatCheckpoint | undefined): Record { + return cp?.values ?? {}; +} +``` + +- [ ] **Step 3: Rewire `chat-debug.component.ts`** + +In `libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts`: + +- Remove: `TODO(phase-3)` header comment. +- Remove: `import type { AgentRef } from '../../agent.types';` +- Remove: `import { toChatAgent } from '../../to-chat-agent';` +- Update co-located chat imports to use the local path `'../../agent'` + primitive paths within chat (no cross-lib import needed now). +- Change the input: + +```ts + readonly agent = input.required(); +``` + +- Drop `chatAgent` computed wrapper (agent is already the ChatAgent contract): + +```ts + // was: protected readonly chatAgent = computed(() => toChatAgent(this.ref())); + // now: use this.agent() directly +``` + +- Replace all `this.ref().` reads: + - `this.ref().history()` → `this.agent().history()` + - `this.ref().messages()` → `this.agent().messages()` + - `this.ref().isLoading()` → `this.agent().isLoading()` + +- Replace all `[agent]="chatAgent()"` in template with `[agent]="agent()"`. + +- `selectedState` / `previousState` computeds take `ChatCheckpoint` directly: + +```ts + readonly selectedState = computed((): Record => { + const idx = this.selectedCheckpointIndex(); + const history = this.agent().history(); + return extractStateValues(history[idx]); + }); + + readonly previousState = computed((): Record => { + const idx = this.selectedCheckpointIndex(); + const history = this.agent().history(); + if (idx <= 0) return {}; + return extractStateValues(history[idx - 1]); + }); +``` + +- `checkpoints` computed: + +```ts + readonly checkpoints = computed((): DebugCheckpoint[] => + this.agent().history().map((cp, i) => toDebugCheckpoint(cp, i)), + ); +``` + +- Convert the `@cacheplane/chat` imports block to relative imports, matching the convention used by sibling composition `libs/chat/src/lib/compositions/chat/chat.component.ts`: + +```ts +import { ChatMessagesComponent } from '../../primitives/chat-messages/chat-messages.component'; +import { MessageTemplateDirective } from '../../primitives/chat-messages/message-template.directive'; +import { ChatInputComponent } from '../../primitives/chat-input/chat-input.component'; +import { ChatTypingIndicatorComponent } from '../../primitives/chat-typing-indicator/chat-typing-indicator.component'; +import { ChatErrorComponent } from '../../primitives/chat-error/chat-error.component'; +import { messageContent } from '../shared/message-utils'; +import { CHAT_THEME_STYLES } from '../../styles/chat-theme'; +import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown'; +``` + +- Add type import: + +```ts +import type { ChatAgentWithHistory } from '../../agent'; +``` + +- [ ] **Step 4: Rewire spec to use `mockChatAgent` + `history`** + +In `libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts`: + +Replace `createMockAgentRef` imports with: +```ts +import { mockChatAgent } from '../../testing/mock-chat-agent'; +import type { ChatAgentWithHistory } from '../../agent'; +``` + +Replace usages: +```ts +const agent = mockChatAgent({ + history: [ + { id: 'ck1', label: 'nodeA', values: { step: 1 } }, + { id: 'ck2', label: 'nodeB', values: { step: 2 } }, + ], +}) as unknown as ChatAgentWithHistory; +``` + +Replace `[ref]="agent"` with `[agent]="agent"` in any template strings in the spec. + +- [ ] **Step 5: Delete old files** + +```bash +rm -r libs/langgraph/src/lib/compositions/chat-debug +``` + +- [ ] **Step 6: Update public APIs** + +In `libs/chat/src/public-api.ts`, add: +```ts +export { ChatDebugComponent } from './lib/compositions/chat-debug/chat-debug.component'; +``` + +In `libs/langgraph/src/public-api.ts`, remove: +- `ChatDebugComponent` +- All debug sub-component re-exports added in Phase-1 (keep only what langgraph itself still needs — at this point none). + +- [ ] **Step 7: Run chat lint + test + build** + +```bash +npx nx run-many -t lint,test,build -p chat +``` +Expected: PASS. + +- [ ] **Step 8: Run langgraph lint + test + build** + +```bash +npx nx run-many -t lint,test,build -p langgraph +``` +Expected: PASS. (May surface unused `toChatAgent` imports or similar — clean up.) + +- [ ] **Step 9: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-debug/ \ + libs/chat/src/public-api.ts \ + libs/langgraph/src/public-api.ts +git add -u libs/langgraph/src/lib/compositions +git commit -m "refactor(chat,langgraph): move chat-debug composition tree to chat, retarget to ChatCheckpoint" +``` + +--- + +### Task 8: Update cockpit consumers + +**Files:** +- Modify: `cockpit/chat/debug/angular/src/app/debug.component.ts` +- Modify: `cockpit/chat/timeline/angular/src/app/timeline.component.ts` + +- [ ] **Step 1: Update `cockpit/chat/debug/angular/src/app/debug.component.ts`** + +Template change: +```html + +``` +(was `[ref]="stream"`) + +The import switches from `@cacheplane/langgraph` to `@cacheplane/chat` for `ChatDebugComponent`. + +The component already does `protected readonly chatAgent = toChatAgent(this.stream);` from Phase-1. Type now widens to `ChatAgentWithHistory` automatically. + +Keep replay/fork handlers (they still call `this.stream.*` LangGraph APIs). + +- [ ] **Step 2: Update `cockpit/chat/timeline/angular/src/app/timeline.component.ts`** + +Same pattern — template `[ref]="stream"` → `[agent]="chatAgent"`, switch `ChatTimelineSliderComponent` import from `@cacheplane/langgraph` to `@cacheplane/chat`. + +- [ ] **Step 3: Build and e2e-light-test the apps** + +```bash +npx nx run-many -t build -p cockpit-chat-debug-angular,cockpit-chat-timeline-angular +``` +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/chat/debug/angular/src/app/debug.component.ts \ + cockpit/chat/timeline/angular/src/app/timeline.component.ts +git commit -m "refactor(cockpit): rebind chat-debug and chat-timeline demos to @cacheplane/chat" +``` + +--- + +### Task 9: Final verification + +- [ ] **Step 1: Verify nx graph — langgraph → chat only** + +```bash +npx nx graph --file=/tmp/nxgraph.json && \ + jq '.graph.dependencies["@cacheplane/chat"], .graph.dependencies["@cacheplane/langgraph"]' /tmp/nxgraph.json +``` + +Expected: `@cacheplane/chat` does NOT depend on `@cacheplane/langgraph`. `@cacheplane/langgraph` depends on `@cacheplane/chat`. + +- [ ] **Step 2: Full affected lint + test + build** + +```bash +npx nx run-many -t lint,test,build -p chat,langgraph +npx nx affected -t build --base=origin/main +``` + +Expected: all pass. + +- [ ] **Step 3: Grep for stale references** + +```bash +rg "AgentRef" libs/chat/src +rg "ThreadState" libs/chat/src +rg "TODO\\(phase-[23]\\)" libs/ +``` + +Expected: zero hits in `libs/chat/src`; no `TODO(phase-2)` or `TODO(phase-3)` markers remain in either lib. + +- [ ] **Step 4: Push and open PR** + +```bash +git push -u origin feat/chat-runtime-decoupling-phase-2 +gh pr create --title "feat(chat): complete runtime decoupling — compositions back to @cacheplane/chat" --body "$(cat <<'EOF' +## Summary +- Adds `ChatAgentWithHistory` sub-contract and `ChatCheckpoint` neutral shape to `@cacheplane/chat`. +- Moves `chat-timeline`, `chat-timeline-slider`, and `chat-debug` back into `@cacheplane/chat`, retargeted to the sub-contract. +- Widens `toChatAgent` to return `ChatAgentWithHistory`; it translates `ThreadState[]` → `ChatCheckpoint[]`. +- Cockpit debug/timeline demos rebind via one-line template edit. + +## Test Plan +- [ ] `nx run-many -t lint,test,build -p chat,langgraph` passes +- [ ] `nx affected -t build` passes +- [ ] Cockpit chat-debug demo renders checkpoints and replay/fork still function +- [ ] Cockpit chat-timeline demo renders checkpoints +- [ ] `nx graph` shows langgraph → chat only (no reverse edge) +EOF +)" +``` + +--- + +## Out of Scope + +- Replay/fork methods on the sub-contract (design: parent-handled outputs). +- `extra` passthrough on `ChatCheckpoint`. +- Non-LangGraph adapter implementation (separate future effort). From f074a694689d7eabf4e1e8b5bb1ed1a85a82b729 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:12:34 -0700 Subject: [PATCH 03/12] feat(chat): add ChatAgentWithHistory sub-contract and ChatCheckpoint Co-Authored-By: Claude Opus 4.7 --- .../src/lib/agent/chat-agent-with-history.ts | 15 +++++++++++++++ libs/chat/src/lib/agent/chat-checkpoint.ts | 17 +++++++++++++++++ libs/chat/src/lib/agent/index.ts | 2 ++ libs/chat/src/public-api.ts | 2 ++ 4 files changed, 36 insertions(+) create mode 100644 libs/chat/src/lib/agent/chat-agent-with-history.ts create mode 100644 libs/chat/src/lib/agent/chat-checkpoint.ts diff --git a/libs/chat/src/lib/agent/chat-agent-with-history.ts b/libs/chat/src/lib/agent/chat-agent-with-history.ts new file mode 100644 index 000000000..715dce97f --- /dev/null +++ b/libs/chat/src/lib/agent/chat-agent-with-history.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import type { Signal } from '@angular/core'; +import type { ChatAgent } from './chat-agent'; +import type { ChatCheckpoint } from './chat-checkpoint'; + +/** + * Extends ChatAgent with a required `history` signal. + * + * Compositions that need time-travel / checkpoint data (chat-timeline, + * chat-debug) take this richer contract. Adapters that cannot supply + * history should return plain ChatAgent instead of stubbing an empty array. + */ +export interface ChatAgentWithHistory extends ChatAgent { + history: Signal; +} diff --git a/libs/chat/src/lib/agent/chat-checkpoint.ts b/libs/chat/src/lib/agent/chat-checkpoint.ts new file mode 100644 index 000000000..91e08fcdf --- /dev/null +++ b/libs/chat/src/lib/agent/chat-checkpoint.ts @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +/** + * Runtime-neutral snapshot of a point in an agent's execution history. + * + * Consumed by time-travel / debug UIs. `id` is adapter-opaque — UIs emit + * it back to the parent app on replay/fork, and the parent app dispatches + * to the underlying runtime. + */ +export interface ChatCheckpoint { + /** Adapter-opaque checkpoint identifier (e.g. LangGraph checkpoint_id). */ + id?: string; + /** Human-friendly label for the checkpoint (e.g. next node name). */ + label?: string; + /** State snapshot at this checkpoint. */ + values: Record; +} diff --git a/libs/chat/src/lib/agent/index.ts b/libs/chat/src/lib/agent/index.ts index d309a845a..e99bd9827 100644 --- a/libs/chat/src/lib/agent/index.ts +++ b/libs/chat/src/lib/agent/index.ts @@ -9,3 +9,5 @@ export type { ChatInterrupt } from './chat-interrupt'; export type { ChatSubagent, ChatSubagentStatus } from './chat-subagent'; export type { ChatSubmitInput, ChatSubmitOptions } from './chat-submit'; export type { ChatCustomEvent } from './chat-custom-event'; +export type { ChatCheckpoint } from './chat-checkpoint'; +export type { ChatAgentWithHistory } from './chat-agent-with-history'; diff --git a/libs/chat/src/public-api.ts b/libs/chat/src/public-api.ts index 58bf9e817..223aab929 100644 --- a/libs/chat/src/public-api.ts +++ b/libs/chat/src/public-api.ts @@ -19,6 +19,8 @@ export type { ChatSubmitInput, ChatSubmitOptions, ChatCustomEvent, + ChatCheckpoint, + ChatAgentWithHistory, } from './lib/agent'; export { isUserMessage, From 43baf7db4702e17831475d23e5e7b387c2cbf3f0 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:15:05 -0700 Subject: [PATCH 04/12] feat(chat): extend mockChatAgent with optional history Co-Authored-By: Claude Opus 4.7 --- libs/chat/src/lib/testing/mock-chat-agent.spec.ts | 15 +++++++++++++++ libs/chat/src/lib/testing/mock-chat-agent.ts | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/libs/chat/src/lib/testing/mock-chat-agent.spec.ts b/libs/chat/src/lib/testing/mock-chat-agent.spec.ts index fba4fe3b3..97fd32da5 100644 --- a/libs/chat/src/lib/testing/mock-chat-agent.spec.ts +++ b/libs/chat/src/lib/testing/mock-chat-agent.spec.ts @@ -1,5 +1,6 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { mockChatAgent } from './mock-chat-agent'; +import type { ChatAgentWithHistory } from '../agent'; describe('mockChatAgent', () => { it('starts in idle state with empty messages', () => { @@ -40,3 +41,17 @@ describe('mockChatAgent', () => { expect(agent.subagents!().size).toBe(0); }); }); + +describe('mockChatAgent with history', () => { + it('exposes history signal when history option supplied', () => { + const agent = mockChatAgent({ history: [{ id: 'c1', label: 'start', values: {} }] }); + const withHistory = agent as ChatAgentWithHistory; + expect(typeof withHistory.history).toBe('function'); + expect(withHistory.history()).toEqual([{ id: 'c1', label: 'start', values: {} }]); + }); + + it('omits history when option absent', () => { + const agent = mockChatAgent({}); + expect((agent as Partial).history).toBeUndefined(); + }); +}); diff --git a/libs/chat/src/lib/testing/mock-chat-agent.ts b/libs/chat/src/lib/testing/mock-chat-agent.ts index 013245585..75e340a52 100644 --- a/libs/chat/src/lib/testing/mock-chat-agent.ts +++ b/libs/chat/src/lib/testing/mock-chat-agent.ts @@ -10,6 +10,7 @@ import type { ChatSubagent, ChatSubmitInput, ChatSubmitOptions, + ChatCheckpoint, } from '../agent'; import type { ChatCustomEvent } from '../agent/chat-custom-event'; @@ -22,6 +23,7 @@ export interface MockChatAgent extends ChatAgent { state: WritableSignal>; interrupt?: WritableSignal; subagents?: WritableSignal>; + history?: WritableSignal; customEvents$?: Observable; /** Captured calls to submit() in order. */ submitCalls: Array<{ input: ChatSubmitInput; opts?: ChatSubmitOptions }>; @@ -38,6 +40,7 @@ export interface MockChatAgentOptions { state?: Record; withInterrupt?: boolean; withSubagents?: boolean; + history?: ChatCheckpoint[]; customEvents$?: Observable; } @@ -55,6 +58,9 @@ export function mockChatAgent(opts: MockChatAgentOptions = {}): MockChatAgent { const subagents = opts.withSubagents ? signal>(new Map()) : undefined; + const history = opts.history + ? signal(opts.history) + : undefined; const submitCalls: MockChatAgent['submitCalls'] = []; let stopCount = 0; @@ -63,6 +69,7 @@ export function mockChatAgent(opts: MockChatAgentOptions = {}): MockChatAgent { messages, status, isLoading, error, toolCalls, state, ...(interrupt ? { interrupt } : {}), ...(subagents ? { subagents } : {}), + ...(history ? { history } : {}), ...(opts.customEvents$ ? { customEvents$: opts.customEvents$ } : {}), submit: async (input, submitOpts) => { submitCalls.push({ input, opts: submitOpts }); }, stop: async () => { stopCount++; }, From edb3f0062a36cc85f1fb5b0530d924795c492e4c Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:16:00 -0700 Subject: [PATCH 05/12] test(chat): add ChatAgentWithHistory conformance helper Co-Authored-By: Claude Opus 4.7 --- .../chat-agent-with-history-conformance.ts | 37 +++++++++++++++++++ libs/chat/src/public-api.ts | 1 + 2 files changed, 38 insertions(+) create mode 100644 libs/chat/src/lib/testing/chat-agent-with-history-conformance.ts diff --git a/libs/chat/src/lib/testing/chat-agent-with-history-conformance.ts b/libs/chat/src/lib/testing/chat-agent-with-history-conformance.ts new file mode 100644 index 000000000..baafdcb37 --- /dev/null +++ b/libs/chat/src/lib/testing/chat-agent-with-history-conformance.ts @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { describe, it, expect } from 'vitest'; +import type { ChatAgentWithHistory, ChatCheckpoint } from '../agent'; +import { runChatAgentConformance } from './chat-agent-conformance'; + +/** + * Conformance suite for ChatAgentWithHistory implementations. + * + * Runs the base ChatAgent conformance suite, then verifies the history + * signal is present and returns an array of ChatCheckpoint-shaped entries. + */ +export function runChatAgentWithHistoryConformance( + label: string, + factory: (seed?: { history?: ChatCheckpoint[] }) => ChatAgentWithHistory, +): void { + runChatAgentConformance(label, () => factory()); + + describe(`${label} — history`, () => { + it('exposes a history signal', () => { + const agent = factory(); + expect(typeof agent.history).toBe('function'); + expect(Array.isArray(agent.history())).toBe(true); + }); + + it('reflects seeded checkpoints', () => { + const seed: ChatCheckpoint[] = [ + { id: 'c1', label: 'Step 1', values: { foo: 1 } }, + { id: 'c2', label: 'Step 2', values: { foo: 2 } }, + ]; + const agent = factory({ history: seed }); + const entries = agent.history(); + expect(entries).toHaveLength(2); + expect(entries[0].id).toBe('c1'); + expect(entries[1].values).toEqual({ foo: 2 }); + }); + }); +} diff --git a/libs/chat/src/public-api.ts b/libs/chat/src/public-api.ts index 223aab929..0e9dad318 100644 --- a/libs/chat/src/public-api.ts +++ b/libs/chat/src/public-api.ts @@ -118,3 +118,4 @@ export { isPathRef, isFunctionCall } from '@cacheplane/a2ui'; export { mockChatAgent } from './lib/testing/mock-chat-agent'; export type { MockChatAgent, MockChatAgentOptions } from './lib/testing/mock-chat-agent'; export { runChatAgentConformance } from './lib/testing/chat-agent-conformance'; +export { runChatAgentWithHistoryConformance } from './lib/testing/chat-agent-with-history-conformance'; From 7ada258a6e03ad7ab474d6f3a458581f6cc3b61e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:19:38 -0700 Subject: [PATCH 06/12] feat(langgraph): widen toChatAgent to ChatAgentWithHistory, translate history Co-Authored-By: Claude Opus 4.7 --- libs/langgraph/src/lib/to-chat-agent.spec.ts | 18 +++++++++++++++ libs/langgraph/src/lib/to-chat-agent.ts | 24 +++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/src/lib/to-chat-agent.spec.ts b/libs/langgraph/src/lib/to-chat-agent.spec.ts index 432d830ab..2957ac52e 100644 --- a/libs/langgraph/src/lib/to-chat-agent.spec.ts +++ b/libs/langgraph/src/lib/to-chat-agent.spec.ts @@ -97,6 +97,24 @@ describe('toChatAgent (LangGraph adapter)', () => { }); }); + it('translates ThreadState history into ChatCheckpoint[]', () => { + TestBed.runInInjectionContext(() => { + const ref = stubAgentRef({ + history: signal([ + { values: { step: 1 }, next: ['nodeA'], checkpoint: { checkpoint_id: 'ck1' } }, + { values: { step: 2 }, next: [], checkpoint: { checkpoint_id: 'ck2' } }, + { values: { step: 3 }, next: ['nodeC'], checkpoint: undefined }, + ] as any), + }); + const chat = toChatAgent(ref); + expect(chat.history()).toEqual([ + { id: 'ck1', label: 'nodeA', values: { step: 1 } }, + { id: 'ck2', label: undefined, values: { step: 2 } }, + { id: undefined, label: 'nodeC', values: { step: 3 } }, + ]); + }); + }); + it('exposes customEvents$ that emits newly-appended events with type aliased from name', () => { const customSig = signal([]); const ref = stubAgentRef({ customEvents: customSig }); diff --git a/libs/langgraph/src/lib/to-chat-agent.ts b/libs/langgraph/src/lib/to-chat-agent.ts index 4cc3df38d..b6ee57748 100644 --- a/libs/langgraph/src/lib/to-chat-agent.ts +++ b/libs/langgraph/src/lib/to-chat-agent.ts @@ -4,7 +4,8 @@ import { Subject, type Observable } from 'rxjs'; import type { BaseMessage } from '@langchain/core/messages'; import type { ToolCallWithResult, Interrupt } from '@langchain/langgraph-sdk'; import type { - ChatAgent, + ChatAgentWithHistory, + ChatCheckpoint, ChatCustomEvent, ChatMessage, ChatRole, @@ -16,7 +17,7 @@ import type { ChatSubmitInput, ChatSubmitOptions, } from '@cacheplane/chat'; -import type { AgentRef, CustomStreamEvent, SubagentStreamRef } from './agent.types'; +import type { AgentRef, CustomStreamEvent, SubagentStreamRef, ThreadState } from './agent.types'; import { ResourceStatus } from './agent.types'; /** @@ -27,7 +28,7 @@ import { ResourceStatus } from './agent.types'; * Must be called within an Angular injection context (uses `computed` and * `effect`). */ -export function toChatAgent(ref: AgentRef): ChatAgent { +export function toChatAgent(ref: AgentRef): ChatAgentWithHistory { const messages = computed(() => ref.messages().map(toChatMessage), ); @@ -57,6 +58,10 @@ export function toChatAgent(ref: AgentRef): ChatAgent { const customEvents$ = buildCustomEvents$(ref); + const history = computed(() => + ref.history().map(toChatCheckpoint), + ); + return { messages, status, @@ -67,6 +72,7 @@ export function toChatAgent(ref: AgentRef): ChatAgent { interrupt, subagents, customEvents$, + history, submit: (input: ChatSubmitInput, opts?: ChatSubmitOptions) => ref.submit(buildSubmitPayload(input), opts ? { signal: opts.signal } as never : undefined), stop: () => ref.stop(), @@ -183,3 +189,15 @@ function buildSubmitPayload(input: ChatSubmitInput): unknown { function randomId(): string { return Math.random().toString(36).slice(2); } + +function toChatCheckpoint(state: ThreadState): ChatCheckpoint { + return { + id: state.checkpoint?.checkpoint_id ?? undefined, + label: state.next?.[0] ?? undefined, + values: isRecord(state.values) ? state.values : {}, + }; +} + +function isRecord(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v); +} From a715e10e73e451e3da7ae9ecd145c490d9c9e77b Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:30:05 -0700 Subject: [PATCH 07/12] refactor(chat,langgraph): move chat-timeline primitive to chat, retarget to ChatAgentWithHistory Co-Authored-By: Claude Opus 4.7 --- .../chat-timeline.component.spec.ts | 25 ++++++++ .../chat-timeline/chat-timeline.component.ts | 37 ++++++++++++ libs/chat/src/public-api.ts | 1 + .../chat-timeline.component.spec.ts | 60 ------------------- .../chat-timeline/chat-timeline.component.ts | 43 ------------- libs/langgraph/src/public-api.ts | 2 - 6 files changed, 63 insertions(+), 105 deletions(-) create mode 100644 libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts create mode 100644 libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.ts delete mode 100644 libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts delete mode 100644 libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.ts diff --git a/libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts b/libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts new file mode 100644 index 000000000..272013f50 --- /dev/null +++ b/libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { describe, it, expect } from 'vitest'; +import { signal, computed } from '@angular/core'; +import { mockChatAgent } from '../../testing/mock-chat-agent'; +import type { ChatCheckpoint } from '../../agent'; + +describe('ChatTimelineComponent', () => { + it('renders a template for each checkpoint', () => { + const checkpoints: ChatCheckpoint[] = [ + { id: 'a', label: 'nodeA', values: {} }, + { id: 'b', label: 'nodeB', values: {} }, + ]; + const agent = mockChatAgent({ history: checkpoints }); + + // Mirrors the computed inside ChatTimelineComponent: + // history = computed(() => this.agent().history()) + const agentSig = signal(agent as any); + const history = computed(() => agentSig().history()); + + expect(history()).toHaveLength(2); + // Simulate what the @for template renders: index:label + const rendered = history().map((cp, i) => `${i}:${cp.label}`).join(''); + expect(rendered).toBe('0:nodeA1:nodeB'); + }); +}); diff --git a/libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.ts b/libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.ts new file mode 100644 index 000000000..343352ab1 --- /dev/null +++ b/libs/chat/src/lib/primitives/chat-timeline/chat-timeline.component.ts @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, computed, contentChild, input, output, + TemplateRef, ChangeDetectionStrategy, +} from '@angular/core'; +import { NgTemplateOutlet } from '@angular/common'; +import type { ChatAgentWithHistory, ChatCheckpoint } from '../../agent'; + +@Component({ + selector: 'chat-timeline', + standalone: true, + imports: [NgTemplateOutlet], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @for (cp of history(); track $index) { + @if (templateRef()) { + + } + } + `, +}) +export class ChatTimelineComponent { + readonly agent = input.required(); + + readonly checkpointSelected = output(); + + readonly templateRef = contentChild(TemplateRef); + + readonly history = computed(() => this.agent().history()); + + selectCheckpoint(cp: ChatCheckpoint): void { + this.checkpointSelected.emit(cp); + } +} diff --git a/libs/chat/src/public-api.ts b/libs/chat/src/public-api.ts index 0e9dad318..4f5d1aeed 100644 --- a/libs/chat/src/public-api.ts +++ b/libs/chat/src/public-api.ts @@ -41,6 +41,7 @@ export { ChatToolCallsComponent } from './lib/primitives/chat-tool-calls/chat-to export { ChatSubagentsComponent } from './lib/primitives/chat-subagents/chat-subagents.component'; export { ChatThreadListComponent } from './lib/primitives/chat-thread-list/chat-thread-list.component'; export type { Thread } from './lib/primitives/chat-thread-list/chat-thread-list.component'; +export { ChatTimelineComponent } from './lib/primitives/chat-timeline/chat-timeline.component'; // DI provider export { provideChat, CHAT_CONFIG } from './lib/provide-chat'; diff --git a/libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts b/libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts deleted file mode 100644 index 6ac601ed2..000000000 --- a/libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 -import { describe, it, expect } from 'vitest'; -import { signal, computed } from '@angular/core'; -import { createMockAgentRef } from '../../testing/mock-agent-ref'; -import type { ThreadState } from '../../agent.types'; - -const makeState = (id: string): ThreadState => - ({ checkpoint_id: id, values: {}, next: [], metadata: {} } as any); - -describe('ChatTimelineComponent — history computed', () => { - it('returns empty array when ref has no history', () => { - const mockRef = createMockAgentRef(); - const ref$ = signal(mockRef); - - const history = computed(() => ref$().history()); - - expect(history()).toHaveLength(0); - }); - - it('returns history states from ref', () => { - const states = [makeState('cp-1'), makeState('cp-2')]; - const mockRef = createMockAgentRef(); - (mockRef.history as ReturnType[]>>).set(states); - - const ref$ = signal(mockRef); - const history = computed(() => ref$().history()); - - expect(history()).toHaveLength(2); - expect(history()[0]).toBe(states[0]); - expect(history()[1]).toBe(states[1]); - }); - - it('history updates reactively when ref changes', () => { - const emptyRef = createMockAgentRef(); - const loadedRef = createMockAgentRef(); - const states = [makeState('cp-3')]; - (loadedRef.history as ReturnType[]>>).set(states); - - const ref$ = signal(emptyRef); - const history = computed(() => ref$().history()); - - expect(history()).toHaveLength(0); - ref$.set(loadedRef); - expect(history()).toHaveLength(1); - expect(history()[0]).toBe(states[0]); - }); - - it('history updates reactively when ref.history signal changes', () => { - const mockRef = createMockAgentRef(); - const ref$ = signal(mockRef); - const history = computed(() => ref$().history()); - - expect(history()).toHaveLength(0); - - const states = [makeState('cp-4'), makeState('cp-5')]; - (mockRef.history as ReturnType[]>>).set(states); - - expect(history()).toHaveLength(2); - }); -}); diff --git a/libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.ts b/libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.ts deleted file mode 100644 index f7b26dabc..000000000 --- a/libs/langgraph/src/lib/primitives/chat-timeline/chat-timeline.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 -// TODO(phase-3): migrate from AgentRef to ChatAgent contract. -import { - Component, - computed, - contentChild, - input, - output, - TemplateRef, - ChangeDetectionStrategy, -} from '@angular/core'; -import { NgTemplateOutlet } from '@angular/common'; -import type { AgentRef, ThreadState } from '../../agent.types'; - -@Component({ - selector: 'chat-timeline', - standalone: true, - imports: [NgTemplateOutlet], - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` - @for (state of history(); track $index) { - @if (templateRef()) { - - } - } - `, -}) -export class ChatTimelineComponent { - readonly ref = input.required>(); - - readonly checkpointSelected = output>(); - - readonly templateRef = contentChild(TemplateRef); - - readonly history = computed((): ThreadState[] => this.ref().history()); - - selectCheckpoint(state: ThreadState): void { - this.checkpointSelected.emit(state); - } -} diff --git a/libs/langgraph/src/public-api.ts b/libs/langgraph/src/public-api.ts index dad4df532..454c424b8 100644 --- a/libs/langgraph/src/public-api.ts +++ b/libs/langgraph/src/public-api.ts @@ -31,8 +31,6 @@ export { MockAgentTransport } from './lib/transport/mock-stream.transport'; export { FetchStreamTransport } from './lib/transport/fetch-stream.transport'; // LangGraph-specific chat primitives (checkpoint_id / ThreadState / fork-replay UI) -export { ChatTimelineComponent } from './lib/primitives/chat-timeline/chat-timeline.component'; - export { ChatTimelineSliderComponent } from './lib/compositions/chat-timeline-slider/chat-timeline-slider.component'; export { ChatDebugComponent } from './lib/compositions/chat-debug/chat-debug.component'; From ff7dfcabf29ba68e870f049dedd366e9318f5758 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:34:25 -0700 Subject: [PATCH 08/12] refactor(chat,langgraph): move chat-timeline-slider to chat, retarget to ChatAgentWithHistory Moves ChatTimelineSliderComponent from @cacheplane/langgraph to @cacheplane/chat, replacing the LangGraph-specific AgentRef/ThreadState types with the runtime-neutral ChatAgentWithHistory/ChatCheckpoint contract. Updates both public-api.ts files accordingly. Co-Authored-By: Claude Opus 4.7 --- .../chat-timeline-slider.component.spec.ts | 38 +++++++++++++ .../chat-timeline-slider.component.ts | 57 ++++++------------- libs/chat/src/public-api.ts | 1 + .../chat-timeline-slider.component.spec.ts | 24 -------- libs/langgraph/src/public-api.ts | 2 - 5 files changed, 55 insertions(+), 67 deletions(-) create mode 100644 libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts rename libs/{langgraph => chat}/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts (61%) delete mode 100644 libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts diff --git a/libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts b/libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts new file mode 100644 index 000000000..be1ba6059 --- /dev/null +++ b/libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { describe, it, expect, vi } from 'vitest'; +import { TestBed } from '@angular/core/testing'; +import { ChatTimelineSliderComponent } from './chat-timeline-slider.component'; +import type { ChatCheckpoint } from '../../agent'; + +describe('ChatTimelineSliderComponent', () => { + it('replay() emits the checkpoint id when id is present', () => { + TestBed.runInInjectionContext(() => { + const slider = new ChatTimelineSliderComponent(); + const spy = vi.fn(); + slider.replayRequested.subscribe(spy); + slider.replay({ id: 'ck1', values: {} } as ChatCheckpoint); + expect(spy).toHaveBeenCalledWith('ck1'); + }); + }); + + it('replay() does not emit when id is undefined', () => { + TestBed.runInInjectionContext(() => { + const slider = new ChatTimelineSliderComponent(); + const spy = vi.fn(); + slider.replayRequested.subscribe(spy); + slider.replay({ values: {} } as ChatCheckpoint); + expect(spy).not.toHaveBeenCalled(); + }); + }); + + it('fork() updates selectedIndex and emits the id', () => { + TestBed.runInInjectionContext(() => { + const slider = new ChatTimelineSliderComponent(); + const spy = vi.fn(); + slider.forkRequested.subscribe(spy); + slider.fork({ id: 'ck2', values: {} } as ChatCheckpoint, 3); + expect(slider.selectedIndex()).toBe(3); + expect(spy).toHaveBeenCalledWith('ck2'); + }); + }); +}); diff --git a/libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts b/libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts similarity index 61% rename from libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts rename to libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts index 9c808f327..e1a264175 100644 --- a/libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts +++ b/libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts @@ -1,14 +1,9 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 -// TODO(phase-3): migrate from AgentRef to ChatAgent contract. import { - Component, - computed, - input, - output, - signal, + Component, computed, input, output, signal, ChangeDetectionStrategy, } from '@angular/core'; -import type { AgentRef, ThreadState } from '../../agent.types'; +import type { ChatAgentWithHistory, ChatCheckpoint } from '../../agent'; @Component({ selector: 'chat-timeline-slider', @@ -26,12 +21,11 @@ import type { AgentRef, ThreadState } from '../../agent.types'; }
- @for (state of history(); track $index; let i = $index) { + @for (cp of history(); track $index; let i = $index) {
- -

- {{ checkpointLabel(state, i) }} + {{ cp.label ?? 'Step ' + (i + 1) }}

- @if (state.checkpoint?.checkpoint_id) { -

{{ state.checkpoint?.checkpoint_id }}

+ @if (cp.id) { +

{{ cp.id }}

}
-
+ (click)="replay(cp)" + >Replay + (click)="fork(cp, i)" + >Fork
} @@ -73,34 +61,21 @@ import type { AgentRef, ThreadState } from '../../agent.types'; `, }) export class ChatTimelineSliderComponent { - readonly ref = input.required>(); + readonly agent = input.required(); readonly selectedIndex = signal(-1); - readonly history = computed((): ThreadState[] => this.ref().history()); + readonly history = computed(() => this.agent().history()); - /** Emits the checkpoint_id when the user requests replay from that checkpoint. */ readonly replayRequested = output(); - /** Emits the checkpoint_id when the user requests a fork from that checkpoint. */ readonly forkRequested = output(); - checkpointLabel(state: ThreadState, index: number): string { - if (state.checkpoint?.checkpoint_id) { - return `Checkpoint ${index + 1}`; - } - return `State ${index + 1}`; + replay(cp: ChatCheckpoint): void { + if (cp.id) this.replayRequested.emit(cp.id); } - replay(state: ThreadState): void { - if (state.checkpoint?.checkpoint_id) { - this.replayRequested.emit(state.checkpoint.checkpoint_id); - } - } - - fork(state: ThreadState, index: number): void { + fork(cp: ChatCheckpoint, index: number): void { this.selectedIndex.set(index); - if (state.checkpoint?.checkpoint_id) { - this.forkRequested.emit(state.checkpoint.checkpoint_id); - } + if (cp.id) this.forkRequested.emit(cp.id); } } diff --git a/libs/chat/src/public-api.ts b/libs/chat/src/public-api.ts index 4f5d1aeed..630c38128 100644 --- a/libs/chat/src/public-api.ts +++ b/libs/chat/src/public-api.ts @@ -54,6 +54,7 @@ export type { InterruptAction } from './lib/compositions/chat-interrupt-panel/ch export { ChatToolCallCardComponent } from './lib/compositions/chat-tool-call-card/chat-tool-call-card.component'; export type { ToolCallInfo } from './lib/compositions/chat-tool-call-card/chat-tool-call-card.component'; export { ChatSubagentCardComponent } from './lib/compositions/chat-subagent-card/chat-subagent-card.component'; +export { ChatTimelineSliderComponent } from './lib/compositions/chat-timeline-slider/chat-timeline-slider.component'; // Shared styles & utilities export { CHAT_THEME_STYLES } from './lib/styles/chat-theme'; diff --git a/libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts b/libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts deleted file mode 100644 index 2745c87dc..000000000 --- a/libs/langgraph/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 -import { describe, it, expect } from 'vitest'; -import { ChatTimelineSliderComponent } from './chat-timeline-slider.component'; - -describe('ChatTimelineSliderComponent', () => { - it('is defined', () => { - expect(ChatTimelineSliderComponent).toBeDefined(); - expect(typeof ChatTimelineSliderComponent).toBe('function'); - }); - - it('checkpointLabel returns label with index+1 when no checkpoint_id', () => { - const checkpointLabel = ChatTimelineSliderComponent.prototype.checkpointLabel; - const state = {} as any; - expect(checkpointLabel(state, 0)).toBe('State 1'); - expect(checkpointLabel(state, 4)).toBe('State 5'); - }); - - it('checkpointLabel uses "Checkpoint N" when checkpoint_id is present', () => { - const checkpointLabel = ChatTimelineSliderComponent.prototype.checkpointLabel; - const state = { checkpoint: { checkpoint_id: 'abc123' } } as any; - expect(checkpointLabel(state, 0)).toBe('Checkpoint 1'); - expect(checkpointLabel(state, 2)).toBe('Checkpoint 3'); - }); -}); diff --git a/libs/langgraph/src/public-api.ts b/libs/langgraph/src/public-api.ts index 454c424b8..4824ece70 100644 --- a/libs/langgraph/src/public-api.ts +++ b/libs/langgraph/src/public-api.ts @@ -31,8 +31,6 @@ export { MockAgentTransport } from './lib/transport/mock-stream.transport'; export { FetchStreamTransport } from './lib/transport/fetch-stream.transport'; // LangGraph-specific chat primitives (checkpoint_id / ThreadState / fork-replay UI) -export { ChatTimelineSliderComponent } from './lib/compositions/chat-timeline-slider/chat-timeline-slider.component'; - export { ChatDebugComponent } from './lib/compositions/chat-debug/chat-debug.component'; export { toDebugCheckpoint, extractStateValues } from './lib/compositions/chat-debug/debug-utils'; export { DebugCheckpointCardComponent } from './lib/compositions/chat-debug/debug-checkpoint-card.component'; From 08764115629092df9c89840a5ba21b68663eba68 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:41:25 -0700 Subject: [PATCH 09/12] refactor(chat,langgraph): move chat-debug composition tree to chat, retarget to ChatCheckpoint Moves all 11 files from libs/langgraph/src/lib/compositions/chat-debug/ to libs/chat/src/lib/compositions/chat-debug/. Rewires chat-debug.component.ts to use ChatAgentWithHistory (replacing AgentRef + toChatAgent adapter) and debug-utils.ts to consume ChatCheckpoint (replacing ThreadState). Updates public APIs: ChatDebugComponent exported from @cacheplane/chat; all debug-* re-exports removed from @cacheplane/langgraph. Co-Authored-By: Claude Opus 4.7 --- .../chat-debug/chat-debug.component.spec.ts | 47 +++++++++---------- .../chat-debug/chat-debug.component.ts | 45 ++++++++---------- .../debug-checkpoint-card.component.ts | 0 .../chat-debug/debug-controls.component.ts | 0 .../chat-debug/debug-detail.component.ts | 0 .../chat-debug/debug-state-diff.component.ts | 0 .../debug-state-inspector.component.ts | 0 .../chat-debug/debug-summary.component.ts | 0 .../chat-debug/debug-timeline.component.ts | 0 .../compositions/chat-debug/debug-utils.ts | 14 ++++++ .../lib/compositions/chat-debug/state-diff.ts | 0 libs/chat/src/public-api.ts | 1 + .../compositions/chat-debug/debug-utils.ts | 24 ---------- libs/langgraph/src/public-api.ts | 14 ------ 14 files changed, 55 insertions(+), 90 deletions(-) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/chat-debug.component.spec.ts (82%) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/chat-debug.component.ts (87%) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts (100%) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/debug-controls.component.ts (100%) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/debug-detail.component.ts (100%) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/debug-state-diff.component.ts (100%) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/debug-state-inspector.component.ts (100%) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/debug-summary.component.ts (100%) rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/debug-timeline.component.ts (100%) create mode 100644 libs/chat/src/lib/compositions/chat-debug/debug-utils.ts rename libs/{langgraph => chat}/src/lib/compositions/chat-debug/state-diff.ts (100%) delete mode 100644 libs/langgraph/src/lib/compositions/chat-debug/debug-utils.ts diff --git a/libs/langgraph/src/lib/compositions/chat-debug/chat-debug.component.spec.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts similarity index 82% rename from libs/langgraph/src/lib/compositions/chat-debug/chat-debug.component.spec.ts rename to libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts index 9d4d56323..c7456b72f 100644 --- a/libs/langgraph/src/lib/compositions/chat-debug/chat-debug.component.spec.ts +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts @@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest'; import { computeStateDiff } from './state-diff'; import type { DiffEntry } from './state-diff'; import { toDebugCheckpoint, extractStateValues } from './debug-utils'; -import type { DebugCheckpoint } from './debug-checkpoint-card.component'; +import type { ChatCheckpoint } from '../../agent'; import { DebugCheckpointCardComponent } from './debug-checkpoint-card.component'; import { DebugControlsComponent } from './debug-controls.component'; import { DebugSummaryComponent } from './debug-summary.component'; @@ -86,46 +86,41 @@ describe('computeStateDiff', () => { // ── toDebugCheckpoint ────────────────────────────────────────────────────── describe('toDebugCheckpoint', () => { - it('uses first next node as name when available', () => { - const state = { next: ['agent'], checkpoint: { checkpoint_id: 'cp1' } } as any; - const cp = toDebugCheckpoint(state, 0); - expect(cp.node).toBe('agent'); - expect(cp.checkpointId).toBe('cp1'); + it('uses label as node name when available', () => { + const cp: ChatCheckpoint = { id: 'cp1', label: 'agent', values: {} }; + const result = toDebugCheckpoint(cp, 0); + expect(result.node).toBe('agent'); + expect(result.checkpointId).toBe('cp1'); }); - it('falls back to Step N when next is empty', () => { - const state = { next: [], checkpoint: {} } as any; - const cp = toDebugCheckpoint(state, 2); - expect(cp.node).toBe('Step 3'); + it('falls back to Step N when label is absent', () => { + const cp: ChatCheckpoint = { values: {} }; + const result = toDebugCheckpoint(cp, 2); + expect(result.node).toBe('Step 3'); }); - it('returns undefined checkpointId when not present', () => { - const state = { next: ['tool'], checkpoint: {} } as any; - const cp = toDebugCheckpoint(state, 0); - expect(cp.checkpointId).toBeUndefined(); + it('returns undefined checkpointId when id is not present', () => { + const cp: ChatCheckpoint = { label: 'tool', values: {} }; + const result = toDebugCheckpoint(cp, 0); + expect(result.checkpointId).toBeUndefined(); }); }); // ── extractStateValues ───────────────────────────────────────────────────── describe('extractStateValues', () => { - it('returns empty object for undefined state', () => { + it('returns empty object for undefined checkpoint', () => { expect(extractStateValues(undefined)).toEqual({}); }); - it('extracts values from a ThreadState', () => { - const state = { values: { messages: [], count: 5 } } as any; - expect(extractStateValues(state)).toEqual({ messages: [], count: 5 }); + it('extracts values from a ChatCheckpoint', () => { + const cp: ChatCheckpoint = { values: { messages: [], count: 5 } }; + expect(extractStateValues(cp)).toEqual({ messages: [], count: 5 }); }); - it('returns empty object for non-object values', () => { - const state = { values: 'invalid' } as any; - expect(extractStateValues(state)).toEqual({}); - }); - - it('returns empty object for array values', () => { - const state = { values: [1, 2, 3] } as any; - expect(extractStateValues(state)).toEqual({}); + it('returns empty object for a checkpoint with empty values', () => { + const cp: ChatCheckpoint = { values: {} }; + expect(extractStateValues(cp)).toEqual({}); }); }); diff --git a/libs/langgraph/src/lib/compositions/chat-debug/chat-debug.component.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts similarity index 87% rename from libs/langgraph/src/lib/compositions/chat-debug/chat-debug.component.ts rename to libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts index 19272b095..831a6576f 100644 --- a/libs/langgraph/src/lib/compositions/chat-debug/chat-debug.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts @@ -1,5 +1,4 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 -// TODO(phase-3): migrate from AgentRef to ChatAgent contract. import { Component, computed, @@ -12,19 +11,15 @@ import { ChangeDetectionStrategy, } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; -import type { AgentRef } from '../../agent.types'; -import { toChatAgent } from '../../to-chat-agent'; -import { - ChatMessagesComponent, - MessageTemplateDirective, - ChatInputComponent, - ChatTypingIndicatorComponent, - ChatErrorComponent, - messageContent, - CHAT_THEME_STYLES, - CHAT_MARKDOWN_STYLES, - renderMarkdown, -} from '@cacheplane/chat'; +import type { ChatAgentWithHistory } from '../../agent'; +import { ChatMessagesComponent } from '../../primitives/chat-messages/chat-messages.component'; +import { MessageTemplateDirective } from '../../primitives/chat-messages/message-template.directive'; +import { ChatInputComponent } from '../../primitives/chat-input/chat-input.component'; +import { ChatTypingIndicatorComponent } from '../../primitives/chat-typing-indicator/chat-typing-indicator.component'; +import { ChatErrorComponent } from '../../primitives/chat-error/chat-error.component'; +import { messageContent } from '../shared/message-utils'; +import { CHAT_THEME_STYLES } from '../../styles/chat-theme'; +import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown'; import { DebugTimelineComponent } from './debug-timeline.component'; import { DebugDetailComponent } from './debug-detail.component'; import { DebugControlsComponent } from './debug-controls.component'; @@ -60,7 +55,7 @@ import { toDebugCheckpoint, extractStateValues } from './debug-utils'; aria-live="polite" >
- +
@@ -104,17 +99,17 @@ import { toDebugCheckpoint, extractStateValues } from './debug-utils'; - +
- +
@@ -193,26 +188,24 @@ import { toDebugCheckpoint, extractStateValues } from './debug-utils'; export class ChatDebugComponent { private readonly sanitizer = inject(DomSanitizer); - readonly ref = input.required>(); - - protected readonly chatAgent = computed(() => toChatAgent(this.ref())); + readonly agent = input.required(); readonly debugOpen = signal(true); readonly selectedCheckpointIndex = signal(-1); readonly checkpoints = computed((): DebugCheckpoint[] => - this.ref().history().map((state, i) => toDebugCheckpoint(state, i)), + this.agent().history().map((cp, i) => toDebugCheckpoint(cp, i)), ); readonly selectedState = computed((): Record => { const idx = this.selectedCheckpointIndex(); - const history = this.ref().history(); + const history = this.agent().history(); return extractStateValues(history[idx]); }); readonly previousState = computed((): Record => { const idx = this.selectedCheckpointIndex(); - const history = this.ref().history(); + const history = this.agent().history(); if (idx <= 0) return {}; return extractStateValues(history[idx - 1]); }); @@ -223,14 +216,14 @@ export class ChatDebugComponent { private readonly scrollContainer = viewChild>('scrollContainer'); /** Track message count to trigger auto-scroll */ - private readonly messageCount = computed(() => this.ref().messages().length); + private readonly messageCount = computed(() => this.agent().messages().length); private prevMessageCount = 0; constructor() { effect(() => { const count = this.messageCount(); - this.ref().isLoading(); // track + this.agent().isLoading(); // track const el = this.scrollContainer()?.nativeElement; if (!el) return; diff --git a/libs/langgraph/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts similarity index 100% rename from libs/langgraph/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts rename to libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts diff --git a/libs/langgraph/src/lib/compositions/chat-debug/debug-controls.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-controls.component.ts similarity index 100% rename from libs/langgraph/src/lib/compositions/chat-debug/debug-controls.component.ts rename to libs/chat/src/lib/compositions/chat-debug/debug-controls.component.ts diff --git a/libs/langgraph/src/lib/compositions/chat-debug/debug-detail.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-detail.component.ts similarity index 100% rename from libs/langgraph/src/lib/compositions/chat-debug/debug-detail.component.ts rename to libs/chat/src/lib/compositions/chat-debug/debug-detail.component.ts diff --git a/libs/langgraph/src/lib/compositions/chat-debug/debug-state-diff.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts similarity index 100% rename from libs/langgraph/src/lib/compositions/chat-debug/debug-state-diff.component.ts rename to libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts diff --git a/libs/langgraph/src/lib/compositions/chat-debug/debug-state-inspector.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts similarity index 100% rename from libs/langgraph/src/lib/compositions/chat-debug/debug-state-inspector.component.ts rename to libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts diff --git a/libs/langgraph/src/lib/compositions/chat-debug/debug-summary.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts similarity index 100% rename from libs/langgraph/src/lib/compositions/chat-debug/debug-summary.component.ts rename to libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts diff --git a/libs/langgraph/src/lib/compositions/chat-debug/debug-timeline.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts similarity index 100% rename from libs/langgraph/src/lib/compositions/chat-debug/debug-timeline.component.ts rename to libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-utils.ts b/libs/chat/src/lib/compositions/chat-debug/debug-utils.ts new file mode 100644 index 000000000..1487d7b14 --- /dev/null +++ b/libs/chat/src/lib/compositions/chat-debug/debug-utils.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import type { ChatCheckpoint } from '../../agent'; +import type { DebugCheckpoint } from './debug-checkpoint-card.component'; + +export function toDebugCheckpoint(cp: ChatCheckpoint, index: number): DebugCheckpoint { + return { + node: cp.label ?? `Step ${index + 1}`, + checkpointId: cp.id, + }; +} + +export function extractStateValues(cp: ChatCheckpoint | undefined): Record { + return cp?.values ?? {}; +} diff --git a/libs/langgraph/src/lib/compositions/chat-debug/state-diff.ts b/libs/chat/src/lib/compositions/chat-debug/state-diff.ts similarity index 100% rename from libs/langgraph/src/lib/compositions/chat-debug/state-diff.ts rename to libs/chat/src/lib/compositions/chat-debug/state-diff.ts diff --git a/libs/chat/src/public-api.ts b/libs/chat/src/public-api.ts index 630c38128..788c3e19a 100644 --- a/libs/chat/src/public-api.ts +++ b/libs/chat/src/public-api.ts @@ -55,6 +55,7 @@ export { ChatToolCallCardComponent } from './lib/compositions/chat-tool-call-car export type { ToolCallInfo } from './lib/compositions/chat-tool-call-card/chat-tool-call-card.component'; export { ChatSubagentCardComponent } from './lib/compositions/chat-subagent-card/chat-subagent-card.component'; export { ChatTimelineSliderComponent } from './lib/compositions/chat-timeline-slider/chat-timeline-slider.component'; +export { ChatDebugComponent } from './lib/compositions/chat-debug/chat-debug.component'; // Shared styles & utilities export { CHAT_THEME_STYLES } from './lib/styles/chat-theme'; diff --git a/libs/langgraph/src/lib/compositions/chat-debug/debug-utils.ts b/libs/langgraph/src/lib/compositions/chat-debug/debug-utils.ts deleted file mode 100644 index 5d830df63..000000000 --- a/libs/langgraph/src/lib/compositions/chat-debug/debug-utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 -import type { ThreadState } from '../../agent.types'; -import type { DebugCheckpoint } from './debug-checkpoint-card.component'; - -/** - * Derives a DebugCheckpoint from a ThreadState entry. - */ -export function toDebugCheckpoint(state: ThreadState, index: number): DebugCheckpoint { - const node = state.next?.[0] ?? `Step ${index + 1}`; - const checkpointId = state.checkpoint?.checkpoint_id ?? undefined; - return { node, checkpointId }; -} - -/** - * Extracts state values from a ThreadState, returning an empty object if unavailable. - */ -export function extractStateValues(state: ThreadState | undefined): Record { - if (!state) return {}; - const vals = state.values; - if (typeof vals === 'object' && vals !== null && !Array.isArray(vals)) { - return vals as Record; - } - return {}; -} diff --git a/libs/langgraph/src/public-api.ts b/libs/langgraph/src/public-api.ts index 4824ece70..7418e7ea4 100644 --- a/libs/langgraph/src/public-api.ts +++ b/libs/langgraph/src/public-api.ts @@ -30,20 +30,6 @@ export { toChatAgent } from './lib/to-chat-agent'; export { MockAgentTransport } from './lib/transport/mock-stream.transport'; export { FetchStreamTransport } from './lib/transport/fetch-stream.transport'; -// LangGraph-specific chat primitives (checkpoint_id / ThreadState / fork-replay UI) -export { ChatDebugComponent } from './lib/compositions/chat-debug/chat-debug.component'; -export { toDebugCheckpoint, extractStateValues } from './lib/compositions/chat-debug/debug-utils'; -export { DebugCheckpointCardComponent } from './lib/compositions/chat-debug/debug-checkpoint-card.component'; -export type { DebugCheckpoint } from './lib/compositions/chat-debug/debug-checkpoint-card.component'; -export { DebugStateInspectorComponent } from './lib/compositions/chat-debug/debug-state-inspector.component'; -export { DebugStateDiffComponent } from './lib/compositions/chat-debug/debug-state-diff.component'; -export { DebugTimelineComponent } from './lib/compositions/chat-debug/debug-timeline.component'; -export { DebugDetailComponent } from './lib/compositions/chat-debug/debug-detail.component'; -export { DebugControlsComponent } from './lib/compositions/chat-debug/debug-controls.component'; -export { DebugSummaryComponent } from './lib/compositions/chat-debug/debug-summary.component'; -export { computeStateDiff } from './lib/compositions/chat-debug/state-diff'; -export type { DiffEntry } from './lib/compositions/chat-debug/state-diff'; - // Mock test utility for LangGraph AgentRef export { createMockAgentRef } from './lib/testing/mock-agent-ref'; export type { MockAgentRef } from './lib/testing/mock-agent-ref'; From 54c81daf4f4fec4e6c240c97eab267e71e6588f9 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:44:01 -0700 Subject: [PATCH 10/12] refactor(cockpit): rebind chat-debug and chat-timeline demos to @cacheplane/chat Move ChatDebugComponent and ChatTimelineSliderComponent imports from @cacheplane/langgraph to @cacheplane/chat, and update consumer bindings to use [agent] instead of [ref] for consistency with Phase-2 primitives. Co-Authored-By: Claude Opus 4.7 --- cockpit/chat/debug/angular/src/app/debug.component.ts | 7 ++++--- .../chat/timeline/angular/src/app/timeline.component.ts | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cockpit/chat/debug/angular/src/app/debug.component.ts b/cockpit/chat/debug/angular/src/app/debug.component.ts index 1ca0f0e0f..c4f581fc1 100644 --- a/cockpit/chat/debug/angular/src/app/debug.component.ts +++ b/cockpit/chat/debug/angular/src/app/debug.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { Component } from '@angular/core'; -import { ChatDebugComponent } from '@cacheplane/langgraph'; -import { agent } from '@cacheplane/langgraph'; +import { ChatDebugComponent } from '@cacheplane/chat'; +import { agent, toChatAgent } from '@cacheplane/langgraph'; import { ExampleChatLayoutComponent } from '@cacheplane/example-layouts'; import { environment } from '../environments/environment'; @@ -16,7 +16,7 @@ import { environment } from '../environments/environment'; imports: [ChatDebugComponent, ExampleChatLayoutComponent], template: ` - + `, }) @@ -25,4 +25,5 @@ export class DebugPageComponent { apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + protected readonly chatAgent = toChatAgent(this.stream); } diff --git a/cockpit/chat/timeline/angular/src/app/timeline.component.ts b/cockpit/chat/timeline/angular/src/app/timeline.component.ts index d9e39f3fa..681f1cfae 100644 --- a/cockpit/chat/timeline/angular/src/app/timeline.component.ts +++ b/cockpit/chat/timeline/angular/src/app/timeline.component.ts @@ -1,8 +1,8 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { Component } from '@angular/core'; -import { ChatComponent } from '@cacheplane/chat'; +import { ChatComponent, ChatTimelineSliderComponent } from '@cacheplane/chat'; import { ExampleChatLayoutComponent } from '@cacheplane/example-layouts'; -import { agent, toChatAgent, ChatTimelineSliderComponent } from '@cacheplane/langgraph'; +import { agent, toChatAgent } from '@cacheplane/langgraph'; import { environment } from '../environments/environment'; /** @@ -20,7 +20,7 @@ import { environment } from '../environments/environment';

Timeline

- +

How It Works

From 21ac75208134ded57fbe1dbc82f52b0f176222fd Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 21:47:04 -0700 Subject: [PATCH 11/12] chore(chat): allow vitest in @nx/dependency-checks ignore list Conformance helpers (chat-agent-conformance, chat-agent-with-history- conformance) legitimately import from vitest. Adding to ignoredDependencies matches how vite/@nx/vite are already excluded. Co-Authored-By: Claude Opus 4.7 --- libs/chat/eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/chat/eslint.config.mjs b/libs/chat/eslint.config.mjs index b46547840..9ab73aa5b 100644 --- a/libs/chat/eslint.config.mjs +++ b/libs/chat/eslint.config.mjs @@ -10,7 +10,7 @@ export default [ 'error', { ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'], - ignoredDependencies: ['vite', '@nx/vite'], + ignoredDependencies: ['vite', '@nx/vite', 'vitest'], }, ], }, From 5d9b6148690f30a114a5e1a43c58cff3f8e05af5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 22 Apr 2026 08:14:50 -0700 Subject: [PATCH 12/12] chore(langgraph): drop unused @angular/common, @angular/platform-browser peer-deps After the chat-debug tree moved to @cacheplane/chat in Phase-2, @cacheplane/langgraph no longer consumes NgTemplateOutlet or DomSanitizer. @nx/dependency-checks correctly flagged both as unused. Co-Authored-By: Claude Opus 4.7 --- libs/langgraph/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/langgraph/package.json b/libs/langgraph/package.json index bcd934f54..a6eb6e897 100644 --- a/libs/langgraph/package.json +++ b/libs/langgraph/package.json @@ -5,8 +5,6 @@ "@cacheplane/chat": "^0.0.1", "@cacheplane/licensing": "^0.0.1", "@angular/core": "^20.0.0 || ^21.0.0", - "@angular/common": "^20.0.0 || ^21.0.0", - "@angular/platform-browser": "^20.0.0 || ^21.0.0", "@langchain/core": "^1.1.33", "@langchain/langgraph-sdk": "^1.7.4", "rxjs": "~7.8.0"