Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
| Interrupt / human-in-the-loop | `interrupt()` / `interrupts()` | `interrupt` / `interrupts` |
| Tool call progress | `toolProgress()` | `toolProgress` |
| Tool calls with results | `toolCalls()` | `toolCalls` |
| Branch / history | `branch()` / `history()` | `branch` / `history` |
| Branch / history | `branch()` / `history()` / `experimentalBranchTree()` | `branch` / `history` / `experimental_branchTree` |
| Pending run queue | `queue()` | `queue` |
| Subagent streaming and lookup helpers | `subagents()` / `activeSubagents()` / `getSubagent()` | `subagents` / `activeSubagents` / helper methods |
| Reactive thread switching | `Signal<string \| null>` input | prop |
Expand Down
134 changes: 134 additions & 0 deletions apps/website/content/docs/agent/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@
}
]
},
{
"name": "getHistory",
"signature": "getHistory(threadId: string, signal: AbortSignal)",
"description": "Load persisted checkpoint history for a thread.",
"params": [
{
"name": "threadId",
"type": "string",
"description": "",
"optional": false
},
{
"name": "signal",
"type": "AbortSignal",
"description": "",
"optional": false
}
]
},
{
"name": "joinStream",
"signature": "joinStream(threadId: string, runId: string, lastEventId: string | undefined, signal: AbortSignal)",
Expand Down Expand Up @@ -170,6 +189,18 @@
"description": "",
"optional": false
},
{
"name": "history",
"type": "ThreadState<DefaultValues>[]",
"description": "",
"optional": false
},
{
"name": "historyCalls",
"type": "string[]",
"description": "",
"optional": false
},
{
"name": "joinedRuns",
"type": "object[]",
Expand Down Expand Up @@ -266,6 +297,25 @@
}
]
},
{
"name": "getHistory",
"signature": "getHistory(threadId: string, signal: AbortSignal)",
"description": "Optional: load persisted checkpoint history for a thread.",
"params": [
{
"name": "threadId",
"type": "string",
"description": "",
"optional": false
},
{
"name": "signal",
"type": "AbortSignal",
"description": "",
"optional": false
}
]
},
{
"name": "isStreaming",
"signature": "isStreaming()",
Expand Down Expand Up @@ -342,6 +392,72 @@
}
]
},
{
"name": "AgentBranchTree",
"kind": "interface",
"description": "Tree representation of LangGraph checkpoint history for time-travel UIs.",
"properties": [
{
"name": "items",
"type": "AgentBranchTreeNode<T> | AgentBranchTreeFork<T>[]",
"description": "",
"optional": false
},
{
"name": "type",
"type": "\"sequence\"",
"description": "",
"optional": false
}
],
"examples": []
},
{
"name": "AgentBranchTreeFork",
"kind": "interface",
"description": "A branch fork where each item is an alternate checkpoint sequence.",
"properties": [
{
"name": "items",
"type": "AgentBranchTree<T>[]",
"description": "",
"optional": false
},
{
"name": "type",
"type": "\"fork\"",
"description": "",
"optional": false
}
],
"examples": []
},
{
"name": "AgentBranchTreeNode",
"kind": "interface",
"description": "A checkpoint entry in the experimental branch tree.",
"properties": [
{
"name": "path",
"type": "string[]",
"description": "",
"optional": false
},
{
"name": "type",
"type": "\"node\"",
"description": "",
"optional": false
},
{
"name": "value",
"type": "ThreadState<T>",
"description": "",
"optional": false
}
],
"examples": []
},
{
"name": "AgentConfig",
"kind": "interface",
Expand Down Expand Up @@ -541,6 +657,12 @@
"description": "",
"optional": true
},
{
"name": "getHistory",
"type": "unknown",
"description": "",
"optional": true
},
{
"name": "joinStream",
"type": "unknown",
Expand Down Expand Up @@ -649,6 +771,12 @@
"description": "",
"optional": false
},
{
"name": "experimentalBranchTree",
"type": "Signal<AgentBranchTree<T>>",
"description": "Experimental branch tree derived from LangGraph checkpoint history.",
"optional": false
},
{
"name": "getMessagesMetadata",
"type": "object",
Expand Down Expand Up @@ -875,6 +1003,12 @@
"description": "",
"optional": false
},
{
"name": "experimentalBranchTree",
"type": "WritableSignal<AgentBranchTree<any>>",
"description": "Experimental branch tree derived from LangGraph checkpoint history.",
"optional": false
},
{
"name": "getMessagesMetadata",
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,10 @@ agent.isLoading() // Signal<boolean> — is the agent running?
agent.interrupt() // Signal<Interrupt> — agent is paused

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

agent.toolCalls() // Signal<ToolCallWithResult[]> — tool results
agent.toolProgress() // Signal<ToolProgress[]> — active tool execution
Expand Down
6 changes: 4 additions & 2 deletions apps/website/content/docs/agent/concepts/state-management.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,10 @@ const agent = agent<ChatState>({
});

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

For full checkpoint and time-travel patterns, see the [Persistence guide](/docs/guides/persistence) and [Time Travel guide](/docs/guides/time-travel).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ chat.messages() // Signal<BaseMessage[]>
chat.status() // Signal<'idle' | 'loading' | 'resolved' | 'error'>
chat.error() // Signal<Error | null>
chat.interrupt() // Signal<Interrupt | undefined>
chat.history() // Signal<ThreadState[]>
chat.history() // Signal<AgentCheckpoint[]>
chat.langGraphHistory() // Signal<ThreadState[]>
```

No RxJS. No manual subscriptions. No async pipes. Just Signals that work with Angular's `OnPush` change detection out of the box.
Expand Down
55 changes: 32 additions & 23 deletions apps/website/content/docs/agent/guides/time-travel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,19 @@ export class HistoryViewerComponent {
readonly agent = this.agentService.agent;

readonly checkpoints = computed(() => this.agent.history());
readonly rawCheckpoints = computed(() => this.agent.langGraphHistory());
readonly checkpointCount = computed(() => this.agent.history().length);

readonly activeIndex = computed(() =>
this.checkpoints().length - 1
);

fork(index: number) {
const checkpoint = this.checkpoints()[index];
const checkpoint = this.rawCheckpoints()[index]?.checkpoint;
if (!checkpoint) return;
this.agent.submit(
{ messages: [{ role: 'user', content: 'Try a different approach' }] },
{ checkpoint: checkpoint.checkpoint }
{ checkpoint }
);
}

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

## Browsing execution history

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.
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.

```typescript
const agent = agent<AgentState>({
assistantId: 'agent',
threadId: signal(threadId),
});

// Full execution timeline
// Runtime-neutral execution timeline
const checkpoints = computed(() => agent.history());
const checkpointCount = computed(() => agent.history().length);

// Raw LangGraph checkpoints
const rawCheckpoints = computed(() => agent.langGraphHistory());

// Access a specific checkpoint
const latestCheckpoint = computed(() => {
const history = agent.history();
return history[history.length - 1];
});
```

Each `ThreadState` entry exposes `checkpoint`, `metadata`, `created_at`, and the full `values` snapshot, giving you complete visibility into every step of execution.
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.

## Forking from a checkpoint

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.

```typescript
forkFromCheckpoint(index: number) {
const checkpoint = this.agent.history()[index];
const checkpoint = this.agent.langGraphHistory()[index]?.checkpoint;
if (!checkpoint) return;
this.agent.submit(
{ messages: [{ role: 'user', content: 'Try a different approach' }] },
{ checkpoint: checkpoint.checkpoint }
{ checkpoint }
);
}

// Fork with a completely different input
retryWithAlternative(index: number, newInput: string) {
const checkpoint = this.agent.history()[index];
const checkpoint = this.agent.langGraphHistory()[index]?.checkpoint;
if (!checkpoint) return;
this.agent.submit(
{ messages: [{ role: 'user', content: newInput }] },
{ checkpoint: checkpoint.checkpoint }
{ checkpoint }
);
}
```

## Branch navigation

Use `branch()` and `setBranch()` to navigate between execution branches. Branches are automatically created when you fork from a checkpoint.
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.

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

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

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

readonly checkpoints = computed(() => this.agent.history());
readonly rawCheckpoints = computed(() => this.agent.langGraphHistory());
readonly activeIndex = computed(() =>
this.checkpoints().length - 1
);

fork(index: number) {
const checkpoint = this.checkpoints()[index];
const checkpoint = this.rawCheckpoints()[index]?.checkpoint;
if (!checkpoint) return;
this.agent.submit(
{ messages: [{ role: 'user', content: 'Try a different approach' }] },
{ checkpoint: checkpoint.checkpoint }
{ checkpoint }
);
}

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

readonly history = computed(() => this.agent.history());
readonly rawHistory = computed(() => this.agent.langGraphHistory());
readonly canUndo = computed(() => this.history().length > 1);

undo() {
const history = this.history();
if (history.length < 2) return;

// Go back one step
const previousCheckpoint = history[history.length - 2];
const previousCheckpoint = this.rawHistory()[history.length - 2]?.checkpoint;
if (!previousCheckpoint) return;
this.agent.submit(undefined, {
checkpoint: previousCheckpoint.checkpoint,
checkpoint: previousCheckpoint,
});
}

replayWith(index: number, newMessage: string) {
const checkpoint = this.history()[index];
const checkpoint = this.rawHistory()[index]?.checkpoint;
if (!checkpoint) return;
this.agent.submit(
{ messages: [{ role: 'user', content: newMessage }] },
{ checkpoint: checkpoint.checkpoint }
{ checkpoint }
);
}
}
Expand Down
Loading
Loading