@economic/agents 2.9.1 → 2.10.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 +5 -3
- package/dist/index.d.mts +28 -5
- package/dist/index.mjs +653 -11
- package/package.json +1 -2
- package/dist/route-agent-request-9vqbi1bt.d.mts +0 -24
- package/dist/route-agent-request-J98VlcOj.mjs +0 -483
- package/dist/v1.d.mts +0 -264
- package/dist/v1.mjs +0 -1078
- package/schema/v1/0001_initial.sql +0 -35
- package/schema/v1/0002_add_rating_comment.sql +0 -1
- package/schema/v1/0003_drop_events.sql +0 -5
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,126 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
@@ -893,11 +1504,14 @@ async function getChats(sql) {
|
|
|
893
1504
|
return sql`SELECT * FROM chats ORDER BY updated_at DESC`;
|
|
894
1505
|
}
|
|
895
1506
|
function recordChatFeedback(sql, durableObjectName, messageId, rating, dateTime = Date.now()) {
|
|
896
|
-
sql
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1507
|
+
if (!getChat(sql, durableObjectName)) throw new Error(`Chat not found: ${durableObjectName}`);
|
|
1508
|
+
if (rating === 0) sql`DELETE FROM chat_message_feedback
|
|
1509
|
+
WHERE chat_id = ${durableObjectName} AND message_id = ${messageId}`;
|
|
1510
|
+
else sql`INSERT INTO chat_message_feedback (chat_id, message_id, rating, updated_at)
|
|
1511
|
+
VALUES (${durableObjectName}, ${messageId}, ${rating}, ${dateTime})
|
|
1512
|
+
ON CONFLICT (chat_id, message_id) DO UPDATE SET
|
|
1513
|
+
rating = excluded.rating,
|
|
1514
|
+
updated_at = excluded.updated_at`;
|
|
901
1515
|
const positiveRows = sql`SELECT COUNT(*) AS count
|
|
902
1516
|
FROM chat_message_feedback
|
|
903
1517
|
WHERE chat_id = ${durableObjectName} AND rating > 0`;
|
|
@@ -927,7 +1541,7 @@ async function summariseChatWithAI(sql, durableObjectName, messages, model) {
|
|
|
927
1541
|
The title should be a short title for the chat, max 8-10 words.
|
|
928
1542
|
The summary should be a short 1-2 sentence summary of the chat.
|
|
929
1543
|
The summary should reflect the direction of the chat.`;
|
|
930
|
-
if (chat) systemPrompt +=
|
|
1544
|
+
if (chat) systemPrompt += `\n\nThe previous summary: ${chat.summary}`;
|
|
931
1545
|
try {
|
|
932
1546
|
const { output: { title, summary } } = await generateText({
|
|
933
1547
|
model,
|
|
@@ -1287,6 +1901,10 @@ var Assistant = class extends Agent$1 {
|
|
|
1287
1901
|
console.error("[Assistant] Failed to update agent_registry activity", error);
|
|
1288
1902
|
}
|
|
1289
1903
|
}
|
|
1904
|
+
observability = createWorkerObservability({ getIdentity: () => ({
|
|
1905
|
+
agentName: this.constructor.name,
|
|
1906
|
+
agentClass: this.constructor.name
|
|
1907
|
+
}) });
|
|
1290
1908
|
};
|
|
1291
1909
|
//#endregion
|
|
1292
1910
|
//#region src/server/features/messages.ts
|
|
@@ -1322,6 +1940,10 @@ function ensureFeedbackTableExists(sql) {
|
|
|
1322
1940
|
*/
|
|
1323
1941
|
function submitMessageFeedback(sql, messageId, rating, comment, now = /* @__PURE__ */ new Date()) {
|
|
1324
1942
|
try {
|
|
1943
|
+
if (rating === 0) {
|
|
1944
|
+
sql`DELETE FROM assistant_messages_feedback WHERE message_id = ${messageId}`;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1325
1947
|
sql`INSERT INTO assistant_messages_feedback (message_id, rating, comment, created_at, updated_at)
|
|
1326
1948
|
VALUES (${messageId}, ${rating}, ${comment ?? null}, ${now.getTime()}, ${now.getTime()})
|
|
1327
1949
|
ON CONFLICT (message_id) DO UPDATE SET
|
|
@@ -1339,7 +1961,9 @@ function submitMessageFeedback(sql, messageId, rating, comment, now = /* @__PURE
|
|
|
1339
1961
|
*/
|
|
1340
1962
|
function getMessageFeedback(sql) {
|
|
1341
1963
|
try {
|
|
1342
|
-
const feedback = sql`SELECT message_id, rating, comment, created_at, updated_at
|
|
1964
|
+
const feedback = sql`SELECT message_id, rating, comment, created_at, updated_at
|
|
1965
|
+
FROM assistant_messages_feedback
|
|
1966
|
+
WHERE rating IN (1, -1)`;
|
|
1343
1967
|
return Object.fromEntries(feedback.map((row) => [row.message_id, row]));
|
|
1344
1968
|
} catch {
|
|
1345
1969
|
return {};
|
|
@@ -1501,14 +2125,19 @@ var ChatAgent = class extends Agent {
|
|
|
1501
2125
|
if (parent?.registerChat) await parent.registerChat(this.name);
|
|
1502
2126
|
return super.beforeTurn(ctx);
|
|
1503
2127
|
}
|
|
1504
|
-
async onChatResponse(
|
|
2128
|
+
async onChatResponse(result) {
|
|
2129
|
+
recordTurnCompletedAnalytics(this.env.AGENTS_ANALYTICS, {
|
|
2130
|
+
agentName: this.getTelemetryAgentName(),
|
|
2131
|
+
durableObjectName: this.name,
|
|
2132
|
+
actorId: this.getTelemetryActorId()
|
|
2133
|
+
}, result.requestId, result.status, result.continuation, result.message.parts);
|
|
1505
2134
|
const parent = await this.getParentAgent();
|
|
1506
2135
|
if (parent?.recordChatTurn) await parent.recordChatTurn(this.name, this.messages);
|
|
1507
2136
|
}
|
|
1508
2137
|
/**
|
|
1509
2138
|
* Submit feedback for a message by its id.
|
|
1510
2139
|
* @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.
|
|
2140
|
+
* @param rating - The rating to give the message. 1 = thumbs up, -1 = thumbs down, 0 = clear.
|
|
1512
2141
|
* @returns The message id and the rating.
|
|
1513
2142
|
*/
|
|
1514
2143
|
async submitMessageFeedback(messageId, rating, comment) {
|
|
@@ -1566,6 +2195,19 @@ function skill(definition) {
|
|
|
1566
2195
|
};
|
|
1567
2196
|
}
|
|
1568
2197
|
//#endregion
|
|
2198
|
+
//#region src/server/route-agent-request.ts
|
|
2199
|
+
async function routeAgentRequest(request, env, options) {
|
|
2200
|
+
const response = await routeAgentRequest$1(request, env, options);
|
|
2201
|
+
if (!response) return null;
|
|
2202
|
+
const protocol = request.headers.get("Sec-WebSocket-Protocol");
|
|
2203
|
+
if (response.status === 101 && protocol) {
|
|
2204
|
+
const newResponse = new Response(null, response);
|
|
2205
|
+
newResponse.headers.set("Sec-WebSocket-Protocol", protocol.split(",")[0].trim());
|
|
2206
|
+
return newResponse;
|
|
2207
|
+
}
|
|
2208
|
+
return response;
|
|
2209
|
+
}
|
|
2210
|
+
//#endregion
|
|
1569
2211
|
//#region src/server/index.ts
|
|
1570
2212
|
function getCurrentToolContext() {
|
|
1571
2213
|
return getCurrentAgent().agent._lastBody;
|