@economic/agents 2.12.0 → 2.13.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 CHANGED
@@ -640,7 +640,30 @@ When configured:
640
640
  AI telemetry is emitted per turn from the `ai` SDK's OpenTelemetry spans:
641
641
 
642
642
  - **Audit logs** → one JSON object per turn in `AGENTS_AUDIT_LOGS` (model, actor, IP, prompt, response, tool calls).
643
- - **Analytics** → per-turn and per-tool data points in `AGENTS_ANALYTICS`.
643
+ - **Analytics** → typed per-turn and per-tool events. By default these are written to
644
+ `AGENTS_ANALYTICS` using the existing Analytics Engine schema.
645
+
646
+ Additional analytics destinations can receive the same events by overriding
647
+ `getAnalyticsHandlers()`. Analytics Engine remains enabled whenever `AGENTS_ANALYTICS` is bound:
648
+
649
+ ```typescript
650
+ import type { AnalyticsHandler } from "@economic/agents";
651
+
652
+ const warehouse: AnalyticsHandler = {
653
+ name: "warehouse",
654
+ async handle(event) {
655
+ await sendToWarehouse(event);
656
+ },
657
+ };
658
+
659
+ getAnalyticsHandlers() {
660
+ return [warehouse];
661
+ }
662
+ ```
663
+
664
+ Handler failures are isolated and recorded as operational errors. Delivery runs through the
665
+ agent's lifetime management without blocking the response. `prompt.received` includes the raw
666
+ prompt for destinations such as conversation classification; handlers must treat it as sensitive.
644
667
 
645
668
  Separately, the SDK writes structured operational logs for completed/failed LLM and tool calls, turn duration, framework failures, and selected recovery warnings. Prompt, response, tool argument, and tool result content is excluded. These records use the stable fields `source`, `event`, `level`, `timestamp`, `agentName`, `agentClass`, `durableObjectName`, and `attributes`, while preserving Cloudflare's diagnostic events.
646
669
 
@@ -669,21 +692,25 @@ Imported from `@economic/agents` unless noted.
669
692
  | `ToolContext` | `RequestContext & { _userContext?: UserContext }`. |
670
693
  | `AgentEnv` | The bindings the runtime expects. |
671
694
  | `AgentConnectionState` / `AgentConnectionStatus` / `AgentConnectionType` | Connection state shared with the client. |
695
+ | `AnalyticsHandler` / `AnalyticsEvent` | Typed, consumer-configured product-analytics destinations. |
696
+ | `createAnalyticsEngineHandler(dataset)` | Analytics Engine destination using the existing v2 positional schema. |
672
697
 
673
698
  ### Members
674
699
 
675
- | Member | On | Description |
676
- | ---------------------------------------------- | ----------- | -------------------------------------------- |
677
- | `getModel(ctx?)` | `Agent` | Required. Model for inference. |
678
- | `getSystemPrompt(ctx?)` | `Agent` | Required. System prompt. |
679
- | `getTools()` | `Agent` | Always-on tools (default `{}`). |
680
- | `getSkills()` | `Agent` | Local skills (default `[]`). |
681
- | `static getJwtAuthConfig(env)` | `Agent` | Optional JWT verification and actor binding. |
682
- | `getUserContext(jwtToken)` | `Agent` | Optional per-user context after auth. |
683
- | `submitMessageFeedback` / `getMessageFeedback` | `ChatAgent` | Callable message feedback. |
684
- | `agent` | `Assistant` | Required. Your `ChatAgent` subclass. |
685
- | `fastModel` | `Assistant` | Required. Cheap model for titles/summaries. |
686
- | `createChat` / `getChats` / `deleteChat` | `Assistant` | Callable chat management. |
700
+ | Member | On | Description |
701
+ | ---------------------------------------------- | ----------- | ---------------------------------------------------------------------- |
702
+ | `getModel(ctx?)` | `Agent` | Required. Model for inference. |
703
+ | `getSystemPrompt(ctx?)` | `Agent` | Required. System prompt. |
704
+ | `getTools()` | `Agent` | Always-on tools (default `{}`). |
705
+ | `getSkills()` | `Agent` | Local skills (default `[]`). |
706
+ | `getAnalyticsHandlers()` | `Agent` | Additional analytics destinations; Analytics Engine remains automatic. |
707
+ | `getAnalyticsAttributes()` | `Agent` | Additional dimensions included on every analytics event. |
708
+ | `static getJwtAuthConfig(env)` | `Agent` | Optional JWT verification and actor binding. |
709
+ | `getUserContext(jwtToken)` | `Agent` | Optional per-user context after auth. |
710
+ | `submitMessageFeedback` / `getMessageFeedback` | `ChatAgent` | Callable message feedback. |
711
+ | `agent` | `Assistant` | Required. Your `ChatAgent` subclass. |
712
+ | `fastModel` | `Assistant` | Required. Cheap model for titles/summaries. |
713
+ | `createChat` / `getChats` / `deleteChat` | `Assistant` | Callable chat management. |
687
714
 
688
715
  ### Package root (`@economic/agents`)
689
716
 
package/dist/index.d.mts CHANGED
@@ -114,6 +114,102 @@ type Tool<Context extends Record<string, unknown> = Record<string, unknown>, INP
114
114
  };
115
115
  declare function tool<Context extends Record<string, unknown> = Record<string, unknown>, INPUT extends JSONValue | unknown | never = any, OUTPUT extends JSONValue | unknown | never = any>(definition: Tool<Context, INPUT, OUTPUT>): Tool$1<INPUT, OUTPUT>;
116
116
  //#endregion
117
+ //#region src/server/features/analytics/types.d.ts
118
+ type AnalyticsAttribute = string | number | boolean;
119
+ type AnalyticsAttributes = Readonly<Record<string, AnalyticsAttribute>>;
120
+ interface AnalyticsUsage {
121
+ inputTokens: number;
122
+ outputTokens: number;
123
+ totalTokens: number;
124
+ cachedInputTokens?: number;
125
+ cacheWriteTokens?: number;
126
+ reasoningTokens?: number;
127
+ }
128
+ type AnalyticsEventPayload = {
129
+ type: "prompt.received"; /** Raw model messages. Handlers must treat this field as sensitive data. */
130
+ messages: readonly ModelMessage[];
131
+ continuation: boolean;
132
+ } | {
133
+ type: "turn.completed";
134
+ requestId: string;
135
+ status: string;
136
+ continuation: boolean;
137
+ responseSize: number;
138
+ } | {
139
+ type: "llm.completed";
140
+ model: string;
141
+ provider: string;
142
+ finishReason: string;
143
+ usage: AnalyticsUsage;
144
+ durationMs: number;
145
+ inputSize: number;
146
+ responseSize: number;
147
+ } | {
148
+ type: "tool.completed";
149
+ toolCallId?: string;
150
+ toolName: string;
151
+ skillNames: readonly string[];
152
+ success: boolean;
153
+ durationMs: number;
154
+ inputSize: number;
155
+ outputSize: number;
156
+ } | {
157
+ type: "feedback.submitted";
158
+ messageId: string;
159
+ rating: 1 | -1 | 0;
160
+ };
161
+ type AnalyticsEvent = AnalyticsEventPayload & {
162
+ schemaVersion: 1;
163
+ eventId: string;
164
+ timestamp: number;
165
+ agentName: string;
166
+ durableObjectName: string;
167
+ actorId: string;
168
+ attributes: AnalyticsAttributes;
169
+ };
170
+ /** Receives typed product-analytics events emitted by the SDK. */
171
+ interface AnalyticsHandler {
172
+ readonly name?: string;
173
+ handle(event: AnalyticsEvent): void | Promise<void>;
174
+ }
175
+ interface AnalyticsEventContext {
176
+ agentName: string;
177
+ durableObjectName: string;
178
+ actorId: string;
179
+ attributes?: AnalyticsAttributes;
180
+ }
181
+ //#endregion
182
+ //#region src/server/features/analytics/analytics-engine.d.ts
183
+ /**
184
+ * Creates a Cloudflare Analytics Engine destination for SDK analytics events.
185
+ * It preserves the SDK v2 positional schema and never writes prompt content.
186
+ */
187
+ declare function createAnalyticsEngineHandler(dataset: AnalyticsEngineDataset): AnalyticsHandler;
188
+ //#endregion
189
+ //#region src/server/features/analytics/dispatcher.d.ts
190
+ declare function createAnalyticsEvent(context: AnalyticsEventContext, payload: AnalyticsEventPayload): AnalyticsEvent;
191
+ declare function dispatchAnalyticsEvent(handlers: readonly AnalyticsHandler[], event: AnalyticsEvent): Promise<void>;
192
+ //#endregion
193
+ //#region src/server/features/analytics/emitter.d.ts
194
+ interface AnalyticsOptions {
195
+ getContext(): AnalyticsEventContext;
196
+ getHandlers(): readonly AnalyticsHandler[];
197
+ keepAlive<T>(callback: () => Promise<T>): Promise<T>;
198
+ }
199
+ declare class Analytics {
200
+ private readonly options;
201
+ private handlers?;
202
+ constructor(options: AnalyticsOptions);
203
+ private getHandlers;
204
+ private emit;
205
+ get enabled(): boolean;
206
+ get context(): AnalyticsEventContext;
207
+ promptReceived(messages: readonly ModelMessage[], continuation: boolean): void;
208
+ turnCompleted(requestId: string, status: string, continuation: boolean, response: unknown): void;
209
+ feedbackSubmitted(messageId: string, rating: 1 | -1 | 0): void;
210
+ event(payload: AnalyticsEventPayload | (() => AnalyticsEventPayload)): void;
211
+ }
212
+ //#endregion
117
213
  //#region src/server/agents/Agent.d.ts
118
214
  /** What the model would see on its next step — returned by {@link Agent.inspectTurn}. */
119
215
  interface TurnSnapshot {
@@ -305,6 +401,14 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
305
401
  * Best-effort and non-blocking — never allowed to interfere with a turn.
306
402
  */
307
403
  protected registerInstanceActivity(): Promise<void>;
404
+ protected readonly analytics: Analytics;
405
+ /**
406
+ * Additional product-analytics destinations for this agent. Analytics Engine
407
+ * remains enabled whenever `AGENTS_ANALYTICS` is bound.
408
+ */
409
+ getAnalyticsHandlers(): readonly AnalyticsHandler[];
410
+ /** Business dimensions added to every analytics event. */
411
+ getAnalyticsAttributes(): AnalyticsAttributes;
308
412
  observability?: Observability;
309
413
  /**
310
414
  * Resolves the analytics/telemetry agent name to the entry-point agent: the
@@ -455,4 +559,4 @@ declare function routeAgentRequest<Env>(request: Request, env: Env, options?: Ag
455
559
  //#region src/server/index.d.ts
456
560
  declare function getCurrentToolContext(): any;
457
561
  //#endregion
458
- export { type Action, type ActionContext, Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type ExchangedToken, type JwtAuthConfig, type RegisteredAction, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, type UnauthorizedPolicy, type UnauthorizedToolPolicy, action, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
562
+ export { type Action, type ActionContext, Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, type AnalyticsAttribute, type AnalyticsAttributes, type AnalyticsEvent, type AnalyticsEventPayload, type AnalyticsHandler, type AnalyticsUsage, Assistant, ChatAgent, type ExchangedToken, type JwtAuthConfig, type RegisteredAction, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, type UnauthorizedPolicy, type UnauthorizedToolPolicy, action, createAnalyticsEngineHandler, createAnalyticsEvent, createJwtAuthConfig, dispatchAnalyticsEvent, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
package/dist/index.mjs CHANGED
@@ -510,94 +510,35 @@ async function handleAuditSpan(span, auditLogs, context) {
510
510
  //#endregion
511
511
  //#region src/server/features/telemetry/analytics.ts
512
512
  const emittedToolCallIds = /* @__PURE__ */ new Set();
513
- function safeJsonString(value) {
514
- if (value === void 0) return void 0;
513
+ function serializedSize$1(value) {
515
514
  try {
516
- return JSON.stringify(value);
515
+ return JSON.stringify(value)?.length ?? 0;
517
516
  } catch {
518
- return;
519
- }
520
- }
521
- function writeAnalyticsDatapoint(analytics, dataPoint) {
522
- if (!analytics) return;
523
- try {
524
- analytics.writeDataPoint(dataPoint);
525
- } catch (error) {
526
- console.error("[Agent] Failed to write analytics datapoint", error);
517
+ return 0;
527
518
  }
528
519
  }
529
- /**
530
- * Writes a message-feedback rating to Analytics Engine so sentiment can be
531
- * aggregated across all chats without fanning out to every chat DO.
532
- *
533
- * Emitted on every rating change; the dashboard takes the latest rating per
534
- * `messageId` (blob4) so re-rates and clears are idempotent.
535
- */
536
- function recordFeedbackAnalytics(analytics, context, messageId, rating) {
537
- writeAnalyticsDatapoint(analytics, {
538
- indexes: [context.actorId],
539
- blobs: [
540
- "feedback",
541
- context.agentName,
542
- context.durableObjectName,
543
- messageId
544
- ],
545
- doubles: [rating]
546
- });
547
- }
548
- function recordPromptReceivedAnalytics(analytics, context, messages, continuation) {
549
- if (!analytics) return;
550
- writeAnalyticsDatapoint(analytics, {
551
- indexes: [context.actorId],
552
- blobs: [
553
- "prompt_received",
554
- context.agentName,
555
- context.durableObjectName,
556
- String(continuation)
557
- ],
558
- doubles: [safeJsonString(messages)?.length ?? 0]
559
- });
560
- }
561
- function recordTurnCompletedAnalytics(analytics, context, requestId, status, continuation, response) {
562
- if (!analytics) return;
563
- writeAnalyticsDatapoint(analytics, {
564
- indexes: [context.actorId],
565
- blobs: [
566
- "turn_completed",
567
- context.agentName,
568
- context.durableObjectName,
569
- requestId,
570
- status,
571
- String(continuation)
572
- ],
573
- doubles: [safeJsonString(response)?.length ?? 0]
574
- });
575
- }
576
- function handleAnalyticsSpan(span, analytics, context) {
520
+ function handleAnalyticsSpan(span, analytics) {
521
+ if (!analytics.enabled) return;
577
522
  if (span.name === "ai.streamText.doStream") {
578
523
  const promptMessages = parseJson(stringAttribute(span, "ai.prompt.messages"));
579
524
  const responseText = stringAttribute(span, "ai.response.text") ?? "";
580
525
  const responseToolCalls = parseJson(stringAttribute(span, "ai.response.toolCalls"));
581
- writeAnalyticsDatapoint(analytics, {
582
- indexes: [context.actorId],
583
- blobs: [
584
- "llm_call",
585
- context.agentName,
586
- context.durableObjectName,
587
- stringAttribute(span, "ai.model.id") ?? "",
588
- stringAttribute(span, "ai.model.provider") ?? "",
589
- stringAttribute(span, "ai.response.finishReason") ?? ""
590
- ],
591
- doubles: [
592
- numberAttribute(span, "ai.usage.inputTokens") ?? 0,
593
- numberAttribute(span, "ai.usage.outputTokens") ?? 0,
594
- numberAttribute(span, "ai.usage.totalTokens") ?? 0,
595
- numberAttribute(span, "ai.usage.inputTokenDetails.cacheReadTokens") ?? 0,
596
- numberAttribute(span, "ai.usage.inputTokenDetails.cacheWriteTokens") ?? 0,
597
- durationMs(span.duration),
598
- promptCharCount(promptMessages ?? []),
599
- responseText.length
600
- ]
526
+ analytics.event({
527
+ type: "llm.completed",
528
+ model: stringAttribute(span, "ai.model.id") ?? "",
529
+ provider: stringAttribute(span, "ai.model.provider") ?? "",
530
+ finishReason: stringAttribute(span, "ai.response.finishReason") ?? "",
531
+ usage: {
532
+ inputTokens: numberAttribute(span, "ai.usage.inputTokens") ?? 0,
533
+ outputTokens: numberAttribute(span, "ai.usage.outputTokens") ?? 0,
534
+ totalTokens: numberAttribute(span, "ai.usage.totalTokens") ?? 0,
535
+ cachedInputTokens: numberAttribute(span, "ai.usage.inputTokenDetails.cacheReadTokens") ?? void 0,
536
+ cacheWriteTokens: numberAttribute(span, "ai.usage.inputTokenDetails.cacheWriteTokens") ?? void 0,
537
+ reasoningTokens: numberAttribute(span, "ai.usage.reasoningTokens") ?? void 0
538
+ },
539
+ durationMs: durationMs(span.duration),
540
+ inputSize: promptCharCount(promptMessages ?? []),
541
+ responseSize: responseText.length
601
542
  });
602
543
  if (Array.isArray(responseToolCalls)) for (const value of responseToolCalls) {
603
544
  if (!value || typeof value !== "object") continue;
@@ -607,70 +548,39 @@ function handleAnalyticsSpan(span, analytics, context) {
607
548
  const toolName = typeof toolCall.toolName === "string" ? toolCall.toolName : void 0;
608
549
  if (!toolName) continue;
609
550
  const input = toolCall.input;
610
- const argsRaw = safeJsonString(input);
611
- const success = span.status.code === 0;
612
551
  const activatedSkills = extractSkillNames(toolName, input);
613
- const skillNames = activatedSkills.length ? activatedSkills : [currentSkillByChat.get(context.durableObjectName) ?? ""];
552
+ const skillNames = activatedSkills.length ? activatedSkills : [currentSkillByChat.get(analytics.context.durableObjectName) ?? ""];
614
553
  if (toolCallId) emittedToolCallIds.add(toolCallId);
615
- for (const skillName of skillNames) writeAnalyticsDatapoint(analytics, {
616
- indexes: [context.actorId],
617
- blobs: [
618
- "tool_call",
619
- context.agentName,
620
- context.durableObjectName,
621
- toolName,
622
- skillName,
623
- success ? "success" : "error"
624
- ],
625
- doubles: [
626
- 0,
627
- success ? 1 : 0,
628
- argsRaw?.length ?? 0,
629
- 0,
630
- 0,
631
- 0,
632
- 0,
633
- 0
634
- ]
554
+ analytics.event({
555
+ type: "tool.completed",
556
+ toolCallId,
557
+ toolName,
558
+ skillNames,
559
+ success: span.status.code === 0,
560
+ durationMs: 0,
561
+ inputSize: serializedSize$1(input),
562
+ outputSize: 0
635
563
  });
636
564
  }
637
565
  return;
638
566
  }
639
- if (span.name === "ai.toolCall") {
640
- const toolName = stringAttribute(span, "ai.toolCall.name");
641
- const toolCallId = stringAttribute(span, "ai.toolCall.id");
642
- if (toolCallId && emittedToolCallIds.has(toolCallId)) return;
643
- if (toolCallId) emittedToolCallIds.add(toolCallId);
644
- const argsRaw = stringAttribute(span, "ai.toolCall.args");
645
- const toolInput = parseJson(argsRaw);
646
- const success = span.status.code === 0;
647
- const duration = durationMs(span.duration);
648
- const argsSize = argsRaw?.length ?? 0;
649
- const resultSize = stringAttribute(span, "ai.toolCall.result")?.length ?? 0;
650
- const activatedSkills = extractSkillNames(toolName, toolInput);
651
- const skillNames = activatedSkills.length ? activatedSkills : [currentSkillByChat.get(context.durableObjectName) ?? ""];
652
- for (const skillName of skillNames) writeAnalyticsDatapoint(analytics, {
653
- indexes: [context.actorId],
654
- blobs: [
655
- "tool_call",
656
- context.agentName,
657
- context.durableObjectName,
658
- toolName ?? "",
659
- skillName,
660
- success ? "success" : "error"
661
- ],
662
- doubles: [
663
- duration,
664
- success ? 1 : 0,
665
- argsSize,
666
- resultSize,
667
- 0,
668
- 0,
669
- 0,
670
- 0
671
- ]
672
- });
673
- }
567
+ if (span.name !== "ai.toolCall") return;
568
+ const toolCallId = stringAttribute(span, "ai.toolCall.id");
569
+ if (toolCallId && emittedToolCallIds.has(toolCallId)) return;
570
+ if (toolCallId) emittedToolCallIds.add(toolCallId);
571
+ const toolName = stringAttribute(span, "ai.toolCall.name") ?? "";
572
+ const argsRaw = stringAttribute(span, "ai.toolCall.args");
573
+ const activatedSkills = extractSkillNames(toolName, parseJson(argsRaw));
574
+ analytics.event({
575
+ type: "tool.completed",
576
+ toolCallId,
577
+ toolName,
578
+ skillNames: activatedSkills.length ? activatedSkills : [currentSkillByChat.get(analytics.context.durableObjectName) ?? ""],
579
+ success: span.status.code === 0,
580
+ durationMs: durationMs(span.duration),
581
+ inputSize: argsRaw?.length ?? 0,
582
+ outputSize: stringAttribute(span, "ai.toolCall.result")?.length ?? 0
583
+ });
674
584
  }
675
585
  //#endregion
676
586
  //#region src/server/features/observability/index.ts
@@ -692,6 +602,36 @@ function writeOperationalLog(record, logger = console) {
692
602
  else logger.log(message);
693
603
  } catch {}
694
604
  }
605
+ function errorAttributes(error) {
606
+ if (!(error instanceof Error)) return { error: String(error) };
607
+ const causes = [];
608
+ let cause = error.cause;
609
+ while (cause !== void 0 && causes.length < 5) {
610
+ causes.push(cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause));
611
+ cause = cause instanceof Error ? cause.cause : void 0;
612
+ }
613
+ return {
614
+ error: error.message,
615
+ errorType: error.name,
616
+ stack: error.stack,
617
+ ...causes.length ? { causes } : {}
618
+ };
619
+ }
620
+ function logOperationalError(event, error, context = {}) {
621
+ writeOperationalLog({
622
+ source: "@economic/agents",
623
+ event,
624
+ level: "error",
625
+ timestamp: Date.now(),
626
+ agentName: context.agentName,
627
+ agentClass: context.agentClass,
628
+ durableObjectName: context.durableObjectName,
629
+ attributes: {
630
+ ...context.attributes,
631
+ ...errorAttributes(error)
632
+ }
633
+ });
634
+ }
695
635
  function isFailure(event) {
696
636
  return event.type.endsWith("error") || event.type.endsWith("failed") || event.type.endsWith("exhausted") || "error" in event.payload && Boolean(event.payload.error);
697
637
  }
@@ -793,11 +733,11 @@ var AgentSpanExporter = class {
793
733
  if (span.name === "ai.streamText.doStream") {
794
734
  rememberDoStream(span, this.context.durableObjectName);
795
735
  attachToolCallsToChat(span, this.context.durableObjectName);
796
- handleAnalyticsSpan(span, this.analytics, this.context);
736
+ handleAnalyticsSpan(span, this.analytics);
797
737
  } else if (span.name === "ai.streamText") await handleAuditSpan(span, this.auditLogs, this.context);
798
738
  else if (span.name === "ai.toolCall") {
799
739
  rememberToolCall(span);
800
- handleAnalyticsSpan(span, this.analytics, this.context);
740
+ handleAnalyticsSpan(span, this.analytics);
801
741
  }
802
742
  }
803
743
  resultCallback({ code: 0 });
@@ -815,6 +755,254 @@ function createAgentTracer(auditLogs, analytics, context) {
815
755
  return new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(new AgentSpanExporter(auditLogs, analytics, context))] }).getTracer("@economic/agents");
816
756
  }
817
757
  //#endregion
758
+ //#region src/server/features/analytics/utils.ts
759
+ function serializedSize(value) {
760
+ try {
761
+ return JSON.stringify(value)?.length ?? 0;
762
+ } catch {
763
+ return 0;
764
+ }
765
+ }
766
+ //#endregion
767
+ //#region src/server/features/analytics/analytics-engine.ts
768
+ const MAX_INDEX_BYTES = 96;
769
+ const MAX_BLOB_BYTES = 16 * 1024;
770
+ const encoder = new TextEncoder();
771
+ /**
772
+ * Creates a Cloudflare Analytics Engine destination for SDK analytics events.
773
+ * It preserves the SDK v2 positional schema and never writes prompt content.
774
+ */
775
+ function createAnalyticsEngineHandler(dataset) {
776
+ return {
777
+ name: "Cloudflare Analytics Engine",
778
+ async handle(event) {
779
+ for (const point of toAnalyticsEnginePoints(event)) dataset.writeDataPoint(await boundAnalyticsPoint(point));
780
+ }
781
+ };
782
+ }
783
+ async function boundAnalyticsPoint(point) {
784
+ return {
785
+ ...point,
786
+ indexes: await Promise.all(point.indexes.map(boundIndex)),
787
+ blobs: boundBlobs(point.blobs)
788
+ };
789
+ }
790
+ async function boundIndex(value) {
791
+ if (byteLength(value) <= MAX_INDEX_BYTES) return value;
792
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value));
793
+ const suffix = `:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
794
+ return `${truncateUtf8(value, MAX_INDEX_BYTES - byteLength(suffix))}${suffix}`;
795
+ }
796
+ function boundBlobs(values) {
797
+ const blobs = [...values];
798
+ let sizes = blobs.map(byteLength);
799
+ let total = sizes.reduce((sum, size) => sum + size, 0);
800
+ if (total <= MAX_BLOB_BYTES) return blobs;
801
+ const attributesIndex = blobs.length - 2;
802
+ blobs[attributesIndex] = "{}";
803
+ sizes = blobs.map(byteLength);
804
+ total = sizes.reduce((sum, size) => sum + size, 0);
805
+ while (total > MAX_BLOB_BYTES) {
806
+ const largestSize = Math.max(...sizes);
807
+ const largestIndex = sizes.indexOf(largestSize);
808
+ const value = truncateUtf8(blobs[largestIndex] ?? "", largestSize - (total - MAX_BLOB_BYTES));
809
+ const nextSize = byteLength(value);
810
+ blobs[largestIndex] = value;
811
+ sizes[largestIndex] = nextSize;
812
+ total -= largestSize - nextSize;
813
+ }
814
+ return blobs;
815
+ }
816
+ function truncateUtf8(value, maxBytes) {
817
+ let bytes = 0;
818
+ let end = 0;
819
+ for (const character of value) {
820
+ const characterBytes = byteLength(character);
821
+ if (bytes + characterBytes > maxBytes) break;
822
+ bytes += characterBytes;
823
+ end += character.length;
824
+ }
825
+ return value.slice(0, end);
826
+ }
827
+ function byteLength(value) {
828
+ return encoder.encode(value).byteLength;
829
+ }
830
+ function toAnalyticsEnginePoints(event) {
831
+ const common = [
832
+ event.eventId,
833
+ JSON.stringify(event.attributes),
834
+ String(event.schemaVersion)
835
+ ];
836
+ const indexes = [event.actorId];
837
+ switch (event.type) {
838
+ case "prompt.received": return [{
839
+ indexes,
840
+ blobs: [
841
+ "prompt_received",
842
+ event.agentName,
843
+ event.durableObjectName,
844
+ String(event.continuation),
845
+ ...common
846
+ ],
847
+ doubles: [serializedSize(event.messages)]
848
+ }];
849
+ case "turn.completed": return [{
850
+ indexes,
851
+ blobs: [
852
+ "turn_completed",
853
+ event.agentName,
854
+ event.durableObjectName,
855
+ event.requestId,
856
+ event.status,
857
+ String(event.continuation),
858
+ ...common
859
+ ],
860
+ doubles: [event.responseSize]
861
+ }];
862
+ case "llm.completed": return [{
863
+ indexes,
864
+ blobs: [
865
+ "llm_call",
866
+ event.agentName,
867
+ event.durableObjectName,
868
+ event.model,
869
+ event.provider,
870
+ event.finishReason,
871
+ ...common
872
+ ],
873
+ doubles: [
874
+ event.usage.inputTokens,
875
+ event.usage.outputTokens,
876
+ event.usage.totalTokens,
877
+ event.usage.cachedInputTokens ?? 0,
878
+ event.usage.cacheWriteTokens ?? 0,
879
+ event.durationMs,
880
+ event.inputSize,
881
+ event.responseSize,
882
+ event.usage.reasoningTokens ?? 0
883
+ ]
884
+ }];
885
+ case "tool.completed": return (event.skillNames.length ? event.skillNames : [""]).map((skillName) => ({
886
+ indexes,
887
+ blobs: [
888
+ "tool_call",
889
+ event.agentName,
890
+ event.durableObjectName,
891
+ event.toolName,
892
+ skillName,
893
+ event.success ? "success" : "error",
894
+ event.toolCallId ?? "",
895
+ ...common
896
+ ],
897
+ doubles: [
898
+ event.durationMs,
899
+ event.success ? 1 : 0,
900
+ event.inputSize,
901
+ event.outputSize,
902
+ 0,
903
+ 0,
904
+ 0,
905
+ 0
906
+ ]
907
+ }));
908
+ case "feedback.submitted": return [{
909
+ indexes,
910
+ blobs: [
911
+ "feedback",
912
+ event.agentName,
913
+ event.durableObjectName,
914
+ event.messageId,
915
+ ...common
916
+ ],
917
+ doubles: [event.rating]
918
+ }];
919
+ }
920
+ }
921
+ //#endregion
922
+ //#region src/server/features/analytics/dispatcher.ts
923
+ function createAnalyticsEvent(context, payload) {
924
+ return {
925
+ ...payload,
926
+ schemaVersion: 1,
927
+ eventId: crypto.randomUUID(),
928
+ timestamp: Date.now(),
929
+ agentName: context.agentName,
930
+ durableObjectName: context.durableObjectName,
931
+ actorId: context.actorId,
932
+ attributes: context.attributes ?? {}
933
+ };
934
+ }
935
+ async function dispatchAnalyticsEvent(handlers, event) {
936
+ await Promise.all(handlers.map(async (handler) => {
937
+ try {
938
+ await handler.handle(event);
939
+ } catch (error) {
940
+ writeOperationalLog({
941
+ source: "@economic/agents",
942
+ event: "agent.analytics.handler.failed",
943
+ level: "error",
944
+ timestamp: Date.now(),
945
+ agentName: event.agentName,
946
+ durableObjectName: event.durableObjectName,
947
+ attributes: {
948
+ handler: handler.name ?? "handler",
949
+ error: error instanceof Error ? error.message : String(error)
950
+ }
951
+ });
952
+ }
953
+ }));
954
+ }
955
+ //#endregion
956
+ //#region src/server/features/analytics/emitter.ts
957
+ var Analytics = class {
958
+ options;
959
+ handlers;
960
+ constructor(options) {
961
+ this.options = options;
962
+ }
963
+ getHandlers() {
964
+ return this.handlers ??= this.options.getHandlers();
965
+ }
966
+ emit(payload) {
967
+ const handlers = this.getHandlers();
968
+ if (handlers.length === 0) return;
969
+ const eventPayload = typeof payload === "function" ? payload() : payload;
970
+ this.options.keepAlive(() => dispatchAnalyticsEvent(handlers, createAnalyticsEvent(this.context, eventPayload)));
971
+ }
972
+ get enabled() {
973
+ return this.getHandlers().length > 0;
974
+ }
975
+ get context() {
976
+ return this.options.getContext();
977
+ }
978
+ promptReceived(messages, continuation) {
979
+ this.emit({
980
+ type: "prompt.received",
981
+ messages,
982
+ continuation
983
+ });
984
+ }
985
+ turnCompleted(requestId, status, continuation, response) {
986
+ this.emit(() => ({
987
+ type: "turn.completed",
988
+ requestId,
989
+ status,
990
+ continuation,
991
+ responseSize: serializedSize(response)
992
+ }));
993
+ }
994
+ feedbackSubmitted(messageId, rating) {
995
+ this.emit({
996
+ type: "feedback.submitted",
997
+ messageId,
998
+ rating
999
+ });
1000
+ }
1001
+ event(payload) {
1002
+ this.emit(payload);
1003
+ }
1004
+ };
1005
+ //#endregion
818
1006
  //#region src/server/features/registry.ts
819
1007
  /**
820
1008
  * Registers a top-level DO in the global registry.
@@ -964,11 +1152,7 @@ var Agent = class extends Think {
964
1152
  * `returned.tools` into your own `TurnConfig.tools` if you return tools.
965
1153
  */
966
1154
  async beforeTurn(ctx) {
967
- recordPromptReceivedAnalytics(this.env.AGENTS_ANALYTICS, {
968
- agentName: this.getTelemetryAgentName(),
969
- durableObjectName: this.name,
970
- actorId: this.getTelemetryActorId()
971
- }, ctx.messages, ctx.continuation);
1155
+ this.analytics.promptReceived(ctx.messages, ctx.continuation);
972
1156
  this.registerInstanceActivity();
973
1157
  const body = await this._applyTokenExchange(ctx.body);
974
1158
  this._turn.context = this.getToolContextForCurrentTurn(body);
@@ -987,7 +1171,7 @@ var Agent = class extends Think {
987
1171
  experimental_transform: buildSourcesTransform(),
988
1172
  experimental_telemetry: {
989
1173
  isEnabled: true,
990
- tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.env.AGENTS_ANALYTICS, {
1174
+ tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.analytics, {
991
1175
  agentName: this.getTelemetryAgentName(),
992
1176
  agentClass: this.constructor.name,
993
1177
  durableObjectName: this.name,
@@ -1346,7 +1530,12 @@ var Agent = class extends Think {
1346
1530
  try {
1347
1531
  result = await verifyJwt(request, config);
1348
1532
  } catch (error) {
1349
- console.error(`[Agent] JWT verification error - ${error}`);
1533
+ logOperationalError("agent.authentication.failed", error, {
1534
+ agentName: this.getTelemetryAgentName(),
1535
+ agentClass: this.constructor.name,
1536
+ durableObjectName: this.name,
1537
+ attributes: { stage: "jwt-verification" }
1538
+ });
1350
1539
  return {
1351
1540
  success: false,
1352
1541
  status: 401,
@@ -1369,7 +1558,12 @@ var Agent = class extends Think {
1369
1558
  try {
1370
1559
  exchanged = await exchangeToken(token, exchangeConfig);
1371
1560
  } catch (error) {
1372
- console.error(`[Agent] Token exchange error - ${error}`);
1561
+ logOperationalError("agent.authentication.failed", error, {
1562
+ agentName: this.getTelemetryAgentName(),
1563
+ agentClass: this.constructor.name,
1564
+ durableObjectName: this.name,
1565
+ attributes: { stage: "token-exchange" }
1566
+ });
1373
1567
  return {
1374
1568
  success: false,
1375
1569
  status: 500,
@@ -1377,7 +1571,12 @@ var Agent = class extends Think {
1377
1571
  };
1378
1572
  }
1379
1573
  if (!exchanged.success) {
1380
- console.error(`[Agent] Token exchange failed - ${exchanged.message}`);
1574
+ logOperationalError("agent.authentication.failed", new Error(exchanged.message), {
1575
+ agentName: this.getTelemetryAgentName(),
1576
+ agentClass: this.constructor.name,
1577
+ durableObjectName: this.name,
1578
+ attributes: { stage: "token-exchange" }
1579
+ });
1381
1580
  return {
1382
1581
  success: false,
1383
1582
  status: exchanged.subjectTokenRejected ? 401 : 500,
@@ -1395,7 +1594,12 @@ var Agent = class extends Think {
1395
1594
  userContext: this.userContext
1396
1595
  };
1397
1596
  } catch (error) {
1398
- console.error(`[Agent] getUserContext error - ${error}`);
1597
+ logOperationalError("agent.authentication.failed", error, {
1598
+ agentName: this.getTelemetryAgentName(),
1599
+ agentClass: this.constructor.name,
1600
+ durableObjectName: this.name,
1601
+ attributes: { stage: "user-context" }
1602
+ });
1399
1603
  const closeCode = getConnectCloseCode(error);
1400
1604
  return {
1401
1605
  success: false,
@@ -1459,7 +1663,11 @@ var Agent = class extends Think {
1459
1663
  createdAt: Date.now()
1460
1664
  });
1461
1665
  } catch (error) {
1462
- console.error("[Agent] Failed to register in agent_registry", error);
1666
+ logOperationalError("agent.registry.registration.failed", error, {
1667
+ agentName: this.getTelemetryAgentName(),
1668
+ agentClass: this.constructor.name,
1669
+ durableObjectName: this.name
1670
+ });
1463
1671
  }
1464
1672
  }
1465
1673
  /**
@@ -1485,9 +1693,34 @@ var Agent = class extends Think {
1485
1693
  updatedAt: Date.now()
1486
1694
  });
1487
1695
  } catch (error) {
1488
- console.error("[Agent] Failed to update agent_registry activity", error);
1696
+ logOperationalError("agent.registry.activity.failed", error, {
1697
+ agentName: this.getTelemetryAgentName(),
1698
+ agentClass: this.constructor.name,
1699
+ durableObjectName: this.name
1700
+ });
1489
1701
  }
1490
1702
  }
1703
+ analytics = new Analytics({
1704
+ getContext: () => ({
1705
+ agentName: this.getTelemetryAgentName(),
1706
+ durableObjectName: this.name,
1707
+ actorId: this.getTelemetryActorId(),
1708
+ attributes: this.getAnalyticsAttributes()
1709
+ }),
1710
+ getHandlers: () => [...this.env.AGENTS_ANALYTICS ? [createAnalyticsEngineHandler(this.env.AGENTS_ANALYTICS)] : [], ...this.getAnalyticsHandlers()],
1711
+ keepAlive: (callback) => this.keepAliveWhile(callback)
1712
+ });
1713
+ /**
1714
+ * Additional product-analytics destinations for this agent. Analytics Engine
1715
+ * remains enabled whenever `AGENTS_ANALYTICS` is bound.
1716
+ */
1717
+ getAnalyticsHandlers() {
1718
+ return [];
1719
+ }
1720
+ /** Business dimensions added to every analytics event. */
1721
+ getAnalyticsAttributes() {
1722
+ return {};
1723
+ }
1491
1724
  observability = createWorkerObservability({ getIdentity: () => ({
1492
1725
  agentName: this.getTelemetryAgentName(),
1493
1726
  agentClass: this.constructor.name
@@ -1884,7 +2117,12 @@ var Assistant = class extends Agent$1 {
1884
2117
  try {
1885
2118
  result = await verifyJwt(request, config);
1886
2119
  } catch (error) {
1887
- console.error(`[Assistant] JWT verification error - ${error}`);
2120
+ logOperationalError("agent.authentication.failed", error, {
2121
+ agentName: this.constructor.name,
2122
+ agentClass: this.constructor.name,
2123
+ durableObjectName: this.name,
2124
+ attributes: { stage: "jwt-verification" }
2125
+ });
1888
2126
  return {
1889
2127
  success: false,
1890
2128
  status: 401,
@@ -1896,13 +2134,18 @@ var Assistant = class extends Agent$1 {
1896
2134
  durableObjectName: this.name,
1897
2135
  parentPath: this.parentPath
1898
2136
  })) {
1899
- console.error("[Assistant] Authenticated actor does not own the requested assistant", {
1900
- authenticatedActorId: config.getActorId(result.claims),
1901
- requiredActorId: getTargetActorId({
1902
- durableObjectName: this.name,
1903
- parentPath: this.parentPath
1904
- }),
1905
- durableObjectName: this.name
2137
+ logOperationalError("agent.authentication.failed", /* @__PURE__ */ new Error("Authenticated actor does not own the requested assistant"), {
2138
+ agentName: this.constructor.name,
2139
+ agentClass: this.constructor.name,
2140
+ durableObjectName: this.name,
2141
+ attributes: {
2142
+ stage: "actor-authorization",
2143
+ authenticatedActorId: config.getActorId(result.claims),
2144
+ requiredActorId: getTargetActorId({
2145
+ durableObjectName: this.name,
2146
+ parentPath: this.parentPath
2147
+ })
2148
+ }
1906
2149
  });
1907
2150
  return {
1908
2151
  success: false,
@@ -1940,9 +2183,11 @@ var Assistant = class extends Agent$1 {
1940
2183
  try {
1941
2184
  await this.deleteSubAgent(this.agent, id);
1942
2185
  } catch (cleanupError) {
1943
- console.error("[Assistant] Failed to clean up an unscheduled chat facet", {
1944
- durableObjectName: id,
1945
- error: cleanupError
2186
+ logOperationalError("assistant.chat.cleanup.failed", cleanupError, {
2187
+ agentName: this.constructor.name,
2188
+ agentClass: this.constructor.name,
2189
+ durableObjectName: this.name,
2190
+ attributes: { chatName: id }
1946
2191
  });
1947
2192
  }
1948
2193
  throw error;
@@ -1973,16 +2218,20 @@ var Assistant = class extends Agent$1 {
1973
2218
  touchChat(this.sql.bind(this), durableObjectName);
1974
2219
  this.keepAliveWhile(async () => {
1975
2220
  await Promise.all([summariseChatWithAI(this.sql.bind(this), durableObjectName, messages, this.fastModel), this.scheduleChatForAutoDeletion(durableObjectName).catch((error) => {
1976
- console.error("[Assistant] Failed to reschedule chat deletion", {
1977
- durableObjectName,
1978
- error
2221
+ logOperationalError("assistant.chat.reschedule.failed", error, {
2222
+ agentName: this.constructor.name,
2223
+ agentClass: this.constructor.name,
2224
+ durableObjectName: this.name,
2225
+ attributes: { chatName: durableObjectName }
1979
2226
  });
1980
2227
  })]);
1981
2228
  await this.syncChatToGlobalList(durableObjectName);
1982
2229
  }).catch((error) => {
1983
- console.error("[Assistant] Failed to record chat metadata", {
1984
- durableObjectName,
1985
- error
2230
+ logOperationalError("assistant.chat.metadata.failed", error, {
2231
+ agentName: this.constructor.name,
2232
+ agentClass: this.constructor.name,
2233
+ durableObjectName: this.name,
2234
+ attributes: { chatName: durableObjectName }
1986
2235
  });
1987
2236
  });
1988
2237
  }
@@ -2024,7 +2273,11 @@ var Assistant = class extends Agent$1 {
2024
2273
  createdAt: Date.now()
2025
2274
  });
2026
2275
  } catch (error) {
2027
- console.error("[Assistant] Failed to register in agent_registry", error);
2276
+ logOperationalError("agent.registry.registration.failed", error, {
2277
+ agentName: this.constructor.name,
2278
+ agentClass: this.constructor.name,
2279
+ durableObjectName: this.name
2280
+ });
2028
2281
  }
2029
2282
  }
2030
2283
  /**
@@ -2041,7 +2294,11 @@ var Assistant = class extends Agent$1 {
2041
2294
  updatedAt
2042
2295
  });
2043
2296
  } catch (error) {
2044
- console.error("[Assistant] Failed to update agent_registry activity", error);
2297
+ logOperationalError("agent.registry.activity.failed", error, {
2298
+ agentName: this.constructor.name,
2299
+ agentClass: this.constructor.name,
2300
+ durableObjectName: this.name
2301
+ });
2045
2302
  }
2046
2303
  }
2047
2304
  async syncChatToGlobalList(durableObjectName) {
@@ -2443,11 +2700,7 @@ var ChatAgent = class extends Agent {
2443
2700
  return config;
2444
2701
  }
2445
2702
  async onChatResponse(result) {
2446
- recordTurnCompletedAnalytics(this.env.AGENTS_ANALYTICS, {
2447
- agentName: this.getTelemetryAgentName(),
2448
- durableObjectName: this.name,
2449
- actorId: this.getTelemetryActorId()
2450
- }, result.requestId, result.status, result.continuation, result.message.parts);
2703
+ this.analytics.turnCompleted(result.requestId, result.status, result.continuation, result.message.parts);
2451
2704
  const parent = await this.getParentAgent();
2452
2705
  if (parent?.recordChatTurn) await parent.recordChatTurn(this.name, this.messages);
2453
2706
  }
@@ -2519,18 +2772,15 @@ var ChatAgent = class extends Agent {
2519
2772
  async submitMessageFeedback(messageId, rating, comment) {
2520
2773
  const result = submitMessageFeedback(this.sql.bind(this), messageId, rating, comment);
2521
2774
  const parent = await this.getParentAgent();
2522
- recordFeedbackAnalytics(this.env.AGENTS_ANALYTICS, {
2523
- agentName: this.getTelemetryAgentName(),
2524
- durableObjectName: this.name,
2525
- actorId: this.getTelemetryActorId()
2526
- }, messageId, rating);
2775
+ if (rating === 1 || rating === -1 || rating === 0) this.analytics.feedbackSubmitted(messageId, rating);
2527
2776
  if (parent?.recordChatFeedback) try {
2528
2777
  await parent.recordChatFeedback(this.name, messageId, rating);
2529
2778
  } catch (error) {
2530
- console.error("[ChatAgent] Failed to update parent chat feedback summary", {
2779
+ logOperationalError("agent.feedback.summary.failed", error, {
2780
+ agentName: this.getTelemetryAgentName(),
2781
+ agentClass: this.constructor.name,
2531
2782
  durableObjectName: this.name,
2532
- messageId,
2533
- error
2783
+ attributes: { messageId }
2534
2784
  });
2535
2785
  }
2536
2786
  return result;
@@ -2620,4 +2870,4 @@ function getCurrentToolContext() {
2620
2870
  return getCurrentAgent().agent._lastBody;
2621
2871
  }
2622
2872
  //#endregion
2623
- export { Agent, Assistant, ChatAgent, action, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
2873
+ export { Agent, Assistant, ChatAgent, action, createAnalyticsEngineHandler, createAnalyticsEvent, createJwtAuthConfig, dispatchAnalyticsEvent, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [