From 5d9466742f770bef0215e0e7387bde047e4a47eb Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sun, 5 Apr 2026 17:56:42 -0700 Subject: [PATCH] =?UTF-8?q?feat(cockpit):=20Tier=203=20=E2=80=94=20time-tr?= =?UTF-8?q?avel=20checkpoint=20nav=20+=20durable-execution=20step=20pipeli?= =?UTF-8?q?ne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace LegacyChatComponent with ChatComponent + ChatTimelineSliderComponent in time-travel, and ChatComponent with a status bar pipeline in durable-execution. Co-Authored-By: Claude Sonnet 4.6 --- .../src/app/durable-execution.component.ts | 132 ++++++++---------- .../angular/src/app/time-travel.component.ts | 87 +++--------- 2 files changed, 85 insertions(+), 134 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 2aa442d12..6191207e8 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,96 +1,88 @@ -import { Component } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, computed } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; -/** - * DurableExecutionComponent demonstrates fault-tolerant multi-step execution - * with `streamResource()`. - * - * This example shows how a graph checkpoints at each node, enabling it to - * resume after failures. The sidebar shows execution status in real time: - * - `stream.status()` as a badge (idle/loading/resolved/error) - * - `stream.hasValue()` indicator for received data - * - A "Retry" button that calls `stream.reload()` when `stream.error()` is set - * - * The backend processes each request through three nodes: - * analyze → plan → generate - * Each node updates `state.step` so the UI can track progress. - */ @Component({ selector: 'app-durable-execution', standalone: true, - imports: [LegacyChatComponent], + imports: [ChatComponent], template: ` - - -

Execution Status

- -
- Status -
- - {{ stream.status() }} - -
-
- -
- Data Received -
- - {{ stream.hasValue() ? 'Yes' : 'No' }} -
+
+ +
+ +
+ @for (step of steps; track step) { +
+ + + + {{ step }} + +
+ @if (!$last) { + + } + }
- @if (stream.error()) { -
-
Execution Failed
- -
- } - - + } +
+
+ + + +
`, }) export class DurableExecutionComponent { - /** - * The streaming resource backing this durable-execution demo. - * - * The graph runs three nodes (analyze → plan → generate), checkpointing - * after each one. If the graph fails partway through, `stream.reload()` - * re-submits the last input so the run can resume from the last checkpoint. - */ + protected readonly steps = ['analyze', 'plan', 'generate']; + protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - /** - * Submit a message to be processed through the multi-node graph. - */ - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); + protected readonly currentStep = computed(() => { + const val = this.stream.value() as Record; + return (val?.['step'] as string) ?? ''; + }); + + protected isStepComplete(step: string): boolean { + const idx = this.steps.indexOf(step); + const currentIdx = this.steps.indexOf(this.currentStep()); + return currentIdx > idx; } - /** - * Returns a colour for the status badge based on the current stream status. - */ - statusBadgeColor(): string { + protected statusColor(): string { switch (this.stream.status()) { case 'loading': - case 'reloading': return '#2563eb'; - case 'resolved': return '#16a34a'; - case 'error': return '#dc2626'; - default: return '#6b7280'; + case 'reloading': + return '#2563eb'; + case 'resolved': + return '#16a34a'; + case 'error': + return '#dc2626'; + default: + return '#6b7280'; } } } 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 17759117d..0a196171b 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,84 +1,43 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { Component } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { ChatComponent, ChatTimelineSliderComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; -/** - * TimeTravelComponent demonstrates replaying and branching conversation history. - * - * Key integration points: - * - `stream.history()` — array of ThreadState snapshots - * - `stream.branch()` — current branch identifier - * - `stream.setBranch(id)` — switch to a different checkpoint - */ @Component({ selector: 'app-time-travel', standalone: true, - imports: [LegacyChatComponent], + imports: [ChatComponent, ChatTimelineSliderComponent], template: ` - - -

History

- @for (state of stream.history(); track $index) { - - } - @if (stream.history().length === 0) { -

No history yet. Send a message to begin.

- } -
-
+
+ + +
`, }) export class TimeTravelComponent { - /** - * The streaming resource with checkpointing enabled. - * - * `stream.history()` provides an array of ThreadState snapshots for - * the current thread. `stream.branch()` tracks the active checkpoint. - * Call `stream.setBranch(checkpointId)` to replay from a past state. - */ protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - /** - * Submit a message to the current thread. - */ - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); + protected onReplay(checkpointId: string): void { + this.stream.setBranch(checkpointId); } - /** - * Branch the conversation from the selected checkpoint. - * After calling setBranch, the next submit will fork from that point. - */ - selectCheckpoint(state: { checkpoint_id?: string }): void { - if (state.checkpoint_id) { - this.stream.setBranch(state.checkpoint_id); - } - } - - /** - * Format a checkpoint for display in the sidebar. - */ - formatCheckpoint(state: { checkpoint_id?: string; created_at?: string }): string { - const id = state.checkpoint_id ?? 'unknown'; - const short = id.substring(0, 8); - if (state.created_at) { - const ts = new Date(state.created_at).toLocaleTimeString(); - return `${short}... @ ${ts}`; - } - return `${short}...`; + protected onFork(checkpointId: string): void { + this.stream.setBranch(checkpointId); + // Fork: set branch, then next submit creates a new branch from this point } }