Skip to content

Commit 514644e

Browse files
authored
feat(langgraph): load history and expose branch tree
1 parent 17ad443 commit 514644e

19 files changed

Lines changed: 658 additions & 44 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
9393
| Interrupt / human-in-the-loop | `interrupt()` / `interrupts()` | `interrupt` / `interrupts` |
9494
| Tool call progress | `toolProgress()` | `toolProgress` |
9595
| Tool calls with results | `toolCalls()` | `toolCalls` |
96-
| Branch / history | `branch()` / `history()` | `branch` / `history` |
96+
| Branch / history | `branch()` / `history()` / `experimentalBranchTree()` | `branch` / `history` / `experimental_branchTree` |
9797
| Pending run queue | `queue()` | `queue` |
9898
| Subagent streaming and lookup helpers | `subagents()` / `activeSubagents()` / `getSubagent()` | `subagents` / `activeSubagents` / helper methods |
9999
| Reactive thread switching | `Signal<string \| null>` input | prop |

apps/website/content/docs/agent/api/api-docs.json

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@
7878
}
7979
]
8080
},
81+
{
82+
"name": "getHistory",
83+
"signature": "getHistory(threadId: string, signal: AbortSignal)",
84+
"description": "Load persisted checkpoint history for a thread.",
85+
"params": [
86+
{
87+
"name": "threadId",
88+
"type": "string",
89+
"description": "",
90+
"optional": false
91+
},
92+
{
93+
"name": "signal",
94+
"type": "AbortSignal",
95+
"description": "",
96+
"optional": false
97+
}
98+
]
99+
},
81100
{
82101
"name": "joinStream",
83102
"signature": "joinStream(threadId: string, runId: string, lastEventId: string | undefined, signal: AbortSignal)",
@@ -170,6 +189,18 @@
170189
"description": "",
171190
"optional": false
172191
},
192+
{
193+
"name": "history",
194+
"type": "ThreadState<DefaultValues>[]",
195+
"description": "",
196+
"optional": false
197+
},
198+
{
199+
"name": "historyCalls",
200+
"type": "string[]",
201+
"description": "",
202+
"optional": false
203+
},
173204
{
174205
"name": "joinedRuns",
175206
"type": "object[]",
@@ -266,6 +297,25 @@
266297
}
267298
]
268299
},
300+
{
301+
"name": "getHistory",
302+
"signature": "getHistory(threadId: string, signal: AbortSignal)",
303+
"description": "Optional: load persisted checkpoint history for a thread.",
304+
"params": [
305+
{
306+
"name": "threadId",
307+
"type": "string",
308+
"description": "",
309+
"optional": false
310+
},
311+
{
312+
"name": "signal",
313+
"type": "AbortSignal",
314+
"description": "",
315+
"optional": false
316+
}
317+
]
318+
},
269319
{
270320
"name": "isStreaming",
271321
"signature": "isStreaming()",
@@ -342,6 +392,72 @@
342392
}
343393
]
344394
},
395+
{
396+
"name": "AgentBranchTree",
397+
"kind": "interface",
398+
"description": "Tree representation of LangGraph checkpoint history for time-travel UIs.",
399+
"properties": [
400+
{
401+
"name": "items",
402+
"type": "AgentBranchTreeNode<T> | AgentBranchTreeFork<T>[]",
403+
"description": "",
404+
"optional": false
405+
},
406+
{
407+
"name": "type",
408+
"type": "\"sequence\"",
409+
"description": "",
410+
"optional": false
411+
}
412+
],
413+
"examples": []
414+
},
415+
{
416+
"name": "AgentBranchTreeFork",
417+
"kind": "interface",
418+
"description": "A branch fork where each item is an alternate checkpoint sequence.",
419+
"properties": [
420+
{
421+
"name": "items",
422+
"type": "AgentBranchTree<T>[]",
423+
"description": "",
424+
"optional": false
425+
},
426+
{
427+
"name": "type",
428+
"type": "\"fork\"",
429+
"description": "",
430+
"optional": false
431+
}
432+
],
433+
"examples": []
434+
},
435+
{
436+
"name": "AgentBranchTreeNode",
437+
"kind": "interface",
438+
"description": "A checkpoint entry in the experimental branch tree.",
439+
"properties": [
440+
{
441+
"name": "path",
442+
"type": "string[]",
443+
"description": "",
444+
"optional": false
445+
},
446+
{
447+
"name": "type",
448+
"type": "\"node\"",
449+
"description": "",
450+
"optional": false
451+
},
452+
{
453+
"name": "value",
454+
"type": "ThreadState<T>",
455+
"description": "",
456+
"optional": false
457+
}
458+
],
459+
"examples": []
460+
},
345461
{
346462
"name": "AgentConfig",
347463
"kind": "interface",
@@ -541,6 +657,12 @@
541657
"description": "",
542658
"optional": true
543659
},
660+
{
661+
"name": "getHistory",
662+
"type": "unknown",
663+
"description": "",
664+
"optional": true
665+
},
544666
{
545667
"name": "joinStream",
546668
"type": "unknown",
@@ -649,6 +771,12 @@
649771
"description": "",
650772
"optional": false
651773
},
774+
{
775+
"name": "experimentalBranchTree",
776+
"type": "Signal<AgentBranchTree<T>>",
777+
"description": "Experimental branch tree derived from LangGraph checkpoint history.",
778+
"optional": false
779+
},
652780
{
653781
"name": "getMessagesMetadata",
654782
"type": "object",
@@ -875,6 +1003,12 @@
8751003
"description": "",
8761004
"optional": false
8771005
},
1006+
{
1007+
"name": "experimentalBranchTree",
1008+
"type": "WritableSignal<AgentBranchTree<any>>",
1009+
"description": "Experimental branch tree derived from LangGraph checkpoint history.",
1010+
"optional": false
1011+
},
8781012
{
8791013
"name": "getMessagesMetadata",
8801014
"type": "object",

apps/website/content/docs/agent/concepts/langgraph-basics.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,10 @@ agent.isLoading() // Signal<boolean> — is the agent running?
319319
agent.interrupt() // Signal<Interrupt> — agent is paused
320320

321321
// Debugging
322-
agent.history() // Signal<ThreadState[]> — checkpoint timeline
322+
agent.history() // Signal<AgentCheckpoint[]> — runtime-neutral timeline
323+
agent.langGraphHistory() // Signal<ThreadState[]> — raw LangGraph checkpoints
323324
agent.branch() // Signal<string> — time-travel branch
325+
agent.experimentalBranchTree() // Signal<AgentBranchTree<T>> — branch tree
324326

325327
agent.toolCalls() // Signal<ToolCallWithResult[]> — tool results
326328
agent.toolProgress() // Signal<ToolProgress[]> — active tool execution

apps/website/content/docs/agent/concepts/state-management.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,10 @@ const agent = agent<ChatState>({
355355
});
356356

357357
// Read checkpoint history for time-travel UI
358-
const history = agent.history(); // Signal<ThreadState[]>
359-
const branch = agent.branch(); // Signal<string> — active branch ID
358+
const history = agent.history(); // Signal<AgentCheckpoint[]>
359+
const rawHistory = agent.langGraphHistory(); // Signal<ThreadState[]>
360+
const branch = agent.branch(); // Signal<string> — active branch ID
361+
const branchTree = agent.experimentalBranchTree();
360362
```
361363

362364
For full checkpoint and time-travel patterns, see the [Persistence guide](/docs/guides/persistence) and [Time Travel guide](/docs/guides/time-travel).

apps/website/content/docs/agent/getting-started/introduction.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ chat.messages() // Signal<BaseMessage[]>
2020
chat.status() // Signal<'idle' | 'loading' | 'resolved' | 'error'>
2121
chat.error() // Signal<Error | null>
2222
chat.interrupt() // Signal<Interrupt | undefined>
23-
chat.history() // Signal<ThreadState[]>
23+
chat.history() // Signal<AgentCheckpoint[]>
24+
chat.langGraphHistory() // Signal<ThreadState[]>
2425
```
2526

2627
No RxJS. No manual subscriptions. No async pipes. Just Signals that work with Angular's `OnPush` change detection out of the box.

apps/website/content/docs/agent/guides/time-travel.mdx

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,19 @@ export class HistoryViewerComponent {
7474
readonly agent = this.agentService.agent;
7575

7676
readonly checkpoints = computed(() => this.agent.history());
77+
readonly rawCheckpoints = computed(() => this.agent.langGraphHistory());
7778
readonly checkpointCount = computed(() => this.agent.history().length);
7879

7980
readonly activeIndex = computed(() =>
8081
this.checkpoints().length - 1
8182
);
8283

8384
fork(index: number) {
84-
const checkpoint = this.checkpoints()[index];
85+
const checkpoint = this.rawCheckpoints()[index]?.checkpoint;
86+
if (!checkpoint) return;
8587
this.agent.submit(
8688
{ messages: [{ role: 'user', content: 'Try a different approach' }] },
87-
{ checkpoint: checkpoint.checkpoint }
89+
{ checkpoint }
8890
);
8991
}
9092

@@ -99,63 +101,65 @@ export class HistoryViewerComponent {
99101

100102
## Browsing execution history
101103

102-
The `history()` signal contains an array of `ThreadState` checkpoints ordered from oldest to newest. Each checkpoint captures the complete agent state at that point in execution, including messages, intermediate results, and any custom state fields.
104+
The `history()` signal contains runtime-neutral `AgentCheckpoint` entries for the thread. For LangGraph-specific checkpoint metadata, `langGraphHistory()` exposes the raw `ThreadState[]`. The framework loads this history with `threads.getHistory()` when a thread is selected and refreshes it after a run completes.
103105

104106
```typescript
105107
const agent = agent<AgentState>({
106108
assistantId: 'agent',
107109
threadId: signal(threadId),
108110
});
109111

110-
// Full execution timeline
112+
// Runtime-neutral execution timeline
111113
const checkpoints = computed(() => agent.history());
112114
const checkpointCount = computed(() => agent.history().length);
113115

116+
// Raw LangGraph checkpoints
117+
const rawCheckpoints = computed(() => agent.langGraphHistory());
118+
114119
// Access a specific checkpoint
115120
const latestCheckpoint = computed(() => {
116121
const history = agent.history();
117122
return history[history.length - 1];
118123
});
119124
```
120125

121-
Each `ThreadState` entry exposes `checkpoint`, `metadata`, `created_at`, and the full `values` snapshot, giving you complete visibility into every step of execution.
126+
Each runtime-neutral checkpoint exposes `id`, `label`, and `values`. Each raw `ThreadState` entry exposes `checkpoint`, `parent_checkpoint`, `metadata`, `created_at`, and the full `values` snapshot, giving you complete visibility into every step of execution.
122127

123128
## Forking from a checkpoint
124129

125130
Submit with a specific checkpoint to branch execution from an earlier state. This creates a new branch in the thread graph while leaving the original path intact.
126131

127132
```typescript
128133
forkFromCheckpoint(index: number) {
129-
const checkpoint = this.agent.history()[index];
134+
const checkpoint = this.agent.langGraphHistory()[index]?.checkpoint;
135+
if (!checkpoint) return;
130136
this.agent.submit(
131137
{ messages: [{ role: 'user', content: 'Try a different approach' }] },
132-
{ checkpoint: checkpoint.checkpoint }
138+
{ checkpoint }
133139
);
134140
}
135141

136142
// Fork with a completely different input
137143
retryWithAlternative(index: number, newInput: string) {
138-
const checkpoint = this.agent.history()[index];
144+
const checkpoint = this.agent.langGraphHistory()[index]?.checkpoint;
145+
if (!checkpoint) return;
139146
this.agent.submit(
140147
{ messages: [{ role: 'user', content: newInput }] },
141-
{ checkpoint: checkpoint.checkpoint }
148+
{ checkpoint }
142149
);
143150
}
144151
```
145152

146153
## Branch navigation
147154

148-
Use `branch()` and `setBranch()` to navigate between execution branches. Branches are automatically created when you fork from a checkpoint.
155+
Use `branch()`, `setBranch()`, and `experimentalBranchTree()` to navigate between execution branches. Branches are automatically created when you fork from a checkpoint, and the branch tree is derived from raw `ThreadState.parent_checkpoint` relationships.
149156

150157
```typescript
151158
// Current branch identifier
152159
const activeBranch = computed(() => agent.branch());
153160

154-
// All available branches (if exposed by your graph)
155-
const allBranches = computed(() => agent.history()
156-
.map(s => s.metadata?.branch)
157-
.filter(Boolean)
158-
);
161+
// Full branch tree for custom time-travel UIs
162+
const branchTree = computed(() => agent.experimentalBranchTree());
159163

160164
// Switch to a different branch
161165
selectBranch(branchId: string) {
@@ -184,15 +188,17 @@ export class HistoryViewerComponent {
184188
readonly agent = this.agentService.agent;
185189

186190
readonly checkpoints = computed(() => this.agent.history());
191+
readonly rawCheckpoints = computed(() => this.agent.langGraphHistory());
187192
readonly activeIndex = computed(() =>
188193
this.checkpoints().length - 1
189194
);
190195

191196
fork(index: number) {
192-
const checkpoint = this.checkpoints()[index];
197+
const checkpoint = this.rawCheckpoints()[index]?.checkpoint;
198+
if (!checkpoint) return;
193199
this.agent.submit(
194200
{ messages: [{ role: 'user', content: 'Try a different approach' }] },
195-
{ checkpoint: checkpoint.checkpoint }
201+
{ checkpoint }
196202
);
197203
}
198204

@@ -205,10 +211,10 @@ export class HistoryViewerComponent {
205211
<Tab label="history-viewer.component.html">
206212
```html
207213
<ul class="checkpoint-list">
208-
@for (cp of checkpoints(); track cp.checkpoint.id; let i = $index) {
214+
@for (cp of checkpoints(); track cp.id; let i = $index) {
209215
<li [class.active]="i === activeIndex()">
210216
<span class="step">Step {{ i + 1 }}</span>
211-
<span class="time">{{ formatTime(cp.created_at) }}</span>
217+
<span class="time">{{ cp.label ?? cp.id }}</span>
212218
<button (click)="fork(i)">Fork from here</button>
213219
</li>
214220
}
@@ -258,24 +264,27 @@ export class ReplayComponent {
258264
readonly agent = inject(AgentService).agent;
259265

260266
readonly history = computed(() => this.agent.history());
267+
readonly rawHistory = computed(() => this.agent.langGraphHistory());
261268
readonly canUndo = computed(() => this.history().length > 1);
262269

263270
undo() {
264271
const history = this.history();
265272
if (history.length < 2) return;
266273

267274
// Go back one step
268-
const previousCheckpoint = history[history.length - 2];
275+
const previousCheckpoint = this.rawHistory()[history.length - 2]?.checkpoint;
276+
if (!previousCheckpoint) return;
269277
this.agent.submit(undefined, {
270-
checkpoint: previousCheckpoint.checkpoint,
278+
checkpoint: previousCheckpoint,
271279
});
272280
}
273281

274282
replayWith(index: number, newMessage: string) {
275-
const checkpoint = this.history()[index];
283+
const checkpoint = this.rawHistory()[index]?.checkpoint;
284+
if (!checkpoint) return;
276285
this.agent.submit(
277286
{ messages: [{ role: 'user', content: newMessage }] },
278-
{ checkpoint: checkpoint.checkpoint }
287+
{ checkpoint }
279288
);
280289
}
281290
}

0 commit comments

Comments
 (0)