@economic/agents 2.12.0 → 2.14.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
@@ -421,6 +421,7 @@ import { z } from "zod";
421
421
 
422
422
  export const weatherSkill = skill({
423
423
  name: "weather",
424
+ command: "weather",
424
425
  description: "Look up current weather and forecasts. Use for any weather question.",
425
426
  instructions: `
426
427
  # Weather
@@ -445,6 +446,16 @@ Use this skill whenever the user asks about current conditions or a forecast.
445
446
 
446
447
  Return skills from `getSkills()`. Add `authorize(ctx)` to gate a skill (and its tools). Denied skills are hidden from the catalog and `activate_skill` by default. Set `onUnauthorized` to a reason string, or a function that returns one, to expose a denied skill with user-facing guidance. A function can return `undefined` to keep it hidden. `skill()` throws if `description` or `instructions` is missing.
447
448
 
449
+ Set `command` to make a code-defined skill directly invocable from chat. For example,
450
+ `/weather in Copenhagen` loads the weather instructions and enables its tools before the
451
+ first model step; the model still interprets `in Copenhagen` and chooses the appropriate
452
+ tool. Commands use lower-case letters, numbers, and hyphens, without the leading `/`.
453
+
454
+ `ChatAgent` also exposes an authenticated `GET getInvocableSkills` action for command
455
+ pickers. It returns the commands available to the current user as
456
+ `{ command, name, description }[]`. The existing skill `description` is used for both the
457
+ model catalog and the human-facing picker.
458
+
448
459
  ### Native Agent Skills
449
460
 
450
461
  Native Agent Skills are folders containing a `SKILL.md` file, with optional references and scripts:
@@ -640,7 +651,30 @@ When configured:
640
651
  AI telemetry is emitted per turn from the `ai` SDK's OpenTelemetry spans:
641
652
 
642
653
  - **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`.
654
+ - **Analytics** → typed per-turn and per-tool events. By default these are written to
655
+ `AGENTS_ANALYTICS` using the existing Analytics Engine schema.
656
+
657
+ Additional analytics destinations can receive the same events by overriding
658
+ `getAnalyticsHandlers()`. Analytics Engine remains enabled whenever `AGENTS_ANALYTICS` is bound:
659
+
660
+ ```typescript
661
+ import type { AnalyticsHandler } from "@economic/agents";
662
+
663
+ const warehouse: AnalyticsHandler = {
664
+ name: "warehouse",
665
+ async handle(event) {
666
+ await sendToWarehouse(event);
667
+ },
668
+ };
669
+
670
+ getAnalyticsHandlers() {
671
+ return [warehouse];
672
+ }
673
+ ```
674
+
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.
644
678
 
645
679
  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
680
 
@@ -669,21 +703,25 @@ Imported from `@economic/agents` unless noted.
669
703
  | `ToolContext` | `RequestContext & { _userContext?: UserContext }`. |
670
704
  | `AgentEnv` | The bindings the runtime expects. |
671
705
  | `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. |
672
708
 
673
709
  ### Members
674
710
 
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. |
711
+ | Member | On | Description |
712
+ | ---------------------------------------------- | ----------- | ---------------------------------------------------------------------- |
713
+ | `getModel(ctx?)` | `Agent` | Required. Model for inference. |
714
+ | `getSystemPrompt(ctx?)` | `Agent` | Required. System prompt. |
715
+ | `getTools()` | `Agent` | Always-on tools (default `{}`). |
716
+ | `getSkills()` | `Agent` | Local skills (default `[]`). |
717
+ | `getAnalyticsHandlers()` | `Agent` | Additional analytics destinations; Analytics Engine remains automatic. |
718
+ | `getAnalyticsAttributes()` | `Agent` | Additional dimensions included on every analytics event. |
719
+ | `static getJwtAuthConfig(env)` | `Agent` | Optional JWT verification and actor binding. |
720
+ | `getUserContext(jwtToken)` | `Agent` | Optional per-user context after auth. |
721
+ | `submitMessageFeedback` / `getMessageFeedback` | `ChatAgent` | Callable message feedback. |
722
+ | `agent` | `Assistant` | Required. Your `ChatAgent` subclass. |
723
+ | `fastModel` | `Assistant` | Required. Cheap model for titles/summaries. |
724
+ | `createChat` / `getChats` / `deleteChat` | `Assistant` | Callable chat management. |
687
725
 
688
726
  ### Package root (`@economic/agents`)
689
727
 
package/dist/index.d.mts CHANGED
@@ -114,6 +114,108 @@ 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: "skill.invoked";
158
+ skillName: string;
159
+ command: string;
160
+ source: "slash-command";
161
+ } | {
162
+ type: "feedback.submitted";
163
+ messageId: string;
164
+ rating: 1 | -1 | 0;
165
+ };
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
+ interface AnalyticsEventContext {
181
+ agentName: string;
182
+ durableObjectName: string;
183
+ actorId: string;
184
+ attributes?: AnalyticsAttributes;
185
+ }
186
+ //#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
+ //#region src/server/features/analytics/emitter.d.ts
199
+ interface AnalyticsOptions {
200
+ getContext(): AnalyticsEventContext;
201
+ getHandlers(): readonly AnalyticsHandler[];
202
+ keepAlive<T>(callback: () => Promise<T>): Promise<T>;
203
+ }
204
+ declare class Analytics {
205
+ private readonly options;
206
+ private handlers?;
207
+ constructor(options: AnalyticsOptions);
208
+ private getHandlers;
209
+ private emit;
210
+ get enabled(): boolean;
211
+ get context(): AnalyticsEventContext;
212
+ promptReceived(messages: readonly ModelMessage[], continuation: boolean): void;
213
+ turnCompleted(requestId: string, status: string, continuation: boolean, response: unknown): void;
214
+ feedbackSubmitted(messageId: string, rating: 1 | -1 | 0): void;
215
+ skillInvoked(skillName: string, command: string): void;
216
+ event(payload: AnalyticsEventPayload | (() => AnalyticsEventPayload)): void;
217
+ }
218
+ //#endregion
117
219
  //#region src/server/agents/Agent.d.ts
118
220
  /** What the model would see on its next step — returned by {@link Agent.inspectTurn}. */
119
221
  interface TurnSnapshot {
@@ -305,6 +407,14 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
305
407
  * Best-effort and non-blocking — never allowed to interfere with a turn.
306
408
  */
307
409
  protected registerInstanceActivity(): Promise<void>;
410
+ protected readonly analytics: Analytics;
411
+ /**
412
+ * Additional product-analytics destinations for this agent. Analytics Engine
413
+ * remains enabled whenever `AGENTS_ANALYTICS` is bound.
414
+ */
415
+ getAnalyticsHandlers(): readonly AnalyticsHandler[];
416
+ /** Business dimensions added to every analytics event. */
417
+ getAnalyticsAttributes(): AnalyticsAttributes;
308
418
  observability?: Observability;
309
419
  /**
310
420
  * Resolves the analytics/telemetry agent name to the entry-point agent: the
@@ -328,12 +438,69 @@ type MessageFeedback = {
328
438
  updated_at: number;
329
439
  };
330
440
  //#endregion
441
+ //#region src/server/util/actions.d.ts
442
+ /** Tool execution options plus the additional context supplied by native Think actions. */
443
+ type ActionContext<Context extends Record<string, unknown> = Record<string, unknown>> = Omit<ToolExecutionOptions, "experimental_context"> & ActionContext$1 & {
444
+ experimental_context: Context; /** The action's resolved domain key, or its tool-call fallback when none was declared. */
445
+ idempotencyKey: string;
446
+ };
447
+ type Action<Context extends Record<string, unknown> = Record<string, unknown>, Input = any, Output = any> = Omit<ActionConfig<FlexibleSchema<Input>, Output>, "approval" | "execute" | "idempotencyKey" | "inputSchema" | "permissions"> & {
448
+ description: string;
449
+ inputSchema: FlexibleSchema<Input>;
450
+ approval?: ActionApprovalPolicy<Input>;
451
+ idempotencyKey?: ActionIdempotencyKey<Input>;
452
+ permissions?: ActionPermissionSpec<Input>;
453
+ authorize?: (ctx?: Context) => boolean;
454
+ onUnauthorized?: UnauthorizedPolicy<Context>;
455
+ execute(input: Input, ctx: ActionContext<Context>): Promise<Output> | Output;
456
+ };
457
+ type RegisteredAction<Context extends Record<string, unknown> = Record<string, unknown>, InputSchema extends FlexibleSchema = FlexibleSchema, Output = unknown> = Action$1<InputSchema, Output> & {
458
+ authorize?: (ctx?: Context) => boolean;
459
+ onUnauthorized?: UnauthorizedPolicy<Context>;
460
+ };
461
+ /** A native Think action with the SDK's standard authorization policy. */
462
+ declare function action<Context extends Record<string, unknown> = Record<string, unknown>, Input = any, Output = any>(definition: Action<Context, Input, Output>): RegisteredAction<Context, FlexibleSchema<Input>, Output>;
463
+ //#endregion
464
+ //#region src/server/util/skills.d.ts
465
+ interface Skill<Context extends Record<string, unknown> = Record<string, unknown>> {
466
+ name: string;
467
+ description: string;
468
+ instructions: string;
469
+ /** Enables direct invocation from chat with `/command`. */
470
+ command?: string;
471
+ tools?: Record<string, Tool<Context>>;
472
+ actions?: Record<string, RegisteredAction<Context, any, any>>;
473
+ authorize?: (ctx?: Context) => boolean;
474
+ onUnauthorized?: UnauthorizedPolicy<Context>;
475
+ }
476
+ interface SkillSource<Context extends Record<string, unknown> = Record<string, unknown>> extends SkillSource$1 {
477
+ command?: string;
478
+ tools?: Record<string, Tool<Context>>;
479
+ actions?: Record<string, RegisteredAction<Context, any, any>>;
480
+ authorize?: (ctx?: Context | undefined) => boolean;
481
+ onUnauthorized?: UnauthorizedPolicy<Context>;
482
+ }
483
+ declare function skill<Context extends Record<string, unknown> = Record<string, unknown>>(definition: Skill<Context>): SkillSource<Context>;
484
+ //#endregion
485
+ //#region src/server/util/skillCommands.d.ts
486
+ interface InvocableSkill {
487
+ /** Slash command without the leading `/`. */
488
+ command: string;
489
+ /** Name of the skill the command activates. */
490
+ name: string;
491
+ /** Human- and model-readable summary from the skill definition. */
492
+ description: string;
493
+ }
494
+ //#endregion
331
495
  //#region src/server/agents/ChatAgent.d.ts
332
496
  declare abstract class ChatAgent<RequestContext extends Record<string, unknown> = Record<string, unknown>, UserContext extends Record<string, unknown> = Record<string, unknown>, State = unknown> extends Agent<RequestContext, UserContext, State> {
333
497
  protected agentType: AgentConnectionType;
334
498
  onStart(): Promise<void>;
335
499
  configureSession(session: Session): Session;
336
500
  beforeTurn(ctx: TurnContext): Promise<void | TurnConfig>;
501
+ private getSlashCommandSkill;
502
+ private invokeSkill;
503
+ protected getInvocableSkills(userContext?: UserContext | undefined, requestContext?: RequestContext): Promise<InvocableSkill[]>;
337
504
  onChatResponse(result: ChatResponseResult): Promise<void>;
338
505
  onRequest(request: Request): Promise<Response>;
339
506
  /**
@@ -408,51 +575,10 @@ declare abstract class Assistant<State = unknown> extends Agent$1<Cloudflare.Env
408
575
  observability?: Observability;
409
576
  }
410
577
  //#endregion
411
- //#region src/server/util/actions.d.ts
412
- /** Tool execution options plus the additional context supplied by native Think actions. */
413
- type ActionContext<Context extends Record<string, unknown> = Record<string, unknown>> = Omit<ToolExecutionOptions, "experimental_context"> & ActionContext$1 & {
414
- experimental_context: Context; /** The action's resolved domain key, or its tool-call fallback when none was declared. */
415
- idempotencyKey: string;
416
- };
417
- type Action<Context extends Record<string, unknown> = Record<string, unknown>, Input = any, Output = any> = Omit<ActionConfig<FlexibleSchema<Input>, Output>, "approval" | "execute" | "idempotencyKey" | "inputSchema" | "permissions"> & {
418
- description: string;
419
- inputSchema: FlexibleSchema<Input>;
420
- approval?: ActionApprovalPolicy<Input>;
421
- idempotencyKey?: ActionIdempotencyKey<Input>;
422
- permissions?: ActionPermissionSpec<Input>;
423
- authorize?: (ctx?: Context) => boolean;
424
- onUnauthorized?: UnauthorizedPolicy<Context>;
425
- execute(input: Input, ctx: ActionContext<Context>): Promise<Output> | Output;
426
- };
427
- type RegisteredAction<Context extends Record<string, unknown> = Record<string, unknown>, InputSchema extends FlexibleSchema = FlexibleSchema, Output = unknown> = Action$1<InputSchema, Output> & {
428
- authorize?: (ctx?: Context) => boolean;
429
- onUnauthorized?: UnauthorizedPolicy<Context>;
430
- };
431
- /** A native Think action with the SDK's standard authorization policy. */
432
- declare function action<Context extends Record<string, unknown> = Record<string, unknown>, Input = any, Output = any>(definition: Action<Context, Input, Output>): RegisteredAction<Context, FlexibleSchema<Input>, Output>;
433
- //#endregion
434
- //#region src/server/util/skills.d.ts
435
- interface Skill<Context extends Record<string, unknown> = Record<string, unknown>> {
436
- name: string;
437
- description: string;
438
- instructions: string;
439
- tools?: Record<string, Tool<Context>>;
440
- actions?: Record<string, RegisteredAction<Context, any, any>>;
441
- authorize?: (ctx?: Context) => boolean;
442
- onUnauthorized?: UnauthorizedPolicy<Context>;
443
- }
444
- interface SkillSource<Context extends Record<string, unknown> = Record<string, unknown>> extends SkillSource$1 {
445
- tools?: Record<string, Tool<Context>>;
446
- actions?: Record<string, RegisteredAction<Context, any, any>>;
447
- authorize?: (ctx?: Context | undefined) => boolean;
448
- onUnauthorized?: UnauthorizedPolicy<Context>;
449
- }
450
- declare function skill<Context extends Record<string, unknown> = Record<string, unknown>>(definition: Skill<Context>): SkillSource<Context>;
451
- //#endregion
452
578
  //#region src/server/route-agent-request.d.ts
453
579
  declare function routeAgentRequest<Env>(request: Request, env: Env, options?: AgentOptions<Env>): Promise<Response | null>;
454
580
  //#endregion
455
581
  //#region src/server/index.d.ts
456
582
  declare function getCurrentToolContext(): any;
457
583
  //#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 };
584
+ 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 };
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;
517
+ return 0;
519
518
  }
520
519
  }
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);
527
- }
528
- }
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,275 @@ 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 "skill.invoked": return [{
909
+ indexes,
910
+ blobs: [
911
+ "skill_invoked",
912
+ event.agentName,
913
+ event.durableObjectName,
914
+ event.skillName,
915
+ event.command,
916
+ event.source,
917
+ ...common
918
+ ],
919
+ doubles: [1]
920
+ }];
921
+ case "feedback.submitted": return [{
922
+ indexes,
923
+ blobs: [
924
+ "feedback",
925
+ event.agentName,
926
+ event.durableObjectName,
927
+ event.messageId,
928
+ ...common
929
+ ],
930
+ doubles: [event.rating]
931
+ }];
932
+ }
933
+ }
934
+ //#endregion
935
+ //#region src/server/features/analytics/dispatcher.ts
936
+ function createAnalyticsEvent(context, payload) {
937
+ return {
938
+ ...payload,
939
+ schemaVersion: 1,
940
+ eventId: crypto.randomUUID(),
941
+ timestamp: Date.now(),
942
+ agentName: context.agentName,
943
+ durableObjectName: context.durableObjectName,
944
+ actorId: context.actorId,
945
+ attributes: context.attributes ?? {}
946
+ };
947
+ }
948
+ async function dispatchAnalyticsEvent(handlers, event) {
949
+ await Promise.all(handlers.map(async (handler) => {
950
+ try {
951
+ await handler.handle(event);
952
+ } catch (error) {
953
+ writeOperationalLog({
954
+ source: "@economic/agents",
955
+ event: "agent.analytics.handler.failed",
956
+ level: "error",
957
+ timestamp: Date.now(),
958
+ agentName: event.agentName,
959
+ durableObjectName: event.durableObjectName,
960
+ attributes: {
961
+ handler: handler.name ?? "handler",
962
+ error: error instanceof Error ? error.message : String(error)
963
+ }
964
+ });
965
+ }
966
+ }));
967
+ }
968
+ //#endregion
969
+ //#region src/server/features/analytics/emitter.ts
970
+ var Analytics = class {
971
+ options;
972
+ handlers;
973
+ constructor(options) {
974
+ this.options = options;
975
+ }
976
+ getHandlers() {
977
+ return this.handlers ??= this.options.getHandlers();
978
+ }
979
+ emit(payload) {
980
+ const handlers = this.getHandlers();
981
+ if (handlers.length === 0) return;
982
+ const eventPayload = typeof payload === "function" ? payload() : payload;
983
+ this.options.keepAlive(() => dispatchAnalyticsEvent(handlers, createAnalyticsEvent(this.context, eventPayload)));
984
+ }
985
+ get enabled() {
986
+ return this.getHandlers().length > 0;
987
+ }
988
+ get context() {
989
+ return this.options.getContext();
990
+ }
991
+ promptReceived(messages, continuation) {
992
+ this.emit({
993
+ type: "prompt.received",
994
+ messages,
995
+ continuation
996
+ });
997
+ }
998
+ turnCompleted(requestId, status, continuation, response) {
999
+ this.emit(() => ({
1000
+ type: "turn.completed",
1001
+ requestId,
1002
+ status,
1003
+ continuation,
1004
+ responseSize: serializedSize(response)
1005
+ }));
1006
+ }
1007
+ feedbackSubmitted(messageId, rating) {
1008
+ this.emit({
1009
+ type: "feedback.submitted",
1010
+ messageId,
1011
+ rating
1012
+ });
1013
+ }
1014
+ skillInvoked(skillName, command) {
1015
+ this.emit({
1016
+ type: "skill.invoked",
1017
+ skillName,
1018
+ command,
1019
+ source: "slash-command"
1020
+ });
1021
+ }
1022
+ event(payload) {
1023
+ this.emit(payload);
1024
+ }
1025
+ };
1026
+ //#endregion
818
1027
  //#region src/server/features/registry.ts
819
1028
  /**
820
1029
  * Registers a top-level DO in the global registry.
@@ -964,11 +1173,7 @@ var Agent = class extends Think {
964
1173
  * `returned.tools` into your own `TurnConfig.tools` if you return tools.
965
1174
  */
966
1175
  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);
1176
+ this.analytics.promptReceived(ctx.messages, ctx.continuation);
972
1177
  this.registerInstanceActivity();
973
1178
  const body = await this._applyTokenExchange(ctx.body);
974
1179
  this._turn.context = this.getToolContextForCurrentTurn(body);
@@ -977,7 +1182,6 @@ var Agent = class extends Think {
977
1182
  const { tools, activeTools } = await this._prepareTools(ctx.tools, activeSkillName, true);
978
1183
  const baseSystem = await this.session.refreshSystemPrompt();
979
1184
  this._turn.baseSystem = [baseSystem, this._turn.skillsSystemBlock].filter(Boolean).join("\n\n");
980
- console.log(this._turn.baseSystem, "\n\n// end system prompt\n\n");
981
1185
  return {
982
1186
  system: this._turn.baseSystem,
983
1187
  model: this.getModel(this._turn.context),
@@ -987,7 +1191,7 @@ var Agent = class extends Think {
987
1191
  experimental_transform: buildSourcesTransform(),
988
1192
  experimental_telemetry: {
989
1193
  isEnabled: true,
990
- tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.env.AGENTS_ANALYTICS, {
1194
+ tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.analytics, {
991
1195
  agentName: this.getTelemetryAgentName(),
992
1196
  agentClass: this.constructor.name,
993
1197
  durableObjectName: this.name,
@@ -1346,7 +1550,12 @@ var Agent = class extends Think {
1346
1550
  try {
1347
1551
  result = await verifyJwt(request, config);
1348
1552
  } catch (error) {
1349
- console.error(`[Agent] JWT verification error - ${error}`);
1553
+ logOperationalError("agent.authentication.failed", error, {
1554
+ agentName: this.getTelemetryAgentName(),
1555
+ agentClass: this.constructor.name,
1556
+ durableObjectName: this.name,
1557
+ attributes: { stage: "jwt-verification" }
1558
+ });
1350
1559
  return {
1351
1560
  success: false,
1352
1561
  status: 401,
@@ -1369,7 +1578,12 @@ var Agent = class extends Think {
1369
1578
  try {
1370
1579
  exchanged = await exchangeToken(token, exchangeConfig);
1371
1580
  } catch (error) {
1372
- console.error(`[Agent] Token exchange error - ${error}`);
1581
+ logOperationalError("agent.authentication.failed", error, {
1582
+ agentName: this.getTelemetryAgentName(),
1583
+ agentClass: this.constructor.name,
1584
+ durableObjectName: this.name,
1585
+ attributes: { stage: "token-exchange" }
1586
+ });
1373
1587
  return {
1374
1588
  success: false,
1375
1589
  status: 500,
@@ -1377,7 +1591,12 @@ var Agent = class extends Think {
1377
1591
  };
1378
1592
  }
1379
1593
  if (!exchanged.success) {
1380
- console.error(`[Agent] Token exchange failed - ${exchanged.message}`);
1594
+ logOperationalError("agent.authentication.failed", new Error(exchanged.message), {
1595
+ agentName: this.getTelemetryAgentName(),
1596
+ agentClass: this.constructor.name,
1597
+ durableObjectName: this.name,
1598
+ attributes: { stage: "token-exchange" }
1599
+ });
1381
1600
  return {
1382
1601
  success: false,
1383
1602
  status: exchanged.subjectTokenRejected ? 401 : 500,
@@ -1395,7 +1614,12 @@ var Agent = class extends Think {
1395
1614
  userContext: this.userContext
1396
1615
  };
1397
1616
  } catch (error) {
1398
- console.error(`[Agent] getUserContext error - ${error}`);
1617
+ logOperationalError("agent.authentication.failed", error, {
1618
+ agentName: this.getTelemetryAgentName(),
1619
+ agentClass: this.constructor.name,
1620
+ durableObjectName: this.name,
1621
+ attributes: { stage: "user-context" }
1622
+ });
1399
1623
  const closeCode = getConnectCloseCode(error);
1400
1624
  return {
1401
1625
  success: false,
@@ -1459,7 +1683,11 @@ var Agent = class extends Think {
1459
1683
  createdAt: Date.now()
1460
1684
  });
1461
1685
  } catch (error) {
1462
- console.error("[Agent] Failed to register in agent_registry", error);
1686
+ logOperationalError("agent.registry.registration.failed", error, {
1687
+ agentName: this.getTelemetryAgentName(),
1688
+ agentClass: this.constructor.name,
1689
+ durableObjectName: this.name
1690
+ });
1463
1691
  }
1464
1692
  }
1465
1693
  /**
@@ -1485,9 +1713,34 @@ var Agent = class extends Think {
1485
1713
  updatedAt: Date.now()
1486
1714
  });
1487
1715
  } catch (error) {
1488
- console.error("[Agent] Failed to update agent_registry activity", error);
1716
+ logOperationalError("agent.registry.activity.failed", error, {
1717
+ agentName: this.getTelemetryAgentName(),
1718
+ agentClass: this.constructor.name,
1719
+ durableObjectName: this.name
1720
+ });
1489
1721
  }
1490
1722
  }
1723
+ analytics = new Analytics({
1724
+ getContext: () => ({
1725
+ agentName: this.getTelemetryAgentName(),
1726
+ durableObjectName: this.name,
1727
+ actorId: this.getTelemetryActorId(),
1728
+ attributes: this.getAnalyticsAttributes()
1729
+ }),
1730
+ getHandlers: () => [...this.env.AGENTS_ANALYTICS ? [createAnalyticsEngineHandler(this.env.AGENTS_ANALYTICS)] : [], ...this.getAnalyticsHandlers()],
1731
+ keepAlive: (callback) => this.keepAliveWhile(callback)
1732
+ });
1733
+ /**
1734
+ * Additional product-analytics destinations for this agent. Analytics Engine
1735
+ * remains enabled whenever `AGENTS_ANALYTICS` is bound.
1736
+ */
1737
+ getAnalyticsHandlers() {
1738
+ return [];
1739
+ }
1740
+ /** Business dimensions added to every analytics event. */
1741
+ getAnalyticsAttributes() {
1742
+ return {};
1743
+ }
1491
1744
  observability = createWorkerObservability({ getIdentity: () => ({
1492
1745
  agentName: this.getTelemetryAgentName(),
1493
1746
  agentClass: this.constructor.name
@@ -1884,7 +2137,12 @@ var Assistant = class extends Agent$1 {
1884
2137
  try {
1885
2138
  result = await verifyJwt(request, config);
1886
2139
  } catch (error) {
1887
- console.error(`[Assistant] JWT verification error - ${error}`);
2140
+ logOperationalError("agent.authentication.failed", error, {
2141
+ agentName: this.constructor.name,
2142
+ agentClass: this.constructor.name,
2143
+ durableObjectName: this.name,
2144
+ attributes: { stage: "jwt-verification" }
2145
+ });
1888
2146
  return {
1889
2147
  success: false,
1890
2148
  status: 401,
@@ -1896,13 +2154,18 @@ var Assistant = class extends Agent$1 {
1896
2154
  durableObjectName: this.name,
1897
2155
  parentPath: this.parentPath
1898
2156
  })) {
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
2157
+ logOperationalError("agent.authentication.failed", /* @__PURE__ */ new Error("Authenticated actor does not own the requested assistant"), {
2158
+ agentName: this.constructor.name,
2159
+ agentClass: this.constructor.name,
2160
+ durableObjectName: this.name,
2161
+ attributes: {
2162
+ stage: "actor-authorization",
2163
+ authenticatedActorId: config.getActorId(result.claims),
2164
+ requiredActorId: getTargetActorId({
2165
+ durableObjectName: this.name,
2166
+ parentPath: this.parentPath
2167
+ })
2168
+ }
1906
2169
  });
1907
2170
  return {
1908
2171
  success: false,
@@ -1940,9 +2203,11 @@ var Assistant = class extends Agent$1 {
1940
2203
  try {
1941
2204
  await this.deleteSubAgent(this.agent, id);
1942
2205
  } catch (cleanupError) {
1943
- console.error("[Assistant] Failed to clean up an unscheduled chat facet", {
1944
- durableObjectName: id,
1945
- error: cleanupError
2206
+ logOperationalError("assistant.chat.cleanup.failed", cleanupError, {
2207
+ agentName: this.constructor.name,
2208
+ agentClass: this.constructor.name,
2209
+ durableObjectName: this.name,
2210
+ attributes: { chatName: id }
1946
2211
  });
1947
2212
  }
1948
2213
  throw error;
@@ -1973,16 +2238,20 @@ var Assistant = class extends Agent$1 {
1973
2238
  touchChat(this.sql.bind(this), durableObjectName);
1974
2239
  this.keepAliveWhile(async () => {
1975
2240
  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
2241
+ logOperationalError("assistant.chat.reschedule.failed", error, {
2242
+ agentName: this.constructor.name,
2243
+ agentClass: this.constructor.name,
2244
+ durableObjectName: this.name,
2245
+ attributes: { chatName: durableObjectName }
1979
2246
  });
1980
2247
  })]);
1981
2248
  await this.syncChatToGlobalList(durableObjectName);
1982
2249
  }).catch((error) => {
1983
- console.error("[Assistant] Failed to record chat metadata", {
1984
- durableObjectName,
1985
- error
2250
+ logOperationalError("assistant.chat.metadata.failed", error, {
2251
+ agentName: this.constructor.name,
2252
+ agentClass: this.constructor.name,
2253
+ durableObjectName: this.name,
2254
+ attributes: { chatName: durableObjectName }
1986
2255
  });
1987
2256
  });
1988
2257
  }
@@ -2024,7 +2293,11 @@ var Assistant = class extends Agent$1 {
2024
2293
  createdAt: Date.now()
2025
2294
  });
2026
2295
  } catch (error) {
2027
- console.error("[Assistant] Failed to register in agent_registry", error);
2296
+ logOperationalError("agent.registry.registration.failed", error, {
2297
+ agentName: this.constructor.name,
2298
+ agentClass: this.constructor.name,
2299
+ durableObjectName: this.name
2300
+ });
2028
2301
  }
2029
2302
  }
2030
2303
  /**
@@ -2041,7 +2314,11 @@ var Assistant = class extends Agent$1 {
2041
2314
  updatedAt
2042
2315
  });
2043
2316
  } catch (error) {
2044
- console.error("[Assistant] Failed to update agent_registry activity", error);
2317
+ logOperationalError("agent.registry.activity.failed", error, {
2318
+ agentName: this.constructor.name,
2319
+ agentClass: this.constructor.name,
2320
+ durableObjectName: this.name
2321
+ });
2045
2322
  }
2046
2323
  }
2047
2324
  async syncChatToGlobalList(durableObjectName) {
@@ -2081,6 +2358,38 @@ var Assistant = class extends Agent$1 {
2081
2358
  agentClass: this.constructor.name
2082
2359
  }) });
2083
2360
  };
2361
+ //#endregion
2362
+ //#region src/server/util/skillCommands.ts
2363
+ async function listInvocableSkills(sources) {
2364
+ const commands = /* @__PURE__ */ new Set();
2365
+ const skills = [];
2366
+ for (const source of sources) {
2367
+ if (!source.command) continue;
2368
+ if (commands.has(source.command)) throw new Error(`Duplicate skill command "/${source.command}"`);
2369
+ const descriptor = (await source.list()).find((skill) => skill.name === source.id);
2370
+ if (!descriptor) continue;
2371
+ commands.add(source.command);
2372
+ skills.push({
2373
+ command: source.command,
2374
+ name: descriptor.name,
2375
+ description: descriptor.description
2376
+ });
2377
+ }
2378
+ return skills;
2379
+ }
2380
+ function parseSkillCommand(messages, skills) {
2381
+ const message = messages.findLast((candidate) => candidate.role === "user");
2382
+ if (!message) return;
2383
+ const text = getMessageText(message);
2384
+ if (!text?.startsWith("/")) return;
2385
+ const match = /^\/([^\s]+)/.exec(text);
2386
+ if (!match) return;
2387
+ return skills.find((skill) => skill.command === match[1]);
2388
+ }
2389
+ function getMessageText(message) {
2390
+ if (typeof message.content === "string") return message.content;
2391
+ return message.content.filter((part) => part.type === "text").map((part) => part.text).join("") || void 0;
2392
+ }
2084
2393
  const COMPACTION_MAX_OUTPUT_TOKENS = 2e3;
2085
2394
  const createCompactFn = (model) => createCompactFunction({
2086
2395
  protectHead: 2,
@@ -2418,7 +2727,22 @@ var ChatAgent = class extends Agent {
2418
2727
  async beforeTurn(ctx) {
2419
2728
  const parent = await this.getParentAgent();
2420
2729
  if (parent?.registerChat) await parent.registerChat(this.name);
2421
- const config = await super.beforeTurn(ctx) ?? {};
2730
+ const slashCommandSkill = await this.getSlashCommandSkill(ctx);
2731
+ let skillInvocation;
2732
+ if (slashCommandSkill) skillInvocation = await this.invokeSkill(ctx, slashCommandSkill);
2733
+ const turnContext = skillInvocation ? {
2734
+ ...ctx,
2735
+ messages: [...ctx.messages, ...skillInvocation.messages]
2736
+ } : ctx;
2737
+ let config = await super.beforeTurn(turnContext) ?? {};
2738
+ if (skillInvocation) {
2739
+ const transforms = config.experimental_transform ? Array.isArray(config.experimental_transform) ? config.experimental_transform : [config.experimental_transform] : [];
2740
+ config = {
2741
+ ...config,
2742
+ activeTools: config.activeTools?.filter((name) => name !== "activate_skill"),
2743
+ experimental_transform: [skillInvocation.transform, ...transforms]
2744
+ };
2745
+ }
2422
2746
  try {
2423
2747
  const prepared = await prepareModelHistory(this.messages, getModelCompaction(this.sql.bind(this)), createCompactFn(config.model ?? ctx.model), config.system ?? ctx.system);
2424
2748
  if (prepared.stateChanged) if (prepared.state) setModelCompaction(this.sql.bind(this), prepared.state);
@@ -2434,7 +2758,7 @@ var ChatAgent = class extends Agent {
2434
2758
  });
2435
2759
  return {
2436
2760
  ...config,
2437
- messages: this.pruneTurnMessages(modelMessages)
2761
+ messages: this.pruneTurnMessages([...modelMessages, ...skillInvocation?.messages ?? []])
2438
2762
  };
2439
2763
  }
2440
2764
  } catch (error) {
@@ -2442,12 +2766,69 @@ var ChatAgent = class extends Agent {
2442
2766
  }
2443
2767
  return config;
2444
2768
  }
2769
+ async getSlashCommandSkill(context) {
2770
+ if (context.continuation) return;
2771
+ const connectionState = getCurrentAgent()?.connection?.state;
2772
+ const invocableSkills = await this.getInvocableSkills(connectionState?.userContext ?? this.userContext, context.body);
2773
+ return parseSkillCommand(context.messages, invocableSkills);
2774
+ }
2775
+ async invokeSkill(context, skill) {
2776
+ const activateSkill = context.tools.activate_skill;
2777
+ if (!activateSkill?.execute) return;
2778
+ const input = { name: skill.name };
2779
+ const toolCallId = crypto.randomUUID();
2780
+ const output = await activateSkill.execute(input, {
2781
+ toolCallId,
2782
+ messages: context.messages
2783
+ });
2784
+ if (typeof output !== "string") throw new Error("activate_skill returned an invalid result");
2785
+ this.analytics.skillInvoked(skill.name, skill.command);
2786
+ const messages = await convertToModelMessages([{
2787
+ id: crypto.randomUUID(),
2788
+ role: "assistant",
2789
+ parts: [{
2790
+ type: "tool-activate_skill",
2791
+ toolCallId,
2792
+ state: "output-available",
2793
+ input,
2794
+ output
2795
+ }]
2796
+ }], { tools: context.tools });
2797
+ const transform = () => {
2798
+ let emitted = false;
2799
+ return new TransformStream({ transform(chunk, controller) {
2800
+ controller.enqueue(chunk);
2801
+ if (emitted || chunk.type !== "start-step") return;
2802
+ emitted = true;
2803
+ controller.enqueue({
2804
+ type: "tool-call",
2805
+ toolCallId,
2806
+ toolName: "activate_skill",
2807
+ input
2808
+ });
2809
+ controller.enqueue({
2810
+ type: "tool-result",
2811
+ toolCallId,
2812
+ toolName: "activate_skill",
2813
+ input,
2814
+ output
2815
+ });
2816
+ } });
2817
+ };
2818
+ return {
2819
+ messages,
2820
+ transform
2821
+ };
2822
+ }
2823
+ async getInvocableSkills(userContext = this.userContext, requestContext = {}) {
2824
+ const context = {
2825
+ ...requestContext,
2826
+ _userContext: userContext
2827
+ };
2828
+ return listInvocableSkills((await this.getSkills()).filter((source) => source.authorize?.(context) !== false));
2829
+ }
2445
2830
  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);
2831
+ this.analytics.turnCompleted(result.requestId, result.status, result.continuation, result.message.parts);
2451
2832
  const parent = await this.getParentAgent();
2452
2833
  if (parent?.recordChatTurn) await parent.recordChatTurn(this.name, this.messages);
2453
2834
  }
@@ -2456,6 +2837,7 @@ var ChatAgent = class extends Agent {
2456
2837
  if (!auth.success) return Response.json({ error: auth.message }, { status: auth.status });
2457
2838
  const action = new URL(request.url).pathname.split("/").pop();
2458
2839
  if (action === "getMessages" && request.method === "GET") return Response.json(await this.getMessages());
2840
+ if (action === "getInvocableSkills" && request.method === "GET") return Response.json(await this.getInvocableSkills(auth.userContext));
2459
2841
  if (action === "sendMessage" && request.method === "POST") return this.streamMessage(request);
2460
2842
  if (action === "cancelChat" && request.method === "POST") {
2461
2843
  const { requestId } = await request.json();
@@ -2519,18 +2901,15 @@ var ChatAgent = class extends Agent {
2519
2901
  async submitMessageFeedback(messageId, rating, comment) {
2520
2902
  const result = submitMessageFeedback(this.sql.bind(this), messageId, rating, comment);
2521
2903
  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);
2904
+ if (rating === 1 || rating === -1 || rating === 0) this.analytics.feedbackSubmitted(messageId, rating);
2527
2905
  if (parent?.recordChatFeedback) try {
2528
2906
  await parent.recordChatFeedback(this.name, messageId, rating);
2529
2907
  } catch (error) {
2530
- console.error("[ChatAgent] Failed to update parent chat feedback summary", {
2908
+ logOperationalError("agent.feedback.summary.failed", error, {
2909
+ agentName: this.getTelemetryAgentName(),
2910
+ agentClass: this.constructor.name,
2531
2911
  durableObjectName: this.name,
2532
- messageId,
2533
- error
2912
+ attributes: { messageId }
2534
2913
  });
2535
2914
  }
2536
2915
  return result;
@@ -2578,6 +2957,7 @@ function skill(definition) {
2578
2957
  if (!definition.name) throw new Error("Skill name is required");
2579
2958
  if (!definition.description) throw new Error("Skill description is required");
2580
2959
  if (!definition.instructions) throw new Error("Skill content is required");
2960
+ if (definition.command !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(definition.command)) throw new Error("Skill command must use lower-case letters, numbers, and hyphens");
2581
2961
  return {
2582
2962
  id: definition.name,
2583
2963
  fingerprint: definition.name,
@@ -2595,6 +2975,7 @@ function skill(definition) {
2595
2975
  body: definition.instructions
2596
2976
  };
2597
2977
  },
2978
+ command: definition.command,
2598
2979
  tools: definition.tools,
2599
2980
  actions: definition.actions,
2600
2981
  authorize: definition.authorize,
@@ -2620,4 +3001,4 @@ function getCurrentToolContext() {
2620
3001
  return getCurrentAgent().agent._lastBody;
2621
3002
  }
2622
3003
  //#endregion
2623
- export { Agent, Assistant, ChatAgent, action, createJwtAuthConfig, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
3004
+ 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.14.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@cloudflare/think": "^0.17.0",
48
- "@cloudflare/vite-plugin": "^1.43.0 <1.43.2",
48
+ "@cloudflare/vite-plugin": "^1.45.1",
49
49
  "@cloudflare/worker-bundler": "^0.2.1",
50
50
  "agents": "^0.22.0",
51
51
  "vite": ">=8.0.0"