@microsoft/m365agentsplayground-cli 0.2.25-alpha.20260507-efe1416.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +341 -0
  2. package/build/cardValidator.d.ts +18 -0
  3. package/build/cardValidator.d.ts.map +1 -0
  4. package/build/cardValidator.js +47 -0
  5. package/build/conversationServer.d.ts +29 -0
  6. package/build/conversationServer.d.ts.map +1 -0
  7. package/build/conversationServer.js +127 -0
  8. package/build/conversationTypes.d.ts +146 -0
  9. package/build/conversationTypes.d.ts.map +1 -0
  10. package/build/conversationTypes.js +5 -0
  11. package/build/index.d.ts +14 -0
  12. package/build/index.d.ts.map +1 -0
  13. package/build/index.js +25 -0
  14. package/build/notificationSender.d.ts +16 -0
  15. package/build/notificationSender.d.ts.map +1 -0
  16. package/build/notificationSender.js +120 -0
  17. package/build/responseCapture.d.ts +29 -0
  18. package/build/responseCapture.d.ts.map +1 -0
  19. package/build/responseCapture.js +119 -0
  20. package/build/runConversation.d.ts +17 -0
  21. package/build/runConversation.d.ts.map +1 -0
  22. package/build/runConversation.js +338 -0
  23. package/build/serverManager.d.ts +46 -0
  24. package/build/serverManager.d.ts.map +1 -0
  25. package/build/serverManager.js +149 -0
  26. package/build/start-server.d.ts +9 -0
  27. package/build/start-server.d.ts.map +1 -0
  28. package/build/start-server.js +23 -0
  29. package/build/testClient.d.ts +146 -0
  30. package/build/testClient.d.ts.map +1 -0
  31. package/build/testClient.js +434 -0
  32. package/build/types.d.ts +125 -0
  33. package/build/types.d.ts.map +1 -0
  34. package/build/types.js +7 -0
  35. package/build/websocketClient.d.ts +56 -0
  36. package/build/websocketClient.d.ts.map +1 -0
  37. package/build/websocketClient.js +129 -0
  38. package/package.json +36 -0
  39. package/src/cardValidator.ts +56 -0
  40. package/src/conversationServer.ts +147 -0
  41. package/src/conversationTypes.ts +169 -0
  42. package/src/index.ts +37 -0
  43. package/src/notificationSender.ts +135 -0
  44. package/src/responseCapture.ts +145 -0
  45. package/src/runConversation.ts +379 -0
  46. package/src/serverManager.ts +172 -0
  47. package/src/start-server.ts +26 -0
  48. package/src/testClient.ts +515 -0
  49. package/src/types.ts +155 -0
  50. package/src/websocketClient.ts +153 -0
  51. package/tsconfig.json +16 -0
@@ -0,0 +1,125 @@
1
+ import { AccountRole } from "schema";
2
+ import { Message } from "server";
3
+ import type { Attachment } from "server";
4
+ export { ActionType, LogActionType } from "schema";
5
+ export type { IAction, ICreateMessageAction, IUpdateMessageAction, ITypingAction, IActionMessage, ILogAction, IAppendLogAction, LogItem, } from "schema";
6
+ /**
7
+ * Bot configuration matching the .m365agentsplayground.yml format
8
+ */
9
+ export interface BotConfig {
10
+ /**
11
+ * Bot's unique identifier
12
+ */
13
+ id: string;
14
+ /**
15
+ * Bot display name (max 42 characters)
16
+ */
17
+ name: string;
18
+ /**
19
+ * User ID for agentic-role bots
20
+ */
21
+ agenticUserId?: string;
22
+ /**
23
+ * App ID for agentic-role bots
24
+ */
25
+ agenticAppId?: string;
26
+ /**
27
+ * Bot's tenant ID (defaults to root tenant)
28
+ */
29
+ tenantId?: string;
30
+ /**
31
+ * Bot role: "user" | "bot" | "agenticUser"
32
+ */
33
+ role?: AccountRole;
34
+ }
35
+ /**
36
+ * Configuration for the TestClient
37
+ */
38
+ export interface TestClientConfig {
39
+ /**
40
+ * The bot endpoint URL (e.g., "http://localhost:3978/api/messages")
41
+ */
42
+ botEndpoint: string;
43
+ /**
44
+ * Timeout in milliseconds for waiting for bot responses.
45
+ * Default: 5000
46
+ */
47
+ timeout?: number;
48
+ /**
49
+ * Port to run the test server on.
50
+ * Default: auto-assigned
51
+ */
52
+ port?: number;
53
+ /**
54
+ * Bot configuration (id, name, role, etc.)
55
+ * If not provided, uses default bot config.
56
+ */
57
+ bot?: BotConfig;
58
+ /**
59
+ * Chat type to use when sending messages.
60
+ * - "personal": 1:1 personal chat with the bot (default)
61
+ * - "group": group chat context
62
+ * - "channel": team channel context
63
+ */
64
+ chatType?: "personal" | "group" | "channel";
65
+ /**
66
+ * Delivery mode for activities sent to the bot.
67
+ * - "expectReplies": Bot responses come inline in the HTTP response.
68
+ * - "default": Bot posts responses back to the connector URL.
69
+ */
70
+ deliveryMode?: "expectReplies" | "default";
71
+ /**
72
+ * Quiet-period fallback (ms) for streaming bot responses.
73
+ *
74
+ * When the simulator detects a streaming placeholder (`"Loading stream results..."`),
75
+ * it first waits for a `streamType:"final"` WebSocket event (the precise end-of-stream
76
+ * signal sent by teams-ai / teams.ts SDK). If no `streamType:"final"` arrives, it falls
77
+ * back to a quiet-period: resolves after this many milliseconds with no new
78
+ * `updateActivity` events.
79
+ *
80
+ * Default: 800 ms. Has no effect for bots with `stream: false`.
81
+ *
82
+ * @example
83
+ * // Increase for slow LLMs
84
+ * const client = new TestClient({
85
+ * botEndpoint: "http://localhost:3978/api/messages",
86
+ * streamingSettleDelayMs: 2000,
87
+ * });
88
+ */
89
+ streamingSettleDelayMs?: number;
90
+ }
91
+ /**
92
+ * A response from the bot
93
+ */
94
+ export interface BotResponse {
95
+ /**
96
+ * The unique message ID
97
+ */
98
+ messageId: string;
99
+ /**
100
+ * The text content of the response
101
+ */
102
+ text?: string;
103
+ /**
104
+ * Attachments in the response (e.g., Adaptive Cards)
105
+ */
106
+ attachments?: Attachment[];
107
+ /**
108
+ * Timestamp when the message was created
109
+ */
110
+ timestamp: number;
111
+ /**
112
+ * The raw Message object from ConversationManager
113
+ */
114
+ raw: Message;
115
+ }
116
+ /**
117
+ * Promise handlers for async response waiting
118
+ */
119
+ export interface PendingResponse {
120
+ resolve: (messages: Message[]) => void;
121
+ reject: (error: Error) => void;
122
+ timer: NodeJS.Timeout;
123
+ messageCountBefore: number;
124
+ }
125
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGzC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnD,YAAY,EACV,OAAO,EACP,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,cAAc,EAEd,UAAU,EACV,gBAAgB,EAChB,OAAO,GACR,MAAM,QAAQ,CAAC;AAEhB;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,GAAG,CAAC,EAAE,SAAS,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;IAE5C;;;;OAIG;IACH,YAAY,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IAE3C;;;;;;;;;;;;;;;;;OAiBG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAE3B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,GAAG,EAAE,OAAO,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACvC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;CAC5B"}
package/build/types.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LogActionType = exports.ActionType = void 0;
4
+ // Re-export action types for consumers
5
+ var schema_1 = require("schema");
6
+ Object.defineProperty(exports, "ActionType", { enumerable: true, get: function () { return schema_1.ActionType; } });
7
+ Object.defineProperty(exports, "LogActionType", { enumerable: true, get: function () { return schema_1.LogActionType; } });
@@ -0,0 +1,56 @@
1
+ import { IAction, ILogAction } from "schema";
2
+ export interface WebSocketClientOptions {
3
+ port: number;
4
+ onMessage: (action: IAction) => void;
5
+ onError?: (error: Error) => void;
6
+ onClose?: () => void;
7
+ }
8
+ export interface LogWebSocketClientOptions {
9
+ port: number;
10
+ onLogMessage: (action: ILogAction) => void;
11
+ onError?: (error: Error) => void;
12
+ onClose?: () => void;
13
+ }
14
+ /**
15
+ * WebSocket client for connecting to the conversation WebSocket endpoint.
16
+ * Receives real-time message and typing events from the server.
17
+ */
18
+ export declare class WebSocketClient {
19
+ private ws;
20
+ private options;
21
+ constructor(options: WebSocketClientOptions);
22
+ /**
23
+ * Connect to the WebSocket server
24
+ */
25
+ connect(): Promise<void>;
26
+ /**
27
+ * Close the WebSocket connection
28
+ */
29
+ close(): void;
30
+ /**
31
+ * Check if the WebSocket is connected
32
+ */
33
+ isConnected(): boolean;
34
+ }
35
+ /**
36
+ * WebSocket client for connecting to the log WebSocket endpoint.
37
+ * Receives real-time log events from the server.
38
+ */
39
+ export declare class LogWebSocketClient {
40
+ private ws;
41
+ private options;
42
+ constructor(options: LogWebSocketClientOptions);
43
+ /**
44
+ * Connect to the log WebSocket server
45
+ */
46
+ connect(): Promise<void>;
47
+ /**
48
+ * Close the WebSocket connection
49
+ */
50
+ close(): void;
51
+ /**
52
+ * Check if the WebSocket is connected
53
+ */
54
+ isConnected(): boolean;
55
+ }
56
+ //# sourceMappingURL=websocketClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocketClient.d.ts","sourceRoot":"","sources":["../src/websocketClient.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAK7C,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,OAAO,CAAyB;gBAE5B,OAAO,EAAE,sBAAsB;IAI3C;;OAEG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCxB;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,WAAW,IAAI,OAAO;CAGvB;AAED;;;GAGG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,OAAO,CAA4B;gBAE/B,OAAO,EAAE,yBAAyB;IAI9C;;OAEG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCxB;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,WAAW,IAAI,OAAO;CAGvB"}
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LogWebSocketClient = exports.WebSocketClient = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
8
+ const CONVERSATION_WS_PATH = "/_ws_conversation/v1/conversations/stream";
9
+ const LOG_WS_PATH = "/_ws/log/stream";
10
+ /**
11
+ * WebSocket client for connecting to the conversation WebSocket endpoint.
12
+ * Receives real-time message and typing events from the server.
13
+ */
14
+ class WebSocketClient {
15
+ ws = null;
16
+ options;
17
+ constructor(options) {
18
+ this.options = options;
19
+ }
20
+ /**
21
+ * Connect to the WebSocket server
22
+ */
23
+ connect() {
24
+ return new Promise((resolve, reject) => {
25
+ const url = `ws://localhost:${this.options.port}${CONVERSATION_WS_PATH}`;
26
+ this.ws = new ws_1.default(url);
27
+ this.ws.on("open", () => {
28
+ // Small delay to ensure server has processed the connection
29
+ setTimeout(resolve, 50);
30
+ });
31
+ this.ws.on("message", (data) => {
32
+ try {
33
+ const message = JSON.parse(data.toString());
34
+ this.options.onMessage(message);
35
+ }
36
+ catch (err) {
37
+ console.error("Failed to parse WebSocket message:", err);
38
+ }
39
+ });
40
+ this.ws.on("error", (err) => {
41
+ if (this.options.onError) {
42
+ this.options.onError(err);
43
+ }
44
+ reject(err);
45
+ });
46
+ this.ws.on("close", () => {
47
+ if (this.options.onClose) {
48
+ this.options.onClose();
49
+ }
50
+ });
51
+ });
52
+ }
53
+ /**
54
+ * Close the WebSocket connection
55
+ */
56
+ close() {
57
+ if (this.ws) {
58
+ this.ws.close();
59
+ this.ws = null;
60
+ }
61
+ }
62
+ /**
63
+ * Check if the WebSocket is connected
64
+ */
65
+ isConnected() {
66
+ return this.ws !== null && this.ws.readyState === ws_1.default.OPEN;
67
+ }
68
+ }
69
+ exports.WebSocketClient = WebSocketClient;
70
+ /**
71
+ * WebSocket client for connecting to the log WebSocket endpoint.
72
+ * Receives real-time log events from the server.
73
+ */
74
+ class LogWebSocketClient {
75
+ ws = null;
76
+ options;
77
+ constructor(options) {
78
+ this.options = options;
79
+ }
80
+ /**
81
+ * Connect to the log WebSocket server
82
+ */
83
+ connect() {
84
+ return new Promise((resolve, reject) => {
85
+ const url = `ws://localhost:${this.options.port}${LOG_WS_PATH}`;
86
+ this.ws = new ws_1.default(url);
87
+ this.ws.on("open", () => {
88
+ // Small delay to ensure server has processed the connection
89
+ setTimeout(resolve, 50);
90
+ });
91
+ this.ws.on("message", (data) => {
92
+ try {
93
+ const message = JSON.parse(data.toString());
94
+ this.options.onLogMessage(message);
95
+ }
96
+ catch (err) {
97
+ console.error("Failed to parse log WebSocket message:", err);
98
+ }
99
+ });
100
+ this.ws.on("error", (err) => {
101
+ if (this.options.onError) {
102
+ this.options.onError(err);
103
+ }
104
+ reject(err);
105
+ });
106
+ this.ws.on("close", () => {
107
+ if (this.options.onClose) {
108
+ this.options.onClose();
109
+ }
110
+ });
111
+ });
112
+ }
113
+ /**
114
+ * Close the WebSocket connection
115
+ */
116
+ close() {
117
+ if (this.ws) {
118
+ this.ws.close();
119
+ this.ws = null;
120
+ }
121
+ }
122
+ /**
123
+ * Check if the WebSocket is connected
124
+ */
125
+ isConnected() {
126
+ return this.ws !== null && this.ws.readyState === ws_1.default.OPEN;
127
+ }
128
+ }
129
+ exports.LogWebSocketClient = LogWebSocketClient;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@microsoft/m365agentsplayground-cli",
3
+ "version": "0.2.25-alpha.20260507-efe1416.0",
4
+ "main": "build/index.js",
5
+ "types": "build/index.d.ts",
6
+ "bin": {
7
+ "playground-cli-server": "build/start-server.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc --build",
11
+ "server": "node build/start-server.js"
12
+ },
13
+ "devDependencies": {
14
+ "@types/chai": "^4.3.5",
15
+ "@types/express": "5.0.1",
16
+ "@types/express-serve-static-core": "5.0.6",
17
+ "@types/mocha": "^10.0.1",
18
+ "@types/node": "^20.3.1",
19
+ "@types/ws": "^8.5.10",
20
+ "chai": "^4.3.7",
21
+ "mocha": "^10.2.0",
22
+ "ts-node": "^10.9.1",
23
+ "typescript": "^4.9.5"
24
+ },
25
+ "dependencies": {
26
+ "adaptivecards": "^3.0.4",
27
+ "express": "^5.2.0",
28
+ "schema": "^0.2.25-alpha.20260507-efe1416.0",
29
+ "server": "^0.2.25-alpha.20260507-efe1416.0",
30
+ "ws": "^8.18.0"
31
+ },
32
+ "bundledDependencies": [
33
+ "server",
34
+ "schema"
35
+ ]
36
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Adaptive Card schema validation using the official `adaptivecards` package.
3
+ *
4
+ * Validates that a card payload is a well-formed Adaptive Card that Teams can
5
+ * parse. Does NOT check visual rendering — only structural/schema correctness.
6
+ */
7
+
8
+ import { AdaptiveCard, SerializationContext } from "adaptivecards";
9
+
10
+ export interface CardValidationError {
11
+ /** Human-readable description of the issue */
12
+ message: string;
13
+ /** "parsing" | "schema" */
14
+ phase: string;
15
+ }
16
+
17
+ /**
18
+ * Validate a raw Adaptive Card JSON payload.
19
+ * Returns an empty array if the card is valid.
20
+ */
21
+ export function validateAdaptiveCard(
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ payload: Record<string, unknown>
24
+ ): CardValidationError[] {
25
+ const errors: CardValidationError[] = [];
26
+
27
+ // Basic type guard: must have type === "AdaptiveCard"
28
+ if (payload.type !== "AdaptiveCard") {
29
+ errors.push({
30
+ message: `Expected type "AdaptiveCard", got "${payload.type ?? "(missing)"}"`,
31
+ phase: "schema",
32
+ });
33
+ return errors; // No point parsing further
34
+ }
35
+
36
+ try {
37
+ const card = new AdaptiveCard();
38
+ const context = new SerializationContext();
39
+ card.parse(payload, context);
40
+
41
+ for (let i = 0; i < context.eventCount; i++) {
42
+ const event = context.getEventAt(i);
43
+ errors.push({
44
+ message: event.message ?? String(event),
45
+ phase: event.phase !== undefined ? String(event.phase) : "parsing",
46
+ });
47
+ }
48
+ } catch (err) {
49
+ errors.push({
50
+ message: err instanceof Error ? err.message : String(err),
51
+ phase: "parsing",
52
+ });
53
+ }
54
+
55
+ return errors;
56
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * HTTP server for multi-turn conversation execution.
3
+ *
4
+ * Provides a factory function that creates an HTTP server with:
5
+ * POST /run-conversation — execute a multi-turn conversation
6
+ * GET /health — health check
7
+ *
8
+ * All diagnostic logging goes to stderr. The returned port can be
9
+ * written to stdout by the caller (CLI wrapper).
10
+ */
11
+
12
+ import * as http from "http";
13
+ import { runConversation, log, logError } from "./runConversation";
14
+ import type { E2EConfig, ConversationInput } from "./conversationTypes";
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // Public types
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ export interface ConversationServerOptions {
21
+ /** Port to listen on. Default: 0 (OS-assigned). */
22
+ port?: number;
23
+ /** Host to bind to. Default: "127.0.0.1". */
24
+ host?: string;
25
+ }
26
+
27
+ export interface ConversationServer {
28
+ /** The port the server is listening on. */
29
+ port: number;
30
+ /** Gracefully shut down the server. */
31
+ close: () => Promise<void>;
32
+ }
33
+
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // Request body parsing
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ function readBody(req: http.IncomingMessage): Promise<string> {
39
+ return new Promise((resolve, reject) => {
40
+ const chunks: Buffer[] = [];
41
+ req.on("data", (chunk: Buffer) => chunks.push(chunk));
42
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
43
+ req.on("error", reject);
44
+ });
45
+ }
46
+
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+ // Request handler
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+
51
+ interface RunConversationRequest {
52
+ config: E2EConfig;
53
+ scenario: string;
54
+ input: ConversationInput;
55
+ }
56
+
57
+ async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
58
+ const url = req.url ?? "";
59
+ const method = req.method ?? "";
60
+
61
+ if (method === "GET" && url === "/health") {
62
+ res.writeHead(200, { "Content-Type": "application/json" });
63
+ res.end(JSON.stringify({ status: "ok" }));
64
+ return;
65
+ }
66
+
67
+ if (method === "POST" && url === "/run-conversation") {
68
+ let body: RunConversationRequest;
69
+ try {
70
+ const raw = await readBody(req);
71
+ body = JSON.parse(raw) as RunConversationRequest;
72
+ } catch (err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ res.writeHead(400, { "Content-Type": "application/json" });
75
+ res.end(JSON.stringify({ error: `Invalid JSON: ${message}` }));
76
+ return;
77
+ }
78
+
79
+ if (!body.config || !body.scenario || !body.input) {
80
+ res.writeHead(400, { "Content-Type": "application/json" });
81
+ res.end(JSON.stringify({ error: "Missing required fields: config, scenario, input" }));
82
+ return;
83
+ }
84
+
85
+ try {
86
+ log(`HTTP request: ${body.scenario}`);
87
+ const result = await runConversation(body.config, body.scenario, body.input);
88
+
89
+ res.writeHead(200, { "Content-Type": "application/json" });
90
+ res.end(JSON.stringify(result));
91
+ } catch (err) {
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ logError(`Unhandled error in runConversation: ${message}`);
94
+ res.writeHead(500, { "Content-Type": "application/json" });
95
+ res.end(JSON.stringify({ error: message }));
96
+ }
97
+ return;
98
+ }
99
+
100
+ res.writeHead(404, { "Content-Type": "application/json" });
101
+ res.end(JSON.stringify({ error: "Not found" }));
102
+ }
103
+
104
+ // ─────────────────────────────────────────────────────────────────────────────
105
+ // Factory
106
+ // ─────────────────────────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Create and start a conversation server.
110
+ *
111
+ * Returns a handle with the assigned port and a close() method.
112
+ */
113
+ export async function createConversationServer(
114
+ options?: ConversationServerOptions
115
+ ): Promise<ConversationServer> {
116
+ const port = options?.port ?? 0;
117
+ const host = options?.host ?? "127.0.0.1";
118
+
119
+ const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
120
+ handleRequest(req, res).catch((err) => {
121
+ logError(`Request handler crashed: ${String(err)}`);
122
+ if (!res.headersSent) {
123
+ res.writeHead(500, { "Content-Type": "application/json" });
124
+ }
125
+ res.end(JSON.stringify({ error: "Internal server error" }));
126
+ });
127
+ });
128
+
129
+ return new Promise<ConversationServer>((resolve, reject) => {
130
+ server.once("error", reject);
131
+
132
+ server.listen(port, host, () => {
133
+ const addr = server.address();
134
+ const assignedPort = typeof addr === "object" && addr ? addr.port : port;
135
+
136
+ log(`Server listening on http://${host}:${assignedPort}`);
137
+
138
+ resolve({
139
+ port: assignedPort,
140
+ close: () =>
141
+ new Promise<void>((res, rej) => {
142
+ server.close((err: Error | undefined) => (err ? rej(err) : res()));
143
+ }),
144
+ });
145
+ });
146
+ });
147
+ }