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
10 changes: 9 additions & 1 deletion .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:

- name: Claude review
uses: anthropics/claude-code-action@806af32823ef69c8ef357086c573a902af641307 # v1
continue-on-error: true
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Post as the workflow's own GITHUB_TOKEN. Without this the action
Expand All @@ -51,10 +52,17 @@ jobs:
- Public API / DX regressions on the published `@threadplane/*` surface
- Missing or weak test coverage for the change

For docs-heavy PRs, skip generated or bulky derived files unless
they are the only evidence for a problem. In particular, do not
spend review budget on `api-docs.json`, generated public context
files, build artifacts, or lockfile-sized output. Prefer reviewing
the source templates, generators, route code, docs config, and MDX
pages that produce or consume those artifacts.

Post a concise top-level summary via `gh pr comment`. Post specific
issues as inline comments. Be brief; skip nitpicks and style unless
they affect correctness. If the PR looks good, say so briefly.
claude_args: |
--model claude-sonnet-4-6
--max-turns 15
--max-turns 30
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
2 changes: 1 addition & 1 deletion apps/website/content/docs/chat/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1672,7 +1672,7 @@
{
"name": "currentAssistantStreaming",
"type": "Signal<boolean>",
"description": "True iff there's a current (last-index) assistant message that's\nstill streaming. The bubble's own caret already signals loading;\nwe suppress the floor typing-indicator in that case so the user\ndoesn't see two loading affordances at once.\n\nMatches the same `streaming + current` condition the bubble uses\nto enable `.chat-message__caret`:\n `agent().isLoading() && i === agent().messages().length - 1`\n `i === agent().messages().length - 1`\n\nRestricted to assistant role because the caret only renders on\nassistant bubbles (`:host([data-role=\"assistant\"][data-current=...\n ][data-streaming=...])`).",
"description": "True iff there's a current (last-index) assistant message that's\nstill streaming. The bubble's own caret already signals loading;\nwe suppress the floor typing-indicator in that case so the user\ndoesn't see two loading affordances at once.\n\nMatches the same `streaming + current` condition the bubble uses\nto enable `.chat-message__caret`:\n `this.agent().isLoading() && i === this.agent().messages().length - 1`\n `i === this.agent().messages().length - 1`\n\nRestricted to assistant role because the caret only renders on\nassistant bubbles (`:host([data-role=\"assistant\"][data-current=...\n ][data-streaming=...])`).",
"optional": false
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ For me, this is the lowest-friction path for a small team — the tradeoff is th

## 3. Wire `provideChat()` and `provideAgent()`

Register both providers in your application config. Below is a minimal `app.config.ts` for a LangGraph backend (swap `@threadplane/langgraph` for `@threadplane/ag-ui` if you picked that adapter — the API surface is identical).
Register both providers in your application config. Below is a minimal `app.config.ts` for a LangGraph backend. If you picked AG-UI, component calls stay the same, but the provider import and config shape change (`apiUrl`/`assistantId` for LangGraph, `url` for AG-UI).

```ts
// src/app/app.config.ts
Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/chat/guides/error-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ if (err) {
<chat-error [agent]="chatRef" />
```

The Retry button calls `agent().retry()` for you. For non-retryable failures (e.g. `auth`), no button is shown — retrying the same bad credential would just fail again.
The Retry button calls the bound Agent's `retry()` method for you. For non-retryable failures (e.g. `auth`), no button is shown — retrying the same bad credential would just fail again.

## Custom error UI

Expand Down
8 changes: 7 additions & 1 deletion apps/website/content/docs/choosing-an-adapter/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Threadplane ships two adapters that connect a backend agent runtime to `<chat>`

## Code comparison

The two adapters share the same public surface — `provideAgent`, `injectAgent`, `AgentConfig`. Swapping is a one-line import change.
The two adapters share the same public surface — `provideAgent`, `injectAgent`, and an adapter-specific `AgentConfig`. Component code stays the same; the provider import and config shape change.

### LangGraph adapter

Expand Down Expand Up @@ -92,3 +92,9 @@ The `Agent` contract from `@threadplane/chat` is intentionally runtime-neutral.
- `@threadplane/ag-ui` consumes the AG-UI event protocol. Any backend that emits AG-UI events plugs in, regardless of what graph engine sits behind it.

Both adapters are MIT-licensed and live in the same monorepo.

## Next steps

- [LangGraph quickstart](/docs/langgraph/getting-started/quickstart) if your backend is LangGraph Platform and you want native streaming, checkpoints, threads, interrupts, and history APIs.
- [AG-UI quickstart](/docs/ag-ui/getting-started/quickstart) if your backend emits the AG-UI protocol from CrewAI, Mastra, CopilotKit runtime, or another compatible runtime.
- [Chat quickstart](/docs/chat/getting-started/quickstart) if you want to start from the drop-in `<chat>` surface and wire the adapter afterward.
2 changes: 1 addition & 1 deletion apps/website/content/docs/langgraph/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"name": "AgentLifecycleRegistry",
"kind": "class",
"description": "Optional registry that collects per-instance agent lifecycles within\nan Angular injection context. External instrumentation packages\n(e.g. cockpit-telemetry) provide this token and read from it.\n\n`@threadplane/langgraph` does NOT provide this itself — `agent()` writes to\nthe registry only when an external consumer has provided it.",
"description": "Optional registry that collects per-instance agent lifecycles within\nan Angular injection context. External instrumentation packages\n(e.g. cockpit-telemetry) provide this token and read from it.\n\n`@threadplane/langgraph` does NOT provide this itself — the configured agent\ninstance writes to the registry only when an external consumer has provided it.",
"params": [],
"examples": [],
"properties": [
Expand Down
6 changes: 3 additions & 3 deletions apps/website/content/prompts/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
Add angular to my Angular 20+ application.

Install: npm install @threadplane/langgraph@latest
Install: npm install @threadplane/chat @threadplane/langgraph

1. In app.config.ts, add provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat' }) to the providers array. Import it from '@threadplane/langgraph'.

2. Create a ChatComponent that calls injectAgent() in the constructor or as a field initializer. injectAgent() MUST be called inside an Angular injection context — constructor or field initializer is correct; ngOnInit is not.
2. Create a component that imports ChatComponent from '@threadplane/chat' and calls injectAgent() in the constructor or as a field initializer. injectAgent() MUST be called inside an Angular injection context — constructor or field initializer is correct; ngOnInit is not.

3. The component template should loop over chat.messages() using @for and render each message's content. Add an input field and a button that calls chat.submit({ message: inputValue }).
3. The component template can start with <chat [agent]="chat" />. For custom layouts, loop over chat.messages() with @for and call chat.submit({ message: inputValue }) from your input.

4. In app.config.ts provideAgent call, the apiUrl should point to the LangGraph server. For local dev this is http://localhost:2024. For production use the LangGraph Platform URL from environment.ts.

Expand Down
2 changes: 2 additions & 0 deletions apps/website/content/prompts/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const transport = new MockAgentTransport();
providers: [provideAgent({ transport, assistantId: 'test', apiUrl: '' })]
const chat = injectAgent();

In Angular tests, put the provider in TestBed.configureTestingModule({ providers: [...] }) or the component's providers array, then read chat with injectAgent() from an Angular injection context.

To emit a streaming response:
transport.emit([
{ type: 'values', values: { messages: [] } },
Expand Down
8 changes: 4 additions & 4 deletions apps/website/content/prompts/thread-persistence.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
Add thread persistence to my Angular component that uses angular, so conversations survive page refresh.

1. On component init, read the stored thread ID: const storedId = localStorage.getItem('chat-thread-id').
1. Put thread persistence in an injectable service so provider config can read it from Angular DI.

2. Create a signal: threadId = signal<string | null>(storedId).
2. In that service, create a signal from storage: threadId = signal<string | null>(localStorage.getItem('chat-thread-id')).

3. Pass it to provideAgent: provideAgent({ assistantId: 'chat', threadId: this.threadId, onThreadId: (id) => { this.threadId.set(id); localStorage.setItem('chat-thread-id', id); } }).
3. Configure the agent with a provider factory: provideAgent(() => { const memory = inject(ThreadMemory); return { assistantId: 'chat', threadId: memory.threadId, onThreadId: (id) => memory.remember(id) }; }).

4. The onThreadId callback fires once when the server creates a new thread. After that, the same thread ID is reused and the full conversation history is restored from the LangGraph server.

5. To start a new conversation, call this.threadId.set(null) — this causes the injected agent to create a fresh thread on the next submit.
5. To start a new conversation, call memory.threadId.set(null) and clear storage — this causes the injected agent to create a fresh thread on the next submit.

No changes to the template are needed.
20 changes: 10 additions & 10 deletions apps/website/scripts/generate-whitepaper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ Context: Angular teams building AI agent applications must wire streaming transp

Cover:
- Why streaming state is hard in Angular (zone.js, change detection, timing)
- The Angular signals approach: how agent() exposes messages() as Signal<Message[]>
- The Angular signals approach: how injectAgent() exposes messages() as Signal<Message[]>
- How isLoading() lets developers drive loading UI without polling
- Code example: minimal agent() setup (TypeScript snippet, 8-12 lines)
- Code example: minimal provideAgent() plus injectAgent() setup (TypeScript snippet, 8-12 lines)
- Production checklist item: "Are your message signals OnPush-compatible?"

Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`,
Expand All @@ -106,7 +106,7 @@ Cover:
- The threadId signal and onThreadId callback pattern
- How to persist threadId to localStorage and restore on mount
- Thread list UI and switching between conversations
- Code example: provideAgent() plus agent({ threadId }) with thread persistence (8-12 lines)
- Code example: provideAgent() with a threadId signal plus injectAgent() in the component (8-12 lines)
- Production checklist item: "Does your agent UI resume threads correctly after a browser refresh?"

Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`,
Expand Down Expand Up @@ -142,7 +142,7 @@ Context: Production agents that take consequential actions — sending emails, d
Cover:
- The LangGraph interrupt() and Command.RESUME pattern
- Why polling and custom websocket approaches are brittle
- The interrupt() signal in agent() and how it maps to approval state
- The interrupt() signal returned by injectAgent() and how it maps to approval state
- <chat-interrupt> headless and <chat-interrupt-panel> prebuilt
- The three approval actions: approve, edit, cancel — and how each maps to a resume command
- Code example: interrupt signal binding (8-12 lines)
Expand Down Expand Up @@ -221,20 +221,20 @@ Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engi
},
{
id: 'agent-api',
title: 'The agent() API',
title: 'The injectAgent() API',
prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent UI in Angular".

Chapter topic: The agent() API
Chapter topic: The injectAgent() API

Context: @threadplane/langgraph exposes an Angular signals-based API for connecting LangGraph agents to Angular components. The core primitive is agent() — a function that returns reactive signals wired directly to the agent stream, with no manual subscription management, no zone-patching, and no token accumulation logic.
Context: @threadplane/langgraph exposes an Angular signals-based API for connecting LangGraph agents to Angular components. The core primitive is injectAgent() — an Angular DI helper that returns reactive signals wired directly to the configured agent stream, with no manual subscription management, no zone-patching, and no token accumulation logic.

Cover:
- How agent() returns a LangGraphAgent with typed signals: messages(), isLoading(), error(), interrupt(), and langGraph* raw signals
- How injectAgent() returns a LangGraphAgent with typed signals: messages(), isLoading(), error(), interrupt(), and langGraph* raw signals
- The provideAgent() provider and how it configures the agent endpoint and stream transport
- Why the Angular signals design works with OnPush change detection out of the box
- How to bind agent state directly in Angular templates without async pipe or manual subscriptions
- Code example: minimal agent() setup with template binding (10-14 lines)
- The contrast: what the equivalent hand-rolled code looks like vs. agent() in 3 lines
- Code example: minimal provideAgent() setup with injectAgent() template binding (10-14 lines)
- The contrast: what the equivalent hand-rolled code looks like vs. injectAgent() in 3 lines

Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`,
},
Expand Down
6 changes: 3 additions & 3 deletions apps/website/src/app/ag-ui/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default async function AgUiPage() {
maxWidth: 680,
}}
>
Build an Angular agent UI on any AG-UI-compatible runtime — LangGraph, CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, CopilotKit. Same primitives, same chat surface, same testing story.
Build an Angular agent UI on any AG-UI-compatible runtime — CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, CopilotKit, or LangGraph fronted by AG-UI. Same Agent contract and chat surface; runtime-specific history and checkpoint behavior stays with the backend.
</p>
<div style={{ display: 'flex', justifyContent: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
<Button variant="primary" size="lg" href="/docs/ag-ui/getting-started/quickstart">Get started</Button>
Expand Down Expand Up @@ -111,10 +111,10 @@ export default async function AgUiPage() {
id="primitives"
eyebrow="Same primitives"
headline="Drop-in for everything @threadplane/chat ships."
body="provideAgent registers an AG-UI client and exposes the same Agent contract that @threadplane/langgraph provides. Every chat primitive — durable threads, interrupts, subagents, generative UI, citations — works against any AG-UI runtime."
body="provideAgent registers an AG-UI client and exposes the same Agent contract that @threadplane/langgraph provides. Chat rendering, status, tool calls, generative UI, and citations use the same Angular primitives; durable checkpointed threads and history depend on the backend protocol, so use @threadplane/langgraph when you need the native LangGraph thread API."
bullets={[
'provideAgent + injectAgent — same names across adapters',
'Identical Agent contract: messages() / status() / interrupt() / reload()',
'Shared Agent contract: messages() / status() / reload()',
'Same A2UI surface, themes, and citations rendering',
'MockAgentTransport works the same way for tests',
]}
Expand Down
15 changes: 10 additions & 5 deletions apps/website/src/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ const BACKENDS: Backend[] = [
{
title: 'LangGraph',
blurb: 'For LangChain & LangGraph backends.',
install: 'npm i @threadplane/langgraph',
install: 'npm i @threadplane/chat @threadplane/langgraph',
href: '/docs/langgraph/getting-started/quickstart',
logoSrc: '/logos/langgraph.svg',
attribution: 'LangChain',
},
{
title: 'AG-UI',
blurb: 'For CrewAI, Mastra, Pydantic AI, Strands, and more.',
install: 'npm i @threadplane/ag-ui',
install: 'npm i @threadplane/chat @threadplane/ag-ui @ag-ui/client @ag-ui/core',
href: '/docs/ag-ui/getting-started/quickstart',
logoSrc: '/logos/runtimes/copilotkit.svg',
attribution: 'AG-UI · CopilotKit',
Expand Down Expand Up @@ -347,8 +347,9 @@ export default function DocsLandingPage() {
maxWidth: '52ch',
}}
>
A suite of MIT-licensed libraries for streaming agent interfaces.
Pick your backend to get started.
Streaming agent interfaces with runtime adapters, a shared Agent contract,
and a drop-in chat surface. Most packages are MIT; @threadplane/chat is
dual-licensed for noncommercial/evaluation and commercial production use.
</p>
</div>
</Container>
Expand All @@ -374,7 +375,7 @@ export default function DocsLandingPage() {
<code style={snippetCodeStyle}>{b.install}</code>
<CopyButton text={b.install} />
</div>
<span style={ctaStyle}>Quickstart →</span>
<span style={ctaStyle}>Adapter quickstart →</span>
</Card>
</Link>
))}
Expand All @@ -384,6 +385,10 @@ export default function DocsLandingPage() {
<Link href="/docs/choosing-an-adapter" style={helperLinkStyle}>
Choosing an adapter →
</Link>
{' '}Want the drop-in UI first?{' '}
<Link href="/docs/chat/getting-started/quickstart" style={helperLinkStyle}>
Chat quickstart →
</Link>
</p>
</Container>
</Section>
Expand Down
7 changes: 5 additions & 2 deletions apps/website/src/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ function buildLlmsTxt(): string {
'- @threadplane/ag-ui — adapter for any AG-UI-compatible backend (CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, AWS Strands, CopilotKit runtime)',
'- @threadplane/render — generative UI runtime (Vercel json-render + Google A2UI)',
'- @threadplane/a2ui — protocol types, JSONL parser, dynamic value resolver, and pointer helpers for A2UI streams',
'- @threadplane/middleware — backend LangGraph helpers for browser-executed client tools',
'- @threadplane/licensing — offline license verification and non-blocking package check helpers',
'- @threadplane/telemetry — browser, Node, and shared telemetry helpers with privacy controls',
'',
'## Install',
'# LangGraph backend:',
'npm install @threadplane/chat @threadplane/langgraph',
'# LangGraph backend with browser client tools:',
'npm install @threadplane/chat @threadplane/langgraph @threadplane/middleware',
'# AG-UI backend:',
'npm install @threadplane/chat @threadplane/ag-ui',
'npm install @threadplane/chat @threadplane/ag-ui @ag-ui/client @ag-ui/core',
'',
'## Key API (symmetric across adapters)',
'- provideAgent(config) — wires the adapter into Angular DI. Same name across @threadplane/langgraph and @threadplane/ag-ui.',
Expand All @@ -59,7 +62,7 @@ function buildLlmsTxt(): string {
'// Template: <chat [agent]="agent" />',
'',
'## License',
'MITfree for any use, commercial or noncommercial.',
'Most packages are MIT. @threadplane/chat is free for noncommercial/evaluation use and commercially licensed for production use.',
'',
'## Full reference',
'https://threadplane.ai/llms-full.txt',
Expand Down
Loading
Loading