@animalabs/agent-framework 0.1.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 (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/src/agent.d.ts +130 -0
  4. package/dist/src/agent.d.ts.map +1 -0
  5. package/dist/src/agent.js +406 -0
  6. package/dist/src/agent.js.map +1 -0
  7. package/dist/src/api/mcp-server.d.ts +37 -0
  8. package/dist/src/api/mcp-server.d.ts.map +1 -0
  9. package/dist/src/api/mcp-server.js +598 -0
  10. package/dist/src/api/mcp-server.js.map +1 -0
  11. package/dist/src/api/server.d.ts +83 -0
  12. package/dist/src/api/server.d.ts.map +1 -0
  13. package/dist/src/api/server.js +785 -0
  14. package/dist/src/api/server.js.map +1 -0
  15. package/dist/src/api/types.d.ts +395 -0
  16. package/dist/src/api/types.d.ts.map +1 -0
  17. package/dist/src/api/types.js +5 -0
  18. package/dist/src/api/types.js.map +1 -0
  19. package/dist/src/framework.d.ts +309 -0
  20. package/dist/src/framework.d.ts.map +1 -0
  21. package/dist/src/framework.js +2309 -0
  22. package/dist/src/framework.js.map +1 -0
  23. package/dist/src/gate/event-gate.d.ts +75 -0
  24. package/dist/src/gate/event-gate.d.ts.map +1 -0
  25. package/dist/src/gate/event-gate.js +503 -0
  26. package/dist/src/gate/event-gate.js.map +1 -0
  27. package/dist/src/gate/index.d.ts +3 -0
  28. package/dist/src/gate/index.d.ts.map +1 -0
  29. package/dist/src/gate/index.js +2 -0
  30. package/dist/src/gate/index.js.map +1 -0
  31. package/dist/src/gate/types.d.ts +82 -0
  32. package/dist/src/gate/types.d.ts.map +1 -0
  33. package/dist/src/gate/types.js +9 -0
  34. package/dist/src/gate/types.js.map +1 -0
  35. package/dist/src/index.d.ts +17 -0
  36. package/dist/src/index.d.ts.map +1 -0
  37. package/dist/src/index.js +18 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/mcpl/channel-registry.d.ts +141 -0
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
  41. package/dist/src/mcpl/channel-registry.js +647 -0
  42. package/dist/src/mcpl/channel-registry.js.map +1 -0
  43. package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
  44. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
  45. package/dist/src/mcpl/checkpoint-manager.js +430 -0
  46. package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
  47. package/dist/src/mcpl/errors.d.ts +52 -0
  48. package/dist/src/mcpl/errors.d.ts.map +1 -0
  49. package/dist/src/mcpl/errors.js +93 -0
  50. package/dist/src/mcpl/errors.js.map +1 -0
  51. package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
  52. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
  53. package/dist/src/mcpl/feature-set-manager.js +250 -0
  54. package/dist/src/mcpl/feature-set-manager.js.map +1 -0
  55. package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
  56. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
  57. package/dist/src/mcpl/hook-orchestrator.js +217 -0
  58. package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
  59. package/dist/src/mcpl/index.d.ts +14 -0
  60. package/dist/src/mcpl/index.d.ts.map +1 -0
  61. package/dist/src/mcpl/index.js +21 -0
  62. package/dist/src/mcpl/index.js.map +1 -0
  63. package/dist/src/mcpl/inference-router.d.ts +67 -0
  64. package/dist/src/mcpl/inference-router.d.ts.map +1 -0
  65. package/dist/src/mcpl/inference-router.js +249 -0
  66. package/dist/src/mcpl/inference-router.js.map +1 -0
  67. package/dist/src/mcpl/push-handler.d.ts +60 -0
  68. package/dist/src/mcpl/push-handler.d.ts.map +1 -0
  69. package/dist/src/mcpl/push-handler.js +162 -0
  70. package/dist/src/mcpl/push-handler.js.map +1 -0
  71. package/dist/src/mcpl/scope-manager.d.ts +76 -0
  72. package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
  73. package/dist/src/mcpl/scope-manager.js +213 -0
  74. package/dist/src/mcpl/scope-manager.js.map +1 -0
  75. package/dist/src/mcpl/server-connection.d.ts +174 -0
  76. package/dist/src/mcpl/server-connection.d.ts.map +1 -0
  77. package/dist/src/mcpl/server-connection.js +559 -0
  78. package/dist/src/mcpl/server-connection.js.map +1 -0
  79. package/dist/src/mcpl/server-registry.d.ts +63 -0
  80. package/dist/src/mcpl/server-registry.d.ts.map +1 -0
  81. package/dist/src/mcpl/server-registry.js +119 -0
  82. package/dist/src/mcpl/server-registry.js.map +1 -0
  83. package/dist/src/mcpl/types.d.ts +755 -0
  84. package/dist/src/mcpl/types.d.ts.map +1 -0
  85. package/dist/src/mcpl/types.js +48 -0
  86. package/dist/src/mcpl/types.js.map +1 -0
  87. package/dist/src/module-registry.d.ts +93 -0
  88. package/dist/src/module-registry.d.ts.map +1 -0
  89. package/dist/src/module-registry.js +386 -0
  90. package/dist/src/module-registry.js.map +1 -0
  91. package/dist/src/modules/api/index.d.ts +22 -0
  92. package/dist/src/modules/api/index.d.ts.map +1 -0
  93. package/dist/src/modules/api/index.js +63 -0
  94. package/dist/src/modules/api/index.js.map +1 -0
  95. package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
  96. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
  97. package/dist/src/modules/discord/discord-js-client.js +273 -0
  98. package/dist/src/modules/discord/discord-js-client.js.map +1 -0
  99. package/dist/src/modules/discord/index.d.ts +82 -0
  100. package/dist/src/modules/discord/index.d.ts.map +1 -0
  101. package/dist/src/modules/discord/index.js +959 -0
  102. package/dist/src/modules/discord/index.js.map +1 -0
  103. package/dist/src/modules/discord/types.d.ts +327 -0
  104. package/dist/src/modules/discord/types.d.ts.map +1 -0
  105. package/dist/src/modules/discord/types.js +5 -0
  106. package/dist/src/modules/discord/types.js.map +1 -0
  107. package/dist/src/modules/index.d.ts +11 -0
  108. package/dist/src/modules/index.d.ts.map +1 -0
  109. package/dist/src/modules/index.js +7 -0
  110. package/dist/src/modules/index.js.map +1 -0
  111. package/dist/src/modules/workspace/index.d.ts +59 -0
  112. package/dist/src/modules/workspace/index.d.ts.map +1 -0
  113. package/dist/src/modules/workspace/index.js +831 -0
  114. package/dist/src/modules/workspace/index.js.map +1 -0
  115. package/dist/src/modules/workspace/sync.d.ts +48 -0
  116. package/dist/src/modules/workspace/sync.d.ts.map +1 -0
  117. package/dist/src/modules/workspace/sync.js +253 -0
  118. package/dist/src/modules/workspace/sync.js.map +1 -0
  119. package/dist/src/modules/workspace/types.d.ts +166 -0
  120. package/dist/src/modules/workspace/types.d.ts.map +1 -0
  121. package/dist/src/modules/workspace/types.js +5 -0
  122. package/dist/src/modules/workspace/types.js.map +1 -0
  123. package/dist/src/modules/workspace/watcher.d.ts +43 -0
  124. package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
  125. package/dist/src/modules/workspace/watcher.js +120 -0
  126. package/dist/src/modules/workspace/watcher.js.map +1 -0
  127. package/dist/src/queue.d.ts +50 -0
  128. package/dist/src/queue.d.ts.map +1 -0
  129. package/dist/src/queue.js +95 -0
  130. package/dist/src/queue.js.map +1 -0
  131. package/dist/src/safe-slice.d.ts +8 -0
  132. package/dist/src/safe-slice.d.ts.map +1 -0
  133. package/dist/src/safe-slice.js +18 -0
  134. package/dist/src/safe-slice.js.map +1 -0
  135. package/dist/src/types/agent.d.ts +118 -0
  136. package/dist/src/types/agent.d.ts.map +1 -0
  137. package/dist/src/types/agent.js +2 -0
  138. package/dist/src/types/agent.js.map +1 -0
  139. package/dist/src/types/events.d.ts +220 -0
  140. package/dist/src/types/events.d.ts.map +1 -0
  141. package/dist/src/types/events.js +2 -0
  142. package/dist/src/types/events.js.map +1 -0
  143. package/dist/src/types/framework.d.ts +184 -0
  144. package/dist/src/types/framework.d.ts.map +1 -0
  145. package/dist/src/types/framework.js +2 -0
  146. package/dist/src/types/framework.js.map +1 -0
  147. package/dist/src/types/index.d.ts +7 -0
  148. package/dist/src/types/index.d.ts.map +1 -0
  149. package/dist/src/types/index.js +7 -0
  150. package/dist/src/types/index.js.map +1 -0
  151. package/dist/src/types/inference-log.d.ts +64 -0
  152. package/dist/src/types/inference-log.d.ts.map +1 -0
  153. package/dist/src/types/inference-log.js +5 -0
  154. package/dist/src/types/inference-log.js.map +1 -0
  155. package/dist/src/types/module.d.ts +302 -0
  156. package/dist/src/types/module.d.ts.map +1 -0
  157. package/dist/src/types/module.js +2 -0
  158. package/dist/src/types/module.js.map +1 -0
  159. package/dist/src/types/trace.d.ts +173 -0
  160. package/dist/src/types/trace.d.ts.map +1 -0
  161. package/dist/src/types/trace.js +2 -0
  162. package/dist/src/types/trace.js.map +1 -0
  163. package/dist/test/event-gate.test.d.ts +2 -0
  164. package/dist/test/event-gate.test.d.ts.map +1 -0
  165. package/dist/test/event-gate.test.js +535 -0
  166. package/dist/test/event-gate.test.js.map +1 -0
  167. package/dist/test/framework.test.d.ts +5 -0
  168. package/dist/test/framework.test.d.ts.map +1 -0
  169. package/dist/test/framework.test.js +738 -0
  170. package/dist/test/framework.test.js.map +1 -0
  171. package/dist/tsconfig.tsbuildinfo +1 -0
  172. package/package.json +58 -0
  173. package/src/agent.ts +512 -0
  174. package/src/api/mcp-server.ts +687 -0
  175. package/src/api/server.ts +1023 -0
  176. package/src/api/types.ts +542 -0
  177. package/src/framework.ts +2784 -0
  178. package/src/gate/event-gate.ts +623 -0
  179. package/src/gate/index.ts +12 -0
  180. package/src/gate/types.ts +112 -0
  181. package/src/index.ts +63 -0
  182. package/src/mcpl/channel-registry.ts +824 -0
  183. package/src/mcpl/checkpoint-manager.ts +545 -0
  184. package/src/mcpl/errors.ts +114 -0
  185. package/src/mcpl/feature-set-manager.ts +333 -0
  186. package/src/mcpl/hook-orchestrator.ts +281 -0
  187. package/src/mcpl/index.ts +134 -0
  188. package/src/mcpl/inference-router.ts +335 -0
  189. package/src/mcpl/push-handler.ts +232 -0
  190. package/src/mcpl/scope-manager.ts +262 -0
  191. package/src/mcpl/server-connection.ts +694 -0
  192. package/src/mcpl/server-registry.ts +144 -0
  193. package/src/mcpl/types.ts +991 -0
  194. package/src/module-registry.ts +547 -0
  195. package/src/modules/api/index.ts +94 -0
  196. package/src/modules/discord/discord-js-client.ts +353 -0
  197. package/src/modules/discord/index.ts +1146 -0
  198. package/src/modules/discord/types.ts +465 -0
  199. package/src/modules/index.ts +43 -0
  200. package/src/modules/workspace/index.ts +969 -0
  201. package/src/modules/workspace/sync.ts +314 -0
  202. package/src/modules/workspace/types.ts +202 -0
  203. package/src/modules/workspace/watcher.ts +142 -0
  204. package/src/queue.ts +106 -0
  205. package/src/safe-slice.ts +18 -0
  206. package/src/types/agent.ts +119 -0
  207. package/src/types/events.ts +252 -0
  208. package/src/types/framework.ts +211 -0
  209. package/src/types/index.ts +6 -0
  210. package/src/types/inference-log.ts +64 -0
  211. package/src/types/module.ts +367 -0
  212. package/src/types/trace.ts +196 -0
@@ -0,0 +1,1023 @@
1
+ /**
2
+ * WebSocket API Server for the agent framework
3
+ *
4
+ * Provides real-time bidirectional communication:
5
+ * - Commands from clients (send messages, branch, inspect)
6
+ * - Events from framework (inference, tools, speech)
7
+ */
8
+
9
+ import { WebSocketServer, WebSocket } from 'ws';
10
+ import { createServer, IncomingMessage, ServerResponse } from 'node:http';
11
+ import type { ContentBlock } from '@animalabs/membrane';
12
+ import type { AgentFramework } from '../framework.js';
13
+ import type { TraceEvent, ProcessEvent } from '../types/index.js';
14
+ import type {
15
+ ApiServerConfig,
16
+ ApiRequest,
17
+ ApiResponse,
18
+ ApiEvent,
19
+ MessageSendParams,
20
+ MessageListParams,
21
+ InferenceRequestParams,
22
+ InferenceAbortParams,
23
+ BranchCreateParams,
24
+ BranchSwitchParams,
25
+ BranchDeleteParams,
26
+ AgentContextParams,
27
+ ModuleStateParams,
28
+ StoreInspectParams,
29
+ StoreStatesParams,
30
+ StoreSearchParams,
31
+ StoreSubscribeParams,
32
+ StoreUnsubscribeParams,
33
+ InferenceTailParams,
34
+ InferenceInspectParams,
35
+ InferenceSearchParams,
36
+ EventsTailParams,
37
+ EventsInspectParams,
38
+ EventsSearchParams,
39
+ EventsSubscribeParams,
40
+ PersistedEvent,
41
+ AgentInfo,
42
+ ModuleInfo,
43
+ BranchInfo,
44
+ StateInfo,
45
+ } from './types.js';
46
+
47
+ export * from './types.js';
48
+
49
+ const DEFAULT_CONFIG: Required<ApiServerConfig> = {
50
+ port: 8765,
51
+ host: 'localhost',
52
+ path: '/ws',
53
+ enableHttp: true,
54
+ };
55
+
56
+ /** Polling interval for subscriptions (ms) */
57
+ const SUBSCRIPTION_POLL_INTERVAL = 50;
58
+
59
+ /**
60
+ * API Server for the agent framework.
61
+ */
62
+ export class ApiServer {
63
+ private config: Required<ApiServerConfig>;
64
+ private framework: AgentFramework;
65
+ private wss: WebSocketServer | null = null;
66
+ private httpServer: ReturnType<typeof createServer> | null = null;
67
+ private clients: Set<WebSocket> = new Set();
68
+ private currentBranch = 'main';
69
+
70
+ /** Map from client to their subscription IDs */
71
+ private clientSubscriptions: Map<WebSocket, Set<string>> = new Map();
72
+ /** Subscription poll interval timer */
73
+ private subscriptionPollInterval: ReturnType<typeof setInterval> | null = null;
74
+
75
+ constructor(framework: AgentFramework, config: ApiServerConfig = {}) {
76
+ this.framework = framework;
77
+ this.config = { ...DEFAULT_CONFIG, ...config };
78
+ }
79
+
80
+ /**
81
+ * Start the API server.
82
+ */
83
+ async start(): Promise<void> {
84
+ // Create HTTP server for both WebSocket upgrade and REST endpoints
85
+ this.httpServer = createServer((req, res) => {
86
+ if (this.config.enableHttp) {
87
+ this.handleHttpRequest(req, res);
88
+ } else {
89
+ res.writeHead(404);
90
+ res.end();
91
+ }
92
+ });
93
+
94
+ // Create WebSocket server
95
+ this.wss = new WebSocketServer({
96
+ server: this.httpServer,
97
+ path: this.config.path,
98
+ });
99
+
100
+ this.wss.on('connection', (ws) => {
101
+ this.handleConnection(ws);
102
+ });
103
+
104
+ // Subscribe to trace events
105
+ this.framework.onTrace((event) => {
106
+ this.handleTraceEvent(event);
107
+ });
108
+
109
+ // Start listening
110
+ await new Promise<void>((resolve) => {
111
+ this.httpServer!.listen(this.config.port, this.config.host, () => {
112
+ console.log(
113
+ `API server listening on ws://${this.config.host}:${this.config.port}${this.config.path}`
114
+ );
115
+ resolve();
116
+ });
117
+ });
118
+
119
+ // Start subscription polling
120
+ this.subscriptionPollInterval = setInterval(() => {
121
+ this.pollSubscriptions();
122
+ }, SUBSCRIPTION_POLL_INTERVAL);
123
+ }
124
+
125
+ /**
126
+ * Stop the API server.
127
+ */
128
+ async stop(): Promise<void> {
129
+ // Stop subscription polling
130
+ if (this.subscriptionPollInterval) {
131
+ clearInterval(this.subscriptionPollInterval);
132
+ this.subscriptionPollInterval = null;
133
+ }
134
+
135
+ // Cleanup all subscriptions
136
+ const store = this.framework.getStore();
137
+ for (const [_, subscriptionIds] of this.clientSubscriptions) {
138
+ for (const subId of subscriptionIds) {
139
+ store.unsubscribe(subId);
140
+ }
141
+ }
142
+ this.clientSubscriptions.clear();
143
+
144
+ // Close all client connections
145
+ for (const client of this.clients) {
146
+ client.close();
147
+ }
148
+ this.clients.clear();
149
+
150
+ // Close servers
151
+ if (this.wss) {
152
+ this.wss.close();
153
+ this.wss = null;
154
+ }
155
+
156
+ if (this.httpServer) {
157
+ await new Promise<void>((resolve) => {
158
+ this.httpServer!.close(() => resolve());
159
+ });
160
+ this.httpServer = null;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Get the server URL.
166
+ */
167
+ get url(): string {
168
+ return `ws://${this.config.host}:${this.config.port}${this.config.path}`;
169
+ }
170
+
171
+ // ==========================================================================
172
+ // WebSocket Handling
173
+ // ==========================================================================
174
+
175
+ private handleConnection(ws: WebSocket): void {
176
+ this.clients.add(ws);
177
+
178
+ ws.on('message', (data) => {
179
+ try {
180
+ const message = JSON.parse(data.toString());
181
+ this.handleClientMessage(ws, message);
182
+ } catch (error) {
183
+ this.sendResponse(ws, {
184
+ type: 'response',
185
+ success: false,
186
+ error: 'Invalid JSON',
187
+ });
188
+ }
189
+ });
190
+
191
+ ws.on('close', () => {
192
+ this.cleanupClientSubscriptions(ws);
193
+ this.clients.delete(ws);
194
+ });
195
+
196
+ ws.on('error', (error) => {
197
+ console.error('WebSocket error:', error);
198
+ this.cleanupClientSubscriptions(ws);
199
+ this.clients.delete(ws);
200
+ });
201
+
202
+ // Send welcome event
203
+ this.sendEvent(ws, 'connected', {
204
+ currentBranch: this.currentBranch,
205
+ agents: this.framework.getAllAgents().map((a) => a.name),
206
+ });
207
+ }
208
+
209
+ private async handleClientMessage(
210
+ ws: WebSocket,
211
+ message: ApiRequest
212
+ ): Promise<void> {
213
+ if (message.type !== 'request') {
214
+ this.sendResponse(ws, {
215
+ type: 'response',
216
+ id: message.id,
217
+ success: false,
218
+ error: 'Expected request message',
219
+ });
220
+ return;
221
+ }
222
+
223
+ try {
224
+ const result = await this.executeCommand(ws, message.command, message.params);
225
+ this.sendResponse(ws, {
226
+ type: 'response',
227
+ id: message.id,
228
+ success: true,
229
+ data: result,
230
+ });
231
+ } catch (error) {
232
+ this.sendResponse(ws, {
233
+ type: 'response',
234
+ id: message.id,
235
+ success: false,
236
+ error: error instanceof Error ? error.message : String(error),
237
+ });
238
+ }
239
+ }
240
+
241
+ private sendResponse(ws: WebSocket, response: ApiResponse): void {
242
+ if (ws.readyState === WebSocket.OPEN) {
243
+ ws.send(JSON.stringify(response));
244
+ }
245
+ }
246
+
247
+ private sendEvent(ws: WebSocket, event: string, data: unknown): void {
248
+ if (ws.readyState === WebSocket.OPEN) {
249
+ const message: ApiEvent = { type: 'event', event, data };
250
+ ws.send(JSON.stringify(message));
251
+ }
252
+ }
253
+
254
+ private broadcast(event: string, data: unknown): void {
255
+ const message: ApiEvent = { type: 'event', event, data };
256
+ const json = JSON.stringify(message);
257
+
258
+ for (const client of this.clients) {
259
+ if (client.readyState === WebSocket.OPEN) {
260
+ client.send(json);
261
+ }
262
+ }
263
+ }
264
+
265
+ // ==========================================================================
266
+ // Trace Event Handling
267
+ // ==========================================================================
268
+
269
+ private handleTraceEvent(event: TraceEvent): void {
270
+ // Skip process:received — clients don't need raw input events
271
+ if (event.type === 'process:received') {
272
+ return;
273
+ }
274
+
275
+ // Broadcast all other trace events directly — no translation needed
276
+ // TraceEvents are already serializable (error is string, not Error)
277
+ this.broadcast(event.type, event);
278
+ }
279
+
280
+ // ==========================================================================
281
+ // Command Execution
282
+ // ==========================================================================
283
+
284
+ private async executeCommand(
285
+ ws: WebSocket,
286
+ command: string,
287
+ params?: Record<string, unknown>
288
+ ): Promise<unknown> {
289
+ switch (command) {
290
+ // Conversation
291
+ case 'message.send':
292
+ return this.cmdMessageSend(params as unknown as MessageSendParams);
293
+ case 'message.list':
294
+ return this.cmdMessageList(params as unknown as MessageListParams);
295
+ case 'inference.request':
296
+ return this.cmdInferenceRequest(params as unknown as InferenceRequestParams);
297
+ case 'inference.abort':
298
+ return this.cmdInferenceAbort(params as unknown as InferenceAbortParams);
299
+
300
+ // Branching
301
+ case 'branch.list':
302
+ return this.cmdBranchList();
303
+ case 'branch.create':
304
+ return this.cmdBranchCreate(params as unknown as BranchCreateParams);
305
+ case 'branch.switch':
306
+ return this.cmdBranchSwitch(params as unknown as BranchSwitchParams);
307
+ case 'branch.current':
308
+ return this.cmdBranchCurrent();
309
+ case 'branch.delete':
310
+ return this.cmdBranchDelete(params as unknown as BranchDeleteParams);
311
+
312
+ // Undo/Redo
313
+ case 'undo':
314
+ return this.cmdUndo(params as unknown as { agentName: string });
315
+ case 'redo':
316
+ return this.cmdRedo(params as unknown as { agentName: string });
317
+ case 'undo.state':
318
+ return this.cmdUndoState(params as unknown as { agentName: string });
319
+
320
+ // Inspection
321
+ case 'agent.list':
322
+ return this.cmdAgentList();
323
+ case 'agent.context':
324
+ return this.cmdAgentContext(params as unknown as AgentContextParams);
325
+ case 'module.list':
326
+ return this.cmdModuleList();
327
+ case 'module.state':
328
+ return this.cmdModuleState(params as unknown as ModuleStateParams);
329
+ case 'store.states':
330
+ return this.cmdStoreStates(params as unknown as StoreStatesParams);
331
+ case 'store.inspect':
332
+ return this.cmdStoreInspect(params as unknown as StoreInspectParams);
333
+ case 'store.search':
334
+ return this.cmdStoreSearch(params as unknown as StoreSearchParams);
335
+
336
+ // Subscriptions
337
+ case 'store.subscribe':
338
+ return this.cmdStoreSubscribe(ws, params as unknown as StoreSubscribeParams);
339
+ case 'store.unsubscribe':
340
+ return this.cmdStoreUnsubscribe(ws, params as unknown as StoreUnsubscribeParams);
341
+
342
+ // Inference logs
343
+ case 'inference.tail':
344
+ return this.cmdInferenceTail(params as unknown as InferenceTailParams);
345
+ case 'inference.inspect':
346
+ return this.cmdInferenceInspect(params as unknown as InferenceInspectParams);
347
+ case 'inference.search':
348
+ return this.cmdInferenceSearch(params as unknown as InferenceSearchParams);
349
+
350
+ // Event logs
351
+ case 'events.tail':
352
+ return this.cmdEventsTail(params as unknown as EventsTailParams);
353
+ case 'events.inspect':
354
+ return this.cmdEventsInspect(params as unknown as EventsInspectParams);
355
+ case 'events.search':
356
+ return this.cmdEventsSearch(params as unknown as EventsSearchParams);
357
+ case 'events.subscribe':
358
+ return this.cmdEventsSubscribe(params as unknown as EventsSubscribeParams);
359
+
360
+ default:
361
+ throw new Error(`Unknown command: ${command}`);
362
+ }
363
+ }
364
+
365
+ // ==========================================================================
366
+ // Conversation Commands
367
+ // ==========================================================================
368
+
369
+ private async cmdMessageSend(params: MessageSendParams): Promise<{ messageId: string }> {
370
+ if (!params.participant || !params.content) {
371
+ throw new Error('participant and content are required');
372
+ }
373
+
374
+ // Push api:message event for the ApiModule to handle
375
+ this.framework.pushEvent({
376
+ type: 'api:message',
377
+ participant: params.participant,
378
+ content: params.content,
379
+ triggerInference: params.triggerInference ?? true,
380
+ targetAgents: params.targetAgents,
381
+ } as ProcessEvent);
382
+
383
+ return { messageId: 'pending' }; // TODO: return actual ID
384
+ }
385
+
386
+ private async cmdMessageList(params?: MessageListParams): Promise<{ messages: unknown[] }> {
387
+ // Get messages from first agent's context manager
388
+ const agent = this.framework.getAllAgents()[0];
389
+ if (!agent) {
390
+ return { messages: [] };
391
+ }
392
+
393
+ const contextManager = agent.getContextManager();
394
+ const { messages } = await contextManager.compile();
395
+
396
+ const limit = params?.limit ?? 50;
397
+ const offset = params?.offset ?? 0;
398
+
399
+ return {
400
+ messages: messages.slice(-(limit + offset), offset ? -offset : undefined),
401
+ };
402
+ }
403
+
404
+ private async cmdInferenceRequest(params?: InferenceRequestParams): Promise<{ queued: boolean }> {
405
+ // Push api:inference-request event for the ApiModule to handle
406
+ this.framework.pushEvent({
407
+ type: 'api:inference-request',
408
+ agentName: params?.agentName,
409
+ reason: params?.reason,
410
+ } as ProcessEvent);
411
+
412
+ return { queued: true };
413
+ }
414
+
415
+ private async cmdInferenceAbort(params?: InferenceAbortParams): Promise<{ aborted: boolean }> {
416
+ if (!params?.agentName) {
417
+ throw new Error('agentName is required');
418
+ }
419
+
420
+ const aborted = this.framework.abortInference(params.agentName, params.reason);
421
+ return { aborted };
422
+ }
423
+
424
+ // ==========================================================================
425
+ // Branch Commands
426
+ // ==========================================================================
427
+
428
+ private async cmdBranchList(): Promise<{ branches: BranchInfo[] }> {
429
+ const store = this.framework.getStore();
430
+ const branches = store.listBranches();
431
+
432
+ return {
433
+ branches: branches.map((branch) => ({
434
+ name: branch.name,
435
+ isCurrent: branch.name === this.currentBranch,
436
+ })),
437
+ };
438
+ }
439
+
440
+ private async cmdBranchCreate(params: BranchCreateParams): Promise<{ name: string }> {
441
+ if (!params.name) {
442
+ throw new Error('name is required');
443
+ }
444
+
445
+ const store = this.framework.getStore();
446
+ store.createBranch(params.name);
447
+
448
+ this.broadcast('branch:created', {
449
+ name: params.name,
450
+ parent: this.currentBranch,
451
+ });
452
+
453
+ if (params.switchTo) {
454
+ await this.cmdBranchSwitch({ name: params.name });
455
+ }
456
+
457
+ return { name: params.name };
458
+ }
459
+
460
+ private async cmdBranchSwitch(params: BranchSwitchParams): Promise<{ switched: boolean }> {
461
+ if (!params.name) {
462
+ throw new Error('name is required');
463
+ }
464
+
465
+ const store = this.framework.getStore();
466
+ const previousBranch = this.currentBranch;
467
+
468
+ store.switchBranch(params.name);
469
+ this.currentBranch = params.name;
470
+
471
+ // Materialize config files from the new branch
472
+ const ws = this.framework.getModule('workspace');
473
+ if (ws && 'materializeMount' in ws) {
474
+ await (ws as any).materializeMount('_config');
475
+ }
476
+
477
+ this.broadcast('branch:switched', {
478
+ from: previousBranch,
479
+ to: params.name,
480
+ });
481
+
482
+ return { switched: true };
483
+ }
484
+
485
+ private async cmdBranchCurrent(): Promise<{ name: string }> {
486
+ return { name: this.currentBranch };
487
+ }
488
+
489
+ private async cmdBranchDelete(params: BranchDeleteParams): Promise<{ deleted: boolean }> {
490
+ if (!params.name) {
491
+ throw new Error('name is required');
492
+ }
493
+
494
+ if (params.name === this.currentBranch) {
495
+ throw new Error('Cannot delete current branch');
496
+ }
497
+
498
+ if (params.name === 'main') {
499
+ throw new Error('Cannot delete main branch');
500
+ }
501
+
502
+ const store = this.framework.getStore();
503
+ store.deleteBranch(params.name);
504
+
505
+ this.broadcast('branch:deleted', { name: params.name });
506
+
507
+ return { deleted: true };
508
+ }
509
+
510
+ // ==========================================================================
511
+ // Undo/Redo Commands
512
+ // ==========================================================================
513
+
514
+ private cmdUndo(params: { agentName: string }): unknown {
515
+ if (!params.agentName) {
516
+ throw new Error('agentName is required');
517
+ }
518
+
519
+ const result = this.framework.undoLastTurn(params.agentName);
520
+
521
+ if (result.undone) {
522
+ this.currentBranch = result.toBranch!;
523
+ this.broadcast('branch:switched', {
524
+ from: result.fromBranch,
525
+ to: result.toBranch,
526
+ reason: 'undo',
527
+ });
528
+ }
529
+
530
+ return result;
531
+ }
532
+
533
+ private cmdRedo(params: { agentName: string }): unknown {
534
+ if (!params.agentName) {
535
+ throw new Error('agentName is required');
536
+ }
537
+
538
+ const result = this.framework.redo(params.agentName);
539
+
540
+ if (result.redone) {
541
+ this.currentBranch = result.toBranch!;
542
+ this.broadcast('branch:switched', {
543
+ from: result.fromBranch,
544
+ to: result.toBranch,
545
+ reason: 'redo',
546
+ });
547
+ }
548
+
549
+ return result;
550
+ }
551
+
552
+ private cmdUndoState(params: { agentName: string }): unknown {
553
+ if (!params.agentName) {
554
+ throw new Error('agentName is required');
555
+ }
556
+
557
+ return this.framework.getUndoRedoState(params.agentName);
558
+ }
559
+
560
+ // ==========================================================================
561
+ // Inspection Commands
562
+ // ==========================================================================
563
+
564
+ private async cmdAgentList(): Promise<{ agents: AgentInfo[] }> {
565
+ const agents = this.framework.getAllAgents();
566
+
567
+ return {
568
+ agents: agents.map((a) => ({
569
+ name: a.name,
570
+ model: a.model,
571
+ status: a.state.status,
572
+ systemPromptPreview: a.systemPrompt.substring(0, 200) + '...',
573
+ allowedTools: a.allowedTools,
574
+ })),
575
+ };
576
+ }
577
+
578
+ private async cmdAgentContext(params: AgentContextParams): Promise<{ context: unknown }> {
579
+ if (!params.agentName) {
580
+ throw new Error('agentName is required');
581
+ }
582
+
583
+ const agent = this.framework.getAgent(params.agentName);
584
+ if (!agent) {
585
+ throw new Error(`Agent not found: ${params.agentName}`);
586
+ }
587
+
588
+ const contextManager = agent.getContextManager();
589
+ const { messages } = await contextManager.compile(
590
+ params.maxTokens
591
+ ? { maxTokens: params.maxTokens, reserveForResponse: 4096 }
592
+ : undefined
593
+ );
594
+
595
+ return { context: messages };
596
+ }
597
+
598
+ private async cmdModuleList(): Promise<{ modules: ModuleInfo[] }> {
599
+ const modules = this.framework.getAllModules();
600
+ const allTools = this.framework.getAllTools();
601
+
602
+ return {
603
+ modules: modules.map((module) => ({
604
+ name: module.name,
605
+ tools: allTools
606
+ .filter((t) => t.name.startsWith(`${module.name}:`))
607
+ .map((t) => t.name),
608
+ })),
609
+ };
610
+ }
611
+
612
+ private async cmdModuleState(params: ModuleStateParams): Promise<{ state: unknown }> {
613
+ if (!params.moduleName) {
614
+ throw new Error('moduleName is required');
615
+ }
616
+
617
+ const store = this.framework.getStore();
618
+ const stateId = `modules/${params.moduleName}/state`;
619
+ const state = store.getStateJson(stateId);
620
+
621
+ return { state };
622
+ }
623
+
624
+ private async cmdStoreStates(params?: StoreStatesParams): Promise<{ states: StateInfo[] }> {
625
+ const store = this.framework.getStore();
626
+ let stateInfos = store.listStates();
627
+
628
+ // Filter by namespace
629
+ if (params?.namespace) {
630
+ stateInfos = stateInfos.filter((info) => info.id.startsWith(params.namespace!));
631
+ }
632
+
633
+ // Apply limit (default 50)
634
+ const limit = params?.limit ?? 50;
635
+ stateInfos = stateInfos.slice(-limit);
636
+
637
+ return {
638
+ states: stateInfos.map((info) => ({
639
+ id: info.id,
640
+ strategy: info.strategy,
641
+ hasData: store.getStateJson(info.id) !== null,
642
+ })),
643
+ };
644
+ }
645
+
646
+ private async cmdStoreSearch(params?: StoreSearchParams): Promise<{ results: Array<{ stateId: string; preview: string }>; total: number; hasMore: boolean }> {
647
+ const store = this.framework.getStore();
648
+ let stateInfos = store.listStates();
649
+
650
+ // Filter by namespace
651
+ if (params?.namespace) {
652
+ stateInfos = stateInfos.filter((info) => info.id.startsWith(params.namespace!));
653
+ }
654
+
655
+ const limit = params?.limit ?? 20;
656
+ const offset = params?.offset ?? 0;
657
+ const previewLength = params?.previewLength ?? 300;
658
+ const pattern = params?.contentPattern ? new RegExp(params.contentPattern, 'i') : null;
659
+
660
+ // Collect all matching results first (for accurate total count)
661
+ const allMatches: Array<{ stateId: string; content: string }> = [];
662
+
663
+ for (const info of stateInfos) {
664
+ const data = store.getStateJson(info.id);
665
+ if (data === null) continue;
666
+
667
+ const content = JSON.stringify(data);
668
+
669
+ // If pattern specified, filter by it
670
+ if (pattern && !pattern.test(content)) continue;
671
+
672
+ allMatches.push({ stateId: info.id, content });
673
+ }
674
+
675
+ // Apply pagination
676
+ const paged = allMatches.slice(offset, offset + limit);
677
+ const results = paged.map(({ stateId, content }) => ({
678
+ stateId,
679
+ preview: content.length > previewLength ? content.slice(0, previewLength) + '...' : content,
680
+ }));
681
+
682
+ return {
683
+ results,
684
+ total: allMatches.length,
685
+ hasMore: offset + limit < allMatches.length,
686
+ };
687
+ }
688
+
689
+ private async cmdStoreInspect(params: StoreInspectParams): Promise<{ data: unknown }> {
690
+ if (!params.stateId) {
691
+ throw new Error('stateId is required');
692
+ }
693
+
694
+ const store = this.framework.getStore();
695
+ const data = store.getStateJson(params.stateId);
696
+
697
+ return { data };
698
+ }
699
+
700
+ // ==========================================================================
701
+ // Inference Log Commands
702
+ // ==========================================================================
703
+
704
+ private async cmdInferenceTail(params?: InferenceTailParams): Promise<{ entries: unknown[] }> {
705
+ const entries = this.framework.tailInferenceLogs(
706
+ params?.count ?? 10,
707
+ params?.agentName
708
+ );
709
+
710
+ return { entries };
711
+ }
712
+
713
+ private async cmdInferenceInspect(params: InferenceInspectParams): Promise<{ entry: unknown }> {
714
+ if (params.sequence === undefined || params.sequence === null) {
715
+ throw new Error('sequence is required');
716
+ }
717
+
718
+ const entry = this.framework.getInferenceLog(params.sequence);
719
+ if (!entry) {
720
+ throw new Error(`Inference log not found: ${params.sequence}`);
721
+ }
722
+
723
+ return { entry };
724
+ }
725
+
726
+ private async cmdInferenceSearch(params?: InferenceSearchParams): Promise<unknown> {
727
+ const result = this.framework.queryInferenceLogs({
728
+ agentName: params?.agentName,
729
+ limit: params?.limit,
730
+ offset: params?.offset,
731
+ pattern: params?.pattern,
732
+ errorsOnly: params?.errorsOnly,
733
+ });
734
+
735
+ return result;
736
+ }
737
+
738
+ // ==========================================================================
739
+ // Event Log Commands
740
+ // ==========================================================================
741
+
742
+ private async cmdEventsTail(params?: EventsTailParams): Promise<{ entries: unknown[] }> {
743
+ const entries = this.framework.tailProcessLogs(
744
+ params?.count ?? 10,
745
+ params?.eventType
746
+ );
747
+
748
+ return { entries };
749
+ }
750
+
751
+ private async cmdEventsInspect(params: EventsInspectParams): Promise<{ entry: unknown }> {
752
+ if (params.sequence === undefined || params.sequence === null) {
753
+ throw new Error('sequence is required');
754
+ }
755
+
756
+ const entry = this.framework.getProcessLog(params.sequence);
757
+ if (!entry) {
758
+ throw new Error(`Process log not found: ${params.sequence}`);
759
+ }
760
+
761
+ return { entry };
762
+ }
763
+
764
+ private async cmdEventsSearch(params?: EventsSearchParams): Promise<unknown> {
765
+ const result = this.framework.queryProcessLogs({
766
+ eventType: params?.eventType,
767
+ moduleName: params?.moduleName,
768
+ limit: params?.limit,
769
+ offset: params?.offset,
770
+ pattern: params?.pattern,
771
+ });
772
+
773
+ return result;
774
+ }
775
+
776
+ private async cmdEventsSubscribe(
777
+ params?: EventsSubscribeParams
778
+ ): Promise<{ subscribed: string[]; history: PersistedEvent[] }> {
779
+ const types = params?.types ?? ['*'];
780
+ const limit = params?.limit ?? 100;
781
+
782
+ // Get historical events from the process log
783
+ const processEntries = this.framework.tailProcessLogs(limit);
784
+
785
+ // Convert ProcessLogEntry to PersistedEvent format
786
+ const history: PersistedEvent[] = processEntries.map((item) => {
787
+ const { sequence, entry } = item;
788
+ const event = entry.processEvent;
789
+ const timestamp = entry.timestamp;
790
+
791
+ // Extract agent/module info from event if available
792
+ const agentName =
793
+ (event as Record<string, unknown>).agentName as string | undefined;
794
+ const moduleName =
795
+ (event as Record<string, unknown>).moduleName as string | undefined;
796
+
797
+ return {
798
+ id: `evt-${sequence}`,
799
+ sequence,
800
+ timestamp,
801
+ type: event?.type ?? 'unknown',
802
+ payload: event,
803
+ source: 'chronicle',
804
+ agentName,
805
+ moduleName,
806
+ };
807
+ });
808
+
809
+ // Filter by types if not wildcard
810
+ const filteredHistory =
811
+ types.includes('*')
812
+ ? history
813
+ : history.filter((evt) =>
814
+ types.some((pattern) => matchEventType(pattern, evt.type))
815
+ );
816
+
817
+ return {
818
+ subscribed: types,
819
+ history: filteredHistory,
820
+ };
821
+ }
822
+
823
+ // ==========================================================================
824
+ // HTTP Handling (optional REST endpoints)
825
+ // ==========================================================================
826
+
827
+ private handleHttpRequest(req: IncomingMessage, res: ServerResponse): void {
828
+ // CORS headers
829
+ res.setHeader('Access-Control-Allow-Origin', '*');
830
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
831
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
832
+
833
+ if (req.method === 'OPTIONS') {
834
+ res.writeHead(204);
835
+ res.end();
836
+ return;
837
+ }
838
+
839
+ const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
840
+
841
+ // Health check
842
+ if (url.pathname === '/health') {
843
+ res.writeHead(200, { 'Content-Type': 'application/json' });
844
+ res.end(JSON.stringify({ status: 'ok', branch: this.currentBranch }));
845
+ return;
846
+ }
847
+
848
+ // Status
849
+ if (url.pathname === '/status') {
850
+ const agents = this.framework.getAllAgents();
851
+ res.writeHead(200, { 'Content-Type': 'application/json' });
852
+ res.end(
853
+ JSON.stringify({
854
+ branch: this.currentBranch,
855
+ agents: agents.map((a) => ({ name: a.name, status: a.state.status })),
856
+ clients: this.clients.size,
857
+ queueDepth: this.framework.getQueueDepth(),
858
+ })
859
+ );
860
+ return;
861
+ }
862
+
863
+ res.writeHead(404, { 'Content-Type': 'application/json' });
864
+ res.end(JSON.stringify({ error: 'Not found' }));
865
+ }
866
+
867
+ // ==========================================================================
868
+ // Subscription Commands
869
+ // ==========================================================================
870
+
871
+ private async cmdStoreSubscribe(
872
+ ws: WebSocket,
873
+ params?: StoreSubscribeParams
874
+ ): Promise<{ subscriptionId: string }> {
875
+ const store = this.framework.getStore();
876
+
877
+ // Build subscription config
878
+ const config: {
879
+ bufferSize?: number;
880
+ maxSnapshotBytes?: number;
881
+ fromSequence?: number;
882
+ filter?: {
883
+ recordTypes?: string[];
884
+ branch?: string;
885
+ stateIds?: string[];
886
+ includeRecords?: boolean;
887
+ includeStateChanges?: boolean;
888
+ includeBranchEvents?: boolean;
889
+ };
890
+ } = {};
891
+
892
+ if (params?.bufferSize !== undefined) {
893
+ config.bufferSize = params.bufferSize;
894
+ }
895
+ if (params?.maxSnapshotBytes !== undefined) {
896
+ config.maxSnapshotBytes = params.maxSnapshotBytes;
897
+ }
898
+ if (params?.fromSequence !== undefined) {
899
+ config.fromSequence = params.fromSequence;
900
+ }
901
+ if (params?.filter) {
902
+ config.filter = {
903
+ recordTypes: params.filter.recordTypes,
904
+ branch: params.filter.branch,
905
+ stateIds: params.filter.stateIds,
906
+ includeRecords: params.filter.includeRecords ?? false,
907
+ includeStateChanges: params.filter.includeStateChanges ?? false,
908
+ includeBranchEvents: params.filter.includeBranchEvents ?? false,
909
+ };
910
+ }
911
+
912
+ // Create subscription
913
+ const subscriptionId = store.subscribe(config);
914
+
915
+ // Track this subscription for the client
916
+ if (!this.clientSubscriptions.has(ws)) {
917
+ this.clientSubscriptions.set(ws, new Set());
918
+ }
919
+ this.clientSubscriptions.get(ws)!.add(subscriptionId);
920
+
921
+ // If fromSequence is set, perform catch-up
922
+ if (params?.fromSequence !== undefined) {
923
+ store.catchUpSubscription(subscriptionId);
924
+ } else {
925
+ // Mark as caught up immediately for live-only subscriptions
926
+ // The store will send the CaughtUp event
927
+ store.catchUpSubscription(subscriptionId);
928
+ }
929
+
930
+ return { subscriptionId };
931
+ }
932
+
933
+ private async cmdStoreUnsubscribe(
934
+ ws: WebSocket,
935
+ params: StoreUnsubscribeParams
936
+ ): Promise<{ success: boolean }> {
937
+ if (!params.subscriptionId) {
938
+ throw new Error('subscriptionId is required');
939
+ }
940
+
941
+ const store = this.framework.getStore();
942
+
943
+ // Remove from client tracking
944
+ const clientSubs = this.clientSubscriptions.get(ws);
945
+ if (clientSubs) {
946
+ clientSubs.delete(params.subscriptionId);
947
+ }
948
+
949
+ // Unsubscribe from store
950
+ store.unsubscribe(params.subscriptionId);
951
+
952
+ return { success: true };
953
+ }
954
+
955
+ // ==========================================================================
956
+ // Subscription Helpers
957
+ // ==========================================================================
958
+
959
+ /**
960
+ * Cleanup all subscriptions for a disconnected client.
961
+ */
962
+ private cleanupClientSubscriptions(ws: WebSocket): void {
963
+ const subscriptionIds = this.clientSubscriptions.get(ws);
964
+ if (!subscriptionIds) return;
965
+
966
+ const store = this.framework.getStore();
967
+ for (const subId of subscriptionIds) {
968
+ store.unsubscribe(subId);
969
+ }
970
+
971
+ this.clientSubscriptions.delete(ws);
972
+ }
973
+
974
+ /**
975
+ * Poll all active subscriptions and push events to clients.
976
+ */
977
+ private pollSubscriptions(): void {
978
+ const store = this.framework.getStore();
979
+
980
+ for (const [ws, subscriptionIds] of this.clientSubscriptions) {
981
+ if (ws.readyState !== WebSocket.OPEN) continue;
982
+
983
+ for (const subId of subscriptionIds) {
984
+ // Poll events from this subscription (non-blocking)
985
+ let event = store.pollSubscription(subId);
986
+ let eventCount = 0;
987
+ const maxEventsPerPoll = 100; // Prevent blocking too long
988
+
989
+ while (event && eventCount < maxEventsPerPoll) {
990
+ // Parse the event data and send to client
991
+ try {
992
+ const eventData = JSON.parse(event.data);
993
+ const eventType = `store:${event.eventType}`;
994
+
995
+ this.sendEvent(ws, eventType, {
996
+ subscriptionId: subId,
997
+ ...eventData,
998
+ });
999
+ } catch (error) {
1000
+ console.error('Failed to parse subscription event:', error);
1001
+ }
1002
+
1003
+ // Check for more events
1004
+ event = store.pollSubscription(subId);
1005
+ eventCount++;
1006
+ }
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * Check if an event type matches a pattern.
1014
+ * Supports wildcards like 'inference:*' or exact matches.
1015
+ */
1016
+ function matchEventType(pattern: string, eventType: string): boolean {
1017
+ if (pattern === '*') return true;
1018
+ if (pattern.endsWith(':*')) {
1019
+ const prefix = pattern.slice(0, -1);
1020
+ return eventType.startsWith(prefix);
1021
+ }
1022
+ return pattern === eventType;
1023
+ }