@cef-ai/cli 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +1273 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +66 -0
  4. package/dist/commands/deploy/clone.d.ts +13 -0
  5. package/dist/commands/deploy/clone.js +143 -0
  6. package/dist/commands/deploy/delete.d.ts +14 -0
  7. package/dist/commands/deploy/delete.js +134 -0
  8. package/dist/commands/deploy/deploy.d.ts +24 -0
  9. package/dist/commands/deploy/deploy.js +654 -0
  10. package/dist/commands/deploy/index.d.ts +2 -0
  11. package/dist/commands/deploy/index.js +216 -0
  12. package/dist/commands/deploy/status.d.ts +14 -0
  13. package/dist/commands/deploy/status.js +61 -0
  14. package/dist/commands/dev/index.d.ts +2 -0
  15. package/dist/commands/dev/index.js +134 -0
  16. package/dist/commands/dev/server.d.ts +14 -0
  17. package/dist/commands/dev/server.js +167 -0
  18. package/dist/commands/dev/ui.d.ts +1 -0
  19. package/dist/commands/dev/ui.js +984 -0
  20. package/dist/commands/playground/index.d.ts +2 -0
  21. package/dist/commands/playground/index.js +75 -0
  22. package/dist/commands/playground/test.d.ts +15 -0
  23. package/dist/commands/playground/test.js +166 -0
  24. package/dist/commands/service/index.d.ts +2 -0
  25. package/dist/commands/service/index.js +75 -0
  26. package/dist/lib/api.d.ts +146 -0
  27. package/dist/lib/api.js +330 -0
  28. package/dist/lib/config.d.ts +12 -0
  29. package/dist/lib/config.js +120 -0
  30. package/dist/lib/runtime/agent-router.d.ts +12 -0
  31. package/dist/lib/runtime/agent-router.js +78 -0
  32. package/dist/lib/runtime/context-builder.d.ts +27 -0
  33. package/dist/lib/runtime/context-builder.js +166 -0
  34. package/dist/lib/runtime/cubby-manager.d.ts +20 -0
  35. package/dist/lib/runtime/cubby-manager.js +161 -0
  36. package/dist/lib/runtime/index.d.ts +51 -0
  37. package/dist/lib/runtime/index.js +183 -0
  38. package/dist/lib/runtime/log-collector.d.ts +17 -0
  39. package/dist/lib/runtime/log-collector.js +61 -0
  40. package/dist/lib/runtime/stream-manager.d.ts +10 -0
  41. package/dist/lib/runtime/stream-manager.js +77 -0
  42. package/dist/lib/runtime/types.d.ts +76 -0
  43. package/dist/lib/runtime/types.js +2 -0
  44. package/dist/lib/typecheck.d.ts +31 -0
  45. package/dist/lib/typecheck.js +175 -0
  46. package/dist/lib/types.d.ts +246 -0
  47. package/dist/lib/types.js +2 -0
  48. package/dist/lib/utils.d.ts +4 -0
  49. package/dist/lib/utils.js +33 -0
  50. package/example/.env.example +6 -0
  51. package/example/agents/object-detection/tasks/yolo.ts +28 -0
  52. package/example/agents/parking-violation-detector/tasks/detect.ts +326 -0
  53. package/example/cef.config.yaml +91 -0
  54. package/example/engagements/engagement.ts +902 -0
  55. package/package.json +69 -0
@@ -0,0 +1,330 @@
1
+ import crypto from 'crypto';
2
+ // ROB error message fragments (for consistent handling and easier maintenance)
3
+ const ROB_HTTP_FORBIDDEN = '403';
4
+ const ROB_MSG_FORBIDDEN = 'Forbidden';
5
+ const ROB_HTTP_CONFLICT = '409';
6
+ const ROB_MSG_ALREADY_EXISTS = 'already exists';
7
+ async function fetchJson(url, init) {
8
+ let res;
9
+ try {
10
+ res = await fetch(url, {
11
+ ...init,
12
+ signal: AbortSignal.timeout(30_000),
13
+ });
14
+ }
15
+ catch (err) {
16
+ const cause = err?.cause?.message ?? err?.cause ?? err?.message ?? String(err);
17
+ throw new Error(`Network error calling ${url}: ${cause}`);
18
+ }
19
+ if (!res.ok) {
20
+ const body = await res.text();
21
+ throw new Error(`HTTP ${res.status} ${res.statusText} (${url}): ${body}`);
22
+ }
23
+ const text = await res.text();
24
+ if (!text)
25
+ return {};
26
+ try {
27
+ return JSON.parse(text);
28
+ }
29
+ catch (err) {
30
+ const msg = err instanceof Error ? err.message : String(err);
31
+ throw new Error(`Invalid JSON from ${url}: ${msg}`);
32
+ }
33
+ }
34
+ async function request(url, init) {
35
+ return fetchJson(url, init);
36
+ }
37
+ async function requestAs(url, init) {
38
+ return fetchJson(url, init);
39
+ }
40
+ export function createApiClient(env, asPubKey, agentServiceId) {
41
+ const base = `${env.orchestratorUrl}/api/v1/agent-services/${asPubKey}`;
42
+ const headers = {
43
+ 'Content-Type': 'application/json',
44
+ Authorization: `Bearer ${env.authToken}`,
45
+ };
46
+ function post(url, body) {
47
+ return request(url, { method: 'POST', headers, body: JSON.stringify(body) });
48
+ }
49
+ function put(url, body) {
50
+ return request(url, { method: 'PUT', headers, body: JSON.stringify(body) });
51
+ }
52
+ function get(url) {
53
+ return requestAs(url, { method: 'GET', headers });
54
+ }
55
+ function del(url) {
56
+ return request(url, { method: 'DELETE', headers });
57
+ }
58
+ let cachedServiceId = undefined;
59
+ const fallbackServiceId = agentServiceId;
60
+ function getRobHeaders() {
61
+ let robOrigin = env.robOrigin;
62
+ if (!robOrigin) {
63
+ const match = env.robApiUrl.match(/api\.rob\.(\w+)\.cere\.io/);
64
+ if (match)
65
+ robOrigin = `https://rob.${match[1]}.cere.io`;
66
+ }
67
+ return { ...headers, ...(robOrigin && { Origin: robOrigin, Referer: `${robOrigin}/` }) };
68
+ }
69
+ async function requireServiceId() {
70
+ if (cachedServiceId)
71
+ return cachedServiceId;
72
+ if (!env.robApiUrl)
73
+ throw new Error('CEF_ROB_API_URL is required for this operation');
74
+ try {
75
+ const services = await listAgentServices(env.robApiUrl, env.authToken);
76
+ const match = services.find(s => s.publicKey === asPubKey);
77
+ if (!match) {
78
+ throw new Error(`No agent service found with publicKey ${asPubKey}. ` +
79
+ `Run \`npx tsx src/cli.ts services\` to see available services.`);
80
+ }
81
+ cachedServiceId = match.id;
82
+ return cachedServiceId;
83
+ }
84
+ catch (err) {
85
+ if (fallbackServiceId) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ console.warn(`[ROB] Could not list data services (${msg}). Using agentServiceId from config: ${fallbackServiceId}`);
88
+ cachedServiceId = fallbackServiceId;
89
+ return fallbackServiceId;
90
+ }
91
+ throw err;
92
+ }
93
+ }
94
+ return {
95
+ getResolvedAgentServiceId() {
96
+ return cachedServiceId;
97
+ },
98
+ // ─── Engagement ───────────────────────────────────────────────────────
99
+ createEngagement(payload) {
100
+ return post(`${base}/engagements`, payload);
101
+ },
102
+ updateEngagement(engagementId, payload) {
103
+ return put(`${base}/engagements/${engagementId}`, payload);
104
+ },
105
+ // ─── Agent ───────────────────────────────────────────────────────────
106
+ createAgent(payload) {
107
+ return post(`${base}/agents`, payload);
108
+ },
109
+ updateAgent(agentId, payload) {
110
+ return put(`${base}/agents/${agentId}`, payload);
111
+ },
112
+ // ─── Workspace ────────────────────────────────────────────────────────
113
+ async createWorkspaceOnRob(name) {
114
+ const svcId = await requireServiceId();
115
+ const url = `${env.robApiUrl.replace(/\/$/, '')}/data-services/${svcId}/organizations`;
116
+ const res = await request(url, {
117
+ method: 'POST',
118
+ headers: getRobHeaders(),
119
+ body: JSON.stringify({ name }),
120
+ });
121
+ const data = (res.data ?? res);
122
+ const id = String(data.id ?? '');
123
+ if (!id)
124
+ throw new Error(`ROB workspace created but no ID in response: ${JSON.stringify(res)}`);
125
+ return id;
126
+ },
127
+ createWorkspace(payload) {
128
+ return post(`${base}/workspaces`, payload);
129
+ },
130
+ updateWorkspace(workspaceId, payload) {
131
+ return put(`${base}/workspaces/${workspaceId}`, payload);
132
+ },
133
+ getWorkspaces() {
134
+ return get(`${base}/workspaces`);
135
+ },
136
+ // ─── Stream ───────────────────────────────────────────────────────────
137
+ createStream(workspaceId, payload) {
138
+ return post(`${base}/workspaces/${workspaceId}/streams`, payload);
139
+ },
140
+ updateStream(streamId, payload) {
141
+ return put(`${base}/streams/${streamId}`, payload);
142
+ },
143
+ // ─── Deployment ───────────────────────────────────────────────────────
144
+ createDeployment(streamId, payload) {
145
+ return post(`${base}/streams/${streamId}/deployments`, payload);
146
+ },
147
+ updateDeployment(deploymentId, payload) {
148
+ return put(`${base}/deployments/${deploymentId}`, payload);
149
+ },
150
+ // ─── Cubby ────────────────────────────────────────────────────────────
151
+ async createCubby(payload) {
152
+ return post(`${base}/cubbies`, payload);
153
+ },
154
+ async updateCubby(cubbyId, payload) {
155
+ return put(`${base}/cubbies/${cubbyId}`, payload);
156
+ },
157
+ // ─── Status (read) ────────────────────────────────────────────────────
158
+ getAgents() {
159
+ return get(`${base}/agents`);
160
+ },
161
+ getEngagements() {
162
+ return get(`${base}/engagements`);
163
+ },
164
+ // ─── Ontology (full workspace/stream/deployment tree) ──────────────────
165
+ getOntology() {
166
+ return requestAs(`${base}/ontology`, { method: 'GET', headers });
167
+ },
168
+ // ─── Cubbies (read) ─────────────────────────────────────────────────
169
+ async getCubbies() {
170
+ const res = await get(`${base}/cubbies`);
171
+ return res.data;
172
+ },
173
+ async getCubbyInstances(cubbyId) {
174
+ const res = await requestAs(`${base}/cubbies/${cubbyId}/instances`, { method: 'GET', headers });
175
+ return Array.isArray(res) ? res : (res.data ?? []);
176
+ },
177
+ async getCubbyStats(cubbyId) {
178
+ return requestAs(`${base}/cubbies/${cubbyId}/stats`, { method: 'GET', headers });
179
+ },
180
+ // ─── Delete ─────────────────────────────────────────────────────────
181
+ deleteEngagement(engagementId) {
182
+ return del(`${base}/engagements/${engagementId}`);
183
+ },
184
+ deleteAgent(agentId) {
185
+ return del(`${base}/agents/${agentId}`);
186
+ },
187
+ deleteWorkspace(workspaceId) {
188
+ return del(`${base}/workspaces/${workspaceId}`);
189
+ },
190
+ deleteStream(streamId) {
191
+ return del(`${base}/streams/${streamId}`);
192
+ },
193
+ deleteDeployment(deploymentId) {
194
+ return del(`${base}/deployments/${deploymentId}`);
195
+ },
196
+ // ─── Raft ─────────────────────────────────────────────────────────────
197
+ createRaft(streamId, payload) {
198
+ return post(`${base}/streams/${streamId}/rafts`, payload);
199
+ },
200
+ updateRaft(raftId, payload) {
201
+ return put(`${base}/rafts/${raftId}`, payload);
202
+ },
203
+ deleteRaft(raftId) {
204
+ return del(`${base}/rafts/${raftId}`);
205
+ },
206
+ // Shared ROB (RMS) sync: register or update raft. Best-effort: warns on failure.
207
+ async syncRaftToRob(payload, method) {
208
+ if (!env.robApiUrl)
209
+ return;
210
+ const action = method === 'POST' ? 'registration' : 'update';
211
+ try {
212
+ const svcId = await requireServiceId();
213
+ const base = `${env.robApiUrl.replace(/\/$/, '')}/rms-node-backend/rafts`;
214
+ const url = method === 'POST' ? base : `${base}/${payload.raftId}`;
215
+ await request(url, {
216
+ method,
217
+ headers: getRobHeaders(),
218
+ body: JSON.stringify({
219
+ id: payload.raftId,
220
+ name: payload.name,
221
+ description: payload.description ?? '',
222
+ dataServiceId: svcId,
223
+ dataSourcesIds: payload.dataSourcesIds ?? [],
224
+ indexingScript: {
225
+ id: payload.streamId,
226
+ name: payload.name,
227
+ tsCode: payload.tsCode,
228
+ },
229
+ queryOperations: payload.queryOperations ?? [],
230
+ }),
231
+ });
232
+ }
233
+ catch (robErr) {
234
+ const msg = robErr instanceof Error ? robErr.message : String(robErr);
235
+ if (msg.includes(ROB_HTTP_FORBIDDEN) || msg.includes(ROB_MSG_FORBIDDEN)) {
236
+ console.warn(`[raft] ROB ${action} failed for "${payload.name}" (403 Forbidden).` +
237
+ (method === 'POST' ? ' Use the same JWT as the ROB UI (CEF_AUTH_TOKEN).' : ''));
238
+ }
239
+ else if (method === 'POST' &&
240
+ (msg.includes(ROB_HTTP_CONFLICT) || msg.includes(ROB_MSG_ALREADY_EXISTS))) {
241
+ // Already registered, ignore
242
+ }
243
+ else {
244
+ console.warn(`[raft] ROB ${action} failed for "${payload.name}": ${msg}`);
245
+ }
246
+ }
247
+ },
248
+ // Registers a raft with ROB (RMS) so it appears in the ROB UI. Best-effort: warns on failure.
249
+ registerRaftOnRob(payload) {
250
+ return this.syncRaftToRob(payload, 'POST');
251
+ },
252
+ // Updates a raft on ROB (RMS). Best-effort: warns on failure.
253
+ updateRaftOnRob(payload) {
254
+ return this.syncRaftToRob(payload, 'PUT');
255
+ },
256
+ // ─── MCP / Playground ─────────────────────────────────────────────────
257
+ callTool(agentId, toolName, args) {
258
+ const mcpUrl = `${env.orchestratorUrl}/api/v1/mcp/agent-services/${asPubKey}/agents/${agentId}`;
259
+ const body = {
260
+ jsonrpc: '2.0',
261
+ id: Date.now(),
262
+ method: 'tools/call',
263
+ params: { name: toolName, arguments: args },
264
+ context: {
265
+ jobId: `playground-${crypto.randomUUID()}`,
266
+ workspaceId: 'playground',
267
+ engagementId: '',
268
+ deploymentId: '',
269
+ executionId: crypto.randomUUID(),
270
+ },
271
+ };
272
+ return requestAs(mcpUrl, {
273
+ method: 'POST',
274
+ headers,
275
+ body: JSON.stringify(body),
276
+ });
277
+ },
278
+ listTools(agentId) {
279
+ const mcpUrl = `${env.orchestratorUrl}/api/v1/mcp/agent-services/${asPubKey}/agents/${agentId}`;
280
+ return requestAs(mcpUrl, {
281
+ method: 'POST',
282
+ headers,
283
+ body: JSON.stringify({
284
+ jsonrpc: '2.0',
285
+ id: Date.now(),
286
+ method: 'tools/list',
287
+ params: {},
288
+ }),
289
+ });
290
+ },
291
+ };
292
+ }
293
+ export async function listAgentServices(robApiUrl, authToken) {
294
+ const res = await request(robApiUrl + '/data-services', {
295
+ method: 'GET',
296
+ headers: {
297
+ 'Content-Type': 'application/json',
298
+ Authorization: `Bearer ${authToken}`,
299
+ },
300
+ });
301
+ return (res.data ?? []);
302
+ }
303
+ export async function createAgentServiceOnRob(robApiUrl, authToken, name) {
304
+ const res = await request(robApiUrl + '/data-services', {
305
+ method: 'POST',
306
+ headers: {
307
+ 'Content-Type': 'application/json',
308
+ Authorization: `Bearer ${authToken}`,
309
+ },
310
+ body: JSON.stringify({ name }),
311
+ });
312
+ const data = (res.data ?? res);
313
+ return {
314
+ id: String(data.id ?? ''),
315
+ name: String(data.name ?? ''),
316
+ publicKey: String(data.publicKey ?? ''),
317
+ archived: Boolean(data.archived),
318
+ createdAt: String(data.createdAt ?? ''),
319
+ };
320
+ }
321
+ export async function createAgentServiceOnOrchestrator(orchestratorUrl, authToken, agentServicePubKey, bucketId, name) {
322
+ return request(`${orchestratorUrl}/api/v1/agent-services`, {
323
+ method: 'POST',
324
+ headers: {
325
+ 'Content-Type': 'application/json',
326
+ Authorization: `Bearer ${authToken}`,
327
+ },
328
+ body: JSON.stringify({ agentServicePubKey, bucketId, name }),
329
+ });
330
+ }
@@ -0,0 +1,12 @@
1
+ import type { CefConfig, EnvConfig, ResolvedConfig } from './types.js';
2
+ export declare function validateConfig(config: CefConfig): void;
3
+ export declare function loadEnv(): EnvConfig;
4
+ export declare function resolveFilePaths(config: CefConfig, baseDir: string): void;
5
+ export interface DevConfig {
6
+ config: CefConfig;
7
+ configPath: string;
8
+ configDir: string;
9
+ }
10
+ /** Load config for local dev mode. Skips env validation (no auth token required). */
11
+ export declare function loadConfigForDev(configPath: string): DevConfig;
12
+ export declare function loadConfig(configPath: string): ResolvedConfig;
@@ -0,0 +1,120 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { parse } from 'yaml';
4
+ import dotenv from 'dotenv';
5
+ import { toCamelCase } from './utils.js';
6
+ export function validateConfig(config) {
7
+ if (!config || typeof config !== 'object') {
8
+ throw new Error('Config file must contain a YAML object');
9
+ }
10
+ if (!config.agentServicePubKey || typeof config.agentServicePubKey !== 'string') {
11
+ throw new Error('Config missing required field: agentServicePubKey (string)');
12
+ }
13
+ if (!/^0x[0-9a-fA-F]{64}$/.test(config.agentServicePubKey)) {
14
+ throw new Error('Config field agentServicePubKey must be a 64-character hex string prefixed with 0x');
15
+ }
16
+ for (const section of ['engagements', 'agents', 'cubbies', 'workspaces']) {
17
+ const value = config[section];
18
+ if (value !== undefined && !Array.isArray(value)) {
19
+ throw new Error(`Config field "${section}" must be an array`);
20
+ }
21
+ }
22
+ for (const agent of config.agents ?? []) {
23
+ if (!Array.isArray(agent.tasks)) {
24
+ throw new Error(`Agent "${agent.name ?? '?'}" is missing required "tasks" array`);
25
+ }
26
+ }
27
+ for (const cubby of config.cubbies ?? []) {
28
+ if (!cubby.alias || typeof cubby.alias !== 'string') {
29
+ throw new Error(`Cubby "${cubby.name ?? '?'}" is missing required "alias" field`);
30
+ }
31
+ }
32
+ }
33
+ export function loadEnv() {
34
+ let authToken = (process.env['CEF_AUTH_TOKEN'] ?? '').trim();
35
+ if (!authToken)
36
+ throw new Error('Missing required env var: CEF_AUTH_TOKEN');
37
+ if (authToken.toLowerCase().startsWith('bearer ')) {
38
+ authToken = authToken.slice(7).trim();
39
+ }
40
+ const orchestratorUrl = process.env['CEF_ORCHESTRATOR_URL'];
41
+ if (!orchestratorUrl)
42
+ throw new Error('Missing required env var: CEF_ORCHESTRATOR_URL');
43
+ const robApiUrl = (process.env['CEF_ROB_API_URL'] ?? '').trim();
44
+ const robOrigin = (process.env['CEF_ROB_ORIGIN'] ?? '').trim() || undefined;
45
+ return { authToken, orchestratorUrl, robApiUrl, robOrigin };
46
+ }
47
+ function resolveAndValidatePath(baseDir, file, label) {
48
+ const filePath = path.resolve(baseDir, file);
49
+ const resolvedBase = path.resolve(baseDir);
50
+ if (!filePath.startsWith(resolvedBase + path.sep) && filePath !== resolvedBase) {
51
+ throw new Error(`${label} file path escapes config directory: ${file}`);
52
+ }
53
+ if (!fs.existsSync(filePath)) {
54
+ throw new Error(`${label} file not found: ${filePath}`);
55
+ }
56
+ return filePath;
57
+ }
58
+ export function resolveFilePaths(config, baseDir) {
59
+ for (const engagement of config.engagements ?? []) {
60
+ const filePath = resolveAndValidatePath(baseDir, engagement.file, 'Engagement');
61
+ engagement.tsCode = fs.readFileSync(filePath, 'utf8');
62
+ engagement._resolvedFilePath = filePath;
63
+ }
64
+ for (const agent of config.agents ?? []) {
65
+ if (!agent.alias)
66
+ agent.alias = toCamelCase(agent.name);
67
+ for (const task of agent.tasks) {
68
+ if (!task.alias)
69
+ task.alias = toCamelCase(task.name);
70
+ const filePath = resolveAndValidatePath(baseDir, task.file, 'Agent task');
71
+ task.tsCode = fs.readFileSync(filePath, 'utf8');
72
+ task._resolvedFilePath = filePath;
73
+ if (task.batch_file) {
74
+ const batchPath = resolveAndValidatePath(baseDir, task.batch_file, 'Agent task batch');
75
+ task.batchSql = fs.readFileSync(batchPath, 'utf8');
76
+ }
77
+ }
78
+ }
79
+ // Cubbies have no file references to resolve (migrations are inline)
80
+ for (const workspace of config.workspaces ?? []) {
81
+ for (const stream of workspace.streams ?? []) {
82
+ for (const raft of stream.rafts ?? []) {
83
+ if (raft.file) {
84
+ const filePath = resolveAndValidatePath(baseDir, raft.file, 'Raft');
85
+ raft.tsCode = fs.readFileSync(filePath, 'utf8');
86
+ raft._resolvedFilePath = filePath;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ /** Load config for local dev mode. Skips env validation (no auth token required). */
93
+ export function loadConfigForDev(configPath) {
94
+ const absolutePath = path.resolve(configPath);
95
+ if (!fs.existsSync(absolutePath)) {
96
+ throw new Error(`Config file not found: ${absolutePath}`);
97
+ }
98
+ const configDir = path.dirname(absolutePath);
99
+ // Load .env for optional vars but don't fail on missing required vars
100
+ dotenv.config({ path: path.join(configDir, '.env') });
101
+ const raw = fs.readFileSync(absolutePath, 'utf8');
102
+ const config = parse(raw);
103
+ validateConfig(config);
104
+ resolveFilePaths(config, configDir);
105
+ return { config, configPath: absolutePath, configDir };
106
+ }
107
+ export function loadConfig(configPath) {
108
+ const absolutePath = path.resolve(configPath);
109
+ if (!fs.existsSync(absolutePath)) {
110
+ throw new Error(`Config file not found: ${absolutePath}`);
111
+ }
112
+ const configDir = path.dirname(absolutePath);
113
+ dotenv.config({ path: path.join(configDir, '.env') });
114
+ const raw = fs.readFileSync(absolutePath, 'utf8');
115
+ const config = parse(raw);
116
+ validateConfig(config);
117
+ const env = loadEnv();
118
+ resolveFilePaths(config, configDir);
119
+ return { config, env, configPath: absolutePath };
120
+ }
@@ -0,0 +1,12 @@
1
+ import type { AgentConfig } from '../types.js';
2
+ export declare class AgentRouter {
3
+ private tasks;
4
+ private agentTasks;
5
+ loadAgents(agents: AgentConfig[]): void;
6
+ call(agentAlias: string, taskAlias: string, payload: unknown, context: unknown): Promise<unknown>;
7
+ reloadTask(agentAlias: string, taskAlias: string, tsCode: string): void;
8
+ getLoadedAgents(): string[];
9
+ getTasksForAgent(agentAlias: string): string[];
10
+ private compileAndStore;
11
+ private transpile;
12
+ }
@@ -0,0 +1,78 @@
1
+ import vm from 'vm';
2
+ import ts from 'typescript';
3
+ const TS_COMPILE_OPTIONS = {
4
+ target: ts.ScriptTarget.ES2022,
5
+ module: ts.ModuleKind.ES2022,
6
+ strict: false,
7
+ noImplicitAny: false,
8
+ removeComments: false,
9
+ esModuleInterop: true,
10
+ };
11
+ export class AgentRouter {
12
+ tasks = new Map(); // key: agentAlias/taskAlias
13
+ agentTasks = new Map(); // agentAlias -> [taskAlias, ...]
14
+ loadAgents(agents) {
15
+ for (const agent of agents) {
16
+ const taskAliases = [];
17
+ for (const task of agent.tasks) {
18
+ const code = task.tsCode ?? '';
19
+ this.compileAndStore(agent.alias, task.alias, code);
20
+ taskAliases.push(task.alias);
21
+ }
22
+ this.agentTasks.set(agent.alias, taskAliases);
23
+ }
24
+ }
25
+ async call(agentAlias, taskAlias, payload, context) {
26
+ const key = `${agentAlias}/${taskAlias}`;
27
+ const compiled = this.tasks.get(key);
28
+ if (!compiled) {
29
+ if (!this.agentTasks.has(agentAlias)) {
30
+ throw new Error(`Agent "${agentAlias}" not found`);
31
+ }
32
+ throw new Error(`Task "${taskAlias}" not found on agent "${agentAlias}"`);
33
+ }
34
+ const event = { payload, event_type: 'agent_call' };
35
+ // Build restricted sandbox
36
+ const sandbox = {
37
+ event,
38
+ context,
39
+ console: {
40
+ log: context?.log ?? (() => { }),
41
+ warn: context?.warn ?? (() => { }),
42
+ error: context?.error ?? (() => { }),
43
+ },
44
+ Promise,
45
+ // Math, JSON, etc. are available in vm by default
46
+ };
47
+ // Wrap code to capture the handle function and call it
48
+ const wrappedCode = `
49
+ ${compiled.compiledJs}
50
+ __result__ = handle(event, context);
51
+ `;
52
+ const vmContext = vm.createContext(sandbox);
53
+ vm.runInContext(wrappedCode, vmContext);
54
+ // handle returns a Promise
55
+ const result = await sandbox.__result__;
56
+ return result;
57
+ }
58
+ reloadTask(agentAlias, taskAlias, tsCode) {
59
+ this.compileAndStore(agentAlias, taskAlias, tsCode);
60
+ }
61
+ getLoadedAgents() {
62
+ return [...this.agentTasks.keys()];
63
+ }
64
+ getTasksForAgent(agentAlias) {
65
+ return this.agentTasks.get(agentAlias) ?? [];
66
+ }
67
+ compileAndStore(agentAlias, taskAlias, tsCode) {
68
+ const key = `${agentAlias}/${taskAlias}`;
69
+ const compiledJs = this.transpile(tsCode);
70
+ this.tasks.set(key, { agentAlias, taskAlias, compiledJs });
71
+ }
72
+ transpile(tsCode) {
73
+ const result = ts.transpileModule(tsCode, {
74
+ compilerOptions: TS_COMPILE_OPTIONS,
75
+ });
76
+ return result.outputText;
77
+ }
78
+ }
@@ -0,0 +1,27 @@
1
+ import type { LogCollector } from './log-collector.js';
2
+ import type { AgentRouter } from './agent-router.js';
3
+ import type { CubbyManager } from './cubby-manager.js';
4
+ import type { StreamManager } from './stream-manager.js';
5
+ export interface BuildContextOptions {
6
+ executionId: string;
7
+ logCollector: LogCollector;
8
+ agentRouter: AgentRouter;
9
+ cubbyManager: CubbyManager;
10
+ streamManager: StreamManager;
11
+ workspaceId: string;
12
+ agentServiceId: string;
13
+ agentAlias?: string;
14
+ taskAlias?: string;
15
+ callerLabel?: string;
16
+ }
17
+ /**
18
+ * Builds the context object injected into agent/engagement V8 sandboxes.
19
+ *
20
+ * This is THE single source of truth for the Context API shape.
21
+ * When the platform API changes, update this file.
22
+ *
23
+ * Properties are grouped into:
24
+ * 1. Typed properties (from typecheck.ts Context interface)
25
+ * 2. Non-typed extensions (in platform docs but not in typecheck.ts)
26
+ */
27
+ export declare function buildContext(opts: BuildContextOptions): any;