@economic/agents 2.9.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.9.2",
3
+ "version": "2.10.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -12,7 +12,6 @@
12
12
  "exports": {
13
13
  ".": "./dist/index.mjs",
14
14
  "./providers": "./dist/providers.mjs",
15
- "./v1": "./dist/v1.mjs",
16
15
  "./package.json": "./package.json"
17
16
  },
18
17
  "scripts": {
@@ -1,24 +0,0 @@
1
- import { AgentOptions } from "agents";
2
-
3
- //#region src/server/features/auth/index.d.ts
4
- interface JwtAuthConfig<TClaims extends Record<string, unknown> = Record<string, unknown>> {
5
- /** Issuers whose tokens are accepted (exact string or RegExp). */
6
- allowedIssuers: readonly (string | RegExp)[];
7
- /** Expected `aud` claim. */
8
- audience: string;
9
- /** Required OAuth scopes; token `scope` must include all (empty = no scope check). */
10
- requiredScopes?: readonly string[];
11
- /**
12
- * Extract the claims you need from the verified JWT payload.
13
- * These will be stored and made available in the agent's tool context.
14
- */
15
- getClaims: (payload: Record<string, unknown>) => TClaims;
16
- /** Derive the actor ID represented by the verified claims. */
17
- getActorId(claims: TClaims): string;
18
- }
19
- declare function createJwtAuthConfig<TClaims extends Record<string, unknown>>(config: JwtAuthConfig<TClaims>): JwtAuthConfig<TClaims>;
20
- //#endregion
21
- //#region src/server/route-agent-request.d.ts
22
- declare function routeAgentRequest$1<Env>(request: Request, env: Env, options?: AgentOptions<Env>): Promise<Response | null>;
23
- //#endregion
24
- export { JwtAuthConfig as n, createJwtAuthConfig as r, routeAgentRequest$1 as t };
@@ -1,483 +0,0 @@
1
- import { routeAgentRequest } from "agents";
2
- import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
3
- import { BasicTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
4
- //#region src/server/features/auth/index.ts
5
- function createJwtAuthConfig(config) {
6
- return config;
7
- }
8
- const jwksByIssuer = /* @__PURE__ */ new Map();
9
- /** Returns the actor portion of a top-level or parent-scoped agent name. */
10
- function getTargetActorId(target) {
11
- const rootName = target.parentPath?.[0]?.name ?? target.durableObjectName;
12
- const separator = rootName.indexOf(":");
13
- return separator === -1 ? rootName : rootName.slice(0, separator);
14
- }
15
- /** Checks that verified JWT claims belong to the addressed agent instance. */
16
- function isJwtActorAuthorized(config, claims, target) {
17
- if (typeof config.getActorId !== "function") return false;
18
- const authenticatedActorId = config.getActorId(claims);
19
- return typeof authenticatedActorId === "string" && authenticatedActorId.length > 0 && authenticatedActorId === getTargetActorId(target);
20
- }
21
- function getJwksForIssuer(issuer) {
22
- const normalized = issuer.replace(/\/$/, "");
23
- let jwks = jwksByIssuer.get(normalized);
24
- if (!jwks) {
25
- jwks = createRemoteJWKSet(new URL(`${normalized}/.well-known/openid-configuration/jwks`));
26
- jwksByIssuer.set(normalized, jwks);
27
- }
28
- return jwks;
29
- }
30
- function isIssuerAllowed(iss, allowed) {
31
- return allowed.some((rule) => typeof rule === "string" ? rule === iss : rule.test(iss));
32
- }
33
- function extractTokenFromConnectRequest(request) {
34
- const authorization = request.headers.get("Authorization");
35
- if (authorization) {
36
- const match = authorization.match(/^Bearer\s+(.+)$/i);
37
- if (match?.[1]) return match[1].trim();
38
- }
39
- const wsProtocol = request.headers.get("Sec-WebSocket-Protocol");
40
- if (wsProtocol) {
41
- const parts = wsProtocol.split(",").map((p) => p.trim());
42
- const idx = parts.indexOf("bearer");
43
- if (idx !== -1 && parts[idx + 1]) return parts[idx + 1];
44
- }
45
- return null;
46
- }
47
- function hasRequiredScopes(tokenScope, required) {
48
- if (required.length === 0) return true;
49
- if (tokenScope === void 0) return false;
50
- const tokens = Array.isArray(tokenScope) ? tokenScope : tokenScope.split(" ").map((s) => s.trim()).filter(Boolean);
51
- const granted = new Set(tokens);
52
- return required.every((scope) => granted.has(scope));
53
- }
54
- /**
55
- * Verify a JWT from a request and extract claims.
56
- *
57
- * Extracts the token from `Authorization: Bearer` header first,
58
- * then falls back to `Sec-WebSocket-Protocol: bearer, <token>` for WebSocket connections.
59
- *
60
- * Expected auth failures (missing/expired token, insufficient scope) return a failure result.
61
- * Unexpected errors (network issues, malformed requests, untrusted issuers) throw and should
62
- * be caught and logged by the caller.
63
- *
64
- * @param request - The incoming request (from ConnectionContext)
65
- * @param config - JWT verification configuration
66
- * @returns Result object with either verified claims or expected auth failure
67
- * @throws Error for unexpected failures that should be logged
68
- */
69
- async function verifyJwt(request, config) {
70
- const token = extractTokenFromConnectRequest(request);
71
- if (!token) return {
72
- success: false,
73
- status: 401,
74
- message: "Unauthorized: Missing authentication token"
75
- };
76
- let unverifiedPayload;
77
- try {
78
- unverifiedPayload = decodeJwt(token);
79
- } catch {
80
- throw new Error("Invalid token format");
81
- }
82
- const iss = typeof unverifiedPayload.iss === "string" ? unverifiedPayload.iss : void 0;
83
- if (!iss) throw new Error("Missing issuer claim in token");
84
- if (!isIssuerAllowed(iss, config.allowedIssuers)) throw new Error(`Untrusted issuer: ${iss}`);
85
- const jwks = getJwksForIssuer(iss);
86
- let payload;
87
- try {
88
- payload = (await jwtVerify(token, jwks, {
89
- issuer: iss,
90
- audience: config.audience
91
- })).payload;
92
- } catch (error) {
93
- if (error instanceof errors.JWTExpired) return {
94
- success: false,
95
- status: 401,
96
- message: "Unauthorized: Token expired"
97
- };
98
- if (error instanceof errors.JWTClaimValidationFailed) return {
99
- success: false,
100
- status: 401,
101
- message: "Unauthorized: Token claim validation failed"
102
- };
103
- if (error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JWKSNoMatchingKey) throw new Error("Invalid token signature");
104
- throw error;
105
- }
106
- const requiredScopes = config.requiredScopes ?? [];
107
- const scopeClaim = payload.scope;
108
- if (!hasRequiredScopes(scopeClaim, requiredScopes)) return {
109
- success: false,
110
- status: 403,
111
- message: "Forbidden: Insufficient scope"
112
- };
113
- return {
114
- success: true,
115
- claims: config.getClaims(payload)
116
- };
117
- }
118
- //#endregion
119
- //#region src/server/features/telemetry/skills.ts
120
- /**
121
- * Extracts the skill names loaded by an `activate_skill` tool call.
122
- *
123
- * v1 `activate_skill` input is `{ skills: string[] }`; v2 uses
124
- * `{ name: string }`. Return every requested skill name while ignoring empty
125
- * values so analytics does not group activations under a blank skill.
126
- */
127
- function extractSkillNames(toolName, input) {
128
- if (toolName !== "activate_skill" || !input || typeof input !== "object") return [];
129
- const { name, skills } = input;
130
- if (typeof name === "string" && name) return [name];
131
- if (Array.isArray(skills)) return skills.filter((skill) => typeof skill === "string" && skill !== "");
132
- return [];
133
- }
134
- /**
135
- * Single-skill variant used where one representative skill name is sufficient
136
- * (e.g. audit-log attribution and current-skill tracking).
137
- */
138
- function extractSkillName(toolName, input) {
139
- return extractSkillNames(toolName, input)[0];
140
- }
141
- //#endregion
142
- //#region src/server/features/telemetry/utils.ts
143
- function durationMs(duration) {
144
- return duration[0] * 1e3 + duration[1] / 1e6;
145
- }
146
- function stringAttribute(span, key) {
147
- const value = span.attributes[key];
148
- return typeof value === "string" ? value : void 0;
149
- }
150
- function numberAttribute(span, key) {
151
- const value = span.attributes[key];
152
- return typeof value === "number" ? value : void 0;
153
- }
154
- function parseJson(value) {
155
- if (!value) return;
156
- try {
157
- return JSON.parse(value);
158
- } catch {
159
- return;
160
- }
161
- }
162
- //#endregion
163
- //#region src/server/features/telemetry/audit-logs.ts
164
- function safePathSegment(value, fallback) {
165
- return (value || fallback).replace(/[^a-zA-Z0-9._-]/g, "_");
166
- }
167
- function createAuditLogKey(agentName, durableObjectName) {
168
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
169
- const id = crypto.randomUUID().slice(0, 8);
170
- return [
171
- safePathSegment(agentName, "unknown-agent"),
172
- safePathSegment(durableObjectName, "unknown-chat"),
173
- `${timestamp}-${id}.json`
174
- ].join("/");
175
- }
176
- function textFromContent(content) {
177
- if (typeof content === "string") return content;
178
- if (Array.isArray(content)) return content.map((part) => {
179
- if (!part || typeof part !== "object") return "";
180
- const value = part.text;
181
- return typeof value === "string" ? value : "";
182
- }).filter(Boolean).join("\n");
183
- return "";
184
- }
185
- function extractPrompt(messages) {
186
- const firstUserMessage = messages.find((message) => message.role === "user");
187
- return firstUserMessage ? textFromContent(firstUserMessage.content) : "";
188
- }
189
- function promptCharCount(messages) {
190
- const firstUserMessage = messages.find((message) => message.role === "user");
191
- return firstUserMessage ? textFromContent(firstUserMessage.content).length : 0;
192
- }
193
- function extractTools(messages) {
194
- const tools = [];
195
- for (const message of messages) {
196
- if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
197
- for (const part of message.content) {
198
- if (!part || typeof part !== "object") continue;
199
- const record = part;
200
- if (record.type !== "tool_use") continue;
201
- tools.push({
202
- name: typeof record.name === "string" ? record.name : void 0,
203
- input: record.input,
204
- status: "success"
205
- });
206
- }
207
- }
208
- return tools;
209
- }
210
- function pushToolCall(map, key, toolCall) {
211
- const existing = map.get(key);
212
- if (existing) existing.push(toolCall);
213
- else map.set(key, [toolCall]);
214
- }
215
- /**
216
- * Stores the last ai.streamText.doStream span per chat.
217
- * Its ai.prompt.messages contains the full chat history including all
218
- * intermediate tool_use + tool_result turns from the agentic loop.
219
- */
220
- const lastDoStreamByChat = /* @__PURE__ */ new Map();
221
- const toolCallsByParentSpan = /* @__PURE__ */ new Map();
222
- const toolCallsByChat = /* @__PURE__ */ new Map();
223
- const pendingToolCalls = [];
224
- const currentSkillByChat = /* @__PURE__ */ new Map();
225
- function rememberToolCall(span) {
226
- const parentSpanId = span.parentSpanContext?.spanId;
227
- const toolName = stringAttribute(span, "ai.toolCall.name");
228
- const input = parseJson(stringAttribute(span, "ai.toolCall.args"));
229
- const toolCall = {
230
- name: toolName,
231
- input,
232
- status: span.status.code === 0 ? "success" : "error",
233
- skillName: extractSkillName(toolName, input)
234
- };
235
- if (parentSpanId) pushToolCall(toolCallsByParentSpan, parentSpanId, toolCall);
236
- pendingToolCalls.push(toolCall);
237
- }
238
- function attachToolCallsToChat(span, durableObjectName) {
239
- const spanId = span.spanContext().spanId;
240
- const toolCalls = toolCallsByParentSpan.get(spanId) ?? pendingToolCalls.splice(0);
241
- if (!toolCalls.length) return;
242
- toolCallsByParentSpan.delete(spanId);
243
- const currentSkill = currentSkillByChat.get(durableObjectName);
244
- const attributedToolCalls = toolCalls.map((toolCall) => {
245
- const skillName = toolCall.skillName ?? currentSkill;
246
- if (toolCall.skillName) currentSkillByChat.set(durableObjectName, toolCall.skillName);
247
- return {
248
- ...toolCall,
249
- ...skillName ? { skillName } : {}
250
- };
251
- });
252
- toolCallsByChat.set(durableObjectName, [...toolCallsByChat.get(durableObjectName) ?? [], ...attributedToolCalls]);
253
- }
254
- function buildAuditLog(span, context) {
255
- const lastDoStream = lastDoStreamByChat.get(context.durableObjectName);
256
- lastDoStreamByChat.delete(context.durableObjectName);
257
- const promptMessages = parseJson(stringAttribute(lastDoStream ?? span, "ai.prompt.messages"));
258
- const fallbackPrompt = parseJson(stringAttribute(span, "ai.prompt"));
259
- const inputMessages = promptMessages ?? fallbackPrompt?.messages ?? [];
260
- const spanToolCalls = toolCallsByChat.get(context.durableObjectName) ?? [];
261
- toolCallsByChat.delete(context.durableObjectName);
262
- return {
263
- id: createAuditLogKey(context.agentName, context.durableObjectName),
264
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
265
- durableObjectName: context.durableObjectName,
266
- agent: {
267
- name: context.agentName,
268
- llm: {
269
- model: stringAttribute(span, "ai.model.id"),
270
- provider: stringAttribute(span, "ai.model.provider")
271
- }
272
- },
273
- actor: {
274
- id: context.actorId,
275
- ip: {
276
- client: context.clientIp ?? context.forwardedFor?.split(",").map((ip) => ip.trim()).filter(Boolean)[0],
277
- forwardedFor: context.forwardedFor?.split(",").map((ip) => ip.trim()).filter(Boolean) ?? []
278
- }
279
- },
280
- prompt: extractPrompt(inputMessages),
281
- response: stringAttribute(span, "ai.response.text") ?? "",
282
- status: span.status.code === 0 ? "success" : "error",
283
- tools: [...extractTools(inputMessages), ...spanToolCalls]
284
- };
285
- }
286
- function rememberDoStream(span, durableObjectName) {
287
- lastDoStreamByChat.set(durableObjectName, span);
288
- }
289
- async function handleAuditSpan(span, auditLogs, context) {
290
- const auditLog = buildAuditLog(span, context);
291
- if (!auditLogs) return;
292
- await auditLogs.put(auditLog.id, JSON.stringify(auditLog, null, 2), { httpMetadata: { contentType: "application/json" } });
293
- console.log("[AuditLog] Created", auditLog.id);
294
- }
295
- //#endregion
296
- //#region src/server/features/telemetry/analytics.ts
297
- const emittedToolCallIds = /* @__PURE__ */ new Set();
298
- function safeJsonString(value) {
299
- if (value === void 0) return void 0;
300
- try {
301
- return JSON.stringify(value);
302
- } catch {
303
- return;
304
- }
305
- }
306
- function writeAnalyticsDatapoint(analytics, dataPoint) {
307
- if (!analytics) return;
308
- try {
309
- analytics.writeDataPoint(dataPoint);
310
- } catch (error) {
311
- console.error("[Agent] Failed to write analytics datapoint", error);
312
- }
313
- }
314
- /**
315
- * Writes a message-feedback rating to Analytics Engine so sentiment can be
316
- * aggregated across all chats without fanning out to every chat DO.
317
- *
318
- * Emitted on every rating change; the dashboard takes the latest rating per
319
- * `messageId` (blob4) so re-rates and clears are idempotent.
320
- */
321
- function recordFeedbackAnalytics(analytics, context, messageId, rating) {
322
- writeAnalyticsDatapoint(analytics, {
323
- indexes: [context.actorId],
324
- blobs: [
325
- "feedback",
326
- context.agentName,
327
- context.durableObjectName,
328
- messageId
329
- ],
330
- doubles: [rating]
331
- });
332
- }
333
- function handleAnalyticsSpan(span, analytics, context) {
334
- if (span.name === "ai.streamText.doStream") {
335
- const promptMessages = parseJson(stringAttribute(span, "ai.prompt.messages"));
336
- const responseText = stringAttribute(span, "ai.response.text") ?? "";
337
- const responseToolCalls = parseJson(stringAttribute(span, "ai.response.toolCalls"));
338
- writeAnalyticsDatapoint(analytics, {
339
- indexes: [context.actorId],
340
- blobs: [
341
- "llm_call",
342
- context.agentName,
343
- context.durableObjectName,
344
- stringAttribute(span, "ai.model.id") ?? "",
345
- stringAttribute(span, "ai.model.provider") ?? "",
346
- stringAttribute(span, "ai.response.finishReason") ?? ""
347
- ],
348
- doubles: [
349
- numberAttribute(span, "ai.usage.inputTokens") ?? 0,
350
- numberAttribute(span, "ai.usage.outputTokens") ?? 0,
351
- numberAttribute(span, "ai.usage.totalTokens") ?? 0,
352
- numberAttribute(span, "ai.usage.inputTokenDetails.cacheReadTokens") ?? 0,
353
- numberAttribute(span, "ai.usage.inputTokenDetails.cacheWriteTokens") ?? 0,
354
- durationMs(span.duration),
355
- promptCharCount(promptMessages ?? []),
356
- responseText.length
357
- ]
358
- });
359
- if (Array.isArray(responseToolCalls)) for (const value of responseToolCalls) {
360
- if (!value || typeof value !== "object") continue;
361
- const toolCall = value;
362
- const toolCallId = typeof toolCall.toolCallId === "string" ? toolCall.toolCallId : void 0;
363
- if (toolCallId && emittedToolCallIds.has(toolCallId)) continue;
364
- const toolName = typeof toolCall.toolName === "string" ? toolCall.toolName : void 0;
365
- if (!toolName) continue;
366
- const input = toolCall.input;
367
- const argsRaw = safeJsonString(input);
368
- const success = span.status.code === 0;
369
- const activatedSkills = extractSkillNames(toolName, input);
370
- const skillNames = activatedSkills.length ? activatedSkills : [currentSkillByChat.get(context.durableObjectName) ?? ""];
371
- if (toolCallId) emittedToolCallIds.add(toolCallId);
372
- for (const skillName of skillNames) writeAnalyticsDatapoint(analytics, {
373
- indexes: [context.actorId],
374
- blobs: [
375
- "tool_call",
376
- context.agentName,
377
- context.durableObjectName,
378
- toolName,
379
- skillName,
380
- success ? "success" : "error"
381
- ],
382
- doubles: [
383
- 0,
384
- success ? 1 : 0,
385
- argsRaw?.length ?? 0,
386
- 0,
387
- 0,
388
- 0,
389
- 0,
390
- 0
391
- ]
392
- });
393
- }
394
- return;
395
- }
396
- if (span.name === "ai.toolCall") {
397
- const toolName = stringAttribute(span, "ai.toolCall.name");
398
- const toolCallId = stringAttribute(span, "ai.toolCall.id");
399
- if (toolCallId && emittedToolCallIds.has(toolCallId)) return;
400
- if (toolCallId) emittedToolCallIds.add(toolCallId);
401
- const argsRaw = stringAttribute(span, "ai.toolCall.args");
402
- const toolInput = parseJson(argsRaw);
403
- const success = span.status.code === 0;
404
- const duration = durationMs(span.duration);
405
- const argsSize = argsRaw?.length ?? 0;
406
- const resultSize = stringAttribute(span, "ai.toolCall.result")?.length ?? 0;
407
- const activatedSkills = extractSkillNames(toolName, toolInput);
408
- const skillNames = activatedSkills.length ? activatedSkills : [currentSkillByChat.get(context.durableObjectName) ?? ""];
409
- for (const skillName of skillNames) writeAnalyticsDatapoint(analytics, {
410
- indexes: [context.actorId],
411
- blobs: [
412
- "tool_call",
413
- context.agentName,
414
- context.durableObjectName,
415
- toolName ?? "",
416
- skillName,
417
- success ? "success" : "error"
418
- ],
419
- doubles: [
420
- duration,
421
- success ? 1 : 0,
422
- argsSize,
423
- resultSize,
424
- 0,
425
- 0,
426
- 0,
427
- 0
428
- ]
429
- });
430
- }
431
- }
432
- //#endregion
433
- //#region src/server/features/telemetry/index.ts
434
- var AgentSpanExporter = class {
435
- auditLogs;
436
- analytics;
437
- context;
438
- constructor(auditLogs, analytics, context) {
439
- this.auditLogs = auditLogs;
440
- this.analytics = analytics;
441
- this.context = context;
442
- }
443
- export(spans, resultCallback) {
444
- (async () => {
445
- try {
446
- for (const span of spans) if (span.name === "ai.streamText.doStream") {
447
- rememberDoStream(span, this.context.durableObjectName);
448
- attachToolCallsToChat(span, this.context.durableObjectName);
449
- handleAnalyticsSpan(span, this.analytics, this.context);
450
- } else if (span.name === "ai.streamText") await handleAuditSpan(span, this.auditLogs, this.context);
451
- else if (span.name === "ai.toolCall") {
452
- rememberToolCall(span);
453
- handleAnalyticsSpan(span, this.analytics, this.context);
454
- }
455
- resultCallback({ code: 0 });
456
- } catch (error) {
457
- resultCallback({
458
- code: 1,
459
- error: error instanceof Error ? error : new Error(String(error))
460
- });
461
- }
462
- })();
463
- }
464
- async shutdown() {}
465
- };
466
- function createAgentTracer(auditLogs, analytics, context) {
467
- return new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(new AgentSpanExporter(auditLogs, analytics, context))] }).getTracer("@economic/agents");
468
- }
469
- //#endregion
470
- //#region src/server/route-agent-request.ts
471
- async function routeAgentRequest$1(request, env, options) {
472
- const response = await routeAgentRequest(request, env, options);
473
- if (!response) return null;
474
- const protocol = request.headers.get("Sec-WebSocket-Protocol");
475
- if (response.status === 101 && protocol) {
476
- const newResponse = new Response(null, response);
477
- newResponse.headers.set("Sec-WebSocket-Protocol", protocol.split(",")[0].trim());
478
- return newResponse;
479
- }
480
- return response;
481
- }
482
- //#endregion
483
- export { extractTokenFromConnectRequest as a, verifyJwt as c, createJwtAuthConfig as i, createAgentTracer as n, getTargetActorId as o, recordFeedbackAnalytics as r, isJwtActorAuthorized as s, routeAgentRequest$1 as t };