Skip to content

Commit 9dc9ef9

Browse files
committed
feat: wire AGUIMock into config, suite, exports, and CLI
1 parent 9fafbd1 commit 9dc9ef9

4 files changed

Lines changed: 148 additions & 11 deletions

File tree

src/cli.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createServer } from "./server.js";
66
import { loadFixtureFile, loadFixturesFromDir, validateFixtures } from "./fixture-loader.js";
77
import { Logger, type LogLevel } from "./logger.js";
88
import { watchFixtures } from "./watcher.js";
9+
import { AGUIMock } from "./agui-mock.js";
910
import type { ChaosConfig, RecordConfig } from "./types.js";
1011

1112
const HELP = `
@@ -32,6 +33,9 @@ Options:
3233
--provider-azure <url> Upstream URL for Azure OpenAI
3334
--provider-ollama <url> Upstream URL for Ollama
3435
--provider-cohere <url> Upstream URL for Cohere
36+
--agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)
37+
--agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)
38+
--agui-proxy-only AG-UI proxy mode: forward without saving
3539
--chaos-drop <rate> Probability (0-1) of dropping requests with 500
3640
--chaos-malformed <rate> Probability (0-1) of returning malformed JSON
3741
--chaos-disconnect <rate> Probability (0-1) of destroying connection
@@ -60,6 +64,9 @@ const { values } = parseArgs({
6064
"provider-azure": { type: "string" },
6165
"provider-ollama": { type: "string" },
6266
"provider-cohere": { type: "string" },
67+
"agui-record": { type: "boolean", default: false },
68+
"agui-upstream": { type: "string" },
69+
"agui-proxy-only": { type: "boolean", default: false },
6370
"chaos-drop": { type: "string" },
6471
"chaos-malformed": { type: "string" },
6572
"chaos-disconnect": { type: "string" },
@@ -168,6 +175,22 @@ if (values.record || values["proxy-only"]) {
168175
};
169176
}
170177

178+
// Parse AG-UI record/proxy config from CLI flags
179+
let aguiMount: { path: string; handler: AGUIMock } | undefined;
180+
if (values["agui-record"] || values["agui-proxy-only"]) {
181+
if (!values["agui-upstream"]) {
182+
console.error("Error: --agui-record/--agui-proxy-only requires --agui-upstream");
183+
process.exit(1);
184+
}
185+
const agui = new AGUIMock();
186+
agui.enableRecording({
187+
upstream: values["agui-upstream"],
188+
fixturePath: resolve(fixturePath, "agui-recorded"),
189+
proxyOnly: values["agui-proxy-only"],
190+
});
191+
aguiMount = { path: "/agui", handler: agui };
192+
}
193+
171194
async function main() {
172195
// Load fixtures from path (detect file vs directory)
173196
let isDir: boolean;
@@ -219,17 +242,23 @@ async function main() {
219242
}
220243
}
221244

222-
const instance = await createServer(fixtures, {
223-
port,
224-
host,
225-
latency,
226-
chunkSize,
227-
logLevel,
228-
chaos,
229-
metrics: values.metrics,
230-
record,
231-
strict: values.strict,
232-
});
245+
const mounts = aguiMount ? [aguiMount] : undefined;
246+
247+
const instance = await createServer(
248+
fixtures,
249+
{
250+
port,
251+
host,
252+
latency,
253+
chunkSize,
254+
logLevel,
255+
chaos,
256+
metrics: values.metrics,
257+
record,
258+
strict: values.strict,
259+
},
260+
mounts,
261+
);
233262

234263
logger.info(`aimock server listening on ${instance.url}`);
235264

src/config-loader.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import * as path from "node:path";
33
import { LLMock } from "./llmock.js";
44
import { MCPMock } from "./mcp-mock.js";
55
import { A2AMock } from "./a2a-mock.js";
6+
import { AGUIMock } from "./agui-mock.js";
67
import type { ChaosConfig, RecordConfig } from "./types.js";
78
import type { MCPToolDefinition, MCPPromptDefinition } from "./mcp-types.js";
89
import type { A2AAgentDefinition, A2APart, A2AArtifact, A2AStreamEvent } from "./a2a-types.js";
10+
import type { AGUIEvent } from "./agui-types.js";
911
import { VectorMock } from "./vector-mock.js";
1012
import type { QueryResult } from "./vector-types.js";
1113
import { Logger } from "./logger.js";
@@ -56,6 +58,18 @@ export interface A2AConfig {
5658
agents?: A2AConfigAgent[];
5759
}
5860

61+
export interface AGUIConfigFixture {
62+
match: { message?: string; toolName?: string; stateKey?: string };
63+
text?: string; // shorthand: uses buildTextResponse
64+
events?: AGUIEvent[]; // raw events
65+
delayMs?: number;
66+
}
67+
68+
export interface AGUIConfig {
69+
path?: string; // mount path, default "/agui"
70+
fixtures?: AGUIConfigFixture[];
71+
}
72+
5973
export interface VectorConfigCollection {
6074
name: string;
6175
dimension: number;
@@ -80,6 +94,7 @@ export interface AimockConfig {
8094
};
8195
mcp?: MCPConfig;
8296
a2a?: A2AConfig;
97+
agui?: AGUIConfig;
8398
vector?: VectorConfig;
8499
services?: { search?: boolean; rerank?: boolean; moderate?: boolean };
85100
metrics?: boolean;
@@ -198,6 +213,38 @@ export async function startFromConfig(
198213
logger.info(`A2AMock mounted at ${a2aPath}`);
199214
}
200215

216+
// AG-UI
217+
if (config.agui) {
218+
const aguiConfig = config.agui;
219+
const agui = new AGUIMock();
220+
221+
if (aguiConfig.fixtures) {
222+
for (const f of aguiConfig.fixtures) {
223+
if (f.text) {
224+
agui.onMessage(f.match.message ?? /.*/, f.text, { delayMs: f.delayMs });
225+
} else if (f.events) {
226+
agui.addFixture({
227+
match: {
228+
message: f.match.message,
229+
toolName: f.match.toolName,
230+
stateKey: f.match.stateKey,
231+
},
232+
events: f.events,
233+
delayMs: f.delayMs,
234+
});
235+
} else {
236+
logger.warn(
237+
`AG-UI fixture has neither text nor events — it will be skipped (match: ${JSON.stringify(f.match)})`,
238+
);
239+
}
240+
}
241+
}
242+
243+
const aguiPath = aguiConfig.path ?? "/agui";
244+
llmock.mount(aguiPath, agui);
245+
logger.info(`AGUIMock mounted at ${aguiPath}`);
246+
}
247+
201248
// Vector
202249
if (config.vector) {
203250
const vectorConfig = config.vector;

src/index.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,55 @@ export type {
156156
A2ATaskState,
157157
} from "./a2a-types.js";
158158

159+
// AG-UI
160+
export { AGUIMock } from "./agui-mock.js";
161+
export { proxyAndRecordAGUI } from "./agui-recorder.js";
162+
export type {
163+
AGUIMockOptions,
164+
AGUIRunAgentInput,
165+
AGUIMessage,
166+
AGUIToolDefinition,
167+
AGUIToolCall,
168+
AGUIEvent,
169+
AGUIEventType,
170+
AGUIFixture,
171+
AGUIFixtureMatch,
172+
AGUIRecordConfig,
173+
// Key individual event types
174+
AGUIRunStartedEvent,
175+
AGUIRunFinishedEvent,
176+
AGUIRunErrorEvent,
177+
AGUITextMessageStartEvent,
178+
AGUITextMessageContentEvent,
179+
AGUITextMessageEndEvent,
180+
AGUITextMessageChunkEvent,
181+
AGUIToolCallStartEvent,
182+
AGUIToolCallArgsEvent,
183+
AGUIToolCallEndEvent,
184+
AGUIToolCallResultEvent,
185+
AGUIStateSnapshotEvent,
186+
AGUIStateDeltaEvent,
187+
AGUIMessagesSnapshotEvent,
188+
AGUIActivitySnapshotEvent,
189+
AGUIActivityDeltaEvent,
190+
} from "./agui-types.js";
191+
export {
192+
buildTextResponse as buildAGUITextResponse,
193+
buildTextChunkResponse as buildAGUITextChunkResponse,
194+
buildToolCallResponse as buildAGUIToolCallResponse,
195+
buildStateUpdate as buildAGUIStateUpdate,
196+
buildStateDelta as buildAGUIStateDelta,
197+
buildMessagesSnapshot as buildAGUIMessagesSnapshot,
198+
buildReasoningResponse as buildAGUIReasoningResponse,
199+
buildActivityResponse as buildAGUIActivityResponse,
200+
buildErrorResponse as buildAGUIErrorResponse,
201+
buildStepWithText as buildAGUIStepWithText,
202+
buildCompositeResponse as buildAGUICompositeResponse,
203+
extractLastUserMessage as extractAGUILastUserMessage,
204+
findFixture as findAGUIFixture,
205+
writeAGUIEventStream,
206+
} from "./agui-handler.js";
207+
159208
// JSON-RPC
160209
export { createJsonRpcDispatcher } from "./jsonrpc.js";
161210
export type { JsonRpcResponse, MethodHandler, JsonRpcDispatcherOptions } from "./jsonrpc.js";

src/suite.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,27 @@ import { LLMock } from "./llmock.js";
22
import { MCPMock } from "./mcp-mock.js";
33
import { A2AMock } from "./a2a-mock.js";
44
import { VectorMock } from "./vector-mock.js";
5+
import { AGUIMock } from "./agui-mock.js";
56
import type { MockServerOptions } from "./types.js";
67
import type { MCPMockOptions } from "./mcp-types.js";
78
import type { A2AMockOptions } from "./a2a-types.js";
89
import type { VectorMockOptions } from "./vector-types.js";
10+
import type { AGUIMockOptions } from "./agui-types.js";
911

1012
export interface MockSuiteOptions {
1113
llm?: MockServerOptions;
1214
mcp?: MCPMockOptions;
1315
a2a?: A2AMockOptions;
1416
vector?: VectorMockOptions;
17+
agui?: AGUIMockOptions;
1518
}
1619

1720
export interface MockSuite {
1821
llm: LLMock;
1922
mcp?: MCPMock;
2023
a2a?: A2AMock;
2124
vector?: VectorMock;
25+
agui?: AGUIMock;
2226
start(): Promise<void>;
2327
stop(): Promise<void>;
2428
reset(): void;
@@ -29,6 +33,7 @@ export async function createMockSuite(options: MockSuiteOptions = {}): Promise<M
2933
let mcp: MCPMock | undefined;
3034
let a2a: A2AMock | undefined;
3135
let vector: VectorMock | undefined;
36+
let agui: AGUIMock | undefined;
3237

3338
if (options.mcp) {
3439
mcp = new MCPMock(options.mcp);
@@ -45,11 +50,17 @@ export async function createMockSuite(options: MockSuiteOptions = {}): Promise<M
4550
llm.mount("/vector", vector);
4651
}
4752

53+
if (options.agui) {
54+
agui = new AGUIMock(options.agui);
55+
llm.mount("/agui", agui);
56+
}
57+
4858
return {
4959
llm,
5060
mcp,
5161
a2a,
5262
vector,
63+
agui,
5364
async start() {
5465
await llm.start();
5566
},
@@ -61,6 +72,7 @@ export async function createMockSuite(options: MockSuiteOptions = {}): Promise<M
6172
if (mcp) mcp.reset();
6273
if (a2a) a2a.reset();
6374
if (vector) vector.reset();
75+
if (agui) agui.reset();
6476
},
6577
};
6678
}

0 commit comments

Comments
 (0)