@economic/agents 2.9.2 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,9 +1,126 @@
1
- import { a as extractTokenFromConnectRequest, c as verifyJwt, i as createJwtAuthConfig, n as createAgentTracer, o as getTargetActorId, r as recordFeedbackAnalytics, s as isJwtActorAuthorized, t as routeAgentRequest } from "./route-agent-request-J98VlcOj.mjs";
2
- import { Agent as Agent$1, callable, getCurrentAgent } from "agents";
1
+ import { Agent as Agent$1, callable, getCurrentAgent, routeAgentRequest as routeAgentRequest$1 } from "agents";
3
2
  import { Think, skills } from "@cloudflare/think";
4
3
  import { Output, convertToModelMessages, generateText, jsonSchema, pruneMessages, tool as tool$1 } from "ai";
4
+ import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
5
+ import { BasicTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
6
+ import { genericObservability } from "agents/observability";
5
7
  import { nanoid } from "nanoid";
6
8
  import { createCompactFunction } from "agents/experimental/memory/utils";
9
+ //#region src/server/features/auth/index.ts
10
+ function createJwtAuthConfig(config) {
11
+ return config;
12
+ }
13
+ const jwksByIssuer = /* @__PURE__ */ new Map();
14
+ /** Returns the actor portion of a top-level or parent-scoped agent name. */
15
+ function getTargetActorId(target) {
16
+ const rootName = target.parentPath?.[0]?.name ?? target.durableObjectName;
17
+ const separator = rootName.indexOf(":");
18
+ return separator === -1 ? rootName : rootName.slice(0, separator);
19
+ }
20
+ /** Checks that verified JWT claims belong to the addressed agent instance. */
21
+ function isJwtActorAuthorized(config, claims, target) {
22
+ if (typeof config.getActorId !== "function") return false;
23
+ const authenticatedActorId = config.getActorId(claims);
24
+ return typeof authenticatedActorId === "string" && authenticatedActorId.length > 0 && authenticatedActorId === getTargetActorId(target);
25
+ }
26
+ function getJwksForIssuer(issuer) {
27
+ const normalized = issuer.replace(/\/$/, "");
28
+ let jwks = jwksByIssuer.get(normalized);
29
+ if (!jwks) {
30
+ jwks = createRemoteJWKSet(new URL(`${normalized}/.well-known/openid-configuration/jwks`));
31
+ jwksByIssuer.set(normalized, jwks);
32
+ }
33
+ return jwks;
34
+ }
35
+ function isIssuerAllowed(iss, allowed) {
36
+ return allowed.some((rule) => typeof rule === "string" ? rule === iss : rule.test(iss));
37
+ }
38
+ function extractTokenFromConnectRequest(request) {
39
+ const authorization = request.headers.get("Authorization");
40
+ if (authorization) {
41
+ const match = authorization.match(/^Bearer\s+(.+)$/i);
42
+ if (match?.[1]) return match[1].trim();
43
+ }
44
+ const wsProtocol = request.headers.get("Sec-WebSocket-Protocol");
45
+ if (wsProtocol) {
46
+ const parts = wsProtocol.split(",").map((p) => p.trim());
47
+ const idx = parts.indexOf("bearer");
48
+ if (idx !== -1 && parts[idx + 1]) return parts[idx + 1];
49
+ }
50
+ return null;
51
+ }
52
+ function hasRequiredScopes(tokenScope, required) {
53
+ if (required.length === 0) return true;
54
+ if (tokenScope === void 0) return false;
55
+ const tokens = Array.isArray(tokenScope) ? tokenScope : tokenScope.split(" ").map((s) => s.trim()).filter(Boolean);
56
+ const granted = new Set(tokens);
57
+ return required.every((scope) => granted.has(scope));
58
+ }
59
+ /**
60
+ * Verify a JWT from a request and extract claims.
61
+ *
62
+ * Extracts the token from `Authorization: Bearer` header first,
63
+ * then falls back to `Sec-WebSocket-Protocol: bearer, <token>` for WebSocket connections.
64
+ *
65
+ * Expected auth failures (missing/expired token, insufficient scope) return a failure result.
66
+ * Unexpected errors (network issues, malformed requests, untrusted issuers) throw and should
67
+ * be caught and logged by the caller.
68
+ *
69
+ * @param request - The incoming request (from ConnectionContext)
70
+ * @param config - JWT verification configuration
71
+ * @returns Result object with either verified claims or expected auth failure
72
+ * @throws Error for unexpected failures that should be logged
73
+ */
74
+ async function verifyJwt(request, config) {
75
+ const token = extractTokenFromConnectRequest(request);
76
+ if (!token) return {
77
+ success: false,
78
+ status: 401,
79
+ message: "Unauthorized: Missing authentication token"
80
+ };
81
+ let unverifiedPayload;
82
+ try {
83
+ unverifiedPayload = decodeJwt(token);
84
+ } catch {
85
+ throw new Error("Invalid token format");
86
+ }
87
+ const iss = typeof unverifiedPayload.iss === "string" ? unverifiedPayload.iss : void 0;
88
+ if (!iss) throw new Error("Missing issuer claim in token");
89
+ if (!isIssuerAllowed(iss, config.allowedIssuers)) throw new Error(`Untrusted issuer: ${iss}`);
90
+ const jwks = getJwksForIssuer(iss);
91
+ let payload;
92
+ try {
93
+ payload = (await jwtVerify(token, jwks, {
94
+ issuer: iss,
95
+ audience: config.audience
96
+ })).payload;
97
+ } catch (error) {
98
+ if (error instanceof errors.JWTExpired) return {
99
+ success: false,
100
+ status: 401,
101
+ message: "Unauthorized: Token expired"
102
+ };
103
+ if (error instanceof errors.JWTClaimValidationFailed) return {
104
+ success: false,
105
+ status: 401,
106
+ message: "Unauthorized: Token claim validation failed"
107
+ };
108
+ if (error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JWKSNoMatchingKey) throw new Error("Invalid token signature");
109
+ throw error;
110
+ }
111
+ const requiredScopes = config.requiredScopes ?? [];
112
+ const scopeClaim = payload.scope;
113
+ if (!hasRequiredScopes(scopeClaim, requiredScopes)) return {
114
+ success: false,
115
+ status: 403,
116
+ message: "Forbidden: Insufficient scope"
117
+ };
118
+ return {
119
+ success: true,
120
+ claims: config.getClaims(payload)
121
+ };
122
+ }
123
+ //#endregion
7
124
  //#region src/server/util/agent.ts
8
125
  function sendConnectionStatus(connection, status) {
9
126
  connection.send(JSON.stringify({
@@ -253,6 +370,490 @@ Unavailable tools:
253
370
  ${restrictions}`;
254
371
  }
255
372
  //#endregion
373
+ //#region src/server/features/telemetry/skills.ts
374
+ /**
375
+ * Extracts the skill names loaded by an `activate_skill` tool call.
376
+ *
377
+ * v1 `activate_skill` input is `{ skills: string[] }`; v2 uses
378
+ * `{ name: string }`. Return every requested skill name while ignoring empty
379
+ * values so analytics does not group activations under a blank skill.
380
+ */
381
+ function extractSkillNames(toolName, input) {
382
+ if (toolName !== "activate_skill" || !input || typeof input !== "object") return [];
383
+ const { name, skills } = input;
384
+ if (typeof name === "string" && name) return [name];
385
+ if (Array.isArray(skills)) return skills.filter((skill) => typeof skill === "string" && skill !== "");
386
+ return [];
387
+ }
388
+ /**
389
+ * Single-skill variant used where one representative skill name is sufficient
390
+ * (e.g. audit-log attribution and current-skill tracking).
391
+ */
392
+ function extractSkillName(toolName, input) {
393
+ return extractSkillNames(toolName, input)[0];
394
+ }
395
+ //#endregion
396
+ //#region src/server/features/telemetry/utils.ts
397
+ function durationMs(duration) {
398
+ return duration[0] * 1e3 + duration[1] / 1e6;
399
+ }
400
+ function stringAttribute(span, key) {
401
+ const value = span.attributes[key];
402
+ return typeof value === "string" ? value : void 0;
403
+ }
404
+ function numberAttribute(span, key) {
405
+ const value = span.attributes[key];
406
+ return typeof value === "number" ? value : void 0;
407
+ }
408
+ function parseJson(value) {
409
+ if (!value) return;
410
+ try {
411
+ return JSON.parse(value);
412
+ } catch {
413
+ return;
414
+ }
415
+ }
416
+ //#endregion
417
+ //#region src/server/features/telemetry/audit-logs.ts
418
+ function safePathSegment(value, fallback) {
419
+ return (value || fallback).replace(/[^a-zA-Z0-9._-]/g, "_");
420
+ }
421
+ function createAuditLogKey(agentName, durableObjectName) {
422
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
423
+ const id = crypto.randomUUID().slice(0, 8);
424
+ return [
425
+ safePathSegment(agentName, "unknown-agent"),
426
+ safePathSegment(durableObjectName, "unknown-chat"),
427
+ `${timestamp}-${id}.json`
428
+ ].join("/");
429
+ }
430
+ function textFromContent(content) {
431
+ if (typeof content === "string") return content;
432
+ if (Array.isArray(content)) return content.map((part) => {
433
+ if (!part || typeof part !== "object") return "";
434
+ const value = part.text;
435
+ return typeof value === "string" ? value : "";
436
+ }).filter(Boolean).join("\n");
437
+ return "";
438
+ }
439
+ function extractPrompt(messages) {
440
+ const firstUserMessage = messages.find((message) => message.role === "user");
441
+ return firstUserMessage ? textFromContent(firstUserMessage.content) : "";
442
+ }
443
+ function promptCharCount(messages) {
444
+ const firstUserMessage = messages.find((message) => message.role === "user");
445
+ return firstUserMessage ? textFromContent(firstUserMessage.content).length : 0;
446
+ }
447
+ function extractTools(messages) {
448
+ const tools = [];
449
+ for (const message of messages) {
450
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
451
+ for (const part of message.content) {
452
+ if (!part || typeof part !== "object") continue;
453
+ const record = part;
454
+ if (record.type !== "tool_use") continue;
455
+ tools.push({
456
+ name: typeof record.name === "string" ? record.name : void 0,
457
+ input: record.input,
458
+ status: "success"
459
+ });
460
+ }
461
+ }
462
+ return tools;
463
+ }
464
+ function pushToolCall(map, key, toolCall) {
465
+ const existing = map.get(key);
466
+ if (existing) existing.push(toolCall);
467
+ else map.set(key, [toolCall]);
468
+ }
469
+ /**
470
+ * Stores the last ai.streamText.doStream span per chat.
471
+ * Its ai.prompt.messages contains the full chat history including all
472
+ * intermediate tool_use + tool_result turns from the agentic loop.
473
+ */
474
+ const lastDoStreamByChat = /* @__PURE__ */ new Map();
475
+ const toolCallsByParentSpan = /* @__PURE__ */ new Map();
476
+ const toolCallsByChat = /* @__PURE__ */ new Map();
477
+ const pendingToolCalls = [];
478
+ const currentSkillByChat = /* @__PURE__ */ new Map();
479
+ function rememberToolCall(span) {
480
+ const parentSpanId = span.parentSpanContext?.spanId;
481
+ const toolName = stringAttribute(span, "ai.toolCall.name");
482
+ const input = parseJson(stringAttribute(span, "ai.toolCall.args"));
483
+ const toolCall = {
484
+ name: toolName,
485
+ input,
486
+ status: span.status.code === 0 ? "success" : "error",
487
+ skillName: extractSkillName(toolName, input)
488
+ };
489
+ if (parentSpanId) pushToolCall(toolCallsByParentSpan, parentSpanId, toolCall);
490
+ pendingToolCalls.push(toolCall);
491
+ }
492
+ function attachToolCallsToChat(span, durableObjectName) {
493
+ const spanId = span.spanContext().spanId;
494
+ const toolCalls = toolCallsByParentSpan.get(spanId) ?? pendingToolCalls.splice(0);
495
+ if (!toolCalls.length) return;
496
+ toolCallsByParentSpan.delete(spanId);
497
+ const currentSkill = currentSkillByChat.get(durableObjectName);
498
+ const attributedToolCalls = toolCalls.map((toolCall) => {
499
+ const skillName = toolCall.skillName ?? currentSkill;
500
+ if (toolCall.skillName) currentSkillByChat.set(durableObjectName, toolCall.skillName);
501
+ return {
502
+ ...toolCall,
503
+ ...skillName ? { skillName } : {}
504
+ };
505
+ });
506
+ toolCallsByChat.set(durableObjectName, [...toolCallsByChat.get(durableObjectName) ?? [], ...attributedToolCalls]);
507
+ }
508
+ function buildAuditLog(span, context) {
509
+ const lastDoStream = lastDoStreamByChat.get(context.durableObjectName);
510
+ lastDoStreamByChat.delete(context.durableObjectName);
511
+ const promptMessages = parseJson(stringAttribute(lastDoStream ?? span, "ai.prompt.messages"));
512
+ const fallbackPrompt = parseJson(stringAttribute(span, "ai.prompt"));
513
+ const inputMessages = promptMessages ?? fallbackPrompt?.messages ?? [];
514
+ const spanToolCalls = toolCallsByChat.get(context.durableObjectName) ?? [];
515
+ toolCallsByChat.delete(context.durableObjectName);
516
+ return {
517
+ id: createAuditLogKey(context.agentName, context.durableObjectName),
518
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
519
+ durableObjectName: context.durableObjectName,
520
+ agent: {
521
+ name: context.agentName,
522
+ llm: {
523
+ model: stringAttribute(span, "ai.model.id"),
524
+ provider: stringAttribute(span, "ai.model.provider")
525
+ }
526
+ },
527
+ actor: {
528
+ id: context.actorId,
529
+ ip: {
530
+ client: context.clientIp ?? context.forwardedFor?.split(",").map((ip) => ip.trim()).filter(Boolean)[0],
531
+ forwardedFor: context.forwardedFor?.split(",").map((ip) => ip.trim()).filter(Boolean) ?? []
532
+ }
533
+ },
534
+ prompt: extractPrompt(inputMessages),
535
+ response: stringAttribute(span, "ai.response.text") ?? "",
536
+ status: span.status.code === 0 ? "success" : "error",
537
+ tools: [...extractTools(inputMessages), ...spanToolCalls]
538
+ };
539
+ }
540
+ function rememberDoStream(span, durableObjectName) {
541
+ lastDoStreamByChat.set(durableObjectName, span);
542
+ }
543
+ async function handleAuditSpan(span, auditLogs, context) {
544
+ const auditLog = buildAuditLog(span, context);
545
+ if (!auditLogs) return;
546
+ await auditLogs.put(auditLog.id, JSON.stringify(auditLog, null, 2), { httpMetadata: { contentType: "application/json" } });
547
+ console.log("[AuditLog] Created", auditLog.id);
548
+ }
549
+ //#endregion
550
+ //#region src/server/features/telemetry/analytics.ts
551
+ const emittedToolCallIds = /* @__PURE__ */ new Set();
552
+ function safeJsonString(value) {
553
+ if (value === void 0) return void 0;
554
+ try {
555
+ return JSON.stringify(value);
556
+ } catch {
557
+ return;
558
+ }
559
+ }
560
+ function writeAnalyticsDatapoint(analytics, dataPoint) {
561
+ if (!analytics) return;
562
+ try {
563
+ analytics.writeDataPoint(dataPoint);
564
+ } catch (error) {
565
+ console.error("[Agent] Failed to write analytics datapoint", error);
566
+ }
567
+ }
568
+ /**
569
+ * Writes a message-feedback rating to Analytics Engine so sentiment can be
570
+ * aggregated across all chats without fanning out to every chat DO.
571
+ *
572
+ * Emitted on every rating change; the dashboard takes the latest rating per
573
+ * `messageId` (blob4) so re-rates and clears are idempotent.
574
+ */
575
+ function recordFeedbackAnalytics(analytics, context, messageId, rating) {
576
+ writeAnalyticsDatapoint(analytics, {
577
+ indexes: [context.actorId],
578
+ blobs: [
579
+ "feedback",
580
+ context.agentName,
581
+ context.durableObjectName,
582
+ messageId
583
+ ],
584
+ doubles: [rating]
585
+ });
586
+ }
587
+ function recordPromptReceivedAnalytics(analytics, context, messages, continuation) {
588
+ if (!analytics) return;
589
+ writeAnalyticsDatapoint(analytics, {
590
+ indexes: [context.actorId],
591
+ blobs: [
592
+ "prompt_received",
593
+ context.agentName,
594
+ context.durableObjectName,
595
+ String(continuation)
596
+ ],
597
+ doubles: [safeJsonString(messages)?.length ?? 0]
598
+ });
599
+ }
600
+ function recordTurnCompletedAnalytics(analytics, context, requestId, status, continuation, response) {
601
+ if (!analytics) return;
602
+ writeAnalyticsDatapoint(analytics, {
603
+ indexes: [context.actorId],
604
+ blobs: [
605
+ "turn_completed",
606
+ context.agentName,
607
+ context.durableObjectName,
608
+ requestId,
609
+ status,
610
+ String(continuation)
611
+ ],
612
+ doubles: [safeJsonString(response)?.length ?? 0]
613
+ });
614
+ }
615
+ function handleAnalyticsSpan(span, analytics, context) {
616
+ if (span.name === "ai.streamText.doStream") {
617
+ const promptMessages = parseJson(stringAttribute(span, "ai.prompt.messages"));
618
+ const responseText = stringAttribute(span, "ai.response.text") ?? "";
619
+ const responseToolCalls = parseJson(stringAttribute(span, "ai.response.toolCalls"));
620
+ writeAnalyticsDatapoint(analytics, {
621
+ indexes: [context.actorId],
622
+ blobs: [
623
+ "llm_call",
624
+ context.agentName,
625
+ context.durableObjectName,
626
+ stringAttribute(span, "ai.model.id") ?? "",
627
+ stringAttribute(span, "ai.model.provider") ?? "",
628
+ stringAttribute(span, "ai.response.finishReason") ?? ""
629
+ ],
630
+ doubles: [
631
+ numberAttribute(span, "ai.usage.inputTokens") ?? 0,
632
+ numberAttribute(span, "ai.usage.outputTokens") ?? 0,
633
+ numberAttribute(span, "ai.usage.totalTokens") ?? 0,
634
+ numberAttribute(span, "ai.usage.inputTokenDetails.cacheReadTokens") ?? 0,
635
+ numberAttribute(span, "ai.usage.inputTokenDetails.cacheWriteTokens") ?? 0,
636
+ durationMs(span.duration),
637
+ promptCharCount(promptMessages ?? []),
638
+ responseText.length
639
+ ]
640
+ });
641
+ if (Array.isArray(responseToolCalls)) for (const value of responseToolCalls) {
642
+ if (!value || typeof value !== "object") continue;
643
+ const toolCall = value;
644
+ const toolCallId = typeof toolCall.toolCallId === "string" ? toolCall.toolCallId : void 0;
645
+ if (toolCallId && emittedToolCallIds.has(toolCallId)) continue;
646
+ const toolName = typeof toolCall.toolName === "string" ? toolCall.toolName : void 0;
647
+ if (!toolName) continue;
648
+ const input = toolCall.input;
649
+ const argsRaw = safeJsonString(input);
650
+ const success = span.status.code === 0;
651
+ const activatedSkills = extractSkillNames(toolName, input);
652
+ const skillNames = activatedSkills.length ? activatedSkills : [currentSkillByChat.get(context.durableObjectName) ?? ""];
653
+ if (toolCallId) emittedToolCallIds.add(toolCallId);
654
+ for (const skillName of skillNames) writeAnalyticsDatapoint(analytics, {
655
+ indexes: [context.actorId],
656
+ blobs: [
657
+ "tool_call",
658
+ context.agentName,
659
+ context.durableObjectName,
660
+ toolName,
661
+ skillName,
662
+ success ? "success" : "error"
663
+ ],
664
+ doubles: [
665
+ 0,
666
+ success ? 1 : 0,
667
+ argsRaw?.length ?? 0,
668
+ 0,
669
+ 0,
670
+ 0,
671
+ 0,
672
+ 0
673
+ ]
674
+ });
675
+ }
676
+ return;
677
+ }
678
+ if (span.name === "ai.toolCall") {
679
+ const toolName = stringAttribute(span, "ai.toolCall.name");
680
+ const toolCallId = stringAttribute(span, "ai.toolCall.id");
681
+ if (toolCallId && emittedToolCallIds.has(toolCallId)) return;
682
+ if (toolCallId) emittedToolCallIds.add(toolCallId);
683
+ const argsRaw = stringAttribute(span, "ai.toolCall.args");
684
+ const toolInput = parseJson(argsRaw);
685
+ const success = span.status.code === 0;
686
+ const duration = durationMs(span.duration);
687
+ const argsSize = argsRaw?.length ?? 0;
688
+ const resultSize = stringAttribute(span, "ai.toolCall.result")?.length ?? 0;
689
+ const activatedSkills = extractSkillNames(toolName, toolInput);
690
+ const skillNames = activatedSkills.length ? activatedSkills : [currentSkillByChat.get(context.durableObjectName) ?? ""];
691
+ for (const skillName of skillNames) writeAnalyticsDatapoint(analytics, {
692
+ indexes: [context.actorId],
693
+ blobs: [
694
+ "tool_call",
695
+ context.agentName,
696
+ context.durableObjectName,
697
+ toolName ?? "",
698
+ skillName,
699
+ success ? "success" : "error"
700
+ ],
701
+ doubles: [
702
+ duration,
703
+ success ? 1 : 0,
704
+ argsSize,
705
+ resultSize,
706
+ 0,
707
+ 0,
708
+ 0,
709
+ 0
710
+ ]
711
+ });
712
+ }
713
+ }
714
+ //#endregion
715
+ //#region src/server/features/observability/index.ts
716
+ const measuredEvents = /* @__PURE__ */ new Set(["chat:turn:finish"]);
717
+ const warningEvents = /* @__PURE__ */ new Set([
718
+ "schedule:duplicate_warning",
719
+ "alarm:memory_limit_reset",
720
+ "fiber:run:interrupted",
721
+ "chat:onstart:degraded",
722
+ "chat:stream:stalled",
723
+ "agent_tool:recovery:deadline",
724
+ "agent_tool:detached:live_count_warning"
725
+ ]);
726
+ function writeOperationalLog(record, logger = console) {
727
+ try {
728
+ const message = JSON.stringify(record);
729
+ if (record.level === "error") logger.error(message);
730
+ else if (record.level === "warn") logger.warn(message);
731
+ else logger.log(message);
732
+ } catch {}
733
+ }
734
+ function isFailure(event) {
735
+ return event.type.endsWith("error") || event.type.endsWith("failed") || event.type.endsWith("exhausted") || "error" in event.payload && Boolean(event.payload.error);
736
+ }
737
+ /**
738
+ * Preserves Cloudflare Agents diagnostics while mirroring the operational
739
+ * subset into structured Worker logs for Logpush destinations.
740
+ */
741
+ function createWorkerObservability(options = {}) {
742
+ const diagnostics = options.diagnostics ?? genericObservability;
743
+ const logger = options.logger ?? console;
744
+ return { emit(event) {
745
+ diagnostics.emit(event);
746
+ const failed = isFailure(event);
747
+ if (!failed && !warningEvents.has(event.type) && !measuredEvents.has(event.type)) return;
748
+ const identity = options.getIdentity?.();
749
+ const attributes = Object.fromEntries(Object.entries(event.payload).filter(([key]) => key !== "error"));
750
+ writeOperationalLog({
751
+ source: "@economic/agents",
752
+ event: event.type,
753
+ level: failed ? "error" : warningEvents.has(event.type) ? "warn" : "info",
754
+ timestamp: event.timestamp,
755
+ agentName: identity?.agentName ?? event.agent,
756
+ agentClass: identity?.agentClass ?? event.agent,
757
+ durableObjectName: event.name,
758
+ attributes
759
+ }, logger);
760
+ } };
761
+ }
762
+ //#endregion
763
+ //#region src/server/features/observability/ai.ts
764
+ const AI_MODEL_SPANS = /* @__PURE__ */ new Set(["ai.generateText.doGenerate", "ai.streamText.doStream"]);
765
+ function failed(span) {
766
+ return span.status.code === 2 || stringAttribute(span, "ai.response.finishReason") === "error";
767
+ }
768
+ function baseRecord(span, context) {
769
+ return {
770
+ source: "@economic/agents",
771
+ level: failed(span) ? "error" : "info",
772
+ timestamp: Date.now(),
773
+ agentName: context.agentName,
774
+ agentClass: context.agentClass,
775
+ durableObjectName: context.durableObjectName
776
+ };
777
+ }
778
+ function operationalLogFromAISpan(span, context) {
779
+ if (AI_MODEL_SPANS.has(span.name)) {
780
+ const isFailure = failed(span);
781
+ return {
782
+ ...baseRecord(span, context),
783
+ event: isFailure ? "agent.llm.failed" : "agent.llm.completed",
784
+ attributes: {
785
+ operation: span.name,
786
+ provider: stringAttribute(span, "ai.model.provider"),
787
+ model: stringAttribute(span, "ai.model.id"),
788
+ finishReason: stringAttribute(span, "ai.response.finishReason"),
789
+ durationMs: durationMs(span.duration),
790
+ timeToFirstChunkMs: numberAttribute(span, "ai.response.msToFirstChunk"),
791
+ inputTokens: numberAttribute(span, "ai.usage.inputTokens"),
792
+ outputTokens: numberAttribute(span, "ai.usage.outputTokens"),
793
+ totalTokens: numberAttribute(span, "ai.usage.totalTokens"),
794
+ cachedInputTokens: numberAttribute(span, "ai.usage.cachedInputTokens"),
795
+ cacheWriteTokens: numberAttribute(span, "ai.usage.inputTokenDetails.cacheWriteTokens"),
796
+ reasoningTokens: numberAttribute(span, "ai.usage.reasoningTokens")
797
+ }
798
+ };
799
+ }
800
+ if (span.name === "ai.toolCall") {
801
+ const isFailure = failed(span);
802
+ return {
803
+ ...baseRecord(span, context),
804
+ event: isFailure ? "agent.tool.failed" : "agent.tool.completed",
805
+ attributes: {
806
+ toolName: stringAttribute(span, "ai.toolCall.name"),
807
+ durationMs: durationMs(span.duration)
808
+ }
809
+ };
810
+ }
811
+ }
812
+ function logAISpan(span, context, logger = console) {
813
+ const record = operationalLogFromAISpan(span, context);
814
+ if (record) writeOperationalLog(record, logger);
815
+ }
816
+ //#endregion
817
+ //#region src/server/features/telemetry/index.ts
818
+ var AgentSpanExporter = class {
819
+ auditLogs;
820
+ analytics;
821
+ context;
822
+ constructor(auditLogs, analytics, context) {
823
+ this.auditLogs = auditLogs;
824
+ this.analytics = analytics;
825
+ this.context = context;
826
+ }
827
+ export(spans, resultCallback) {
828
+ (async () => {
829
+ try {
830
+ for (const span of spans) {
831
+ logAISpan(span, this.context);
832
+ if (span.name === "ai.streamText.doStream") {
833
+ rememberDoStream(span, this.context.durableObjectName);
834
+ attachToolCallsToChat(span, this.context.durableObjectName);
835
+ handleAnalyticsSpan(span, this.analytics, this.context);
836
+ } else if (span.name === "ai.streamText") await handleAuditSpan(span, this.auditLogs, this.context);
837
+ else if (span.name === "ai.toolCall") {
838
+ rememberToolCall(span);
839
+ handleAnalyticsSpan(span, this.analytics, this.context);
840
+ }
841
+ }
842
+ resultCallback({ code: 0 });
843
+ } catch (error) {
844
+ resultCallback({
845
+ code: 1,
846
+ error: error instanceof Error ? error : new Error(String(error))
847
+ });
848
+ }
849
+ })();
850
+ }
851
+ async shutdown() {}
852
+ };
853
+ function createAgentTracer(auditLogs, analytics, context) {
854
+ return new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(new AgentSpanExporter(auditLogs, analytics, context))] }).getTracer("@economic/agents");
855
+ }
856
+ //#endregion
256
857
  //#region src/server/features/registry.ts
257
858
  /**
258
859
  * Registers a top-level DO in the global registry.
@@ -441,6 +1042,11 @@ var Agent = class extends Think {
441
1042
  * `returned.tools` into your own `TurnConfig.tools` if you return tools.
442
1043
  */
443
1044
  async beforeTurn(ctx) {
1045
+ recordPromptReceivedAnalytics(this.env.AGENTS_ANALYTICS, {
1046
+ agentName: this.getTelemetryAgentName(),
1047
+ durableObjectName: this.name,
1048
+ actorId: this.getTelemetryActorId()
1049
+ }, ctx.messages, ctx.continuation);
444
1050
  this.registerInstanceActivity();
445
1051
  const body = await this._applyTokenExchange(ctx.body);
446
1052
  this._turn.context = this.getToolContextForCurrentTurn(body);
@@ -460,6 +1066,7 @@ var Agent = class extends Think {
460
1066
  isEnabled: true,
461
1067
  tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.env.AGENTS_ANALYTICS, {
462
1068
  agentName: this.getTelemetryAgentName(),
1069
+ agentClass: this.constructor.name,
463
1070
  durableObjectName: this.name,
464
1071
  actorId: this.getTelemetryActorId(),
465
1072
  clientIp: this.clientIp,
@@ -800,6 +1407,10 @@ var Agent = class extends Think {
800
1407
  console.error("[Agent] Failed to update agent_registry activity", error);
801
1408
  }
802
1409
  }
1410
+ observability = createWorkerObservability({ getIdentity: () => ({
1411
+ agentName: this.getTelemetryAgentName(),
1412
+ agentClass: this.constructor.name
1413
+ }) });
803
1414
  /**
804
1415
  * Resolves the analytics/telemetry agent name to the entry-point agent: the
805
1416
  * top-level DO registered in the `agent` catalog table.
@@ -876,11 +1487,12 @@ function registerChat(sql, durableObjectName, dateTime) {
876
1487
  function renameChat(sql, durableObjectName, title, dateTime = Date.now()) {
877
1488
  const trimmed = title.trim();
878
1489
  if (!trimmed) throw new Error("Chat title cannot be empty");
879
- sql`UPDATE chats
1490
+ if (!sql`UPDATE chats
880
1491
  SET title = ${trimmed},
881
1492
  title_set_by_user = 1,
882
1493
  updated_at = ${dateTime}
883
- WHERE durable_object_name = ${durableObjectName}`;
1494
+ WHERE durable_object_name = ${durableObjectName}
1495
+ RETURNING durable_object_name`.length) throw new Error(`Chat not found: ${durableObjectName}`);
884
1496
  }
885
1497
  function deleteChat(sql, durableObjectName) {
886
1498
  sql`DELETE FROM chats WHERE durable_object_name = ${durableObjectName}`;
@@ -893,11 +1505,15 @@ async function getChats(sql) {
893
1505
  return sql`SELECT * FROM chats ORDER BY updated_at DESC`;
894
1506
  }
895
1507
  function recordChatFeedback(sql, durableObjectName, messageId, rating, dateTime = Date.now()) {
896
- sql`INSERT INTO chat_message_feedback (chat_id, message_id, rating, updated_at)
897
- VALUES (${durableObjectName}, ${messageId}, ${rating}, ${dateTime})
898
- ON CONFLICT (chat_id, message_id) DO UPDATE SET
899
- rating = excluded.rating,
900
- updated_at = excluded.updated_at`;
1508
+ if (rating !== 1 && rating !== -1 && rating !== 0) throw new RangeError("Message feedback rating must be 1, -1, or 0");
1509
+ if (!getChat(sql, durableObjectName)) throw new Error(`Chat not found: ${durableObjectName}`);
1510
+ if (rating === 0) sql`DELETE FROM chat_message_feedback
1511
+ WHERE chat_id = ${durableObjectName} AND message_id = ${messageId}`;
1512
+ else sql`INSERT INTO chat_message_feedback (chat_id, message_id, rating, updated_at)
1513
+ VALUES (${durableObjectName}, ${messageId}, ${rating}, ${dateTime})
1514
+ ON CONFLICT (chat_id, message_id) DO UPDATE SET
1515
+ rating = excluded.rating,
1516
+ updated_at = excluded.updated_at`;
901
1517
  const positiveRows = sql`SELECT COUNT(*) AS count
902
1518
  FROM chat_message_feedback
903
1519
  WHERE chat_id = ${durableObjectName} AND rating > 0`;
@@ -927,7 +1543,7 @@ async function summariseChatWithAI(sql, durableObjectName, messages, model) {
927
1543
  The title should be a short title for the chat, max 8-10 words.
928
1544
  The summary should be a short 1-2 sentence summary of the chat.
929
1545
  The summary should reflect the direction of the chat.`;
930
- if (chat) systemPrompt += `${systemPrompt}\n\nThe previous summary: ${chat.summary}`;
1546
+ if (chat) systemPrompt += `\n\nThe previous summary: ${chat.summary}`;
931
1547
  try {
932
1548
  const { output: { title, summary } } = await generateText({
933
1549
  model,
@@ -1222,9 +1838,15 @@ var Assistant = class extends Agent$1 {
1222
1838
  registerChat(this.sql.bind(this), durableObjectName, Date.now());
1223
1839
  }
1224
1840
  async recordChatTurn(durableObjectName, messages) {
1225
- summariseChatWithAI(this.sql.bind(this), durableObjectName, messages, this.fastModel);
1226
- this.scheduleChatForAutoDeletion(durableObjectName).catch((error) => {
1227
- console.error("[Assistant] Failed to reschedule chat deletion", {
1841
+ this.keepAliveWhile(async () => {
1842
+ await Promise.all([summariseChatWithAI(this.sql.bind(this), durableObjectName, messages, this.fastModel), this.scheduleChatForAutoDeletion(durableObjectName).catch((error) => {
1843
+ console.error("[Assistant] Failed to reschedule chat deletion", {
1844
+ durableObjectName,
1845
+ error
1846
+ });
1847
+ })]);
1848
+ }).catch((error) => {
1849
+ console.error("[Assistant] Failed to record chat metadata", {
1228
1850
  durableObjectName,
1229
1851
  error
1230
1852
  });
@@ -1287,6 +1909,10 @@ var Assistant = class extends Agent$1 {
1287
1909
  console.error("[Assistant] Failed to update agent_registry activity", error);
1288
1910
  }
1289
1911
  }
1912
+ observability = createWorkerObservability({ getIdentity: () => ({
1913
+ agentName: this.constructor.name,
1914
+ agentClass: this.constructor.name
1915
+ }) });
1290
1916
  };
1291
1917
  //#endregion
1292
1918
  //#region src/server/features/messages.ts
@@ -1321,7 +1947,12 @@ function ensureFeedbackTableExists(sql) {
1321
1947
  * @param now - The date and time to use for the created_at and updated_at columns.
1322
1948
  */
1323
1949
  function submitMessageFeedback(sql, messageId, rating, comment, now = /* @__PURE__ */ new Date()) {
1950
+ if (rating !== 1 && rating !== -1 && rating !== 0) throw new RangeError("Message feedback rating must be 1, -1, or 0");
1324
1951
  try {
1952
+ if (rating === 0) {
1953
+ sql`DELETE FROM assistant_messages_feedback WHERE message_id = ${messageId}`;
1954
+ return;
1955
+ }
1325
1956
  sql`INSERT INTO assistant_messages_feedback (message_id, rating, comment, created_at, updated_at)
1326
1957
  VALUES (${messageId}, ${rating}, ${comment ?? null}, ${now.getTime()}, ${now.getTime()})
1327
1958
  ON CONFLICT (message_id) DO UPDATE SET
@@ -1339,7 +1970,9 @@ function submitMessageFeedback(sql, messageId, rating, comment, now = /* @__PURE
1339
1970
  */
1340
1971
  function getMessageFeedback(sql) {
1341
1972
  try {
1342
- const feedback = sql`SELECT message_id, rating, comment, created_at, updated_at FROM assistant_messages_feedback`;
1973
+ const feedback = sql`SELECT message_id, rating, comment, created_at, updated_at
1974
+ FROM assistant_messages_feedback
1975
+ WHERE rating IN (1, -1)`;
1343
1976
  return Object.fromEntries(feedback.map((row) => [row.message_id, row]));
1344
1977
  } catch {
1345
1978
  return {};
@@ -1501,14 +2134,19 @@ var ChatAgent = class extends Agent {
1501
2134
  if (parent?.registerChat) await parent.registerChat(this.name);
1502
2135
  return super.beforeTurn(ctx);
1503
2136
  }
1504
- async onChatResponse(_result) {
2137
+ async onChatResponse(result) {
2138
+ recordTurnCompletedAnalytics(this.env.AGENTS_ANALYTICS, {
2139
+ agentName: this.getTelemetryAgentName(),
2140
+ durableObjectName: this.name,
2141
+ actorId: this.getTelemetryActorId()
2142
+ }, result.requestId, result.status, result.continuation, result.message.parts);
1505
2143
  const parent = await this.getParentAgent();
1506
2144
  if (parent?.recordChatTurn) await parent.recordChatTurn(this.name, this.messages);
1507
2145
  }
1508
2146
  /**
1509
2147
  * Submit feedback for a message by its id.
1510
2148
  * @param messageId - The id of the message to give feedback on.
1511
- * @param rating - The rating to give the message. 1 = thumbs up, -1 = thumbs down.
2149
+ * @param rating - The rating to give the message. 1 = thumbs up, -1 = thumbs down, 0 = clear.
1512
2150
  * @returns The message id and the rating.
1513
2151
  */
1514
2152
  async submitMessageFeedback(messageId, rating, comment) {
@@ -1566,6 +2204,19 @@ function skill(definition) {
1566
2204
  };
1567
2205
  }
1568
2206
  //#endregion
2207
+ //#region src/server/route-agent-request.ts
2208
+ async function routeAgentRequest(request, env, options) {
2209
+ const response = await routeAgentRequest$1(request, env, options);
2210
+ if (!response) return null;
2211
+ const protocol = request.headers.get("Sec-WebSocket-Protocol");
2212
+ if (response.status === 101 && protocol) {
2213
+ const newResponse = new Response(null, response);
2214
+ newResponse.headers.set("Sec-WebSocket-Protocol", protocol.split(",")[0].trim());
2215
+ return newResponse;
2216
+ }
2217
+ return response;
2218
+ }
2219
+ //#endregion
1569
2220
  //#region src/server/index.ts
1570
2221
  function getCurrentToolContext() {
1571
2222
  return getCurrentAgent().agent._lastBody;