@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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/src/agent.d.ts +130 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +406 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/api/mcp-server.d.ts +37 -0
- package/dist/src/api/mcp-server.d.ts.map +1 -0
- package/dist/src/api/mcp-server.js +598 -0
- package/dist/src/api/mcp-server.js.map +1 -0
- package/dist/src/api/server.d.ts +83 -0
- package/dist/src/api/server.d.ts.map +1 -0
- package/dist/src/api/server.js +785 -0
- package/dist/src/api/server.js.map +1 -0
- package/dist/src/api/types.d.ts +395 -0
- package/dist/src/api/types.d.ts.map +1 -0
- package/dist/src/api/types.js +5 -0
- package/dist/src/api/types.js.map +1 -0
- package/dist/src/framework.d.ts +309 -0
- package/dist/src/framework.d.ts.map +1 -0
- package/dist/src/framework.js +2309 -0
- package/dist/src/framework.js.map +1 -0
- package/dist/src/gate/event-gate.d.ts +75 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -0
- package/dist/src/gate/event-gate.js +503 -0
- package/dist/src/gate/event-gate.js.map +1 -0
- package/dist/src/gate/index.d.ts +3 -0
- package/dist/src/gate/index.d.ts.map +1 -0
- package/dist/src/gate/index.js +2 -0
- package/dist/src/gate/index.js.map +1 -0
- package/dist/src/gate/types.d.ts +82 -0
- package/dist/src/gate/types.d.ts.map +1 -0
- package/dist/src/gate/types.js +9 -0
- package/dist/src/gate/types.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +141 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
- package/dist/src/mcpl/channel-registry.js +647 -0
- package/dist/src/mcpl/channel-registry.js.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.js +430 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +52 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -0
- package/dist/src/mcpl/errors.js +93 -0
- package/dist/src/mcpl/errors.js.map +1 -0
- package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
- package/dist/src/mcpl/feature-set-manager.js +250 -0
- package/dist/src/mcpl/feature-set-manager.js.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.js +217 -0
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +14 -0
- package/dist/src/mcpl/index.d.ts.map +1 -0
- package/dist/src/mcpl/index.js +21 -0
- package/dist/src/mcpl/index.js.map +1 -0
- package/dist/src/mcpl/inference-router.d.ts +67 -0
- package/dist/src/mcpl/inference-router.d.ts.map +1 -0
- package/dist/src/mcpl/inference-router.js +249 -0
- package/dist/src/mcpl/inference-router.js.map +1 -0
- package/dist/src/mcpl/push-handler.d.ts +60 -0
- package/dist/src/mcpl/push-handler.d.ts.map +1 -0
- package/dist/src/mcpl/push-handler.js +162 -0
- package/dist/src/mcpl/push-handler.js.map +1 -0
- package/dist/src/mcpl/scope-manager.d.ts +76 -0
- package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
- package/dist/src/mcpl/scope-manager.js +213 -0
- package/dist/src/mcpl/scope-manager.js.map +1 -0
- package/dist/src/mcpl/server-connection.d.ts +174 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -0
- package/dist/src/mcpl/server-connection.js +559 -0
- package/dist/src/mcpl/server-connection.js.map +1 -0
- package/dist/src/mcpl/server-registry.d.ts +63 -0
- package/dist/src/mcpl/server-registry.d.ts.map +1 -0
- package/dist/src/mcpl/server-registry.js +119 -0
- package/dist/src/mcpl/server-registry.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +755 -0
- package/dist/src/mcpl/types.d.ts.map +1 -0
- package/dist/src/mcpl/types.js +48 -0
- package/dist/src/mcpl/types.js.map +1 -0
- package/dist/src/module-registry.d.ts +93 -0
- package/dist/src/module-registry.d.ts.map +1 -0
- package/dist/src/module-registry.js +386 -0
- package/dist/src/module-registry.js.map +1 -0
- package/dist/src/modules/api/index.d.ts +22 -0
- package/dist/src/modules/api/index.d.ts.map +1 -0
- package/dist/src/modules/api/index.js +63 -0
- package/dist/src/modules/api/index.js.map +1 -0
- package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
- package/dist/src/modules/discord/discord-js-client.js +273 -0
- package/dist/src/modules/discord/discord-js-client.js.map +1 -0
- package/dist/src/modules/discord/index.d.ts +82 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -0
- package/dist/src/modules/discord/index.js +959 -0
- package/dist/src/modules/discord/index.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +327 -0
- package/dist/src/modules/discord/types.d.ts.map +1 -0
- package/dist/src/modules/discord/types.js +5 -0
- package/dist/src/modules/discord/types.js.map +1 -0
- package/dist/src/modules/index.d.ts +11 -0
- package/dist/src/modules/index.d.ts.map +1 -0
- package/dist/src/modules/index.js +7 -0
- package/dist/src/modules/index.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +59 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -0
- package/dist/src/modules/workspace/index.js +831 -0
- package/dist/src/modules/workspace/index.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +48 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -0
- package/dist/src/modules/workspace/sync.js +253 -0
- package/dist/src/modules/workspace/sync.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +166 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -0
- package/dist/src/modules/workspace/types.js +5 -0
- package/dist/src/modules/workspace/types.js.map +1 -0
- package/dist/src/modules/workspace/watcher.d.ts +43 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
- package/dist/src/modules/workspace/watcher.js +120 -0
- package/dist/src/modules/workspace/watcher.js.map +1 -0
- package/dist/src/queue.d.ts +50 -0
- package/dist/src/queue.d.ts.map +1 -0
- package/dist/src/queue.js +95 -0
- package/dist/src/queue.js.map +1 -0
- package/dist/src/safe-slice.d.ts +8 -0
- package/dist/src/safe-slice.d.ts.map +1 -0
- package/dist/src/safe-slice.js +18 -0
- package/dist/src/safe-slice.js.map +1 -0
- package/dist/src/types/agent.d.ts +118 -0
- package/dist/src/types/agent.d.ts.map +1 -0
- package/dist/src/types/agent.js +2 -0
- package/dist/src/types/agent.js.map +1 -0
- package/dist/src/types/events.d.ts +220 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/events.js +2 -0
- package/dist/src/types/events.js.map +1 -0
- package/dist/src/types/framework.d.ts +184 -0
- package/dist/src/types/framework.d.ts.map +1 -0
- package/dist/src/types/framework.js +2 -0
- package/dist/src/types/framework.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/inference-log.d.ts +64 -0
- package/dist/src/types/inference-log.d.ts.map +1 -0
- package/dist/src/types/inference-log.js +5 -0
- package/dist/src/types/inference-log.js.map +1 -0
- package/dist/src/types/module.d.ts +302 -0
- package/dist/src/types/module.d.ts.map +1 -0
- package/dist/src/types/module.js +2 -0
- package/dist/src/types/module.js.map +1 -0
- package/dist/src/types/trace.d.ts +173 -0
- package/dist/src/types/trace.d.ts.map +1 -0
- package/dist/src/types/trace.js +2 -0
- package/dist/src/types/trace.js.map +1 -0
- package/dist/test/event-gate.test.d.ts +2 -0
- package/dist/test/event-gate.test.d.ts.map +1 -0
- package/dist/test/event-gate.test.js +535 -0
- package/dist/test/event-gate.test.js.map +1 -0
- package/dist/test/framework.test.d.ts +5 -0
- package/dist/test/framework.test.d.ts.map +1 -0
- package/dist/test/framework.test.js +738 -0
- package/dist/test/framework.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/agent.ts +512 -0
- package/src/api/mcp-server.ts +687 -0
- package/src/api/server.ts +1023 -0
- package/src/api/types.ts +542 -0
- package/src/framework.ts +2784 -0
- package/src/gate/event-gate.ts +623 -0
- package/src/gate/index.ts +12 -0
- package/src/gate/types.ts +112 -0
- package/src/index.ts +63 -0
- package/src/mcpl/channel-registry.ts +824 -0
- package/src/mcpl/checkpoint-manager.ts +545 -0
- package/src/mcpl/errors.ts +114 -0
- package/src/mcpl/feature-set-manager.ts +333 -0
- package/src/mcpl/hook-orchestrator.ts +281 -0
- package/src/mcpl/index.ts +134 -0
- package/src/mcpl/inference-router.ts +335 -0
- package/src/mcpl/push-handler.ts +232 -0
- package/src/mcpl/scope-manager.ts +262 -0
- package/src/mcpl/server-connection.ts +694 -0
- package/src/mcpl/server-registry.ts +144 -0
- package/src/mcpl/types.ts +991 -0
- package/src/module-registry.ts +547 -0
- package/src/modules/api/index.ts +94 -0
- package/src/modules/discord/discord-js-client.ts +353 -0
- package/src/modules/discord/index.ts +1146 -0
- package/src/modules/discord/types.ts +465 -0
- package/src/modules/index.ts +43 -0
- package/src/modules/workspace/index.ts +969 -0
- package/src/modules/workspace/sync.ts +314 -0
- package/src/modules/workspace/types.ts +202 -0
- package/src/modules/workspace/watcher.ts +142 -0
- package/src/queue.ts +106 -0
- package/src/safe-slice.ts +18 -0
- package/src/types/agent.ts +119 -0
- package/src/types/events.ts +252 -0
- package/src/types/framework.ts +211 -0
- package/src/types/index.ts +6 -0
- package/src/types/inference-log.ts +64 -0
- package/src/types/module.ts +367 -0
- 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
|
+
}
|