Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ba89f13
feat(chat): ship-readiness polish — Tailwind, auto-scroll, markdown, …
blove Apr 6, 2026
2dccb0a
Rebrand to Angular Stream Resource (#28)
blove Apr 6, 2026
65f13df
feat(website): add narrative sections, pilot-to-prod page, and rebran…
blove Apr 6, 2026
1c9e7b6
fix(website): replace unsourced stats with verified Gartner and Stack…
blove Apr 6, 2026
16d37f7
merge: resolve conflicts with main
blove Apr 6, 2026
40a967b
docs: add website audit and lead generation specs
blove Apr 6, 2026
5e52aeb
docs: add implementation plans for lead gen and website audit
blove Apr 6, 2026
b8e8ba2
chore: install resend and react-email dependencies
blove Apr 6, 2026
e53d86d
feat: add shared resend module with audience helper
blove Apr 6, 2026
fd13886
feat: add lead notification email template
blove Apr 6, 2026
7f16a19
feat: add whitepaper download email template
blove Apr 6, 2026
e41dd9f
feat: add newsletter welcome email template
blove Apr 6, 2026
c948d1a
feat: wire /api/leads to Resend email + audience
blove Apr 6, 2026
c33e2e1
feat: wire /api/whitepaper-signup to Resend email delivery
blove Apr 6, 2026
4b5d310
feat: add /api/newsletter route with Resend welcome email
blove Apr 6, 2026
5c700cf
fix: convert email templates to plain HTML to avoid React dual-instan…
blove Apr 6, 2026
fba8c65
fix: lazy-init Resend client to gracefully handle missing API key
blove Apr 6, 2026
3092ca4
fix: make ChatFeaturesSection responsive on mobile
blove Apr 6, 2026
150ca98
fix: stack FairComparisonSection rows vertically on mobile
blove Apr 6, 2026
1aaaec2
fix: increase touch targets to meet WCAG 44px minimum
blove Apr 6, 2026
5975f8f
fix: enforce 12px minimum font size on progress bar labels
blove Apr 6, 2026
b65d9e2
feat: add social proof badge strip below stats
blove Apr 6, 2026
fcfe07b
feat: add newsletter signup form to footer
blove Apr 6, 2026
5f62f73
feat: restructure white paper section with soft gate
blove Apr 6, 2026
32b2221
feat: add OpenGraph and Twitter Card meta tags
blove Apr 6, 2026
888701c
merge: resolve ProblemSection conflict with main (keep our stat + fon…
blove Apr 6, 2026
8f6ec67
feat: rebrand from streamResource to agent() — @cacheplane/angular
blove Apr 7, 2026
5dbeb85
merge: resolve conflicts with main (keep renamed versions)
blove Apr 7, 2026
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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx lint stream-resource
- run: npx nx test stream-resource --coverage
- run: npx nx build stream-resource --configuration=production
- run: npx nx lint angular
- run: npx nx test angular --coverage
- run: npx nx build angular --configuration=production

website:
name: Website — lint / build
Expand Down Expand Up @@ -224,7 +224,7 @@ jobs:
run: |
mkdir -p .vercel
cat > .vercel/project.json <<EOF
{"projectId":"${{ secrets.VERCEL_WEBSITE_PROJECT_ID }}","orgId":"${{ secrets.VERCEL_ORG_ID }}","projectName":"stream-resource"}
{"projectId":"${{ secrets.VERCEL_WEBSITE_PROJECT_ID }}","orgId":"${{ secrets.VERCEL_ORG_ID }}","projectName":"angular"}
EOF
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
rm -rf .vercel/output
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
LANGSMITH_TRACING: "true"
LANGSMITH_PROJECT: stream-resource-e2e-ci
LANGSMITH_PROJECT: angular-e2e-ci

- name: Wait for server to be ready
run: |
Expand All @@ -66,6 +66,6 @@ jobs:
curl -sf http://localhost:2024/ok || (echo "Server failed to start after 60s" && exit 1)

- name: Run e2e tests
run: npx nx e2e stream-resource-e2e
run: npx nx e2e angular-e2e
env:
LANGGRAPH_URL: http://localhost:2024
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npx nx test mcp --skip-nx-cache
- run: npx nx test stream-resource
- run: npx nx build stream-resource --configuration=production
- run: npx nx test angular
- run: npx nx build angular --configuration=production
- name: Publish to npm
run: npx nx-release-publish stream-resource
run: npx nx-release-publish angular
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4 changes: 2 additions & 2 deletions COMMERCIAL.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Commercial Licensing

`@cacheplane/stream-resource` is source-available software dual-licensed under:
`@cacheplane/angular` is source-available software dual-licensed under:

- **PolyForm Noncommercial 1.0.0** — free for noncommercial use (see [`LICENSE`](./LICENSE))
- **Angular Stream Resource Commercial License** — required for commercial use (see [`LICENSE-COMMERCIAL`](./LICENSE-COMMERCIAL))
- **Angular Agent Framework Commercial License** — required for commercial use (see [`LICENSE-COMMERCIAL`](./LICENSE-COMMERCIAL))

## What requires a commercial license?

Expand Down
2 changes: 1 addition & 1 deletion LICENSE-COMMERCIAL
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Commercial License
Copyright (c) 2026 Brian Love d/b/a cacheplane. All rights reserved.

This Commercial License ("License") governs commercial use of the
@cacheplane/stream-resource software ("Software"). Use of the Software for
@cacheplane/angular software ("Software"). Use of the Software for
commercial purposes requires a valid license purchased from cacheplane.

--- LICENSE TIERS ---
Expand Down
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<p align="center">
<img
src="https://stream-resource.dev/assets/hero.svg"
alt="Angular Stream Resource — The Enterprise Streaming Resource for LangChain and Angular"
alt="Angular Agent Framework — The Enterprise Streaming Resource for LangChain and Angular"
width="100%"
/>
</p>
Expand All @@ -11,13 +11,13 @@
</p>

<p align="center">
<a href="https://www.npmjs.com/package/@cacheplane/stream-resource">
<img alt="npm version" src="https://img.shields.io/npm/v/@cacheplane%2Fstream-resource?color=6C8EFF&labelColor=080B14&style=flat-square" />
<a href="https://www.npmjs.com/package/@cacheplane/angular">
<img alt="npm version" src="https://img.shields.io/npm/v/@cacheplane%2Fangular?color=6C8EFF&labelColor=080B14&style=flat-square" />
</a>
<a href="./LICENSE">
<img alt="License: PolyForm Noncommercial + Commercial" src="https://img.shields.io/badge/license-PolyForm%20Noncommercial%20%2B%20Commercial-6C8EFF?labelColor=080B14&style=flat-square" />
</a>
<a href="https://angular.dev">
<a href="https://stream-resource.dev">
<img alt="Angular 20+" src="https://img.shields.io/badge/Angular-20%2B-6C8EFF?labelColor=080B14&style=flat-square" />
</a>
<a href="https://langchain-ai.github.io/langgraph/">
Expand All @@ -27,14 +27,14 @@

---

`streamResource()` is the Angular equivalent of LangGraph's React `useStream()` hook — a full-parity implementation built on Angular Signals and the Angular Resource API. It gives enterprise Angular teams the same production-grade streaming primitives available to React developers on LangChain, without compromises or workarounds. 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 — a full-parity implementation built on Angular Signals and the Angular Resource API. It gives enterprise Angular teams the same production-grade streaming primitives available to React developers on LangChain, without compromises or workarounds. 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.

---

## Install

```bash
npm install @cacheplane/stream-resource
npm install @cacheplane/angular
```

**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,7 +45,7 @@ npm install @cacheplane/stream-resource

```typescript
import { Component } from '@angular/core';
import { streamResource } from '@cacheplane/stream-resource';
import { agent } from '@cacheplane/angular';
import type { BaseMessage } from '@langchain/core/messages';

@Component({
Expand All @@ -65,7 +65,7 @@ import type { BaseMessage } from '@langchain/core/messages';
`,
})
export class ChatComponent {
chat = streamResource<{ messages: BaseMessage[] }>({
chat = agent<{ messages: BaseMessage[] }>({
apiUrl: 'https://your-langgraph-platform.com',
assistantId: 'my-agent',
messagesKey: 'messages',
Expand All @@ -83,7 +83,7 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp

## Feature Comparison

| Feature | `streamResource()` (Angular) | `useStream()` (React) |
| Feature | `agent()` (Angular) | `useStream()` (React) |
|---|---|---|
| Streaming state as reactive primitives | Angular Signals | React state |
| Messages signal | `messages()` | `messages` |
Expand All @@ -99,7 +99,7 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
| Submit | `submit(values, opts?)` | `submit(values, opts?)` |
| Stop | `stop()` | `stop()` |
| 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 @@ -111,12 +111,12 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
<p align="center">
<img
src="https://stream-resource.dev/assets/arch-diagram.svg"
alt="Angular Stream Resource architecture: Angular Component → streamResource() → StreamManager Bridge → LangGraph Platform, with signals returned reactively"
alt="Angular Agent Framework architecture: Angular Component → agent() → StreamManager Bridge → LangGraph Platform, with signals returned reactively"
width="100%"
/>
</p>

`streamResource()` creates 12 `BehaviorSubject`s at injection-context time — once, at component construction. The `StreamManager` bridge (the only file that touches `@langchain/langgraph-sdk` internals) pushes stream events into those subjects. `toSignal()` converts each subject to an Angular Signal, also at construction time. Dynamic actions (`submit`, `stop`, `switchThread`) push into the existing subjects — no new subjects are ever created after construction. This architecture is required because `toSignal()` must be called in an injection context and cannot be called again later.
`agent()` creates 12 `BehaviorSubject`s at injection-context time — once, at component construction. The `StreamManager` bridge (the only file that touches `@langchain/langgraph-sdk` internals) pushes stream events into those subjects. `toSignal()` converts each subject to an Angular Signal, also at construction time. Dynamic actions (`submit`, `stop`, `switchThread`) push into the existing subjects — no new subjects are ever created after construction. This architecture is required because `toSignal()` must be called in an injection context and cannot be called again later.

---

Expand All @@ -137,17 +137,17 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp

- [Getting Started](https://stream-resource.dev/docs/getting-started)
- [API Reference](https://stream-resource.dev/api-reference)
- [Testing with MockStreamTransport](https://stream-resource.dev/docs/testing)
- [Testing with MockAgentTransport](https://stream-resource.dev/docs/testing)
- [Human-in-the-Loop / Interrupts](https://stream-resource.dev/docs/interrupts)
- [Subagent Streaming](https://stream-resource.dev/docs/subagents)

---

## License

`@cacheplane/stream-resource` is source-available software dual-licensed:
`@cacheplane/angular` is source-available software dual-licensed:

- **PolyForm Noncommercial 1.0.0** — free for noncommercial use (personal projects, academic, research, non-profit internal tooling). See [`LICENSE`](./LICENSE).
- **Angular Stream Resource Commercial License** — required for any for-profit or revenue-generating use. See [`LICENSE-COMMERCIAL`](./LICENSE-COMMERCIAL) and [`COMMERCIAL.md`](./COMMERCIAL.md).
- **Angular Agent Framework Commercial License** — required for any for-profit or revenue-generating use. See [`LICENSE-COMMERCIAL`](./LICENSE-COMMERCIAL) and [`COMMERCIAL.md`](./COMMERCIAL.md).

This is **not** an open-source license. Commercial use — including use in a for-profit product, service, or organization — requires a paid commercial license. See [pricing](https://stream-resource.dev/pricing).
6 changes: 3 additions & 3 deletions apps/demo/src/app/chat-demo/chat-demo.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, Input, OnInit, Injector, runInInjectionContext } from '@angular/core';
import { streamResource } from '@cacheplane/stream-resource';
import { agent } from '@cacheplane/angular';
import type { BaseMessage } from '@langchain/core/messages';

@Component({
Expand Down Expand Up @@ -34,14 +34,14 @@ export class ChatDemoComponent implements OnInit {
@Input() apiUrl = 'http://localhost:2024';
@Input() assistantId = 'chat_agent';

chat: ReturnType<typeof streamResource<{ messages: BaseMessage[] }>> | null = null;
chat: ReturnType<typeof agent<{ messages: BaseMessage[] }>> | null = null;

constructor(private injector: Injector) {}

ngOnInit() {
// @Input() values are available in ngOnInit, so use runInInjectionContext
runInInjectionContext(this.injector, () => {
this.chat = streamResource<{ messages: BaseMessage[] }>({
this.chat = agent<{ messages: BaseMessage[] }>({
apiUrl: this.apiUrl,
assistantId: this.assistantId,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# streamResource()
# agent()

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

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.

```ts
import { streamResource } from '@cacheplane/stream-resource';
import { agent } from '@cacheplane/angular';

// Inside a component or service with injection context
const repo = streamResource<Repository>({
const repo = agent<Repository>({
url: () => `/api/repos/${this.repoId()}`,
transport: inject(FetchStreamTransport),
});
Expand All @@ -31,7 +31,7 @@ const repo = streamResource<Repository>({

## When to use

Use `streamResource` whenever your UI needs to react to a live data stream from the server:
Use `agent` whenever your UI needs to react to a live data stream from the server:

- **AI / LLM responses** — stream tokens into a chat bubble as they arrive
- **Live feeds** — stock tickers, activity logs, or progress updates
Expand All @@ -40,7 +40,7 @@ Use `streamResource` whenever your UI needs to react to a live data stream from
For plain HTTP requests that return a single value and complete, Angular's built-in `resource()` or `httpResource()` is a better fit.

<Callout type="warning" title="Injection context required">
`streamResource` 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 +68,7 @@ For plain HTTP requests that return a single value and complete, Angular's built
icon="bolt"
href="/docs-v2/concepts/angular-signals"
>
Understand how stream-resource integrates with Angular's reactivity model.
Understand how angular integrates with Angular's reactivity model.
</Card>
</CardGroup>

Expand Down
42 changes: 21 additions & 21 deletions apps/website/content/docs-v2/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
]
},
{
"name": "MockStreamTransport",
"name": "MockAgentTransport",
"kind": "class",
"description": "Test transport for deterministic agent testing without a real LangGraph server.\n\nScript event batches upfront, then emit them manually or step through them\nin your test specs. Supports error injection and close control.",
"params": [
Expand All @@ -99,7 +99,7 @@
}
],
"examples": [
"```typescript\nconst transport = new MockStreamTransport([\n [{ type: 'values', data: { messages: [aiMsg('Hello')] } }],\n [{ type: 'values', data: { status: 'done' } }],\n]);\n```"
"```typescript\nconst transport = new MockAgentTransport([\n [{ type: 'values', data: { messages: [aiMsg('Hello')] } }],\n [{ type: 'values', data: { status: 'done' } }],\n]);\n```"
],
"properties": [],
"methods": [
Expand Down Expand Up @@ -233,9 +233,9 @@
"examples": []
},
{
"name": "StreamResourceConfig",
"name": "AgentConfig",
"kind": "interface",
"description": "Global configuration for streamResource instances.\nProperties set here serve as defaults that can be overridden per-call.",
"description": "Global configuration for agent instances.\nProperties set here serve as defaults that can be overridden per-call.",
"properties": [
{
"name": "apiUrl",
Expand All @@ -245,17 +245,17 @@
},
{
"name": "transport",
"type": "StreamResourceTransport",
"type": "AgentTransport",
"description": "Custom transport implementation. Defaults to FetchStreamTransport.",
"optional": true
}
],
"examples": []
},
{
"name": "StreamResourceOptions",
"name": "AgentOptions",
"kind": "interface",
"description": "Options for creating a streaming resource via streamResource.",
"description": "Options for creating a streaming resource via agent.",
"properties": [
{
"name": "apiUrl",
Expand Down Expand Up @@ -319,17 +319,17 @@
},
{
"name": "transport",
"type": "StreamResourceTransport",
"type": "AgentTransport",
"description": "Custom transport. Defaults to FetchStreamTransport.",
"optional": true
}
],
"examples": []
},
{
"name": "StreamResourceRef",
"name": "AgentRef",
"kind": "interface",
"description": "Reactive reference returned by streamResource. All properties are Angular Signals.",
"description": "Reactive reference returned by agent. All properties are Angular Signals.",
"properties": [
{
"name": "activeSubagents",
Expand Down Expand Up @@ -473,7 +473,7 @@
"examples": []
},
{
"name": "StreamResourceTransport",
"name": "AgentTransport",
"kind": "interface",
"description": "Transport interface for connecting to a LangGraph agent.",
"properties": [
Expand Down Expand Up @@ -718,14 +718,14 @@
"examples": []
},
{
"name": "provideStreamResource",
"name": "provideAgent",
"kind": "function",
"description": "Angular provider factory that registers global defaults for all\nstreamResource instances in the application.\n\nAdd to your `app.config.ts` or module providers array.",
"signature": "provideStreamResource(config: StreamResourceConfig): Provider",
"description": "Angular provider factory that registers global defaults for all\nagent instances in the application.\n\nAdd to your `app.config.ts` or module providers array.",
"signature": "provideAgent(config: AgentConfig): Provider",
"params": [
{
"name": "config",
"type": "StreamResourceConfig",
"type": "AgentConfig",
"description": "Global configuration merged with per-call options",
"optional": false
}
Expand All @@ -735,28 +735,28 @@
"description": ""
},
"examples": [
"```typescript\n// app.config.ts\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideStreamResource({ apiUrl: 'http://localhost:2024' }),\n ],\n};\n```"
"```typescript\n// app.config.ts\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideAgent({ apiUrl: 'http://localhost:2024' }),\n ],\n};\n```"
]
},
{
"name": "streamResource",
"name": "agent",
"kind": "function",
"description": "Creates a streaming resource connected to a LangGraph agent.\n\nMust be called within an Angular injection context (component constructor,\nfield initializer, or `runInInjectionContext`). Returns a ref object whose\nproperties are Angular Signals that update in real-time as the agent streams.",
"signature": "streamResource(options: StreamResourceOptions<T, InferBag<T, Bag>>): StreamResourceRef<T, InferBag<T, Bag>>",
"signature": "agent(options: AgentOptions<T, InferBag<T, Bag>>): AgentRef<T, InferBag<T, Bag>>",
"params": [
{
"name": "options",
"type": "StreamResourceOptions<T, InferBag<T, Bag>>",
"type": "AgentOptions<T, InferBag<T, Bag>>",
"description": "Configuration for the streaming resource",
"optional": false
}
],
"returns": {
"type": "StreamResourceRef<T, InferBag<T, Bag>>",
"type": "AgentRef<T, InferBag<T, Bag>>",
"description": ""
},
"examples": [
"```typescript\n// In a component field initializer\nconst chat = streamResource<{ 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<{ 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```"
]
}
]
Loading
Loading