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
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ This file is for agents working in this repository. It is contributor-facing, no

- `libs/langgraph`: main Angular library (`@ngaf/langgraph`).
- `apps/website`: docs and marketing site.
- `packages/mcp`: MCP server package (`@ngaf/langgraph-mcp`).
- `e2e/agent-e2e`: end-to-end coverage for the workspace.

## Working in This Repo
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ That's it. `chat.messages()` and `chat.status()` are Angular Signals. Bind them
| Error state | `error()` | — |
| Runtime-neutral status | `status()` — `'idle' \| 'running' \| 'error'` | partial |
| Interrupt / human-in-the-loop | `interrupt()` / `interrupts()` | `interrupt` / `interrupts` |
| Tool call progress | `toolProgress()` | `toolProgress` |
| Tool call progress | `toolCalls()` | `toolCalls` |
| Tool calls with results | `toolCalls()` | `toolCalls` |
| 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 |
| Subagent streaming and lookup helpers | `subagents()` / `getSubagent()` | `subagents` / helper methods |
| Reactive thread switching | `Signal<string \| null>` input | prop |
| Submit | `submit(values, opts?)` | `submit(values, opts?)` |
| Stop | `stop()` | `stop()` |
Expand Down
6 changes: 3 additions & 3 deletions apps/website/content/docs/agent/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
}
],
"examples": [
"```typescript\nconst transport = new MockAgentTransport([\n [{ type: 'values', data: { messages: [aiMsg('Hello')] } }],\n [{ type: 'values', data: { status: 'done' } }],\n]);\n```"
"```typescript\nconst transport = new MockAgentTransport([\n [{ type: 'values', messages: [aiMsg('Hello')] }],\n [{ type: 'values', messages: [aiMsg('Done')] }],\n]);\n```"
],
"properties": [
{
Expand Down Expand Up @@ -405,7 +405,7 @@
{
"name": "nextBatch",
"signature": "nextBatch()",
"description": "Advance to the next scripted batch and return its events.",
"description": "Advance to the next scripted batch. Pass the returned events to `emit()`.",
"params": []
},
{
Expand Down Expand Up @@ -1661,7 +1661,7 @@
"description": ""
},
"examples": [
"```typescript\n// In a component field initializer\nconst chat = agent<{ messages: BaseMessage[] }>({\n assistantId: 'chat_agent',\n apiUrl: 'http://localhost:2024',\n threadId: signal(this.savedThreadId),\n onThreadId: (id) => localStorage.setItem('threadId', id),\n});\n\n// Access signals in template\n// chat.messages(), chat.status(), chat.error()\n```"
"```typescript\n// In a component field initializer\nconst chat = agent({\n assistantId: 'chat_agent',\n apiUrl: 'http://localhost:2024',\n threadId: signal(this.savedThreadId),\n onThreadId: (id) => localStorage.setItem('threadId', id),\n});\n\n// Access signals in template\n// chat.messages(), chat.status(), chat.error()\n```"
]
},
{
Expand Down
57 changes: 33 additions & 24 deletions apps/website/content/docs/agent/concepts/agent-architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,13 @@ export class ReactAgentComponent {

messages = this.agent.messages;

// Tools currently executing (spinner, progress bar)
activeTools = computed(() => this.agent.toolProgress());

// Tools that finished with results (expandable cards)
completedTools = computed(() => this.agent.toolCalls());
// Tool calls with status, args, and result data
activeTools = computed(() =>
this.agent.toolCalls().filter((tool) => tool.status === 'running')
);
completedTools = computed(() =>
this.agent.toolCalls().filter((tool) => tool.status === 'complete')
);

send(text: string) {
this.agent.submit({ message: text });
Expand Down Expand Up @@ -233,21 +235,23 @@ The LLM reads the docstring to decide when to call a tool. A vague docstring lik

### How Tools Surface in Angular

When the agent calls a tool, agent() exposes the execution lifecycle through two signals:
When the agent calls a tool, agent() exposes the execution lifecycle through `toolCalls()`:

<Tabs>
<Tab label="toolProgress()">
<Tab label="toolCalls()">

```typescript
// toolProgress() — tools currently executing
// toolCalls() — tool calls with status, args, and results
// Updates in real time as tools start and complete

const agent = agent<AgentState>({
assistantId: 'react_agent',
});

// Each entry has: name, args, status
const activeTools = computed(() => agent.toolProgress());
// Each entry has: id, name, args, status, and optional result
const activeTools = computed(() =>
agent.toolCalls().filter((tool) => tool.status === 'running')
);

// Template usage
@Component({
Expand All @@ -263,18 +267,19 @@ const activeTools = computed(() => agent.toolProgress());
`,
})
export class ToolProgressComponent {
activeTools = computed(() => this.agent.toolProgress());
activeTools = computed(() =>
this.agent.toolCalls().filter((tool) => tool.status === 'running')
);
}
```

</Tab>
<Tab label="toolCalls()">
<Tab label="completed calls">

```typescript
// toolCalls() — completed tool calls with results
// Available after each tool finishes

const completedTools = computed(() => agent.toolCalls());
const completedTools = computed(() =>
agent.toolCalls().filter((tool) => tool.status === 'complete')
);

// Each entry has: name, args, result, duration
@Component({
Expand Down Expand Up @@ -324,7 +329,7 @@ The `should_continue` conditional edge detects `tool_calls` and routes to the `t
LangGraph Platform streams the tool call and result as SSE events to the Angular client.
</Step>
<Step title="agent() updates signals">
`toolProgress()` updates during execution. `toolCalls()` updates when the tool completes. Both trigger OnPush change detection.
`toolCalls()` updates as the tool moves through pending, running, complete, and error states. Each update triggers OnPush change detection.
</Step>
</Steps>

Expand Down Expand Up @@ -442,10 +447,14 @@ export class MultiAgentComponent {
messages = this.orchestrator.messages;

// Currently running delegated work with live status
activeTools = computed(() => this.orchestrator.toolProgress());
activeTools = computed(() =>
this.orchestrator.toolCalls().filter((tool) => tool.status === 'running')
);

// Completed tool calls with results
completedTools = computed(() => this.orchestrator.toolCalls());
completedTools = computed(() =>
this.orchestrator.toolCalls().filter((tool) => tool.status === 'complete')
);

send(text: string) {
this.orchestrator.submit({ message: text });
Expand Down Expand Up @@ -518,7 +527,7 @@ export class AgentComponent {
|---|---|---|
| Tool throws `ToolException` | Error fed back to LLM, agent retries | `toolCalls()` shows error in result |
| Tool throws unexpected error | LangGraph catches it, marks tool as failed | `error()` fires with details |
| LLM returns invalid tool args | ToolNode validation fails, error fed to LLM | `toolProgress()` shows failed status |
| LLM returns invalid tool args | ToolNode validation fails, error fed to LLM | `toolCalls()` shows failed status |
| Transport error (network) | N/A | `error()` fires, `status()` becomes `'error'` |
| Agent exceeds recursion limit | Graph raises `GraphRecursionError` | `error()` fires with recursion message |

Expand Down Expand Up @@ -594,7 +603,7 @@ export class DebugTimelineComponent {

timeTravel(checkpointId: string) {
this.currentCheckpoint.set(checkpointId);
this.agent.submit(null, { checkpointId });
this.agent.submit({}, { checkpointId });
}
}
```
Expand Down Expand Up @@ -622,7 +631,7 @@ builder.add_edge("tools", "model")
graph = builder.compile()
```

**Angular signals used:** `messages()`, `toolCalls()`, `toolProgress()`, `status()`
**Angular signals used:** `messages()`, `toolCalls()`, `status()`

### Single Agent with Human-in-the-Loop

Expand All @@ -640,7 +649,7 @@ def execute_action(state: AgentState) -> dict:
return perform_action(state["pending_action"])
```

**Angular signals used:** `messages()`, `interrupt()`, `status()` plus `submit(null, { resume })` to approve
**Angular signals used:** `messages()`, `interrupt()`, `status()` plus `submit({ resume })` to approve

### Multi-Agent Supervisor

Expand All @@ -654,7 +663,7 @@ builder.add_node("analyst", analyst_subgraph)
builder.add_conditional_edges("supervisor", route_to_agent)
```

**Angular signals used:** `messages()`, `subagents()`, `activeSubagents()`, `toolCalls()`, `toolProgress()`, `status()`
**Angular signals used:** `messages()`, `subagents()`, `toolCalls()`, `status()`

### Decision Matrix

Expand Down
67 changes: 31 additions & 36 deletions apps/website/content/docs/agent/concepts/angular-signals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Under the hood, agent() receives Server-Sent Events (SSE) over HTTP and feeds th
// Simplified view of what agent does internally:

// 1. SSE events arrive as an observable stream
const messages$ = new BehaviorSubject<BaseMessage[]>([]);
const messages$ = new BehaviorSubject<Message[]>([]);
const status$ = new BehaviorSubject<ResourceStatus>('idle');

// 2. Each SSE chunk updates the BehaviorSubject
Expand All @@ -68,8 +68,8 @@ const chat = agent<ChatState>({
assistantId: 'chat_agent',
});

chat.messages(); // Signal<BaseMessage[]>
chat.status(); // Signal<ResourceStatus>
chat.messages(); // Signal<Message[]>
chat.status(); // Signal<'idle' | 'running' | 'error'>
chat.error(); // Signal<unknown>
chat.isLoading(); // Signal<boolean>
chat.value(); // Signal<ChatState>
Expand All @@ -84,7 +84,7 @@ The BehaviorSubject-to-Signal conversion means you get the best of both worlds:

## The Streaming Lifecycle as Signals

Every agent() instance moves through a lifecycle: **idle**, **loading**, tokens arriving, then **resolved** (or **error**). The `status()` Signal reflects each transition in real time.
Every agent() instance moves through a lifecycle: **idle**, **running** while work is in flight, then back to **idle** when the stream completes (or **error** when it fails). The `status()` Signal reflects each transition in real time, while `isLoading()` is the convenience signal for loading UI.

<Steps>
<Step title="idle — Waiting for input">
Expand All @@ -101,39 +101,39 @@ console.log(chat.isLoading()); // false
```
</Step>

<Step title="loading — Request in flight">
After calling `submit()`, the status transitions to `'loading'`. The SSE connection is open and the agent is processing.
<Step title="running — Request in flight">
After calling `submit()`, the status transitions to `'running'`. The SSE connection is open and the agent is processing.

```typescript
chat.submit({ message: 'Explain quantum computing' });

console.log(chat.status()); // 'loading'
console.log(chat.status()); // 'running'
console.log(chat.isLoading()); // true
console.log(chat.messages()); // [] (no tokens yet)
```
</Step>

<Step title="loading — Tokens streaming">
As the agent generates tokens, the `messages()` Signal updates with each chunk. The status remains `'loading'` throughout.
<Step title="running — Tokens streaming">
As the agent generates tokens, the `messages()` Signal updates with each chunk. The status remains `'running'` throughout.

```typescript
// After first few tokens arrive:
console.log(chat.status()); // 'loading' (still streaming)
console.log(chat.messages()); // [AIMessageChunk("Quantum computing uses...")]
console.log(chat.status()); // 'running' (still streaming)
console.log(chat.messages()); // [{ role: 'assistant', content: 'Quantum computing uses...' }]

// After more tokens:
console.log(chat.messages()); // [AIMessageChunk("Quantum computing uses qubits...")]
console.log(chat.messages()); // [{ role: 'assistant', content: 'Quantum computing uses qubits...' }]
// The message content grows as tokens stream in
```
</Step>

<Step title="resolved — Stream complete">
The agent has finished. All tokens have arrived. The status transitions to `'resolved'`.
<Step title="idle — Stream complete">
The agent has finished. All tokens have arrived. The status transitions back to `'idle'`.

```typescript
console.log(chat.status()); // 'resolved'
console.log(chat.status()); // 'idle'
console.log(chat.isLoading()); // false
console.log(chat.messages()); // [AIMessage("Quantum computing uses qubits to...")]
console.log(chat.messages()); // [{ role: 'assistant', content: 'Quantum computing uses qubits to...' }]
```
</Step>

Expand Down Expand Up @@ -168,16 +168,11 @@ const lastMessage = computed(() => chat.messages().at(-1));

// Extract just the assistant's messages
const assistantMessages = computed(() =>
chat.messages().filter(m => m._getType() === 'ai')
chat.messages().filter(m => m.role === 'assistant')
);

// Track which tools the agent is actively calling
const activeTools = computed(() =>
chat.messages()
.filter(m => m._getType() === 'ai')
.flatMap(m => m.tool_calls ?? [])
.filter(tc => !tc.result)
);
const activeTools = computed(() => chat.toolCalls());

// Build a user-facing error message
const errorDisplay = computed(() => {
Expand All @@ -195,7 +190,7 @@ const errorDisplay = computed(() => {
const viewModel = computed(() => ({
messages: chat.messages(),
isStreaming: chat.isLoading(),
canSend: chat.status() !== 'loading',
canSend: !chat.isLoading(),
messageCount: messageCount(),
error: errorDisplay(),
}));
Expand Down Expand Up @@ -238,10 +233,10 @@ effect(() => {
// Track streaming duration for performance monitoring
effect(() => {
const status = chat.status();
if (status === 'loading') {
if (status === 'running') {
this.streamStart = performance.now();
}
if (status === 'resolved' && this.streamStart) {
if (status === 'idle' && this.streamStart) {
const duration = performance.now() - this.streamStart;
this.analytics.track('stream_duration_ms', { duration });
this.streamStart = null;
Expand All @@ -267,7 +262,7 @@ import { agent } from '@ngaf/langgraph';
template: `
<!-- Status bar -->
@switch (chat.status()) {
@case ('loading') {
@case ('running') {
<div class="status-bar streaming">
Agent is responding...
</div>
Expand All @@ -283,18 +278,18 @@ import { agent } from '@ngaf/langgraph';
<!-- Message list -->
<div class="messages" #chatContainer>
@for (message of chat.messages(); track $index) {
@switch (message._getType()) {
@case ('human') {
@switch (message.role) {
@case ('user') {
<div class="message user">
{{ message.content }}
</div>
}
@case ('ai') {
@case ('assistant') {
<div class="message assistant">
{{ message.content }}

<!-- Show tool calls if the assistant invoked any -->
@for (tool of message.tool_calls ?? []; track tool.id) {
@for (tool of chat.toolCalls(); track tool.id) {
<div class="tool-call">
Called: {{ tool.name }}
</div>
Expand Down Expand Up @@ -460,14 +455,14 @@ import { agent } from '@ngaf/langgraph';
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (msg of chat.messages(); track $index) {
@switch (msg._getType()) {
@case ('human') {
@switch (msg.role) {
@case ('user') {
<div class="user">{{ msg.content }}</div>
}
@case ('ai') {
@case ('assistant') {
<div class="assistant">
{{ msg.content }}
@for (tc of msg.tool_calls ?? []; track tc.id) {
@for (tc of chat.toolCalls(); track tc.id) {
<span class="tool-badge">{{ tc.name }}</span>
}
</div>
Expand All @@ -491,7 +486,7 @@ export class ChatComponent {
// Derived state from the Python agent's output
toolsUsed = computed(() =>
this.chat.messages()
.filter(m => m._getType() === 'tool')
.filter(m => m.role === 'tool')
.map(m => m.name)
);

Expand Down
Loading
Loading