@economic/agents 2.14.3 → 2.15.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
@@ -651,30 +651,41 @@ When configured:
651
651
  AI telemetry is emitted per turn from the `ai` SDK's OpenTelemetry spans:
652
652
 
653
653
  - **Audit logs** → one JSON object per turn in `AGENTS_AUDIT_LOGS` (model, actor, IP, prompt, response, tool calls).
654
- - **Analytics** → typed per-turn and per-tool events. By default these are written to
655
- `AGENTS_ANALYTICS` using the existing Analytics Engine schema.
654
+ - **Analytics** → typed per-turn and per-tool events, written to `AGENTS_ANALYTICS` using the
655
+ existing Analytics Engine schema.
656
+ - **Tracing** → completed turns and message feedback, handed to the handlers you return from
657
+ `getTracingHandlers()`.
656
658
 
657
- Additional analytics destinations can receive the same events by overriding
658
- `getAnalyticsHandlers()`. Analytics Engine remains enabled whenever `AGENTS_ANALYTICS` is bound:
659
+ Override `getTracingHandlers()` to send turns and message feedback to a tracing or analytics
660
+ service. Each handler's `handle` receives a `TracingEvent`, a `turn` or a `feedback` event told
661
+ apart by `type`. Both carry their OpenTelemetry `spans`, so a handler that only forwards spans
662
+ doesn't need to check:
659
663
 
660
664
  ```typescript
661
- import type { AnalyticsHandler } from "@economic/agents";
665
+ import type { TracingHandler } from "@economic/agents";
662
666
 
663
- const warehouse: AnalyticsHandler = {
664
- name: "warehouse",
665
- async handle(event) {
666
- await sendToWarehouse(event);
667
- },
667
+ const tracing: TracingHandler = {
668
+ name: "tracing-service",
669
+ handle: (event) => sendToTracingService(event.spans),
668
670
  };
669
671
 
670
- getAnalyticsHandlers() {
671
- return [warehouse];
672
+ getTracingHandlers() {
673
+ return [tracing];
672
674
  }
673
675
  ```
674
676
 
675
- Handler failures are isolated and recorded as operational errors. Delivery runs through the
676
- agent's lifetime management without blocking the response. `prompt.received` includes the raw
677
- prompt for destinations such as conversation classification; handlers must treat it as sensitive.
677
+ A `turn` event comes when a turn ends: who it was for, its request `context` (as tools see it) and
678
+ `spans`, the `ai` SDK's OpenTelemetry spans under the turn's `ai.streamText` span. A turn
679
+ continuing an earlier one, e.g. after a tool approval, sits under that turn in the same trace. A
680
+ `feedback` event comes when a user rates an assistant message: `messageId`, `rating`, `comment` and
681
+ `spans`, the rating as a `gen_ai.evaluation.*` span under the turn that first answered the message
682
+ (none when the rating was removed).
683
+ Hand the spans to any OpenTelemetry exporter or span processor as they are; `examples/assistant`
684
+ has a complete handler.
685
+
686
+ Spans and `context` can hold prompts, tool results and credentials, so pick what you send. Handlers
687
+ run in the background; a failure is logged as `agent.tracing.handler.failed` and never affects the
688
+ chat.
678
689
 
679
690
  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.
680
691
 
@@ -703,8 +714,7 @@ Imported from `@economic/agents` unless noted.
703
714
  | `ToolContext` | `RequestContext & { _userContext?: UserContext }`. |
704
715
  | `AgentEnv` | The bindings the runtime expects. |
705
716
  | `AgentConnectionState` / `AgentConnectionStatus` / `AgentConnectionType` | Connection state shared with the client. |
706
- | `AnalyticsHandler` / `AnalyticsEvent` | Typed, consumer-configured product-analytics destinations. |
707
- | `createAnalyticsEngineHandler(dataset)` | Analytics Engine destination using the existing v2 positional schema. |
717
+ | `TracingHandler` / `TracingEvent` | Consumer-defined destinations for completed turns and message feedback. |
708
718
 
709
719
  ### Members
710
720
 
@@ -714,7 +724,7 @@ Imported from `@economic/agents` unless noted.
714
724
  | `getSystemPrompt(ctx?)` | `Agent` | Required. System prompt. |
715
725
  | `getTools()` | `Agent` | Always-on tools (default `{}`). |
716
726
  | `getSkills()` | `Agent` | Local skills (default `[]`). |
717
- | `getAnalyticsHandlers()` | `Agent` | Additional analytics destinations; Analytics Engine remains automatic. |
727
+ | `getTracingHandlers()` | `Agent` | Destinations for completed turns and message feedback (default: `[]`). |
718
728
  | `getAnalyticsAttributes()` | `Agent` | Additional dimensions included on every analytics event. |
719
729
  | `static getJwtAuthConfig(env)` | `Agent` | Optional JWT verification and actor binding. |
720
730
  | `getUserContext(jwtToken)` | `Agent` | Optional per-user context after auth. |
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Agent as Agent$1, AgentOptions, Connection, ConnectionContext, SubAgentClass } from "agents";
2
2
  import { Action as Action$1, ActionApprovalPolicy, ActionConfig, ActionContext as ActionContext$1, ActionIdempotencyKey, ActionPermissionSpec, ChatResponseResult, PrepareStepContext, Session, Think, ToolCallContext, ToolCallDecision, TurnConfig, TurnContext, skills } from "@cloudflare/think";
3
3
  import { FlexibleSchema, JSONValue, LanguageModel, ModelMessage, Tool as Tool$1, ToolExecutionOptions, UIMessage } from "ai";
4
+ import { ReadableSpan } from "@opentelemetry/sdk-trace-base";
4
5
  import { Observability } from "agents/observability";
5
6
  import { createCompactFunction } from "agents/experimental/memory/utils";
6
7
  import { SkillSource as SkillSource$1 } from "agents/skills";
@@ -114,6 +115,76 @@ type Tool<Context extends Record<string, unknown> = Record<string, unknown>, INP
114
115
  };
115
116
  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
117
  //#endregion
118
+ //#region src/server/features/telemetry/exchange.d.ts
119
+ interface SpanIds {
120
+ traceId: string;
121
+ spanId: string;
122
+ }
123
+ interface MessageRating {
124
+ messageId: string;
125
+ /** 1 thumbs up, -1 thumbs down, 0 rating removed. */
126
+ rating: 1 | -1 | 0;
127
+ comment?: string;
128
+ }
129
+ interface TurnExchange extends SpanIds {
130
+ /** An earlier turn already answered the user message, e.g. before a tool approval. */
131
+ continues: boolean;
132
+ }
133
+ //#endregion
134
+ //#region src/server/features/telemetry/tracing.d.ts
135
+ interface TracingIdentity {
136
+ agentName: string;
137
+ /** The Durable Object — the chat, for a `ChatAgent`. */
138
+ durableObjectName: string;
139
+ actorId: string;
140
+ }
141
+ /** A completed turn and the OpenTelemetry spans the `ai` SDK recorded for it. */
142
+ interface TracedTurn extends TracingIdentity {
143
+ type: "turn";
144
+ /** The turn's request context, as tools receive it. Can hold credentials. */
145
+ context: Readonly<Record<string, unknown>>;
146
+ /**
147
+ * The turn's `ai.streamText` span and, under it, its model and tool calls. A
148
+ * turn continuing an earlier one, e.g. after a tool approval, sits under that
149
+ * turn's span in the same trace.
150
+ */
151
+ spans: readonly ReadableSpan[];
152
+ }
153
+ /** A user's rating of an assistant message, given any time after the turn. */
154
+ interface TracedFeedback extends TracingIdentity, MessageRating {
155
+ type: "feedback";
156
+ /**
157
+ * The rating as a `gen_ai.evaluation.*` span under the turn that first
158
+ * answered the message's user message. Empty when the rating was removed.
159
+ */
160
+ spans: readonly ReadableSpan[];
161
+ }
162
+ type TracingEvent = TracedTurn | TracedFeedback;
163
+ /** Receives completed turns and message feedback, e.g. for a tracing service. */
164
+ interface TracingHandler {
165
+ readonly name?: string;
166
+ handle(event: TracingEvent): void | Promise<void>;
167
+ }
168
+ /** What a turn's tracer needs: where its spans go and the exchange it belongs to. */
169
+ interface TurnTracing {
170
+ onTurnSpans(spans: readonly ReadableSpan[]): void;
171
+ exchange?: TurnExchange;
172
+ }
173
+ interface TracingOptions {
174
+ getIdentity(): TracingIdentity;
175
+ getHandlers(): readonly TracingHandler[];
176
+ keepAlive<T>(callback: () => Promise<T>): Promise<T>;
177
+ }
178
+ /** Hands completed turns and message ratings to an agent's tracing handlers, in the background. */
179
+ declare class Tracing {
180
+ private readonly options;
181
+ constructor(options: TracingOptions);
182
+ /** Nothing when no handlers are configured. */
183
+ turn(context: Readonly<Record<string, unknown>>, messages: readonly UIMessage[]): Promise<TurnTracing | undefined>;
184
+ feedback(messages: readonly UIMessage[], rating: MessageRating): void;
185
+ private sendFeedback;
186
+ }
187
+ //#endregion
117
188
  //#region src/server/features/analytics/types.d.ts
118
189
  type AnalyticsAttribute = string | number | boolean;
119
190
  type AnalyticsAttributes = Readonly<Record<string, AnalyticsAttribute>>;
@@ -163,20 +234,6 @@ type AnalyticsEventPayload = {
163
234
  messageId: string;
164
235
  rating: 1 | -1 | 0;
165
236
  };
166
- type AnalyticsEvent = AnalyticsEventPayload & {
167
- schemaVersion: 1;
168
- eventId: string;
169
- timestamp: number;
170
- agentName: string;
171
- durableObjectName: string;
172
- actorId: string;
173
- attributes: AnalyticsAttributes;
174
- };
175
- /** Receives typed product-analytics events emitted by the SDK. */
176
- interface AnalyticsHandler {
177
- readonly name?: string;
178
- handle(event: AnalyticsEvent): void | Promise<void>;
179
- }
180
237
  interface AnalyticsEventContext {
181
238
  agentName: string;
182
239
  durableObjectName: string;
@@ -184,28 +241,16 @@ interface AnalyticsEventContext {
184
241
  attributes?: AnalyticsAttributes;
185
242
  }
186
243
  //#endregion
187
- //#region src/server/features/analytics/analytics-engine.d.ts
188
- /**
189
- * Creates a Cloudflare Analytics Engine destination for SDK analytics events.
190
- * It preserves the SDK v2 positional schema and never writes prompt content.
191
- */
192
- declare function createAnalyticsEngineHandler(dataset: AnalyticsEngineDataset): AnalyticsHandler;
193
- //#endregion
194
- //#region src/server/features/analytics/dispatcher.d.ts
195
- declare function createAnalyticsEvent(context: AnalyticsEventContext, payload: AnalyticsEventPayload): AnalyticsEvent;
196
- declare function dispatchAnalyticsEvent(handlers: readonly AnalyticsHandler[], event: AnalyticsEvent): Promise<void>;
197
- //#endregion
198
244
  //#region src/server/features/analytics/emitter.d.ts
199
245
  interface AnalyticsOptions {
200
246
  getContext(): AnalyticsEventContext;
201
- getHandlers(): readonly AnalyticsHandler[];
247
+ /** Where events are written; analytics is off without it. */
248
+ getDataset(): AnalyticsEngineDataset | undefined;
202
249
  keepAlive<T>(callback: () => Promise<T>): Promise<T>;
203
250
  }
204
251
  declare class Analytics {
205
252
  private readonly options;
206
- private handlers?;
207
253
  constructor(options: AnalyticsOptions);
208
- private getHandlers;
209
254
  private emit;
210
255
  get enabled(): boolean;
211
256
  get context(): AnalyticsEventContext;
@@ -419,11 +464,12 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
419
464
  */
420
465
  protected registerInstanceActivity(): Promise<void>;
421
466
  protected readonly analytics: Analytics;
467
+ protected readonly tracing: Tracing;
422
468
  /**
423
- * Additional product-analytics destinations for this agent. Analytics Engine
424
- * remains enabled whenever `AGENTS_ANALYTICS` is bound.
469
+ * Destinations for completed turns and message feedback, e.g. a tracing
470
+ * service. They run in the background; a failing one never affects the chat.
425
471
  */
426
- getAnalyticsHandlers(): readonly AnalyticsHandler[];
472
+ getTracingHandlers(): readonly TracingHandler[];
427
473
  /** Business dimensions added to every analytics event. */
428
474
  getAnalyticsAttributes(): AnalyticsAttributes;
429
475
  observability?: Observability;
@@ -594,4 +640,4 @@ declare function routeAgentRequest<Env>(request: Request, env: Env, options?: Ag
594
640
  //#region src/server/index.d.ts
595
641
  declare function getCurrentToolContext(): any;
596
642
  //#endregion
597
- 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 InvocableSkill, 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 };
643
+ export { type Action, type ActionContext, Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, type AnalyticsAttribute, type AnalyticsAttributes, Assistant, ChatAgent, type ExchangedToken, type InvocableSkill, type JwtAuthConfig, type RegisteredAction, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TracedFeedback, type TracedTurn, type TracingEvent, type TracingHandler, type TurnSnapshot, type UnauthorizedPolicy, type UnauthorizedToolPolicy, action, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
package/dist/index.mjs CHANGED
@@ -2,7 +2,8 @@ import { Agent as Agent$1, callable, getCurrentAgent, routeAgentRequest as route
2
2
  import { Think, action as action$1, skills } from "@cloudflare/think";
3
3
  import { Output, convertToModelMessages, generateText, jsonSchema, pruneMessages, tool as tool$1 } from "ai";
4
4
  import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
5
- import { BasicTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
5
+ import { BasicTracerProvider, InMemorySpanExporter, RandomIdGenerator, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
6
+ import { ROOT_CONTEXT, TraceFlags, context, trace } from "@opentelemetry/api";
6
7
  import { genericObservability } from "agents/observability";
7
8
  import { applyToolUpdate, toolApprovalUpdate } from "agents/chat";
8
9
  import { createCompactFunction, estimateMessageTokens, estimateStringTokens, sanitizeToolPairs, truncateOlderMessages } from "agents/experimental/memory/utils";
@@ -584,6 +585,119 @@ function handleAnalyticsSpan(span, analytics) {
584
585
  });
585
586
  }
586
587
  //#endregion
588
+ //#region src/server/features/telemetry/exchange.ts
589
+ async function getExchangeSpanIds(chatName, userMessageId) {
590
+ const bytes = new TextEncoder().encode(`${chatName}:${userMessageId}`);
591
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
592
+ const hex = Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
593
+ return {
594
+ traceId: hex.slice(0, 32),
595
+ spanId: hex.slice(32, 48)
596
+ };
597
+ }
598
+ /**
599
+ * The exchange a turn belongs to: the last user message. The first turn
600
+ * answering it is the root of the exchange's trace; later turns, e.g. after a
601
+ * tool approval, continue that trace under it.
602
+ */
603
+ async function getTurnExchange(chatName, messages) {
604
+ const userMessage = messages.findLast((message) => message.role === "user");
605
+ if (!userMessage) return;
606
+ return {
607
+ ...await getExchangeSpanIds(chatName, userMessage.id),
608
+ continues: messages.at(-1) !== userMessage
609
+ };
610
+ }
611
+ /**
612
+ * Gives a turn's `ai.streamText` span its exchange's ids. On a per-turn tracer
613
+ * that span is the first one started and the only one without a parent.
614
+ */
615
+ var TurnIdGenerator = class {
616
+ exchange;
617
+ random = new RandomIdGenerator();
618
+ turnSpanId;
619
+ constructor(exchange) {
620
+ this.exchange = exchange;
621
+ this.turnSpanId = exchange.spanId;
622
+ }
623
+ generateTraceId() {
624
+ return this.exchange.traceId;
625
+ }
626
+ generateSpanId() {
627
+ const spanId = this.turnSpanId ?? this.random.generateSpanId();
628
+ this.turnSpanId = void 0;
629
+ return spanId;
630
+ }
631
+ };
632
+ /**
633
+ * A rating as a span under the turn that answered the rated message's user
634
+ * message, described with OpenTelemetry's GenAI evaluation attributes. None
635
+ * when the rating was removed or the message has no user message before it.
636
+ */
637
+ async function createFeedbackSpan(chatName, messages, { messageId, rating, comment }) {
638
+ const index = messages.findIndex((message) => message.id === messageId);
639
+ const userMessage = messages.slice(0, Math.max(index, 0)).findLast((message) => message.role === "user");
640
+ if (!userMessage || rating === 0) return;
641
+ const exporter = new InMemorySpanExporter();
642
+ const provider = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] });
643
+ const parent = trace.setSpanContext(ROOT_CONTEXT, {
644
+ ...await getExchangeSpanIds(chatName, userMessage.id),
645
+ traceFlags: TraceFlags.SAMPLED,
646
+ isRemote: true
647
+ });
648
+ const attributes = {
649
+ "gen_ai.evaluation.name": "user_feedback",
650
+ "gen_ai.evaluation.score.value": rating === 1 ? 1 : 0,
651
+ "gen_ai.evaluation.score.label": rating === 1 ? "positive" : "negative",
652
+ ...comment ? { "gen_ai.evaluation.explanation": comment } : {}
653
+ };
654
+ provider.getTracer("@economic/agents").startSpan("user_feedback", { attributes }, parent).end();
655
+ await provider.forceFlush();
656
+ return exporter.getFinishedSpans()[0];
657
+ }
658
+ //#endregion
659
+ //#region src/server/features/telemetry/turn-tracer.ts
660
+ const TURN_SPAN_NAME = "ai.streamText";
661
+ /**
662
+ * Starts every span of a turn as a child of the turn's `ai.streamText` span.
663
+ *
664
+ * The `ai` SDK takes a span's parent from OpenTelemetry's active context, and
665
+ * nothing tracks that here, so each of its spans would otherwise start a trace
666
+ * of its own. A tracer is created per turn, so the turn span is known. With a
667
+ * `parent`, the turn span itself starts under that span, e.g. the first turn
668
+ * of the exchange it continues.
669
+ */
670
+ var TurnTracer = class {
671
+ tracer;
672
+ parent;
673
+ turnSpan;
674
+ constructor(tracer, parent) {
675
+ this.tracer = tracer;
676
+ this.parent = parent;
677
+ }
678
+ startSpan(name, options, ctx = context.active()) {
679
+ if (name === TURN_SPAN_NAME) {
680
+ const parent = this.parent && {
681
+ traceId: this.parent.traceId,
682
+ spanId: this.parent.spanId,
683
+ traceFlags: TraceFlags.SAMPLED,
684
+ isRemote: true
685
+ };
686
+ this.turnSpan = this.tracer.startSpan(name, options, parent ? trace.setSpanContext(ctx, parent) : trace.deleteSpan(ctx));
687
+ return this.turnSpan;
688
+ }
689
+ const parent = this.turnSpan ? trace.setSpan(ctx, this.turnSpan) : ctx;
690
+ return this.tracer.startSpan(name, options, parent);
691
+ }
692
+ startActiveSpan(name, ...args) {
693
+ const fn = args[args.length - 1];
694
+ const options = args.length > 1 ? args[0] : void 0;
695
+ const ctx = args.length > 2 ? args[1] : context.active();
696
+ const span = this.startSpan(name, options, ctx);
697
+ return context.with(trace.setSpan(ctx, span), () => fn(span));
698
+ }
699
+ };
700
+ //#endregion
587
701
  //#region src/server/features/observability/index.ts
588
702
  const measuredEvents = /* @__PURE__ */ new Set(["chat:turn:finish"]);
589
703
  const warningEvents = /* @__PURE__ */ new Set([
@@ -721,16 +835,20 @@ var AgentSpanExporter = class {
721
835
  auditLogs;
722
836
  analytics;
723
837
  context;
724
- constructor(auditLogs, analytics, context) {
838
+ onTurnSpans;
839
+ turnSpans = [];
840
+ constructor(auditLogs, analytics, context, onTurnSpans) {
725
841
  this.auditLogs = auditLogs;
726
842
  this.analytics = analytics;
727
843
  this.context = context;
844
+ this.onTurnSpans = onTurnSpans;
728
845
  }
729
846
  export(spans, resultCallback) {
730
847
  (async () => {
731
848
  try {
732
849
  for (const span of spans) {
733
850
  logAISpan(span, this.context);
851
+ this.collectTurnSpan(span);
734
852
  if (span.name === "ai.streamText.doStream") {
735
853
  rememberDoStream(span, this.context.durableObjectName);
736
854
  attachToolCallsToChat(span, this.context.durableObjectName);
@@ -751,9 +869,84 @@ var AgentSpanExporter = class {
751
869
  })();
752
870
  }
753
871
  async shutdown() {}
872
+ collectTurnSpan(span) {
873
+ if (!this.onTurnSpans) return;
874
+ this.turnSpans.push(span);
875
+ if (span.name !== "ai.streamText") return;
876
+ const spans = this.turnSpans;
877
+ this.turnSpans = [];
878
+ this.onTurnSpans(spans);
879
+ }
754
880
  };
755
- function createAgentTracer(auditLogs, analytics, context) {
756
- return new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(new AgentSpanExporter(auditLogs, analytics, context))] }).getTracer("@economic/agents");
881
+ function createAgentTracer(auditLogs, analytics, context, tracing) {
882
+ const exchange = tracing?.exchange;
883
+ return new TurnTracer(new BasicTracerProvider({
884
+ ...exchange && !exchange.continues ? { idGenerator: new TurnIdGenerator(exchange) } : {},
885
+ spanProcessors: [new SimpleSpanProcessor(new AgentSpanExporter(auditLogs, analytics, context, tracing?.onTurnSpans))]
886
+ }).getTracer("@economic/agents"), exchange?.continues ? exchange : void 0);
887
+ }
888
+ //#endregion
889
+ //#region src/server/features/telemetry/tracing.ts
890
+ /** Hands completed turns and message ratings to an agent's tracing handlers, in the background. */
891
+ var Tracing = class {
892
+ options;
893
+ constructor(options) {
894
+ this.options = options;
895
+ }
896
+ /** Nothing when no handlers are configured. */
897
+ async turn(context, messages) {
898
+ try {
899
+ const handlers = this.options.getHandlers();
900
+ if (handlers.length === 0) return;
901
+ const identity = this.options.getIdentity();
902
+ return {
903
+ exchange: await getTurnExchange(identity.durableObjectName, messages),
904
+ onTurnSpans: (spans) => {
905
+ this.options.keepAlive(() => dispatch(handlers, {
906
+ type: "turn",
907
+ ...identity,
908
+ context,
909
+ spans
910
+ }));
911
+ }
912
+ };
913
+ } catch (error) {
914
+ logOperationalError("agent.tracing.failed", error);
915
+ return;
916
+ }
917
+ }
918
+ feedback(messages, rating) {
919
+ this.sendFeedback(messages, rating).catch((error) => {
920
+ logOperationalError("agent.tracing.failed", error);
921
+ });
922
+ }
923
+ async sendFeedback(messages, rating) {
924
+ const handlers = this.options.getHandlers();
925
+ if (handlers.length === 0) return;
926
+ const identity = this.options.getIdentity();
927
+ await this.options.keepAlive(async () => {
928
+ const span = await createFeedbackSpan(identity.durableObjectName, messages, rating);
929
+ await dispatch(handlers, {
930
+ type: "feedback",
931
+ ...identity,
932
+ ...rating,
933
+ spans: span ? [span] : []
934
+ });
935
+ });
936
+ }
937
+ };
938
+ async function dispatch(handlers, event) {
939
+ await Promise.all(handlers.map(async (handler) => {
940
+ try {
941
+ await handler.handle(event);
942
+ } catch (error) {
943
+ logOperationalError("agent.tracing.handler.failed", error, {
944
+ agentName: event.agentName,
945
+ durableObjectName: event.durableObjectName,
946
+ attributes: { handler: handler.name ?? "handler" }
947
+ });
948
+ }
949
+ }));
757
950
  }
758
951
  //#endregion
759
952
  //#region src/server/features/analytics/utils.ts
@@ -770,16 +963,11 @@ const MAX_INDEX_BYTES = 96;
770
963
  const MAX_BLOB_BYTES = 16 * 1024;
771
964
  const encoder = new TextEncoder();
772
965
  /**
773
- * Creates a Cloudflare Analytics Engine destination for SDK analytics events.
774
- * It preserves the SDK v2 positional schema and never writes prompt content.
966
+ * Writes an SDK analytics event to Cloudflare Analytics Engine. It preserves
967
+ * the SDK v2 positional schema and never writes prompt content.
775
968
  */
776
- function createAnalyticsEngineHandler(dataset) {
777
- return {
778
- name: "Cloudflare Analytics Engine",
779
- async handle(event) {
780
- for (const point of toAnalyticsEnginePoints(event)) dataset.writeDataPoint(await boundAnalyticsPoint(point));
781
- }
782
- };
969
+ async function writeAnalyticsEvent(dataset, event) {
970
+ for (const point of toAnalyticsEnginePoints(event)) dataset.writeDataPoint(await boundAnalyticsPoint(point));
783
971
  }
784
972
  async function boundAnalyticsPoint(point) {
785
973
  return {
@@ -933,58 +1121,29 @@ function toAnalyticsEnginePoints(event) {
933
1121
  }
934
1122
  }
935
1123
  //#endregion
936
- //#region src/server/features/analytics/dispatcher.ts
937
- function createAnalyticsEvent(context, payload) {
938
- return {
939
- ...payload,
940
- schemaVersion: 1,
941
- eventId: crypto.randomUUID(),
942
- timestamp: Date.now(),
943
- agentName: context.agentName,
944
- durableObjectName: context.durableObjectName,
945
- actorId: context.actorId,
946
- attributes: context.attributes ?? {}
947
- };
948
- }
949
- async function dispatchAnalyticsEvent(handlers, event) {
950
- await Promise.all(handlers.map(async (handler) => {
951
- try {
952
- await handler.handle(event);
953
- } catch (error) {
954
- writeOperationalLog({
955
- source: "@economic/agents",
956
- event: "agent.analytics.handler.failed",
957
- level: "error",
958
- timestamp: Date.now(),
959
- agentName: event.agentName,
960
- durableObjectName: event.durableObjectName,
961
- attributes: {
962
- handler: handler.name ?? "handler",
963
- error: error instanceof Error ? error.message : String(error)
964
- }
965
- });
966
- }
967
- }));
968
- }
969
- //#endregion
970
1124
  //#region src/server/features/analytics/emitter.ts
971
1125
  var Analytics = class {
972
1126
  options;
973
- handlers;
974
1127
  constructor(options) {
975
1128
  this.options = options;
976
1129
  }
977
- getHandlers() {
978
- return this.handlers ??= this.options.getHandlers();
979
- }
980
1130
  emit(payload) {
981
- const handlers = this.getHandlers();
982
- if (handlers.length === 0) return;
983
- const eventPayload = typeof payload === "function" ? payload() : payload;
984
- this.options.keepAlive(() => dispatchAnalyticsEvent(handlers, createAnalyticsEvent(this.context, eventPayload)));
1131
+ const dataset = this.options.getDataset();
1132
+ if (!dataset) return;
1133
+ const event = createEvent(this.context, typeof payload === "function" ? payload() : payload);
1134
+ this.options.keepAlive(async () => {
1135
+ try {
1136
+ await writeAnalyticsEvent(dataset, event);
1137
+ } catch (error) {
1138
+ logOperationalError("agent.analytics.write.failed", error, {
1139
+ agentName: event.agentName,
1140
+ durableObjectName: event.durableObjectName
1141
+ });
1142
+ }
1143
+ });
985
1144
  }
986
1145
  get enabled() {
987
- return this.getHandlers().length > 0;
1146
+ return this.options.getDataset() !== void 0;
988
1147
  }
989
1148
  get context() {
990
1149
  return this.options.getContext();
@@ -1024,6 +1183,18 @@ var Analytics = class {
1024
1183
  this.emit(payload);
1025
1184
  }
1026
1185
  };
1186
+ function createEvent(context, payload) {
1187
+ return {
1188
+ ...payload,
1189
+ schemaVersion: 1,
1190
+ eventId: crypto.randomUUID(),
1191
+ timestamp: Date.now(),
1192
+ agentName: context.agentName,
1193
+ durableObjectName: context.durableObjectName,
1194
+ actorId: context.actorId,
1195
+ attributes: context.attributes ?? {}
1196
+ };
1197
+ }
1027
1198
  //#endregion
1028
1199
  //#region src/server/features/registry.ts
1029
1200
  /**
@@ -1190,6 +1361,7 @@ var Agent = class extends Think {
1190
1361
  const { tools, activeTools } = await this._prepareTools(ctx.tools, activeSkillName, true);
1191
1362
  const baseSystem = await this.session.refreshSystemPrompt();
1192
1363
  this._turn.baseSystem = [baseSystem, this._turn.skillsSystemBlock].filter(Boolean).join("\n\n");
1364
+ const turnTracing = await this.tracing.turn(this._turn.context ?? {}, this.messages);
1193
1365
  return {
1194
1366
  system: this._turn.baseSystem,
1195
1367
  model: this.getModel(this._turn.context),
@@ -1206,7 +1378,7 @@ var Agent = class extends Think {
1206
1378
  actorId: this.getTelemetryActorId(),
1207
1379
  clientIp: this.clientIp,
1208
1380
  forwardedFor: this.forwardedFor
1209
- }),
1381
+ }, turnTracing),
1210
1382
  metadata: {
1211
1383
  agentName: this.getTelemetryAgentName(),
1212
1384
  version: "v2",
@@ -1735,14 +1907,23 @@ var Agent = class extends Think {
1735
1907
  actorId: this.getTelemetryActorId(),
1736
1908
  attributes: this.getAnalyticsAttributes()
1737
1909
  }),
1738
- getHandlers: () => [...this.env.AGENTS_ANALYTICS ? [createAnalyticsEngineHandler(this.env.AGENTS_ANALYTICS)] : [], ...this.getAnalyticsHandlers()],
1910
+ getDataset: () => this.env.AGENTS_ANALYTICS,
1911
+ keepAlive: (callback) => this.keepAliveWhile(callback)
1912
+ });
1913
+ tracing = new Tracing({
1914
+ getIdentity: () => ({
1915
+ agentName: this.getTelemetryAgentName(),
1916
+ durableObjectName: this.name,
1917
+ actorId: this.getTelemetryActorId()
1918
+ }),
1919
+ getHandlers: () => this.getTracingHandlers(),
1739
1920
  keepAlive: (callback) => this.keepAliveWhile(callback)
1740
1921
  });
1741
1922
  /**
1742
- * Additional product-analytics destinations for this agent. Analytics Engine
1743
- * remains enabled whenever `AGENTS_ANALYTICS` is bound.
1923
+ * Destinations for completed turns and message feedback, e.g. a tracing
1924
+ * service. They run in the background; a failing one never affects the chat.
1744
1925
  */
1745
- getAnalyticsHandlers() {
1926
+ getTracingHandlers() {
1746
1927
  return [];
1747
1928
  }
1748
1929
  /** Business dimensions added to every analytics event. */
@@ -2968,7 +3149,14 @@ var ChatAgent = class extends Agent {
2968
3149
  async submitMessageFeedback(messageId, rating, comment) {
2969
3150
  const result = submitMessageFeedback(this.sql.bind(this), messageId, rating, comment);
2970
3151
  const parent = await this.getParentAgent();
2971
- if (rating === 1 || rating === -1 || rating === 0) this.analytics.feedbackSubmitted(messageId, rating);
3152
+ if (rating === 1 || rating === -1 || rating === 0) {
3153
+ this.analytics.feedbackSubmitted(messageId, rating);
3154
+ this.tracing.feedback(this.messages, {
3155
+ messageId,
3156
+ rating,
3157
+ comment
3158
+ });
3159
+ }
2972
3160
  if (parent?.recordChatFeedback) try {
2973
3161
  await parent.recordChatFeedback(this.name, messageId, rating);
2974
3162
  } catch (error) {
@@ -3068,4 +3256,4 @@ function getCurrentToolContext() {
3068
3256
  return getCurrentAgent().agent._lastBody;
3069
3257
  }
3070
3258
  //#endregion
3071
- export { Agent, Assistant, ChatAgent, action, createAnalyticsEngineHandler, createAnalyticsEvent, createJwtAuthConfig, dispatchAnalyticsEvent, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
3259
+ export { Agent, Assistant, ChatAgent, action, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.14.3",
3
+ "version": "2.15.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -26,6 +26,7 @@
26
26
  "@ai-sdk/google": "^3.0.73",
27
27
  "@cloudflare/ai-chat": "^0.11.0",
28
28
  "@cloudflare/think": "^0.17.0",
29
+ "@opentelemetry/api": "^1.9.0",
29
30
  "@opentelemetry/sdk-trace-base": "^2.8.0",
30
31
  "agents": "^0.22.0",
31
32
  "ai": "^6.0.197",