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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ jobs:
- run: npm ci
- run: npx nx lint website
- run: npm run generate-api-docs
# nx build website triggers demo:build first (dependsOn in project.json)
- name: Verify generated API docs are committed
run: git diff --exit-code -- apps/website/content/docs/*/api/api-docs.json
- run: npx nx build website

cockpit:
Expand Down
43 changes: 20 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<p align="center">
<img
src="https://cacheplane.ai/assets/hero.svg"
alt="Angular Agent Framework — The Angular Agent Framework for LangChain"
alt="Angular Agent Framework — agent UI primitives for Angular"
width="100%"
/>
</p>

<p align="center">
<em>The Angular Agent Framework for LangChain</em>
<em>Agent UI primitives and LangGraph streaming adapters for Angular</em>
</p>

<p align="center">
Expand All @@ -27,14 +27,14 @@

---

`agent()` is the Angular equivalent of LangGraph's React `useStream()` hook, built on Angular Signals and the Angular Resource API. It gives enterprise Angular teams production-grade streaming primitives for LangChain. Drop it into any Angular 20+ component, point it at your LangGraph Platform endpoint, and get reactive, signal-driven access to streaming state, messages, tool calls, interrupts, and thread history.
`agent()` is the Angular equivalent of LangGraph's React `useStream()` hook, projected into a runtime-neutral `Agent` contract consumed by `@ngaf/chat`. Drop it into any Angular 20+ component, point it at your LangGraph Platform endpoint, and get signal-driven access to messages, status, tool calls, interrupts, subagents, regenerate, and thread history.

---

## Install

```bash
npm install @ngaf/langgraph
npm install @ngaf/langgraph @ngaf/chat
```

**Peer dependencies:** `@angular/core ^20.0.0 || ^21.0.0`, `@langchain/core ^1.1.0`, `@langchain/langgraph-sdk ^1.7.0`, `rxjs ~7.8.0`
Expand All @@ -45,17 +45,14 @@ npm install @ngaf/langgraph

```typescript
import { Component } from '@angular/core';
import { ChatComponent as NgafChatComponent } from '@ngaf/chat';
import { agent } from '@ngaf/langgraph';
import type { BaseMessage } from '@langchain/core/messages';

@Component({
selector: 'app-chat',
selector: 'app-support-chat',
imports: [NgafChatComponent],
template: `
<ul>
@for (msg of chat.messages(); track $index) {
<li>{{ msg.content }}</li>
}
</ul>
<chat [agent]="chat" />

@if (chat.isLoading()) {
<span>Streaming…</span>
Expand All @@ -64,20 +61,19 @@ import type { BaseMessage } from '@langchain/core/messages';
<button (click)="send()">Send</button>
`,
})
export class ChatComponent {
chat = agent<{ messages: BaseMessage[] }>({
export class SupportChatComponent {
chat = agent({
apiUrl: 'https://your-langgraph-platform.com',
assistantId: 'my-agent',
messagesKey: 'messages',
});

send() {
this.chat.submit({ messages: [{ role: 'human', content: 'Hello' }] });
void this.chat.submit({ message: 'Hello' });
}
}
```

That's it. `chat.messages()` is an Angular Signal. Bind it directly in your template — no subscriptions, no `async` pipe, no zone.js required.
That's it. `chat.messages()` and `chat.status()` are Angular Signals. Bind them directly in your template — no subscriptions, no `async` pipe, no zone.js required.

---

Expand All @@ -89,7 +85,7 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
| Messages signal | `messages()` | `messages` |
| Loading state | `isLoading()` | `isLoading` |
| Error state | `error()` | — |
| Resource status (idle/loading/resolved/error) | `status()` — full `ResourceStatus` | partial |
| Runtime-neutral status | `status()` — `'idle' \| 'running' \| 'error'` | partial |
| Interrupt / human-in-the-loop | `interrupt()` / `interrupts()` | `interrupt` / `interrupts` |
| Tool call progress | `toolProgress()` | `toolProgress` |
| Tool calls with results | `toolCalls()` | `toolCalls` |
Expand All @@ -99,8 +95,9 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
| Reactive thread switching | `Signal<string \| null>` input | prop |
| Submit | `submit(values, opts?)` | `submit(values, opts?)` |
| Stop | `stop()` | `stop()` |
| Regenerate response | `regenerate(assistantMessageIndex)` | — |
| Reload last submission | `reload()` | — |
| Custom transport (for testing) | `MockStreamTransport` | mock fetch |
| Custom transport (for testing) | `MockAgentTransport` | mock fetch |
| Angular `ResourceRef<T>` compatibility | Full duck-type parity | N/A |
| Angular 20+ Signals API | Native | N/A |
| SSR / Server Components | Client-side only | React Server Components (React) |
Expand All @@ -123,11 +120,11 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp

## Documentation

- [Getting Started](https://cacheplane.ai/docs/getting-started)
- [API Reference](https://cacheplane.ai/api-reference)
- [Testing with MockStreamTransport](https://cacheplane.ai/docs/testing)
- [Human-in-the-Loop / Interrupts](https://cacheplane.ai/docs/interrupts)
- [Subagent Streaming](https://cacheplane.ai/docs/subagents)
- [Agent Quickstart](https://cacheplane.ai/docs/agent/getting-started/quickstart)
- [agent() API](https://cacheplane.ai/docs/agent/api/agent)
- [Chat Introduction](https://cacheplane.ai/docs/chat/getting-started/introduction)
- [Human-in-the-Loop / Interrupts](https://cacheplane.ai/docs/agent/guides/interrupts)
- [Subgraph and Subagent Streaming](https://cacheplane.ai/docs/agent/guides/subgraphs)

---

Expand Down
31 changes: 31 additions & 0 deletions apps/website/content/docs/ag-ui/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@
"description": "Milliseconds between successive token emissions.",
"optional": true
},
{
"name": "reasoningTokens",
"type": "string[]",
"description": "Optional reasoning chunks emitted before the text reply.",
"optional": true
},
{
"name": "tokens",
"type": "string[]",
Expand All @@ -416,6 +422,31 @@
],
"examples": []
},
{
"name": "bridgeCitationsState",
"kind": "function",
"description": "",
"signature": "bridgeCitationsState(thread: ThreadStateLike, messages: Message[]): Message[]",
"params": [
{
"name": "thread",
"type": "ThreadStateLike",
"description": "",
"optional": false
},
{
"name": "messages",
"type": "Message[]",
"description": "",
"optional": false
}
],
"returns": {
"type": "Message[]",
"description": ""
},
"examples": []
},
{
"name": "injectAgUiAgent",
"kind": "function",
Expand Down
92 changes: 67 additions & 25 deletions apps/website/content/docs/agent/api/agent.mdx
Original file line number Diff line number Diff line change
@@ -1,46 +1,88 @@
# agent()

`agent` is the core primitive of the library. It creates a reactive resource that opens a server-sent event stream, tracks loading and error states, and exposes the latest emitted value — all within Angular's signal-based reactivity model.
`agent()` is the LangGraph adapter for Angular. It connects to a LangGraph Platform assistant, consumes the LangGraph SDK event stream, and projects the result into the runtime-neutral `Agent` contract used by `@ngaf/chat`.

When the `url` signal changes, the resource tears down the previous connection and opens a fresh one automatically. You never write subscription management or cleanup logic yourself.
Call it in an Angular injection context, usually as a component field initializer. The returned object exposes Angular Signals for UI state and async methods for user actions.

```ts
import { agent } from '@ngaf/langgraph';

// Inside a component or service with injection context
const repo = agent<Repository>({
url: () => `/api/repos/${this.repoId()}`,
transport: inject(FetchStreamTransport),
readonly chat = agent({
apiUrl: 'http://localhost:2024',
assistantId: 'my-agent',
threadId: this.threadId,
onThreadId: (id) => localStorage.setItem('threadId', id),
});

// Use in template
// repo.value() — latest emitted value (or undefined)
// repo.status() — 'idle' | 'loading' | 'streaming' | 'error'
// repo.error() — the thrown error, when status is 'error'
// repo.interrupt() — call to cancel the stream immediately
await this.chat.submit({ message: 'Hello' });
```

## Key signals
## Runtime-neutral surface

| Signal | Type | Description |
These fields are stable across runtime adapters and are what chat components consume.

| Field | Type | Description |
|--------|------|-------------|
| `value()` | `T \| undefined` | The latest value emitted by the stream. Starts as `undefined` and updates with each SSE event. |
| `status()` | `'idle' \| 'loading' \| 'streaming' \| 'error'` | Lifecycle state of the current connection. |
| `error()` | `unknown` | The error thrown when `status()` is `'error'`. `undefined` otherwise. |
| `interrupt()` | `() => void` | Closes the active stream without an error — useful for user-initiated cancellation. |
| `messages()` | `Message[]` | Runtime-neutral chat messages with `role`, `content`, optional `toolCallId`, citations, reasoning, and raw extras. |
| `status()` | `'idle' \| 'running' \| 'error'` | UI lifecycle status. LangGraph loading and reloading both map to `running`. |
| `isLoading()` | `boolean` | Convenience signal for active streaming. |
| `error()` | `unknown` | Latest runtime error, when present. |
| `toolCalls()` | `ToolCall[]` | Tool calls projected into the chat contract. |
| `state()` | `Record<string, unknown>` | Latest state values projected as a plain object. |
| `submit(input, opts?)` | `Promise<void>` | Submit a user message, resume payload, and/or state patch. |
| `stop()` | `Promise<void>` | Stop the active run. |
| `regenerate(index)` | `Promise<void>` | Remove the assistant message at `index` and all following messages, then rerun from the preceding user message. |
| `interrupt()` | `AgentInterrupt \| undefined` | Optional current interrupt, when the backend pauses for human input. |
| `subagents()` | `Map<string, Subagent>` | Optional subagent streams keyed by tool-call id. |

## LangGraph-specific surface

`agent()` also returns LangGraph-specific signals and helpers for apps that need the raw platform model:

- `value()` and `hasValue()` for raw state values.
- `langGraphMessages()`, `langGraphToolCalls()`, and `langGraphHistory()` for raw SDK-shaped data.
- `history()`, `branch()`, `setBranch()`, and `experimentalBranchTree()` for checkpoint and time-travel UIs.
- `queue()`, `joinStream()`, and `switchThread()` for persisted threads and queued runs.
- `getSubagent()`, `getSubagentsByType()`, and `getSubagentsByMessage()` for subgraph/subagent inspection.

## Submit and resume

Use the runtime-neutral submit shape for normal chat input:

```ts
await chat.submit({ message: 'Explain streaming in Angular' });
```

Resume an interrupt with `resume`; combine it with `state` when the resume action also needs to update graph state:

## When to use
```ts
await chat.submit({
resume: { approved: true },
state: { reviewer: 'Ada' },
});
```

LangGraph run options are still accepted as the second argument:

```ts
await chat.submit(
{ message: 'Queue this run' },
{ multitaskStrategy: 'enqueue' },
);
```

## Regenerate semantics

Use `agent` whenever your UI needs to react to a live data stream from the server:
`regenerate(assistantMessageIndex)` has replace semantics. It keeps the user message before the selected assistant message, removes the selected assistant message and every later message, synchronizes that rollback with LangGraph state when possible, then reruns with no new user message appended.

- **AI / LLM responses** — stream tokens into a chat bubble as they arrive
- **Live feeds** — stock tickers, activity logs, or progress updates
- **Long-running jobs** — subscribe to backend task progress over SSE
```ts
await chat.regenerate(3);
```

For plain HTTP requests that return a single value and complete, Angular's built-in `resource()` or `httpResource()` is a better fit.
The method throws when the selected index is not an assistant message, when no preceding user message exists, or while another response is already loading.

<Callout type="warning" title="Injection context required">
`agent` must be called during construction, inside an injection
`agent()` must be called during construction, inside an injection
context (e.g. a component constructor, field initializer, or a function
passed to `runInInjectionContext`). Calling it outside an injection context
will throw.
Expand Down Expand Up @@ -68,7 +110,7 @@ For plain HTTP requests that return a single value and complete, Angular's built
icon="bolt"
href="/docs/agent/concepts/angular-signals"
>
Understand how angular integrates with Angular's reactivity model.
Understand how `agent()` integrates with Angular's reactivity model.
</Card>
</CardGroup>

Expand Down
Loading
Loading