From a68e8bd8525affa444ba4c8d76dd45712c1baa31 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sun, 5 Apr 2026 18:22:47 -0700 Subject: [PATCH] =?UTF-8?q?fix(cockpit):=20production=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20empty=20states,=20typed=20APIs,=20broken=20interpol?= =?UTF-8?q?ations,=20learner-friendly=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../angular/src/app/filesystem.component.ts | 39 ++++----- .../angular/src/app/memory.component.ts | 27 ++++--- .../angular/src/app/planning.component.ts | 39 ++++----- .../angular/src/app/sandboxes.component.ts | 80 ++++++++++--------- .../angular/src/app/skills.component.ts | 40 +++++----- .../angular/src/app/subagents.component.ts | 37 ++++----- .../src/app/deployment-runtime.component.ts | 42 +++++----- .../src/app/durable-execution.component.ts | 12 +++ .../angular/src/app/interrupts.component.ts | 23 +++--- .../angular/src/app/persistence.component.ts | 14 ++++ .../angular/src/app/time-travel.component.ts | 25 +++++- 11 files changed, 216 insertions(+), 162 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 49a4c818a..1a3748729 100644 --- a/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts +++ b/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts @@ -2,6 +2,7 @@ import { Component, computed } from '@angular/core'; import { ChatDebugComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; +import { AIMessage } from '@langchain/core/messages'; import { environment } from '../environments/environment'; @Component({ @@ -11,20 +12,21 @@ import { environment } from '../environments/environment'; template: `
- @if (fileOps().length > 0) { - - } +
`, }) @@ -38,11 +40,10 @@ export class FilesystemComponent { const messages = this.stream.messages(); const ops: { name: string; path: string }[] = []; for (const msg of messages) { - if ('tool_calls' in msg && Array.isArray((msg as any).tool_calls)) { - for (const tc of (msg as any).tool_calls) { - if (tc.name === 'read_file' || tc.name === 'write_file') { - ops.push({ name: tc.name, path: tc.args?.path ?? '' }); - } + if (!(msg instanceof AIMessage)) continue; + for (const tc of this.stream.getToolCalls(msg)) { + if (tc.call.name === 'read_file' || tc.call.name === 'write_file') { + ops.push({ name: tc.call.name, path: (tc.call.args as Record)?.['path'] ?? '' }); } } } 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 150ad8a86..279f044d8 100644 --- a/cockpit/deep-agents/memory/angular/src/app/memory.component.ts +++ b/cockpit/deep-agents/memory/angular/src/app/memory.component.ts @@ -11,19 +11,20 @@ import { environment } from '../environments/environment'; template: `
- @if (memoryEntries().length > 0) { - - } +
`, }) 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 bd64e74a7..deb555a67 100644 --- a/cockpit/deep-agents/planning/angular/src/app/planning.component.ts +++ b/cockpit/deep-agents/planning/angular/src/app/planning.component.ts @@ -11,25 +11,26 @@ import { environment } from '../environments/environment'; template: `
- @if (planSteps().length > 0) { - - } +
`, }) 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 ad222836f..458a7cd56 100644 --- a/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts +++ b/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts @@ -2,6 +2,7 @@ import { Component, computed } from '@angular/core'; import { ChatDebugComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; +import { AIMessage } from '@langchain/core/messages'; import { environment } from '../environments/environment'; @Component({ @@ -11,32 +12,34 @@ import { environment } from '../environments/environment'; template: `
- @if (execLogs().length > 0) { -
`, }) @@ -50,21 +53,20 @@ export class SandboxesComponent { const messages = this.stream.messages(); const logs: { code: string; stdout: string; exitStatus: number }[] = []; for (const msg of messages) { - if ('tool_calls' in msg && Array.isArray((msg as any).tool_calls)) { - for (const tc of (msg as any).tool_calls) { - if (tc.name === 'run_code') { - const resultIdx = messages.indexOf(msg) + 1; - const resultMsg = messages[resultIdx]; - let stdout = '', exitStatus = 0; - if (resultMsg && typeof resultMsg.content === 'string') { - try { - const parsed = JSON.parse(resultMsg.content); - stdout = parsed.stdout ?? ''; - exitStatus = parsed.exit_status ?? 0; - } catch { /* ignore */ } - } - logs.push({ code: tc.args?.code ?? '', stdout, exitStatus }); + if (!(msg instanceof AIMessage)) continue; + for (const tc of this.stream.getToolCalls(msg)) { + if (tc.call.name === 'run_code') { + const resultIdx = messages.indexOf(msg) + 1; + const resultMsg = messages[resultIdx]; + let stdout = '', exitStatus = 0; + if (resultMsg && typeof resultMsg.content === 'string') { + try { + const parsed = JSON.parse(resultMsg.content); + stdout = parsed.stdout ?? ''; + exitStatus = parsed.exit_status ?? 0; + } catch { /* ignore */ } } + logs.push({ code: (tc.call.args as Record)?.['code'] ?? '', stdout, exitStatus }); } } } 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 5853f5364..59e2e0f2f 100644 --- a/cockpit/deep-agents/skills/angular/src/app/skills.component.ts +++ b/cockpit/deep-agents/skills/angular/src/app/skills.component.ts @@ -2,6 +2,7 @@ import { Component, computed } from '@angular/core'; import { ChatDebugComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; +import { AIMessage } from '@langchain/core/messages'; import { environment } from '../environments/environment'; const SKILL_ICONS: Record = { @@ -17,20 +18,21 @@ const SKILL_ICONS: Record = { template: `
- @if (skillInvocations().length > 0) { - - } +
`, }) @@ -44,11 +46,11 @@ export class SkillsComponent { const messages = this.stream.messages(); const invocations: { name: string; icon: string }[] = []; for (const msg of messages) { - if ('tool_calls' in msg && Array.isArray((msg as any).tool_calls)) { - for (const tc of (msg as any).tool_calls) { - if (tc.name === 'calculator' || tc.name === 'word_count' || tc.name === 'summarize') { - invocations.push({ name: tc.name, icon: SKILL_ICONS[tc.name] ?? '🔧' }); - } + if (!(msg instanceof AIMessage)) continue; + for (const tc of this.stream.getToolCalls(msg)) { + const name = tc.call.name; + if (name === 'calculator' || name === 'word_count' || name === 'summarize') { + invocations.push({ name, icon: SKILL_ICONS[name] ?? '🔧' }); } } } 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 ec19c4aa1..648aa531a 100644 --- a/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts +++ b/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts @@ -2,6 +2,7 @@ import { Component, computed } from '@angular/core'; import { ChatDebugComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; +import { AIMessage } from '@langchain/core/messages'; import { environment } from '../environments/environment'; @Component({ @@ -11,20 +12,21 @@ import { environment } from '../environments/environment'; template: `
- @if (delegations().length > 0) { - - } +
`, }) @@ -38,10 +40,9 @@ export class SubagentsComponent { const messages = this.stream.messages(); const entries: { name: string }[] = []; for (const msg of messages) { - if ('tool_calls' in msg && Array.isArray((msg as any).tool_calls)) { - for (const tc of (msg as any).tool_calls) { - entries.push({ name: tc.name }); - } + if (!(msg instanceof AIMessage)) continue; + for (const tc of this.stream.getToolCalls(msg)) { + entries.push({ name: tc.call.name }); } } return entries; diff --git a/cockpit/langgraph/deployment-runtime/angular/src/app/deployment-runtime.component.ts b/cockpit/langgraph/deployment-runtime/angular/src/app/deployment-runtime.component.ts index 856a4cbb9..6888ed6dd 100644 --- a/cockpit/langgraph/deployment-runtime/angular/src/app/deployment-runtime.component.ts +++ b/cockpit/langgraph/deployment-runtime/angular/src/app/deployment-runtime.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { LegacyChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -20,24 +20,24 @@ import { environment } from '../environments/environment'; [error]="stream.error()" (sendMessage)="send($event)"> -

Deployment

+

Deployment

-
API URL
-
+
API URL
+
{{ apiUrl }}
-
Assistant ID
-
+
Assistant ID
+
{{ assistantId }}
-
Status
+
Status
@@ -45,11 +45,11 @@ import { environment } from '../environments/environment';
- @if (currentThreadId) { + @if (currentThreadId()) {
-
Thread ID
-
- {{ currentThreadId }} +
Thread ID
+
+ {{ currentThreadId() }}
} @@ -62,13 +62,13 @@ export class DeploymentRuntimeComponent { apiUrl: environment.langGraphApiUrl, assistantId: environment.deploymentRuntimeAssistantId, onThreadId: (id: string) => { - this.currentThreadId = id; + this.currentThreadId.set(id); }, }); - readonly apiUrl = environment.langGraphApiUrl; - readonly assistantId = environment.deploymentRuntimeAssistantId; - currentThreadId = ''; + protected readonly apiUrl = environment.langGraphApiUrl; + protected readonly assistantId = environment.deploymentRuntimeAssistantId; + protected readonly currentThreadId = signal(''); send(text: string): void { this.stream.submit({ messages: [{ role: 'human', content: text }] }); @@ -76,15 +76,15 @@ export class DeploymentRuntimeComponent { statusBadgeBackground(): string { const status = this.stream.status(); - if (status === 'loading') return 'rgba(0,160,80,0.12)'; - if (status === 'error') return 'rgba(200,40,40,0.1)'; - return 'rgba(0,64,144,0.08)'; + if (status === 'loading') return 'var(--chat-success-bg, rgba(0,160,80,0.12))'; + if (status === 'error') return 'var(--chat-error-bg, rgba(200,40,40,0.1))'; + return 'var(--chat-accent-bg, rgba(0,64,144,0.08))'; } statusBadgeColor(): string { const status = this.stream.status(); - if (status === 'loading') return '#00802a'; - if (status === 'error') return '#c82828'; - return '#004090'; + if (status === 'loading') return 'var(--chat-success-text, #4ade80)'; + if (status === 'error') return 'var(--chat-error-text, #f87171)'; + return 'var(--chat-accent, #60a5fa)'; } } 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 6191207e8..047325e90 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,4 +1,16 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +/** + * DurableExecutionComponent demonstrates LangGraph's durable execution model. + * + * Unlike a stateless API call, a LangGraph graph persists its execution state + * after every node. If the server restarts mid-run, the graph resumes from the + * last completed node — this is "durable execution". + * + * This example visualises the pipeline steps (`analyze → plan → generate`) and + * tracks which step the agent is currently executing via the `step` state key. + * A retry button is shown when the stream enters an error state, demonstrating + * how `stream.reload()` re-submits the last input to resume a failed run. + */ import { Component, computed } from '@angular/core'; import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; diff --git a/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts b/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts index 0c41a4b08..de14f8ab1 100644 --- a/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts +++ b/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts @@ -46,20 +46,17 @@ export class InterruptsComponent { /** * Handle an interrupt action from the panel. * - * Submitting null resumes the graph unconditionally (LangGraph convention). - * Tier 3 will add edit/respond flows with richer resume payloads. + * Submitting null resumes the graph unconditionally — this is the + * LangGraph convention for "proceed without modification". + * + * In a production app, 'edit' would let the user modify the response + * before approval, and 'respond' would send a reply payload. + * For this demo, all actions simply resume the graph. */ protected onInterruptAction(action: InterruptAction): void { - switch (action) { - case 'accept': - this.stream.submit(null); // Resume with approval - break; - case 'ignore': - case 'respond': - case 'edit': - // For now, just resume — Tier 3 will add edit/respond flows - this.stream.submit(null); - break; - } + // In a production app, 'edit' would let the user modify the response before approval. + // For this demo, all actions simply resume the graph. + void action; // Each branch intentionally does the same thing in this demo + this.stream.submit(null); } } diff --git a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts index c83ae851b..e8f31bd01 100644 --- a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts +++ b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts @@ -1,4 +1,18 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +/** + * PersistenceComponent demonstrates LangGraph's thread-based persistence. + * + * Each conversation is stored as a "thread" on the LangGraph backend. Threads + * survive page refreshes and can be resumed at any time by switching back to + * them. This example tracks created threads in a local signal and lets the + * user switch between them via the chat sidebar. + * + * Key integration points: + * - `threadId: null` — lets streamResource auto-create a new thread on first submit + * - `onThreadId` — called once the backend assigns a thread ID; used here to + * add the thread to the local list and set it as active + * - `stream.switchThread(id)` — reconnects the resource to an existing thread + */ import { Component, signal } from '@angular/core'; import { ChatComponent, type Thread } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; 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 0a196171b..331417862 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,4 +1,20 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +/** + * TimeTravelComponent demonstrates LangGraph's checkpoint and time-travel API. + * + * Every time the agent sends a response, LangGraph saves a checkpoint — a + * snapshot of the full conversation state at that moment. Time travel lets + * you jump back to any checkpoint and continue from there. + * + * Two modes are exposed: + * - **Replay**: re-runs the graph from the selected checkpoint with the same + * input, producing the same (or a different, if non-deterministic) output. + * - **Fork**: sets the active branch to the checkpoint so the *next* submit() + * starts a new conversation branch diverging from that point. + * + * Both modes call `stream.setBranch(checkpointId)` under the hood; the + * difference is only conceptual and reflected in how the user interacts next. + */ import { Component } from '@angular/core'; import { ChatComponent, ChatTimelineSliderComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; @@ -32,12 +48,19 @@ export class TimeTravelComponent { assistantId: environment.streamingAssistantId, }); + /** + * Replay: sets the branch to replay from this checkpoint. + * The graph re-runs from this point with the same input. + */ protected onReplay(checkpointId: string): void { this.stream.setBranch(checkpointId); } + /** + * Fork: sets the branch, then the next submit() creates a new + * conversation branch diverging from this checkpoint. + */ protected onFork(checkpointId: string): void { this.stream.setBranch(checkpointId); - // Fork: set branch, then next submit creates a new branch from this point } }