@agentick/gateway 0.0.1

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +477 -0
  3. package/dist/agent-registry.d.ts +51 -0
  4. package/dist/agent-registry.d.ts.map +1 -0
  5. package/dist/agent-registry.js +78 -0
  6. package/dist/agent-registry.js.map +1 -0
  7. package/dist/app-registry.d.ts +51 -0
  8. package/dist/app-registry.d.ts.map +1 -0
  9. package/dist/app-registry.js +78 -0
  10. package/dist/app-registry.js.map +1 -0
  11. package/dist/bin.d.ts +8 -0
  12. package/dist/bin.d.ts.map +1 -0
  13. package/dist/bin.js +37 -0
  14. package/dist/bin.js.map +1 -0
  15. package/dist/gateway.d.ts +165 -0
  16. package/dist/gateway.d.ts.map +1 -0
  17. package/dist/gateway.js +1339 -0
  18. package/dist/gateway.js.map +1 -0
  19. package/dist/http-transport.d.ts +65 -0
  20. package/dist/http-transport.d.ts.map +1 -0
  21. package/dist/http-transport.js +517 -0
  22. package/dist/http-transport.js.map +1 -0
  23. package/dist/index.d.ts +16 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +23 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/protocol.d.ts +162 -0
  28. package/dist/protocol.d.ts.map +1 -0
  29. package/dist/protocol.js +16 -0
  30. package/dist/protocol.js.map +1 -0
  31. package/dist/session-manager.d.ts +101 -0
  32. package/dist/session-manager.d.ts.map +1 -0
  33. package/dist/session-manager.js +208 -0
  34. package/dist/session-manager.js.map +1 -0
  35. package/dist/testing.d.ts +92 -0
  36. package/dist/testing.d.ts.map +1 -0
  37. package/dist/testing.js +129 -0
  38. package/dist/testing.js.map +1 -0
  39. package/dist/transport-protocol.d.ts +162 -0
  40. package/dist/transport-protocol.d.ts.map +1 -0
  41. package/dist/transport-protocol.js +16 -0
  42. package/dist/transport-protocol.js.map +1 -0
  43. package/dist/transport.d.ts +115 -0
  44. package/dist/transport.d.ts.map +1 -0
  45. package/dist/transport.js +56 -0
  46. package/dist/transport.js.map +1 -0
  47. package/dist/types.d.ts +314 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +37 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/websocket-server.d.ts +87 -0
  52. package/dist/websocket-server.d.ts.map +1 -0
  53. package/dist/websocket-server.js +245 -0
  54. package/dist/websocket-server.js.map +1 -0
  55. package/dist/ws-transport.d.ts +17 -0
  56. package/dist/ws-transport.d.ts.map +1 -0
  57. package/dist/ws-transport.js +174 -0
  58. package/dist/ws-transport.js.map +1 -0
  59. package/package.json +51 -0
  60. package/src/__tests__/custom-methods.spec.ts +220 -0
  61. package/src/__tests__/gateway-methods.spec.ts +262 -0
  62. package/src/__tests__/gateway.spec.ts +404 -0
  63. package/src/__tests__/guards.spec.ts +235 -0
  64. package/src/__tests__/protocol.spec.ts +58 -0
  65. package/src/__tests__/session-manager.spec.ts +220 -0
  66. package/src/__tests__/ws-transport.spec.ts +246 -0
  67. package/src/app-registry.ts +103 -0
  68. package/src/bin.ts +38 -0
  69. package/src/gateway.ts +1712 -0
  70. package/src/http-transport.ts +623 -0
  71. package/src/index.ts +94 -0
  72. package/src/session-manager.ts +272 -0
  73. package/src/testing.ts +236 -0
  74. package/src/transport-protocol.ts +249 -0
  75. package/src/transport.ts +191 -0
  76. package/src/types.ts +392 -0
  77. package/src/websocket-server.ts +303 -0
  78. package/src/ws-transport.ts +205 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Session Manager
3
+ *
4
+ * Manages persistent sessions across apps.
5
+ */
6
+
7
+ import type { Session } from "@agentick/core";
8
+ import { devToolsEmitter, type DTGatewaySessionEvent } from "@agentick/shared";
9
+ import type { AppRegistry, AppInfo } from "./app-registry.js";
10
+ import type { SessionState } from "./types.js";
11
+ import { parseSessionKey, formatSessionKey } from "./transport-protocol.js";
12
+
13
+ interface ManagedSession {
14
+ state: SessionState;
15
+ coreSession: Session | null;
16
+ appInfo: AppInfo;
17
+ /** The session name without app prefix - used when creating App sessions */
18
+ sessionName: string;
19
+ }
20
+
21
+ /**
22
+ * SessionManager configuration
23
+ */
24
+ export interface SessionManagerConfig {
25
+ /** Gateway ID for DevTools events */
26
+ gatewayId: string;
27
+ }
28
+
29
+ export class SessionManager {
30
+ private sessions = new Map<string, ManagedSession>();
31
+ private registry: AppRegistry;
32
+ private gatewayId: string;
33
+ private devToolsSequence = 0;
34
+
35
+ constructor(registry: AppRegistry, config?: SessionManagerConfig) {
36
+ this.registry = registry;
37
+ this.gatewayId = config?.gatewayId ?? "gateway";
38
+ }
39
+
40
+ /**
41
+ * Normalize a session key to the canonical format (appId:sessionName).
42
+ * This ensures consistent lookups regardless of input format.
43
+ */
44
+ private normalizeKey(sessionKey: string): string {
45
+ const { appId, sessionName } = parseSessionKey(sessionKey, this.registry.defaultId);
46
+ return formatSessionKey({ appId, sessionName });
47
+ }
48
+
49
+ /**
50
+ * Emit a DevTools session event
51
+ */
52
+ private emitDevToolsEvent(
53
+ action: DTGatewaySessionEvent["action"],
54
+ sessionId: string,
55
+ appId: string,
56
+ messageCount?: number,
57
+ clientId?: string,
58
+ ): void {
59
+ if (!devToolsEmitter.hasSubscribers()) return;
60
+
61
+ devToolsEmitter.emitEvent({
62
+ type: "gateway_session",
63
+ executionId: this.gatewayId,
64
+ action,
65
+ sessionId,
66
+ appId,
67
+ messageCount,
68
+ clientId,
69
+ sequence: this.devToolsSequence++,
70
+ timestamp: Date.now(),
71
+ } as DTGatewaySessionEvent);
72
+ }
73
+
74
+ /**
75
+ * Get or create a session
76
+ */
77
+ async getOrCreate(sessionKey: string, clientId?: string): Promise<ManagedSession> {
78
+ const normalizedKey = this.normalizeKey(sessionKey);
79
+
80
+ // Check if session exists
81
+ let session = this.sessions.get(normalizedKey);
82
+ if (session) {
83
+ session.state.lastActivityAt = new Date();
84
+ return session;
85
+ }
86
+
87
+ // Parse session key to get app and name
88
+ const { appId, sessionName } = parseSessionKey(sessionKey, this.registry.defaultId);
89
+
90
+ // Get the app
91
+ const appInfo = this.registry.resolve(appId);
92
+
93
+ // Create session state
94
+ const state: SessionState = {
95
+ id: formatSessionKey({ appId, sessionName }),
96
+ appId,
97
+ createdAt: new Date(),
98
+ lastActivityAt: new Date(),
99
+ messageCount: 0,
100
+ isActive: false,
101
+ subscribers: new Set(),
102
+ };
103
+
104
+ // Create the managed session
105
+ // Note: sessionName is the name WITHOUT app prefix - used when creating App sessions
106
+ session = {
107
+ state,
108
+ coreSession: null,
109
+ appInfo,
110
+ sessionName,
111
+ };
112
+
113
+ this.sessions.set(state.id, session);
114
+
115
+ // Emit DevTools event for session creation
116
+ this.emitDevToolsEvent("created", state.id, appId, 0, clientId);
117
+
118
+ return session;
119
+ }
120
+
121
+ /**
122
+ * Get an existing session
123
+ */
124
+ get(sessionKey: string): ManagedSession | undefined {
125
+ return this.sessions.get(this.normalizeKey(sessionKey));
126
+ }
127
+
128
+ /**
129
+ * Check if a session exists
130
+ */
131
+ has(sessionKey: string): boolean {
132
+ return this.sessions.has(this.normalizeKey(sessionKey));
133
+ }
134
+
135
+ /**
136
+ * Close a session
137
+ */
138
+ async close(sessionKey: string): Promise<void> {
139
+ const normalizedKey = this.normalizeKey(sessionKey);
140
+ const session = this.sessions.get(normalizedKey);
141
+ if (!session) return;
142
+
143
+ const { id, appId, messageCount } = session.state;
144
+
145
+ // Clean up session if active
146
+ if (session.coreSession) {
147
+ session.coreSession.close();
148
+ session.coreSession = null;
149
+ }
150
+
151
+ this.sessions.delete(normalizedKey);
152
+
153
+ // Emit DevTools event for session closure
154
+ this.emitDevToolsEvent("closed", id, appId, messageCount);
155
+ }
156
+
157
+ /**
158
+ * Reset a session (clear history but keep session)
159
+ */
160
+ async reset(sessionKey: string): Promise<void> {
161
+ const normalizedKey = this.normalizeKey(sessionKey);
162
+ const session = this.sessions.get(normalizedKey);
163
+ if (!session) return;
164
+
165
+ const { id, appId, messageCount } = session.state;
166
+
167
+ // Reset session state
168
+ session.state.messageCount = 0;
169
+ session.state.lastActivityAt = new Date();
170
+ if (session.coreSession) {
171
+ session.coreSession.close();
172
+ session.coreSession = null;
173
+ }
174
+
175
+ // Emit DevTools event for session reset (treated as closed + recreated)
176
+ this.emitDevToolsEvent("closed", id, appId, messageCount);
177
+ }
178
+
179
+ /**
180
+ * Get all session IDs
181
+ */
182
+ ids(): string[] {
183
+ return Array.from(this.sessions.keys());
184
+ }
185
+
186
+ /**
187
+ * Get all sessions
188
+ */
189
+ all(): ManagedSession[] {
190
+ return Array.from(this.sessions.values());
191
+ }
192
+
193
+ /**
194
+ * Get sessions for a specific app
195
+ */
196
+ forApp(appId: string): ManagedSession[] {
197
+ return this.all().filter((s) => s.state.appId === appId);
198
+ }
199
+
200
+ /**
201
+ * Get session count
202
+ */
203
+ get size(): number {
204
+ return this.sessions.size;
205
+ }
206
+
207
+ /**
208
+ * Add a subscriber to a session.
209
+ * Creates the session if it doesn't exist (ensures subscription is never lost).
210
+ */
211
+ async subscribe(sessionKey: string, clientId: string): Promise<void> {
212
+ const session = await this.getOrCreate(sessionKey, clientId);
213
+ session.state.subscribers.add(clientId);
214
+ }
215
+
216
+ /**
217
+ * Remove a subscriber from a session
218
+ */
219
+ unsubscribe(sessionKey: string, clientId: string): void {
220
+ const session = this.sessions.get(this.normalizeKey(sessionKey));
221
+ if (session) {
222
+ session.state.subscribers.delete(clientId);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Remove a client from all subscriptions
228
+ */
229
+ unsubscribeAll(clientId: string): void {
230
+ for (const session of this.sessions.values()) {
231
+ session.state.subscribers.delete(clientId);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Get subscribers for a session
237
+ */
238
+ getSubscribers(sessionKey: string): Set<string> {
239
+ const session = this.sessions.get(this.normalizeKey(sessionKey));
240
+ return session?.state.subscribers ?? new Set();
241
+ }
242
+
243
+ /**
244
+ * Update message count for a session
245
+ */
246
+ incrementMessageCount(sessionKey: string, clientId?: string): void {
247
+ const session = this.sessions.get(this.normalizeKey(sessionKey));
248
+ if (session) {
249
+ session.state.messageCount++;
250
+ session.state.lastActivityAt = new Date();
251
+
252
+ // Emit DevTools event for session message
253
+ this.emitDevToolsEvent(
254
+ "message",
255
+ session.state.id,
256
+ session.state.appId,
257
+ session.state.messageCount,
258
+ clientId,
259
+ );
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Set session active state
265
+ */
266
+ setActive(sessionKey: string, isActive: boolean): void {
267
+ const session = this.sessions.get(this.normalizeKey(sessionKey));
268
+ if (session) {
269
+ session.state.isActive = isActive;
270
+ }
271
+ }
272
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Gateway Testing Utilities
3
+ *
4
+ * Provides helpers for testing gateway interactions.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { createTestGateway, createMockApp } from '@agentick/gateway/testing';
9
+ *
10
+ * test('gateway handles messages', async () => {
11
+ * const mockApp = createMockApp({
12
+ * response: 'Hello!',
13
+ * });
14
+ *
15
+ * const { gateway, client, cleanup } = await createTestGateway({
16
+ * agents: { chat: mockApp },
17
+ * defaultAgent: 'chat',
18
+ * });
19
+ *
20
+ * try {
21
+ * const response = await client.send('main', 'Hi there');
22
+ * expect(response.payload.messageId).toBeDefined();
23
+ * } finally {
24
+ * await cleanup();
25
+ * }
26
+ * });
27
+ * ```
28
+ *
29
+ * @module @agentick/gateway/testing
30
+ */
31
+
32
+ import WebSocket from "ws";
33
+ import type { Gateway } from "./gateway.js";
34
+ import { createGateway } from "./gateway.js";
35
+ import type { GatewayConfig, GatewayEvents } from "./types.js";
36
+
37
+ // Re-export mock factories from core
38
+ export {
39
+ createMockApp,
40
+ createMockSession,
41
+ createMockExecutionHandle,
42
+ createTestProcedure,
43
+ type MockAppOptions,
44
+ type MockSessionOptions,
45
+ type MockSession,
46
+ type MockApp,
47
+ type MockSessionExecutionHandle,
48
+ type MockExecutionHandleOptions,
49
+ type TestProcedure,
50
+ type TestProcedureOptions,
51
+ } from "@agentick/core/testing";
52
+
53
+ // ============================================================================
54
+ // Test Gateway Factory
55
+ // ============================================================================
56
+
57
+ export interface TestGatewayOptions extends Omit<GatewayConfig, "port" | "host"> {
58
+ /** Custom port (default: random available port) */
59
+ port?: number;
60
+ }
61
+
62
+ export interface TestGatewayClient {
63
+ /** Send a request to the gateway */
64
+ request<T = unknown>(
65
+ method: string,
66
+ params?: Record<string, unknown>,
67
+ ): Promise<{
68
+ ok: boolean;
69
+ payload?: T;
70
+ error?: { code: string; message: string };
71
+ }>;
72
+
73
+ /** Send a message to a session */
74
+ send(
75
+ sessionId: string,
76
+ message: string,
77
+ ): Promise<{
78
+ ok: boolean;
79
+ payload?: { messageId: string };
80
+ error?: { code: string; message: string };
81
+ }>;
82
+
83
+ /** Collect events for a session */
84
+ collectEvents(
85
+ sessionId: string,
86
+ timeout?: number,
87
+ ): Promise<Array<{ type: string; data: unknown }>>;
88
+
89
+ /** Close the client connection */
90
+ close(): void;
91
+
92
+ /** The raw WebSocket */
93
+ ws: WebSocket;
94
+ }
95
+
96
+ export interface TestGatewayResult {
97
+ /** The gateway instance */
98
+ gateway: Gateway;
99
+
100
+ /** A connected test client */
101
+ client: TestGatewayClient;
102
+
103
+ /** Gateway URL */
104
+ url: string;
105
+
106
+ /** Port the gateway is running on */
107
+ port: number;
108
+
109
+ /** Clean up resources */
110
+ cleanup: () => Promise<void>;
111
+ }
112
+
113
+ /**
114
+ * Create a test gateway with a connected client.
115
+ *
116
+ * Automatically handles port allocation, client connection, and cleanup.
117
+ */
118
+ export async function createTestGateway(options: TestGatewayOptions): Promise<TestGatewayResult> {
119
+ // Use random high port to avoid conflicts
120
+ const port = options.port ?? 19000 + Math.floor(Math.random() * 1000);
121
+ const host = "127.0.0.1";
122
+ const url = `ws://${host}:${port}`;
123
+
124
+ const gateway = createGateway({
125
+ ...options,
126
+ port,
127
+ host,
128
+ });
129
+
130
+ await gateway.start();
131
+
132
+ // Create and connect client
133
+ const ws = new WebSocket(url);
134
+ await new Promise<void>((resolve, reject) => {
135
+ ws.on("open", () => resolve());
136
+ ws.on("error", reject);
137
+ });
138
+
139
+ // Authenticate
140
+ ws.send(JSON.stringify({ type: "connect", clientId: "test-client" }));
141
+ await new Promise((r) => setTimeout(r, 50));
142
+
143
+ let requestId = 0;
144
+ const pendingRequests = new Map<
145
+ string,
146
+ {
147
+ resolve: (value: any) => void;
148
+ reject: (error: Error) => void;
149
+ }
150
+ >();
151
+
152
+ ws.on("message", (data) => {
153
+ const msg = JSON.parse(data.toString());
154
+ if (msg.type === "res" && pendingRequests.has(msg.id)) {
155
+ const { resolve } = pendingRequests.get(msg.id)!;
156
+ pendingRequests.delete(msg.id);
157
+ resolve({ ok: msg.ok, payload: msg.payload, error: msg.error });
158
+ }
159
+ });
160
+
161
+ const client: TestGatewayClient = {
162
+ ws,
163
+
164
+ async request(method, params = {}) {
165
+ const id = `req-${++requestId}`;
166
+ return new Promise((resolve, reject) => {
167
+ pendingRequests.set(id, { resolve, reject });
168
+ ws.send(JSON.stringify({ type: "req", id, method, params }));
169
+
170
+ // Timeout after 5s
171
+ setTimeout(() => {
172
+ if (pendingRequests.has(id)) {
173
+ pendingRequests.delete(id);
174
+ reject(new Error(`Request ${method} timed out`));
175
+ }
176
+ }, 5000);
177
+ });
178
+ },
179
+
180
+ async send(sessionId, message) {
181
+ return this.request("send", { sessionId, message });
182
+ },
183
+
184
+ async collectEvents(sessionId, timeout = 1000) {
185
+ const events: Array<{ type: string; data: unknown }> = [];
186
+
187
+ const handler = (data: WebSocket.Data) => {
188
+ const msg = JSON.parse(data.toString());
189
+ if (msg.type === "event" && msg.sessionId === sessionId) {
190
+ events.push({ type: msg.event, data: msg.data });
191
+ }
192
+ };
193
+
194
+ ws.on("message", handler);
195
+ await new Promise((r) => setTimeout(r, timeout));
196
+ ws.off("message", handler);
197
+
198
+ return events;
199
+ },
200
+
201
+ close() {
202
+ ws.close();
203
+ },
204
+ };
205
+
206
+ const cleanup = async () => {
207
+ client.close();
208
+ await gateway.stop();
209
+ };
210
+
211
+ return { gateway, client, url, port, cleanup };
212
+ }
213
+
214
+ // ============================================================================
215
+ // Event Helpers
216
+ // ============================================================================
217
+
218
+ /**
219
+ * Wait for a specific gateway event.
220
+ */
221
+ export function waitForGatewayEvent<K extends keyof GatewayEvents>(
222
+ gateway: Gateway,
223
+ event: K,
224
+ timeout = 5000,
225
+ ): Promise<GatewayEvents[K]> {
226
+ return new Promise((resolve, reject) => {
227
+ const timer = setTimeout(() => {
228
+ reject(new Error(`Timeout waiting for gateway event: ${event}`));
229
+ }, timeout);
230
+
231
+ gateway.on(event, (payload) => {
232
+ clearTimeout(timer);
233
+ resolve(payload);
234
+ });
235
+ });
236
+ }