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
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Agent Architecture

How AI agents work — the planning, execution, and tool-calling lifecycle that `injectAgent()` connects your Angular app to. This page shows you the Python patterns that power modern agents and exactly how each pattern surfaces in Angular through `@threadplane/langgraph`.
How AI agents work — the planning, execution, and tool-calling lifecycle that `injectAgent()` connects your Angular app to. Let's walk the Python patterns behind modern agents and see exactly how each one surfaces in Angular through `@threadplane/langgraph`.

<Callout type="info" title="Python + Angular, both sides">
Every section below shows the Python backend code first, then the Angular frontend code that consumes it. You need both halves to build a production agent application — LangGraph handles the intelligence, `injectAgent()` handles the reactivity.
</Callout>

## The Agent Loop

Every agent follows a five-phase cycle. Understanding this cycle is critical because each phase maps to a specific `injectAgent()` signal in your Angular app.
Every agent follows a five-phase cycle. It's worth understanding, because each phase maps to a specific `injectAgent()` signal in your Angular app.

<Steps>
<Step title="Receive">
Expand Down Expand Up @@ -194,11 +194,11 @@ export class ReactAgentComponent {
</Tab>
</Tabs>

The key insight: `should_continue` is the decision point. If the LLM's response contains `tool_calls`, the graph routes to the `tools` node. If not, it ends. After tools execute, the graph loops back to `model` so the LLM can reason about the tool results. This loop continues until the LLM responds without requesting any tools.
Here's the key insight: `should_continue` is the decision point. If the LLM's response contains `tool_calls`, the graph routes to the `tools` node. If not, it ends. After tools execute, the graph loops back to `model` so the LLM can reason about the tool results. This loop continues until the LLM responds without requesting any tools.

## Tool Calling Deep Dive

Tools are how agents interact with the outside world. Understanding both the Python definition and the Angular consumption is essential.
Tools are how agents interact with the outside world. You'll want both halves here — the Python definition and the Angular consumption.

### Defining Tools in Python

Expand Down Expand Up @@ -611,7 +611,7 @@ When you submit from a previous checkpoint, LangGraph creates a new branch from

## Choosing an Architecture

Not every application needs a multi-agent swarm. Here is a decision guide for picking the right level of complexity.
Not every application needs a multi-agent swarm. Here's how I'd pick the right level of complexity — and why simpler usually wins until it can't.

### Single Agent with Tools

Expand Down
20 changes: 10 additions & 10 deletions apps/website/content/docs/langgraph/concepts/agent-contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The `Agent` contract is the spine between runtime adapters and chat UI.

`@threadplane/chat` owns the contract. `@threadplane/langgraph` and `@threadplane/ag-ui` produce objects that satisfy it. Chat primitives and compositions consume it without knowing which runtime is behind the stream.

This matters because the UI should not care whether a response came from LangGraph Platform, AG-UI, a local mock, or a custom HTTP service. The boundary is explicit: adapters translate runtime events into Angular signals and a small action surface.
The UI shouldn't care whether a response came from LangGraph Platform, AG-UI, a local mock, or a custom HTTP service. That's the point. The boundary is explicit: adapters translate runtime events into Angular signals and a small action surface.

```text
LangGraph Platform -- @threadplane/langgraph --+
Expand Down Expand Up @@ -61,7 +61,7 @@ await agent.submit({

The input can carry a new user message, an interrupt resume payload, a state patch, or a combination. The adapter decides how that maps to the backend.

This matters because chat UI can send user intent without learning backend protocol details. LangGraph can turn `resume` into a command. An AG-UI adapter can call `runAgent()`. A test mock can just record the call.
So chat UI sends user intent without learning a single backend protocol detail. LangGraph turns `resume` into a command. An AG-UI adapter calls `runAgent()`. A test mock just records the call.

The second argument is intentionally small at the contract layer:

Expand All @@ -88,7 +88,7 @@ Signals are the stable read model.
| `interrupt?.()` | Current human-in-the-loop pause, if supported. |
| `subagents?.()` | Delegated work keyed by tool-call id, if supported. |

The optional signals are important. A simple echo adapter should not fake interrupts or subagents. Components that need those concepts feature-detect the signal and render a neutral fallback when it is absent.
The optional signals matter. A simple echo adapter shouldn't fake interrupts or subagents. Components that need those concepts feature-detect the signal and render a neutral fallback when it's absent.

## Events

Expand All @@ -108,7 +108,7 @@ Current event variants are deliberately narrow:
| `state_update` | Sync state intended for render/generative UI stores. |
| `custom` | Runtime-specific escape hatch with `name` and `data`. |

Do not mirror `messages`, `status`, `toolCalls`, `interrupt`, or `subagents` through `events$`. Put those on signals. Duplicating state creates ordering bugs and makes components guess which source wins.
Don't mirror `messages`, `status`, `toolCalls`, `interrupt`, or `subagents` through `events$`. Put those on signals. Duplicating state creates ordering bugs and makes components guess which source wins.

## Adapters And Transports

Expand All @@ -124,7 +124,7 @@ Do not mirror `messages`, `status`, `toolCalls`, `interrupt`, or `subagents` thr

## Lifecycle

The UI lifecycle is intentionally boring.
The UI lifecycle is intentionally boring — and boring is the goal.

1. User input calls `submit({ message })`.
2. Adapter marks the run active through `status()` and `isLoading()`.
Expand Down Expand Up @@ -154,16 +154,16 @@ For LangGraph-specific code, use `mockLangGraphAgent()` or `MockAgentTransport`

For AG-UI integration, use `FakeAgent` or `provideFakeAgent()` from `@threadplane/ag-ui`.

This matters because the contract is the seam you can test cheaply. Most component tests should not need a network, LangGraph server, or AG-UI runtime.
The contract is the seam you can test cheaply. Most component tests shouldn't need a network, a LangGraph server, or an AG-UI runtime.

## What It Is Not

The `Agent` contract is not a backend protocol. It does not define SSE frames, AG-UI event names, LangGraph thread state, or tool execution semantics.
The `Agent` contract is not a backend protocol. It doesn't define SSE frames, AG-UI event names, LangGraph thread state, or tool execution semantics.

It is not a message database. Persistence belongs to the runtime or application state, then gets projected back through signals.
It's not a message database. Persistence belongs to the runtime or application state, then gets projected back through signals.

It is not a full orchestration API. LangGraph-specific operations such as branch selection, queued runs, checkpoint history, and raw SDK messages stay on `LangGraphAgent`.
It's not a full orchestration API. LangGraph-specific operations such as branch selection, queued runs, checkpoint history, and raw SDK messages stay on `LangGraphAgent`.

It is not a UI renderer. Rendering belongs to `@threadplane/chat`, `@threadplane/render`, and A2UI surfaces. The agent only exposes the state and events those renderers need.
It's not a UI renderer. Rendering belongs to `@threadplane/chat`, `@threadplane/render`, and A2UI surfaces. The agent only exposes the state and events those renderers need.

Keep that boundary sharp. It makes adapters replaceable, tests smaller, and chat components more predictable.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Angular Signals

Angular Signals are the reactive primitive that powers `injectAgent()`. If you're coming from a Python AI/agent background and wondering how Angular handles real-time streaming data, this page is your guide. Every property on a LangGraphAgent is a Signal, which means your templates update automatically as tokens arrive — no manual subscriptions, no async pipes, no RxJS boilerplate.
Angular Signals are the reactive primitive that powers `injectAgent()`. If you're coming from a Python AI/agent background and wondering how Angular handles real-time streaming data, you're in the right place. Every property on a LangGraphAgent is a Signal, so your templates update automatically as tokens arrive — no manual subscriptions, no async pipes, no RxJS boilerplate.

<Callout type="info" title="For Python developers">
Think of Signals like a Python property with built-in change notification. When the value changes, every consumer — templates, computed values, effects — re-evaluates automatically. If you've used Pydantic models with validators that react to field changes, Signals are the Angular equivalent but deeply integrated into the rendering engine.
Expand Down Expand Up @@ -28,7 +28,7 @@ const doubled = computed(() => count() * 2);
console.log(doubled()); // 4
```

The key insight: Angular knows which Signals a template reads. When those Signals change, Angular re-renders only the affected parts of the DOM. No diffing the entire tree, no zone.js overhead.
Here's the key insight: Angular knows which Signals a template reads. When those Signals change, it re-renders only the affected parts of the DOM. No diffing the entire tree, no zone.js overhead.

## How `injectAgent` Uses Signals Internally

Expand Down Expand Up @@ -400,7 +400,7 @@ With older Observable-based patterns, you had to call `ChangeDetectorRef.markFor

## Python Agent to Angular Signals

The real power of `injectAgent()` is how it pairs a Python LangGraph agent with Angular Signals. The agent defines the logic; Signals surface the results in real time.
Where `injectAgent()` really earns its keep is how it pairs a Python LangGraph agent with Angular Signals. The agent defines the logic; Signals surface the results in real time.

<Tabs>
<Tab label="agent.py">
Expand Down Expand Up @@ -501,7 +501,7 @@ When the Python agent calls `search_knowledge_base`, the tool call streams to An

## Performance: Signals vs Alternatives

High-frequency token streaming puts unique pressure on a frontend framework. Here's why Signals with OnPush outperform the alternatives.
High-frequency token streaming puts real pressure on a frontend framework. Here's why Signals with OnPush hold up better than the alternatives.

| Approach | Token update cost | Memory overhead | Cleanup required |
|---|---|---|---|
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# LangGraph Basics

LangGraph is a framework for building stateful AI agents as directed graphs. If you're an Angular developer building AI-powered applications, this page teaches you how LangGraph agents work and why `injectAgent()` is the natural bridge between your frontend and your agent backend.
LangGraph is a framework for building stateful AI agents as directed graphs. If you're an Angular developer building AI-powered applications, let's look at how LangGraph agents work and why `injectAgent()` is the natural bridge between your frontend and your agent backend.

<Callout type="info" title="Why graphs?">
Graphs give you explicit control over agent behavior. Instead of a black-box prompt-and-pray approach, you define exactly how your agent reasons, when it calls tools, and where it pauses for human input. Every step is visible, testable, and debuggable.
Expand Down Expand Up @@ -126,7 +126,7 @@ protected readonly chat = injectAgent();

## Agent Patterns

The power of LangGraph is in the patterns you can build. Each pattern maps to specific `injectAgent()` signals.
LangGraph shines in the patterns you can build. Each one maps to specific `injectAgent()` signals.

### Pattern 1: ReAct Agent (Tool Calling)

Expand Down Expand Up @@ -284,7 +284,7 @@ const chat = injectAgent<ChatState>();

## How `injectAgent()` Bridges the Gap

Here's why `injectAgent()` is the natural Angular companion for LangGraph:
Here's why `injectAgent()` is the natural Angular companion for LangGraph.

<Tabs>
<Tab label="The data flow">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ LangGraph Platform owns the state. Angular owns the view. `injectAgent()` is the

In a traditional Angular app, state lives in an NgRx store or a signals-based service. In a LangGraph app, **the agent's state lives on the server** — in LangGraph Platform's checkpoint store. Your Angular app is a stateless view layer that reads state through signals as the agent streams it back.

This inversion is intentional. Agent state can span multiple LLM calls, tool executions, and human-in-the-loop interrupts. It needs to survive browser refreshes, reconnections, and even server deployments. A server-side checkpoint store handles all of that automatically. Your Angular app just calls `.submit()` and reads signals.
This inversion is intentional. Agent state can span multiple LLM calls, tool executions, and human-in-the-loop interrupts. It has to survive browser refreshes, reconnections, and even server deployments. A server-side checkpoint store handles all of that for you. Your Angular app just calls `.submit()` and reads signals.

<Steps>
<Step title="User submits input">
Expand All @@ -37,7 +37,7 @@ Components using `OnPush` re-render only when signal values change. No manual `d

## Python State Design

On the Python side, your agent's state is a `TypedDict`. The fields you define here are exactly what `agent<T>()` exposes in TypeScript. Getting the Python state design right is the most important architectural decision in your agent.
On the Python side, your agent's state is a `TypedDict`. The fields you define here are exactly what `agent<T>()` exposes in TypeScript. For me, getting the Python state design right is the most important architectural decision in your agent — everything downstream inherits its shape, so it's worth slowing down for.

### The TypedDict Pattern

Expand Down Expand Up @@ -276,7 +276,7 @@ const score = computed(() => agent.value().analysis?.score ?? 0);

## Thread State vs Application State

There are two kinds of state in a LangGraph Angular app, and keeping them separate makes your code much easier to reason about.
There are two kinds of state in a LangGraph Angular app, and keeping them separate makes your code far easier to reason about.

**Thread state** is owned by LangGraph Platform. You read it through `injectAgent()` signals. You never write to it directly — you only send new input via `.submit()`.

Expand Down
6 changes: 5 additions & 1 deletion apps/website/content/docs/langgraph/guides/deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Deploy your LangGraph agent to the cloud and ship your Angular frontend to produ

Your agent code needs a `langgraph.json` manifest at the project root. This file tells LangGraph Cloud how to build and serve your agent.

Let's start there.

```json
{
"dependencies": ["."],
Expand Down Expand Up @@ -128,6 +130,8 @@ Do not ship LangSmith or LangGraph API keys in an Angular bundle. The default `F

For production, put a same-origin backend route, edge function, or API gateway in front of LangGraph. The browser calls your relative URL, and that server-side layer adds the deployment credentials.

For me, the same-origin proxy is worth the extra hop. You give up the simplicity of pointing Angular straight at the deployment, but your keys never leave the server, and HTTP-only cookies just work.

```typescript
// environment.prod.ts
export const environment = {
Expand Down Expand Up @@ -183,7 +187,7 @@ During local development with `langgraph dev`, CORS is permissive by default. Yo

## Error boundaries

Production apps need graceful error handling. Build a reactive error boundary using `injectAgent()` signals.
Production apps need graceful error handling. Let's build a reactive error boundary using `injectAgent()` signals.

```typescript
import { ChangeDetectionStrategy, Component, computed } from '@angular/core';
Expand Down
8 changes: 5 additions & 3 deletions apps/website/content/docs/langgraph/guides/interrupts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Use interrupts when an agent action is irreversible (sending an email, placing a

## The Interrupt Lifecycle

Before diving into code, understand the five-stage lifecycle that every interrupt follows:
Before diving into code, let's walk the five-stage lifecycle that every interrupt follows:

<Steps>
<Step title="Agent Plans">
Expand Down Expand Up @@ -276,7 +276,7 @@ export class ApprovalComponent {

## Multi-Step Approval Pattern

Some workflows require multiple approvals in sequence. For example, an agent that plans a multi-step deployment might need approval at each stage. Each node in the graph can raise its own interrupt.
Some workflows need multiple approvals in sequence. An agent that plans a multi-step deployment might need a sign-off at each stage. Each node in the graph can raise its own interrupt.

<Tabs>
<Tab label="agent.py">
Expand Down Expand Up @@ -496,7 +496,9 @@ Define your interrupt payload interfaces alongside your Python state schema. Thi

## Timeout Handling

Interrupts pause graph execution indefinitely by default — the agent waits until a human responds. In production, you often need to handle cases where no one responds within a reasonable time. There are two strategies for managing interrupt timeouts.
Interrupts pause graph execution indefinitely by default — the agent waits until a human responds. In production, you often need to handle the case where no one responds within a reasonable time. There are two strategies for managing interrupt timeouts.

For me, the server-side timeout is the safer default. It costs you a background job to run and maintain, but it fires even if the user closed the tab — which is exactly when you most need it to.

**Server-side timeout with a background task:** Schedule a background job that checks for stale interrupts and resumes them with a default decision.

Expand Down
4 changes: 2 additions & 2 deletions apps/website/content/docs/langgraph/guides/lifecycle.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Agent Lifecycle Signals

The `@threadplane/langgraph` library exposes per-agent lifecycle signals on every `LangGraphAgent` returned by `injectAgent()`. These are timestamps and classifications derived from the existing stream — useful for debugging, custom dashboards, or telemetry integrations.
The `@threadplane/langgraph` library exposes per-agent lifecycle signals on every `LangGraphAgent` returned by `injectAgent()`. They're timestamps and classifications derived from the stream you already have — handy for debugging, custom dashboards, or telemetry integrations.

## Interface

Expand Down Expand Up @@ -70,7 +70,7 @@ export class MyComponent {
}
```

For app-wide instrumentation, provide `AgentLifecycleRegistry` and read the lifecycles registered by agents created in that injection context:
For app-wide instrumentation, provide `AgentLifecycleRegistry` and read back the lifecycles registered by agents created in that injection context:

```typescript
import { ApplicationConfig, inject } from '@angular/core';
Expand Down
Loading
Loading