agents 0.5.1 → 0.7.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/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { camelCaseToKebabCase } from "./utils.js";
3
3
  import { createHeaderBasedEmailResolver, signAgentHeaders } from "./email.js";
4
4
  import { __DO_NOT_USE_WILL_BREAK__agentContext } from "./internal_context.js";
5
5
  import { isErrorRetryable, tryN, validateRetryOptions } from "./retries.js";
6
- import { i as DisposableStore, n as MCPConnectionState } from "./client-connection-CGMuV62J.js";
6
+ import { a as RPC_DO_PREFIX, n as MCPConnectionState, s as DisposableStore } from "./client-connection-D3Wcd6Q6.js";
7
7
  import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider.js";
8
8
  import { MCPClientManager } from "./mcp/client.js";
9
9
  import { genericObservability } from "./observability/index.js";
@@ -66,6 +66,7 @@ const unstable_callable = (metadata = {}) => {
66
66
  function getNextCronTime(cron) {
67
67
  return parseCronExpression(cron).getNextDate();
68
68
  }
69
+ const KEEP_ALIVE_INTERVAL_MS = 3e4;
69
70
  const STATE_ROW_ID = "cf_state_row_id";
70
71
  const STATE_WAS_CHANGED = "cf_state_was_changed";
71
72
  const DEFAULT_STATE = {};
@@ -106,12 +107,31 @@ function extractInternalFlags(raw) {
106
107
  for (const key of Object.keys(raw)) if (CF_INTERNAL_KEYS.has(key)) result[key] = raw[key];
107
108
  return result;
108
109
  }
110
+ /** Max length for error strings broadcast to clients. */
111
+ const MAX_ERROR_STRING_LENGTH = 500;
112
+ /**
113
+ * Sanitize an error string before broadcasting to clients.
114
+ * MCP error strings may contain untrusted content from external OAuth
115
+ * providers — truncate and strip control characters to limit XSS risk.
116
+ */
117
+ const CONTROL_CHAR_RE = /* @__PURE__ */ new RegExp("[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F]", "g");
118
+ function sanitizeErrorString(error) {
119
+ if (error === null) return null;
120
+ let sanitized = error.replace(CONTROL_CHAR_RE, "");
121
+ if (sanitized.length > MAX_ERROR_STRING_LENGTH) sanitized = sanitized.substring(0, MAX_ERROR_STRING_LENGTH) + "...";
122
+ return sanitized;
123
+ }
109
124
  /**
110
125
  * Tracks which agent constructors have already emitted the onStateUpdate
111
126
  * deprecation warning, so it fires at most once per class.
112
127
  */
113
128
  const _onStateUpdateWarnedClasses = /* @__PURE__ */ new WeakSet();
114
129
  /**
130
+ * Tracks which agent constructors have already emitted the
131
+ * sendIdentityOnConnect deprecation warning, so it fires at most once per class.
132
+ */
133
+ const _sendIdentityWarnedClasses = /* @__PURE__ */ new WeakSet();
134
+ /**
115
135
  * Default options for Agent configuration.
116
136
  * Child classes can override specific options without spreading.
117
137
  */
@@ -234,6 +254,17 @@ var Agent = class Agent extends Server {
234
254
  return this._cachedOptions;
235
255
  }
236
256
  /**
257
+ * Emit an observability event with auto-generated timestamp.
258
+ * @internal
259
+ */
260
+ _emit(type, payload = {}) {
261
+ this.observability?.emit({
262
+ type,
263
+ payload,
264
+ timestamp: Date.now()
265
+ });
266
+ }
267
+ /**
237
268
  * Execute SQL queries against the Agent's database
238
269
  * @template T Type of the returned rows
239
270
  * @param strings SQL query template strings
@@ -246,7 +277,7 @@ var Agent = class Agent extends Server {
246
277
  query = strings.reduce((acc, str, i) => acc + str + (i < values.length ? "?" : ""), "");
247
278
  return [...this.ctx.storage.sql.exec(query, ...values)];
248
279
  } catch (e) {
249
- throw this.onError(new SqlError(query, e));
280
+ throw new SqlError(query, e);
250
281
  }
251
282
  }
252
283
  constructor(ctx, env) {
@@ -291,29 +322,17 @@ var Agent = class Agent extends Server {
291
322
  const { maxAttempts, baseDelayMs, maxDelayMs } = resolveRetryConfig(parseRetryOptions(row), this._resolvedOptions.retry);
292
323
  const parsedPayload = JSON.parse(row.payload);
293
324
  try {
294
- this.observability?.emit({
295
- displayMessage: `Schedule ${row.id} executed`,
296
- id: nanoid(),
297
- payload: {
298
- callback: row.callback,
299
- id: row.id
300
- },
301
- timestamp: Date.now(),
302
- type: "schedule:execute"
303
- }, this.ctx);
325
+ this._emit("schedule:execute", {
326
+ callback: row.callback,
327
+ id: row.id
328
+ });
304
329
  await tryN(maxAttempts, async (attempt) => {
305
- if (attempt > 1) this.observability?.emit({
306
- displayMessage: `Retrying schedule callback "${row.callback}" (attempt ${attempt}/${maxAttempts})`,
307
- id: nanoid(),
308
- payload: {
309
- callback: row.callback,
310
- id: row.id,
311
- attempt,
312
- maxAttempts
313
- },
314
- timestamp: Date.now(),
315
- type: "schedule:retry"
316
- }, this.ctx);
330
+ if (attempt > 1) this._emit("schedule:retry", {
331
+ callback: row.callback,
332
+ id: row.id,
333
+ attempt,
334
+ maxAttempts
335
+ });
317
336
  await callback.bind(this)(parsedPayload, row);
318
337
  }, {
319
338
  baseDelayMs,
@@ -321,6 +340,12 @@ var Agent = class Agent extends Server {
321
340
  });
322
341
  } catch (e) {
323
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
+ });
324
349
  try {
325
350
  await this.onError(e);
326
351
  } catch {}
@@ -509,35 +534,27 @@ var Agent = class Agent extends Server {
509
534
  const metadata = callableMetadata.get(methodFn);
510
535
  if (metadata?.streaming) {
511
536
  const stream = new StreamingResponse(connection, id);
512
- this.observability?.emit({
513
- displayMessage: `RPC streaming call to ${method}`,
514
- id: nanoid(),
515
- payload: {
516
- method,
517
- streaming: true
518
- },
519
- timestamp: Date.now(),
520
- type: "rpc"
521
- }, this.ctx);
537
+ this._emit("rpc", {
538
+ method,
539
+ streaming: true
540
+ });
522
541
  try {
523
542
  await methodFn.apply(this, [stream, ...args]);
524
543
  } catch (err) {
525
544
  console.error(`Error in streaming method "${method}":`, err);
545
+ this._emit("rpc:error", {
546
+ method,
547
+ error: err instanceof Error ? err.message : String(err)
548
+ });
526
549
  if (!stream.isClosed) stream.error(err instanceof Error ? err.message : String(err));
527
550
  }
528
551
  return;
529
552
  }
530
553
  const result = await methodFn.apply(this, args);
531
- this.observability?.emit({
532
- displayMessage: `RPC call to ${method}`,
533
- id: nanoid(),
534
- payload: {
535
- method,
536
- streaming: metadata?.streaming
537
- },
538
- timestamp: Date.now(),
539
- type: "rpc"
540
- }, this.ctx);
554
+ this._emit("rpc", {
555
+ method,
556
+ streaming: metadata?.streaming
557
+ });
541
558
  const response = {
542
559
  done: true,
543
560
  id,
@@ -555,6 +572,10 @@ var Agent = class Agent extends Server {
555
572
  };
556
573
  connection.send(JSON.stringify(response));
557
574
  console.error("RPC error:", e);
575
+ this._emit("rpc:error", {
576
+ method: parsed.method,
577
+ error: e instanceof Error ? e.message : String(e)
578
+ });
558
579
  }
559
580
  return;
560
581
  }
@@ -572,11 +593,20 @@ var Agent = class Agent extends Server {
572
593
  }, async () => {
573
594
  if (this.shouldConnectionBeReadonly(connection, ctx)) this.setConnectionReadonly(connection, true);
574
595
  if (this.shouldSendProtocolMessages(connection, ctx)) {
575
- if (this._resolvedOptions.sendIdentityOnConnect) connection.send(JSON.stringify({
576
- name: this.name,
577
- agent: camelCaseToKebabCase(this._ParentClass.name),
578
- type: MessageType.CF_AGENT_IDENTITY
579
- }));
596
+ if (this._resolvedOptions.sendIdentityOnConnect) {
597
+ const ctor = this.constructor;
598
+ if (ctor.options?.sendIdentityOnConnect === void 0 && !_sendIdentityWarnedClasses.has(ctor)) {
599
+ if (!new URL(ctx.request.url).pathname.includes(this.name)) {
600
+ _sendIdentityWarnedClasses.add(ctor);
601
+ console.warn(`[Agent] ${ctor.name}: sending instance name "${this.name}" to clients via sendIdentityOnConnect (the name is not visible in the URL with custom routing). If this name is sensitive, add \`static options = { sendIdentityOnConnect: false }\` to opt out. Set it to true to silence this message.`);
602
+ }
603
+ }
604
+ connection.send(JSON.stringify({
605
+ name: this.name,
606
+ agent: camelCaseToKebabCase(this._ParentClass.name),
607
+ type: MessageType.CF_AGENT_IDENTITY
608
+ }));
609
+ }
580
610
  if (this.state) connection.send(JSON.stringify({
581
611
  state: this.state,
582
612
  type: MessageType.CF_AGENT_STATE
@@ -586,13 +616,7 @@ var Agent = class Agent extends Server {
586
616
  type: MessageType.CF_AGENT_MCP_SERVERS
587
617
  }));
588
618
  } else this._setConnectionNoProtocol(connection);
589
- this.observability?.emit({
590
- displayMessage: "Connection established",
591
- id: nanoid(),
592
- payload: { connectionId: connection.id },
593
- timestamp: Date.now(),
594
- type: "connect"
595
- }, this.ctx);
619
+ this._emit("connect", { connectionId: connection.id });
596
620
  return this._tryCatch(() => _onConnect(connection, ctx));
597
621
  });
598
622
  };
@@ -606,6 +630,7 @@ var Agent = class Agent extends Server {
606
630
  }, async () => {
607
631
  await this._tryCatch(async () => {
608
632
  await this.mcp.restoreConnectionsFromStorage(this.name);
633
+ await this._restoreRpcMcpServers();
609
634
  this.broadcastMcpServers();
610
635
  this._checkOrphanedWorkflows();
611
636
  return _onStart(props);
@@ -671,13 +696,7 @@ var Agent = class Agent extends Server {
671
696
  request,
672
697
  email
673
698
  }, async () => {
674
- this.observability?.emit({
675
- displayMessage: "State updated",
676
- id: nanoid(),
677
- payload: {},
678
- timestamp: Date.now(),
679
- type: "state:update"
680
- }, this.ctx);
699
+ this._emit("state:update");
681
700
  await this._callStatePersistenceHook(nextState, source);
682
701
  });
683
702
  } catch (e) {
@@ -1079,18 +1098,12 @@ var Agent = class Agent extends Server {
1079
1098
  const parsedPayload = JSON.parse(row.payload);
1080
1099
  try {
1081
1100
  await tryN(maxAttempts, async (attempt) => {
1082
- if (attempt > 1) this.observability?.emit({
1083
- displayMessage: `Retrying queue callback "${row.callback}" (attempt ${attempt}/${maxAttempts})`,
1084
- id: nanoid(),
1085
- payload: {
1086
- callback: row.callback,
1087
- id: row.id,
1088
- attempt,
1089
- maxAttempts
1090
- },
1091
- timestamp: Date.now(),
1092
- type: "queue:retry"
1093
- }, this.ctx);
1101
+ if (attempt > 1) this._emit("queue:retry", {
1102
+ callback: row.callback,
1103
+ id: row.id,
1104
+ attempt,
1105
+ maxAttempts
1106
+ });
1094
1107
  await callback.bind(this)(parsedPayload, row);
1095
1108
  }, {
1096
1109
  baseDelayMs,
@@ -1098,11 +1111,17 @@ var Agent = class Agent extends Server {
1098
1111
  });
1099
1112
  } catch (e) {
1100
1113
  console.error(`queue callback "${row.callback}" failed after ${maxAttempts} attempts`, e);
1114
+ this._emit("queue:error", {
1115
+ callback: row.callback,
1116
+ id: row.id,
1117
+ error: e instanceof Error ? e.message : String(e),
1118
+ attempts: maxAttempts
1119
+ });
1101
1120
  try {
1102
1121
  await this.onError(e);
1103
1122
  } catch {}
1104
1123
  } finally {
1105
- await this.dequeue(row.id);
1124
+ this.dequeue(row.id);
1106
1125
  }
1107
1126
  });
1108
1127
  }
@@ -1177,16 +1196,10 @@ var Agent = class Agent extends Server {
1177
1196
  const id = nanoid(9);
1178
1197
  if (options?.retry) validateRetryOptions(options.retry, this._resolvedOptions.retry);
1179
1198
  const retryJson = options?.retry ? JSON.stringify(options.retry) : null;
1180
- const emitScheduleCreate = (schedule) => this.observability?.emit({
1181
- displayMessage: `Schedule ${schedule.id} created`,
1182
- id: nanoid(),
1183
- payload: {
1184
- callback,
1185
- id
1186
- },
1187
- timestamp: Date.now(),
1188
- type: "schedule:create"
1189
- }, this.ctx);
1199
+ const emitScheduleCreate = () => this._emit("schedule:create", {
1200
+ callback,
1201
+ id
1202
+ });
1190
1203
  if (typeof callback !== "string") throw new Error("Callback must be a string");
1191
1204
  if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
1192
1205
  if (when instanceof Date) {
@@ -1204,7 +1217,7 @@ var Agent = class Agent extends Server {
1204
1217
  time: timestamp,
1205
1218
  type: "scheduled"
1206
1219
  };
1207
- emitScheduleCreate(schedule);
1220
+ emitScheduleCreate();
1208
1221
  return schedule;
1209
1222
  }
1210
1223
  if (typeof when === "number") {
@@ -1224,7 +1237,7 @@ var Agent = class Agent extends Server {
1224
1237
  time: timestamp,
1225
1238
  type: "delayed"
1226
1239
  };
1227
- emitScheduleCreate(schedule);
1240
+ emitScheduleCreate();
1228
1241
  return schedule;
1229
1242
  }
1230
1243
  if (typeof when === "string") {
@@ -1244,7 +1257,7 @@ var Agent = class Agent extends Server {
1244
1257
  time: timestamp,
1245
1258
  type: "cron"
1246
1259
  };
1247
- emitScheduleCreate(schedule);
1260
+ emitScheduleCreate();
1248
1261
  return schedule;
1249
1262
  }
1250
1263
  throw new Error(`Invalid schedule type: ${JSON.stringify(when)}(${typeof when}) trying to schedule ${callback}`);
@@ -1284,16 +1297,10 @@ var Agent = class Agent extends Server {
1284
1297
  time: timestamp,
1285
1298
  type: "interval"
1286
1299
  };
1287
- this.observability?.emit({
1288
- displayMessage: `Schedule ${schedule.id} created`,
1289
- id: nanoid(),
1290
- payload: {
1291
- callback,
1292
- id
1293
- },
1294
- timestamp: Date.now(),
1295
- type: "schedule:create"
1296
- }, this.ctx);
1300
+ this._emit("schedule:create", {
1301
+ callback,
1302
+ id
1303
+ });
1297
1304
  return schedule;
1298
1305
  }
1299
1306
  /**
@@ -1351,20 +1358,77 @@ var Agent = class Agent extends Server {
1351
1358
  async cancelSchedule(id) {
1352
1359
  const schedule = this.getSchedule(id);
1353
1360
  if (!schedule) return false;
1354
- this.observability?.emit({
1355
- displayMessage: `Schedule ${id} cancelled`,
1356
- id: nanoid(),
1357
- payload: {
1358
- callback: schedule.callback,
1359
- id: schedule.id
1360
- },
1361
- timestamp: Date.now(),
1362
- type: "schedule:cancel"
1363
- }, this.ctx);
1361
+ this._emit("schedule:cancel", {
1362
+ callback: schedule.callback,
1363
+ id: schedule.id
1364
+ });
1364
1365
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
1365
1366
  await this._scheduleNextAlarm();
1366
1367
  return true;
1367
1368
  }
1369
+ /**
1370
+ * Keep the Durable Object alive via alarm heartbeats.
1371
+ * Returns a disposer function that stops the heartbeat when called.
1372
+ *
1373
+ * Use this when you have long-running work and need to prevent the
1374
+ * DO from going idle (eviction after ~70-140s of inactivity).
1375
+ * The heartbeat fires every 30 seconds via the scheduling system.
1376
+ *
1377
+ * @experimental This API may change between releases.
1378
+ *
1379
+ * @example
1380
+ * ```ts
1381
+ * const dispose = await this.keepAlive();
1382
+ * try {
1383
+ * // ... long-running work ...
1384
+ * } finally {
1385
+ * dispose();
1386
+ * }
1387
+ * ```
1388
+ */
1389
+ async keepAlive() {
1390
+ const heartbeatSeconds = Math.ceil(KEEP_ALIVE_INTERVAL_MS / 1e3);
1391
+ const schedule = await this.scheduleEvery(heartbeatSeconds, "_cf_keepAliveHeartbeat");
1392
+ let disposed = false;
1393
+ return () => {
1394
+ if (disposed) return;
1395
+ disposed = true;
1396
+ this.cancelSchedule(schedule.id);
1397
+ };
1398
+ }
1399
+ /**
1400
+ * Run an async function while keeping the Durable Object alive.
1401
+ * The heartbeat is automatically stopped when the function completes
1402
+ * (whether it succeeds or throws).
1403
+ *
1404
+ * This is the recommended way to use keepAlive — it guarantees cleanup
1405
+ * so you cannot forget to dispose the heartbeat.
1406
+ *
1407
+ * @experimental This API may change between releases.
1408
+ *
1409
+ * @example
1410
+ * ```ts
1411
+ * const result = await this.keepAliveWhile(async () => {
1412
+ * const data = await longRunningComputation();
1413
+ * return data;
1414
+ * });
1415
+ * ```
1416
+ */
1417
+ async keepAliveWhile(fn) {
1418
+ const dispose = await this.keepAlive();
1419
+ try {
1420
+ return await fn();
1421
+ } finally {
1422
+ dispose();
1423
+ }
1424
+ }
1425
+ /**
1426
+ * Internal no-op callback invoked by the keepAlive heartbeat schedule.
1427
+ * Its only purpose is to keep the DO alive — the alarm machinery
1428
+ * handles the rest.
1429
+ * @internal
1430
+ */
1431
+ async _cf_keepAliveHeartbeat() {}
1368
1432
  async _scheduleNextAlarm() {
1369
1433
  const result = this.sql`
1370
1434
  SELECT time FROM cf_agents_schedules
@@ -1395,13 +1459,7 @@ var Agent = class Agent extends Server {
1395
1459
  setTimeout(() => {
1396
1460
  this.ctx.abort("destroyed");
1397
1461
  }, 0);
1398
- this.observability?.emit({
1399
- displayMessage: "Agent destroyed",
1400
- id: nanoid(),
1401
- payload: {},
1402
- timestamp: Date.now(),
1403
- type: "destroy"
1404
- }, this.ctx);
1462
+ this._emit("destroy");
1405
1463
  }
1406
1464
  /**
1407
1465
  * Check if a method is callable
@@ -1481,16 +1539,10 @@ var Agent = class Agent extends Server {
1481
1539
  if (e instanceof Error && e.message.includes("UNIQUE constraint failed")) throw new Error(`Workflow with ID "${workflowId}" is already being tracked`);
1482
1540
  throw e;
1483
1541
  }
1484
- this.observability?.emit({
1485
- displayMessage: `Workflow ${instance.id} started`,
1486
- id: nanoid(),
1487
- payload: {
1488
- workflowId: instance.id,
1489
- workflowName
1490
- },
1491
- timestamp: Date.now(),
1492
- type: "workflow:start"
1493
- }, this.ctx);
1542
+ this._emit("workflow:start", {
1543
+ workflowId: instance.id,
1544
+ workflowName
1545
+ });
1494
1546
  return instance.id;
1495
1547
  }
1496
1548
  /**
@@ -1519,16 +1571,10 @@ var Agent = class Agent extends Server {
1519
1571
  baseDelayMs: 200,
1520
1572
  maxDelayMs: 3e3
1521
1573
  });
1522
- this.observability?.emit({
1523
- displayMessage: `Event sent to workflow ${workflowId}`,
1524
- id: nanoid(),
1525
- payload: {
1526
- workflowId,
1527
- eventType: event.type
1528
- },
1529
- timestamp: Date.now(),
1530
- type: "workflow:event"
1531
- }, this.ctx);
1574
+ this._emit("workflow:event", {
1575
+ workflowId,
1576
+ eventType: event.type
1577
+ });
1532
1578
  }
1533
1579
  /**
1534
1580
  * Approve a waiting workflow.
@@ -1556,16 +1602,10 @@ var Agent = class Agent extends Server {
1556
1602
  metadata: data?.metadata
1557
1603
  }
1558
1604
  });
1559
- this.observability?.emit({
1560
- displayMessage: `Workflow ${workflowId} approved`,
1561
- id: nanoid(),
1562
- payload: {
1563
- workflowId,
1564
- reason: data?.reason
1565
- },
1566
- timestamp: Date.now(),
1567
- type: "workflow:approved"
1568
- }, this.ctx);
1605
+ this._emit("workflow:approved", {
1606
+ workflowId,
1607
+ reason: data?.reason
1608
+ });
1569
1609
  }
1570
1610
  /**
1571
1611
  * Reject a waiting workflow.
@@ -1591,16 +1631,10 @@ var Agent = class Agent extends Server {
1591
1631
  reason: data?.reason
1592
1632
  }
1593
1633
  });
1594
- this.observability?.emit({
1595
- displayMessage: `Workflow ${workflowId} rejected`,
1596
- id: nanoid(),
1597
- payload: {
1598
- workflowId,
1599
- reason: data?.reason
1600
- },
1601
- timestamp: Date.now(),
1602
- type: "workflow:rejected"
1603
- }, this.ctx);
1634
+ this._emit("workflow:rejected", {
1635
+ workflowId,
1636
+ reason: data?.reason
1637
+ });
1604
1638
  }
1605
1639
  /**
1606
1640
  * Terminate a running workflow.
@@ -1637,16 +1671,10 @@ var Agent = class Agent extends Server {
1637
1671
  }
1638
1672
  const status = await instance.status();
1639
1673
  this._updateWorkflowTracking(workflowId, status);
1640
- this.observability?.emit({
1641
- displayMessage: `Workflow ${workflowId} terminated`,
1642
- id: nanoid(),
1643
- payload: {
1644
- workflowId,
1645
- workflowName: workflowInfo.workflowName
1646
- },
1647
- timestamp: Date.now(),
1648
- type: "workflow:terminated"
1649
- }, this.ctx);
1674
+ this._emit("workflow:terminated", {
1675
+ workflowId,
1676
+ workflowName: workflowInfo.workflowName
1677
+ });
1650
1678
  }
1651
1679
  /**
1652
1680
  * Pause a running workflow.
@@ -1683,16 +1711,10 @@ var Agent = class Agent extends Server {
1683
1711
  }
1684
1712
  const status = await instance.status();
1685
1713
  this._updateWorkflowTracking(workflowId, status);
1686
- this.observability?.emit({
1687
- displayMessage: `Workflow ${workflowId} paused`,
1688
- id: nanoid(),
1689
- payload: {
1690
- workflowId,
1691
- workflowName: workflowInfo.workflowName
1692
- },
1693
- timestamp: Date.now(),
1694
- type: "workflow:paused"
1695
- }, this.ctx);
1714
+ this._emit("workflow:paused", {
1715
+ workflowId,
1716
+ workflowName: workflowInfo.workflowName
1717
+ });
1696
1718
  }
1697
1719
  /**
1698
1720
  * Resume a paused workflow.
@@ -1728,16 +1750,10 @@ var Agent = class Agent extends Server {
1728
1750
  }
1729
1751
  const status = await instance.status();
1730
1752
  this._updateWorkflowTracking(workflowId, status);
1731
- this.observability?.emit({
1732
- displayMessage: `Workflow ${workflowId} resumed`,
1733
- id: nanoid(),
1734
- payload: {
1735
- workflowId,
1736
- workflowName: workflowInfo.workflowName
1737
- },
1738
- timestamp: Date.now(),
1739
- type: "workflow:resumed"
1740
- }, this.ctx);
1753
+ this._emit("workflow:resumed", {
1754
+ workflowId,
1755
+ workflowName: workflowInfo.workflowName
1756
+ });
1741
1757
  }
1742
1758
  /**
1743
1759
  * Restart a workflow instance.
@@ -1795,16 +1811,10 @@ var Agent = class Agent extends Server {
1795
1811
  const status = await instance.status();
1796
1812
  this._updateWorkflowTracking(workflowId, status);
1797
1813
  }
1798
- this.observability?.emit({
1799
- displayMessage: `Workflow ${workflowId} restarted`,
1800
- id: nanoid(),
1801
- payload: {
1802
- workflowId,
1803
- workflowName: workflowInfo.workflowName
1804
- },
1805
- timestamp: Date.now(),
1806
- type: "workflow:restarted"
1807
- }, this.ctx);
1814
+ this._emit("workflow:restarted", {
1815
+ workflowId,
1816
+ workflowName: workflowInfo.workflowName
1817
+ });
1808
1818
  }
1809
1819
  /**
1810
1820
  * Find a workflow binding by its name.
@@ -2096,6 +2106,37 @@ var Agent = class Agent extends Server {
2096
2106
  if (key === className || camelCaseToKebabCase(key) === camelCaseToKebabCase(className)) return key;
2097
2107
  }
2098
2108
  }
2109
+ _findBindingNameForNamespace(namespace) {
2110
+ for (const [key, value] of Object.entries(this.env)) if (value === namespace) return key;
2111
+ }
2112
+ async _restoreRpcMcpServers() {
2113
+ const rpcServers = this.mcp.getRpcServersFromStorage();
2114
+ for (const server of rpcServers) {
2115
+ if (this.mcp.mcpConnections[server.id]) continue;
2116
+ const opts = server.server_options ? JSON.parse(server.server_options) : {};
2117
+ const namespace = this.env[opts.bindingName];
2118
+ if (!namespace) {
2119
+ console.warn(`[Agent] Cannot restore RPC MCP server "${server.name}": binding "${opts.bindingName}" not found in env`);
2120
+ continue;
2121
+ }
2122
+ const normalizedName = server.server_url.replace(RPC_DO_PREFIX, "");
2123
+ try {
2124
+ await this.mcp.connect(`${RPC_DO_PREFIX}${normalizedName}`, {
2125
+ reconnect: { id: server.id },
2126
+ transport: {
2127
+ type: "rpc",
2128
+ namespace,
2129
+ name: normalizedName,
2130
+ props: opts.props
2131
+ }
2132
+ });
2133
+ const conn = this.mcp.mcpConnections[server.id];
2134
+ if (conn && conn.connectionState === MCPConnectionState.CONNECTED) await this.mcp.discoverIfConnected(server.id);
2135
+ } catch (error) {
2136
+ console.error(`[Agent] Error restoring RPC MCP server "${server.name}":`, error);
2137
+ }
2138
+ }
2139
+ }
2099
2140
  /**
2100
2141
  * Handle a callback from a workflow.
2101
2142
  * Called when the Agent receives a callback at /_workflow/callback.
@@ -2202,63 +2243,87 @@ var Agent = class Agent extends Server {
2202
2243
  });
2203
2244
  } else if (action === "reset") this.setState(this.initialState);
2204
2245
  }
2205
- /**
2206
- * Connect to a new MCP Server
2207
- *
2208
- * @example
2209
- * // Simple usage
2210
- * await this.addMcpServer("github", "https://mcp.github.com");
2211
- *
2212
- * @example
2213
- * // With options (preferred for custom headers, transport, etc.)
2214
- * await this.addMcpServer("github", "https://mcp.github.com", {
2215
- * transport: { headers: { "Authorization": "Bearer ..." } }
2216
- * });
2217
- *
2218
- * @example
2219
- * // Legacy 5-parameter signature (still supported)
2220
- * await this.addMcpServer("github", url, callbackHost, agentsPrefix, options);
2221
- *
2222
- * @param serverName Name of the MCP server
2223
- * @param url MCP Server URL
2224
- * @param callbackHostOrOptions Options object, or callback host string (legacy)
2225
- * @param agentsPrefix agents routing prefix if not using `agents` (legacy)
2226
- * @param options MCP client and transport options (legacy)
2227
- * @returns Server id and state - either "authenticating" with authUrl, or "ready"
2228
- * @throws If connection or discovery fails
2229
- */
2230
- async addMcpServer(serverName, url, callbackHostOrOptions, agentsPrefix, options) {
2246
+ async addMcpServer(serverName, urlOrBinding, callbackHostOrOptions, agentsPrefix, options) {
2247
+ const isHttpTransport = typeof urlOrBinding === "string";
2248
+ const normalizedUrl = isHttpTransport ? new URL(urlOrBinding).href : void 0;
2249
+ const existingServer = this.mcp.listServers().find((s) => s.name === serverName && (!isHttpTransport || new URL(s.server_url).href === normalizedUrl));
2250
+ if (existingServer && this.mcp.mcpConnections[existingServer.id]) {
2251
+ const conn = this.mcp.mcpConnections[existingServer.id];
2252
+ if (conn.connectionState === MCPConnectionState.AUTHENTICATING && conn.options.transport.authProvider?.authUrl) return {
2253
+ id: existingServer.id,
2254
+ state: MCPConnectionState.AUTHENTICATING,
2255
+ authUrl: conn.options.transport.authProvider.authUrl
2256
+ };
2257
+ if (conn.connectionState === MCPConnectionState.FAILED) throw new Error(`MCP server "${serverName}" is in failed state: ${conn.connectionError}`);
2258
+ return {
2259
+ id: existingServer.id,
2260
+ state: MCPConnectionState.READY
2261
+ };
2262
+ }
2263
+ if (typeof urlOrBinding !== "string") {
2264
+ const rpcOpts = callbackHostOrOptions;
2265
+ const normalizedName = serverName.toLowerCase().replace(/\s+/g, "-");
2266
+ const reconnectId = existingServer?.id;
2267
+ const { id } = await this.mcp.connect(`${RPC_DO_PREFIX}${normalizedName}`, {
2268
+ reconnect: reconnectId ? { id: reconnectId } : void 0,
2269
+ transport: {
2270
+ type: "rpc",
2271
+ namespace: urlOrBinding,
2272
+ name: normalizedName,
2273
+ props: rpcOpts?.props
2274
+ }
2275
+ });
2276
+ const conn = this.mcp.mcpConnections[id];
2277
+ if (conn && conn.connectionState === MCPConnectionState.CONNECTED) {
2278
+ const discoverResult = await this.mcp.discoverIfConnected(id);
2279
+ if (discoverResult && !discoverResult.success) throw new Error(`Failed to discover MCP server capabilities: ${discoverResult.error}`);
2280
+ } else if (conn && conn.connectionState === MCPConnectionState.FAILED) throw new Error(`Failed to connect to MCP server "${serverName}" via RPC: ${conn.connectionError}`);
2281
+ const bindingName = this._findBindingNameForNamespace(urlOrBinding);
2282
+ if (bindingName) this.mcp.saveRpcServerToStorage(id, serverName, normalizedName, bindingName, rpcOpts?.props);
2283
+ return {
2284
+ id,
2285
+ state: MCPConnectionState.READY
2286
+ };
2287
+ }
2288
+ const httpOptions = callbackHostOrOptions;
2231
2289
  let resolvedCallbackHost;
2232
2290
  let resolvedAgentsPrefix;
2233
2291
  let resolvedOptions;
2234
2292
  let resolvedCallbackPath;
2235
- if (typeof callbackHostOrOptions === "object" && callbackHostOrOptions !== null) {
2236
- resolvedCallbackHost = callbackHostOrOptions.callbackHost;
2237
- resolvedCallbackPath = callbackHostOrOptions.callbackPath;
2238
- resolvedAgentsPrefix = callbackHostOrOptions.agentsPrefix ?? "agents";
2293
+ if (typeof httpOptions === "object" && httpOptions !== null) {
2294
+ resolvedCallbackHost = httpOptions.callbackHost;
2295
+ resolvedCallbackPath = httpOptions.callbackPath;
2296
+ resolvedAgentsPrefix = httpOptions.agentsPrefix ?? "agents";
2239
2297
  resolvedOptions = {
2240
- client: callbackHostOrOptions.client,
2241
- transport: callbackHostOrOptions.transport,
2242
- retry: callbackHostOrOptions.retry
2298
+ client: httpOptions.client,
2299
+ transport: httpOptions.transport,
2300
+ retry: httpOptions.retry
2243
2301
  };
2244
2302
  } else {
2245
- resolvedCallbackHost = callbackHostOrOptions;
2303
+ resolvedCallbackHost = httpOptions;
2246
2304
  resolvedAgentsPrefix = agentsPrefix ?? "agents";
2247
2305
  resolvedOptions = options;
2248
2306
  }
2249
- if (!this._resolvedOptions.sendIdentityOnConnect && !resolvedCallbackPath) throw new Error("callbackPath is required in addMcpServer options when sendIdentityOnConnect is false — the default callback URL would expose the instance name. Provide a callbackPath and route the callback request to this agent via getAgentByName.");
2307
+ if (!this._resolvedOptions.sendIdentityOnConnect && resolvedCallbackHost && !resolvedCallbackPath) throw new Error("callbackPath is required in addMcpServer options when sendIdentityOnConnect is false — the default callback URL would expose the instance name. Provide a callbackPath and route the callback request to this agent via getAgentByName.");
2250
2308
  if (!resolvedCallbackHost) {
2251
2309
  const { request } = getCurrentAgent();
2252
- if (!request) throw new Error("callbackHost is required when not called within a request context");
2253
- const requestUrl = new URL(request.url);
2254
- resolvedCallbackHost = `${requestUrl.protocol}//${requestUrl.host}`;
2310
+ if (request) {
2311
+ const requestUrl = new URL(request.url);
2312
+ resolvedCallbackHost = `${requestUrl.protocol}//${requestUrl.host}`;
2313
+ }
2314
+ }
2315
+ let callbackUrl;
2316
+ if (resolvedCallbackHost) {
2317
+ const normalizedHost = resolvedCallbackHost.replace(/\/$/, "");
2318
+ callbackUrl = resolvedCallbackPath ? `${normalizedHost}/${resolvedCallbackPath.replace(/^\//, "")}` : `${normalizedHost}/${resolvedAgentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
2255
2319
  }
2256
- const normalizedHost = resolvedCallbackHost.replace(/\/$/, "");
2257
- const callbackUrl = resolvedCallbackPath ? `${normalizedHost}/${resolvedCallbackPath.replace(/^\//, "")}` : `${normalizedHost}/${resolvedAgentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
2258
2320
  await this.mcp.ensureJsonSchema();
2259
2321
  const id = nanoid(8);
2260
- const authProvider = this.createMcpOAuthProvider(callbackUrl);
2261
- authProvider.serverId = id;
2322
+ let authProvider;
2323
+ if (callbackUrl) {
2324
+ authProvider = this.createMcpOAuthProvider(callbackUrl);
2325
+ authProvider.serverId = id;
2326
+ }
2262
2327
  const transportType = resolvedOptions?.transport?.type ?? "auto";
2263
2328
  let headerTransportOpts = {};
2264
2329
  if (resolvedOptions?.transport?.headers) headerTransportOpts = {
@@ -2269,7 +2334,7 @@ var Agent = class Agent extends Server {
2269
2334
  requestInit: { headers: resolvedOptions?.transport?.headers }
2270
2335
  };
2271
2336
  await this.mcp.registerServer(id, {
2272
- url,
2337
+ url: normalizedUrl,
2273
2338
  name: serverName,
2274
2339
  callbackUrl,
2275
2340
  client: resolvedOptions?.client,
@@ -2281,12 +2346,15 @@ var Agent = class Agent extends Server {
2281
2346
  retry: resolvedOptions?.retry
2282
2347
  });
2283
2348
  const result = await this.mcp.connectToServer(id);
2284
- if (result.state === MCPConnectionState.FAILED) throw new Error(`Failed to connect to MCP server at ${url}: ${result.error}`);
2285
- if (result.state === MCPConnectionState.AUTHENTICATING) return {
2286
- id,
2287
- state: result.state,
2288
- authUrl: result.authUrl
2289
- };
2349
+ if (result.state === MCPConnectionState.FAILED) throw new Error(`Failed to connect to MCP server at ${normalizedUrl}: ${result.error}`);
2350
+ if (result.state === MCPConnectionState.AUTHENTICATING) {
2351
+ if (!callbackUrl) throw new Error("This MCP server requires OAuth authentication. Provide callbackHost in addMcpServer options to enable the OAuth flow.");
2352
+ return {
2353
+ id,
2354
+ state: result.state,
2355
+ authUrl: result.authUrl
2356
+ };
2357
+ }
2290
2358
  const discoverResult = await this.mcp.discoverIfConnected(id);
2291
2359
  if (discoverResult && !discoverResult.success) throw new Error(`Failed to discover MCP server capabilities: ${discoverResult.error}`);
2292
2360
  return {
@@ -2312,7 +2380,7 @@ var Agent = class Agent extends Server {
2312
2380
  mcpState.servers[server.id] = {
2313
2381
  auth_url: server.auth_url,
2314
2382
  capabilities: serverConn?.serverCapabilities ?? null,
2315
- error: serverConn?.connectionError ?? null,
2383
+ error: sanitizeErrorString(serverConn?.connectionError ?? null),
2316
2384
  instructions: serverConn?.instructions ?? null,
2317
2385
  name: server.name,
2318
2386
  server_url: server.server_url,
@@ -2431,16 +2499,22 @@ async function routeAgentEmail(email, env, options) {
2431
2499
  }
2432
2500
  if (!agentMapCache.has(env)) {
2433
2501
  const map = {};
2502
+ const originalNames = [];
2434
2503
  for (const [key, value] of Object.entries(env)) if (value && typeof value === "object" && "idFromName" in value && typeof value.idFromName === "function") {
2435
2504
  map[key] = value;
2436
2505
  map[camelCaseToKebabCase(key)] = value;
2506
+ map[key.toLowerCase()] = value;
2507
+ originalNames.push(key);
2437
2508
  }
2438
- agentMapCache.set(env, map);
2509
+ agentMapCache.set(env, {
2510
+ map,
2511
+ originalNames
2512
+ });
2439
2513
  }
2440
- const agentMap = agentMapCache.get(env);
2441
- const namespace = agentMap[routingInfo.agentName];
2514
+ const cached = agentMapCache.get(env);
2515
+ const namespace = cached.map[routingInfo.agentName];
2442
2516
  if (!namespace) {
2443
- const availableAgents = Object.keys(agentMap).filter((key) => !key.includes("-")).join(", ");
2517
+ const availableAgents = cached.originalNames.join(", ");
2444
2518
  throw new Error(`Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`);
2445
2519
  }
2446
2520
  const agent = await getAgentByName(namespace, routingInfo.agentId);