@economic/agents 2.3.18 → 2.3.20

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.d.mts CHANGED
@@ -114,6 +114,32 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
114
114
  * cold start, so registration is not allowed to block the connection.
115
115
  */
116
116
  protected registerInstance(): Promise<void>;
117
+ /**
118
+ * The `(agent_name, durable_object_name)` key of the registered row for this
119
+ * DO. For a parented (facet) agent this resolves to the top-level parent's
120
+ * row, since facets are not registered themselves.
121
+ */
122
+ protected getRegisteredInstanceKey(): {
123
+ agentName: string;
124
+ durableObjectName: string;
125
+ };
126
+ /**
127
+ * Records activity against this DO's registered row by advancing its
128
+ * `updated_at`, so instances can be ordered by recency of activity.
129
+ * Best-effort and non-blocking — never allowed to interfere with a turn.
130
+ */
131
+ protected touchInstance(): Promise<void>;
132
+ /**
133
+ * Resolves the analytics/telemetry agent name to the entry-point agent: the
134
+ * top-level DO registered in the `agent` catalog table.
135
+ *
136
+ * `parentPath` is root-first and empty for top-level DOs, so:
137
+ * - chat agent running under an Assistant → the Assistant's class name
138
+ * (e.g. `EvaAssistant`), since the Assistant is the catalog entry point;
139
+ * - standalone chat agent → its own class name (e.g. `EvaAgent`).
140
+ */
141
+ protected getTelemetryAgentName(): string;
142
+ protected getTelemetryActorId(): string;
117
143
  }
118
144
  //#endregion
119
145
  //#region src/server/features/messages.d.ts
@@ -238,6 +264,13 @@ declare abstract class Assistant<State = unknown> extends Agent$1<Cloudflare.Env
238
264
  * Best-effort and idempotent: no-ops on conflict, retried on the next start.
239
265
  */
240
266
  private registerInstance;
267
+ /**
268
+ * Records activity against this Assistant's registry row by advancing its
269
+ * `updated_at`, so instances can be ordered by recency of activity. An
270
+ * Assistant is a top-level DO, so its row is keyed by its own class + name.
271
+ * Best-effort and non-blocking.
272
+ */
273
+ private touchInstance;
241
274
  /**
242
275
  * Binding of the legacy v1 chat Durable Object class, used to migrate a
243
276
  * user's v1 chats into facets the first time they connect. Set this on the
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as verifyJwt, n as createAgentTracer, r as extractTokenFromConnectRequest, t as routeAgentRequest } from "./route-agent-request-lVm3eus2.mjs";
1
+ import { a as verifyJwt, i as extractTokenFromConnectRequest, n as createAgentTracer, r as recordFeedbackAnalytics, t as routeAgentRequest } from "./route-agent-request-sspiKVkm.mjs";
2
2
  import { Agent as Agent$1, callable, getCurrentAgent } from "agents";
3
3
  import { Think, skills } from "@cloudflare/think";
4
4
  import { Output, convertToModelMessages, generateText, jsonSchema, pruneMessages, tool as tool$1 } from "ai";
@@ -28,12 +28,31 @@ const TURN_PROTOCOL_RULES_PROMPT = `These rules are specific for responding to u
28
28
  * Registers a top-level DO in the global registry.
29
29
  *
30
30
  * Idempotent: keyed on `(agent_name, durable_object_name)`, so repeated calls
31
- * across cold starts no-op and `created_at` reflects the first registration.
31
+ * across cold starts no-op and timestamps reflect the first registration.
32
+ * `updated_at` is seeded to `created_at`; ongoing activity is recorded
33
+ * separately via {@link touchAgentInstance} so cold-start re-registration
34
+ * never inflates the activity timestamp.
32
35
  */
33
36
  async function registerAgentInstance(db, input) {
34
- await db.prepare(`INSERT INTO agent_registry (agent_name, durable_object_name, actor_id, created_at)
35
- VALUES (?, ?, ?, ?)
36
- ON CONFLICT (agent_name, durable_object_name) DO NOTHING`).bind(input.agentName, input.durableObjectName, input.actorId, input.createdAt).run();
37
+ await db.prepare(`INSERT INTO agent_registry (agent_name, durable_object_name, actor_id, created_at, updated_at)
38
+ VALUES (?, ?, ?, ?, ?)
39
+ ON CONFLICT (agent_name, durable_object_name) DO NOTHING`).bind(input.agentName, input.durableObjectName, input.actorId, input.createdAt, input.createdAt).run();
40
+ }
41
+ /**
42
+ * Records activity on a registered top-level DO by advancing its `updated_at`.
43
+ *
44
+ * Used to order instances by recency of activity. Keyed on
45
+ * `(agent_name, durable_object_name)` to target the exact registered row —
46
+ * for a parented (facet) agent this is the top-level parent's row, since
47
+ * facets are not registered themselves.
48
+ *
49
+ * Best-effort: a no-op if the row does not yet exist (registration is
50
+ * retried on the next cold start).
51
+ */
52
+ async function touchAgentInstance(db, input) {
53
+ await db.prepare(`UPDATE agent_registry
54
+ SET updated_at = ?
55
+ WHERE agent_name = ? AND durable_object_name = ?`).bind(input.updatedAt, input.agentName, input.durableObjectName).run();
37
56
  }
38
57
  //#endregion
39
58
  //#region src/server/util/agent.ts
@@ -172,6 +191,7 @@ var Agent = class extends Think {
172
191
  * `returned.tools` into your own `TurnConfig.tools` if you return tools.
173
192
  */
174
193
  async beforeTurn(ctx) {
194
+ this.touchInstance();
175
195
  this._toolContextForCurrentTurn = this.getToolContextForCurrentTurn(ctx.body);
176
196
  this._toolsForCurrentTurn = ctx.tools;
177
197
  const activeSkillName = this._getLastActivatedSkillName(ctx.messages);
@@ -195,17 +215,17 @@ var Agent = class extends Think {
195
215
  experimental_telemetry: {
196
216
  isEnabled: true,
197
217
  tracer: createAgentTracer(this.env.AGENTS_AUDIT_LOGS, this.env.AGENTS_ANALYTICS, {
198
- agentName: this.constructor.name,
218
+ agentName: this.getTelemetryAgentName(),
199
219
  durableObjectName: this.name,
200
- actorId: this.getActorIdFromDurableObjectName(),
220
+ actorId: this.getTelemetryActorId(),
201
221
  clientIp: this.clientIp,
202
222
  forwardedFor: this.forwardedFor
203
223
  }),
204
224
  metadata: {
205
- agentName: this.constructor.name,
225
+ agentName: this.getTelemetryAgentName(),
206
226
  version: "v2",
207
227
  durableObjectName: this.name,
208
- actorId: this.getActorIdFromDurableObjectName()
228
+ actorId: this.getTelemetryActorId()
209
229
  }
210
230
  }
211
231
  };
@@ -312,6 +332,47 @@ var Agent = class extends Think {
312
332
  console.error("[Agent] Failed to register in agent_registry", error);
313
333
  }
314
334
  }
335
+ /**
336
+ * The `(agent_name, durable_object_name)` key of the registered row for this
337
+ * DO. For a parented (facet) agent this resolves to the top-level parent's
338
+ * row, since facets are not registered themselves.
339
+ */
340
+ getRegisteredInstanceKey() {
341
+ return {
342
+ agentName: this.parentPath[0]?.className ?? this.constructor.name,
343
+ durableObjectName: this.parentPath[0]?.name ?? this.name
344
+ };
345
+ }
346
+ /**
347
+ * Records activity against this DO's registered row by advancing its
348
+ * `updated_at`, so instances can be ordered by recency of activity.
349
+ * Best-effort and non-blocking — never allowed to interfere with a turn.
350
+ */
351
+ async touchInstance() {
352
+ try {
353
+ await touchAgentInstance(this.env.AGENTS_DB, {
354
+ ...this.getRegisteredInstanceKey(),
355
+ updatedAt: Date.now()
356
+ });
357
+ } catch (error) {
358
+ console.error("[Agent] Failed to update agent_registry activity", error);
359
+ }
360
+ }
361
+ /**
362
+ * Resolves the analytics/telemetry agent name to the entry-point agent: the
363
+ * top-level DO registered in the `agent` catalog table.
364
+ *
365
+ * `parentPath` is root-first and empty for top-level DOs, so:
366
+ * - chat agent running under an Assistant → the Assistant's class name
367
+ * (e.g. `EvaAssistant`), since the Assistant is the catalog entry point;
368
+ * - standalone chat agent → its own class name (e.g. `EvaAgent`).
369
+ */
370
+ getTelemetryAgentName() {
371
+ return this.parentPath[0]?.className ?? this.constructor.name;
372
+ }
373
+ getTelemetryActorId() {
374
+ return this.parentPath[0]?.name ?? this.getActorIdFromDurableObjectName();
375
+ }
315
376
  };
316
377
  //#endregion
317
378
  //#region src/server/features/chats.ts
@@ -703,6 +764,7 @@ var Assistant = class extends Agent$1 {
703
764
  const now = Date.now();
704
765
  await this.subAgent(this.agent, id);
705
766
  registerChat(this.sql.bind(this), id, now);
767
+ this.touchInstance(now);
706
768
  return id;
707
769
  }
708
770
  async deleteChat(id) {
@@ -747,6 +809,23 @@ var Assistant = class extends Agent$1 {
747
809
  }
748
810
  }
749
811
  /**
812
+ * Records activity against this Assistant's registry row by advancing its
813
+ * `updated_at`, so instances can be ordered by recency of activity. An
814
+ * Assistant is a top-level DO, so its row is keyed by its own class + name.
815
+ * Best-effort and non-blocking.
816
+ */
817
+ async touchInstance(updatedAt) {
818
+ try {
819
+ await touchAgentInstance(this.env.AGENTS_DB, {
820
+ agentName: this.constructor.name,
821
+ durableObjectName: this.name,
822
+ updatedAt
823
+ });
824
+ } catch (error) {
825
+ console.error("[Assistant] Failed to update agent_registry activity", error);
826
+ }
827
+ }
828
+ /**
750
829
  * Binding of the legacy v1 chat Durable Object class, used to migrate a
751
830
  * user's v1 chats into facets the first time they connect. Set this on the
752
831
  * concrete subclass to enable lazy v1 -> v2 migration; leave undefined to
@@ -1012,7 +1091,13 @@ var ChatAgent = class extends Agent {
1012
1091
  * @returns The message id and the rating.
1013
1092
  */
1014
1093
  async submitMessageFeedback(messageId, rating, comment) {
1015
- return submitMessageFeedback(this.sql.bind(this), messageId, rating, comment);
1094
+ const result = submitMessageFeedback(this.sql.bind(this), messageId, rating, comment);
1095
+ recordFeedbackAnalytics(this.env.AGENTS_ANALYTICS, {
1096
+ agentName: this.getTelemetryAgentName(),
1097
+ durableObjectName: this.name,
1098
+ actorId: this.getTelemetryActorId()
1099
+ }, messageId, rating);
1100
+ return result;
1016
1101
  }
1017
1102
  /**
1018
1103
  * Returns all message feedback for the current chat.
@@ -101,6 +101,27 @@ async function verifyJwt(request, config) {
101
101
  };
102
102
  }
103
103
  //#endregion
104
+ //#region src/server/features/telemetry/skills.ts
105
+ /**
106
+ * Extracts the skill names loaded by an `activate_skill` tool call.
107
+ *
108
+ * v2 `activate_skill` input is `{ skills: string[] }` — a single call can load
109
+ * multiple skills, so this returns every requested skill name.
110
+ */
111
+ function extractSkillNames(toolName, input) {
112
+ if (toolName !== "activate_skill" || !input || typeof input !== "object") return [];
113
+ const { skills } = input;
114
+ if (!Array.isArray(skills)) return [];
115
+ return skills.filter((skill) => typeof skill === "string");
116
+ }
117
+ /**
118
+ * Single-skill variant used where one representative skill name is sufficient
119
+ * (e.g. audit-log attribution and current-skill tracking).
120
+ */
121
+ function extractSkillName(toolName, input) {
122
+ return extractSkillNames(toolName, input)[0];
123
+ }
124
+ //#endregion
104
125
  //#region src/server/features/telemetry/utils.ts
105
126
  function durationMs(duration) {
106
127
  return duration[0] * 1e3 + duration[1] / 1e6;
@@ -169,18 +190,6 @@ function extractTools(messages) {
169
190
  }
170
191
  return tools;
171
192
  }
172
- function extractSkillName(toolName, input) {
173
- if (!input || typeof input !== "object") return;
174
- const record = input;
175
- if (toolName === "load_context") {
176
- const key = record.key;
177
- return typeof key === "string" ? key : void 0;
178
- }
179
- if (toolName === "activate_skill") {
180
- const skill = record.skill ?? record.name ?? record.key;
181
- return typeof skill === "string" ? skill : void 0;
182
- }
183
- }
184
193
  function pushToolCall(map, key, toolCall) {
185
194
  const existing = map.get(key);
186
195
  if (existing) existing.push(toolCall);
@@ -276,16 +285,35 @@ function writeAnalyticsDatapoint(analytics, dataPoint) {
276
285
  console.error("[Agent] Failed to write analytics datapoint", error);
277
286
  }
278
287
  }
279
- function handleAnalyticsSpan(span, analytics) {
288
+ /**
289
+ * Writes a message-feedback rating to Analytics Engine so sentiment can be
290
+ * aggregated across all chats without fanning out to every chat DO.
291
+ *
292
+ * Emitted on every rating change; the dashboard takes the latest rating per
293
+ * `messageId` (blob4) so re-rates and clears are idempotent.
294
+ */
295
+ function recordFeedbackAnalytics(analytics, context, messageId, rating) {
296
+ writeAnalyticsDatapoint(analytics, {
297
+ indexes: [context.actorId],
298
+ blobs: [
299
+ "feedback",
300
+ context.agentName,
301
+ context.durableObjectName,
302
+ messageId
303
+ ],
304
+ doubles: [rating]
305
+ });
306
+ }
307
+ function handleAnalyticsSpan(span, analytics, context) {
280
308
  if (span.name === "ai.streamText.doStream") {
281
309
  const promptMessages = parseJson(stringAttribute(span, "ai.prompt.messages"));
282
310
  const responseText = stringAttribute(span, "ai.response.text") ?? "";
283
311
  writeAnalyticsDatapoint(analytics, {
284
- indexes: [stringAttribute(span, "ai.telemetry.metadata.actorId") ?? ""],
312
+ indexes: [context.actorId],
285
313
  blobs: [
286
314
  "llm_call",
287
- stringAttribute(span, "ai.telemetry.metadata.agentName") ?? "",
288
- stringAttribute(span, "ai.telemetry.metadata.durableObjectName") ?? "",
315
+ context.agentName,
316
+ context.durableObjectName,
289
317
  stringAttribute(span, "ai.model.id") ?? "",
290
318
  stringAttribute(span, "ai.model.provider") ?? "",
291
319
  stringAttribute(span, "ai.response.finishReason") ?? ""
@@ -305,25 +333,29 @@ function handleAnalyticsSpan(span, analytics) {
305
333
  }
306
334
  if (span.name === "ai.toolCall") {
307
335
  const toolName = stringAttribute(span, "ai.toolCall.name");
308
- const toolInput = parseJson(stringAttribute(span, "ai.toolCall.args"));
309
- const durableObjectName = stringAttribute(span, "ai.telemetry.metadata.durableObjectName") ?? "";
310
- const skillName = extractSkillName(toolName, toolInput) ?? currentSkillByChat.get(durableObjectName) ?? "";
336
+ const argsRaw = stringAttribute(span, "ai.toolCall.args");
337
+ const toolInput = parseJson(argsRaw);
311
338
  const success = span.status.code === 0;
312
- writeAnalyticsDatapoint(analytics, {
313
- indexes: [stringAttribute(span, "ai.telemetry.metadata.actorId") ?? ""],
339
+ const duration = durationMs(span.duration);
340
+ const argsSize = argsRaw?.length ?? 0;
341
+ const resultSize = stringAttribute(span, "ai.toolCall.result")?.length ?? 0;
342
+ const activatedSkills = extractSkillNames(toolName, toolInput);
343
+ const skillNames = activatedSkills.length ? activatedSkills : [currentSkillByChat.get(context.durableObjectName) ?? ""];
344
+ for (const skillName of skillNames) writeAnalyticsDatapoint(analytics, {
345
+ indexes: [context.actorId],
314
346
  blobs: [
315
347
  "tool_call",
316
- stringAttribute(span, "ai.telemetry.metadata.agentName") ?? "",
317
- durableObjectName,
348
+ context.agentName,
349
+ context.durableObjectName,
318
350
  toolName ?? "",
319
351
  skillName,
320
352
  success ? "success" : "error"
321
353
  ],
322
354
  doubles: [
323
- durationMs(span.duration),
355
+ duration,
324
356
  success ? 1 : 0,
325
- 0,
326
- 0,
357
+ argsSize,
358
+ resultSize,
327
359
  0,
328
360
  0,
329
361
  0,
@@ -349,11 +381,11 @@ var AgentSpanExporter = class {
349
381
  for (const span of spans) if (span.name === "ai.streamText.doStream") {
350
382
  rememberDoStream(span, this.context.durableObjectName);
351
383
  attachToolCallsToChat(span, this.context.durableObjectName);
352
- handleAnalyticsSpan(span, this.analytics);
384
+ handleAnalyticsSpan(span, this.analytics, this.context);
353
385
  } else if (span.name === "ai.streamText") await handleAuditSpan(span, this.auditLogs, this.context);
354
386
  else if (span.name === "ai.toolCall") {
355
387
  rememberToolCall(span);
356
- handleAnalyticsSpan(span, this.analytics);
388
+ handleAnalyticsSpan(span, this.analytics, this.context);
357
389
  }
358
390
  resultCallback({ code: 0 });
359
391
  } catch (error) {
@@ -383,4 +415,4 @@ async function routeAgentRequest$1(request, env, options) {
383
415
  return response;
384
416
  }
385
417
  //#endregion
386
- export { verifyJwt as i, createAgentTracer as n, extractTokenFromConnectRequest as r, routeAgentRequest$1 as t };
418
+ export { verifyJwt as a, extractTokenFromConnectRequest as i, createAgentTracer as n, recordFeedbackAnalytics as r, routeAgentRequest$1 as t };
package/dist/v1.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as verifyJwt, n as createAgentTracer, r as extractTokenFromConnectRequest, t as routeAgentRequest } from "./route-agent-request-lVm3eus2.mjs";
1
+ import { a as verifyJwt, i as extractTokenFromConnectRequest, n as createAgentTracer, t as routeAgentRequest } from "./route-agent-request-sspiKVkm.mjs";
2
2
  import { Agent as Agent$1, callable, getCurrentAgent } from "agents";
3
3
  import { Output, convertToModelMessages, generateText, jsonSchema, stepCountIs, streamText, tool } from "ai";
4
4
  import { AIChatAgent } from "@cloudflare/ai-chat";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.3.18",
3
+ "version": "2.3.20",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -26,9 +26,9 @@
26
26
  "@ai-sdk/anthropic": "^3.0.77",
27
27
  "@ai-sdk/google": "^3.0.73",
28
28
  "@cloudflare/ai-chat": "^0.8.5",
29
- "@cloudflare/think": "^0.9.0",
29
+ "@cloudflare/think": ">=0.9.0",
30
30
  "@opentelemetry/sdk-trace-base": "^2.8.0",
31
- "agents": "^0.16.0",
31
+ "agents": ">=0.16.0 <1.0.0",
32
32
  "ai": "^6.0.197",
33
33
  "jose": "^6.2.3",
34
34
  "nanoid": "^5.1.11"
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@cloudflare/think": ">=0.9.0",
49
- "@cloudflare/vite-plugin": ">=1.40.2",
49
+ "@cloudflare/vite-plugin": "^1.42.0",
50
50
  "@cloudflare/worker-bundler": ">=0.2.1",
51
51
  "agents": ">=0.16.0 <1.0.0",
52
52
  "vite": ">=8.0.0"
@@ -0,0 +1,13 @@
1
+ -- ─── agent_registry.updated_at ────────────────────────────────────────────────────
2
+ -- Adds an activity timestamp to the registry so top-level DOs (one row per
3
+ -- Assistant, i.e. per user) can be ordered by recency of activity. Bumped by
4
+ -- the owning DO whenever a chat is created or has a turn.
5
+ --
6
+ -- Existing rows are backfilled to created_at so ordering remains sensible
7
+ -- before any new activity updates the column.
8
+
9
+ ALTER TABLE agent_registry ADD COLUMN updated_at INTEGER;
10
+
11
+ UPDATE agent_registry SET updated_at = created_at WHERE updated_at IS NULL;
12
+
13
+ CREATE INDEX IF NOT EXISTS agent_registry_updated ON agent_registry(updated_at);