agents 0.7.1 → 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.
@@ -554,7 +554,7 @@ declare class MCPClientConnection {
554
554
  */
555
555
  getTransport(
556
556
  transportType: BaseTransportType
557
- ): RPCClientTransport | SSEClientTransport | StreamableHTTPClientTransport;
557
+ ): StreamableHTTPClientTransport | SSEClientTransport | RPCClientTransport;
558
558
  private tryConnect;
559
559
  private _capabilityErrorHandler;
560
560
  }
@@ -601,4 +601,4 @@ export {
601
601
  MaybePromise as x,
602
602
  StreamableHTTPEdgeClientTransport as y
603
603
  };
604
- //# sourceMappingURL=client-storage-yDVwzgfF.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-yDVwzgfF.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-yDVwzgfF.js";
10
+ } from "./client-storage-BPjfP_is.js";
11
11
  import {
12
12
  AgentMcpOAuthProvider,
13
13
  AgentsOAuthProvider,
@@ -596,7 +596,23 @@ declare class Agent<
596
596
  }
597
597
  ): Promise<Schedule<T>>;
598
598
  /**
599
- * 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
+ *
600
616
  * @template T Type of the payload data
601
617
  * @param intervalSeconds Number of seconds between executions
602
618
  * @param callback Name of the method to call
@@ -611,6 +627,7 @@ declare class Agent<
611
627
  payload?: T,
612
628
  options?: {
613
629
  retry?: RetryOptions;
630
+ _idempotent?: boolean;
614
631
  }
615
632
  ): Promise<Schedule<T>>;
616
633
  /**
@@ -688,15 +705,25 @@ declare class Agent<
688
705
  */
689
706
  _cf_keepAliveHeartbeat(): Promise<void>;
690
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;
691
715
  /**
692
716
  * Method called when an alarm fires.
693
717
  * Executes any scheduled tasks that are due.
694
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
+ *
695
722
  * @remarks
696
723
  * To schedule a task, please use the `this.schedule` method instead.
697
724
  * See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
698
725
  */
699
- readonly alarm: () => Promise<void>;
726
+ alarm(): Promise<void>;
700
727
  /**
701
728
  * Destroy the Agent, removing all state and scheduled tasks
702
729
  */
package/dist/index.js CHANGED
@@ -293,85 +293,6 @@ var Agent = class Agent extends Server {
293
293
  this.initialState = DEFAULT_STATE;
294
294
  this.observability = genericObservability;
295
295
  this._flushingQueue = false;
296
- this.alarm = async () => {
297
- const now = Math.floor(Date.now() / 1e3);
298
- const result = this.sql`
299
- SELECT * FROM cf_agents_schedules WHERE time <= ${now}
300
- `;
301
- if (result && Array.isArray(result)) for (const row of result) {
302
- const callback = this[row.callback];
303
- if (!callback) {
304
- console.error(`callback ${row.callback} not found`);
305
- continue;
306
- }
307
- if (row.type === "interval" && row.running === 1) {
308
- const executionStartedAt = row.execution_started_at ?? 0;
309
- const hungTimeoutSeconds = this._resolvedOptions.hungScheduleTimeoutSeconds;
310
- const elapsedSeconds = now - executionStartedAt;
311
- if (elapsedSeconds < hungTimeoutSeconds) {
312
- console.warn(`Skipping interval schedule ${row.id}: previous execution still running`);
313
- continue;
314
- }
315
- console.warn(`Forcing reset of hung interval schedule ${row.id} (started ${elapsedSeconds}s ago)`);
316
- }
317
- if (row.type === "interval") this.sql`UPDATE cf_agents_schedules SET running = 1, execution_started_at = ${now} WHERE id = ${row.id}`;
318
- await __DO_NOT_USE_WILL_BREAK__agentContext.run({
319
- agent: this,
320
- connection: void 0,
321
- request: void 0,
322
- email: void 0
323
- }, async () => {
324
- const { maxAttempts, baseDelayMs, maxDelayMs } = resolveRetryConfig(parseRetryOptions(row), this._resolvedOptions.retry);
325
- const parsedPayload = JSON.parse(row.payload);
326
- try {
327
- this._emit("schedule:execute", {
328
- callback: row.callback,
329
- id: row.id
330
- });
331
- await tryN(maxAttempts, async (attempt) => {
332
- if (attempt > 1) this._emit("schedule:retry", {
333
- callback: row.callback,
334
- id: row.id,
335
- attempt,
336
- maxAttempts
337
- });
338
- await callback.bind(this)(parsedPayload, row);
339
- }, {
340
- baseDelayMs,
341
- maxDelayMs
342
- });
343
- } catch (e) {
344
- console.error(`error executing callback "${row.callback}" after ${maxAttempts} attempts`, e);
345
- this._emit("schedule:error", {
346
- callback: row.callback,
347
- id: row.id,
348
- error: e instanceof Error ? e.message : String(e),
349
- attempts: maxAttempts
350
- });
351
- try {
352
- await this.onError(e);
353
- } catch {}
354
- }
355
- });
356
- if (this._destroyed) return;
357
- if (row.type === "cron") {
358
- const nextExecutionTime = getNextCronTime(row.cron);
359
- const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
360
- this.sql`
361
- UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
362
- `;
363
- } else if (row.type === "interval") {
364
- const nextTimestamp = Math.floor(Date.now() / 1e3) + (row.intervalSeconds ?? 0);
365
- this.sql`
366
- UPDATE cf_agents_schedules SET running = 0, time = ${nextTimestamp} WHERE id = ${row.id}
367
- `;
368
- } else this.sql`
369
- DELETE FROM cf_agents_schedules WHERE id = ${row.id}
370
- `;
371
- }
372
- if (this._destroyed) return;
373
- await this._scheduleNextAlarm();
374
- };
375
296
  if (!wrappedClasses.has(this.constructor)) {
376
297
  this._autoWrapCustomMethods();
377
298
  wrappedClasses.add(this.constructor);
@@ -1300,7 +1221,23 @@ var Agent = class Agent extends Server {
1300
1221
  throw new Error(`Invalid schedule type: ${JSON.stringify(when)}(${typeof when}) trying to schedule ${callback}`);
1301
1222
  }
1302
1223
  /**
1303
- * 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
+ *
1304
1241
  * @template T Type of the payload data
1305
1242
  * @param intervalSeconds Number of seconds between executions
1306
1243
  * @param callback Name of the method to call
@@ -1316,13 +1253,37 @@ var Agent = class Agent extends Server {
1316
1253
  if (typeof callback !== "string") throw new Error("Callback must be a string");
1317
1254
  if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
1318
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
+ }
1319
1280
  const id = nanoid(9);
1320
1281
  const time = new Date(Date.now() + intervalSeconds * 1e3);
1321
1282
  const timestamp = Math.floor(time.getTime() / 1e3);
1322
1283
  const retryJson = options?.retry ? JSON.stringify(options.retry) : null;
1323
1284
  this.sql`
1324
1285
  INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, intervalSeconds, time, running, retry_options)
1325
- VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'interval', ${intervalSeconds}, ${timestamp}, 0, ${retryJson})
1286
+ VALUES (${id}, ${callback}, ${payloadJson}, 'interval', ${intervalSeconds}, ${timestamp}, 0, ${retryJson})
1326
1287
  `;
1327
1288
  await this._scheduleNextAlarm();
1328
1289
  const schedule = {
@@ -1425,7 +1386,7 @@ var Agent = class Agent extends Server {
1425
1386
  */
1426
1387
  async keepAlive() {
1427
1388
  const heartbeatSeconds = Math.ceil(KEEP_ALIVE_INTERVAL_MS / 1e3);
1428
- const schedule = await this.scheduleEvery(heartbeatSeconds, "_cf_keepAliveHeartbeat");
1389
+ const schedule = await this.scheduleEvery(heartbeatSeconds, "_cf_keepAliveHeartbeat", void 0, { _idempotent: false });
1429
1390
  let disposed = false;
1430
1391
  return () => {
1431
1392
  if (disposed) return;
@@ -1480,6 +1441,104 @@ var Agent = class Agent extends Server {
1480
1441
  }
1481
1442
  }
1482
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
+ /**
1483
1542
  * Destroy the Agent, removing all state and scheduled tasks
1484
1543
  */
1485
1544
  async destroy() {