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
50 changes: 45 additions & 5 deletions apps/website/content/docs/ag-ui/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,38 @@
],
"examples": []
},
{
"name": "AgUiFakeAgentConfig",
"kind": "interface",
"description": "Config accepted by provideFakeAgent: the shared `FakeAgentConfig`\n(`tokens`, `reasoningTokens`, `delayMs`) plus the AG-UI-only `script`, which\nreplaces the canned token reply with raw AG-UI events.",
"properties": [
{
"name": "delayMs",
"type": "number",
"description": "Milliseconds between successive token emissions.",
"optional": true
},
{
"name": "reasoningTokens",
"type": "string[]",
"description": "Optional reasoning chunks emitted before the text reply.",
"optional": true
},
{
"name": "script",
"type": "FakeAgentScript",
"description": "Deterministic event branches — see FakeAgentScript.",
"optional": true
},
{
"name": "tokens",
"type": "string[]",
"description": "Assistant reply, streamed token-by-token.",
"optional": true
}
],
"examples": []
},
{
"name": "CustomStreamEvent",
"kind": "interface",
Expand Down Expand Up @@ -546,6 +578,13 @@
],
"examples": []
},
{
"name": "FakeAgentScript",
"kind": "type",
"description": "Deterministic event branches for FakeAgent, reachable through the\nconstructor and through `provideFakeAgent({ script })`.\n\nEach branch supplies a raw AG-UI event sequence — tool calls, state\nsnapshots, custom events, anything the protocol defines. `when: 'initial'`\nmatches a turn whose history carries no tool result; `{ toolMessageFor: id }`\nmatches the follow-up turn whose history carries a tool result for that tool\ncall id. The first matching branch wins, and its `events` are wrapped in\n`RUN_STARTED` / `RUN_FINISHED`. When no branch matches, the canned token\nreply is streamed instead.",
"signature": "readonly { events: readonly BaseEvent[]; when: \"initial\" | { toolMessageFor: string } }[]",
"examples": []
},
{
"name": "bridgeCitationsState",
"kind": "function",
Expand Down Expand Up @@ -590,7 +629,7 @@
{
"name": "provideAgent",
"kind": "function",
"description": "Provides an Agent instance wired through HttpAgent and toAgent.\nConstructs an HttpAgent from config and wraps it in the runtime-neutral\nAgent contract via toAgent(). Returns a provider array suitable for\nbootstrapApplication or TestBed.configureTestingModule().\n\n**Static vs factory config.** Pass a plain `AgentConfig` object when the\nconfig is known up front. Pass a `() => AgentConfig` factory when the config\ndepends on runtime/DI state — the factory runs inside an Angular injection\ncontext, so it may call `inject()` to read services or route params.\n\n**Typed state via AgentRef.** Pass a typed ref as the first argument to flow\nthe state shape from `provideAgent` to `injectAgent` without repeating the\ngeneric at every call site.\n\n**Several agents at one injector level.** Each `provideAgent(ref, …)` call\nbuilds its own agent, so two (or more) refs may be provided side by side in a\nsingle `providers` array and `injectAgent(refA)` / `injectAgent(refB)` return\ndistinct agents. The ref-less `injectAgent()` resolves a single shared token,\nwhich can only point at one of them: when more than one ref is provided at\nthe same level the **last** call wins. Always inject by ref when an injector\nprovides more than one agent.",
"description": "Provides an Agent instance wired through HttpAgent and toAgent.\nConstructs an HttpAgent from config and wraps it in the runtime-neutral\nAgent contract via toAgent(). Returns a provider array suitable for\nbootstrapApplication or TestBed.configureTestingModule().\n\n**Static vs factory config.** Pass a plain `AgentConfig` object when the\nconfig is known up front. Pass a `() => AgentConfig` factory when the config\ndepends on runtime/DI state — the factory runs inside an Angular injection\ncontext, so it may call `inject()` to read services or route params.\n\n**Typed state via AgentRef.** Pass a typed ref as the first argument to flow\nthe state shape from `provideAgent` to `injectAgent` without repeating the\ngeneric at every call site.\n\n**Several agents at one injector level.** Each `provideAgent(ref, …)` call\nbuilds its own agent, so two (or more) refs may be provided side by side in a\nsingle `providers` array and `injectAgent(refA)` / `injectAgent(refB)` return\ndistinct agents. The ref-less `injectAgent()` resolves a single shared token,\nwhich can only point at one of them: when more than one ref is provided at\nthe same level the **last** call wins. Always inject by ref when an injector\nprovides more than one agent. Development builds emit a one-time\n`console.warn` naming the refs involved when an injector level registers more\nthan one, so the silent last-ref-wins aliasing is visible during development.",
"signature": "provideAgent(ref: AgentRef<T>, configOrFactory: AgentConfig | () => AgentConfig): Provider[]",
"params": [
{
Expand All @@ -617,12 +656,12 @@
{
"name": "provideFakeAgent",
"kind": "function",
"description": "Registers an in-process FakeAgent under AGENT.\n\nUse for offline demos and development. Drop-in replacement for\nprovideAgent({ url }) when no real backend is available.",
"signature": "provideFakeAgent(config: FakeAgentConfig): Provider[]",
"description": "Registers an in-process FakeAgent under AGENT.\n\nUse for offline demos and development. Drop-in replacement for\nprovideAgent({ url }) when no real backend is available.\n\nPass `script` to stream exact AG-UI events instead of the canned token\nreply — the adapter reduces them into `toolCalls()`, `state()`,\n`customEvents()`, and `interrupt()` exactly as it would real wire events.",
"signature": "provideFakeAgent(config: AgUiFakeAgentConfig): Provider[]",
"params": [
{
"name": "config",
"type": "FakeAgentConfig",
"type": "AgUiFakeAgentConfig",
"description": "",
"optional": true
}
Expand All @@ -632,7 +671,8 @@
"description": ""
},
"examples": [
"```ts\nTestBed.configureTestingModule({\n providers: [provideFakeAgent({ tokens: ['Hello from the fake agent'] })],\n});\n```"
"```ts\nTestBed.configureTestingModule({\n providers: [provideFakeAgent({ tokens: ['Hello from the fake agent'] })],\n});\n```",
"```ts\nTestBed.configureTestingModule({\n providers: [provideFakeAgent({\n delayMs: 0,\n script: [{\n when: 'initial',\n events: [\n { type: EventType.TOOL_CALL_START, toolCallId: 't1', toolCallName: 'get_weather' },\n { type: EventType.TOOL_CALL_ARGS, toolCallId: 't1', delta: '{\"city\":\"SF\"}' },\n { type: EventType.TOOL_CALL_END, toolCallId: 't1' },\n ] as BaseEvent[],\n }],\n })],\n});\n```"
]
},
{
Expand Down
61 changes: 52 additions & 9 deletions apps/website/content/docs/ag-ui/api/fake-agent.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: FakeAgent and provideFakeAgent() stream a canned AG-UI response in process, with an optional script of raw events for exact test streams.
description: FakeAgent and provideFakeAgent() stream a canned AG-UI response in process, or an exact script of raw events for tool calls, state, and interrupts.
---

# FakeAgent
Expand All @@ -22,7 +22,7 @@ bootstrapApplication(AppComponent, {
});
```

Pass a `FakeAgentConfig` to customize the canned response:
Pass an `AgUiFakeAgentConfig` to customize the canned response:

```ts
provideFakeAgent({
Expand All @@ -31,13 +31,16 @@ provideFakeAgent({
})
```

### FakeAgentConfig
### AgUiFakeAgentConfig

| Option | Type | Description |
|--------|------|-------------|
| `tokens` | `string[]` | Assistant reply streamed token-by-token. Defaults to a fixed placeholder message. |
| `reasoningTokens` | `string[]` | Optional reasoning chunks emitted before the text reply. |
| `delayMs` | `number` | Milliseconds between successive token emissions. Defaults to `60`. |
| `script` | [`FakeAgentScript`](#script) | Raw AG-UI event branches that replace the canned reply. |

The first three options are the shared `FakeAgentConfig` that every adapter's `provideFakeAgent()` accepts. `script` is the AG-UI-specific addition.

## FakeAgent class

Expand All @@ -58,20 +61,60 @@ const agent = toAgent(new FakeAgent({

### script

The constructor accepts a fourth option that `FakeAgentConfig` does not carry, so it is reachable only by constructing `FakeAgent` yourself:
`FakeAgentScript` is an exported type. Both the constructor and `provideFakeAgent()` accept it:

```ts
script?: readonly {
type FakeAgentScript = readonly {
when: 'initial' | { toolMessageFor: string };
events: readonly BaseEvent[];
}[];
```

Each branch supplies a raw AG-UI event sequence for tests that need an exact stream — tool calls, `STATE_SNAPSHOT`, `CUSTOM` events, anything the protocol defines. `when: 'initial'` matches the first turn; `{ toolMessageFor: id }` matches the turn whose input carries a tool result for that tool call id. The first matching branch wins, and `FakeAgent` wraps its `events` in `RUN_STARTED` and `RUN_FINISHED` for you. When no branch matches, the canned token reply is emitted instead.
Each branch supplies a raw AG-UI event sequence for tests that need an exact stream — tool calls, `STATE_SNAPSHOT`, `CUSTOM` events, anything the protocol defines. `when: 'initial'` matches a turn whose history carries no tool result; `{ toolMessageFor: id }` matches the follow-up turn whose history carries a tool result for that tool call id, which is what a resolved client tool produces. The first matching branch wins, and `FakeAgent` wraps its `events` in `RUN_STARTED` and `RUN_FINISHED` for you. When no branch matches, the canned token reply is emitted instead.

| Option | Type | Description |
|--------|------|-------------|
| `script` | `readonly { when: 'initial' \| { toolMessageFor: string }; events: readonly BaseEvent[] }[]` | Deterministic event branches. Constructor only — not part of `FakeAgentConfig`, so `provideFakeAgent()` cannot set it. |
Through DI, a script reaches the reducer exactly as wire events would, so `toolCalls()`, `state()`, `customEvents()`, and `interrupt()` all populate:

```ts
import { TestBed } from '@angular/core/testing';
import { EventType, type BaseEvent } from '@ag-ui/client';
import { provideFakeAgent, injectAgent } from '@threadplane/ag-ui';

it('reduces a scripted tool call', async () => {
TestBed.configureTestingModule({
providers: [
provideFakeAgent({
delayMs: 0,
script: [
{
when: 'initial',
events: [
{
type: EventType.TOOL_CALL_START,
toolCallId: 'tool-1',
toolCallName: 'get_weather',
} as BaseEvent,
{
type: EventType.TOOL_CALL_ARGS,
toolCallId: 'tool-1',
delta: '{"city":"SF"}',
} as BaseEvent,
{ type: EventType.TOOL_CALL_END, toolCallId: 'tool-1' } as BaseEvent,
],
},
],
}),
],
});

const agent = TestBed.runInInjectionContext(() => injectAgent());
await agent.submit({ message: 'weather?' });

expect(agent.toolCalls()[0]).toMatchObject({
name: 'get_weather',
args: { city: 'SF' },
});
});
```

## TestBed example

Expand Down
12 changes: 12 additions & 0 deletions apps/website/content/docs/ag-ui/api/provide-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ const support = injectAgent(SUPPORT); // the support agent

<Callout type="warning" title="The ref-less injectAgent() resolves the last ref provided">
The ref form also aliases the shared token so that the no-argument `injectAgent()` keeps working. That token can only point at one agent, so when several refs are provided at the same injector level the **last** `provideAgent(ref, …)` call wins. In the example above, a bare `injectAgent()` returns the support agent. Always inject by ref when an injector provides more than one agent.

Development builds do not leave this silent. The first time such an injector builds one of its agents, the adapter emits a single `console.warn` naming every ref registered at that level and the one the ref-less `injectAgent()` resolves:

```text
[@threadplane/ag-ui] provideAgent() was called with more than one AgentRef at the
same injector level (trip, support). The ref-less injectAgent() reads a single
shared token, so it resolves the last ref provided (support) and the others are
reachable only by ref. Inject by ref — injectAgent(ref) — when an injector
provides more than one agent.
```

The warning is development-only (`isDevMode()`), fires once per injector, and never changes what DI hands back: each ref keeps its own agent. Refs provided at different injector levels — one in the application config, another in a component's `providers` — do not collide and do not warn.
</Callout>

With a single ref the alias is exact: one instance, one config evaluation, reachable both as `injectAgent(TRIP)` and as `injectAgent()`.
Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/ag-ui/concepts/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const trip = injectAgent(TRIP); // AgUiAgent<TripState>
const support = injectAgent(SUPPORT); // AgUiAgent<SupportState>
```

The ref form also aliases the shared token that the no-argument `injectAgent()` reads. That token can only point at one agent, so when several refs are provided at the same level the **last** `provideAgent(ref, …)` call wins — inject by ref whenever an injector provides more than one agent. See [provideAgent()](/docs/ag-ui/api/provide-agent) for the full rule.
The ref form also aliases the shared token that the no-argument `injectAgent()` reads. That token can only point at one agent, so when several refs are provided at the same level the **last** `provideAgent(ref, …)` call wins — inject by ref whenever an injector provides more than one agent. Development builds emit a one-time `console.warn` naming the refs involved when an injector level registers more than one, so the aliasing is visible while you build. See [provideAgent()](/docs/ag-ui/api/provide-agent) for the full rule.

Use `provideFakeAgent()` when you need the UI to run without a backend:

Expand Down
35 changes: 32 additions & 3 deletions apps/website/content/docs/ag-ui/guides/fake-agent.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Run the chat UI with no backend using provideFakeAgent(), what the canned stream contains, and when to construct FakeAgent with a script instead.
description: Run the chat UI with no backend using provideFakeAgent(), what the canned stream contains, and how to script exact AG-UI events instead.
---

# Fake Agent
Expand Down Expand Up @@ -97,11 +97,40 @@ That gives you the same `Agent` contract as `provideFakeAgent()`.
| `tokens` | `string[]` | A short canned greeting | Emitted as text deltas in order. |
| `reasoningTokens` | `string[]` | `[]` | Emitted before text deltas. |
| `delayMs` | `number` | `60` | Delay between events after the initial start delay. |
| `script` | `readonly { when: 'initial' \| { toolMessageFor: string }; events: readonly BaseEvent[] }[]` | `[]` | Constructor only — not part of `FakeAgentConfig`, so `provideFakeAgent()` cannot set it. Supplies a raw AG-UI event sequence per branch, wrapped in `RUN_STARTED` / `RUN_FINISHED`. |
| `script` | `FakeAgentScript` | `[]` | Raw AG-UI event branches that replace the canned reply, each wrapped in `RUN_STARTED` / `RUN_FINISHED`. Accepted by the constructor and by `provideFakeAgent()`. |

## Scripting exact events

Set `script` when the canned text reply is not enough — tool calls, shared state, custom events, and interrupts all reach the UI through it, with no backend and no direct construction:

```ts
import { EventType, type BaseEvent } from '@ag-ui/client';
import { provideFakeAgent } from '@threadplane/ag-ui';

providers: [
provideFakeAgent({
delayMs: 0,
script: [
{
when: 'initial',
events: [
{
type: EventType.CUSTOM,
name: 'on_interrupt',
value: { kind: 'approval', amount: 42 },
} as BaseEvent,
],
},
],
}),
]
```

`when: 'initial'` matches a turn whose history carries no tool result. `{ toolMessageFor: 'tool-1' }` matches the follow-up turn whose history carries a tool result for `tool-1`, which is what resolving a client tool produces — so a two-branch script plays a tool call and then the reply that follows it. The first matching branch wins; when none matches, the canned token reply streams instead.

## What it does not do

`provideFakeAgent()` does not call a model, execute tools, persist history, or simulate interrupts. Constructed directly, `FakeAgent` accepts a `script` of raw AG-UI events for tests that need an exact stream, so tool calls, state, and interrupts are reachable that way.
`provideFakeAgent()` does not call a model, execute tools, or persist history. It streams what you give it: a canned token reply by default, or the exact events in `script`.

It is deliberately small. Use it to keep UI work moving, not to validate backend behavior.

Expand Down
Loading
Loading