@economic/agents 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,6 +92,14 @@ export default {
92
92
  },
93
93
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["SupportAgent"] }],
94
94
  // see "Bindings"
95
+ "d1_databases": [
96
+ {
97
+ "binding": "AGENTS_DB",
98
+ "database_name": "agents",
99
+ "database_id": "agents",
100
+ "migrations_dir": "node_modules/@economic/agents/schema",
101
+ },
102
+ ],
95
103
  "r2_buckets": [{ "binding": "AGENTS_AUDIT_LOGS", "bucket_name": "agents-audit-logs" }],
96
104
  "analytics_engine_datasets": [{ "binding": "AGENTS_ANALYTICS", "dataset": "agents-analytics" }],
97
105
  }
package/dist/index.d.mts CHANGED
@@ -13,10 +13,11 @@ type ToolContext<RequestContext extends Record<string, unknown> = Record<string,
13
13
  _userContext?: UserContext;
14
14
  };
15
15
  interface AgentEnv {
16
- AGENT_DB: D1Database;
16
+ AGENTS_DB: D1Database;
17
17
  AGENTS_AUDIT_LOGS: R2Bucket;
18
18
  AGENTS_ANALYTICS: AnalyticsEngineDataset;
19
19
  SKILLS_BUCKET: R2Bucket;
20
+ LOCAL_AGENT_DB?: D1Database;
20
21
  }
21
22
  type SqlFn = InstanceType<typeof Agent$1>["sql"];
22
23
  type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized";
@@ -57,10 +58,10 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
57
58
  protected getParentAgent<T extends Agent$1>(): T | undefined;
58
59
  abstract getModel(ctx?: ToolContext<RequestContext, UserContext>): LanguageModel;
59
60
  abstract getSystemPrompt(ctx?: ToolContext<RequestContext, UserContext>): string;
60
- configureSession(session: Session): Session;
61
61
  onStart(): Promise<void>;
62
62
  onClose(): Promise<void>;
63
63
  onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
64
+ configureSession(session: Session): Session;
64
65
  /**
65
66
  * Merges the client request `body` into `experimental_context` for tools
66
67
  * returned from {@link getTools} only (Think-internal tools are unchanged).
@@ -116,6 +117,16 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
116
117
  * @param jwtToken - A valid JWT token following authentication.
117
118
  */
118
119
  protected getUserContext?(jwtToken: string): Promise<UserContext>;
120
+ /**
121
+ * Self-registers this DO in the global `agent_registry`. Only top-level DOs
122
+ * are registered — facets are enumerable via their parent's own index, so
123
+ * registering them would explode the row count and duplicate that index.
124
+ *
125
+ * Best-effort and idempotent: called once per cold start from `onStart`, the
126
+ * insert no-ops on conflict. A failed write is simply retried on the next
127
+ * cold start, so registration is not allowed to block the connection.
128
+ */
129
+ protected registerInstance(): Promise<void>;
119
130
  }
120
131
  //#endregion
121
132
  //#region src/server/features/messages.d.ts
@@ -152,9 +163,7 @@ type MigrationDeps = {
152
163
  /** The `Assistant` DO's bound `sql` tag (used to register migrated chats). */sql: SqlFn; /** Shared D1 database holding v1 `conversations` and `message_ratings`. */
153
164
  db: D1Database; /** The user being migrated (the `Assistant` DO name). */
154
165
  userId: string; /** Resolves a v1 chat DO stub by its `userId:chatId` name. */
155
- legacyNamespace: {
156
- getByName(name: string): LegacyChatStub;
157
- }; /** Creates (or resolves) a v2 chat facet for the given chat id and returns its stub. */
166
+ durableObjectBinding: DurableObjectNamespace; /** Creates (or resolves) a v2 chat facet for the given chat id and returns its stub. */
158
167
  createFacet: (chatId: string) => Promise<FacetStub>;
159
168
  };
160
169
  /**
@@ -221,22 +230,25 @@ type Chat = {
221
230
  declare const DELETE_CHAT_CALLBACK: "deleteChatCallback";
222
231
  //#endregion
223
232
  //#region src/server/agents/Assistant.d.ts
224
- declare abstract class Assistant extends Agent$1<Cloudflare.Env, AgentConnectionState> {
233
+ declare abstract class Assistant extends Agent$1<Cloudflare.Env & AgentEnv, AgentConnectionState> {
225
234
  initialState: AgentConnectionState;
226
235
  protected abstract agent: SubAgentClass<ChatAgent>;
227
236
  protected abstract fastModel: LanguageModel;
237
+ onStart(): void;
238
+ onClose(): Promise<void>;
239
+ onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
228
240
  /**
229
- * Binding name of the legacy v1 chat Durable Object class, used to migrate a
241
+ * Binding of the legacy v1 chat Durable Object class, used to migrate a
230
242
  * user's v1 chats into facets the first time they connect. Set this on the
231
243
  * concrete subclass to enable lazy v1 -> v2 migration; leave undefined to
232
244
  * disable it (e.g. for greenfield deployments with no v1 data).
233
245
  */
234
- protected legacyBinding?: keyof Cloudflare.Env;
246
+ protected migrationBinding?: {
247
+ binding: DurableObjectNamespace;
248
+ db: D1Database;
249
+ };
235
250
  /** In-flight migration, shared across concurrent connections to this DO. */
236
251
  private _migrationPromise?;
237
- onStart(): void;
238
- onClose(): Promise<void>;
239
- onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
240
252
  /**
241
253
  * Runs the lazy v1 -> v2 migration for this user. Concurrent connections to
242
254
  * this DO share a single in-flight run. Idempotency across runs/restarts is
@@ -251,6 +263,15 @@ declare abstract class Assistant extends Agent$1<Cloudflare.Env, AgentConnection
251
263
  recordChatTurn(durableObjectName: string, messages: UIMessage[]): Promise<void>;
252
264
  private [DELETE_CHAT_CALLBACK];
253
265
  private scheduleChatForAutoDeletion;
266
+ /**
267
+ * Self-registers this Assistant in the global `agent_registry`. An Assistant
268
+ * is a top-level DO keyed directly by user id, so the DO name *is* the actor.
269
+ *
270
+ * Runs pre-auth in onStart (the DO has already persisted state by this point,
271
+ * so it must be tracked regardless of whether auth later succeeds).
272
+ * Best-effort and idempotent: no-ops on conflict, retried on the next start.
273
+ */
274
+ private registerInstance;
254
275
  }
255
276
  //#endregion
256
277
  export { Agent, type AgentConnectionState, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, Assistant, ChatAgent, type FacetStub, type LegacyChatStub, type LegacyMessageFeedback, type MigrationDeps, type Skill, type Tool, type ToolContext, type ToolSet, getCurrentToolContext, migrateUserFromV1, routeAgentRequest, skill, tool };
package/dist/index.mjs CHANGED
@@ -42,6 +42,19 @@ var LocalSkillProvider = class {
42
42
  }
43
43
  };
44
44
  //#endregion
45
+ //#region src/server/features/registry.ts
46
+ /**
47
+ * Registers a top-level DO in the global registry.
48
+ *
49
+ * Idempotent: keyed on `(agent_name, durable_object_name)`, so repeated calls
50
+ * across cold starts no-op and `created_at` reflects the first registration.
51
+ */
52
+ async function registerAgent(db, input) {
53
+ await db.prepare(`INSERT INTO agent_registry (agent_name, durable_object_name, actor_id, created_at)
54
+ VALUES (?, ?, ?, ?)
55
+ ON CONFLICT (agent_name, durable_object_name) DO NOTHING`).bind(input.agentName, input.durableObjectName, input.actorId, input.createdAt).run();
56
+ }
57
+ //#endregion
45
58
  //#region src/server/agents/Agent.ts
46
59
  function getCurrentToolContext() {
47
60
  return getCurrentAgent().agent._lastBody;
@@ -66,26 +79,16 @@ var Agent = class extends Think {
66
79
  if (parent) return this.env[parent.className]?.getByName(parent.name);
67
80
  }
68
81
  }
69
- configureSession(session) {
70
- let configuredSession = session.withContext("soul", { provider: { get: async () => {
71
- return this.getSystemPrompt(this._requestContext !== void 0 ? this._buildToolContext() : void 0);
72
- } } }).withContext("critical-rules", { provider: { get: async () => SECURITY_RULES_PROMPT } }).withContext("turn-protocol-rules", { provider: { get: async () => TURN_PROTOCOL_RULES_PROMPT } });
73
- const remoteSkills = this.getRemoteSkills();
74
- if (remoteSkills.length) if (this.env.SKILLS_BUCKET) configuredSession = configuredSession.withContext("skills", { provider: new R2SkillProvider(this.env.SKILLS_BUCKET, {
75
- prefix: "skills/",
76
- keys: remoteSkills
77
- }) });
78
- else console.error("[Agent] Connection rejected: Remote skills defined, but no SKILLS_BUCKET R2 binding found");
79
- const localSkills = this.getSkills();
80
- if (localSkills.length) configuredSession = configuredSession.withContext("local-skills", { provider: new LocalSkillProvider(localSkills) });
81
- return configuredSession.withCachedPrompt();
82
- }
83
82
  async onStart() {
84
83
  this.setState({
85
84
  ...this.initialState,
86
85
  status: "connecting"
87
86
  });
88
87
  let hasCorrectBindings = true;
88
+ if (!this.env.AGENTS_DB) {
89
+ hasCorrectBindings = false;
90
+ console.warn("[Agent] Connection rejected: no AGENTS_DB bound. Agents database is required for registration.");
91
+ }
89
92
  if (!this.env.AGENTS_AUDIT_LOGS) {
90
93
  hasCorrectBindings = false;
91
94
  console.error("[Agent] Connection rejected: no AGENTS_AUDIT_LOGS bound. Audit logs are required.");
@@ -98,6 +101,7 @@ var Agent = class extends Think {
98
101
  });
99
102
  throw new Error("Could not connect to agent, bindings not found");
100
103
  }
104
+ this.registerInstance();
101
105
  }
102
106
  async onClose() {
103
107
  this.setState({
@@ -148,6 +152,20 @@ var Agent = class extends Think {
148
152
  status: "connected"
149
153
  });
150
154
  }
155
+ configureSession(session) {
156
+ let configuredSession = session.withContext("soul", { provider: { get: async () => {
157
+ return this.getSystemPrompt(this._requestContext !== void 0 ? this._buildToolContext() : void 0);
158
+ } } }).withContext("critical-rules", { provider: { get: async () => SECURITY_RULES_PROMPT } }).withContext("turn-protocol-rules", { provider: { get: async () => TURN_PROTOCOL_RULES_PROMPT } });
159
+ const remoteSkills = this.getRemoteSkills();
160
+ if (remoteSkills.length) if (this.env.SKILLS_BUCKET) configuredSession = configuredSession.withContext("skills", { provider: new R2SkillProvider(this.env.SKILLS_BUCKET, {
161
+ prefix: "skills/",
162
+ keys: remoteSkills
163
+ }) });
164
+ else console.error("[Agent] Connection rejected: Remote skills defined, but no SKILLS_BUCKET R2 binding found");
165
+ const localSkills = this.getSkills();
166
+ if (localSkills.length) configuredSession = configuredSession.withContext("local-skills", { provider: new LocalSkillProvider(localSkills) });
167
+ return configuredSession.withCachedPrompt();
168
+ }
151
169
  /**
152
170
  * Merges the client request `body` into `experimental_context` for tools
153
171
  * returned from {@link getTools} only (Think-internal tools are unchanged).
@@ -268,6 +286,28 @@ var Agent = class extends Think {
268
286
  * Define getUserContext to set a user context.
269
287
  */
270
288
  _userContext;
289
+ /**
290
+ * Self-registers this DO in the global `agent_registry`. Only top-level DOs
291
+ * are registered — facets are enumerable via their parent's own index, so
292
+ * registering them would explode the row count and duplicate that index.
293
+ *
294
+ * Best-effort and idempotent: called once per cold start from `onStart`, the
295
+ * insert no-ops on conflict. A failed write is simply retried on the next
296
+ * cold start, so registration is not allowed to block the connection.
297
+ */
298
+ async registerInstance() {
299
+ if (this.parentPath.length > 0) return;
300
+ try {
301
+ await registerAgent(this.env.AGENTS_DB, {
302
+ agentName: this.constructor.name,
303
+ durableObjectName: this.name,
304
+ actorId: this.getActorIdFromDurableObjectName(),
305
+ createdAt: Date.now()
306
+ });
307
+ } catch (error) {
308
+ console.error("[Agent] Failed to register in agent_registry", error);
309
+ }
310
+ }
271
311
  };
272
312
  //#endregion
273
313
  //#region src/server/features/chats.ts
@@ -423,14 +463,14 @@ function toEpochMs(value) {
423
463
  * remain as a backstop until then.
424
464
  */
425
465
  async function migrateUserFromV1(deps) {
426
- const { sql, db, userId, legacyNamespace, createFacet } = deps;
466
+ const { sql, db, userId, durableObjectBinding, createFacet } = deps;
427
467
  const conversations = await listLegacyConversations(db, userId);
428
468
  let migrated = 0;
429
469
  let failed = 0;
430
470
  for (const conversation of conversations) {
431
471
  const legacyName = conversation.durable_object_name;
432
472
  try {
433
- const { messages } = await legacyNamespace.getByName(legacyName).exportForMigration();
473
+ const { messages } = await durableObjectBinding.getByName(legacyName).exportForMigration();
434
474
  const feedback = await listLegacyFeedback(db, legacyName);
435
475
  const newChatId = nanoid();
436
476
  const facet = await createFacet(newChatId);
@@ -463,15 +503,6 @@ var Assistant = class extends Agent$1 {
463
503
  status: "connecting",
464
504
  type: "assistant"
465
505
  };
466
- /**
467
- * Binding name of the legacy v1 chat Durable Object class, used to migrate a
468
- * user's v1 chats into facets the first time they connect. Set this on the
469
- * concrete subclass to enable lazy v1 -> v2 migration; leave undefined to
470
- * disable it (e.g. for greenfield deployments with no v1 data).
471
- */
472
- legacyBinding;
473
- /** In-flight migration, shared across concurrent connections to this DO. */
474
- _migrationPromise;
475
506
  onStart() {
476
507
  this.setState({
477
508
  ...this.initialState,
@@ -479,6 +510,19 @@ var Assistant = class extends Agent$1 {
479
510
  subAgentName: this.agent.name
480
511
  });
481
512
  ensureChatsTableExists(this.sql.bind(this));
513
+ let hasCorrectBindings = true;
514
+ if (!this.env.AGENTS_DB) {
515
+ hasCorrectBindings = false;
516
+ console.warn("[Agent] Connection rejected: no AGENTS_DB bound. Agents database is required for registration.");
517
+ }
518
+ if (!hasCorrectBindings) {
519
+ this.setState({
520
+ ...this.initialState,
521
+ status: "disconnected"
522
+ });
523
+ throw new Error("Could not connect to agent, bindings not found");
524
+ }
525
+ this.registerInstance();
482
526
  }
483
527
  async onClose() {
484
528
  this.setState({
@@ -529,30 +573,35 @@ var Assistant = class extends Agent$1 {
529
573
  });
530
574
  }
531
575
  /**
576
+ * Binding of the legacy v1 chat Durable Object class, used to migrate a
577
+ * user's v1 chats into facets the first time they connect. Set this on the
578
+ * concrete subclass to enable lazy v1 -> v2 migration; leave undefined to
579
+ * disable it (e.g. for greenfield deployments with no v1 data).
580
+ */
581
+ migrationBinding;
582
+ /** In-flight migration, shared across concurrent connections to this DO. */
583
+ _migrationPromise;
584
+ /**
532
585
  * Runs the lazy v1 -> v2 migration for this user. Concurrent connections to
533
586
  * this DO share a single in-flight run. Idempotency across runs/restarts is
534
587
  * handled by `migrateUserFromV1` deleting each chat's v1 `conversations` row,
535
588
  * so an already-migrated chat is never re-enumerated.
536
589
  */
537
590
  async ensureMigrated() {
538
- if (!this.legacyBinding) return;
591
+ if (!this.migrationBinding) return;
539
592
  this._migrationPromise ??= this.runMigration().finally(() => {
540
593
  this._migrationPromise = void 0;
541
594
  });
542
595
  await this._migrationPromise;
543
596
  }
544
597
  async runMigration() {
545
- const legacyNamespace = this.env[this.legacyBinding];
546
- if (!legacyNamespace?.getByName) {
547
- console.error("[Assistant] Migration skipped: legacy binding not found", { legacyBinding: this.legacyBinding });
548
- return;
549
- }
598
+ if (!this.migrationBinding) return;
550
599
  try {
551
600
  const result = await migrateUserFromV1({
552
601
  sql: this.sql.bind(this),
553
- db: this.env.AGENT_DB,
602
+ db: this.migrationBinding.db,
554
603
  userId: this.name,
555
- legacyNamespace,
604
+ durableObjectBinding: this.migrationBinding.binding,
556
605
  createFacet: async (chatId) => {
557
606
  return await this.subAgent(this.agent, chatId);
558
607
  }
@@ -596,6 +645,26 @@ var Assistant = class extends Agent$1 {
596
645
  await Promise.all(scheduleIds.map((scheduleId) => this.cancelSchedule(scheduleId)));
597
646
  await this.schedule(new Date(Date.now() + retentionMs), DELETE_CHAT_CALLBACK, durableObjectName, { idempotent: true });
598
647
  }
648
+ /**
649
+ * Self-registers this Assistant in the global `agent_registry`. An Assistant
650
+ * is a top-level DO keyed directly by user id, so the DO name *is* the actor.
651
+ *
652
+ * Runs pre-auth in onStart (the DO has already persisted state by this point,
653
+ * so it must be tracked regardless of whether auth later succeeds).
654
+ * Best-effort and idempotent: no-ops on conflict, retried on the next start.
655
+ */
656
+ async registerInstance() {
657
+ try {
658
+ await registerAgent(this.env.AGENTS_DB, {
659
+ agentName: this.constructor.name,
660
+ durableObjectName: this.name,
661
+ actorId: this.name,
662
+ createdAt: Date.now()
663
+ });
664
+ } catch (error) {
665
+ console.error("[Assistant] Failed to register in agent_registry", error);
666
+ }
667
+ }
599
668
  };
600
669
  //#endregion
601
670
  //#region src/server/features/messages.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -0,0 +1,18 @@
1
+ -- ─── Agents registry ────────────────────────────────────────────────────────────
2
+ -- A directory of every top-level Durable Object in the system. DOs cannot be
3
+ -- enumerated via the runtime, so each top-level DO self-registers here on
4
+ -- creation (facets are omitted — they are enumerable via their parent's own
5
+ -- index). Source of truth for "what exists" and "all DOs for a given user".
6
+ -- Distinct from the audit/analytics event stream, which records "what happened".
7
+
8
+ CREATE TABLE IF NOT EXISTS agent_registry (
9
+ agent_name TEXT NOT NULL,
10
+ durable_object_name TEXT NOT NULL,
11
+ actor_id TEXT NOT NULL,
12
+ created_at INTEGER NOT NULL,
13
+
14
+ PRIMARY KEY (agent_name, durable_object_name)
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS agent_registry_actor ON agent_registry(actor_id);
18
+ CREATE INDEX IF NOT EXISTS agent_registry_class ON agent_registry(agent_name);
@@ -1,12 +0,0 @@
1
- -- ─── Audit logging ────────────────────────────────────────────────────────────
2
-
3
- CREATE TABLE IF NOT EXISTS audit_events (
4
- id TEXT PRIMARY KEY,
5
- durable_object_name TEXT NOT NULL,
6
- message TEXT NOT NULL,
7
- payload TEXT,
8
- created_at TEXT NOT NULL
9
- );
10
-
11
- CREATE INDEX IF NOT EXISTS audit_events_do ON audit_events(durable_object_name);
12
- CREATE INDEX IF NOT EXISTS audit_events_ts ON audit_events(created_at);
File without changes
File without changes
File without changes