@nebulaos/client 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/README.md +4 -0
- package/dist/index.d.mts +629 -0
- package/dist/index.d.ts +629 -0
- package/dist/index.js +1819 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1765 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1765 @@
|
|
|
1
|
+
// src/config/client-config.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var serverConfigSchema = z.object({
|
|
4
|
+
url: z.string().min(1),
|
|
5
|
+
apiKey: z.string().min(1),
|
|
6
|
+
batchSize: z.number().int().positive().optional(),
|
|
7
|
+
flushInterval: z.number().int().positive().optional()
|
|
8
|
+
});
|
|
9
|
+
var otelTracingConfigSchema = z.object({
|
|
10
|
+
/** OTLP HTTP endpoint. Defaults to http://localhost:4318/v1/traces */
|
|
11
|
+
otlpEndpoint: z.string().optional()
|
|
12
|
+
});
|
|
13
|
+
var clientConfigSchema = z.object({
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated clientId is now automatically extracted from the API key.
|
|
16
|
+
* This field is optional and only used for local identification/logging.
|
|
17
|
+
*/
|
|
18
|
+
clientId: z.string().min(1).optional(),
|
|
19
|
+
/**
|
|
20
|
+
* List of agents to register. Can be either:
|
|
21
|
+
* - Agent instances (backwards compatible, but may cause memory leakage between executions)
|
|
22
|
+
* - Factory functions that return agent instances (recommended for proper isolation)
|
|
23
|
+
*/
|
|
24
|
+
agents: z.array(z.custom()).optional(),
|
|
25
|
+
tools: z.array(z.custom()).optional(),
|
|
26
|
+
workflows: z.array(z.custom()).optional(),
|
|
27
|
+
server: serverConfigSchema.optional(),
|
|
28
|
+
/**
|
|
29
|
+
* OpenTelemetry tracing configuration.
|
|
30
|
+
* When set, the client will set up an OTel TracerProvider and export
|
|
31
|
+
* spans directly to the configured OTLP endpoint.
|
|
32
|
+
*/
|
|
33
|
+
tracing: otelTracingConfigSchema.optional(),
|
|
34
|
+
/**
|
|
35
|
+
* Client-side log level (same semantics as @nebulaos/core ConsoleLogger).
|
|
36
|
+
* When set to anything other than "none", the client will emit debug logs for
|
|
37
|
+
* WS/HTTP communication (without full payloads).
|
|
38
|
+
*/
|
|
39
|
+
logLevel: z.custom().optional(),
|
|
40
|
+
// Backwards compatibility (deprecated): prefer `logLevel`.
|
|
41
|
+
debug: z.object({
|
|
42
|
+
enabled: z.boolean().optional(),
|
|
43
|
+
level: z.custom().optional()
|
|
44
|
+
}).optional()
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// src/errors/client-errors.ts
|
|
48
|
+
var ClientError = class extends Error {
|
|
49
|
+
constructor(message, code, statusCode = 500, details) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.code = code;
|
|
52
|
+
this.statusCode = statusCode;
|
|
53
|
+
this.details = details;
|
|
54
|
+
this.name = "ClientError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var RegistryError = class extends ClientError {
|
|
58
|
+
constructor(message, details) {
|
|
59
|
+
super(message, "REGISTRY_ERROR", 400, details);
|
|
60
|
+
this.name = "RegistryError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var NotFoundError = class extends ClientError {
|
|
64
|
+
constructor(entity, id) {
|
|
65
|
+
super(`${entity} '${id}' not found`, "NOT_FOUND", 404);
|
|
66
|
+
this.name = "NotFoundError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var AuthenticationError = class extends ClientError {
|
|
70
|
+
constructor(message = "Authentication failed: Invalid API key or missing credentials", details) {
|
|
71
|
+
super(message, "AUTHENTICATION_ERROR", 401, details);
|
|
72
|
+
this.name = "AuthenticationError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var ConnectionError = class extends ClientError {
|
|
76
|
+
constructor(message, details) {
|
|
77
|
+
super(message, "CONNECTION_ERROR", 0, details);
|
|
78
|
+
this.name = "ConnectionError";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/registry/registry.ts
|
|
83
|
+
var Registry = class {
|
|
84
|
+
agentsById = /* @__PURE__ */ new Map();
|
|
85
|
+
agentsByName = /* @__PURE__ */ new Map();
|
|
86
|
+
workflows = /* @__PURE__ */ new Map();
|
|
87
|
+
toolsById = /* @__PURE__ */ new Map();
|
|
88
|
+
httpClient;
|
|
89
|
+
/**
|
|
90
|
+
* Sets the HTTP client to be injected into agents for tool execution.
|
|
91
|
+
*/
|
|
92
|
+
setHttpClient(client) {
|
|
93
|
+
this.httpClient = client;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Registers an agent factory or instance.
|
|
97
|
+
*
|
|
98
|
+
* If an instance is passed directly (backwards compatibility), it will be wrapped
|
|
99
|
+
* in a factory that always returns the same instance. For proper isolation between
|
|
100
|
+
* executions, prefer passing a factory function.
|
|
101
|
+
*
|
|
102
|
+
* @param agentOrFactory - Agent instance or factory function
|
|
103
|
+
*/
|
|
104
|
+
registerAgent(agentOrFactory) {
|
|
105
|
+
let factory;
|
|
106
|
+
let sampleAgent;
|
|
107
|
+
if (typeof agentOrFactory === "function") {
|
|
108
|
+
factory = agentOrFactory;
|
|
109
|
+
sampleAgent = factory();
|
|
110
|
+
} else {
|
|
111
|
+
const instance = agentOrFactory;
|
|
112
|
+
factory = () => instance;
|
|
113
|
+
sampleAgent = instance;
|
|
114
|
+
}
|
|
115
|
+
const id = "id" in sampleAgent && typeof sampleAgent.id === "string" && sampleAgent.id ? sampleAgent.id : sampleAgent.name;
|
|
116
|
+
if (this.agentsById.has(id)) {
|
|
117
|
+
throw new RegistryError(`Agent '${id}' already registered`);
|
|
118
|
+
}
|
|
119
|
+
if (this.agentsByName.has(sampleAgent.name)) {
|
|
120
|
+
throw new RegistryError(`Agent name '${sampleAgent.name}' already registered`);
|
|
121
|
+
}
|
|
122
|
+
const entry = {
|
|
123
|
+
factory,
|
|
124
|
+
id,
|
|
125
|
+
name: sampleAgent.name
|
|
126
|
+
};
|
|
127
|
+
this.agentsById.set(id, entry);
|
|
128
|
+
this.agentsByName.set(sampleAgent.name, entry);
|
|
129
|
+
}
|
|
130
|
+
registerWorkflow(workflow) {
|
|
131
|
+
const workflowId = workflow.id;
|
|
132
|
+
if (!workflowId) {
|
|
133
|
+
throw new RegistryError("Workflow id is required to register workflow");
|
|
134
|
+
}
|
|
135
|
+
if (this.workflows.has(workflowId)) {
|
|
136
|
+
throw new RegistryError(`Workflow '${workflowId}' already registered`);
|
|
137
|
+
}
|
|
138
|
+
this.workflows.set(workflowId, workflow);
|
|
139
|
+
}
|
|
140
|
+
registerTool(tool) {
|
|
141
|
+
const id = tool?.id;
|
|
142
|
+
if (!id || typeof id !== "string") {
|
|
143
|
+
throw new RegistryError("Tool id is required to register tool");
|
|
144
|
+
}
|
|
145
|
+
if (this.toolsById.has(id)) {
|
|
146
|
+
throw new RegistryError(`Tool '${id}' already registered`);
|
|
147
|
+
}
|
|
148
|
+
this.toolsById.set(id, tool);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Gets a new agent instance by id or name.
|
|
152
|
+
*
|
|
153
|
+
* Each call creates a fresh instance via the registered factory,
|
|
154
|
+
* ensuring no memory/context leakage between executions.
|
|
155
|
+
*
|
|
156
|
+
* @param idOrName - Agent id or name
|
|
157
|
+
* @returns A new agent instance
|
|
158
|
+
*/
|
|
159
|
+
getAgent(idOrName) {
|
|
160
|
+
const entry = this.agentsById.get(idOrName) ?? this.agentsByName.get(idOrName);
|
|
161
|
+
if (!entry) {
|
|
162
|
+
throw new NotFoundError("agent", idOrName);
|
|
163
|
+
}
|
|
164
|
+
const agent = entry.factory();
|
|
165
|
+
if (this.httpClient && typeof agent.setHttpClient === "function") {
|
|
166
|
+
agent.setHttpClient(this.httpClient);
|
|
167
|
+
}
|
|
168
|
+
return agent;
|
|
169
|
+
}
|
|
170
|
+
getWorkflow(id) {
|
|
171
|
+
const workflow = this.workflows.get(id);
|
|
172
|
+
if (!workflow) {
|
|
173
|
+
throw new NotFoundError("workflow", id);
|
|
174
|
+
}
|
|
175
|
+
return workflow;
|
|
176
|
+
}
|
|
177
|
+
getTool(id) {
|
|
178
|
+
const tool = this.toolsById.get(id);
|
|
179
|
+
if (!tool) {
|
|
180
|
+
throw new NotFoundError("tool", id);
|
|
181
|
+
}
|
|
182
|
+
return tool;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Lists all registered agents by creating a sample instance of each.
|
|
186
|
+
*
|
|
187
|
+
* Used for Cloud registration where we need agent metadata (name, id, tools, etc).
|
|
188
|
+
* Creates one instance per agent to extract metadata.
|
|
189
|
+
*
|
|
190
|
+
* @returns Array of agent instances (one per registered agent)
|
|
191
|
+
*/
|
|
192
|
+
listAgents() {
|
|
193
|
+
return Array.from(this.agentsById.values()).map((entry) => entry.factory());
|
|
194
|
+
}
|
|
195
|
+
listTools() {
|
|
196
|
+
return Array.from(this.toolsById.values());
|
|
197
|
+
}
|
|
198
|
+
listWorkflows() {
|
|
199
|
+
return Array.from(this.workflows.values());
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/connection/server-connection.ts
|
|
204
|
+
import { io } from "socket.io-client";
|
|
205
|
+
import { v4 as uuid } from "uuid";
|
|
206
|
+
import { ExecutionContext, Tracing } from "@nebulaos/core";
|
|
207
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
208
|
+
|
|
209
|
+
// src/logger/time.ts
|
|
210
|
+
function nowMs() {
|
|
211
|
+
const anyGlobal = globalThis;
|
|
212
|
+
if (anyGlobal?.performance?.now) return anyGlobal.performance.now();
|
|
213
|
+
return Date.now();
|
|
214
|
+
}
|
|
215
|
+
function durationMs(startMs) {
|
|
216
|
+
return Math.max(0, nowMs() - startMs);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/logger/summaries.ts
|
|
220
|
+
function summarizeValue(value) {
|
|
221
|
+
if (value === null) return "null";
|
|
222
|
+
if (value === void 0) return "undefined";
|
|
223
|
+
if (typeof value === "string") return `string(len=${value.length})`;
|
|
224
|
+
if (typeof value === "number") return "number";
|
|
225
|
+
if (typeof value === "boolean") return "boolean";
|
|
226
|
+
if (Array.isArray(value)) return `array(len=${value.length})`;
|
|
227
|
+
if (typeof value === "object") return `object(keys=${Object.keys(value).length})`;
|
|
228
|
+
return typeof value;
|
|
229
|
+
}
|
|
230
|
+
function summarizeDomainEvent(event) {
|
|
231
|
+
return {
|
|
232
|
+
type: event.type,
|
|
233
|
+
executionId: event.executionId,
|
|
234
|
+
rootExecutionId: event.rootExecutionId,
|
|
235
|
+
parentExecutionId: event.parentExecutionId,
|
|
236
|
+
correlationId: event.correlationId,
|
|
237
|
+
traceId: event.trace?.traceId,
|
|
238
|
+
spanId: event.trace?.spanId,
|
|
239
|
+
parentSpanId: event.trace?.parentSpanId
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function summarizeTelemetryEvent(event) {
|
|
243
|
+
return {
|
|
244
|
+
type: event.type,
|
|
245
|
+
executionId: event.executionId,
|
|
246
|
+
correlationId: event.correlationId,
|
|
247
|
+
traceId: event.trace?.traceId,
|
|
248
|
+
spanId: event.trace?.spanId,
|
|
249
|
+
parentSpanId: event.trace?.parentSpanId,
|
|
250
|
+
spanKind: event.span?.kind,
|
|
251
|
+
spanName: event.span?.name
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function summarizeDomainBatch(events) {
|
|
255
|
+
const counts = {};
|
|
256
|
+
for (const e of events) {
|
|
257
|
+
counts[e.type] = (counts[e.type] ?? 0) + 1;
|
|
258
|
+
}
|
|
259
|
+
const topTypes = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 6).map(([type, count]) => `${type}\xD7${count}`);
|
|
260
|
+
return {
|
|
261
|
+
count: events.length,
|
|
262
|
+
topTypes: topTypes.join(", ")
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/connection/server-connection.ts
|
|
267
|
+
var ServerConnection = class {
|
|
268
|
+
constructor(config, logger) {
|
|
269
|
+
this.config = config;
|
|
270
|
+
this.clientId = config.clientId;
|
|
271
|
+
this.instanceId = config.instanceId ?? uuid();
|
|
272
|
+
this.logger = logger;
|
|
273
|
+
this.socket = this.createSocket();
|
|
274
|
+
}
|
|
275
|
+
socket;
|
|
276
|
+
reconnectAttempts = 0;
|
|
277
|
+
registry;
|
|
278
|
+
agents = [];
|
|
279
|
+
tools = [];
|
|
280
|
+
workflows = [];
|
|
281
|
+
snapshotTimer;
|
|
282
|
+
connectionAttempted = false;
|
|
283
|
+
lastConnectError = null;
|
|
284
|
+
hasAuthenticated = false;
|
|
285
|
+
logger;
|
|
286
|
+
commandStarts = /* @__PURE__ */ new Map();
|
|
287
|
+
clientId;
|
|
288
|
+
instanceId;
|
|
289
|
+
/**
|
|
290
|
+
* Sets the registry for executing agents and workflows.
|
|
291
|
+
*/
|
|
292
|
+
setRegistry(registry) {
|
|
293
|
+
this.registry = registry;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Stores agents and workflows for registration.
|
|
297
|
+
*/
|
|
298
|
+
registerResources(agents, tools, workflows) {
|
|
299
|
+
this.agents = agents;
|
|
300
|
+
this.tools = tools;
|
|
301
|
+
this.workflows = workflows;
|
|
302
|
+
}
|
|
303
|
+
collectAgentsFromWorkflows() {
|
|
304
|
+
const out = [];
|
|
305
|
+
const seen = /* @__PURE__ */ new Set();
|
|
306
|
+
for (const workflow of this.workflows) {
|
|
307
|
+
const workflowWithNodes = workflow;
|
|
308
|
+
if (typeof workflowWithNodes.__internalNodes !== "function") continue;
|
|
309
|
+
const nodes = workflowWithNodes.__internalNodes();
|
|
310
|
+
if (!Array.isArray(nodes)) continue;
|
|
311
|
+
for (const node of nodes) {
|
|
312
|
+
if (node.type !== "agent") continue;
|
|
313
|
+
const agentNode = node;
|
|
314
|
+
const agent = agentNode.agent;
|
|
315
|
+
if (!agent) continue;
|
|
316
|
+
const id = agent.id ?? agent.name;
|
|
317
|
+
if (!id) continue;
|
|
318
|
+
if (seen.has(id)) continue;
|
|
319
|
+
seen.add(id);
|
|
320
|
+
out.push(agent);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
collectToolsFromAgents(agents) {
|
|
326
|
+
const out = [];
|
|
327
|
+
const seen = /* @__PURE__ */ new Set();
|
|
328
|
+
for (const agent of agents) {
|
|
329
|
+
const agentWithConfig = agent;
|
|
330
|
+
const config = agentWithConfig.config;
|
|
331
|
+
if (!config || typeof config !== "object") continue;
|
|
332
|
+
const tools = Array.isArray(config.tools) ? config.tools : [];
|
|
333
|
+
for (const tool of tools) {
|
|
334
|
+
if (!tool || typeof tool.id !== "string") continue;
|
|
335
|
+
if (seen.has(tool.id)) continue;
|
|
336
|
+
seen.add(tool.id);
|
|
337
|
+
out.push(tool);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Creates and configures the Socket.IO client.
|
|
344
|
+
*/
|
|
345
|
+
createSocket() {
|
|
346
|
+
const socket = io(`${this.config.url}/runtime`, {
|
|
347
|
+
auth: { token: this.config.apiKey },
|
|
348
|
+
reconnection: true,
|
|
349
|
+
reconnectionDelay: 1e3,
|
|
350
|
+
reconnectionDelayMax: 5e3,
|
|
351
|
+
transports: ["websocket"]
|
|
352
|
+
});
|
|
353
|
+
this.setupEventHandlers(socket);
|
|
354
|
+
return socket;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Sets up Socket.IO event handlers
|
|
358
|
+
*/
|
|
359
|
+
setupEventHandlers(socket) {
|
|
360
|
+
socket.on("connect", () => {
|
|
361
|
+
if (this.logger) {
|
|
362
|
+
this.logger.info("WS connected", { url: this.config.url, transport: "websocket" });
|
|
363
|
+
} else {
|
|
364
|
+
console.log("\u2705 Connected to NebulaOS Cloud");
|
|
365
|
+
}
|
|
366
|
+
this.reconnectAttempts = 0;
|
|
367
|
+
this.lastConnectError = null;
|
|
368
|
+
this.hasAuthenticated = false;
|
|
369
|
+
});
|
|
370
|
+
socket.on("disconnect", (reason) => {
|
|
371
|
+
if (!this.hasAuthenticated && reason === "io server disconnect") {
|
|
372
|
+
if (this.logger) {
|
|
373
|
+
this.logger.error("WS rejected by server (likely auth failure)", { reason });
|
|
374
|
+
} else {
|
|
375
|
+
console.error(
|
|
376
|
+
`\u{1F510} Connection rejected by server (likely authentication failure). Please check your NEBULAOS_API_KEY environment variable. (Reason: ${reason})`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
if (this.logger) {
|
|
381
|
+
this.logger.warn("WS disconnected", { reason });
|
|
382
|
+
} else {
|
|
383
|
+
console.warn(`\u26A0\uFE0F Disconnected: ${reason}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
socket.on("connect_error", (error) => {
|
|
388
|
+
this.reconnectAttempts++;
|
|
389
|
+
this.lastConnectError = error;
|
|
390
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
391
|
+
const rawStack = error instanceof Error ? error.stack : void 0;
|
|
392
|
+
const errorWithData = error;
|
|
393
|
+
const serverData = errorWithData.data;
|
|
394
|
+
const serverMessage = typeof serverData?.message === "string" ? serverData.message : typeof serverData?.error === "string" ? serverData.error : void 0;
|
|
395
|
+
const lower = rawMessage.toLowerCase();
|
|
396
|
+
const looksLikeAuth = lower.includes("authorization") || lower.includes("token") || lower.includes("unauthorized") || lower.includes("authentication") || lower.includes("api key") || lower.includes("invalid");
|
|
397
|
+
const meta = {
|
|
398
|
+
url: this.config.url,
|
|
399
|
+
attempt: this.reconnectAttempts,
|
|
400
|
+
serverMessage,
|
|
401
|
+
serverData,
|
|
402
|
+
// Preserve raw stack for debugging (dev use)
|
|
403
|
+
stack: rawStack,
|
|
404
|
+
hint: looksLikeAuth ? "Looks like auth failure. Check NEBULAOS_API_KEY." : "See error details above (network/DNS/URL/CORS)."
|
|
405
|
+
};
|
|
406
|
+
if (this.logger) {
|
|
407
|
+
this.logger.error(`WS connect_error: ${rawMessage}`, meta);
|
|
408
|
+
} else {
|
|
409
|
+
console.error(`\u274C connect_error (attempt ${this.reconnectAttempts}): ${rawMessage}`, meta);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
socket.on("client:online", () => {
|
|
413
|
+
this.hasAuthenticated = true;
|
|
414
|
+
if (this.logger) {
|
|
415
|
+
this.logger.info("WS authenticated (client:online)");
|
|
416
|
+
}
|
|
417
|
+
void this.registerFull();
|
|
418
|
+
});
|
|
419
|
+
socket.on(
|
|
420
|
+
"command:execute:agent",
|
|
421
|
+
(payload) => void this.handleAgentCommand(payload)
|
|
422
|
+
);
|
|
423
|
+
socket.on(
|
|
424
|
+
"command:execute:workflow",
|
|
425
|
+
(payload) => void this.handleWorkflowCommand(payload)
|
|
426
|
+
);
|
|
427
|
+
socket.on(
|
|
428
|
+
"command:execute:tool",
|
|
429
|
+
(payload) => void this.handleToolCommand(payload)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Handles agent execution commands from the server
|
|
434
|
+
*/
|
|
435
|
+
async handleAgentCommand(payload) {
|
|
436
|
+
const startedAt = nowMs();
|
|
437
|
+
this.commandStarts.set(payload.commandId, startedAt);
|
|
438
|
+
if (this.logger) {
|
|
439
|
+
this.logger.info("Command received", {
|
|
440
|
+
type: payload.type,
|
|
441
|
+
target: payload.targetName,
|
|
442
|
+
commandId: payload.commandId,
|
|
443
|
+
executionId: payload.executionId,
|
|
444
|
+
input: summarizeValue(payload.input)
|
|
445
|
+
});
|
|
446
|
+
} else {
|
|
447
|
+
console.log(`\u{1F4E5} Received agent execution command: ${payload.targetName}`);
|
|
448
|
+
}
|
|
449
|
+
if (!this.registry) {
|
|
450
|
+
this.sendError(payload.commandId, {
|
|
451
|
+
code: "REGISTRY_NOT_SET",
|
|
452
|
+
message: "Registry not configured"
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const agent = this.registry.getAgent(payload.targetName);
|
|
458
|
+
let agentInput;
|
|
459
|
+
if (payload.input) {
|
|
460
|
+
if (typeof payload.input === "string") {
|
|
461
|
+
agentInput = payload.input;
|
|
462
|
+
} else if (Array.isArray(payload.input)) {
|
|
463
|
+
agentInput = payload.input;
|
|
464
|
+
} else {
|
|
465
|
+
agentInput = JSON.stringify(payload.input);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const options = payload.options && typeof payload.options === "object" ? { ...payload.options, executionId: payload.executionId } : { executionId: payload.executionId };
|
|
469
|
+
const result = await ExecutionContext.run(
|
|
470
|
+
{
|
|
471
|
+
executionId: payload.executionId,
|
|
472
|
+
rootExecutionId: payload.executionId,
|
|
473
|
+
parentExecutionId: void 0
|
|
474
|
+
},
|
|
475
|
+
async () => agent.execute(agentInput, options)
|
|
476
|
+
);
|
|
477
|
+
this.sendResponse(payload.commandId, payload.executionId, result);
|
|
478
|
+
if (this.logger) {
|
|
479
|
+
this.logger.info("Command handled", {
|
|
480
|
+
commandId: payload.commandId,
|
|
481
|
+
executionId: payload.executionId,
|
|
482
|
+
durationMs: Number(durationMs(startedAt).toFixed(2))
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
this.sendError(payload.commandId, {
|
|
487
|
+
code: "AGENT_EXECUTION_ERROR",
|
|
488
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
489
|
+
details: error
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Handles workflow execution commands from the server
|
|
495
|
+
*/
|
|
496
|
+
async handleWorkflowCommand(payload) {
|
|
497
|
+
const startedAt = nowMs();
|
|
498
|
+
this.commandStarts.set(payload.commandId, startedAt);
|
|
499
|
+
if (this.logger) {
|
|
500
|
+
this.logger.info("Command received", {
|
|
501
|
+
type: payload.type,
|
|
502
|
+
target: payload.targetName,
|
|
503
|
+
commandId: payload.commandId,
|
|
504
|
+
executionId: payload.executionId,
|
|
505
|
+
input: summarizeValue(payload.input)
|
|
506
|
+
});
|
|
507
|
+
} else {
|
|
508
|
+
console.log(`\u{1F4E5} Received workflow execution command: ${payload.targetName}`);
|
|
509
|
+
}
|
|
510
|
+
if (!this.registry) {
|
|
511
|
+
this.sendError(payload.commandId, {
|
|
512
|
+
code: "REGISTRY_NOT_SET",
|
|
513
|
+
message: "Registry not configured"
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const workflow = this.registry.getWorkflow(payload.targetName);
|
|
519
|
+
const result = await ExecutionContext.run(
|
|
520
|
+
{
|
|
521
|
+
executionId: payload.executionId,
|
|
522
|
+
rootExecutionId: payload.executionId,
|
|
523
|
+
parentExecutionId: void 0
|
|
524
|
+
},
|
|
525
|
+
async () => workflow.run(
|
|
526
|
+
payload.input,
|
|
527
|
+
{ executionId: payload.executionId }
|
|
528
|
+
)
|
|
529
|
+
);
|
|
530
|
+
this.sendResponse(payload.commandId, payload.executionId, result);
|
|
531
|
+
if (this.logger) {
|
|
532
|
+
this.logger.info("Command handled", {
|
|
533
|
+
commandId: payload.commandId,
|
|
534
|
+
executionId: payload.executionId,
|
|
535
|
+
durationMs: Number(durationMs(startedAt).toFixed(2))
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.sendError(payload.commandId, {
|
|
540
|
+
code: "WORKFLOW_EXECUTION_ERROR",
|
|
541
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
542
|
+
details: error
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Handles tool execution commands from the server.
|
|
548
|
+
*/
|
|
549
|
+
async handleToolCommand(payload) {
|
|
550
|
+
const startedAt = nowMs();
|
|
551
|
+
this.commandStarts.set(payload.commandId, startedAt);
|
|
552
|
+
if (this.logger) {
|
|
553
|
+
this.logger.info("Command received", {
|
|
554
|
+
type: payload.type,
|
|
555
|
+
target: payload.targetName,
|
|
556
|
+
commandId: payload.commandId,
|
|
557
|
+
executionId: payload.executionId,
|
|
558
|
+
input: summarizeValue(payload.input)
|
|
559
|
+
});
|
|
560
|
+
} else {
|
|
561
|
+
console.log(`\u{1F4E5} Received tool execution command: ${payload.targetName}`);
|
|
562
|
+
}
|
|
563
|
+
if (!this.registry) {
|
|
564
|
+
this.sendError(payload.commandId, {
|
|
565
|
+
code: "REGISTRY_NOT_SET",
|
|
566
|
+
message: "Registry not configured"
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const tool = this.registry.getTool(payload.targetName);
|
|
572
|
+
const result = await ExecutionContext.run(
|
|
573
|
+
{
|
|
574
|
+
executionId: payload.executionId,
|
|
575
|
+
rootExecutionId: payload.executionId,
|
|
576
|
+
parentExecutionId: void 0
|
|
577
|
+
},
|
|
578
|
+
async () => Tracing.withSpan(
|
|
579
|
+
{
|
|
580
|
+
kind: "tool",
|
|
581
|
+
name: `tool:${tool.id}`,
|
|
582
|
+
executionId: payload.executionId,
|
|
583
|
+
data: {
|
|
584
|
+
toolId: tool.id
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
async () => {
|
|
588
|
+
return tool.execute({}, payload.input);
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
);
|
|
592
|
+
this.sendResponse(payload.commandId, payload.executionId, result);
|
|
593
|
+
if (this.logger) {
|
|
594
|
+
this.logger.info("Command handled", {
|
|
595
|
+
commandId: payload.commandId,
|
|
596
|
+
executionId: payload.executionId,
|
|
597
|
+
durationMs: Number(durationMs(startedAt).toFixed(2))
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
this.sendError(payload.commandId, {
|
|
602
|
+
code: "TOOL_EXECUTION_ERROR",
|
|
603
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
604
|
+
details: error
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Executes an agent and waits for completion
|
|
610
|
+
*/
|
|
611
|
+
// NOTE:
|
|
612
|
+
// We intentionally removed the previous "wait for start event to resolve executionId" logic.
|
|
613
|
+
// Cloud already provides a stable `payload.executionId` and expects it back in `command:response`.
|
|
614
|
+
/**
|
|
615
|
+
* Sends a successful command response to the server
|
|
616
|
+
*/
|
|
617
|
+
sendResponse(commandId, executionId, result) {
|
|
618
|
+
const response = {
|
|
619
|
+
commandId,
|
|
620
|
+
executionId,
|
|
621
|
+
result
|
|
622
|
+
};
|
|
623
|
+
this.socket.emit("command:response", response);
|
|
624
|
+
const startedAt = this.commandStarts.get(commandId);
|
|
625
|
+
if (this.logger) {
|
|
626
|
+
this.logger.info("Command response sent", {
|
|
627
|
+
commandId,
|
|
628
|
+
executionId,
|
|
629
|
+
durationMs: startedAt ? Number(durationMs(startedAt).toFixed(2)) : void 0,
|
|
630
|
+
result: summarizeValue(result)
|
|
631
|
+
});
|
|
632
|
+
} else {
|
|
633
|
+
console.log(`\u2705 Sent response for command ${commandId}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Sends an error response to the server
|
|
638
|
+
*/
|
|
639
|
+
sendError(commandId, error) {
|
|
640
|
+
const errorResponse = {
|
|
641
|
+
commandId,
|
|
642
|
+
error
|
|
643
|
+
};
|
|
644
|
+
this.socket.emit("command:error", errorResponse);
|
|
645
|
+
const startedAt = this.commandStarts.get(commandId);
|
|
646
|
+
if (this.logger) {
|
|
647
|
+
this.logger.error("Command error sent", {
|
|
648
|
+
commandId,
|
|
649
|
+
durationMs: startedAt ? Number(durationMs(startedAt).toFixed(2)) : void 0,
|
|
650
|
+
code: error.code,
|
|
651
|
+
message: error.message
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
console.error(`\u274C Sent error for command ${commandId}:`, error.message);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Gets the stable ID for an agent (id if available, otherwise name).
|
|
659
|
+
*/
|
|
660
|
+
getAgentId(agent) {
|
|
661
|
+
return agent.id ?? agent.name;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Gets the description from an agent if available.
|
|
665
|
+
*/
|
|
666
|
+
getAgentDescription(agent) {
|
|
667
|
+
const agentWithConfig = agent;
|
|
668
|
+
const config = agentWithConfig.config;
|
|
669
|
+
if (config && typeof config === "object" && "description" in config) {
|
|
670
|
+
return typeof config.description === "string" ? config.description : void 0;
|
|
671
|
+
}
|
|
672
|
+
return void 0;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Builds the Cloud registration payload (RegisterClientDto compatible).
|
|
676
|
+
*/
|
|
677
|
+
async buildRegisterPayload(input) {
|
|
678
|
+
const workflowAgents = this.collectAgentsFromWorkflows();
|
|
679
|
+
const agents = [...this.agents];
|
|
680
|
+
for (const agent of workflowAgents) {
|
|
681
|
+
const id = this.getAgentId(agent);
|
|
682
|
+
const exists = agents.some((x) => this.getAgentId(x) === id);
|
|
683
|
+
if (!exists) agents.push(agent);
|
|
684
|
+
}
|
|
685
|
+
const agentTools = this.collectToolsFromAgents(agents);
|
|
686
|
+
const tools = [...this.tools];
|
|
687
|
+
for (const tool of agentTools) {
|
|
688
|
+
if (tools.some((x) => x.id === tool.id)) continue;
|
|
689
|
+
tools.push(tool);
|
|
690
|
+
}
|
|
691
|
+
const resources = [
|
|
692
|
+
...agents.map((agent) => {
|
|
693
|
+
const runtimeResourceId = this.getAgentId(agent);
|
|
694
|
+
const description = this.getAgentDescription(agent);
|
|
695
|
+
return {
|
|
696
|
+
type: "agent",
|
|
697
|
+
runtimeResourceId,
|
|
698
|
+
displayName: agent.name,
|
|
699
|
+
kind: agent.kind,
|
|
700
|
+
description
|
|
701
|
+
};
|
|
702
|
+
}),
|
|
703
|
+
...this.workflows.map((workflow) => {
|
|
704
|
+
const definition = input.includeDefinitions ? this.buildWorkflowDefinition(workflow) : void 0;
|
|
705
|
+
return {
|
|
706
|
+
type: "workflow",
|
|
707
|
+
runtimeResourceId: workflow.id,
|
|
708
|
+
displayName: workflow.name ?? workflow.id,
|
|
709
|
+
description: workflow.description,
|
|
710
|
+
...definition ? { definition } : {}
|
|
711
|
+
};
|
|
712
|
+
}),
|
|
713
|
+
...tools.map((tool) => {
|
|
714
|
+
const definition = input.includeDefinitions ? this.buildToolDefinition(tool) : void 0;
|
|
715
|
+
return {
|
|
716
|
+
type: "tool",
|
|
717
|
+
runtimeResourceId: tool.id,
|
|
718
|
+
displayName: tool.id,
|
|
719
|
+
description: tool.description,
|
|
720
|
+
...definition ? { definition } : {}
|
|
721
|
+
};
|
|
722
|
+
})
|
|
723
|
+
];
|
|
724
|
+
if (input.includeDefinitions) {
|
|
725
|
+
await Promise.all(
|
|
726
|
+
resources.filter((r) => r.type === "agent").map(async (r) => {
|
|
727
|
+
const agent = agents.find((a) => this.getAgentId(a) === r.runtimeResourceId);
|
|
728
|
+
if (!agent) return;
|
|
729
|
+
const def = await this.buildAgentDefinition(agent);
|
|
730
|
+
if (def) r.definition = def;
|
|
731
|
+
})
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
instanceId: this.instanceId,
|
|
736
|
+
// Dedupe by (type + runtimeResourceId) to avoid duplicates when tools are discovered from multiple sources.
|
|
737
|
+
resources: resources.filter((r, idx, arr) => {
|
|
738
|
+
const key = `${r.type}:${r.runtimeResourceId}`;
|
|
739
|
+
return idx === arr.findIndex((x) => `${x.type}:${x.runtimeResourceId}` === key);
|
|
740
|
+
})
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
isZodSchema(value) {
|
|
744
|
+
return !!value && typeof value === "object" && "safeParse" in value && typeof value.safeParse === "function";
|
|
745
|
+
}
|
|
746
|
+
isInstructionResolvable(value) {
|
|
747
|
+
return !!value && typeof value === "object" && "resolve" in value && typeof value.resolve === "function";
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Agent definition payload for Cloud persistence + UI inspection.
|
|
751
|
+
*
|
|
752
|
+
* IMPORTANT:
|
|
753
|
+
* - Do NOT embed tool schemas here; tools are registered as independent resources.
|
|
754
|
+
* - We only reference tool ids to avoid duplication.
|
|
755
|
+
*/
|
|
756
|
+
async buildAgentDefinition(agent) {
|
|
757
|
+
const agentWithConfig = agent;
|
|
758
|
+
const config = agentWithConfig.config;
|
|
759
|
+
if (!config || typeof config !== "object") return void 0;
|
|
760
|
+
const model = config.model;
|
|
761
|
+
const instructions = config.instructions;
|
|
762
|
+
const tools = Array.isArray(config.tools) ? config.tools : [];
|
|
763
|
+
const interceptors = config.interceptors;
|
|
764
|
+
let instructionsText = "";
|
|
765
|
+
if (typeof instructions === "string") {
|
|
766
|
+
instructionsText = instructions;
|
|
767
|
+
} else if (this.isInstructionResolvable(instructions)) {
|
|
768
|
+
instructionsText = await instructions.resolve();
|
|
769
|
+
}
|
|
770
|
+
const requestInterceptorNames = interceptors?.request?.map((fn) => {
|
|
771
|
+
if (typeof fn === "function" && fn.name) {
|
|
772
|
+
return fn.name;
|
|
773
|
+
}
|
|
774
|
+
return "anonymous";
|
|
775
|
+
}) ?? [];
|
|
776
|
+
const responseInterceptorNames = interceptors?.response?.map((fn) => {
|
|
777
|
+
if (typeof fn === "function" && fn.name) {
|
|
778
|
+
return fn.name;
|
|
779
|
+
}
|
|
780
|
+
return "anonymous";
|
|
781
|
+
}) ?? [];
|
|
782
|
+
const modelInfo = model && typeof model === "object" && "providerName" in model && "modelName" in model ? {
|
|
783
|
+
provider: model.providerName,
|
|
784
|
+
model: model.modelName,
|
|
785
|
+
capabilities: model.capabilities
|
|
786
|
+
} : void 0;
|
|
787
|
+
return {
|
|
788
|
+
schemaVersion: 1,
|
|
789
|
+
resourceType: "agent",
|
|
790
|
+
agentId: agent.id ?? agent.name,
|
|
791
|
+
agentName: agent.name,
|
|
792
|
+
kind: agent.kind,
|
|
793
|
+
model: modelInfo,
|
|
794
|
+
instructionsText,
|
|
795
|
+
toolIds: tools.map((t) => t.id),
|
|
796
|
+
interceptors: {
|
|
797
|
+
request: requestInterceptorNames,
|
|
798
|
+
response: responseInterceptorNames
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Builds a workflow definition payload suitable for Cloud persistence + UI graph rendering.
|
|
804
|
+
*
|
|
805
|
+
* The base definition comes from `workflow.describe()` (graph nodes/edges).
|
|
806
|
+
* If the workflow was created with an input Zod schema, we also attach `inputSchema`
|
|
807
|
+
* as a JSON Schema object so the UI can render an input form.
|
|
808
|
+
*/
|
|
809
|
+
buildWorkflowDefinition(workflow) {
|
|
810
|
+
const workflowWithNodes = workflow;
|
|
811
|
+
if (typeof workflowWithNodes.describe !== "function") return void 0;
|
|
812
|
+
const base = workflowWithNodes.describe();
|
|
813
|
+
if (!base || typeof base !== "object") return void 0;
|
|
814
|
+
const workflowUnknown = workflow;
|
|
815
|
+
const workflowWithConfig = workflowUnknown && typeof workflowUnknown === "object" && "config" in workflowUnknown && typeof workflowUnknown.config === "object" ? workflowUnknown.config?.inputSchema : void 0;
|
|
816
|
+
const inputSchemaCandidate = workflowWithConfig;
|
|
817
|
+
const withSchemas = this.isZodSchema(inputSchemaCandidate) ? { ...base, inputSchema: zodToJsonSchema(inputSchemaCandidate) } : base;
|
|
818
|
+
const nodes = Array.isArray(withSchemas.nodes) ? withSchemas.nodes : void 0;
|
|
819
|
+
const agentIds = Array.isArray(nodes) ? nodes.map((n) => typeof n?.agentId === "string" && n.agentId ? n.agentId : void 0).filter((x) => !!x) : [];
|
|
820
|
+
return {
|
|
821
|
+
...withSchemas,
|
|
822
|
+
schemaVersion: 1,
|
|
823
|
+
resourceType: "workflow",
|
|
824
|
+
refs: {
|
|
825
|
+
agents: Array.from(new Set(agentIds)),
|
|
826
|
+
tools: [],
|
|
827
|
+
workflows: []
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
buildToolDefinition(tool) {
|
|
832
|
+
const inputSchemaCandidate = tool.inputSchema;
|
|
833
|
+
const inputSchema = this.isZodSchema(inputSchemaCandidate) ? zodToJsonSchema(inputSchemaCandidate) : void 0;
|
|
834
|
+
return {
|
|
835
|
+
schemaVersion: 1,
|
|
836
|
+
resourceType: "tool",
|
|
837
|
+
toolId: tool.id,
|
|
838
|
+
description: tool.description,
|
|
839
|
+
...inputSchema ? { inputSchema } : {},
|
|
840
|
+
outputType: "unknown"
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Full registration (discovery) call. This is idempotent on the Cloud side.
|
|
845
|
+
*/
|
|
846
|
+
async registerFull() {
|
|
847
|
+
const payload = await this.buildRegisterPayload({ includeDefinitions: true });
|
|
848
|
+
const start = nowMs();
|
|
849
|
+
return new Promise((resolve, reject) => {
|
|
850
|
+
const timeout = setTimeout(() => {
|
|
851
|
+
reject(
|
|
852
|
+
new ConnectionError(
|
|
853
|
+
"Registration timeout: Server did not respond to registration request. Please check your connection and try again."
|
|
854
|
+
)
|
|
855
|
+
);
|
|
856
|
+
}, 1e4);
|
|
857
|
+
this.socket.emit("client:register:full", payload, (response) => {
|
|
858
|
+
clearTimeout(timeout);
|
|
859
|
+
if (response.success) {
|
|
860
|
+
const defCount = payload.resources.filter((r) => !!r.definition).length;
|
|
861
|
+
const agentDefCount = payload.resources.filter(
|
|
862
|
+
(r) => r.type === "agent" && !!r.definition
|
|
863
|
+
).length;
|
|
864
|
+
const workflowDefCount = payload.resources.filter(
|
|
865
|
+
(r) => r.type === "workflow" && !!r.definition
|
|
866
|
+
).length;
|
|
867
|
+
const toolDefCount = payload.resources.filter(
|
|
868
|
+
(r) => r.type === "tool" && !!r.definition
|
|
869
|
+
).length;
|
|
870
|
+
if (this.logger) {
|
|
871
|
+
this.logger.info("Client registered", {
|
|
872
|
+
clientId: this.clientId,
|
|
873
|
+
instanceId: payload.instanceId,
|
|
874
|
+
resources: payload.resources.length,
|
|
875
|
+
definitions: {
|
|
876
|
+
total: defCount,
|
|
877
|
+
agents: agentDefCount,
|
|
878
|
+
workflows: workflowDefCount,
|
|
879
|
+
tools: toolDefCount
|
|
880
|
+
},
|
|
881
|
+
durationMs: Number(durationMs(start).toFixed(2))
|
|
882
|
+
});
|
|
883
|
+
} else {
|
|
884
|
+
console.log(
|
|
885
|
+
`\u2705 Client registered successfully: ${this.clientId} (${payload.instanceId})`
|
|
886
|
+
);
|
|
887
|
+
console.log(` - ${payload.resources.length} resources registered`);
|
|
888
|
+
console.log(
|
|
889
|
+
` - definitions: total=${defCount}, agents=${agentDefCount}, workflows=${workflowDefCount}, tools=${toolDefCount}`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
resolve();
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const errorMessage = response.error ?? "Unknown registration error";
|
|
896
|
+
if (errorMessage.includes("authentication") || errorMessage.includes("Unauthorized") || errorMessage.includes("token") || errorMessage.includes("API key")) {
|
|
897
|
+
reject(
|
|
898
|
+
new AuthenticationError(
|
|
899
|
+
`Registration failed: ${errorMessage}. Please check your NEBULAOS_API_KEY environment variable.`
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
reject(
|
|
905
|
+
new ConnectionError(
|
|
906
|
+
`Registration failed: ${errorMessage}. Please check your connection and configuration.`
|
|
907
|
+
)
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
startSnapshotLoop() {
|
|
913
|
+
const intervalMs = this.config.snapshotIntervalMs ?? 6e4;
|
|
914
|
+
if (this.snapshotTimer) clearInterval(this.snapshotTimer);
|
|
915
|
+
this.snapshotTimer = setInterval(() => {
|
|
916
|
+
if (!this.socket.connected) return;
|
|
917
|
+
void this.buildRegisterPayload({ includeDefinitions: false }).then((payload) => {
|
|
918
|
+
this.socket.emit("client:snapshot", payload);
|
|
919
|
+
});
|
|
920
|
+
}, intervalMs);
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Connects to the Cloud runtime.
|
|
924
|
+
*/
|
|
925
|
+
async connect() {
|
|
926
|
+
return new Promise((resolve, reject) => {
|
|
927
|
+
if (this.socket.connected) {
|
|
928
|
+
resolve();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
this.connectionAttempted = true;
|
|
932
|
+
this.hasAuthenticated = false;
|
|
933
|
+
let resolved = false;
|
|
934
|
+
let authTimeout;
|
|
935
|
+
const timeout = setTimeout(() => {
|
|
936
|
+
if (resolved) return;
|
|
937
|
+
resolved = true;
|
|
938
|
+
this.connectionAttempted = false;
|
|
939
|
+
reject(
|
|
940
|
+
new ConnectionError(
|
|
941
|
+
`Connection timeout after 10 seconds. Please check your NEBULAOS_URL (${this.config.url}) and ensure the server is running.`
|
|
942
|
+
)
|
|
943
|
+
);
|
|
944
|
+
}, 1e4);
|
|
945
|
+
const cleanup = () => {
|
|
946
|
+
clearTimeout(timeout);
|
|
947
|
+
if (authTimeout) clearTimeout(authTimeout);
|
|
948
|
+
this.socket.off("connect", connectHandler);
|
|
949
|
+
this.socket.off("connect_error", errorHandler);
|
|
950
|
+
this.socket.off("disconnect", disconnectHandler);
|
|
951
|
+
this.socket.off("client:online", onlineHandler);
|
|
952
|
+
};
|
|
953
|
+
const onlineHandler = () => {
|
|
954
|
+
if (resolved) return;
|
|
955
|
+
resolved = true;
|
|
956
|
+
cleanup();
|
|
957
|
+
this.connectionAttempted = false;
|
|
958
|
+
this.hasAuthenticated = true;
|
|
959
|
+
this.startSnapshotLoop();
|
|
960
|
+
resolve();
|
|
961
|
+
};
|
|
962
|
+
const connectHandler = () => {
|
|
963
|
+
clearTimeout(timeout);
|
|
964
|
+
authTimeout = setTimeout(() => {
|
|
965
|
+
if (resolved) return;
|
|
966
|
+
resolved = true;
|
|
967
|
+
cleanup();
|
|
968
|
+
this.connectionAttempted = false;
|
|
969
|
+
reject(
|
|
970
|
+
new AuthenticationError(
|
|
971
|
+
`Connected but authentication was not confirmed by server. Please check your NEBULAOS_API_KEY environment variable.`
|
|
972
|
+
)
|
|
973
|
+
);
|
|
974
|
+
}, 2500);
|
|
975
|
+
};
|
|
976
|
+
const disconnectHandler = (reason) => {
|
|
977
|
+
if (resolved) return;
|
|
978
|
+
if (this.connectionAttempted && reason === "io server disconnect") {
|
|
979
|
+
resolved = true;
|
|
980
|
+
cleanup();
|
|
981
|
+
this.connectionAttempted = false;
|
|
982
|
+
if (this.lastConnectError) {
|
|
983
|
+
const errorMsg = this.lastConnectError.message.toLowerCase();
|
|
984
|
+
if (errorMsg.includes("authorization") || errorMsg.includes("token") || errorMsg.includes("unauthorized") || errorMsg.includes("authentication") || errorMsg.includes("api key") || errorMsg.includes("invalid")) {
|
|
985
|
+
reject(
|
|
986
|
+
new AuthenticationError(
|
|
987
|
+
`Authentication failed: Invalid API key or missing credentials. Please check your NEBULAOS_API_KEY environment variable. (Error: ${this.lastConnectError.message})`
|
|
988
|
+
)
|
|
989
|
+
);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
reject(
|
|
994
|
+
new AuthenticationError(
|
|
995
|
+
`Connection rejected by server: Invalid API key or missing credentials. Please check your NEBULAOS_API_KEY environment variable. (Disconnect reason: ${reason})`
|
|
996
|
+
)
|
|
997
|
+
);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
const errorHandler = (error) => {
|
|
1002
|
+
if (resolved) return;
|
|
1003
|
+
const errorMsg = error.message.toLowerCase();
|
|
1004
|
+
if (errorMsg.includes("authorization") || errorMsg.includes("token") || errorMsg.includes("unauthorized") || errorMsg.includes("authentication") || errorMsg.includes("api key") || errorMsg.includes("invalid")) {
|
|
1005
|
+
resolved = true;
|
|
1006
|
+
cleanup();
|
|
1007
|
+
this.connectionAttempted = false;
|
|
1008
|
+
reject(
|
|
1009
|
+
new AuthenticationError(
|
|
1010
|
+
`Authentication failed: Invalid API key or missing credentials. Please check your NEBULAOS_API_KEY environment variable. (Server error: ${error.message})`
|
|
1011
|
+
)
|
|
1012
|
+
);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (errorMsg.includes("timeout") || errorMsg.includes("econnrefused")) {
|
|
1016
|
+
resolved = true;
|
|
1017
|
+
cleanup();
|
|
1018
|
+
this.connectionAttempted = false;
|
|
1019
|
+
reject(
|
|
1020
|
+
new ConnectionError(
|
|
1021
|
+
`Connection failed: Unable to reach NebulaOS Cloud at ${this.config.url}. Please check your NEBULAOS_URL environment variable and ensure the server is running. (Error: ${error.message})`
|
|
1022
|
+
)
|
|
1023
|
+
);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
setTimeout(() => {
|
|
1027
|
+
if (!resolved) {
|
|
1028
|
+
resolved = true;
|
|
1029
|
+
cleanup();
|
|
1030
|
+
this.connectionAttempted = false;
|
|
1031
|
+
reject(
|
|
1032
|
+
new ConnectionError(
|
|
1033
|
+
`Connection error: ${error.message}. Please check your NEBULAOS_URL (${this.config.url}) and NEBULAOS_API_KEY.`
|
|
1034
|
+
)
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
}, 1e3);
|
|
1038
|
+
};
|
|
1039
|
+
this.socket.once("connect", connectHandler);
|
|
1040
|
+
this.socket.once("connect_error", errorHandler);
|
|
1041
|
+
this.socket.once("disconnect", disconnectHandler);
|
|
1042
|
+
this.socket.once("client:online", onlineHandler);
|
|
1043
|
+
this.socket.connect();
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Disconnects from the server gracefully
|
|
1048
|
+
*/
|
|
1049
|
+
async disconnect() {
|
|
1050
|
+
console.log("\u{1F6D1} Disconnecting from NebulaOS Cloud...");
|
|
1051
|
+
if (this.snapshotTimer) clearInterval(this.snapshotTimer);
|
|
1052
|
+
this.snapshotTimer = void 0;
|
|
1053
|
+
if (this.socket.connected) {
|
|
1054
|
+
await this.sendShutdownNotice({ reason: "disconnect()" });
|
|
1055
|
+
this.socket.disconnect();
|
|
1056
|
+
}
|
|
1057
|
+
console.log("\u{1F44B} Disconnected");
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Best-effort shutdown notice to allow the Cloud to immediately clean up instance availability.
|
|
1061
|
+
* Never throws: disconnect must be resilient.
|
|
1062
|
+
*/
|
|
1063
|
+
async sendShutdownNotice(input) {
|
|
1064
|
+
const payload = {
|
|
1065
|
+
instanceId: this.instanceId,
|
|
1066
|
+
...input.reason ? { reason: input.reason } : {}
|
|
1067
|
+
};
|
|
1068
|
+
await new Promise((resolve) => {
|
|
1069
|
+
const timeout = setTimeout(() => resolve(), 1500);
|
|
1070
|
+
try {
|
|
1071
|
+
this.socket.emit("client:shutdown", payload, (_response) => {
|
|
1072
|
+
clearTimeout(timeout);
|
|
1073
|
+
resolve();
|
|
1074
|
+
});
|
|
1075
|
+
} catch {
|
|
1076
|
+
clearTimeout(timeout);
|
|
1077
|
+
resolve();
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Checks if the client is connected
|
|
1083
|
+
*/
|
|
1084
|
+
isConnected() {
|
|
1085
|
+
return this.socket.connected;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Returns the client ID
|
|
1089
|
+
*/
|
|
1090
|
+
getClientId() {
|
|
1091
|
+
return this.clientId;
|
|
1092
|
+
}
|
|
1093
|
+
getInstanceId() {
|
|
1094
|
+
return this.instanceId;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Returns the socket instance for telemetry
|
|
1098
|
+
*/
|
|
1099
|
+
getSocket() {
|
|
1100
|
+
return this.socket;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// src/client.ts
|
|
1105
|
+
import { ConsoleLogger, DomainEvents, Tracing as Tracing3 } from "@nebulaos/core";
|
|
1106
|
+
|
|
1107
|
+
// src/telemetry/http-telemetry-exporter.ts
|
|
1108
|
+
import axios from "axios";
|
|
1109
|
+
var HttpTelemetryExporter = class {
|
|
1110
|
+
constructor(input, config = {}, logger) {
|
|
1111
|
+
this.input = input;
|
|
1112
|
+
this.logger = logger;
|
|
1113
|
+
this.baseUrl = input.cloudUrl.replace(/\/+$/, "");
|
|
1114
|
+
this.timeoutMs = config.timeoutMs ?? 2e3;
|
|
1115
|
+
}
|
|
1116
|
+
baseUrl;
|
|
1117
|
+
timeoutMs;
|
|
1118
|
+
async exportBatch(events) {
|
|
1119
|
+
for (const event of events) {
|
|
1120
|
+
await this.sendSingle(event);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async sendSingle(event) {
|
|
1124
|
+
const url = `${this.baseUrl}/telemetry/events`;
|
|
1125
|
+
const start = nowMs();
|
|
1126
|
+
try {
|
|
1127
|
+
const res = await axios.post(
|
|
1128
|
+
url,
|
|
1129
|
+
{ events: [event] },
|
|
1130
|
+
{
|
|
1131
|
+
timeout: this.timeoutMs,
|
|
1132
|
+
headers: {
|
|
1133
|
+
"Content-Type": "application/json",
|
|
1134
|
+
...this.input.apiKey ? { Authorization: `Bearer ${this.input.apiKey}` } : {}
|
|
1135
|
+
},
|
|
1136
|
+
// Telemetry should never throw due to non-2xx.
|
|
1137
|
+
validateStatus: () => true
|
|
1138
|
+
}
|
|
1139
|
+
);
|
|
1140
|
+
if (this.logger) {
|
|
1141
|
+
this.logger.debug(
|
|
1142
|
+
"HTTP telemetry sent",
|
|
1143
|
+
{
|
|
1144
|
+
method: "POST",
|
|
1145
|
+
path: "/telemetry/events",
|
|
1146
|
+
status: res.status,
|
|
1147
|
+
durationMs: Number(durationMs(start).toFixed(2)),
|
|
1148
|
+
timeoutMs: this.timeoutMs,
|
|
1149
|
+
event: summarizeTelemetryEvent(event)
|
|
1150
|
+
}
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
if (this.logger) {
|
|
1155
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1156
|
+
this.logger.warn(
|
|
1157
|
+
"HTTP telemetry failed (best-effort)",
|
|
1158
|
+
{
|
|
1159
|
+
method: "POST",
|
|
1160
|
+
path: "/telemetry/events",
|
|
1161
|
+
durationMs: Number(durationMs(start).toFixed(2)),
|
|
1162
|
+
timeoutMs: this.timeoutMs,
|
|
1163
|
+
error: message,
|
|
1164
|
+
event: summarizeTelemetryEvent(event)
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
// src/domain-events/http-domain-events-exporter.ts
|
|
1173
|
+
import axios2 from "axios";
|
|
1174
|
+
var HttpDomainEventsExporter = class {
|
|
1175
|
+
constructor(input, config = {}, logger) {
|
|
1176
|
+
this.input = input;
|
|
1177
|
+
this.logger = logger;
|
|
1178
|
+
this.baseUrl = input.cloudUrl.replace(/\/+$/, "");
|
|
1179
|
+
this.timeoutMs = config.timeoutMs ?? 2e3;
|
|
1180
|
+
}
|
|
1181
|
+
baseUrl;
|
|
1182
|
+
timeoutMs;
|
|
1183
|
+
async exportBatch(events) {
|
|
1184
|
+
if (events.length === 0) return;
|
|
1185
|
+
const url = `${this.baseUrl}/execution/events`;
|
|
1186
|
+
const start = nowMs();
|
|
1187
|
+
try {
|
|
1188
|
+
const res = await axios2.post(
|
|
1189
|
+
url,
|
|
1190
|
+
{ events },
|
|
1191
|
+
{
|
|
1192
|
+
timeout: this.timeoutMs,
|
|
1193
|
+
headers: {
|
|
1194
|
+
"Content-Type": "application/json",
|
|
1195
|
+
...this.input.apiKey ? { Authorization: `Bearer ${this.input.apiKey}` } : {}
|
|
1196
|
+
},
|
|
1197
|
+
// Domain events should never throw due to non-2xx.
|
|
1198
|
+
validateStatus: () => true
|
|
1199
|
+
}
|
|
1200
|
+
);
|
|
1201
|
+
if (this.logger) {
|
|
1202
|
+
this.logger.debug(
|
|
1203
|
+
"HTTP domain events sent",
|
|
1204
|
+
{
|
|
1205
|
+
method: "POST",
|
|
1206
|
+
path: "/execution/events",
|
|
1207
|
+
status: res.status,
|
|
1208
|
+
durationMs: Number(durationMs(start).toFixed(2)),
|
|
1209
|
+
timeoutMs: this.timeoutMs,
|
|
1210
|
+
batch: summarizeDomainBatch(events),
|
|
1211
|
+
sample: events.slice(0, 3).map((e) => summarizeDomainEvent(e))
|
|
1212
|
+
}
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
} catch {
|
|
1216
|
+
if (this.logger) {
|
|
1217
|
+
this.logger.warn(
|
|
1218
|
+
"HTTP domain events failed (best-effort)",
|
|
1219
|
+
{
|
|
1220
|
+
method: "POST",
|
|
1221
|
+
path: "/execution/events",
|
|
1222
|
+
durationMs: Number(durationMs(start).toFixed(2)),
|
|
1223
|
+
timeoutMs: this.timeoutMs,
|
|
1224
|
+
batch: summarizeDomainBatch(events),
|
|
1225
|
+
sample: events.slice(0, 3).map((e) => summarizeDomainEvent(e))
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
// src/logger/client-debug.ts
|
|
1234
|
+
function parseBool(value) {
|
|
1235
|
+
if (!value) return false;
|
|
1236
|
+
const v = value.trim().toLowerCase();
|
|
1237
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
1238
|
+
}
|
|
1239
|
+
function resolveClientDebugConfig(input) {
|
|
1240
|
+
const env = process.env.NEBULAOS_CLIENT_DEBUG?.trim().toLowerCase();
|
|
1241
|
+
if (env) {
|
|
1242
|
+
if (env === "off" || env === "0" || env === "false") {
|
|
1243
|
+
return { enabled: false, level: "none" };
|
|
1244
|
+
}
|
|
1245
|
+
if (env === "debug") {
|
|
1246
|
+
return { enabled: true, level: "debug" };
|
|
1247
|
+
}
|
|
1248
|
+
if (env === "info") {
|
|
1249
|
+
return { enabled: true, level: "info" };
|
|
1250
|
+
}
|
|
1251
|
+
if (parseBool(env)) {
|
|
1252
|
+
return { enabled: true, level: "debug" };
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (input?.logLevel) {
|
|
1256
|
+
return {
|
|
1257
|
+
enabled: input.logLevel !== "none",
|
|
1258
|
+
level: input.logLevel
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const enabled = input?.enabled ?? false;
|
|
1262
|
+
const level = input?.level ?? (enabled ? "debug" : "none");
|
|
1263
|
+
return { enabled, level };
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/client.ts
|
|
1267
|
+
import { v4 as uuid2 } from "uuid";
|
|
1268
|
+
|
|
1269
|
+
// src/tracing/setup.ts
|
|
1270
|
+
import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
|
|
1271
|
+
import { Resource } from "@opentelemetry/resources";
|
|
1272
|
+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
|
1273
|
+
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
|
1274
|
+
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
1275
|
+
function setupOTelTracing(config) {
|
|
1276
|
+
const resource = new Resource({
|
|
1277
|
+
"service.name": config.serviceName
|
|
1278
|
+
});
|
|
1279
|
+
const provider = new BasicTracerProvider({ resource });
|
|
1280
|
+
provider.register({
|
|
1281
|
+
contextManager: new AsyncLocalStorageContextManager()
|
|
1282
|
+
});
|
|
1283
|
+
registerInstrumentations({
|
|
1284
|
+
tracerProvider: provider,
|
|
1285
|
+
instrumentations: [
|
|
1286
|
+
new HttpInstrumentation()
|
|
1287
|
+
]
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/tracing/otel-provider.ts
|
|
1292
|
+
import { context, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
1293
|
+
import {
|
|
1294
|
+
ActiveSpan
|
|
1295
|
+
} from "@nebulaos/core";
|
|
1296
|
+
var OTelTracingProvider = class {
|
|
1297
|
+
constructor(serviceName, exporter) {
|
|
1298
|
+
this.exporter = exporter;
|
|
1299
|
+
this.tracer = trace.getTracer(serviceName);
|
|
1300
|
+
}
|
|
1301
|
+
tracer;
|
|
1302
|
+
/**
|
|
1303
|
+
* Ensures an OTel ID string is in W3C lowercase hex format.
|
|
1304
|
+
*
|
|
1305
|
+
* If the value is already valid hex of the expected length it is returned
|
|
1306
|
+
* as-is. Otherwise we assume it is base64-encoded and convert it.
|
|
1307
|
+
*
|
|
1308
|
+
* @param id Raw ID from OTel spanContext()
|
|
1309
|
+
* @param hexLen Expected hex length (16 for spanId, 32 for traceId)
|
|
1310
|
+
*/
|
|
1311
|
+
ensureW3cHex(id, hexLen) {
|
|
1312
|
+
if (!id) return void 0;
|
|
1313
|
+
const hexRegex = hexLen === 16 ? /^[0-9a-f]{16}$/ : /^[0-9a-f]{32}$/;
|
|
1314
|
+
if (hexRegex.test(id)) return id;
|
|
1315
|
+
try {
|
|
1316
|
+
const hex = Buffer.from(id, "base64").toString("hex");
|
|
1317
|
+
return hex.slice(0, hexLen).padStart(hexLen, "0");
|
|
1318
|
+
} catch {
|
|
1319
|
+
return void 0;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
startSpan(input) {
|
|
1323
|
+
const parentCtx = context.active();
|
|
1324
|
+
const parentSpan = trace.getSpan(parentCtx);
|
|
1325
|
+
const otelSpan = this.tracer.startSpan(
|
|
1326
|
+
`${input.kind}:${input.name}`,
|
|
1327
|
+
{
|
|
1328
|
+
attributes: {
|
|
1329
|
+
"nebula.span.kind": input.kind,
|
|
1330
|
+
"nebula.span.name": input.name,
|
|
1331
|
+
...input.correlationId ? { "nebula.correlationId": input.correlationId } : {},
|
|
1332
|
+
...input.executionId ? { "nebula.executionId": input.executionId } : {}
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
parentCtx
|
|
1336
|
+
);
|
|
1337
|
+
const spanContext = otelSpan.spanContext();
|
|
1338
|
+
const traceId = this.ensureW3cHex(spanContext.traceId, 32) ?? spanContext.traceId;
|
|
1339
|
+
const spanId = this.ensureW3cHex(spanContext.spanId, 16) ?? spanContext.spanId;
|
|
1340
|
+
const parentSpanId = this.ensureW3cHex(parentSpan?.spanContext().spanId, 16);
|
|
1341
|
+
const activeSpan = new OTelActiveSpan(
|
|
1342
|
+
traceId,
|
|
1343
|
+
spanId,
|
|
1344
|
+
parentSpanId,
|
|
1345
|
+
input.kind,
|
|
1346
|
+
input.name,
|
|
1347
|
+
input.correlationId,
|
|
1348
|
+
input.executionId,
|
|
1349
|
+
this.exporter,
|
|
1350
|
+
otelSpan
|
|
1351
|
+
);
|
|
1352
|
+
const startEvent = {
|
|
1353
|
+
v: 1,
|
|
1354
|
+
type: "telemetry:span:start",
|
|
1355
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1356
|
+
trace: { traceId, spanId, parentSpanId },
|
|
1357
|
+
correlationId: input.correlationId,
|
|
1358
|
+
executionId: input.executionId,
|
|
1359
|
+
span: {
|
|
1360
|
+
kind: input.kind,
|
|
1361
|
+
name: input.name,
|
|
1362
|
+
data: input.data ?? {}
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
this.exporter.exportBatch([startEvent]).catch(() => {
|
|
1366
|
+
});
|
|
1367
|
+
return activeSpan;
|
|
1368
|
+
}
|
|
1369
|
+
getContext() {
|
|
1370
|
+
const span = trace.getSpan(context.active());
|
|
1371
|
+
if (!span) return void 0;
|
|
1372
|
+
const sc = span.spanContext();
|
|
1373
|
+
return {
|
|
1374
|
+
traceId: this.ensureW3cHex(sc.traceId, 32) ?? sc.traceId,
|
|
1375
|
+
spanId: this.ensureW3cHex(sc.spanId, 16) ?? sc.spanId
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
async withSpan(input, fn) {
|
|
1379
|
+
const parentCtx = context.active();
|
|
1380
|
+
const parentSpan = trace.getSpan(parentCtx);
|
|
1381
|
+
const otelSpan = this.tracer.startSpan(
|
|
1382
|
+
`${input.kind}:${input.name}`,
|
|
1383
|
+
{
|
|
1384
|
+
attributes: {
|
|
1385
|
+
"nebula.span.kind": input.kind,
|
|
1386
|
+
"nebula.span.name": input.name,
|
|
1387
|
+
...input.correlationId ? { "nebula.correlationId": input.correlationId } : {},
|
|
1388
|
+
...input.executionId ? { "nebula.executionId": input.executionId } : {}
|
|
1389
|
+
}
|
|
1390
|
+
},
|
|
1391
|
+
parentCtx
|
|
1392
|
+
);
|
|
1393
|
+
const spanContext = otelSpan.spanContext();
|
|
1394
|
+
const traceId = this.ensureW3cHex(spanContext.traceId, 32) ?? spanContext.traceId;
|
|
1395
|
+
const spanId = this.ensureW3cHex(spanContext.spanId, 16) ?? spanContext.spanId;
|
|
1396
|
+
const parentSpanId = this.ensureW3cHex(parentSpan?.spanContext().spanId, 16);
|
|
1397
|
+
const activeSpan = new OTelActiveSpan(
|
|
1398
|
+
traceId,
|
|
1399
|
+
spanId,
|
|
1400
|
+
parentSpanId,
|
|
1401
|
+
input.kind,
|
|
1402
|
+
input.name,
|
|
1403
|
+
input.correlationId,
|
|
1404
|
+
input.executionId,
|
|
1405
|
+
this.exporter,
|
|
1406
|
+
otelSpan
|
|
1407
|
+
);
|
|
1408
|
+
const startEvent = {
|
|
1409
|
+
v: 1,
|
|
1410
|
+
type: "telemetry:span:start",
|
|
1411
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1412
|
+
trace: {
|
|
1413
|
+
traceId,
|
|
1414
|
+
spanId,
|
|
1415
|
+
parentSpanId
|
|
1416
|
+
},
|
|
1417
|
+
correlationId: input.correlationId,
|
|
1418
|
+
executionId: input.executionId,
|
|
1419
|
+
span: {
|
|
1420
|
+
kind: input.kind,
|
|
1421
|
+
name: input.name,
|
|
1422
|
+
data: input.data ?? {}
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
try {
|
|
1426
|
+
await this.exporter.exportBatch([startEvent]);
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
const ctx = trace.setSpan(parentCtx, otelSpan);
|
|
1430
|
+
return context.with(ctx, async () => {
|
|
1431
|
+
try {
|
|
1432
|
+
const result = await fn(activeSpan);
|
|
1433
|
+
if (!activeSpan.isEnded) {
|
|
1434
|
+
await activeSpan.end({ status: "success" });
|
|
1435
|
+
}
|
|
1436
|
+
return result;
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
await activeSpan.end({ status: "error" });
|
|
1439
|
+
throw error;
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
var OTelActiveSpan = class extends ActiveSpan {
|
|
1445
|
+
constructor(traceId, spanId, parentSpanId, kind, name, correlationId, executionId, exporter, otelSpan) {
|
|
1446
|
+
super(traceId, spanId, parentSpanId, kind, name, correlationId, executionId, exporter);
|
|
1447
|
+
this.otelSpan = otelSpan;
|
|
1448
|
+
}
|
|
1449
|
+
async end(input) {
|
|
1450
|
+
if (this.isEnded) return;
|
|
1451
|
+
if (input.status === "error") {
|
|
1452
|
+
this.otelSpan.setStatus({ code: SpanStatusCode.ERROR });
|
|
1453
|
+
} else {
|
|
1454
|
+
this.otelSpan.setStatus({ code: SpanStatusCode.OK });
|
|
1455
|
+
}
|
|
1456
|
+
this.otelSpan.end();
|
|
1457
|
+
await super.end(input);
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
// src/http/instrumented-http-client.ts
|
|
1462
|
+
import { Tracing as Tracing2 } from "@nebulaos/core";
|
|
1463
|
+
var InstrumentedHttpClient = class {
|
|
1464
|
+
async fetch(url, options) {
|
|
1465
|
+
const method = options?.method || "GET";
|
|
1466
|
+
const spanName = `http:${method} ${new URL(url).origin}`;
|
|
1467
|
+
return Tracing2.withSpan(
|
|
1468
|
+
{
|
|
1469
|
+
kind: "http",
|
|
1470
|
+
name: spanName,
|
|
1471
|
+
data: {
|
|
1472
|
+
"http.method": method,
|
|
1473
|
+
"http.url": url
|
|
1474
|
+
}
|
|
1475
|
+
},
|
|
1476
|
+
async (span) => {
|
|
1477
|
+
const startTime = Date.now();
|
|
1478
|
+
try {
|
|
1479
|
+
const response = await fetch(url, options);
|
|
1480
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1481
|
+
let data;
|
|
1482
|
+
if (contentType.includes("application/json")) {
|
|
1483
|
+
data = await response.json();
|
|
1484
|
+
} else {
|
|
1485
|
+
data = await response.text();
|
|
1486
|
+
}
|
|
1487
|
+
await span.end({
|
|
1488
|
+
status: response.ok ? "success" : "error",
|
|
1489
|
+
data: {
|
|
1490
|
+
"http.status_code": response.status,
|
|
1491
|
+
"http.response_content_length": response.headers.get("content-length"),
|
|
1492
|
+
"http.duration_ms": Date.now() - startTime
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
return {
|
|
1496
|
+
status: response.status,
|
|
1497
|
+
statusText: response.statusText,
|
|
1498
|
+
headers: response.headers,
|
|
1499
|
+
data
|
|
1500
|
+
};
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
await span.end({
|
|
1503
|
+
status: "error",
|
|
1504
|
+
data: {
|
|
1505
|
+
"http.error": error instanceof Error ? error.message : String(error),
|
|
1506
|
+
"http.duration_ms": Date.now() - startTime
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
throw error;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
async get(url, options) {
|
|
1515
|
+
return this.fetch(url, { ...options, method: "GET" });
|
|
1516
|
+
}
|
|
1517
|
+
async post(url, body, options) {
|
|
1518
|
+
return this.fetch(url, {
|
|
1519
|
+
...options,
|
|
1520
|
+
method: "POST",
|
|
1521
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
1522
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
// src/client.ts
|
|
1528
|
+
var NebulaClient = class {
|
|
1529
|
+
constructor(config) {
|
|
1530
|
+
this.config = config;
|
|
1531
|
+
clientConfigSchema.parse(config);
|
|
1532
|
+
if (!config.clientId) {
|
|
1533
|
+
config.clientId = `client-${uuid2()}`;
|
|
1534
|
+
}
|
|
1535
|
+
if (config.server) {
|
|
1536
|
+
this.setGlobalClientContext(config.server.apiKey, config.server.url);
|
|
1537
|
+
}
|
|
1538
|
+
this.registry = new Registry();
|
|
1539
|
+
this.registry.setHttpClient(new InstrumentedHttpClient());
|
|
1540
|
+
config.agents?.forEach((agent) => this.registry.registerAgent(agent));
|
|
1541
|
+
config.tools?.forEach((tool) => this.registry.registerTool(tool));
|
|
1542
|
+
config.workflows?.forEach((workflow) => this.registry.registerWorkflow(workflow));
|
|
1543
|
+
if (config.server) {
|
|
1544
|
+
const debug = resolveClientDebugConfig({
|
|
1545
|
+
logLevel: config.logLevel,
|
|
1546
|
+
// deprecated:
|
|
1547
|
+
enabled: config.debug?.enabled,
|
|
1548
|
+
level: config.debug?.level
|
|
1549
|
+
});
|
|
1550
|
+
if (debug.enabled) {
|
|
1551
|
+
this.logger = new ConsoleLogger(debug.level, "nebulaos/client");
|
|
1552
|
+
this.logger.info(
|
|
1553
|
+
"Client debug enabled",
|
|
1554
|
+
{ level: debug.level, ws: true, http: true }
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
this.serverConnection = new ServerConnection({
|
|
1558
|
+
url: config.server.url,
|
|
1559
|
+
apiKey: config.server.apiKey,
|
|
1560
|
+
clientId: config.clientId
|
|
1561
|
+
}, this.logger);
|
|
1562
|
+
this.serverConnection.setRegistry(this.registry);
|
|
1563
|
+
this.telemetryExporter = new HttpTelemetryExporter(
|
|
1564
|
+
{
|
|
1565
|
+
cloudUrl: config.server.url,
|
|
1566
|
+
apiKey: config.server.apiKey,
|
|
1567
|
+
clientId: config.clientId
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
timeoutMs: 2e3
|
|
1571
|
+
},
|
|
1572
|
+
this.logger
|
|
1573
|
+
);
|
|
1574
|
+
Tracing3.setExporter(this.telemetryExporter);
|
|
1575
|
+
setupOTelTracing({
|
|
1576
|
+
serviceName: config.clientId ?? "nebulaos-client"
|
|
1577
|
+
});
|
|
1578
|
+
const otelProvider = new OTelTracingProvider(
|
|
1579
|
+
config.clientId ?? "nebulaos-client",
|
|
1580
|
+
this.telemetryExporter
|
|
1581
|
+
);
|
|
1582
|
+
Tracing3.setProvider(otelProvider);
|
|
1583
|
+
this.domainEventsExporter = new HttpDomainEventsExporter(
|
|
1584
|
+
{
|
|
1585
|
+
cloudUrl: config.server.url,
|
|
1586
|
+
apiKey: config.server.apiKey,
|
|
1587
|
+
clientId: config.clientId
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
timeoutMs: 2e3
|
|
1591
|
+
},
|
|
1592
|
+
this.logger
|
|
1593
|
+
);
|
|
1594
|
+
DomainEvents.setExporter(this.domainEventsExporter);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
registry;
|
|
1598
|
+
serverConnection;
|
|
1599
|
+
telemetryExporter;
|
|
1600
|
+
domainEventsExporter;
|
|
1601
|
+
logger;
|
|
1602
|
+
registerAgent(agent) {
|
|
1603
|
+
this.registry.registerAgent(agent);
|
|
1604
|
+
}
|
|
1605
|
+
registerTool(tool) {
|
|
1606
|
+
this.registry.registerTool(tool);
|
|
1607
|
+
}
|
|
1608
|
+
registerWorkflow(workflow) {
|
|
1609
|
+
this.registry.registerWorkflow(workflow);
|
|
1610
|
+
}
|
|
1611
|
+
async start() {
|
|
1612
|
+
if (this.serverConnection) {
|
|
1613
|
+
this.serverConnection.registerResources(
|
|
1614
|
+
this.registry.listAgents(),
|
|
1615
|
+
this.registry.listTools(),
|
|
1616
|
+
this.registry.listWorkflows()
|
|
1617
|
+
);
|
|
1618
|
+
try {
|
|
1619
|
+
await this.serverConnection.connect();
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
if (error instanceof Error) {
|
|
1622
|
+
throw error;
|
|
1623
|
+
}
|
|
1624
|
+
throw new Error(`Failed to connect to NebulaOS Cloud: ${String(error)}`);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
async stop() {
|
|
1629
|
+
await this.serverConnection?.disconnect();
|
|
1630
|
+
}
|
|
1631
|
+
getClientId() {
|
|
1632
|
+
return this.serverConnection?.getClientId();
|
|
1633
|
+
}
|
|
1634
|
+
isConnected() {
|
|
1635
|
+
return this.serverConnection?.isConnected() ?? false;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Sets global client context for skills to access API key and server URL
|
|
1639
|
+
* This allows skills like RagOpenAISkill to automatically use the client's credentials
|
|
1640
|
+
*/
|
|
1641
|
+
setGlobalClientContext(apiKey, serverUrl) {
|
|
1642
|
+
try {
|
|
1643
|
+
import("@nebulaos/rag-openai-skill").then((module) => {
|
|
1644
|
+
if (module.ClientContext) {
|
|
1645
|
+
module.ClientContext.setApiKey(apiKey);
|
|
1646
|
+
module.ClientContext.setServerUrl(serverUrl);
|
|
1647
|
+
}
|
|
1648
|
+
}).catch(() => {
|
|
1649
|
+
});
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
// src/telemetry/telemetry.service.ts
|
|
1656
|
+
var TelemetryService = class {
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
// src/telemetry/batcher.ts
|
|
1660
|
+
var Batcher = class {
|
|
1661
|
+
};
|
|
1662
|
+
|
|
1663
|
+
// src/telemetry/socket-telemetry-exporter.ts
|
|
1664
|
+
var SocketTelemetryExporter = class {
|
|
1665
|
+
constructor(serverConnection, config = {}) {
|
|
1666
|
+
this.serverConnection = serverConnection;
|
|
1667
|
+
this.config = config;
|
|
1668
|
+
}
|
|
1669
|
+
queue = [];
|
|
1670
|
+
flushTimer;
|
|
1671
|
+
flushing = false;
|
|
1672
|
+
async exportBatch(events) {
|
|
1673
|
+
this.queue.push(...events);
|
|
1674
|
+
this.ensureTimer();
|
|
1675
|
+
await this.flushIfNeeded();
|
|
1676
|
+
}
|
|
1677
|
+
async flush() {
|
|
1678
|
+
if (this.flushing) return;
|
|
1679
|
+
if (this.queue.length === 0) return;
|
|
1680
|
+
this.flushing = true;
|
|
1681
|
+
try {
|
|
1682
|
+
const maxBatchSize = this.config.maxBatchSize ?? 50;
|
|
1683
|
+
while (this.queue.length > 0) {
|
|
1684
|
+
const batch = this.queue.splice(0, maxBatchSize);
|
|
1685
|
+
await this.sendTelemetryBatch(batch);
|
|
1686
|
+
}
|
|
1687
|
+
} finally {
|
|
1688
|
+
this.flushing = false;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
stop() {
|
|
1692
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
1693
|
+
this.flushTimer = void 0;
|
|
1694
|
+
}
|
|
1695
|
+
async sendTelemetryBatch(events) {
|
|
1696
|
+
if (!this.serverConnection.isConnected()) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const socket = this.serverConnection.getSocket();
|
|
1700
|
+
socket.emit("telemetry:batch", {
|
|
1701
|
+
clientId: this.serverConnection.getClientId(),
|
|
1702
|
+
events
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
ensureTimer() {
|
|
1706
|
+
if (this.flushTimer) return;
|
|
1707
|
+
const flushIntervalMs = this.config.flushIntervalMs ?? 500;
|
|
1708
|
+
this.flushTimer = setInterval(() => {
|
|
1709
|
+
void this.flush();
|
|
1710
|
+
}, flushIntervalMs);
|
|
1711
|
+
}
|
|
1712
|
+
async flushIfNeeded() {
|
|
1713
|
+
const maxBatchSize = this.config.maxBatchSize ?? 50;
|
|
1714
|
+
if (this.queue.length >= maxBatchSize) {
|
|
1715
|
+
await this.flush();
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
// src/memory/db-memory.ts
|
|
1721
|
+
var DBMemory = class {
|
|
1722
|
+
async get(_key, _sessionId) {
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
async set(_key, _value, _sessionId, _ttl) {
|
|
1726
|
+
}
|
|
1727
|
+
async delete(_key, _sessionId) {
|
|
1728
|
+
}
|
|
1729
|
+
async clear(_sessionId) {
|
|
1730
|
+
}
|
|
1731
|
+
async getHistory(_sessionId, _limit) {
|
|
1732
|
+
return [];
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
// src/prompts/prompt-capture.ts
|
|
1737
|
+
var PromptCapture = class {
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
// src/rag/rag-client.ts
|
|
1741
|
+
var RAGClient = class {
|
|
1742
|
+
};
|
|
1743
|
+
export {
|
|
1744
|
+
AuthenticationError,
|
|
1745
|
+
Batcher,
|
|
1746
|
+
ClientError,
|
|
1747
|
+
ConnectionError,
|
|
1748
|
+
DBMemory,
|
|
1749
|
+
HttpDomainEventsExporter,
|
|
1750
|
+
HttpTelemetryExporter,
|
|
1751
|
+
InstrumentedHttpClient,
|
|
1752
|
+
NebulaClient,
|
|
1753
|
+
NotFoundError,
|
|
1754
|
+
OTelTracingProvider,
|
|
1755
|
+
PromptCapture,
|
|
1756
|
+
RAGClient,
|
|
1757
|
+
RegistryError,
|
|
1758
|
+
SocketTelemetryExporter,
|
|
1759
|
+
TelemetryService,
|
|
1760
|
+
clientConfigSchema,
|
|
1761
|
+
otelTracingConfigSchema,
|
|
1762
|
+
serverConfigSchema,
|
|
1763
|
+
setupOTelTracing
|
|
1764
|
+
};
|
|
1765
|
+
//# sourceMappingURL=index.mjs.map
|