@economic/agents 1.7.3 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -48,8 +48,7 @@ declare function buildLLMParams(config: BuildLLMParamsConfig): LLMParams;
48
48
  //#region src/server/v1/types.d.ts
49
49
  /**
50
50
  * The context object available throughout an agent's lifetime — passed via
51
- * `experimental_context` to tool `execute` functions. Contains the typed
52
- * request body merged with platform capabilities like `logEvent`.
51
+ * `experimental_context` to tool `execute` functions.
53
52
  *
54
53
  * Define your own body shape and compose:
55
54
  * ```typescript
@@ -58,11 +57,12 @@ declare function buildLLMParams(config: BuildLLMParamsConfig): LLMParams;
58
57
  * ```
59
58
  */
60
59
  type AgentToolContext<TBody = Record<string, unknown>, TUserContext = Record<string, unknown> | undefined> = TBody & {
61
- _logEvent: (message: string, payload?: Record<string, unknown>) => void | Promise<void>;
62
60
  _userContext?: TUserContext;
63
61
  };
64
62
  interface AgentEnv {
65
63
  AGENT_DB: D1Database;
64
+ AGENTS_AUDIT_LOGS: R2Bucket;
65
+ AGENTS_ANALYTICS: AnalyticsEngineDataset;
66
66
  }
67
67
  interface ChatAgentEnv extends AgentEnv {}
68
68
  //#endregion
@@ -86,11 +86,10 @@ interface JwtAuthConfig<TClaims extends Record<string, unknown> = Record<string,
86
86
  //#endregion
87
87
  //#region src/server/v1/agent/Agent.d.ts
88
88
  /**
89
- * Base agent for Cloudflare Agents SDK Durable Objects with lazy skill loading,
90
- * audit logging, and `buildLLMParams` wiring.
89
+ * Base agent for Cloudflare Agents SDK Durable Objects with lazy skill loading
90
+ * and `buildLLMParams` wiring.
91
91
  *
92
- * Handles CF infrastructure concerns: DO SQLite persistence for loaded skill state
93
- * and writing audit events to D1.
92
+ * Handles CF infrastructure concerns: DO SQLite persistence for loaded skill state.
94
93
  *
95
94
  * For chat agents with message history, compaction, and conversation recording,
96
95
  * extend {@link ChatAgent} instead.
@@ -123,20 +122,9 @@ declare abstract class Agent<Env extends Cloudflare.Env = Cloudflare.Env, TUserC
123
122
  */
124
123
  protected getUserId(): string;
125
124
  onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
126
- /**
127
- * Writes an audit event to D1 if `AGENT_DB` is bound on the environment,
128
- * otherwise silently does nothing.
129
- *
130
- * Called automatically at the end of each LLM turn (from `onFinish` in
131
- * `buildLLMParams`). Also available via `experimental_context.logEvent` in tool
132
- * `execute` functions.
133
- */
134
- protected logEvent(message: string, payload?: Record<string, unknown>): Promise<void>;
135
125
  /**
136
126
  * Builds the parameter object for a `streamText` or `generateText` call,
137
127
  * pre-filling `activeSkills` from this agent instance.
138
- * Injects `logEvent` into `experimental_context` and wires `onFinish` for
139
- * turn-completed audit events.
140
128
  */
141
129
  protected buildLLMParams<TBody = Record<string, unknown>>(config: Omit<BuildLLMParamsConfig, "messages"> & {
142
130
  options?: OnChatMessageOptions;
@@ -151,12 +139,12 @@ interface MessageRating {
151
139
  //#endregion
152
140
  //#region src/server/v1/agent-chat/ChatAgent.d.ts
153
141
  /**
154
- * Chat agent for Cloudflare Agents SDK: lazy skill loading, audit logging,
155
- * message persistence, compaction, and conversation metadata in D1.
142
+ * Chat agent for Cloudflare Agents SDK: lazy skill loading, message persistence,
143
+ * compaction, and conversation metadata in D1.
156
144
  *
157
145
  * Handles CF infrastructure concerns: DO SQLite for loaded skill state,
158
- * stripping skill meta-tool messages before persistence, history replay to
159
- * newly connected clients, and audit events to D1.
146
+ * stripping skill meta-tool messages before persistence, and history replay to
147
+ * newly connected clients.
160
148
  *
161
149
  * Skill loading, compaction, and LLM calls use `buildLLMParams` from
162
150
  * `@economic/agents` inside `onChatMessage`.
@@ -197,6 +185,8 @@ declare abstract class ChatAgent<Env extends Cloudflare.Env = Cloudflare.Env, TU
197
185
  * Default is 15.
198
186
  */
199
187
  protected maxMessagesBeforeCompaction?: number | undefined;
188
+ protected clientIp?: string;
189
+ protected forwardedFor?: string;
200
190
  /**
201
191
  * Override to enable JWT authentication on WebSocket connections.
202
192
  * Return the auth config based on the incoming request, or undefined to skip auth.
@@ -223,20 +213,9 @@ declare abstract class ChatAgent<Env extends Cloudflare.Env = Cloudflare.Env, TU
223
213
  protected getUserId(): string;
224
214
  onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
225
215
  protected _pendingUserContextRequest?: Promise<void>;
226
- /**
227
- * Writes an audit event to D1 if `AGENT_DB` is bound on the environment,
228
- * otherwise silently does nothing.
229
- *
230
- * Called automatically at the end of each LLM turn (from `onFinish` in
231
- * `buildLLMParams`). Also available via `experimental_context.logEvent` in tool
232
- * `execute` functions.
233
- */
234
- protected logEvent(message: string, payload?: Record<string, unknown>): Promise<void>;
235
216
  /**
236
217
  * Builds the parameter object for a `streamText` or `generateText` call,
237
218
  * pre-filling `messages`, `activeSkills`, and `fastModel` from this agent instance.
238
- * Injects `logEvent` into `experimental_context` and wires `onFinish` for
239
- * turn-completed audit events and conversation recording.
240
219
  *
241
220
  * **Compaction** runs automatically when `fastModel` is set on the class, using
242
221
  * `DEFAULT_MAX_MESSAGES_BEFORE_COMPACTION` (30) as the threshold. Override the
package/dist/index.mjs CHANGED
@@ -2,6 +2,7 @@ import { Output, convertToModelMessages, generateText, jsonSchema, stepCountIs,
2
2
  import { Agent as Agent$1, callable, getCurrentAgent, routeAgentRequest as routeAgentRequest$1 } from "agents";
3
3
  import { AIChatAgent } from "@cloudflare/ai-chat";
4
4
  import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
5
+ import { BasicTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
5
6
  //#region src/server/shared/features/skills/index.ts
6
7
  const TOOL_NAME_ACTIVATE_SKILL = "activate_skill";
7
8
  const TOOL_NAME_LIST_CAPABILITIES = "list_capabilities";
@@ -269,98 +270,6 @@ async function routeAgentRequest(request, env, options) {
269
270
  return response;
270
271
  }
271
272
  //#endregion
272
- //#region src/server/shared/features/audit/audit.ts
273
- /**
274
- * Inserts a single audit event row into the shared `audit_events` D1 table.
275
- *
276
- * Called by `ChatAgentHarness.logEvent()` (and `AgentHarness.logEvent()`). Not intended for direct use.
277
- */
278
- const SENSITIVE_KEYS = /^(password|token|secret|api_key|apikey|authorization|credentials)$/i;
279
- const REDACTED = "[REDACTED]";
280
- /** Deep-clone and redact values for keys that look like secrets (for audit logging). */
281
- function sanitizePayload(value) {
282
- if (value === null || value === void 0) return value;
283
- if (Array.isArray(value)) return value.map(sanitizePayload);
284
- if (typeof value === "object") {
285
- const result = {};
286
- for (const [key, val] of Object.entries(value)) result[key] = SENSITIVE_KEYS.test(key) ? REDACTED : sanitizePayload(val);
287
- return result;
288
- }
289
- return value;
290
- }
291
- async function insertAuditEvent(db, durableObjectName, message, payload) {
292
- await db.prepare(`INSERT INTO audit_events (id, durable_object_name, message, payload, created_at)
293
- VALUES (?, ?, ?, ?, ?)`).bind(crypto.randomUUID(), durableObjectName, message, payload ? JSON.stringify(sanitizePayload(payload)) : null, (/* @__PURE__ */ new Date()).toISOString()).run();
294
- }
295
- function stringifyForSkillScan(output) {
296
- if (typeof output === "string") return output;
297
- if (output === null || output === void 0) return "";
298
- try {
299
- return JSON.stringify(output);
300
- } catch {
301
- return "";
302
- }
303
- }
304
- function forEachStep(event, fn) {
305
- if (event.steps.length > 0) for (const step of event.steps) fn(step);
306
- else fn(event);
307
- }
308
- /**
309
- * User text from provider request body (`contents` is e.g. Gemini format).
310
- */
311
- function extractUserInputFromRequestBody(body) {
312
- const firstContent = body?.contents?.[0];
313
- if (firstContent?.role !== "user" || !firstContent.parts?.length) return "";
314
- return firstContent.parts.map((p) => p.text).filter((t) => typeof t === "string").join(" ").trim();
315
- }
316
- /**
317
- * Builds the payload for a "Turn completed" audit event from the AI SDK
318
- * `OnFinishEvent`.
319
- *
320
- * Returns:
321
- * - `input`: user message text from `request.body.contents[0]` (Gemini-style)
322
- * - `output`: assistant response text (truncated to 200 chars)
323
- * - `toolCalls`: array of { toolName, toolInput, toolOutput }
324
- * - `loadedSkills`: skill names extracted from activate_skill results
325
- */
326
- function buildTurnLogPayload(event) {
327
- const toolCalls = [];
328
- let latestSkills;
329
- const toolOutputs = /* @__PURE__ */ new Map();
330
- forEachStep(event, (step) => {
331
- for (const tr of step.toolResults) toolOutputs.set(tr.toolCallId, tr.output);
332
- });
333
- forEachStep(event, (step) => {
334
- for (const tc of step.toolCalls) toolCalls.push({
335
- name: tc.toolName,
336
- input: tc.input,
337
- output: toolOutputs.get(tc.toolCallId)
338
- });
339
- const considerToolResultForSkills = (toolName, output) => {
340
- if (toolName !== "activate_skill") return;
341
- const s = stringifyForSkillScan(output);
342
- const sentinelIdx = s.indexOf(SKILL_STATE_SENTINEL);
343
- if (sentinelIdx === -1) return;
344
- try {
345
- const stateJson = s.slice(sentinelIdx + 18);
346
- latestSkills = JSON.parse(stateJson);
347
- } catch {}
348
- };
349
- for (const tr of step.toolResults) considerToolResultForSkills(tr.toolName, tr.output);
350
- });
351
- const input = extractUserInputFromRequestBody(event.request?.body);
352
- return {
353
- detail: {
354
- model: event.model.modelId,
355
- tokens: event.usage?.totalTokens
356
- },
357
- loadedSkills: latestSkills ?? [],
358
- toolCalls,
359
- input: input.slice(0, 200),
360
- output: (event.text ?? "").slice(0, 200)
361
- };
362
- }
363
- //#endregion
364
273
  //#region src/server/shared/features/auth/index.ts
365
274
  const jwksByIssuer = /* @__PURE__ */ new Map();
366
275
  function getJwksForIssuer(issuer) {
@@ -461,13 +370,275 @@ async function verifyJwt(request, config) {
461
370
  };
462
371
  }
463
372
  //#endregion
373
+ //#region src/server/shared/features/telemetry/index.ts
374
+ function durationMs(duration) {
375
+ return duration[0] * 1e3 + duration[1] / 1e6;
376
+ }
377
+ function stringAttribute(span, key) {
378
+ const value = span.attributes[key];
379
+ return typeof value === "string" ? value : void 0;
380
+ }
381
+ function numberAttribute(span, key) {
382
+ const value = span.attributes[key];
383
+ return typeof value === "number" ? value : void 0;
384
+ }
385
+ function parseJson(value) {
386
+ if (!value) return;
387
+ try {
388
+ return JSON.parse(value);
389
+ } catch {
390
+ return;
391
+ }
392
+ }
393
+ function safePathSegment(value, fallback) {
394
+ return (value || fallback).replace(/[^a-zA-Z0-9._-]/g, "_");
395
+ }
396
+ function createAuditLogKey(agentName, conversationId) {
397
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
398
+ const id = crypto.randomUUID().slice(0, 8);
399
+ return [
400
+ safePathSegment(agentName, "unknown-agent"),
401
+ safePathSegment(conversationId, "unknown-conversation"),
402
+ `${timestamp}-${id}.json`
403
+ ].join("/");
404
+ }
405
+ function textFromContent(content) {
406
+ if (typeof content === "string") return content;
407
+ if (Array.isArray(content)) return content.map((part) => {
408
+ if (!part || typeof part !== "object") return "";
409
+ const value = part.text;
410
+ return typeof value === "string" ? value : "";
411
+ }).filter(Boolean).join("\n");
412
+ return "";
413
+ }
414
+ function extractPrompt(messages) {
415
+ const firstUserMessage = messages.find((message) => message.role === "user");
416
+ return firstUserMessage ? textFromContent(firstUserMessage.content) : "";
417
+ }
418
+ function promptCharCount(messages) {
419
+ const firstUserMessage = messages.find((message) => message.role === "user");
420
+ return firstUserMessage ? textFromContent(firstUserMessage.content).length : 0;
421
+ }
422
+ function extractTools(messages) {
423
+ const tools = [];
424
+ for (const message of messages) {
425
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
426
+ for (const part of message.content) {
427
+ if (!part || typeof part !== "object") continue;
428
+ const record = part;
429
+ if (record.type !== "tool_use") continue;
430
+ tools.push({
431
+ name: typeof record.name === "string" ? record.name : void 0,
432
+ input: record.input,
433
+ status: "success"
434
+ });
435
+ }
436
+ }
437
+ return tools;
438
+ }
439
+ /**
440
+ * Stores the last ai.streamText.doStream span per conversation.
441
+ * Its ai.prompt.messages contains the full conversation history including all
442
+ * intermediate tool_use + tool_result turns from the agentic loop.
443
+ */
444
+ const lastDoStreamByConversation = /* @__PURE__ */ new Map();
445
+ const toolCallsByParentSpan = /* @__PURE__ */ new Map();
446
+ const toolCallsByConversation = /* @__PURE__ */ new Map();
447
+ const pendingToolCalls = [];
448
+ const currentSkillByConversation = /* @__PURE__ */ new Map();
449
+ function userIndex(userId) {
450
+ return userId;
451
+ }
452
+ function extractSkillName(toolName, input) {
453
+ if (!input || typeof input !== "object") return;
454
+ const record = input;
455
+ if (toolName === "load_context") {
456
+ const key = record.key;
457
+ return typeof key === "string" ? key : void 0;
458
+ }
459
+ if (toolName === "activate_skill") {
460
+ const skill = record.skill ?? record.name ?? record.key;
461
+ return typeof skill === "string" ? skill : void 0;
462
+ }
463
+ }
464
+ function pushToolCall(map, key, toolCall) {
465
+ const existing = map.get(key);
466
+ if (existing) existing.push(toolCall);
467
+ else map.set(key, [toolCall]);
468
+ }
469
+ function rememberToolCall(span) {
470
+ const parentSpanId = span.parentSpanContext?.spanId;
471
+ const toolName = stringAttribute(span, "ai.toolCall.name");
472
+ const input = parseJson(stringAttribute(span, "ai.toolCall.args"));
473
+ const toolCall = {
474
+ name: toolName,
475
+ input,
476
+ status: span.status.code === 0 ? "success" : "error",
477
+ skillName: extractSkillName(toolName, input)
478
+ };
479
+ if (parentSpanId) pushToolCall(toolCallsByParentSpan, parentSpanId, toolCall);
480
+ pendingToolCalls.push(toolCall);
481
+ }
482
+ function attachToolCallsToConversation(span, conversationId) {
483
+ const spanId = span.spanContext().spanId;
484
+ const toolCalls = toolCallsByParentSpan.get(spanId) ?? pendingToolCalls.splice(0);
485
+ if (!toolCalls.length) return;
486
+ toolCallsByParentSpan.delete(spanId);
487
+ const currentSkill = currentSkillByConversation.get(conversationId);
488
+ const attributedToolCalls = toolCalls.map((toolCall) => {
489
+ const skillName = toolCall.skillName ?? currentSkill;
490
+ if (toolCall.skillName) currentSkillByConversation.set(conversationId, toolCall.skillName);
491
+ return {
492
+ ...toolCall,
493
+ ...skillName ? { skillName } : {}
494
+ };
495
+ });
496
+ toolCallsByConversation.set(conversationId, [...toolCallsByConversation.get(conversationId) ?? [], ...attributedToolCalls]);
497
+ }
498
+ function buildAuditLog(span, context) {
499
+ const lastDoStream = lastDoStreamByConversation.get(context.conversationId);
500
+ lastDoStreamByConversation.delete(context.conversationId);
501
+ const promptMessages = parseJson(stringAttribute(lastDoStream ?? span, "ai.prompt.messages"));
502
+ const fallbackPrompt = parseJson(stringAttribute(span, "ai.prompt"));
503
+ const inputMessages = promptMessages ?? fallbackPrompt?.messages ?? [];
504
+ const spanToolCalls = toolCallsByConversation.get(context.conversationId) ?? [];
505
+ toolCallsByConversation.delete(context.conversationId);
506
+ return {
507
+ id: createAuditLogKey(context.agentName, context.conversationId),
508
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
509
+ conversationId: context.conversationId,
510
+ agent: {
511
+ name: context.agentName,
512
+ llm: {
513
+ model: stringAttribute(span, "ai.model.id"),
514
+ provider: stringAttribute(span, "ai.model.provider")
515
+ }
516
+ },
517
+ actor: {
518
+ userId: context.userId,
519
+ ip: {
520
+ client: context.clientIp ?? context.forwardedFor?.split(",").map((ip) => ip.trim()).filter(Boolean)[0],
521
+ forwardedFor: context.forwardedFor?.split(",").map((ip) => ip.trim()).filter(Boolean) ?? []
522
+ }
523
+ },
524
+ prompt: extractPrompt(inputMessages),
525
+ response: stringAttribute(span, "ai.response.text") ?? "",
526
+ status: span.status.code === 0 ? "success" : "error",
527
+ tools: [...extractTools(inputMessages), ...spanToolCalls]
528
+ };
529
+ }
530
+ async function handleAuditSpan(span, auditLogs, context) {
531
+ const auditLog = buildAuditLog(span, context);
532
+ if (!auditLogs) return;
533
+ await auditLogs.put(auditLog.id, JSON.stringify(auditLog, null, 2), { httpMetadata: { contentType: "application/json" } });
534
+ console.log("[AuditLog] Created", auditLog.id);
535
+ }
536
+ function writeAnalyticsDatapoint(analytics, dataPoint) {
537
+ if (!analytics) return;
538
+ try {
539
+ analytics.writeDataPoint(dataPoint);
540
+ } catch (error) {
541
+ console.error("[Agent] Failed to write analytics datapoint", error);
542
+ }
543
+ }
544
+ function handleAnalyticsSpan(span, analytics) {
545
+ if (span.name === "ai.streamText.doStream") {
546
+ const promptMessages = parseJson(stringAttribute(span, "ai.prompt.messages"));
547
+ const responseText = stringAttribute(span, "ai.response.text") ?? "";
548
+ writeAnalyticsDatapoint(analytics, {
549
+ indexes: [userIndex(stringAttribute(span, "ai.telemetry.metadata.userId") ?? "")],
550
+ blobs: [
551
+ "llm_call",
552
+ stringAttribute(span, "ai.telemetry.metadata.agentName") ?? "",
553
+ stringAttribute(span, "ai.telemetry.metadata.conversationId") ?? "",
554
+ stringAttribute(span, "ai.model.id") ?? "",
555
+ stringAttribute(span, "ai.model.provider") ?? "",
556
+ stringAttribute(span, "ai.response.finishReason") ?? ""
557
+ ],
558
+ doubles: [
559
+ numberAttribute(span, "ai.usage.inputTokens") ?? 0,
560
+ numberAttribute(span, "ai.usage.outputTokens") ?? 0,
561
+ numberAttribute(span, "ai.usage.totalTokens") ?? 0,
562
+ numberAttribute(span, "ai.usage.inputTokenDetails.cacheReadTokens") ?? 0,
563
+ numberAttribute(span, "ai.usage.inputTokenDetails.cacheWriteTokens") ?? 0,
564
+ durationMs(span.duration),
565
+ promptCharCount(promptMessages ?? []),
566
+ responseText.length
567
+ ]
568
+ });
569
+ return;
570
+ }
571
+ if (span.name === "ai.toolCall") {
572
+ const toolName = stringAttribute(span, "ai.toolCall.name");
573
+ const toolInput = parseJson(stringAttribute(span, "ai.toolCall.args"));
574
+ const conversationId = stringAttribute(span, "ai.telemetry.metadata.conversationId") ?? "";
575
+ const skillName = extractSkillName(toolName, toolInput) ?? currentSkillByConversation.get(conversationId) ?? "";
576
+ const success = span.status.code === 0;
577
+ writeAnalyticsDatapoint(analytics, {
578
+ indexes: [userIndex(stringAttribute(span, "ai.telemetry.metadata.userId") ?? "")],
579
+ blobs: [
580
+ "tool_call",
581
+ stringAttribute(span, "ai.telemetry.metadata.agentName") ?? "",
582
+ conversationId,
583
+ toolName ?? "",
584
+ skillName,
585
+ success ? "success" : "error"
586
+ ],
587
+ doubles: [
588
+ durationMs(span.duration),
589
+ success ? 1 : 0,
590
+ 0,
591
+ 0,
592
+ 0,
593
+ 0,
594
+ 0,
595
+ 0
596
+ ]
597
+ });
598
+ }
599
+ }
600
+ var AgentSpanExporter = class {
601
+ auditLogs;
602
+ analytics;
603
+ context;
604
+ constructor(auditLogs, analytics, context) {
605
+ this.auditLogs = auditLogs;
606
+ this.analytics = analytics;
607
+ this.context = context;
608
+ }
609
+ export(spans, resultCallback) {
610
+ (async () => {
611
+ try {
612
+ for (const span of spans) if (span.name === "ai.streamText.doStream") {
613
+ lastDoStreamByConversation.set(this.context.conversationId, span);
614
+ attachToolCallsToConversation(span, this.context.conversationId);
615
+ handleAnalyticsSpan(span, this.analytics);
616
+ } else if (span.name === "ai.streamText") await handleAuditSpan(span, this.auditLogs, this.context);
617
+ else if (span.name === "ai.toolCall") {
618
+ rememberToolCall(span);
619
+ handleAnalyticsSpan(span, this.analytics);
620
+ }
621
+ resultCallback({ code: 0 });
622
+ } catch (error) {
623
+ resultCallback({
624
+ code: 1,
625
+ error: error instanceof Error ? error : new Error(String(error))
626
+ });
627
+ }
628
+ })();
629
+ }
630
+ async shutdown() {}
631
+ };
632
+ function createAgentTracer(auditLogs, analytics, context) {
633
+ return new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(new AgentSpanExporter(auditLogs, analytics, context))] }).getTracer("@economic/agents");
634
+ }
635
+ //#endregion
464
636
  //#region src/server/v1/agent/Agent.ts
465
637
  /**
466
- * Base agent for Cloudflare Agents SDK Durable Objects with lazy skill loading,
467
- * audit logging, and `buildLLMParams` wiring.
638
+ * Base agent for Cloudflare Agents SDK Durable Objects with lazy skill loading
639
+ * and `buildLLMParams` wiring.
468
640
  *
469
- * Handles CF infrastructure concerns: DO SQLite persistence for loaded skill state
470
- * and writing audit events to D1.
641
+ * Handles CF infrastructure concerns: DO SQLite persistence for loaded skill state.
471
642
  *
472
643
  * For chat agents with message history, compaction, and conversation recording,
473
644
  * extend {@link ChatAgent} instead.
@@ -490,14 +661,22 @@ var Agent = class extends Agent$1 {
490
661
  return this.name.split(":")[0];
491
662
  }
492
663
  async onConnect(connection, ctx) {
664
+ this.clientIp = ctx.request.headers.get("CF-Connecting-IP") ?? ctx.request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim();
665
+ this.forwardedFor = ctx.request.headers.get("X-Forwarded-For") ?? void 0;
493
666
  if (!this.env.AGENT_DB) {
494
- console.error("[ChatAgent] Connection rejected: no AGENT_DB bound");
495
- connection.close(3e3, "Could not connect to agent, database not found");
667
+ console.error("[Agent] Connection rejected: no AGENT_DB bound");
668
+ connection.close(3e3, "Could not connect to agent");
669
+ return;
670
+ }
671
+ if (!this.env.AGENTS_AUDIT_LOGS) {
672
+ console.error("[Agent] Connection rejected: no AGENTS_AUDIT_LOGS bound. Audit logs are required.");
673
+ connection.close(3e3, "Could not connect to agent");
496
674
  return;
497
675
  }
676
+ if (!this.env.AGENTS_ANALYTICS) console.warn("[Agent] No AGENTS_ANALYTICS bound. Analytics will not be collected.");
498
677
  if (!this.getUserId()) {
499
- console.error("[ChatAgent] Connection rejected: name must be in the format userId:uniqueChatId");
500
- connection.close(3e3, "Could not connect to agent, name is not in correct format");
678
+ console.error("[Agent] Connection rejected: name must be in the format userId:uniqueChatId");
679
+ connection.close(3e3, "Could not connect to agent");
501
680
  return;
502
681
  }
503
682
  if (this.getJwtAuthConfig) {
@@ -507,7 +686,7 @@ var Agent = class extends Agent$1 {
507
686
  try {
508
687
  result = await verifyJwt(ctx.request, config);
509
688
  } catch (error) {
510
- console.error("[ChatAgent] JWT verification error", error);
689
+ console.error("[Agent] JWT verification error", error);
511
690
  connection.close(4001, "Unauthorized");
512
691
  return;
513
692
  }
@@ -525,43 +704,40 @@ var Agent = class extends Agent$1 {
525
704
  return super.onConnect(connection, ctx);
526
705
  }
527
706
  /**
528
- * Writes an audit event to D1 if `AGENT_DB` is bound on the environment,
529
- * otherwise silently does nothing.
530
- *
531
- * Called automatically at the end of each LLM turn (from `onFinish` in
532
- * `buildLLMParams`). Also available via `experimental_context.logEvent` in tool
533
- * `execute` functions.
534
- */
535
- async logEvent(message, payload) {
536
- try {
537
- await insertAuditEvent(this.env.AGENT_DB, this.name, message, payload);
538
- } catch (error) {
539
- console.error("[Agent] Failed to write audit event", error);
540
- }
541
- }
542
- /**
543
707
  * Builds the parameter object for a `streamText` or `generateText` call,
544
708
  * pre-filling `activeSkills` from this agent instance.
545
- * Injects `logEvent` into `experimental_context` and wires `onFinish` for
546
- * turn-completed audit events.
547
709
  */
548
710
  async buildLLMParams(config) {
549
711
  const activeSkills = await getStoredSkills(this.sql.bind(this));
550
712
  const experimental_context = {
551
713
  ...config.experimental_context,
552
714
  ...config.options?.body,
553
- _userContext: this.userContext,
554
- _logEvent: this.logEvent.bind(this)
555
- };
556
- const onFinish = async (event) => {
557
- this.logEvent("Turn completed", buildTurnLogPayload(event));
558
- await config.onFinish?.(event);
715
+ _userContext: this.userContext
559
716
  };
560
717
  return buildLLMParams({
561
718
  ...config,
562
719
  activeSkills,
563
720
  experimental_context,
564
- onFinish
721
+ experimental_telemetry: {
722
+ ...config.experimental_telemetry,
723
+ isEnabled: true,
724
+ tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.env.AGENTS_ANALYTICS, {
725
+ agentName: this.constructor.name,
726
+ conversationId: this.name,
727
+ userId: this.getUserId(),
728
+ ...this.clientIp ? { clientIp: this.clientIp } : {},
729
+ ...this.forwardedFor ? { forwardedFor: this.forwardedFor } : {}
730
+ }),
731
+ metadata: {
732
+ agentName: this.constructor.name,
733
+ version: "v1",
734
+ conversationId: this.name,
735
+ userId: this.getUserId(),
736
+ ...this.clientIp ? { clientIp: this.clientIp } : {},
737
+ ...this.forwardedFor ? { forwardedFor: this.forwardedFor } : {},
738
+ ...config.experimental_telemetry?.metadata
739
+ }
740
+ }
565
741
  });
566
742
  }
567
743
  };
@@ -844,12 +1020,12 @@ async function getMessageRatings(db, durableObjectName) {
844
1020
  //#endregion
845
1021
  //#region src/server/v1/agent-chat/ChatAgent.ts
846
1022
  /**
847
- * Chat agent for Cloudflare Agents SDK: lazy skill loading, audit logging,
848
- * message persistence, compaction, and conversation metadata in D1.
1023
+ * Chat agent for Cloudflare Agents SDK: lazy skill loading, message persistence,
1024
+ * compaction, and conversation metadata in D1.
849
1025
  *
850
1026
  * Handles CF infrastructure concerns: DO SQLite for loaded skill state,
851
- * stripping skill meta-tool messages before persistence, history replay to
852
- * newly connected clients, and audit events to D1.
1027
+ * stripping skill meta-tool messages before persistence, and history replay to
1028
+ * newly connected clients.
853
1029
  *
854
1030
  * Skill loading, compaction, and LLM calls use `buildLLMParams` from
855
1031
  * `@economic/agents` inside `onChatMessage`.
@@ -869,6 +1045,8 @@ var ChatAgent = class extends AIChatAgent {
869
1045
  * Default is 15.
870
1046
  */
871
1047
  maxMessagesBeforeCompaction = 15;
1048
+ clientIp;
1049
+ forwardedFor;
872
1050
  /**
873
1051
  * The user context for the session.
874
1052
  * Define getUserContext to set a user context.
@@ -884,14 +1062,22 @@ var ChatAgent = class extends AIChatAgent {
884
1062
  return this.name.split(":")[0];
885
1063
  }
886
1064
  async onConnect(connection, ctx) {
1065
+ this.clientIp = ctx.request.headers.get("CF-Connecting-IP") ?? ctx.request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim();
1066
+ this.forwardedFor = ctx.request.headers.get("X-Forwarded-For") ?? void 0;
887
1067
  if (!this.env.AGENT_DB) {
888
- console.error("[ChatAgent] Connection rejected: no AGENT_DB bound");
889
- connection.close(3e3, "Could not connect to agent, database not found");
1068
+ console.error("[Agent] Connection rejected: no AGENT_DB bound");
1069
+ connection.close(3e3, "Could not connect to agent");
1070
+ return;
1071
+ }
1072
+ if (!this.env.AGENTS_AUDIT_LOGS) {
1073
+ console.error("[Agent] Connection rejected: no AGENTS_AUDIT_LOGS bound. Audit logs are required.");
1074
+ connection.close(3e3, "Could not connect to agent");
890
1075
  return;
891
1076
  }
1077
+ if (!this.env.AGENTS_ANALYTICS) console.warn("[Agent] No AGENTS_ANALYTICS bound. Analytics will not be collected.");
892
1078
  if (!this.getUserId()) {
893
- console.error("[ChatAgent] Connection rejected: name must be in the format userId:uniqueChatId");
894
- connection.close(3e3, "Could not connect to agent, name is not in correct format");
1079
+ console.error("[Agent] Connection rejected: name must be in the format userId:uniqueChatId");
1080
+ connection.close(3e3, "Could not connect to agent");
895
1081
  return;
896
1082
  }
897
1083
  if (this.getJwtAuthConfig) {
@@ -901,7 +1087,7 @@ var ChatAgent = class extends AIChatAgent {
901
1087
  try {
902
1088
  result = await verifyJwt(ctx.request, config);
903
1089
  } catch (error) {
904
- console.error("[ChatAgent] JWT verification error", error);
1090
+ console.error("[Agent] JWT verification error", error);
905
1091
  connection.close(4001, "Unauthorized");
906
1092
  return;
907
1093
  }
@@ -919,25 +1105,8 @@ var ChatAgent = class extends AIChatAgent {
919
1105
  }
920
1106
  _pendingUserContextRequest;
921
1107
  /**
922
- * Writes an audit event to D1 if `AGENT_DB` is bound on the environment,
923
- * otherwise silently does nothing.
924
- *
925
- * Called automatically at the end of each LLM turn (from `onFinish` in
926
- * `buildLLMParams`). Also available via `experimental_context.logEvent` in tool
927
- * `execute` functions.
928
- */
929
- async logEvent(message, payload) {
930
- try {
931
- await insertAuditEvent(this.env.AGENT_DB, this.name, message, payload);
932
- } catch (error) {
933
- console.error("[ChatAgent] Failed to write audit event", error);
934
- }
935
- }
936
- /**
937
1108
  * Builds the parameter object for a `streamText` or `generateText` call,
938
1109
  * pre-filling `messages`, `activeSkills`, and `fastModel` from this agent instance.
939
- * Injects `logEvent` into `experimental_context` and wires `onFinish` for
940
- * turn-completed audit events and conversation recording.
941
1110
  *
942
1111
  * **Compaction** runs automatically when `fastModel` is set on the class, using
943
1112
  * `DEFAULT_MAX_MESSAGES_BEFORE_COMPACTION` (30) as the threshold. Override the
@@ -949,22 +1118,36 @@ var ChatAgent = class extends AIChatAgent {
949
1118
  const experimental_context = {
950
1119
  ...config.experimental_context,
951
1120
  ...config.options?.body,
952
- _userContext: this.userContext,
953
- _logEvent: this.logEvent.bind(this)
1121
+ _userContext: this.userContext
954
1122
  };
955
1123
  const messages = await convertToModelMessages(this.messages);
956
1124
  const fastModel = this.getFastModel();
957
1125
  const processedMessages = fastModel && this.maxMessagesBeforeCompaction !== void 0 ? await compactIfNeeded(messages, fastModel, this.maxMessagesBeforeCompaction) : messages;
958
- const onFinish = async (event) => {
959
- this.logEvent("Turn completed", buildTurnLogPayload(event));
960
- await config.onFinish?.(event);
961
- };
962
1126
  return buildLLMParams({
963
1127
  ...config,
964
1128
  activeSkills,
965
1129
  messages: processedMessages,
966
1130
  experimental_context,
967
- onFinish
1131
+ experimental_telemetry: {
1132
+ ...config.experimental_telemetry,
1133
+ isEnabled: true,
1134
+ tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.env.AGENTS_ANALYTICS, {
1135
+ agentName: this.constructor.name,
1136
+ conversationId: this.name,
1137
+ userId: this.getUserId(),
1138
+ ...this.clientIp ? { clientIp: this.clientIp } : {},
1139
+ ...this.forwardedFor ? { forwardedFor: this.forwardedFor } : {}
1140
+ }),
1141
+ metadata: {
1142
+ agentName: this.constructor.name,
1143
+ version: "v1",
1144
+ conversationId: this.name,
1145
+ userId: this.getUserId(),
1146
+ ...this.clientIp ? { clientIp: this.clientIp } : {},
1147
+ ...this.forwardedFor ? { forwardedFor: this.forwardedFor } : {},
1148
+ ...config.experimental_telemetry?.metadata
1149
+ }
1150
+ }
968
1151
  });
969
1152
  }
970
1153
  async persistMessages(messages, excludeBroadcastIds = [], options) {
@@ -975,7 +1158,7 @@ var ChatAgent = class extends AIChatAgent {
975
1158
  }
976
1159
  async onChatResponse(result) {
977
1160
  if (result.error) {
978
- console.error("[ChatAgent] Chat response error", result.error);
1161
+ console.error("[Agent] Chat response error", result.error);
979
1162
  return;
980
1163
  }
981
1164
  recordConversationSummary(this.env.AGENT_DB, this.name, this.messages, this.getFastModel());
@@ -992,11 +1175,7 @@ var ChatAgent = class extends AIChatAgent {
992
1175
  }
993
1176
  @callable({ description: "Delete a conversation by its id" }) async deleteConversation(id) {
994
1177
  if (!id.startsWith(`${this.getUserId()}:`)) {
995
- console.error("[ChatAgent] Failed to delete conversation: Not owned by current user", {
996
- conversationName: id,
997
- userId: this.getUserId()
998
- });
999
- this.logEvent("Failed to delete conversation: Not owned by current user", {
1178
+ console.error("[Agent] Failed to delete conversation: Not owned by current user", {
1000
1179
  conversationName: id,
1001
1180
  userId: this.getUserId()
1002
1181
  });
@@ -1005,7 +1184,7 @@ var ChatAgent = class extends AIChatAgent {
1005
1184
  try {
1006
1185
  await deleteConversationRow(this.env.AGENT_DB, id);
1007
1186
  } catch (error) {
1008
- console.error("[ChatAgent] Failed to delete conversation row", {
1187
+ console.error("[Agent] Failed to delete conversation row", {
1009
1188
  conversationName: id,
1010
1189
  error
1011
1190
  });
@@ -1018,12 +1197,12 @@ var ChatAgent = class extends AIChatAgent {
1018
1197
  for (const connection of this.getConnections()) try {
1019
1198
  connection.close(CONVERSATION_EXPIRED_CLOSE_CODE, CONVERSATION_EXPIRED_CLOSE_REASON);
1020
1199
  } catch (error) {
1021
- console.error("[ChatAgent] Failed to close expired conversation connection", error);
1200
+ console.error("[Agent] Failed to close expired conversation connection", error);
1022
1201
  }
1023
1202
  return super.destroy();
1024
1203
  }
1025
1204
  async deleteConversationCallback() {
1026
- if (await this.deleteConversation(this.name)) this.logEvent("Conversation deleted due to inactivity", {
1205
+ if (await this.deleteConversation(this.name)) console.log("[Agent] Conversation deleted due to inactivity", {
1027
1206
  conversationName: this.name,
1028
1207
  retentionDays: this.conversationRetentionDays ?? null
1029
1208
  });
@@ -1061,7 +1240,7 @@ var ChatAgentHarness = class extends ChatAgent {
1061
1240
  return [];
1062
1241
  }
1063
1242
  async onChatMessage(onFinish, options) {
1064
- await this._pendingUserContextRequest;
1243
+ if (this._pendingUserContextRequest) await this._pendingUserContextRequest;
1065
1244
  const ctx = options?.body;
1066
1245
  return streamText(await this.buildLLMParams({
1067
1246
  options,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "1.7.3",
3
+ "version": "1.8.1",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -25,7 +25,8 @@
25
25
  "prepublishOnly": "npm run build"
26
26
  },
27
27
  "dependencies": {
28
- "@clack/prompts": "^1.2.0"
28
+ "@clack/prompts": "^1.2.0",
29
+ "@opentelemetry/sdk-trace-base": "^2.7.1"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@cloudflare/ai-chat": "^0.6.2",
@@ -0,0 +1,5 @@
1
+ -- ─── Audit logging ────────────────────────────────────────────────────────────
2
+
3
+ DROP INDEX IF EXISTS audit_events_do;
4
+ DROP INDEX IF EXISTS audit_events_ts;
5
+ DROP TABLE IF EXISTS audit_events;