agents 0.7.0 → 0.7.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.
@@ -7,6 +7,17 @@ type BaseEvent<
7
7
  Payload extends Record<string, unknown> = Record<string, never>
8
8
  > = {
9
9
  type: T;
10
+ /**
11
+ * The class name of the agent that emitted this event
12
+ * (e.g. "MyChatAgent").
13
+ * Always present on events emitted by an Agent instance.
14
+ */
15
+ agent?: string;
16
+ /**
17
+ * The instance name (Durable Object ID name) of the agent.
18
+ * Always present on events emitted by an Agent instance.
19
+ */
20
+ name?: string;
10
21
  /**
11
22
  * The payload of the event
12
23
  */
@@ -145,6 +156,13 @@ type AgentObservabilityEvent =
145
156
  attempts: number;
146
157
  }
147
158
  >
159
+ | BaseEvent<
160
+ "queue:create",
161
+ {
162
+ callback: string;
163
+ id: string;
164
+ }
165
+ >
148
166
  | BaseEvent<
149
167
  "queue:retry",
150
168
  {
@@ -170,6 +188,30 @@ type AgentObservabilityEvent =
170
188
  connectionId: string;
171
189
  }
172
190
  >
191
+ | BaseEvent<
192
+ "disconnect",
193
+ {
194
+ connectionId: string;
195
+ code: number;
196
+ reason: string;
197
+ }
198
+ >
199
+ | BaseEvent<
200
+ "email:receive",
201
+ {
202
+ from: string;
203
+ to: string;
204
+ subject?: string;
205
+ }
206
+ >
207
+ | BaseEvent<
208
+ "email:reply",
209
+ {
210
+ from: string;
211
+ to: string;
212
+ subject?: string;
213
+ }
214
+ >
173
215
  | BaseEvent<
174
216
  "workflow:start",
175
217
  {
@@ -228,4 +270,4 @@ type AgentObservabilityEvent =
228
270
  >;
229
271
  //#endregion
230
272
  export { MCPObservabilityEvent as n, AgentObservabilityEvent as t };
231
- //# sourceMappingURL=agent-DnmmRjyv.d.ts.map
273
+ //# sourceMappingURL=agent-eZnMHidZ.d.ts.map
@@ -1,4 +1,4 @@
1
- import { n as MCPObservabilityEvent } from "./agent-DnmmRjyv.js";
1
+ import { n as MCPObservabilityEvent } from "./agent-eZnMHidZ.js";
2
2
  import { AgentMcpOAuthProvider } from "./mcp/do-oauth-client-provider.js";
3
3
  import { McpAgent } from "./mcp/index.js";
4
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -601,4 +601,4 @@ export {
601
601
  MaybePromise as x,
602
602
  StreamableHTTPEdgeClientTransport as y
603
603
  };
604
- //# sourceMappingURL=client-storage-tusTuoSF.d.ts.map
604
+ //# sourceMappingURL=client-storage-BPjfP_is.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { RetryOptions } from "../retries.js";
2
- import "../client-storage-tusTuoSF.js";
2
+ import "../client-storage-BPjfP_is.js";
3
3
  import { Agent, Schedule } from "../index.js";
4
4
 
5
5
  //#region src/experimental/forever.d.ts
@@ -92,12 +92,13 @@ declare function withFibers<TBase extends AgentLike>(Base: TBase, options?: {
92
92
  _checkInterruptedFibers(): Promise<void>; /** @internal */
93
93
  _cleanupOrphanedHeartbeats(): void; /** @internal */
94
94
  _maybeCleanupFibers(): void;
95
- readonly alarm: () => Promise<void>;
96
95
  sql: <T = Record<string, string | number | boolean | null>>(strings: TemplateStringsArray, ...values: (string | number | boolean | null)[]) => T[];
97
96
  scheduleEvery: <T = string>(intervalSeconds: number, callback: keyof Agent<Cloudflare.Env, unknown, Record<string, unknown>>, payload?: T | undefined, options?: {
98
97
  retry?: RetryOptions;
98
+ _idempotent?: boolean;
99
99
  }) => Promise<Schedule<T>>;
100
100
  cancelSchedule: (id: string) => Promise<boolean>;
101
+ alarm: () => Promise<void>;
101
102
  keepAlive: () => Promise<() => void>;
102
103
  keepAliveWhile: <T>(fn: () => Promise<T>) => Promise<T>;
103
104
  };
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ import { RetryOptions } from "./retries.js";
7
7
  import {
8
8
  r as MCPConnectionState,
9
9
  w as TransportType
10
- } from "./client-storage-tusTuoSF.js";
10
+ } from "./client-storage-BPjfP_is.js";
11
11
  import {
12
12
  AgentMcpOAuthProvider,
13
13
  AgentsOAuthProvider,
@@ -23,7 +23,7 @@ import {
23
23
  WorkflowPage,
24
24
  WorkflowQueryCriteria
25
25
  } from "./workflow-types.js";
26
- import { Observability } from "./observability/index.js";
26
+ import { Observability, ObservabilityEvent } from "./observability/index.js";
27
27
  import { MessageType } from "./types.js";
28
28
  import {
29
29
  Connection,
@@ -325,7 +325,10 @@ declare class Agent<
325
325
  * Emit an observability event with auto-generated timestamp.
326
326
  * @internal
327
327
  */
328
- private _emit;
328
+ protected _emit(
329
+ type: ObservabilityEvent["type"],
330
+ payload?: Record<string, unknown>
331
+ ): void;
329
332
  /**
330
333
  * Execute SQL queries against the Agent's database
331
334
  * @template T Type of the returned rows
@@ -593,7 +596,23 @@ declare class Agent<
593
596
  }
594
597
  ): Promise<Schedule<T>>;
595
598
  /**
596
- * Schedule a task to run repeatedly at a fixed interval
599
+ * Schedule a task to run repeatedly at a fixed interval.
600
+ *
601
+ * This method is **idempotent** — calling it multiple times with the same
602
+ * `callback`, `intervalSeconds`, and `payload` returns the existing schedule
603
+ * instead of creating a duplicate. A different interval or payload is
604
+ * treated as a distinct schedule and creates a new row.
605
+ *
606
+ * This makes it safe to call in `onStart()`, which runs on every Durable
607
+ * Object wake:
608
+ *
609
+ * ```ts
610
+ * async onStart() {
611
+ * // Only one schedule is created, no matter how many times the DO wakes
612
+ * await this.scheduleEvery(30, "tick");
613
+ * }
614
+ * ```
615
+ *
597
616
  * @template T Type of the payload data
598
617
  * @param intervalSeconds Number of seconds between executions
599
618
  * @param callback Name of the method to call
@@ -608,6 +627,7 @@ declare class Agent<
608
627
  payload?: T,
609
628
  options?: {
610
629
  retry?: RetryOptions;
630
+ _idempotent?: boolean;
611
631
  }
612
632
  ): Promise<Schedule<T>>;
613
633
  /**
@@ -685,15 +705,25 @@ declare class Agent<
685
705
  */
686
706
  _cf_keepAliveHeartbeat(): Promise<void>;
687
707
  private _scheduleNextAlarm;
708
+ /**
709
+ * Override PartyServer's onAlarm hook as a no-op.
710
+ * Agent handles alarm logic directly in the alarm() method override,
711
+ * but super.alarm() calls onAlarm() after #ensureInitialized(),
712
+ * so we suppress the default "Implement onAlarm" warning.
713
+ */
714
+ onAlarm(): void;
688
715
  /**
689
716
  * Method called when an alarm fires.
690
717
  * Executes any scheduled tasks that are due.
691
718
  *
719
+ * Calls super.alarm() first to ensure PartyServer's #ensureInitialized()
720
+ * runs, which hydrates this.name from storage and calls onStart() if needed.
721
+ *
692
722
  * @remarks
693
723
  * To schedule a task, please use the `this.schedule` method instead.
694
724
  * See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
695
725
  */
696
- readonly alarm: () => Promise<void>;
726
+ alarm(): Promise<void>;
697
727
  /**
698
728
  * Destroy the Agent, removing all state and scheduled tasks
699
729
  */
package/dist/index.js CHANGED
@@ -260,6 +260,8 @@ var Agent = class Agent extends Server {
260
260
  _emit(type, payload = {}) {
261
261
  this.observability?.emit({
262
262
  type,
263
+ agent: this._ParentClass.name,
264
+ name: this.name,
263
265
  payload,
264
266
  timestamp: Date.now()
265
267
  });
@@ -291,85 +293,6 @@ var Agent = class Agent extends Server {
291
293
  this.initialState = DEFAULT_STATE;
292
294
  this.observability = genericObservability;
293
295
  this._flushingQueue = false;
294
- this.alarm = async () => {
295
- const now = Math.floor(Date.now() / 1e3);
296
- const result = this.sql`
297
- SELECT * FROM cf_agents_schedules WHERE time <= ${now}
298
- `;
299
- if (result && Array.isArray(result)) for (const row of result) {
300
- const callback = this[row.callback];
301
- if (!callback) {
302
- console.error(`callback ${row.callback} not found`);
303
- continue;
304
- }
305
- if (row.type === "interval" && row.running === 1) {
306
- const executionStartedAt = row.execution_started_at ?? 0;
307
- const hungTimeoutSeconds = this._resolvedOptions.hungScheduleTimeoutSeconds;
308
- const elapsedSeconds = now - executionStartedAt;
309
- if (elapsedSeconds < hungTimeoutSeconds) {
310
- console.warn(`Skipping interval schedule ${row.id}: previous execution still running`);
311
- continue;
312
- }
313
- console.warn(`Forcing reset of hung interval schedule ${row.id} (started ${elapsedSeconds}s ago)`);
314
- }
315
- if (row.type === "interval") this.sql`UPDATE cf_agents_schedules SET running = 1, execution_started_at = ${now} WHERE id = ${row.id}`;
316
- await __DO_NOT_USE_WILL_BREAK__agentContext.run({
317
- agent: this,
318
- connection: void 0,
319
- request: void 0,
320
- email: void 0
321
- }, async () => {
322
- const { maxAttempts, baseDelayMs, maxDelayMs } = resolveRetryConfig(parseRetryOptions(row), this._resolvedOptions.retry);
323
- const parsedPayload = JSON.parse(row.payload);
324
- try {
325
- this._emit("schedule:execute", {
326
- callback: row.callback,
327
- id: row.id
328
- });
329
- await tryN(maxAttempts, async (attempt) => {
330
- if (attempt > 1) this._emit("schedule:retry", {
331
- callback: row.callback,
332
- id: row.id,
333
- attempt,
334
- maxAttempts
335
- });
336
- await callback.bind(this)(parsedPayload, row);
337
- }, {
338
- baseDelayMs,
339
- maxDelayMs
340
- });
341
- } catch (e) {
342
- console.error(`error executing callback "${row.callback}" after ${maxAttempts} attempts`, e);
343
- this._emit("schedule:error", {
344
- callback: row.callback,
345
- id: row.id,
346
- error: e instanceof Error ? e.message : String(e),
347
- attempts: maxAttempts
348
- });
349
- try {
350
- await this.onError(e);
351
- } catch {}
352
- }
353
- });
354
- if (this._destroyed) return;
355
- if (row.type === "cron") {
356
- const nextExecutionTime = getNextCronTime(row.cron);
357
- const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
358
- this.sql`
359
- UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
360
- `;
361
- } else if (row.type === "interval") {
362
- const nextTimestamp = Math.floor(Date.now() / 1e3) + (row.intervalSeconds ?? 0);
363
- this.sql`
364
- UPDATE cf_agents_schedules SET running = 0, time = ${nextTimestamp} WHERE id = ${row.id}
365
- `;
366
- } else this.sql`
367
- DELETE FROM cf_agents_schedules WHERE id = ${row.id}
368
- `;
369
- }
370
- if (this._destroyed) return;
371
- await this._scheduleNextAlarm();
372
- };
373
296
  if (!wrappedClasses.has(this.constructor)) {
374
297
  this._autoWrapCustomMethods();
375
298
  wrappedClasses.add(this.constructor);
@@ -457,7 +380,11 @@ var Agent = class Agent extends Server {
457
380
  this.broadcastMcpServers();
458
381
  }));
459
382
  this._disposables.add(this.mcp.onObservabilityEvent((event) => {
460
- this.observability?.emit(event);
383
+ this.observability?.emit({
384
+ ...event,
385
+ agent: this._ParentClass.name,
386
+ name: this.name
387
+ });
461
388
  }));
462
389
  {
463
390
  const proto = Object.getPrototypeOf(this);
@@ -620,6 +547,22 @@ var Agent = class Agent extends Server {
620
547
  return this._tryCatch(() => _onConnect(connection, ctx));
621
548
  });
622
549
  };
550
+ const _onClose = this.onClose.bind(this);
551
+ this.onClose = (connection, code, reason, wasClean) => {
552
+ return __DO_NOT_USE_WILL_BREAK__agentContext.run({
553
+ agent: this,
554
+ connection,
555
+ request: void 0,
556
+ email: void 0
557
+ }, () => {
558
+ this._emit("disconnect", {
559
+ connectionId: connection.id,
560
+ code,
561
+ reason
562
+ });
563
+ return _onClose(connection, code, reason, wasClean);
564
+ });
565
+ };
623
566
  const _onStart = this.onStart.bind(this);
624
567
  this.onStart = async (props) => {
625
568
  return __DO_NOT_USE_WILL_BREAK__agentContext.run({
@@ -917,6 +860,11 @@ var Agent = class Agent extends Server {
917
860
  request: void 0,
918
861
  email
919
862
  }, async () => {
863
+ this._emit("email:receive", {
864
+ from: email.from,
865
+ to: email.to,
866
+ subject: email.headers.get("subject") ?? void 0
867
+ });
920
868
  if ("onEmail" in this && typeof this.onEmail === "function") return this._tryCatch(() => this.onEmail(email));
921
869
  else {
922
870
  console.log("Received email from:", email.from, "to:", email.to);
@@ -967,6 +915,12 @@ var Agent = class Agent extends Server {
967
915
  raw: msg.asRaw(),
968
916
  to: email.from
969
917
  });
918
+ const rawSubject = email.headers.get("subject");
919
+ this._emit("email:reply", {
920
+ from: email.to,
921
+ to: email.from,
922
+ subject: options.subject ?? (rawSubject ? `Re: ${rawSubject}` : void 0)
923
+ });
970
924
  });
971
925
  }
972
926
  async _tryCatch(fn) {
@@ -1065,6 +1019,10 @@ var Agent = class Agent extends Server {
1065
1019
  INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback, retry_options)
1066
1020
  VALUES (${id}, ${JSON.stringify(payload)}, ${callback}, ${retryJson})
1067
1021
  `;
1022
+ this._emit("queue:create", {
1023
+ callback,
1024
+ id
1025
+ });
1068
1026
  this._flushQueue().catch((e) => {
1069
1027
  console.error("Error flushing queue:", e);
1070
1028
  });
@@ -1263,7 +1221,23 @@ var Agent = class Agent extends Server {
1263
1221
  throw new Error(`Invalid schedule type: ${JSON.stringify(when)}(${typeof when}) trying to schedule ${callback}`);
1264
1222
  }
1265
1223
  /**
1266
- * Schedule a task to run repeatedly at a fixed interval
1224
+ * Schedule a task to run repeatedly at a fixed interval.
1225
+ *
1226
+ * This method is **idempotent** — calling it multiple times with the same
1227
+ * `callback`, `intervalSeconds`, and `payload` returns the existing schedule
1228
+ * instead of creating a duplicate. A different interval or payload is
1229
+ * treated as a distinct schedule and creates a new row.
1230
+ *
1231
+ * This makes it safe to call in `onStart()`, which runs on every Durable
1232
+ * Object wake:
1233
+ *
1234
+ * ```ts
1235
+ * async onStart() {
1236
+ * // Only one schedule is created, no matter how many times the DO wakes
1237
+ * await this.scheduleEvery(30, "tick");
1238
+ * }
1239
+ * ```
1240
+ *
1267
1241
  * @template T Type of the payload data
1268
1242
  * @param intervalSeconds Number of seconds between executions
1269
1243
  * @param callback Name of the method to call
@@ -1279,13 +1253,37 @@ var Agent = class Agent extends Server {
1279
1253
  if (typeof callback !== "string") throw new Error("Callback must be a string");
1280
1254
  if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
1281
1255
  if (options?.retry) validateRetryOptions(options.retry, this._resolvedOptions.retry);
1256
+ const idempotent = options?._idempotent !== false;
1257
+ const payloadJson = JSON.stringify(payload);
1258
+ if (idempotent) {
1259
+ const existing = this.sql`
1260
+ SELECT * FROM cf_agents_schedules
1261
+ WHERE type = 'interval'
1262
+ AND callback = ${callback}
1263
+ AND intervalSeconds = ${intervalSeconds}
1264
+ AND payload IS ${payloadJson}
1265
+ LIMIT 1
1266
+ `;
1267
+ if (existing.length > 0) {
1268
+ const row = existing[0];
1269
+ return {
1270
+ callback: row.callback,
1271
+ id: row.id,
1272
+ intervalSeconds: row.intervalSeconds,
1273
+ payload: JSON.parse(row.payload),
1274
+ retry: parseRetryOptions(row),
1275
+ time: row.time,
1276
+ type: "interval"
1277
+ };
1278
+ }
1279
+ }
1282
1280
  const id = nanoid(9);
1283
1281
  const time = new Date(Date.now() + intervalSeconds * 1e3);
1284
1282
  const timestamp = Math.floor(time.getTime() / 1e3);
1285
1283
  const retryJson = options?.retry ? JSON.stringify(options.retry) : null;
1286
1284
  this.sql`
1287
1285
  INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, intervalSeconds, time, running, retry_options)
1288
- VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'interval', ${intervalSeconds}, ${timestamp}, 0, ${retryJson})
1286
+ VALUES (${id}, ${callback}, ${payloadJson}, 'interval', ${intervalSeconds}, ${timestamp}, 0, ${retryJson})
1289
1287
  `;
1290
1288
  await this._scheduleNextAlarm();
1291
1289
  const schedule = {
@@ -1388,7 +1386,7 @@ var Agent = class Agent extends Server {
1388
1386
  */
1389
1387
  async keepAlive() {
1390
1388
  const heartbeatSeconds = Math.ceil(KEEP_ALIVE_INTERVAL_MS / 1e3);
1391
- const schedule = await this.scheduleEvery(heartbeatSeconds, "_cf_keepAliveHeartbeat");
1389
+ const schedule = await this.scheduleEvery(heartbeatSeconds, "_cf_keepAliveHeartbeat", void 0, { _idempotent: false });
1392
1390
  let disposed = false;
1393
1391
  return () => {
1394
1392
  if (disposed) return;
@@ -1443,6 +1441,104 @@ var Agent = class Agent extends Server {
1443
1441
  }
1444
1442
  }
1445
1443
  /**
1444
+ * Override PartyServer's onAlarm hook as a no-op.
1445
+ * Agent handles alarm logic directly in the alarm() method override,
1446
+ * but super.alarm() calls onAlarm() after #ensureInitialized(),
1447
+ * so we suppress the default "Implement onAlarm" warning.
1448
+ */
1449
+ onAlarm() {}
1450
+ /**
1451
+ * Method called when an alarm fires.
1452
+ * Executes any scheduled tasks that are due.
1453
+ *
1454
+ * Calls super.alarm() first to ensure PartyServer's #ensureInitialized()
1455
+ * runs, which hydrates this.name from storage and calls onStart() if needed.
1456
+ *
1457
+ * @remarks
1458
+ * To schedule a task, please use the `this.schedule` method instead.
1459
+ * See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
1460
+ */
1461
+ async alarm() {
1462
+ await super.alarm();
1463
+ const now = Math.floor(Date.now() / 1e3);
1464
+ const result = this.sql`
1465
+ SELECT * FROM cf_agents_schedules WHERE time <= ${now}
1466
+ `;
1467
+ if (result && Array.isArray(result)) for (const row of result) {
1468
+ const callback = this[row.callback];
1469
+ if (!callback) {
1470
+ console.error(`callback ${row.callback} not found`);
1471
+ continue;
1472
+ }
1473
+ if (row.type === "interval" && row.running === 1) {
1474
+ const executionStartedAt = row.execution_started_at ?? 0;
1475
+ const hungTimeoutSeconds = this._resolvedOptions.hungScheduleTimeoutSeconds;
1476
+ const elapsedSeconds = now - executionStartedAt;
1477
+ if (elapsedSeconds < hungTimeoutSeconds) {
1478
+ console.warn(`Skipping interval schedule ${row.id}: previous execution still running`);
1479
+ continue;
1480
+ }
1481
+ console.warn(`Forcing reset of hung interval schedule ${row.id} (started ${elapsedSeconds}s ago)`);
1482
+ }
1483
+ if (row.type === "interval") this.sql`UPDATE cf_agents_schedules SET running = 1, execution_started_at = ${now} WHERE id = ${row.id}`;
1484
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
1485
+ agent: this,
1486
+ connection: void 0,
1487
+ request: void 0,
1488
+ email: void 0
1489
+ }, async () => {
1490
+ const { maxAttempts, baseDelayMs, maxDelayMs } = resolveRetryConfig(parseRetryOptions(row), this._resolvedOptions.retry);
1491
+ const parsedPayload = JSON.parse(row.payload);
1492
+ try {
1493
+ this._emit("schedule:execute", {
1494
+ callback: row.callback,
1495
+ id: row.id
1496
+ });
1497
+ await tryN(maxAttempts, async (attempt) => {
1498
+ if (attempt > 1) this._emit("schedule:retry", {
1499
+ callback: row.callback,
1500
+ id: row.id,
1501
+ attempt,
1502
+ maxAttempts
1503
+ });
1504
+ await callback.bind(this)(parsedPayload, row);
1505
+ }, {
1506
+ baseDelayMs,
1507
+ maxDelayMs
1508
+ });
1509
+ } catch (e) {
1510
+ console.error(`error executing callback "${row.callback}" after ${maxAttempts} attempts`, e);
1511
+ this._emit("schedule:error", {
1512
+ callback: row.callback,
1513
+ id: row.id,
1514
+ error: e instanceof Error ? e.message : String(e),
1515
+ attempts: maxAttempts
1516
+ });
1517
+ try {
1518
+ await this.onError(e);
1519
+ } catch {}
1520
+ }
1521
+ });
1522
+ if (this._destroyed) return;
1523
+ if (row.type === "cron") {
1524
+ const nextExecutionTime = getNextCronTime(row.cron);
1525
+ const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
1526
+ this.sql`
1527
+ UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
1528
+ `;
1529
+ } else if (row.type === "interval") {
1530
+ const nextTimestamp = Math.floor(Date.now() / 1e3) + (row.intervalSeconds ?? 0);
1531
+ this.sql`
1532
+ UPDATE cf_agents_schedules SET running = 0, time = ${nextTimestamp} WHERE id = ${row.id}
1533
+ `;
1534
+ } else this.sql`
1535
+ DELETE FROM cf_agents_schedules WHERE id = ${row.id}
1536
+ `;
1537
+ }
1538
+ if (this._destroyed) return;
1539
+ await this._scheduleNextAlarm();
1540
+ }
1541
+ /**
1446
1542
  * Destroy the Agent, removing all state and scheduled tasks
1447
1543
  */
1448
1544
  async destroy() {