@cuylabs/agent-core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +81 -323
  2. package/dist/builder-BKkipazh.d.ts +34 -0
  3. package/dist/capabilities/index.d.ts +97 -0
  4. package/dist/capabilities/index.js +46 -0
  5. package/dist/chunk-3C4VKG4P.js +2149 -0
  6. package/dist/chunk-6TDTQJ4P.js +116 -0
  7. package/dist/chunk-7MUFEN4K.js +559 -0
  8. package/dist/chunk-BDBZ3SLK.js +745 -0
  9. package/dist/chunk-DWYX7ASF.js +26 -0
  10. package/dist/chunk-FG4MD5MU.js +54 -0
  11. package/dist/chunk-IVUJDISU.js +556 -0
  12. package/dist/chunk-LRHOS4ZN.js +584 -0
  13. package/dist/chunk-O2ZCFQL6.js +764 -0
  14. package/dist/chunk-P6YF7USR.js +182 -0
  15. package/dist/chunk-QAQADS4X.js +258 -0
  16. package/dist/chunk-QWFMX226.js +879 -0
  17. package/dist/{chunk-6VKLWNRE.js → chunk-SDSBEQXG.js} +1 -132
  18. package/dist/chunk-VBWWUHWI.js +724 -0
  19. package/dist/chunk-VEKUXUVF.js +41 -0
  20. package/dist/chunk-X635CM2F.js +305 -0
  21. package/dist/chunk-YUUJK53A.js +91 -0
  22. package/dist/chunk-ZXAKHMWH.js +283 -0
  23. package/dist/config-D2xeGEHK.d.ts +52 -0
  24. package/dist/context/index.d.ts +259 -0
  25. package/dist/context/index.js +26 -0
  26. package/dist/identifiers-BLUxFqV_.d.ts +12 -0
  27. package/dist/index-DZQJD_hp.d.ts +1067 -0
  28. package/dist/index-ipP3_ztp.d.ts +198 -0
  29. package/dist/index.d.ts +210 -5736
  30. package/dist/index.js +2132 -7767
  31. package/dist/mcp/index.d.ts +26 -0
  32. package/dist/mcp/index.js +14 -0
  33. package/dist/messages-BYWGn8TY.d.ts +110 -0
  34. package/dist/middleware/index.d.ts +8 -0
  35. package/dist/middleware/index.js +12 -0
  36. package/dist/models/index.d.ts +33 -0
  37. package/dist/models/index.js +12 -0
  38. package/dist/network-D76DS5ot.d.ts +5 -0
  39. package/dist/prompt/index.d.ts +225 -0
  40. package/dist/prompt/index.js +45 -0
  41. package/dist/reasoning/index.d.ts +71 -0
  42. package/dist/reasoning/index.js +47 -0
  43. package/dist/registry-CuRWWtcT.d.ts +164 -0
  44. package/dist/resolver-DOfZ-xuk.d.ts +254 -0
  45. package/dist/runner-G1wxEgac.d.ts +852 -0
  46. package/dist/runtime/index.d.ts +357 -0
  47. package/dist/runtime/index.js +64 -0
  48. package/dist/session-manager-Uawm2Le7.d.ts +274 -0
  49. package/dist/skill/index.d.ts +103 -0
  50. package/dist/skill/index.js +39 -0
  51. package/dist/storage/index.d.ts +167 -0
  52. package/dist/storage/index.js +50 -0
  53. package/dist/sub-agent/index.d.ts +14 -0
  54. package/dist/sub-agent/index.js +15 -0
  55. package/dist/tool/index.d.ts +174 -1
  56. package/dist/tool/index.js +12 -3
  57. package/dist/tool-DYp6-cC3.d.ts +239 -0
  58. package/dist/tool-pFAnJc5Y.d.ts +419 -0
  59. package/dist/tracker-DClqYqTj.d.ts +96 -0
  60. package/dist/tracking/index.d.ts +109 -0
  61. package/dist/tracking/index.js +20 -0
  62. package/dist/types-BWo810L_.d.ts +648 -0
  63. package/dist/types-CQaXbRsS.d.ts +47 -0
  64. package/dist/types-VQgymC1N.d.ts +156 -0
  65. package/package.json +89 -5
  66. package/dist/index-BlSTfS-W.d.ts +0 -470
@@ -0,0 +1,764 @@
1
+ // src/middleware/runner.ts
2
+ function isBlockedModelCall(value) {
3
+ return "block" in value && value.block === true;
4
+ }
5
+ var MiddlewareRunner = class {
6
+ stack;
7
+ constructor(middleware = []) {
8
+ this.stack = Object.freeze([...middleware]);
9
+ }
10
+ /** Number of registered middleware */
11
+ get count() {
12
+ return this.stack.length;
13
+ }
14
+ /** Whether any middleware is registered */
15
+ get hasMiddleware() {
16
+ return this.stack.length > 0;
17
+ }
18
+ /** Get the middleware list (for fork inheritance) */
19
+ getMiddleware() {
20
+ return this.stack;
21
+ }
22
+ // --------------------------------------------------------------------------
23
+ // model.input — array order, first block wins
24
+ // --------------------------------------------------------------------------
25
+ async runModelInput(input, ctx) {
26
+ let current = input;
27
+ for (const mw of this.stack) {
28
+ if (!mw.model?.input) continue;
29
+ try {
30
+ const next = await mw.model.input(current, ctx);
31
+ if (!next) continue;
32
+ if (isBlockedModelCall(next)) {
33
+ return next;
34
+ }
35
+ current = next;
36
+ } catch (err) {
37
+ return {
38
+ block: true,
39
+ reason: `Middleware "${mw.name}" model input error: ${err instanceof Error ? err.message : String(err)}`
40
+ };
41
+ }
42
+ }
43
+ return current;
44
+ }
45
+ // --------------------------------------------------------------------------
46
+ // model.chunk — array order, streamed transformation
47
+ // --------------------------------------------------------------------------
48
+ async runModelChunk(chunk, ctx) {
49
+ let current = chunk;
50
+ for (const mw of this.stack) {
51
+ if (!mw.model?.chunk || current === null) continue;
52
+ try {
53
+ const next = await mw.model.chunk(current, ctx);
54
+ if (next === null) {
55
+ current = null;
56
+ continue;
57
+ }
58
+ if (next !== void 0) {
59
+ current = next;
60
+ }
61
+ } catch (err) {
62
+ console.warn(
63
+ `[middleware] "${mw.name}" model.chunk error:`,
64
+ err instanceof Error ? err.message : String(err)
65
+ );
66
+ }
67
+ }
68
+ return current ?? void 0;
69
+ }
70
+ // --------------------------------------------------------------------------
71
+ // model.output — reverse order (innermost first)
72
+ // --------------------------------------------------------------------------
73
+ async runModelOutput(output, ctx) {
74
+ let current = output;
75
+ for (let i = this.stack.length - 1; i >= 0; i--) {
76
+ const mw = this.stack[i];
77
+ if (!mw.model?.output) continue;
78
+ try {
79
+ const next = await mw.model.output(current, ctx);
80
+ if (next) {
81
+ current = next;
82
+ }
83
+ } catch (err) {
84
+ console.warn(
85
+ `[middleware] "${mw.name}" model.output error:`,
86
+ err instanceof Error ? err.message : String(err)
87
+ );
88
+ }
89
+ }
90
+ return current;
91
+ }
92
+ // --------------------------------------------------------------------------
93
+ // beforeToolCall — array order, first "deny" wins
94
+ // --------------------------------------------------------------------------
95
+ /**
96
+ * Run all `beforeToolCall` hooks in order.
97
+ *
98
+ * Returns `{ action: "allow" }` if all middleware allow (or have no hook).
99
+ * Returns `{ action: "deny", reason }` on first denial — remaining
100
+ * middleware are skipped.
101
+ */
102
+ async runBeforeToolCall(tool, args, ctx) {
103
+ for (const mw of this.stack) {
104
+ if (!mw.beforeToolCall) continue;
105
+ try {
106
+ const decision = await mw.beforeToolCall(tool, args, ctx);
107
+ if (decision.action === "deny") {
108
+ return decision;
109
+ }
110
+ } catch (err) {
111
+ return {
112
+ action: "deny",
113
+ reason: `Middleware "${mw.name}" error: ${err instanceof Error ? err.message : String(err)}`
114
+ };
115
+ }
116
+ }
117
+ return { action: "allow" };
118
+ }
119
+ // --------------------------------------------------------------------------
120
+ // afterToolCall — reverse order (innermost first)
121
+ // --------------------------------------------------------------------------
122
+ /**
123
+ * Run all `afterToolCall` hooks in reverse order.
124
+ *
125
+ * Each hook receives the result from the previous hook (or the
126
+ * original tool result for the first hook). Errors are caught
127
+ * and logged — the original result passes through.
128
+ */
129
+ async runAfterToolCall(tool, args, result, ctx) {
130
+ let current = result;
131
+ for (let i = this.stack.length - 1; i >= 0; i--) {
132
+ const mw = this.stack[i];
133
+ if (!mw.afterToolCall) continue;
134
+ try {
135
+ current = await mw.afterToolCall(tool, args, current, ctx);
136
+ } catch (err) {
137
+ console.warn(
138
+ `[middleware] "${mw.name}" afterToolCall error:`,
139
+ err instanceof Error ? err.message : String(err)
140
+ );
141
+ }
142
+ }
143
+ return current;
144
+ }
145
+ // --------------------------------------------------------------------------
146
+ // promptSections — all run, results merged
147
+ // --------------------------------------------------------------------------
148
+ /**
149
+ * Collect prompt sections from all middleware.
150
+ *
151
+ * Returns a flat array of sections. Each middleware can return a single
152
+ * section, an array of sections, or undefined/empty.
153
+ */
154
+ collectPromptSections(ctx) {
155
+ const sections = [];
156
+ for (const mw of this.stack) {
157
+ if (!mw.promptSections) continue;
158
+ try {
159
+ const result = mw.promptSections(ctx);
160
+ if (!result) continue;
161
+ if (Array.isArray(result)) {
162
+ sections.push(...result);
163
+ } else {
164
+ sections.push(result);
165
+ }
166
+ } catch (err) {
167
+ console.warn(
168
+ `[middleware] "${mw.name}" promptSections error:`,
169
+ err instanceof Error ? err.message : String(err)
170
+ );
171
+ }
172
+ }
173
+ return sections;
174
+ }
175
+ // --------------------------------------------------------------------------
176
+ // onEvent — fire-and-forget, all run
177
+ // --------------------------------------------------------------------------
178
+ /**
179
+ * Broadcast an event to all middleware observers.
180
+ *
181
+ * Non-blocking — errors are caught and logged. This never
182
+ * slows down the streaming pipeline.
183
+ */
184
+ emitEvent(event) {
185
+ for (const mw of this.stack) {
186
+ if (!mw.onEvent) continue;
187
+ try {
188
+ mw.onEvent(event);
189
+ } catch {
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Get the OTel context for a session from the telemetry middleware.
195
+ * Returns undefined if no telemetry middleware is registered.
196
+ */
197
+ getOtelContext(sessionId) {
198
+ for (const mw of this.stack) {
199
+ if (mw.getOtelContext) {
200
+ const ctx = mw.getOtelContext(sessionId);
201
+ if (ctx) return ctx;
202
+ }
203
+ }
204
+ return void 0;
205
+ }
206
+ // --------------------------------------------------------------------------
207
+ // onChatStart — array order, sequential
208
+ // --------------------------------------------------------------------------
209
+ /**
210
+ * Run all `onChatStart` hooks in order.
211
+ *
212
+ * Errors are caught and logged — a broken logger should not
213
+ * prevent the chat from starting.
214
+ */
215
+ async runChatStart(sessionId, message) {
216
+ for (const mw of this.stack) {
217
+ if (!mw.onChatStart) continue;
218
+ try {
219
+ await mw.onChatStart(sessionId, message);
220
+ } catch (err) {
221
+ console.warn(
222
+ `[middleware] "${mw.name}" onChatStart error:`,
223
+ err instanceof Error ? err.message : String(err)
224
+ );
225
+ }
226
+ }
227
+ }
228
+ // --------------------------------------------------------------------------
229
+ // onChatEnd — array order, sequential
230
+ // --------------------------------------------------------------------------
231
+ /**
232
+ * Run all `onChatEnd` hooks in order.
233
+ *
234
+ * Always called, even when the stream errored. Errors in handlers
235
+ * are caught and logged.
236
+ */
237
+ async runChatEnd(sessionId, result) {
238
+ for (const mw of this.stack) {
239
+ if (!mw.onChatEnd) continue;
240
+ try {
241
+ await mw.onChatEnd(sessionId, result);
242
+ } catch (err) {
243
+ console.warn(
244
+ `[middleware] "${mw.name}" onChatEnd error:`,
245
+ err instanceof Error ? err.message : String(err)
246
+ );
247
+ }
248
+ }
249
+ }
250
+ };
251
+
252
+ // src/safety/approval/risk.ts
253
+ var DEFAULT_TOOL_RISKS = {
254
+ read: "safe",
255
+ read_file: "safe",
256
+ grep: "safe",
257
+ glob: "safe",
258
+ list_dir: "safe",
259
+ invoke_agent: "safe",
260
+ wait_agent: "safe",
261
+ close_agent: "safe",
262
+ skill: "safe",
263
+ skill_resource: "safe",
264
+ write: "moderate",
265
+ write_file: "moderate",
266
+ edit: "moderate",
267
+ edit_file: "moderate",
268
+ create_file: "moderate",
269
+ bash: "dangerous",
270
+ shell: "dangerous",
271
+ delete_file: "dangerous",
272
+ remove: "dangerous"
273
+ };
274
+ function getToolRisk(tool, customRisks) {
275
+ if (customRisks?.[tool]) {
276
+ return customRisks[tool];
277
+ }
278
+ if (DEFAULT_TOOL_RISKS[tool]) {
279
+ return DEFAULT_TOOL_RISKS[tool];
280
+ }
281
+ return "moderate";
282
+ }
283
+
284
+ // src/safety/approval/errors.ts
285
+ var ApprovalDeniedError = class extends Error {
286
+ constructor(tool, args, message) {
287
+ super(message || `Operation denied: ${tool}`);
288
+ this.tool = tool;
289
+ this.args = args;
290
+ this.name = "ApprovalDeniedError";
291
+ }
292
+ };
293
+ var ApprovalTimeoutError = class extends Error {
294
+ constructor(tool, timeoutMs) {
295
+ super(`Approval timeout after ${timeoutMs}ms for: ${tool}`);
296
+ this.tool = tool;
297
+ this.timeoutMs = timeoutMs;
298
+ this.name = "ApprovalTimeoutError";
299
+ }
300
+ };
301
+
302
+ // src/safety/approval/patterns.ts
303
+ function matchApprovalPattern(pattern, value) {
304
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
305
+ return new RegExp(`^${regex}$`, "i").test(value);
306
+ }
307
+ function extractApprovalPatterns(tool, args) {
308
+ if (!args || typeof args !== "object") {
309
+ return [tool];
310
+ }
311
+ const record = args;
312
+ if ("path" in record && typeof record.path === "string" || "filePath" in record && typeof record.filePath === "string") {
313
+ const path = record.path ?? record.filePath;
314
+ const dir = path.substring(0, path.lastIndexOf("/") + 1);
315
+ return [dir ? `${dir}*` : path];
316
+ }
317
+ if ("command" in record && typeof record.command === "string") {
318
+ const command = record.command.split(/\s+/)[0];
319
+ return [command];
320
+ }
321
+ if ("pattern" in record && typeof record.pattern === "string") {
322
+ return [record.pattern];
323
+ }
324
+ return [tool];
325
+ }
326
+ function describeApprovalOperation(tool, args) {
327
+ if (!args || typeof args !== "object") {
328
+ return `Execute ${tool}`;
329
+ }
330
+ const record = args;
331
+ switch (tool) {
332
+ case "read":
333
+ case "read_file":
334
+ return `Read file: ${record.path ?? record.filePath}`;
335
+ case "write":
336
+ case "write_file":
337
+ case "create_file":
338
+ return `Write file: ${record.path ?? record.filePath}`;
339
+ case "edit":
340
+ case "edit_file":
341
+ return `Edit file: ${record.path ?? record.filePath}`;
342
+ case "delete_file":
343
+ case "remove":
344
+ return `Delete: ${record.path}`;
345
+ case "bash":
346
+ case "shell": {
347
+ const command = String(record.command);
348
+ return `Run command: ${command.slice(0, 100)}${command.length > 100 ? "..." : ""}`;
349
+ }
350
+ case "grep":
351
+ return `Search for: ${record.pattern}`;
352
+ case "glob":
353
+ return `Find files: ${record.pattern}`;
354
+ default:
355
+ return `${tool}(${JSON.stringify(args).slice(0, 50)}...)`;
356
+ }
357
+ }
358
+
359
+ // src/safety/approval/handler.ts
360
+ var requestCounter = 0;
361
+ function findMatchingRule(rules, tool, patterns) {
362
+ for (let index = rules.length - 1; index >= 0; index--) {
363
+ const rule = rules[index];
364
+ const toolMatches = rule.tool === "*" || matchApprovalPattern(rule.tool, tool);
365
+ const patternMatches = patterns.some(
366
+ (pattern) => matchApprovalPattern(rule.pattern, pattern)
367
+ );
368
+ if (toolMatches && patternMatches) {
369
+ return rule;
370
+ }
371
+ }
372
+ return void 0;
373
+ }
374
+ function createApprovalHandler(config = {}) {
375
+ const {
376
+ defaultAction = "ask",
377
+ timeout = 5 * 60 * 1e3,
378
+ onRequest
379
+ } = config;
380
+ const rules = [...config.rules ?? []];
381
+ const pending = /* @__PURE__ */ new Map();
382
+ async function request(sessionId, tool, args, customRisks) {
383
+ const risk = getToolRisk(tool, customRisks);
384
+ const patterns = extractApprovalPatterns(tool, args);
385
+ const matchingRule = findMatchingRule(rules, tool, patterns);
386
+ if (matchingRule) {
387
+ if (matchingRule.action === "allow") {
388
+ return;
389
+ }
390
+ throw new ApprovalDeniedError(
391
+ tool,
392
+ args,
393
+ `Denied by rule: ${matchingRule.pattern}`
394
+ );
395
+ }
396
+ if (risk === "safe" && defaultAction !== "deny") {
397
+ return;
398
+ }
399
+ if (!onRequest) {
400
+ if (defaultAction === "allow") {
401
+ return;
402
+ }
403
+ if (defaultAction === "deny") {
404
+ throw new ApprovalDeniedError(tool, args);
405
+ }
406
+ throw new ApprovalDeniedError(tool, args, "No approval handler configured");
407
+ }
408
+ const id = `approval-${++requestCounter}-${Date.now()}`;
409
+ const requestData = {
410
+ id,
411
+ sessionId,
412
+ tool,
413
+ args,
414
+ description: describeApprovalOperation(tool, args),
415
+ risk,
416
+ patterns,
417
+ timestamp: Date.now()
418
+ };
419
+ const action = await Promise.race([
420
+ new Promise((resolve, reject) => {
421
+ pending.set(id, { resolve, reject });
422
+ onRequest(requestData).then(resolve).catch(reject).finally(() => pending.delete(id));
423
+ }),
424
+ new Promise((_, reject) => {
425
+ setTimeout(() => {
426
+ pending.delete(id);
427
+ reject(new ApprovalTimeoutError(tool, timeout));
428
+ }, timeout);
429
+ })
430
+ ]);
431
+ switch (action) {
432
+ case "allow":
433
+ return;
434
+ case "deny":
435
+ throw new ApprovalDeniedError(tool, args);
436
+ case "remember":
437
+ for (const pattern of patterns) {
438
+ rules.push({ pattern, tool, action: "allow" });
439
+ }
440
+ return;
441
+ }
442
+ }
443
+ function cancelAll(reason) {
444
+ for (const [id, { reject }] of pending) {
445
+ reject(new Error(reason ?? "Cancelled"));
446
+ pending.delete(id);
447
+ }
448
+ }
449
+ function addRule(rule) {
450
+ rules.push(rule);
451
+ }
452
+ function getRules() {
453
+ return rules;
454
+ }
455
+ function clearSessionRules() {
456
+ rules.length = config.rules?.length ?? 0;
457
+ }
458
+ return {
459
+ request,
460
+ cancelAll,
461
+ addRule,
462
+ getRules,
463
+ clearSessionRules
464
+ };
465
+ }
466
+
467
+ // src/middleware/approval.ts
468
+ function approvalMiddleware(config = {}) {
469
+ const handler = createApprovalHandler(config);
470
+ return {
471
+ name: "approval",
472
+ async beforeToolCall(tool, args, ctx) {
473
+ try {
474
+ await handler.request(ctx.sessionID, tool, args, config.customRisks);
475
+ return { action: "allow" };
476
+ } catch (err) {
477
+ const reason = err instanceof Error ? err.message : `Approval denied: ${tool}`;
478
+ return { action: "deny", reason };
479
+ }
480
+ }
481
+ };
482
+ }
483
+
484
+ // src/middleware/telemetry/otel.ts
485
+ var otelModulePromise;
486
+ function getInputMimeType(value) {
487
+ const trimmed = value.trimStart();
488
+ return trimmed.startsWith("{") || trimmed.startsWith("[") ? "application/json" : "text/plain";
489
+ }
490
+ async function getOtelModule() {
491
+ if (!otelModulePromise) {
492
+ otelModulePromise = import("@opentelemetry/api").then((module) => module).catch(() => null);
493
+ }
494
+ return await otelModulePromise;
495
+ }
496
+ function scheduleSpanTimeout(options) {
497
+ const { spans, key, otel, timeoutMs } = options;
498
+ return setTimeout(() => {
499
+ const entry = spans.get(key);
500
+ if (!entry) {
501
+ return;
502
+ }
503
+ entry.span.setStatus({
504
+ code: otel.SpanStatusCode.ERROR,
505
+ message: "Span timed out (possible leak \u2014 onChatEnd never called)"
506
+ });
507
+ entry.span.end();
508
+ spans.delete(key);
509
+ }, timeoutMs);
510
+ }
511
+ function otelMiddleware(config = {}) {
512
+ const recordInputs = config.recordInputs ?? true;
513
+ const recordOutputs = config.recordOutputs ?? true;
514
+ const agentName = config.agentName ?? "agent";
515
+ const emitToolSpans = config.emitToolSpans ?? true;
516
+ const spanTimeoutMs = config.spanTimeoutMs ?? 5 * 60 * 1e3;
517
+ const chatSpans = /* @__PURE__ */ new Map();
518
+ const toolSpans = /* @__PURE__ */ new Map();
519
+ let otel = null;
520
+ let tracer = null;
521
+ async function ensureTracer() {
522
+ if (tracer) {
523
+ return tracer;
524
+ }
525
+ otel = await getOtelModule();
526
+ if (!otel) {
527
+ return null;
528
+ }
529
+ tracer = otel.trace.getTracer("@cuylabs/agent-core");
530
+ return tracer;
531
+ }
532
+ return {
533
+ name: "opentelemetry",
534
+ getOtelContext(sessionId) {
535
+ return chatSpans.get(sessionId)?.ctx;
536
+ },
537
+ async onChatStart(sessionId, message) {
538
+ const activeTracer = await ensureTracer();
539
+ if (!activeTracer || !otel) {
540
+ return;
541
+ }
542
+ const attributes = {
543
+ "openinference.span.kind": "AGENT",
544
+ "gen_ai.operation.name": "invoke_agent",
545
+ "gen_ai.agent.name": agentName,
546
+ "gen_ai.agent.session_id": sessionId
547
+ };
548
+ if (config.agentDescription) {
549
+ attributes["gen_ai.agent.description"] = config.agentDescription;
550
+ }
551
+ if (recordInputs) {
552
+ const inputValue = message.slice(0, 4096);
553
+ attributes["input.value"] = inputValue;
554
+ attributes["input.mime_type"] = getInputMimeType(inputValue);
555
+ }
556
+ const span = activeTracer.startSpan(`invoke_agent ${agentName}`, {
557
+ attributes
558
+ });
559
+ const ctx = otel.trace.setSpan(otel.context.active(), span);
560
+ chatSpans.set(sessionId, {
561
+ span,
562
+ ctx,
563
+ timer: scheduleSpanTimeout({
564
+ spans: chatSpans,
565
+ key: sessionId,
566
+ otel,
567
+ timeoutMs: spanTimeoutMs
568
+ })
569
+ });
570
+ },
571
+ async beforeToolCall(tool, args, ctx) {
572
+ if (!emitToolSpans) {
573
+ return { action: "allow" };
574
+ }
575
+ const activeTracer = await ensureTracer();
576
+ if (!activeTracer || !otel) {
577
+ return { action: "allow" };
578
+ }
579
+ const sessionId = ctx?.sessionID;
580
+ const parent = sessionId ? chatSpans.get(sessionId) : void 0;
581
+ const parentCtx = parent?.ctx ?? otel.context.active();
582
+ const span = activeTracer.startSpan(
583
+ `execute_tool ${tool}`,
584
+ {
585
+ attributes: {
586
+ "openinference.span.kind": "TOOL",
587
+ "gen_ai.operation.name": "execute_tool",
588
+ "gen_ai.tool.name": tool,
589
+ ...recordInputs && args != null ? (() => {
590
+ const argsString = JSON.stringify(args).slice(0, 4096);
591
+ return {
592
+ "gen_ai.tool.call.arguments": argsString,
593
+ "input.value": argsString,
594
+ "input.mime_type": getInputMimeType(argsString),
595
+ "tool.parameters": argsString
596
+ };
597
+ })() : {}
598
+ }
599
+ },
600
+ parentCtx
601
+ );
602
+ const callId = ctx?.messageID ?? JSON.stringify(args);
603
+ const key = `${tool}:${callId}`;
604
+ const toolCtx = otel.trace.setSpan(parentCtx, span);
605
+ toolSpans.set(key, {
606
+ span,
607
+ ctx: toolCtx,
608
+ timer: scheduleSpanTimeout({
609
+ spans: toolSpans,
610
+ key,
611
+ otel,
612
+ timeoutMs: spanTimeoutMs
613
+ })
614
+ });
615
+ return { action: "allow" };
616
+ },
617
+ async afterToolCall(tool, args, result, ctx) {
618
+ const callId = ctx?.messageID ?? JSON.stringify(args);
619
+ const keyPrefix = `${tool}:`;
620
+ const key = `${tool}:${callId}`;
621
+ const entry = toolSpans.get(key) ?? Array.from(toolSpans.entries()).find(
622
+ ([candidate]) => candidate.startsWith(keyPrefix)
623
+ )?.[1];
624
+ if (entry) {
625
+ if (entry.timer) {
626
+ clearTimeout(entry.timer);
627
+ }
628
+ if (recordOutputs && result.output) {
629
+ const outputValue = (typeof result.output === "string" ? result.output : JSON.stringify(result.output)).slice(0, 4096);
630
+ entry.span.setAttribute("gen_ai.tool.call.result", outputValue);
631
+ entry.span.setAttribute("output.value", outputValue);
632
+ entry.span.setAttribute("output.mime_type", getInputMimeType(outputValue));
633
+ }
634
+ entry.span.setStatus({ code: otel?.SpanStatusCode.OK ?? 1 });
635
+ entry.span.end();
636
+ for (const [candidateKey, candidateEntry] of toolSpans) {
637
+ if (candidateEntry === entry) {
638
+ toolSpans.delete(candidateKey);
639
+ break;
640
+ }
641
+ }
642
+ }
643
+ return result;
644
+ },
645
+ onEvent(event) {
646
+ if (event.type !== "tool-error") {
647
+ return;
648
+ }
649
+ for (const [key, entry] of toolSpans) {
650
+ if (!key.startsWith(`${event.toolName}:`)) {
651
+ continue;
652
+ }
653
+ if (entry.timer) {
654
+ clearTimeout(entry.timer);
655
+ }
656
+ entry.span.setStatus({
657
+ code: otel?.SpanStatusCode.ERROR ?? 2,
658
+ message: event.error
659
+ });
660
+ entry.span.recordException({
661
+ name: "ToolError",
662
+ message: event.error
663
+ });
664
+ entry.span.end();
665
+ toolSpans.delete(key);
666
+ break;
667
+ }
668
+ },
669
+ async onChatEnd(sessionId, result) {
670
+ const entry = chatSpans.get(sessionId);
671
+ if (!entry) {
672
+ return;
673
+ }
674
+ if (entry.timer) {
675
+ clearTimeout(entry.timer);
676
+ }
677
+ if (result.usage) {
678
+ entry.span.setAttributes({
679
+ "gen_ai.usage.input_tokens": result.usage.inputTokens ?? 0,
680
+ "gen_ai.usage.output_tokens": result.usage.outputTokens ?? 0
681
+ });
682
+ }
683
+ if (recordOutputs && result.output) {
684
+ const outputValue = result.output.slice(0, 4096);
685
+ entry.span.setAttribute("gen_ai.output.text", outputValue);
686
+ entry.span.setAttribute("output.value", outputValue);
687
+ entry.span.setAttribute("output.mime_type", getInputMimeType(outputValue));
688
+ }
689
+ if (result.error) {
690
+ entry.span.setStatus({
691
+ code: otel?.SpanStatusCode.ERROR ?? 2,
692
+ message: result.error.message
693
+ });
694
+ entry.span.recordException(result.error);
695
+ } else {
696
+ entry.span.setStatus({ code: otel?.SpanStatusCode.OK ?? 1 });
697
+ }
698
+ entry.span.end();
699
+ chatSpans.delete(sessionId);
700
+ }
701
+ };
702
+ }
703
+
704
+ // src/middleware/telemetry/provider.ts
705
+ function createTelemetryConfig(config) {
706
+ const middleware = otelMiddleware({
707
+ agentName: config.agentName,
708
+ agentDescription: config.agentDescription,
709
+ recordInputs: config.recordInputs,
710
+ recordOutputs: config.recordOutputs,
711
+ emitToolSpans: config.emitToolSpans,
712
+ spanTimeoutMs: config.spanTimeoutMs
713
+ });
714
+ const telemetry = {
715
+ isEnabled: true,
716
+ functionId: config.agentName,
717
+ recordInputs: config.recordInputs,
718
+ recordOutputs: config.recordOutputs
719
+ };
720
+ let providerPromise;
721
+ if (config.spanProcessor) {
722
+ providerPromise = createAndRegisterProvider(
723
+ config.spanProcessor,
724
+ config.serviceName ?? config.agentName
725
+ );
726
+ }
727
+ return {
728
+ middleware,
729
+ telemetry,
730
+ shutdown: async () => {
731
+ if (!providerPromise) {
732
+ return;
733
+ }
734
+ const provider = await providerPromise;
735
+ await provider.forceFlush();
736
+ await provider.shutdown();
737
+ }
738
+ };
739
+ }
740
+ async function createAndRegisterProvider(spanProcessor, serviceName) {
741
+ const [{ NodeTracerProvider }, { resourceFromAttributes }, { ATTR_SERVICE_NAME }] = await Promise.all([
742
+ import("@opentelemetry/sdk-trace-node"),
743
+ import("@opentelemetry/resources"),
744
+ import("@opentelemetry/semantic-conventions")
745
+ ]);
746
+ const provider = new NodeTracerProvider({
747
+ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: serviceName }),
748
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
749
+ spanProcessors: [spanProcessor]
750
+ });
751
+ provider.register();
752
+ return provider;
753
+ }
754
+
755
+ export {
756
+ MiddlewareRunner,
757
+ otelMiddleware,
758
+ createTelemetryConfig,
759
+ getToolRisk,
760
+ ApprovalDeniedError,
761
+ ApprovalTimeoutError,
762
+ createApprovalHandler,
763
+ approvalMiddleware
764
+ };