From 82eb87f1d991441f332ca21a6dba6f3531288691 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 11:55:12 -0700 Subject: [PATCH 01/12] fix(stream-resource): fix 3 runtime errors in streaming chat flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Guard Object.keys(v) against null/undefined in values$ subscriber (stream-resource.fn.ts:94) — crashed when values event had no data 2. Handle plain JSON messages from SSE (not hydrated BaseMessage instances) in getMessageType() — _getType() is a class method not available on plain objects; fall back to reading the `type` property 3. Fix event data extraction in processEvent — normalizeSdkEvent spreads data into the event object, so event['values'] was always undefined; use extractEventData() to read from event['data'] instead. Also sync messages$ from values events and merge messages/partial updates by id to preserve the full message history including human messages. --- .../src/lib/internals/stream-manager.bridge.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts index 0620a6a69..9d53086a9 100644 --- a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts +++ b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts @@ -219,23 +219,16 @@ export function createStreamManagerBridge 0 ? rest : d; } From 5a6768ecc4beb34e834673b9c7c26009b757f939 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:07:05 -0700 Subject: [PATCH 02/12] fix(stream-resource): handle both SDK and mock event formats in extractEventData --- .../src/lib/internals/stream-manager.bridge.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts index 9d53086a9..0620a6a69 100644 --- a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts +++ b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts @@ -219,16 +219,23 @@ export function createStreamManagerBridge 0 ? rest : d; } From 536351b17b9f6fd773be5c396d033578358ce759 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:09:40 -0700 Subject: [PATCH 03/12] docs(cockpit): add cockpit examples validation implementation plan --- .../2026-04-06-cockpit-examples-validation.md | 1053 +++++++++++++++++ 1 file changed, 1053 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-06-cockpit-examples-validation.md diff --git a/docs/superpowers/plans/2026-04-06-cockpit-examples-validation.md b/docs/superpowers/plans/2026-04-06-cockpit-examples-validation.md new file mode 100644 index 000000000..eb3ac7993 --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-cockpit-examples-validation.md @@ -0,0 +1,1053 @@ +# Cockpit Examples Validation & Sidebar 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:** Implement capability-specific sidebars for all 9 incomplete cockpit examples, validating that the chat and stream-resource libraries fully support every LangGraph and Deep Agent capability. Each example is a minimal, isolated Angular app that uses the libs — changes discovered here feed back into the libs. + +**Architecture:** Each example follows the pattern: `streamResource()` → `` + custom sidebar derived from `stream.value()` or `stream.messages()`. Sidebars are built directly in each example's component using Tailwind classes and the chat theme CSS vars. No new library components needed — the sidebars are example-specific UI, not reusable primitives. + +**Tech Stack:** Angular 20+, `@cacheplane/chat`, `@cacheplane/stream-resource`, Tailwind CSS v4 + +**Parallelism:** Tasks 1-4 (LangGraph examples) are independent. Tasks 5-8 (Deep Agent examples) are independent. All can run in parallel within their group. + +**Verification:** Each task ends with `npx nx build ` to verify the example compiles. Full E2E testing requires running the Python backend, which is out of scope — the focus is on Angular compilation and correct library usage. + +--- + +## File Structure + +Each task modifies ONE file — the example's main component. All sidebar UI is co-located in the component (small, focused, example-specific). + +### Modified files (9 examples needing sidebars) + +| Task | File | Sidebar Feature | +|------|------|----------------| +| 1 | `cockpit/langgraph/persistence/angular/src/app/persistence.component.ts` | Thread picker list | +| 2 | `cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts` | Checkpoint timeline | +| 3 | `cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts` | Step progress pipeline | +| 4 | `cockpit/deep-agents/planning/angular/src/app/planning.component.ts` | Plan step checklist | +| 5 | `cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts` | File operations log | +| 6 | `cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts` | Delegation tracker | +| 7 | `cockpit/deep-agents/memory/angular/src/app/memory.component.ts` | Learned facts sidebar | +| 8 | `cockpit/deep-agents/skills/angular/src/app/skills.component.ts` | Skill invocations log | +| 9 | `cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts` | Execution output log | + +### Already complete (5 examples — verify only) +- `cockpit/langgraph/streaming/angular/` — basic streaming ✓ +- `cockpit/langgraph/interrupts/angular/` — interrupt panel ✓ +- `cockpit/langgraph/memory/angular/` — facts sidebar ✓ +- `cockpit/langgraph/subgraphs/angular/` — subagent sidebar ✓ +- `cockpit/langgraph/deployment-runtime/angular/` — production config ✓ + +--- + +## Shared Sidebar Pattern + +Every sidebar follows this template. Subagents should reference this when implementing: + +```typescript +// Layout: chat (flex-1) + aside sidebar +template: ` +
+ + +
+` +``` + +Key rules: +- Import `ChatComponent` from `@cacheplane/chat` +- Use `streamResource()` from `@cacheplane/stream-resource` +- Derive sidebar state with `computed()` from `stream.value()` or `stream.messages()` +- Use Tailwind + chat theme CSS vars for styling +- The `` component handles all message rendering, input, typing, errors internally + +--- + +## Task 1: Persistence — Thread Picker Sidebar + +**Files:** +- Modify: `cockpit/langgraph/persistence/angular/src/app/persistence.component.ts` + +**Capability:** Thread-based conversation persistence. Users can create new threads and switch between them. + +- [ ] **Step 1: Read the current component** + +Read `cockpit/langgraph/persistence/angular/src/app/persistence.component.ts` to understand current state. + +- [ ] **Step 2: Implement thread picker sidebar** + +Replace the component with a layout that has chat + thread sidebar: + +```typescript +import { Component, signal, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +interface ThreadEntry { + id: string; + createdAt: string; +} + +@Component({ + selector: 'app-persistence', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class PersistenceComponent { + readonly activeThreadId = signal(''); + readonly threadList = signal([]); + + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.persistenceAssistantId, + onThreadId: (id: string) => { + this.activeThreadId.set(id); + if (!this.threadList().find(t => t.id === id)) { + this.threadList.update(list => [ + { id, createdAt: new Date().toLocaleTimeString() }, + ...list, + ]); + } + }, + }); + + readonly threads = computed(() => + this.threadList().map(t => ({ id: t.id })) + ); + + switchThread(threadId: string): void { + this.activeThreadId.set(threadId); + this.stream.switchThread(threadId); + } + + newThread(): void { + this.activeThreadId.set(''); + this.stream.switchThread(null); + } +} +``` + +- [ ] **Step 3: Build to verify compilation** + +```bash +npx nx build cockpit-langgraph-persistence-angular +``` + +Expected: Build succeeds. + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/langgraph/persistence/angular/src/app/persistence.component.ts +git commit -m "feat(cockpit): add thread picker sidebar to persistence example" +``` + +--- + +## Task 2: Time-Travel — Checkpoint Timeline Sidebar + +**Files:** +- Modify: `cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts` + +**Capability:** Navigate conversation history checkpoints, replay or fork from any point. + +- [ ] **Step 1: Read the current component** + +Read `cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts`. + +- [ ] **Step 2: Implement checkpoint timeline sidebar** + +```typescript +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +@Component({ + selector: 'app-time-travel', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class TimeTravelComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.timeTravelAssistantId, + }); + + readonly checkpoints = computed(() => this.stream.history()); + + replay(state: any): void { + if (state.checkpoint?.checkpoint_id) { + this.stream.setBranch(state.checkpoint.checkpoint_id); + } + } + + fork(state: any): void { + if (state.checkpoint?.checkpoint_id) { + this.stream.setBranch(state.checkpoint.checkpoint_id); + } + } +} +``` + +- [ ] **Step 3: Build to verify** + +```bash +npx nx build cockpit-langgraph-time-travel-angular +``` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts +git commit -m "feat(cockpit): add checkpoint timeline sidebar to time-travel example" +``` + +--- + +## Task 3: Durable Execution — Step Progress Sidebar + +**Files:** +- Modify: `cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts` + +**Capability:** Multi-step pipeline (analyze → plan → generate) with checkpoint recovery. + +- [ ] **Step 1: Read the current component** + +Read `cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts`. + +- [ ] **Step 2: Implement step progress sidebar** + +```typescript +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +const PIPELINE_STEPS = ['analyze', 'plan', 'generate'] as const; + +@Component({ + selector: 'app-durable-execution', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class DurableExecutionComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.durableExecutionAssistantId, + }); + + readonly steps = [...PIPELINE_STEPS]; + + private readonly currentStep = computed(() => { + const val = this.stream.value() as Record; + return (val?.['step'] as string) ?? ''; + }); + + stepStatus(step: string): 'pending' | 'active' | 'complete' { + const current = this.currentStep(); + if (!current) return 'pending'; + const currentIdx = this.steps.indexOf(current); + const stepIdx = this.steps.indexOf(step); + if (stepIdx < currentIdx) return 'complete'; + if (stepIdx === currentIdx) return this.stream.isLoading() ? 'active' : 'complete'; + return 'pending'; + } +} +``` + +- [ ] **Step 3: Build to verify** + +```bash +npx nx build cockpit-langgraph-durable-execution-angular +``` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts +git commit -m "feat(cockpit): add step progress sidebar to durable-execution example" +``` + +--- + +## Task 4: Planning — Plan Step Checklist Sidebar + +**Files:** +- Modify: `cockpit/deep-agents/planning/angular/src/app/planning.component.ts` + +**Capability:** Agent decomposes tasks into ordered steps, executes them sequentially. + +- [ ] **Step 1: Read the current component** + +Read `cockpit/deep-agents/planning/angular/src/app/planning.component.ts`. + +- [ ] **Step 2: Implement plan checklist sidebar** + +```typescript +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +interface PlanStep { + title: string; + status: 'pending' | 'in_progress' | 'complete'; +} + +@Component({ + selector: 'app-planning', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class PlanningComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.planningAssistantId, + }); + + readonly planSteps = computed((): PlanStep[] => { + const val = this.stream.value() as Record; + const plan = val?.['plan']; + if (!Array.isArray(plan)) return []; + return plan.map((s: any) => ({ + title: s.title ?? s.description ?? String(s), + status: s.status ?? 'pending', + })); + }); +} +``` + +- [ ] **Step 3: Build to verify** + +```bash +npx nx build cockpit-deep-agents-planning-angular +``` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/deep-agents/planning/angular/src/app/planning.component.ts +git commit -m "feat(cockpit): add plan checklist sidebar to planning example" +``` + +--- + +## Task 5: Filesystem — File Operations Log Sidebar + +**Files:** +- Modify: `cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts` + +**Capability:** Agent reads/writes files via tool calls. + +- [ ] **Step 1: Read the current component** + +Read `cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts`. + +- [ ] **Step 2: Implement file operations sidebar** + +Derive file operations from `stream.messages()` by filtering for tool call messages with `read_file` or `write_file` names. + +```typescript +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +interface FileOp { + operation: string; + path: string; + result: string; +} + +@Component({ + selector: 'app-filesystem', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class FilesystemComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.filesystemAssistantId, + }); + + readonly fileOps = computed((): FileOp[] => { + const messages = this.stream.messages(); + const ops: FileOp[] = []; + for (const msg of messages) { + const tc = (msg as any).tool_calls; + if (Array.isArray(tc)) { + for (const call of tc) { + if (call.name === 'read_file' || call.name === 'write_file') { + ops.push({ + operation: call.name.replace('_', ' '), + path: call.args?.path ?? call.args?.file_path ?? '(unknown)', + result: '', + }); + } + } + } + // Tool result messages have name + content + if ((msg as any).type === 'tool' && (msg as any).name) { + const lastOp = ops[ops.length - 1]; + if (lastOp) { + lastOp.result = typeof (msg as any).content === 'string' + ? (msg as any).content.substring(0, 100) + : ''; + } + } + } + return ops; + }); +} +``` + +- [ ] **Step 3: Build to verify** + +```bash +npx nx build cockpit-deep-agents-filesystem-angular +``` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts +git commit -m "feat(cockpit): add file operations sidebar to filesystem example" +``` + +--- + +## Task 6: Subagents (DA) — Delegation Tracker Sidebar + +**Files:** +- Modify: `cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts` + +**Capability:** Orchestrator delegates to specialist subagents. + +- [ ] **Step 1: Read the current component** + +Read `cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts`. + +- [ ] **Step 2: Implement delegation tracker sidebar** + +```typescript +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +interface Delegation { + agent: string; + status: string; +} + +@Component({ + selector: 'app-subagents', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class SubagentsComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.subagentsAssistantId, + }); + + readonly delegations = computed((): Delegation[] => { + const messages = this.stream.messages(); + const delegations: Delegation[] = []; + for (const msg of messages) { + const tc = (msg as any).tool_calls; + if (Array.isArray(tc)) { + for (const call of tc) { + delegations.push({ + agent: call.name?.replace(/_/g, ' ') ?? 'unknown', + status: 'running', + }); + } + } + if ((msg as any).type === 'tool') { + const last = delegations[delegations.length - 1]; + if (last) last.status = 'complete'; + } + } + return delegations; + }); +} +``` + +- [ ] **Step 3: Build to verify** + +```bash +npx nx build cockpit-deep-agents-subagents-angular +``` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts +git commit -m "feat(cockpit): add delegation tracker sidebar to subagents example" +``` + +--- + +## Task 7: Memory (DA) — Learned Facts Sidebar + +**Files:** +- Modify: `cockpit/deep-agents/memory/angular/src/app/memory.component.ts` + +**Capability:** Agent extracts and remembers facts from conversations. + +- [ ] **Step 1: Read the current component** + +Read `cockpit/deep-agents/memory/angular/src/app/memory.component.ts`. + +- [ ] **Step 2: Implement learned facts sidebar** + +Same pattern as the LangGraph memory example but using `agent_memory` state field: + +```typescript +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +@Component({ + selector: 'app-memory', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class MemoryComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.memoryAssistantId, + }); + + readonly memoryEntries = computed((): [string, string][] => { + const val = this.stream.value() as Record; + const memory = val?.['agent_memory'] ?? val?.['memory']; + if (!memory || typeof memory !== 'object') return []; + return Object.entries(memory as Record); + }); +} +``` + +- [ ] **Step 3: Build to verify** + +```bash +npx nx build cockpit-deep-agents-memory-angular +``` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/deep-agents/memory/angular/src/app/memory.component.ts +git commit -m "feat(cockpit): add learned facts sidebar to memory example" +``` + +--- + +## Task 8: Skills — Skill Invocations Log Sidebar + +**Files:** +- Modify: `cockpit/deep-agents/skills/angular/src/app/skills.component.ts` + +**Capability:** Agent uses multiple skills (calculator, word_count, summarize). + +- [ ] **Step 1: Read the current component** + +Read `cockpit/deep-agents/skills/angular/src/app/skills.component.ts`. + +- [ ] **Step 2: Implement skill invocations sidebar** + +```typescript +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +interface SkillInvocation { + name: string; + input: string; + output: string; +} + +@Component({ + selector: 'app-skills', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class SkillsComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.skillsAssistantId, + }); + + readonly skillInvocations = computed((): SkillInvocation[] => { + const messages = this.stream.messages(); + const invocations: SkillInvocation[] = []; + for (const msg of messages) { + const tc = (msg as any).tool_calls; + if (Array.isArray(tc)) { + for (const call of tc) { + invocations.push({ + name: call.name ?? 'unknown', + input: JSON.stringify(call.args ?? {}).substring(0, 60), + output: '', + }); + } + } + if ((msg as any).type === 'tool') { + const last = invocations[invocations.length - 1]; + if (last) { + last.output = typeof (msg as any).content === 'string' + ? (msg as any).content.substring(0, 60) + : ''; + } + } + } + return invocations; + }); +} +``` + +- [ ] **Step 3: Build to verify** + +```bash +npx nx build cockpit-deep-agents-skills-angular +``` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/deep-agents/skills/angular/src/app/skills.component.ts +git commit -m "feat(cockpit): add skill invocations sidebar to skills example" +``` + +--- + +## Task 9: Sandboxes — Execution Output Log Sidebar + +**Files:** +- Modify: `cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts` + +**Capability:** Agent generates and executes Python code. + +- [ ] **Step 1: Read the current component** + +Read `cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts`. + +- [ ] **Step 2: Implement execution output sidebar** + +```typescript +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +interface CodeExecution { + code: string; + stdout: string; + stderr: string; + exitStatus: number; +} + +@Component({ + selector: 'app-sandboxes', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class SandboxesComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.sandboxesAssistantId, + }); + + readonly executions = computed((): CodeExecution[] => { + const messages = this.stream.messages(); + const execs: CodeExecution[] = []; + for (const msg of messages) { + const tc = (msg as any).tool_calls; + if (Array.isArray(tc)) { + for (const call of tc) { + if (call.name === 'run_code') { + execs.push({ + code: call.args?.code ?? '', + stdout: '', + stderr: '', + exitStatus: 0, + }); + } + } + } + if ((msg as any).type === 'tool' && (msg as any).name === 'run_code') { + const last = execs[execs.length - 1]; + if (last) { + try { + const result = JSON.parse((msg as any).content); + last.stdout = result.stdout ?? ''; + last.stderr = result.stderr ?? ''; + last.exitStatus = result.exit_status ?? 0; + } catch { + last.stdout = typeof (msg as any).content === 'string' ? (msg as any).content : ''; + } + } + } + } + return execs; + }); +} +``` + +- [ ] **Step 3: Build to verify** + +```bash +npx nx build cockpit-deep-agents-sandboxes-angular +``` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts +git commit -m "feat(cockpit): add execution output sidebar to sandboxes example" +``` + +--- + +## Task 10: Build All & Final Verification + +- [ ] **Step 1: Build all 14 cockpit examples** + +```bash +npx nx run-many -t build --projects='cockpit-*-angular' +``` + +Expected: All 14 examples build successfully. + +- [ ] **Step 2: Run library tests** + +```bash +npx nx test chat && npx nx test render && npx nx test stream-resource +``` + +Expected: All tests pass. + +- [ ] **Step 3: Commit any final fixes** + +If any builds fail, fix the compilation errors and commit. + +- [ ] **Step 4: Final commit** + +```bash +git add -A +git commit -m "feat(cockpit): complete sidebar implementations for all 14 capability examples" +``` From d61a935da1aad88aeeea08003b770854bd2748c6 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:15:17 -0700 Subject: [PATCH 04/12] feat(cockpit): add thread picker sidebar to persistence example Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/persistence.component.ts | 105 ++++++++++++++++-- 1 file changed, 96 insertions(+), 9 deletions(-) diff --git a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts index dabe6861c..c140127f7 100644 --- a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts +++ b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts @@ -1,28 +1,90 @@ -import { Component } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; +interface Thread { + id: string; + label: string; +} + /** * PersistenceComponent demonstrates thread persistence with `streamResource()`. * - * This example shows how conversations persist across browser refreshes. - * Each thread has a unique ID that can be stored and resumed later. - * Use `stream.switchThread(id)` to load a previous conversation, - * or `stream.switchThread(null)` to start fresh. + * Layout: a full-height flex row with the `` area (flex-1) on the left + * and a fixed-width thread-picker sidebar on the right. * * Key integration points: - * - `onThreadId` callback captures new thread IDs for storage - * - `stream.switchThread(id)` resumes a previous conversation - * - `stream.messages()` loads the full history when switching threads + * - `onThreadId` callback captures new thread IDs for the sidebar list + * - `switchThread(id)` resumes a previous conversation + * - `newThread()` starts a fresh conversation */ @Component({ selector: 'app-persistence', standalone: true, imports: [ChatComponent], - template: ``, + styles: ` + :host { + display: flex; + height: 100vh; + } + `, + template: ` + + + + `, }) export class PersistenceComponent { + protected readonly threads = signal([]); + protected readonly activeThreadId = signal(null); + + private threadCounter = 0; + /** * The streaming resource with thread persistence. * @@ -32,5 +94,30 @@ export class PersistenceComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, + onThreadId: (id: string) => { + this.activeThreadId.set(id); + + // Only add if not already tracked + const existing = this.threads(); + if (!existing.some((t) => t.id === id)) { + this.threadCounter++; + this.threads.set([ + ...existing, + { id, label: `Thread ${this.threadCounter}` }, + ]); + } + }, }); + + /** Switch to an existing thread by ID. */ + switchThread(id: string): void { + this.activeThreadId.set(id); + this.stream.switchThread(id); + } + + /** Start a brand-new thread. */ + newThread(): void { + this.activeThreadId.set(null); + this.stream.switchThread(null); + } } From 1384afe48b887801a2f5d873241a74f07c3520c1 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:15:25 -0700 Subject: [PATCH 05/12] feat(cockpit): add checkpoint timeline sidebar to time-travel example Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/time-travel.component.ts | 122 +++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts b/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts index 96f92c31c..aab762004 100644 --- a/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts +++ b/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts @@ -1,11 +1,14 @@ -import { Component } from '@angular/core'; +import { Component, computed, signal } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; +import type { ThreadState } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; /** * TimeTravelComponent demonstrates replaying and branching conversation history. * + * Layout: chat panel (flex-1) + checkpoint timeline sidebar (w-72). + * * Key integration points: * - `stream.history()` -- array of ThreadState snapshots * - `stream.branch()` -- current branch identifier @@ -15,11 +18,126 @@ import { environment } from '../environments/environment'; selector: 'app-time-travel', standalone: true, imports: [ChatComponent], - template: ``, + template: ` +
+ + + + + +
+ `, }) export class TimeTravelComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + + /** Index of the currently selected checkpoint in the sidebar. */ + protected readonly selectedIndex = signal(-1); + + /** Checkpoint history derived from the stream resource. */ + protected readonly checkpoints = computed( + (): ThreadState[] => this.stream.history(), + ); + + /** Display label for a checkpoint entry. */ + protected checkpointLabel( + state: ThreadState, + index: number, + ): string { + if (state.checkpoint?.checkpoint_id) { + return `Checkpoint ${index + 1}`; + } + return `State ${index + 1}`; + } + + /** Replay the conversation from the given checkpoint. */ + protected replay(state: ThreadState, index: number): void { + if (state.checkpoint?.checkpoint_id) { + this.selectedIndex.set(index); + this.stream.setBranch(state.checkpoint.checkpoint_id); + } + } + + /** Fork the conversation from the given checkpoint. */ + protected fork(state: ThreadState, index: number): void { + if (state.checkpoint?.checkpoint_id) { + this.selectedIndex.set(index); + this.stream.setBranch(state.checkpoint.checkpoint_id); + } + } } From a3a5231b4e6cb49329d4300fcc9d24a672c31acd Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:15:46 -0700 Subject: [PATCH 06/12] feat(cockpit): add step progress sidebar to durable-execution example Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/app/durable-execution.component.ts | 97 ++++++++++++++++++- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts b/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts index 5e692ca39..a20bb86b8 100644 --- a/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts +++ b/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts @@ -1,8 +1,27 @@ -import { Component } from '@angular/core'; +import { Component, computed } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; +/** + * Pipeline step definition for the vertical progress indicator. + */ +interface PipelineStep { + id: string; + label: string; + status: 'pending' | 'active' | 'complete'; +} + +/** Ordered node names emitted by the Python graph's `state.step` field. */ +const STEP_ORDER = ['analyze', 'plan', 'generate'] as const; + +/** Human-readable labels for each pipeline step. */ +const STEP_LABELS: Record = { + analyze: 'Analyze', + plan: 'Plan', + generate: 'Generate', +}; + /** * DurableExecutionComponent demonstrates fault-tolerant multi-step execution * with `streamResource()`. @@ -10,17 +29,89 @@ import { environment } from '../environments/environment'; * This example shows how a graph checkpoints at each node, enabling it to * resume after failures. The backend processes each request through three * nodes: analyze, plan, generate. Each node updates `state.step` so the - * UI can track progress. + * UI can track progress via a vertical step indicator in the sidebar. */ @Component({ selector: 'app-durable-execution', standalone: true, imports: [ChatComponent], - template: ``, + template: ` +
+ + +
+ `, }) export class DurableExecutionComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + + /** + * Derives the 3-step pipeline status from the graph's `state.step` field. + * + * Steps before the current one are marked complete, the current step is + * active, and subsequent steps remain pending. + */ + protected readonly steps = computed(() => { + const val = this.stream.value() as Record | undefined; + const currentStep = (val?.['step'] as string) ?? ''; + const activeIndex = STEP_ORDER.indexOf(currentStep as (typeof STEP_ORDER)[number]); + + return STEP_ORDER.map((id, i) => ({ + id, + label: STEP_LABELS[id], + status: activeIndex < 0 ? 'pending' : i < activeIndex ? 'complete' : i === activeIndex ? 'active' : 'pending', + })); + }); } From 73cf1544674b7647c715e0a62d72fee25d7a064d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:15:56 -0700 Subject: [PATCH 07/12] feat(cockpit): add plan checklist sidebar to planning example Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/planning.component.ts | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/cockpit/deep-agents/planning/angular/src/app/planning.component.ts b/cockpit/deep-agents/planning/angular/src/app/planning.component.ts index 53c2b6b04..790565512 100644 --- a/cockpit/deep-agents/planning/angular/src/app/planning.component.ts +++ b/cockpit/deep-agents/planning/angular/src/app/planning.component.ts @@ -1,23 +1,89 @@ -import { Component } from '@angular/core'; +import { Component, computed } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; +/** + * Represents a single step in an agent-generated plan. + */ +interface PlanStep { + title: string; + status: 'pending' | 'in_progress' | 'complete'; +} + /** * PlanningComponent demonstrates agent task decomposition. * * The agent receives a complex task, breaks it into ordered steps, - * and executes them sequentially. + * and executes them sequentially. The sidebar displays a live checklist + * of plan steps derived from the `plan` array in the graph state. + * + * Key integration points: + * - `stream.value()` exposes the full graph state, including the `plan` array + * - `planSteps` is derived from `stream.value()` for reactive sidebar rendering + * - Step status icons update in real time as the agent progresses */ @Component({ selector: 'app-planning', standalone: true, imports: [ChatComponent], - template: ``, + template: ` +
+ + +
+ `, }) export class PlanningComponent { + /** + * The streaming resource connected to the planning graph. + * + * The graph returns a `plan` array alongside messages in its state. + * Each plan entry has a `title` and `status` that drive the sidebar checklist. + */ protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + + /** + * Reactive list of plan steps derived from the graph state. + * + * The Python graph stores the plan as `state.plan` — an array of objects + * with `title` and `status` fields. This signal re-computes whenever + * the stream state changes. + */ + protected readonly planSteps = computed(() => { + const val = this.stream.value() as Record; + const plan = val?.['plan']; + if (!Array.isArray(plan)) return []; + return plan.map((step: Record) => ({ + title: String(step['title'] ?? ''), + status: (['pending', 'in_progress', 'complete'].includes(step['status'] as string) + ? step['status'] + : 'pending') as PlanStep['status'], + })); + }); } From ff9ff6cd61ea2c044a2e91fc1b05717186da460c Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:16:10 -0700 Subject: [PATCH 08/12] feat(cockpit): add skill invocations sidebar to skills example Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/skills.component.ts | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/cockpit/deep-agents/skills/angular/src/app/skills.component.ts b/cockpit/deep-agents/skills/angular/src/app/skills.component.ts index f194e2185..7d43b6180 100644 --- a/cockpit/deep-agents/skills/angular/src/app/skills.component.ts +++ b/cockpit/deep-agents/skills/angular/src/app/skills.component.ts @@ -1,23 +1,131 @@ -import { Component } from '@angular/core'; +import { Component, computed } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; +/** + * Represents a matched skill invocation: tool call paired with its result. + */ +interface SkillInvocation { + id: string; + name: string; + args: Record; + result: string | undefined; +} + /** * SkillsComponent demonstrates a multi-skill agent with specialized tools. * * The agent can calculate math expressions, count words, and summarize text * by selecting the appropriate skill tool for each user request. + * + * The sidebar displays a real-time log of skill invocations derived from + * `stream.messages()`, matching AI tool_calls with their corresponding + * tool result messages. */ @Component({ selector: 'app-skills', standalone: true, imports: [ChatComponent], - template: ``, + template: ` +
+ + +
+ `, }) export class SkillsComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + + private readonly SKILL_NAMES = new Set(['calculator', 'word_count', 'summarize']); + + /** + * Derived signal: extracts skill invocations from the message stream. + * + * Scans AI messages for tool_calls matching known skill names, then pairs + * each with its corresponding tool result message via tool_call_id. + */ + protected readonly invocations = computed(() => { + const msgs = this.stream.messages(); + const resultMap = new Map(); + + // Build a lookup of tool_call_id -> result content from tool messages + for (const msg of msgs) { + const type = typeof msg._getType === 'function' + ? msg._getType() + : (msg as unknown as Record)['type'] ?? ''; + if (type === 'tool') { + const toolCallId = (msg as unknown as Record)['tool_call_id']; + const content = typeof msg.content === 'string' + ? msg.content + : JSON.stringify(msg.content); + if (toolCallId) { + resultMap.set(toolCallId, content); + } + } + } + + // Extract tool_calls from AI messages that match known skill names + const invocations: SkillInvocation[] = []; + for (const msg of msgs) { + const type = typeof msg._getType === 'function' + ? msg._getType() + : (msg as unknown as Record)['type'] ?? ''; + if (type === 'ai') { + const toolCalls = (msg as unknown as Record)['tool_calls']; + if (Array.isArray(toolCalls)) { + for (const tc of toolCalls) { + const call = tc as { id: string; name: string; args: Record }; + if (this.SKILL_NAMES.has(call.name)) { + invocations.push({ + id: call.id, + name: call.name, + args: call.args, + result: resultMap.get(call.id), + }); + } + } + } + } + } + + return invocations; + }); + + protected formatArgs(args: Record): string { + return JSON.stringify(args, null, 2); + } } From 1bf369ecd8b07a9e84c9a152eacff970bc4dc0dd Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:16:19 -0700 Subject: [PATCH 09/12] feat(cockpit): add delegation tracker sidebar to subagents example Derive delegations from stream.messages() by finding tool_calls in AI messages and matching them with tool result messages. Each delegation shows a status dot (green=complete, amber=running, red=error), agent name, and status text. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/subagents.component.ts | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts b/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts index e7bf766a7..cd07b6e35 100644 --- a/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts +++ b/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts @@ -1,23 +1,112 @@ -import { Component } from '@angular/core'; +import { Component, computed } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; +/** + * Delegation status derived from matching tool calls with tool result messages. + */ +interface Delegation { + /** Tool call ID used to track request/response pairing. */ + id: string; + /** Name of the delegated agent (tool name). */ + agent: string; + /** Execution status: running until a matching tool result arrives. */ + status: 'running' | 'complete' | 'error'; + /** Human-readable status text. */ + statusText: string; +} + /** * SubagentsComponent demonstrates the Deep Agents subagent delegation pattern. * * The orchestrator agent receives a task and delegates subtasks to specialist - * subagents via tool calls. + * subagents via tool calls. The sidebar tracks each delegation by scanning + * `stream.messages()` for AI tool_calls and matching ToolMessage results. */ @Component({ selector: 'app-subagents', standalone: true, imports: [ChatComponent], - template: ``, + template: ` +
+ + +
+ `, }) export class SubagentsComponent { + /** + * The streaming resource connected to the subagents orchestrator graph. + */ protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + + /** + * Reactive delegation list derived from messages. + * + * Scans all messages for AI tool_calls, then checks for matching + * ToolMessage results (by tool_call_id) to determine completion status. + */ + protected readonly delegations = computed(() => { + const msgs = this.stream.messages(); + const toolResultIds = new Set(); + const errorResultIds = new Set(); + + // Collect all tool result message IDs and detect errors + for (const msg of msgs) { + const type = typeof msg._getType === 'function' ? msg._getType() : (msg as any).type; + if (type === 'tool') { + const toolCallId = (msg as any).tool_call_id; + if (toolCallId) { + toolResultIds.add(toolCallId); + const status = (msg as any).status; + if (status === 'error') { + errorResultIds.add(toolCallId); + } + } + } + } + + // Extract tool calls from AI messages and match with results + const delegations: Delegation[] = []; + for (const msg of msgs) { + const type = typeof msg._getType === 'function' ? msg._getType() : (msg as any).type; + if (type === 'ai') { + const toolCalls = (msg as any).tool_calls as Array<{ id: string; name: string }> | undefined; + if (toolCalls?.length) { + for (const tc of toolCalls) { + const isError = errorResultIds.has(tc.id); + const isComplete = toolResultIds.has(tc.id); + delegations.push({ + id: tc.id, + agent: tc.name, + status: isError ? 'error' : isComplete ? 'complete' : 'running', + statusText: isError ? 'error' : isComplete ? 'done' : 'running', + }); + } + } + } + } + + return delegations; + }); } From 36052819f2ca90f820baeb2e862bbe5a7faf9964 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:16:52 -0700 Subject: [PATCH 10/12] feat(cockpit): add file operations sidebar to filesystem example Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/filesystem.component.ts | 84 ++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts b/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts index 84935a841..2a6d924f2 100644 --- a/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts +++ b/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts @@ -1,22 +1,100 @@ -import { Component } from '@angular/core'; +import { Component, computed } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; +/** + * Represents a file operation extracted from agent tool calls. + */ +interface FileOperation { + type: 'read' | 'write'; + path: string; + preview: string; +} + /** * FilesystemComponent demonstrates agent file operations. * - * The agent can read and write files using tool calls. + * The agent can read and write files using tool calls. The sidebar displays + * a live log of file operations derived from `stream.messages()`, filtering + * for `read_file` and `write_file` tool calls. + * + * Key integration points: + * - `stream.messages()` exposes the full message history including tool calls + * - `fileOps` filters messages for file-related tool calls and extracts metadata + * - Operations appear in the sidebar in real time as the agent works */ @Component({ selector: 'app-filesystem', standalone: true, imports: [ChatComponent], - template: ``, + template: ` +
+ + +
+ `, }) export class FilesystemComponent { + /** + * The streaming resource connected to the filesystem graph. + * + * The graph uses `read_file` and `write_file` tool calls that appear + * in `stream.messages()`. We filter and display them in the sidebar. + */ protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + + /** + * Reactive list of file operations derived from the message history. + * + * Scans all messages for tool calls named `read_file` or `write_file`, + * extracts the file path and a short result preview for sidebar display. + */ + protected readonly fileOps = computed(() => { + const msgs = this.stream.messages(); + const ops: FileOperation[] = []; + for (const msg of msgs) { + const m = msg as unknown as Record; + if (!('tool_calls' in m) || !Array.isArray(m['tool_calls'])) continue; + for (const tc of m['tool_calls'] as Array>) { + const name = tc['name'] as string | undefined; + if (name !== 'read_file' && name !== 'write_file') continue; + const args = (tc['args'] ?? {}) as Record; + const path = String(args['path'] ?? args['file_path'] ?? ''); + const content = String(args['content'] ?? args['result'] ?? ''); + ops.push({ + type: name === 'read_file' ? 'read' : 'write', + path, + preview: content.slice(0, 80), + }); + } + } + return ops; + }); } From 0b8d70ea6878010c472eda452e6ce04af7b43448 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:16:53 -0700 Subject: [PATCH 11/12] feat(cockpit): add execution output sidebar to sandboxes example Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/sandboxes.component.ts | 112 +++++++++++++++++- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts b/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts index cfcfec045..f86b65a67 100644 --- a/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts +++ b/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts @@ -1,23 +1,129 @@ -import { Component } from '@angular/core'; +import { Component, computed } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; +/** + * Represents a parsed code execution: the code that was run and its output. + */ +interface CodeExecution { + id: string; + code: string; + stdout: string; + stderr: string; + exitStatus: number; +} + /** * SandboxesComponent demonstrates a coding agent that executes Python code. * * The agent writes and runs code snippets to solve problems using a - * `run_code` tool. + * `run_code` tool. The sidebar displays a real-time log of code executions + * derived from `stream.messages()`, showing the code, stdout, and stderr + * for each invocation. */ @Component({ selector: 'app-sandboxes', standalone: true, imports: [ChatComponent], - template: ``, + template: ` +
+ + +
+ `, }) export class SandboxesComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + + /** + * Derived signal: extracts code executions from the message stream. + * + * Scans AI messages for tool_calls with name `run_code`, then pairs each + * with its corresponding tool result message. Tool results are parsed as + * JSON with {stdout, stderr, exit_status} fields. + */ + protected readonly executions = computed(() => { + const msgs = this.stream.messages(); + const resultMap = new Map(); + + // Build a lookup of tool_call_id -> parsed result from tool messages + for (const msg of msgs) { + const type = typeof msg._getType === 'function' + ? msg._getType() + : (msg as unknown as Record)['type'] ?? ''; + if (type === 'tool') { + const toolCallId = (msg as unknown as Record)['tool_call_id']; + if (toolCallId) { + const raw = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content); + try { + const parsed = JSON.parse(raw); + resultMap.set(toolCallId, { + stdout: parsed.stdout ?? '', + stderr: parsed.stderr ?? '', + exitStatus: parsed.exit_status ?? 0, + }); + } catch { + resultMap.set(toolCallId, { stdout: raw, stderr: '', exitStatus: 0 }); + } + } + } + } + + // Extract run_code tool_calls from AI messages + const executions: CodeExecution[] = []; + for (const msg of msgs) { + const type = typeof msg._getType === 'function' + ? msg._getType() + : (msg as unknown as Record)['type'] ?? ''; + if (type === 'ai') { + const toolCalls = (msg as unknown as Record)['tool_calls']; + if (Array.isArray(toolCalls)) { + for (const tc of toolCalls) { + const call = tc as { id: string; name: string; args: Record }; + if (call.name === 'run_code') { + const result = resultMap.get(call.id); + executions.push({ + id: call.id, + code: (call.args['code'] as string) ?? '', + stdout: result?.stdout ?? '', + stderr: result?.stderr ?? '', + exitStatus: result?.exitStatus ?? 0, + }); + } + } + } + } + } + + return executions; + }); } From d637e99d6c72bee6ee14ee4ecf5ea03b02e7fb2a Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:16:58 -0700 Subject: [PATCH 12/12] feat(cockpit): add learned facts sidebar to memory example Derive memory entries from stream.value() by checking for agent_memory or memory dict fields in graph state. Sidebar displays each fact as a bold key with muted value text, plus a count in the header. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/memory.component.ts | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/cockpit/deep-agents/memory/angular/src/app/memory.component.ts b/cockpit/deep-agents/memory/angular/src/app/memory.component.ts index 035fcfd8f..01f230248 100644 --- a/cockpit/deep-agents/memory/angular/src/app/memory.component.ts +++ b/cockpit/deep-agents/memory/angular/src/app/memory.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, computed } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -7,17 +7,65 @@ import { environment } from '../environments/environment'; * MemoryComponent demonstrates persistent agent memory across sessions. * * The agent extracts facts about the user from each conversation turn - * and stores them in `agent_memory` state. + * and stores them in `agent_memory` (or `memory`) state. The sidebar + * displays all learned facts as key-value pairs with a live count. + * + * Key integration points: + * - `stream.value()` exposes the full graph state including the memory dict + * - `memoryEntries` is derived reactively for sidebar rendering + * - Facts appear as the agent learns them during conversation */ @Component({ selector: 'app-da-memory', standalone: true, imports: [ChatComponent], - template: ``, + template: ` +
+ + +
+ `, }) export class MemoryComponent { + /** + * The streaming resource connected to the memory graph. + * + * The graph returns an `agent_memory` (or `memory`) dict alongside messages + * in its state. We derive a reactive signal from `stream.value()` for display. + */ protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); + + /** + * Reactive list of [key, value] memory entries derived from the graph state. + * + * Checks for `agent_memory` first, then falls back to `memory`. + * This signal re-computes whenever the stream state changes. + */ + protected readonly memoryEntries = computed(() => { + const val = this.stream.value() as Record; + const mem = val?.['agent_memory'] ?? val?.['memory']; + if (!mem || typeof mem !== 'object') return []; + return Object.entries(mem as Record); + }); }