Skip to content

Commit b9b5164

Browse files
committed
feat(runtime): add universal Agent class with multi-backend support
Add Agent class extending AbstractAgent that accepts aisdk, tanstack, or custom configurations via a discriminated union. Routes to the appropriate converter, handles RUN_STARTED/FINISHED/ERROR lifecycle, and supports clone/abortRun. Re-export Agent and converters from the agent barrel.
1 parent 3997203 commit b9b5164

2 files changed

Lines changed: 161 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {
2+
AbstractAgent,
3+
BaseEvent,
4+
EventType,
5+
RunAgentInput,
6+
RunStartedEvent,
7+
RunFinishedEvent,
8+
RunErrorEvent,
9+
} from "@ag-ui/client";
10+
import { Observable } from "rxjs";
11+
import { convertAISDKStream } from "./converters/aisdk";
12+
import { convertTanStackStream } from "./converters/tanstack";
13+
14+
/**
15+
* Context passed to the user-supplied factory function.
16+
*/
17+
export interface AgentFactoryContext {
18+
input: RunAgentInput;
19+
abortController: AbortController;
20+
abortSignal: AbortSignal;
21+
}
22+
23+
/**
24+
* Configuration for an agent backed by the Vercel AI SDK.
25+
* The factory must return an object with a `fullStream` async iterable
26+
* (the same shape returned by `streamText()` from the `ai` package).
27+
*/
28+
export interface AISDKAgentConfig {
29+
type: "aisdk";
30+
factory: (ctx: AgentFactoryContext) => { fullStream: AsyncIterable<unknown> };
31+
}
32+
33+
/**
34+
* Configuration for an agent backed by TanStack AI.
35+
* The factory must return an async iterable of TanStack AI stream chunks.
36+
*/
37+
export interface TanStackAgentConfig {
38+
type: "tanstack";
39+
factory: (ctx: AgentFactoryContext) => AsyncIterable<unknown>;
40+
}
41+
42+
/**
43+
* Configuration for a custom agent that directly yields AG-UI events.
44+
* The factory must return an async iterable of `BaseEvent` objects.
45+
*/
46+
export interface CustomAgentConfig {
47+
type: "custom";
48+
factory: (ctx: AgentFactoryContext) => AsyncIterable<BaseEvent>;
49+
}
50+
51+
/**
52+
* Discriminated union of all supported agent configurations.
53+
*/
54+
export type AgentConfig = AISDKAgentConfig | TanStackAgentConfig | CustomAgentConfig;
55+
56+
/**
57+
* Universal Agent class that wraps any supported stream source
58+
* (AI SDK, TanStack AI, or custom) and emits AG-UI events via an Observable.
59+
*
60+
* The Agent handles lifecycle events (RUN_STARTED / RUN_FINISHED / RUN_ERROR)
61+
* while delegating stream-to-event conversion to the appropriate converter.
62+
*/
63+
export class Agent extends AbstractAgent {
64+
private abortController?: AbortController;
65+
66+
constructor(private config: AgentConfig) {
67+
super();
68+
}
69+
70+
run(input: RunAgentInput): Observable<BaseEvent> {
71+
return new Observable<BaseEvent>((subscriber) => {
72+
// Emit RUN_STARTED synchronously
73+
const startEvent: RunStartedEvent = {
74+
type: EventType.RUN_STARTED,
75+
threadId: input.threadId,
76+
runId: input.runId,
77+
};
78+
subscriber.next(startEvent);
79+
80+
this.abortController = new AbortController();
81+
const controller = this.abortController;
82+
83+
const ctx: AgentFactoryContext = {
84+
input,
85+
abortController: controller,
86+
abortSignal: controller.signal,
87+
};
88+
89+
(async () => {
90+
try {
91+
let events: AsyncIterable<BaseEvent>;
92+
93+
switch (this.config.type) {
94+
case "aisdk": {
95+
const result = this.config.factory(ctx);
96+
events = convertAISDKStream(result.fullStream, controller.signal);
97+
break;
98+
}
99+
case "tanstack": {
100+
const stream = this.config.factory(ctx);
101+
events = convertTanStackStream(stream, controller.signal);
102+
break;
103+
}
104+
case "custom": {
105+
events = this.config.factory(ctx);
106+
break;
107+
}
108+
}
109+
110+
for await (const event of events) {
111+
subscriber.next(event);
112+
}
113+
114+
// Stream completed — emit RUN_FINISHED if not aborted
115+
if (!controller.signal.aborted) {
116+
const finishedEvent: RunFinishedEvent = {
117+
type: EventType.RUN_FINISHED,
118+
threadId: input.threadId,
119+
runId: input.runId,
120+
};
121+
subscriber.next(finishedEvent);
122+
}
123+
subscriber.complete();
124+
} catch (error) {
125+
if (controller.signal.aborted) {
126+
subscriber.complete();
127+
} else {
128+
const runErrorEvent: RunErrorEvent = {
129+
type: EventType.RUN_ERROR,
130+
message: error instanceof Error ? error.message : String(error),
131+
};
132+
subscriber.next(runErrorEvent);
133+
subscriber.error(error);
134+
}
135+
} finally {
136+
this.abortController = undefined;
137+
}
138+
})();
139+
140+
// Observable teardown — abort the stream if unsubscribed
141+
return () => {
142+
controller.abort();
143+
};
144+
});
145+
}
146+
147+
clone(): Agent {
148+
const cloned = new Agent(this.config);
149+
// Copy middlewares from parent class
150+
// @ts-expect-error - accessing protected property from parent
151+
cloned.middlewares = [...this.middlewares];
152+
return cloned;
153+
}
154+
155+
abortRun(): void {
156+
this.abortController?.abort();
157+
}
158+
}

packages/runtime/src/agent/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,3 +1438,6 @@ export class BasicAgent extends BuiltInAgent {
14381438
}
14391439

14401440
export type BasicAgentConfiguration = BuiltInAgentConfiguration;
1441+
1442+
export * from "./agent";
1443+
export * from "./converters";

0 commit comments

Comments
 (0)