effect 4.0.3 → 4.0.4

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.
Files changed (207) hide show
  1. package/AGENTS.md +24 -0
  2. package/CLAUDE.md +24 -0
  3. package/ai-docs/package.json +2 -2
  4. package/ai-docs/src/52_graphql/10_basics.ts +111 -0
  5. package/ai-docs/src/52_graphql/20_testing.ts +45 -0
  6. package/ai-docs/src/52_graphql/30_batching.ts +116 -0
  7. package/ai-docs/src/52_graphql/40_aliased-batching.ts +122 -0
  8. package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
  9. package/ai-docs/src/52_graphql/index.md +6 -0
  10. package/dist/Context.d.ts.map +1 -1
  11. package/dist/Context.js +9 -0
  12. package/dist/Context.js.map +1 -1
  13. package/dist/RequestResolver.d.ts.map +1 -1
  14. package/dist/RequestResolver.js +4 -10
  15. package/dist/RequestResolver.js.map +1 -1
  16. package/dist/Scheduler.d.ts +9 -0
  17. package/dist/Scheduler.d.ts.map +1 -1
  18. package/dist/Scheduler.js +9 -0
  19. package/dist/Scheduler.js.map +1 -1
  20. package/dist/Schema.d.ts +8 -0
  21. package/dist/Schema.d.ts.map +1 -1
  22. package/dist/Schema.js +7 -0
  23. package/dist/Schema.js.map +1 -1
  24. package/dist/SchemaGetter.d.ts +4 -0
  25. package/dist/SchemaGetter.d.ts.map +1 -1
  26. package/dist/SchemaGetter.js +52 -33
  27. package/dist/SchemaGetter.js.map +1 -1
  28. package/dist/Sink.d.ts.map +1 -1
  29. package/dist/Sink.js +38 -17
  30. package/dist/Sink.js.map +1 -1
  31. package/dist/Stream.d.ts +11 -0
  32. package/dist/Stream.d.ts.map +1 -1
  33. package/dist/Stream.js +16 -1
  34. package/dist/Stream.js.map +1 -1
  35. package/dist/Utils.js +8 -3
  36. package/dist/Utils.js.map +1 -1
  37. package/dist/ai/AnthropicStructuredOutput.js +0 -2
  38. package/dist/ai/AnthropicStructuredOutput.js.map +1 -1
  39. package/dist/ai/Chat.d.ts +2 -2
  40. package/dist/ai/Chat.d.ts.map +1 -1
  41. package/dist/ai/LanguageModel.d.ts +38 -7
  42. package/dist/ai/LanguageModel.d.ts.map +1 -1
  43. package/dist/ai/LanguageModel.js +25 -7
  44. package/dist/ai/LanguageModel.js.map +1 -1
  45. package/dist/ai/McpServer.d.ts +55 -0
  46. package/dist/ai/McpServer.d.ts.map +1 -1
  47. package/dist/ai/McpServer.js +132 -22
  48. package/dist/ai/McpServer.js.map +1 -1
  49. package/dist/ai/Prompt.d.ts.map +1 -1
  50. package/dist/ai/Prompt.js +28 -5
  51. package/dist/ai/Prompt.js.map +1 -1
  52. package/dist/ai/Response.d.ts +4 -3
  53. package/dist/ai/Response.d.ts.map +1 -1
  54. package/dist/ai/Response.js.map +1 -1
  55. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  56. package/dist/cluster/ClusterWorkflowEngine.js +46 -8
  57. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  58. package/dist/cluster/Sharding.d.ts.map +1 -1
  59. package/dist/cluster/Sharding.js +6 -4
  60. package/dist/cluster/Sharding.js.map +1 -1
  61. package/dist/encoding/Sse.d.ts +6 -0
  62. package/dist/encoding/Sse.d.ts.map +1 -1
  63. package/dist/encoding/Sse.js +62 -70
  64. package/dist/encoding/Sse.js.map +1 -1
  65. package/dist/graphql/GraphQL.d.ts +349 -0
  66. package/dist/graphql/GraphQL.d.ts.map +1 -0
  67. package/dist/graphql/GraphQL.js +207 -0
  68. package/dist/graphql/GraphQL.js.map +1 -0
  69. package/dist/graphql/GraphQLClient.d.ts +562 -0
  70. package/dist/graphql/GraphQLClient.d.ts.map +1 -0
  71. package/dist/graphql/GraphQLClient.js +498 -0
  72. package/dist/graphql/GraphQLClient.js.map +1 -0
  73. package/dist/graphql/GraphQLClientError.d.ts +445 -0
  74. package/dist/graphql/GraphQLClientError.d.ts.map +1 -0
  75. package/dist/graphql/GraphQLClientError.js +479 -0
  76. package/dist/graphql/GraphQLClientError.js.map +1 -0
  77. package/dist/graphql/GraphQLGroup.d.ts +118 -0
  78. package/dist/graphql/GraphQLGroup.d.ts.map +1 -0
  79. package/dist/graphql/GraphQLGroup.js +109 -0
  80. package/dist/graphql/GraphQLGroup.js.map +1 -0
  81. package/dist/graphql/GraphQLMiddleware.d.ts +249 -0
  82. package/dist/graphql/GraphQLMiddleware.d.ts.map +1 -0
  83. package/dist/graphql/GraphQLMiddleware.js +130 -0
  84. package/dist/graphql/GraphQLMiddleware.js.map +1 -0
  85. package/dist/graphql/GraphQLProtocol.d.ts +731 -0
  86. package/dist/graphql/GraphQLProtocol.d.ts.map +1 -0
  87. package/dist/graphql/GraphQLProtocol.js +719 -0
  88. package/dist/graphql/GraphQLProtocol.js.map +1 -0
  89. package/dist/graphql/GraphQLResolver.d.ts +453 -0
  90. package/dist/graphql/GraphQLResolver.d.ts.map +1 -0
  91. package/dist/graphql/GraphQLResolver.js +552 -0
  92. package/dist/graphql/GraphQLResolver.js.map +1 -0
  93. package/dist/graphql/index.d.ts +40 -0
  94. package/dist/graphql/index.d.ts.map +1 -0
  95. package/dist/graphql/index.js +41 -0
  96. package/dist/graphql/index.js.map +1 -0
  97. package/dist/http/HttpClient.d.ts +15 -0
  98. package/dist/http/HttpClient.d.ts.map +1 -1
  99. package/dist/http/HttpClient.js +24 -10
  100. package/dist/http/HttpClient.js.map +1 -1
  101. package/dist/http/HttpEffect.d.ts.map +1 -1
  102. package/dist/http/HttpEffect.js +14 -7
  103. package/dist/http/HttpEffect.js.map +1 -1
  104. package/dist/http/HttpMiddleware.d.ts.map +1 -1
  105. package/dist/http/HttpMiddleware.js +31 -32
  106. package/dist/http/HttpMiddleware.js.map +1 -1
  107. package/dist/http/HttpRouter.d.ts +35 -0
  108. package/dist/http/HttpRouter.d.ts.map +1 -1
  109. package/dist/http/HttpRouter.js +35 -0
  110. package/dist/http/HttpRouter.js.map +1 -1
  111. package/dist/http/HttpServerResponse.d.ts +8 -6
  112. package/dist/http/HttpServerResponse.d.ts.map +1 -1
  113. package/dist/http/HttpServerResponse.js +27 -16
  114. package/dist/http/HttpServerResponse.js.map +1 -1
  115. package/dist/http/Multipart.d.ts.map +1 -1
  116. package/dist/http/Multipart.js +15 -1
  117. package/dist/http/Multipart.js.map +1 -1
  118. package/dist/http-api/HttpApiScalar.d.ts +5 -0
  119. package/dist/http-api/HttpApiScalar.d.ts.map +1 -1
  120. package/dist/http-api/HttpApiScalar.js.map +1 -1
  121. package/dist/internal/dateTime.js +40 -35
  122. package/dist/internal/dateTime.js.map +1 -1
  123. package/dist/internal/effect.js +8 -3
  124. package/dist/internal/effect.js.map +1 -1
  125. package/dist/internal/graphql.d.ts +2 -0
  126. package/dist/internal/graphql.d.ts.map +1 -0
  127. package/dist/internal/graphql.js +195 -0
  128. package/dist/internal/graphql.js.map +1 -0
  129. package/dist/internal/graphqlHeaders.d.ts +4 -0
  130. package/dist/internal/graphqlHeaders.d.ts.map +1 -0
  131. package/dist/internal/graphqlHeaders.js +11 -0
  132. package/dist/internal/graphqlHeaders.js.map +1 -0
  133. package/dist/internal/request.js +20 -1
  134. package/dist/internal/request.js.map +1 -1
  135. package/dist/internal/schema/toCodeDocument.js +4 -1
  136. package/dist/internal/schema/toCodeDocument.js.map +1 -1
  137. package/dist/internal/schema/toJsonSchemaDocument.js +29 -5
  138. package/dist/internal/schema/toJsonSchemaDocument.js.map +1 -1
  139. package/dist/internal/version.js +1 -1
  140. package/dist/net/Host.d.ts +49 -0
  141. package/dist/net/Host.d.ts.map +1 -1
  142. package/dist/net/Host.js +55 -0
  143. package/dist/net/Host.js.map +1 -1
  144. package/dist/net/NetAddress.d.ts +8 -0
  145. package/dist/net/NetAddress.d.ts.map +1 -1
  146. package/dist/net/NetAddress.js +8 -0
  147. package/dist/net/NetAddress.js.map +1 -1
  148. package/dist/rpc/RpcServer.d.ts.map +1 -1
  149. package/dist/rpc/RpcServer.js +36 -9
  150. package/dist/rpc/RpcServer.js.map +1 -1
  151. package/dist/socket/Socket.d.ts +6 -0
  152. package/dist/socket/Socket.d.ts.map +1 -1
  153. package/dist/socket/Socket.js.map +1 -1
  154. package/dist/workflow/DurableDeferred.d.ts.map +1 -1
  155. package/dist/workflow/DurableDeferred.js +32 -3
  156. package/dist/workflow/DurableDeferred.js.map +1 -1
  157. package/dist/workflow/WorkflowEngine.d.ts +24 -2
  158. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  159. package/dist/workflow/WorkflowEngine.js +11 -2
  160. package/dist/workflow/WorkflowEngine.js.map +1 -1
  161. package/package.json +6 -3
  162. package/src/Context.ts +9 -0
  163. package/src/RequestResolver.ts +4 -10
  164. package/src/Scheduler.ts +9 -0
  165. package/src/Schema.ts +8 -0
  166. package/src/SchemaGetter.ts +47 -35
  167. package/src/Sink.ts +45 -18
  168. package/src/Stream.ts +33 -4
  169. package/src/Utils.ts +8 -5
  170. package/src/ai/AnthropicStructuredOutput.ts +2 -2
  171. package/src/ai/Chat.ts +2 -2
  172. package/src/ai/LanguageModel.ts +96 -24
  173. package/src/ai/McpServer.ts +244 -90
  174. package/src/ai/Prompt.ts +24 -10
  175. package/src/ai/Response.ts +6 -3
  176. package/src/cluster/ClusterWorkflowEngine.ts +51 -12
  177. package/src/cluster/Sharding.ts +12 -9
  178. package/src/encoding/Sse.ts +69 -79
  179. package/src/graphql/GraphQL.ts +494 -0
  180. package/src/graphql/GraphQLClient.ts +912 -0
  181. package/src/graphql/GraphQLClientError.ts +542 -0
  182. package/src/graphql/GraphQLGroup.ts +167 -0
  183. package/src/graphql/GraphQLMiddleware.ts +253 -0
  184. package/src/graphql/GraphQLProtocol.ts +1286 -0
  185. package/src/graphql/GraphQLResolver.ts +1010 -0
  186. package/src/graphql/index.ts +48 -0
  187. package/src/http/HttpClient.ts +54 -10
  188. package/src/http/HttpEffect.ts +11 -5
  189. package/src/http/HttpMiddleware.ts +33 -34
  190. package/src/http/HttpRouter.ts +35 -0
  191. package/src/http/HttpServerResponse.ts +28 -16
  192. package/src/http/Multipart.ts +17 -1
  193. package/src/http-api/HttpApiScalar.ts +5 -0
  194. package/src/internal/dateTime.ts +13 -10
  195. package/src/internal/effect.ts +8 -3
  196. package/src/internal/graphql.ts +263 -0
  197. package/src/internal/graphqlHeaders.ts +11 -0
  198. package/src/internal/request.ts +31 -3
  199. package/src/internal/schema/toCodeDocument.ts +7 -1
  200. package/src/internal/schema/toJsonSchemaDocument.ts +34 -2
  201. package/src/internal/version.ts +1 -1
  202. package/src/net/Host.ts +72 -0
  203. package/src/net/NetAddress.ts +9 -0
  204. package/src/rpc/RpcServer.ts +55 -15
  205. package/src/socket/Socket.ts +6 -0
  206. package/src/workflow/DurableDeferred.ts +43 -5
  207. package/src/workflow/WorkflowEngine.ts +48 -4
@@ -104,8 +104,19 @@ interface QueuedServerNotification {
104
104
  const internalState = new WeakMap<object, {
105
105
  readonly core: McpCore.McpCore
106
106
  readonly notifications: Queue.Dequeue<QueuedServerNotification>
107
- readonly notificationDelivery: { consumers: number; listChangeRevision: number }
107
+ readonly notificationDelivery: { consumers: number; listChangeRevision: number; requestEndpoints: number }
108
108
  }>()
109
+
110
+ // Request-scoped HTTP servers deliver a request's own notifications through
111
+ // that request's server instead of the shared notification queue.
112
+ const RequestNotificationQueue = Context.Reference<Queue.Queue<QueuedServerNotification> | undefined>(
113
+ "effect/ai/McpServer/RequestNotificationQueue",
114
+ { defaultValue: () => undefined }
115
+ )
116
+ // Progress and logs belong to the request that emitted them.
117
+ const isRequestNotification = (notification: McpCore.ServerNotification) =>
118
+ notification._tag === "Progress" || notification._tag === "LoggingMessage"
119
+
109
120
  type ServerExtensions = NonNullable<ServerCapabilities["extensions"]>
110
121
  type ServerNotificationRequest<
111
122
  R extends Rpc.Any = RpcGroup.Rpcs<typeof BroadcastServerNotificationRpcs>
@@ -322,7 +333,10 @@ export class McpServer extends Context.Service<McpServer, {
322
333
  readonly annotations: Context.Context<never>
323
334
  }> = []
324
335
  const notificationsQueue = yield* Queue.make<QueuedServerNotification>()
325
- const notificationDelivery = { consumers: 0, listChangeRevision: 0 }
336
+ const notificationDelivery = { consumers: 0, listChangeRevision: 0, requestEndpoints: 0 }
337
+ // With only request-scoped endpoints, no long-lived server ever takes from the shared queue.
338
+ const withoutSharedConsumer = () =>
339
+ notificationDelivery.consumers === 0 && notificationDelivery.requestEndpoints > 0
326
340
  const pendingListChanges = new Map<string, QueuedServerNotification>()
327
341
  const dispatcher = (yield* Scheduler).makeDispatcher()
328
342
  const notifications = yield* RpcClient.makeNoSerialization(BroadcastServerNotificationRpcs, {
@@ -344,6 +358,26 @@ export class McpServer extends Context.Service<McpServer, {
344
358
  requestContext: Context.getOrUndefined(fiber.context, McpRequestContext),
345
359
  requestHeaders: Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest)?.headers
346
360
  }
361
+ // write resumes the caller synchronously, so defer it until delivery completes.
362
+ const acknowledge = Effect.suspend(() =>
363
+ notifications.write({
364
+ clientId: 0,
365
+ requestId: message.id,
366
+ _tag: "Exit",
367
+ exit: Exit.void
368
+ })
369
+ )
370
+ // A request-scoped endpoint delivers its request's own progress and logs on that request's response.
371
+ const requestQueue = Context.get(fiber.context, RequestNotificationQueue)
372
+ if (
373
+ requestQueue !== undefined && queued.requestContext !== undefined && isRequestNotification(notification)
374
+ ) {
375
+ Queue.offerUnsafe(requestQueue, queued)
376
+ return Effect.andThen(Deferred.await(delivered), acknowledge)
377
+ }
378
+ if (withoutSharedConsumer()) {
379
+ return acknowledge
380
+ }
347
381
  let enqueued = false
348
382
  if (message.tag.includes("list_changed")) {
349
383
  const revision = ++notificationDelivery.listChangeRevision
@@ -364,15 +398,6 @@ export class McpServer extends Context.Service<McpServer, {
364
398
  enqueued = true
365
399
  Queue.offerUnsafe(notificationsQueue, queued)
366
400
  }
367
- // write resumes the caller synchronously, so defer it until delivery completes.
368
- const acknowledge = Effect.suspend(() =>
369
- notifications.write({
370
- clientId: 0,
371
- requestId: message.id,
372
- _tag: "Exit",
373
- exit: Exit.void
374
- })
375
- )
376
401
  return enqueued && queued.requestContext !== undefined && notificationDelivery.consumers > 0
377
402
  ? Effect.andThen(Deferred.await(delivered), acknowledge)
378
403
  : acknowledge
@@ -383,7 +408,7 @@ export class McpServer extends Context.Service<McpServer, {
383
408
  const service = McpServer.of({
384
409
  notifications: notifications.client,
385
410
  notifyElicitationComplete: ({ clientId, elicitationId }) =>
386
- Deferred.make<void>().pipe(
411
+ withoutSharedConsumer() ? Effect.void : Deferred.make<void>().pipe(
387
412
  Effect.flatMap((delivered) =>
388
413
  Queue.offer(notificationsQueue, {
389
414
  notification: McpCore.ServerNotification.ElicitationComplete({ elicitationId }),
@@ -727,7 +752,11 @@ const runWithRuntime = Effect.fnUntraced(function*(
727
752
  readonly allowSubscriptions?: boolean | undefined
728
753
  },
729
754
  runtime: McpRuntime.ServerRuntimeShape,
730
- transport: "custom" | "http" | "stdio"
755
+ transport: "custom" | "http" | "stdio",
756
+ requestScoped?: {
757
+ readonly notifications: Queue.Dequeue<QueuedServerNotification>
758
+ readonly handlers: Context.Context<never>
759
+ }
731
760
  ) {
732
761
  const protocol = yield* RpcServer.Protocol
733
762
  const server = yield* McpServer
@@ -817,7 +846,7 @@ const runWithRuntime = Effect.fnUntraced(function*(
817
846
  })
818
847
  let writeFromClient!: (clientId: number, message: RpcMessage.FromClientEncoded) => Effect.Effect<void>
819
848
  const { core, notificationDelivery, notifications } = internalState.get(server)!
820
- const handlers = yield* runtime.installHandlers({
849
+ const handlers = requestScoped?.handlers ?? (yield* runtime.installHandlers({
821
850
  core,
822
851
  subscribeServerNotifications: PubSub.subscribe(serverNotifications),
823
852
  getListChangeRevision: () => notificationDelivery.listChangeRevision,
@@ -831,7 +860,7 @@ const runWithRuntime = Effect.fnUntraced(function*(
831
860
  }),
832
861
  defaultLogLevel,
833
862
  serverInfo: options
834
- })
863
+ }))
835
864
 
836
865
  const clients = yield* RcMap.make({
837
866
  lookup: Effect.fnUntraced(function*(key: McpClientKey) {
@@ -1252,17 +1281,19 @@ const runWithRuntime = Effect.fnUntraced(function*(
1252
1281
  })
1253
1282
  }
1254
1283
 
1255
- yield* Effect.acquireRelease(
1256
- Effect.sync(() => {
1257
- notificationDelivery.consumers++
1258
- }),
1259
- () =>
1284
+ if (requestScoped === undefined) {
1285
+ yield* Effect.acquireRelease(
1260
1286
  Effect.sync(() => {
1261
- notificationDelivery.consumers--
1262
- })
1263
- )
1287
+ notificationDelivery.consumers++
1288
+ }),
1289
+ () =>
1290
+ Effect.sync(() => {
1291
+ notificationDelivery.consumers--
1292
+ })
1293
+ )
1294
+ }
1264
1295
  const notificationTails = new Map<number, Deferred.Deferred<void>>()
1265
- yield* Queue.take(notifications).pipe(
1296
+ yield* Queue.take(requestScoped?.notifications ?? notifications).pipe(
1266
1297
  Effect.flatMap(Effect.fnUntraced(function*(queued) {
1267
1298
  const { delivered, listChangeRevision, notification, targetClientId, requestContext, requestHeaders } = queued
1268
1299
  if (McpProtocolInternal.isSubscriptionServerNotification(notification)) {
@@ -1276,7 +1307,7 @@ const runWithRuntime = Effect.fnUntraced(function*(
1276
1307
  runtime.disconnect(clientId)
1277
1308
  }
1278
1309
  }
1279
- const requestNotification = notification._tag === "LoggingMessage" || notification._tag === "Progress"
1310
+ const requestNotification = isRequestNotification(notification)
1280
1311
  const deliveryClientIds = requestNotification && requestContext !== undefined
1281
1312
  ? [requestContext.clientId]
1282
1313
  : targetClientId === undefined
@@ -1594,11 +1625,20 @@ const mcpStdioSerialization = (
1594
1625
  * Method not found. Request-scoped progress and logs, legacy protocols, and
1595
1626
  * default subscription behavior are unchanged.
1596
1627
  *
1628
+ * The layer runs one long-lived MCP server that every POST shares. For
1629
+ * serverless hosts where execution must belong to each request, use
1630
+ * `layerHttpRequestScoped` instead.
1631
+ *
1597
1632
  * `layerHttp` always implements the single-endpoint Streamable HTTP topology.
1598
1633
  * Using `v2024_11_05` here is a custom compatibility transport for that
1599
1634
  * revision's schema. It does not implement the historical two-endpoint
1600
1635
  * HTTP+SSE transport, GET SSE, event resumption, or session expiry.
1601
1636
  *
1637
+ * **Gotchas**
1638
+ *
1639
+ * This layer starts a scoped server fiber. For sharing it across Cloudflare
1640
+ * Workers requests, see `HttpRouter.toWebHandler` for setup and limitations.
1641
+ *
1602
1642
  * @see {@link layerStdio} for exposing the server over stdio
1603
1643
  * @see {@link layer} for the base MCP server layer without a transport protocol
1604
1644
  *
@@ -1628,6 +1668,105 @@ export const layerHttp = (options: {
1628
1668
  )
1629
1669
  }
1630
1670
 
1671
+ /**
1672
+ * Registers a request-scoped Streamable HTTP MCP endpoint at `options.path`.
1673
+ *
1674
+ * **When to use**
1675
+ *
1676
+ * Use when serving on serverless hosts such as Cloudflare Workers, where execution must
1677
+ * belong to the HTTP request rather than a shared long-lived server.
1678
+ *
1679
+ * **Details**
1680
+ *
1681
+ * Each POST runs its own server in the request scope, with no background
1682
+ * fiber, timer, or queue consumer left after the response body completes.
1683
+ * Only sessionless protocols such as `v2026_07_28` are accepted. Tools,
1684
+ * resources, prompts, completions, multi round-trip input requests, and the
1685
+ * request's own progress and log notifications are supported. Legacy reverse
1686
+ * requests through `McpServer.elicit` are not available.
1687
+ *
1688
+ * Discovery advertises `listChanged: false`, `subscriptions/listen` returns
1689
+ * Method not found, and DELETE returns `405`. Request-scoped clients receive
1690
+ * no broadcast notifications such as list changes. Those still reach clients
1691
+ * of a `layerHttp` endpoint sharing the same `McpServer`, and are dropped when
1692
+ * no such endpoint exists. Use `layerHttp` on a long-lived host for sessions,
1693
+ * subscriptions, or legacy reverse requests.
1694
+ *
1695
+ * HTTP admission, origin checks, and JSON-RPC errors follow `layerHttp`.
1696
+ * Configuring a sessionful protocol through an untyped caller fails layer
1697
+ * construction with `IllegalArgumentError`.
1698
+ *
1699
+ * @see {@link layerHttp} for the long-lived HTTP transport and HTTP admission policy
1700
+ *
1701
+ * @stability unstable
1702
+ * @category layers
1703
+ * @since 4.0.0
1704
+ */
1705
+ export const layerHttpRequestScoped = (options: {
1706
+ readonly name: string
1707
+ readonly version: string
1708
+ readonly instructions?: string | undefined
1709
+ readonly description?: string | undefined
1710
+ readonly websiteUrl?: string | undefined
1711
+ readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined
1712
+ readonly path: HttpRouter.PathInput
1713
+ readonly protocols: Arr.NonEmptyReadonlyArray<
1714
+ McpProtocol.ProtocolAdapter<McpProtocol.ProtocolVersion, McpProtocol.StatelessRuntimeDescriptor>
1715
+ >
1716
+ readonly extensions?: ServerExtensions | undefined
1717
+ readonly allowedOrigins?: ReadonlyArray<string> | undefined
1718
+ }): Layer.Layer<McpServer | McpServerClient, Cause.IllegalArgumentError, HttpRouter.HttpRouter> =>
1719
+ Layer.effectDiscard(Effect.gen(function*() {
1720
+ const runtime = yield* McpRuntime.ServerRuntime
1721
+ const stateful = runtime.protocols.find((protocol) => protocol.runtime._tag === "Stateful")
1722
+ if (stateful !== undefined) {
1723
+ return yield* new Cause.IllegalArgumentError(
1724
+ `McpServer.layerHttpRequestScoped does not support the sessionful MCP protocol ${stateful.protocolVersion}`
1725
+ )
1726
+ }
1727
+ const server = yield* McpServer
1728
+ const { core, notificationDelivery } = internalState.get(server)!
1729
+ // Subscriptions are disabled, so handlers hold no per-request state and
1730
+ // read registrations from the shared core on every call. Request services
1731
+ // still take precedence over the layer context captured here.
1732
+ const handlers = yield* (runtime.installHandlers({
1733
+ core,
1734
+ subscribeServerNotifications: Effect.die("Subscriptions are disabled for request-scoped MCP servers"),
1735
+ getListChangeRevision: () => notificationDelivery.listChangeRevision,
1736
+ defaultLogLevel: yield* CurrentLogLevel,
1737
+ serverInfo: options
1738
+ }) as Effect.Effect<Context.Context<never>>)
1739
+ yield* Effect.acquireRelease(
1740
+ Effect.sync(() => {
1741
+ notificationDelivery.requestEndpoints++
1742
+ }),
1743
+ () =>
1744
+ Effect.sync(() => {
1745
+ notificationDelivery.requestEndpoints--
1746
+ })
1747
+ )
1748
+ // Each POST runs its own RPC server in the request scope, so no fiber,
1749
+ // timer, or queue consumer outlives the request that created it.
1750
+ yield* addMcpHttpRoutes(
1751
+ options,
1752
+ Effect.gen(function*() {
1753
+ const { httpEffect, protocol } = yield* RpcServer.makeProtocolWithHttpEffect().pipe(
1754
+ Effect.provideService(RpcSerialization.RpcSerialization, mcpHttpSerialization)
1755
+ )
1756
+ const notifications = yield* Queue.make<QueuedServerNotification>()
1757
+ yield* runWithRuntime(options, runtime, "http", { notifications, handlers }).pipe(
1758
+ Effect.provideService(RpcServer.Protocol, protocol),
1759
+ Effect.provideService(McpServer, server),
1760
+ Effect.forkScoped({ startImmediately: true })
1761
+ )
1762
+ return yield* Effect.provideService(httpEffect, RequestNotificationQueue, notifications)
1763
+ })
1764
+ )
1765
+ })).pipe(
1766
+ Layer.provideMerge(McpServer.layer),
1767
+ Layer.provide(McpRuntime.layer(options.protocols))
1768
+ )
1769
+
1631
1770
  const layerMcpProtocolHttp = (options: {
1632
1771
  readonly path: HttpRouter.PathInput
1633
1772
  readonly allowedOrigins?: ReadonlyArray<string> | undefined
@@ -1638,81 +1777,96 @@ const layerMcpProtocolHttp = (options: {
1638
1777
  McpRuntime.ServerRuntime | HttpRouter.HttpRouter
1639
1778
  > =>
1640
1779
  Layer.effect(RpcServer.Protocol)(Effect.gen(function*() {
1641
- const runtime = yield* McpRuntime.ServerRuntime
1642
1780
  const { httpEffect, protocol } = yield* RpcServer.makeProtocolWithHttpEffect().pipe(
1643
1781
  Effect.provideService(RpcSerialization.RpcSerialization, mcpHttpSerialization)
1644
1782
  )
1645
- const router = yield* HttpRouter.HttpRouter
1646
- const allowSessionTermination = options.allowSessionTermination === true &&
1647
- runtime.protocols.some((protocol) => protocol.runtime._tag === "Stateful")
1648
- const forbidden = Effect.succeed(HttpServerResponse.empty({ status: 403 }))
1649
- const withAllowedOrigin = (
1650
- handler: (request: HttpServerRequest.HttpServerRequest) => Effect.Effect<HttpServerResponse.HttpServerResponse>
1651
- ) =>
1652
- (request: HttpServerRequest.HttpServerRequest) =>
1653
- isAllowedMcpOrigin(request, options.allowedOrigins) ? handler(request) : forbidden
1654
- const methodNotAllowedResponse = Effect.succeed(HttpServerResponse.empty({
1655
- status: 405,
1656
- headers: { allow: allowSessionTermination ? "POST, DELETE" : "POST" }
1657
- }))
1658
- const methodNotAllowed = withAllowedOrigin(() => methodNotAllowedResponse)
1659
- for (const method of ["GET", "PUT", "PATCH", "OPTIONS"] as const) {
1660
- yield* router.add(method, options.path, methodNotAllowed)
1783
+ yield* addMcpHttpRoutes(options, httpEffect)
1784
+ return protocol
1785
+ }))
1786
+
1787
+ const addMcpHttpRoutes = Effect.fnUntraced(function*(
1788
+ options: {
1789
+ readonly path: HttpRouter.PathInput
1790
+ readonly allowedOrigins?: ReadonlyArray<string> | undefined
1791
+ readonly allowSessionTermination?: boolean | undefined
1792
+ },
1793
+ httpEffect: Effect.Effect<
1794
+ HttpServerResponse.HttpServerResponse,
1795
+ never,
1796
+ Scope.Scope | HttpServerRequest.HttpServerRequest
1797
+ >
1798
+ ) {
1799
+ const runtime = yield* McpRuntime.ServerRuntime
1800
+ const router = yield* HttpRouter.HttpRouter
1801
+ const allowSessionTermination = options.allowSessionTermination === true &&
1802
+ runtime.protocols.some((protocol) => protocol.runtime._tag === "Stateful")
1803
+ const forbidden = Effect.succeed(HttpServerResponse.empty({ status: 403 }))
1804
+ const withAllowedOrigin = (
1805
+ handler: (request: HttpServerRequest.HttpServerRequest) => Effect.Effect<HttpServerResponse.HttpServerResponse>
1806
+ ) =>
1807
+ (request: HttpServerRequest.HttpServerRequest) =>
1808
+ isAllowedMcpOrigin(request, options.allowedOrigins) ? handler(request) : forbidden
1809
+ const methodNotAllowedResponse = Effect.succeed(HttpServerResponse.empty({
1810
+ status: 405,
1811
+ headers: { allow: allowSessionTermination ? "POST, DELETE" : "POST" }
1812
+ }))
1813
+ const methodNotAllowed = withAllowedOrigin(() => methodNotAllowedResponse)
1814
+ for (const method of ["GET", "PUT", "PATCH", "OPTIONS"] as const) {
1815
+ yield* router.add(method, options.path, methodNotAllowed)
1816
+ }
1817
+ yield* router.add(
1818
+ "DELETE",
1819
+ options.path,
1820
+ allowSessionTermination
1821
+ ? withAllowedOrigin((request) => runtime.terminateHttpSession(request.headers))
1822
+ : methodNotAllowed
1823
+ )
1824
+ yield* router.add("POST", options.path, (request) => {
1825
+ if (!isAllowedMcpOrigin(request, options.allowedOrigins)) {
1826
+ return Effect.succeed(HttpServerResponse.empty({ status: 403 }))
1661
1827
  }
1662
- yield* router.add(
1663
- "DELETE",
1664
- options.path,
1665
- allowSessionTermination
1666
- ? withAllowedOrigin((request) => runtime.terminateHttpSession(request.headers))
1667
- : methodNotAllowed
1668
- )
1669
- yield* router.add("POST", options.path, (request) => {
1670
- if (!isAllowedMcpOrigin(request, options.allowedOrigins)) {
1671
- return Effect.succeed(HttpServerResponse.empty({ status: 403 }))
1672
- }
1673
- if (mcpMediaTypes(request.headers["content-type"])[0] !== "application/json") {
1674
- return Effect.succeed(HttpServerResponse.empty({ status: 415 }))
1828
+ if (mcpMediaTypes(request.headers["content-type"])[0] !== "application/json") {
1829
+ return Effect.succeed(HttpServerResponse.empty({ status: 415 }))
1830
+ }
1831
+ const accepted = mcpMediaTypes(request.headers["accept"])
1832
+ if (!accepted.includes("application/json") || !accepted.includes("text/event-stream")) {
1833
+ return Effect.succeed(HttpServerResponse.empty({ status: 406 }))
1834
+ }
1835
+ return Effect.gen(function*() {
1836
+ const parsed = yield* Effect.result(
1837
+ Effect.flatMap(request.text, Schema.decodeEffect(Schema.UnknownFromJsonString))
1838
+ )
1839
+ const admission = runtime.admitHttp(request.headers, parsed)
1840
+ if (admission._tag === "Rejected") {
1841
+ return admission.response
1675
1842
  }
1676
- const accepted = mcpMediaTypes(request.headers["accept"])
1677
- if (!accepted.includes("application/json") || !accepted.includes("text/event-stream")) {
1678
- return Effect.succeed(HttpServerResponse.empty({ status: 406 }))
1843
+ const toResponse = (response: HttpServerResponse.HttpServerResponse) => {
1844
+ // Completed responses may already contain notifications followed by the result.
1845
+ const hasMultipleMessages = response.body._tag === "Uint8Array" &&
1846
+ response.body.body.subarray(0, -1).includes(10)
1847
+ return admission.isSubscription || response.body._tag === "Stream" || hasMultipleMessages
1848
+ ? toServerSentEvents(response, admission.isSubscription)
1849
+ : response
1679
1850
  }
1680
- return Effect.gen(function*() {
1681
- const parsed = yield* Effect.result(
1682
- Effect.flatMap(request.text, Schema.decodeEffect(Schema.UnknownFromJsonString))
1851
+ if (admission.acknowledge) {
1852
+ return yield* Effect.catchCause(
1853
+ Effect.map(httpEffect, toResponse),
1854
+ () => Effect.succeed(HttpServerResponse.empty({ status: 202 }))
1683
1855
  )
1684
- const admission = runtime.admitHttp(request.headers, parsed)
1685
- if (admission._tag === "Rejected") {
1686
- return admission.response
1687
- }
1688
- const toResponse = (response: HttpServerResponse.HttpServerResponse) => {
1689
- // Completed responses may already contain notifications followed by the result.
1690
- const hasMultipleMessages = response.body._tag === "Uint8Array" &&
1691
- response.body.body.subarray(0, -1).includes(10)
1692
- return admission.isSubscription || response.body._tag === "Stream" || hasMultipleMessages
1693
- ? toServerSentEvents(response, admission.isSubscription)
1694
- : response
1695
- }
1696
- if (admission.acknowledge) {
1697
- return yield* Effect.catchCause(
1698
- Effect.map(httpEffect, toResponse),
1699
- () => Effect.succeed(HttpServerResponse.empty({ status: 202 }))
1700
- )
1856
+ }
1857
+ return yield* Effect.flatMap(httpEffect, (response) => {
1858
+ if (response.body._tag !== "Uint8Array" || response.body.body.length > 0) {
1859
+ return Effect.succeed(toResponse(response))
1701
1860
  }
1702
- return yield* Effect.flatMap(httpEffect, (response) => {
1703
- if (response.body._tag !== "Uint8Array" || response.body.body.length > 0) {
1704
- return Effect.succeed(toResponse(response))
1705
- }
1706
- // Only fully cancelled request POSTs may return an empty SSE response.
1707
- // https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
1708
- return httpPostResponses.get(request)?.expected === 0
1709
- ? Effect.succeed(HttpServerResponse.stream(Stream.empty, { contentType: "text/event-stream" }))
1710
- : Effect.die(new Error("MCP request ended without writing its response"))
1711
- })
1861
+ // Only fully cancelled request POSTs may return an empty SSE response.
1862
+ // https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
1863
+ return httpPostResponses.get(request)?.expected === 0
1864
+ ? Effect.succeed(HttpServerResponse.stream(Stream.empty, { contentType: "text/event-stream" }))
1865
+ : Effect.die(new Error("MCP request ended without writing its response"))
1712
1866
  })
1713
1867
  })
1714
- return protocol
1715
- }))
1868
+ })
1869
+ })
1716
1870
 
1717
1871
  const mcpHttpSerialization: RpcSerialization.RpcSerialization["Service"] = (() => {
1718
1872
  const serialization = mcpJsonRpcSerialization()
package/src/ai/Prompt.ts CHANGED
@@ -2159,11 +2159,11 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
2159
2159
  return empty
2160
2160
  }
2161
2161
 
2162
- const assistantParts: Array<AssistantMessagePart> = []
2162
+ const assistantParts: Array<AssistantMessagePart | undefined> = []
2163
2163
  const toolParts: Array<ToolMessagePart> = []
2164
2164
 
2165
- const activeTextDeltas = new Map<string, { text: string; options: ProviderOptions }>()
2166
- const activeReasoningDeltas = new Map<string, { text: string; options: ProviderOptions }>()
2165
+ const activeTextDeltas = new Map<string, { index: number; text: string; options: ProviderOptions }>()
2166
+ const activeReasoningDeltas = new Map<string, { index: number; text: string; options: ProviderOptions }>()
2167
2167
 
2168
2168
  for (const part of parts) {
2169
2169
  switch (part.type) {
@@ -2175,7 +2175,8 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
2175
2175
 
2176
2176
  // Text Parts (streaming)
2177
2177
  case "text-start": {
2178
- activeTextDeltas.set(part.id, { text: "", options: part.metadata })
2178
+ activeTextDeltas.set(part.id, { index: assistantParts.length, text: "", options: part.metadata })
2179
+ assistantParts.push(undefined)
2179
2180
  break
2180
2181
  }
2181
2182
  case "text-delta": {
@@ -2190,7 +2191,8 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
2190
2191
  if (activeTextDeltas.has(part.id)) {
2191
2192
  const active = activeTextDeltas.get(part.id)!
2192
2193
  active.options = mergeOptions(active.options, part.metadata)
2193
- assistantParts.push(makePart("text", active))
2194
+ assistantParts[active.index] = makePart("text", { text: active.text, options: active.options })
2195
+ activeTextDeltas.delete(part.id)
2194
2196
  }
2195
2197
  break
2196
2198
  }
@@ -2203,7 +2205,8 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
2203
2205
 
2204
2206
  // Reasoning Parts (streaming)
2205
2207
  case "reasoning-start": {
2206
- activeReasoningDeltas.set(part.id, { text: "", options: part.metadata })
2208
+ activeReasoningDeltas.set(part.id, { index: assistantParts.length, text: "", options: part.metadata })
2209
+ assistantParts.push(undefined)
2207
2210
  break
2208
2211
  }
2209
2212
  case "reasoning-delta": {
@@ -2218,7 +2221,8 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
2218
2221
  if (activeReasoningDeltas.has(part.id)) {
2219
2222
  const active = activeReasoningDeltas.get(part.id)!
2220
2223
  active.options = mergeOptions(active.options, part.metadata)
2221
- assistantParts.push(makePart("reasoning", active))
2224
+ assistantParts[active.index] = makePart("reasoning", { text: active.text, options: active.options })
2225
+ activeReasoningDeltas.delete(part.id)
2222
2226
  }
2223
2227
  break
2224
2228
  }
@@ -2272,14 +2276,24 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
2272
2276
  }
2273
2277
  }
2274
2278
 
2275
- if (assistantParts.length === 0 && toolParts.length === 0) {
2279
+ // Keep partial text from unended blocks. Unended reasoning is dropped since it
2280
+ // may lack the signature or encrypted content providers require for replay.
2281
+ for (const active of activeTextDeltas.values()) {
2282
+ if (active.text.length > 0) {
2283
+ assistantParts[active.index] = makePart("text", { text: active.text, options: active.options })
2284
+ }
2285
+ }
2286
+
2287
+ const assistantContent = assistantParts.filter((part) => part !== undefined)
2288
+
2289
+ if (assistantContent.length === 0 && toolParts.length === 0) {
2276
2290
  return empty
2277
2291
  }
2278
2292
 
2279
2293
  const messages: Array<Message> = []
2280
2294
 
2281
- if (assistantParts.length > 0) {
2282
- messages.push(makeMessage("assistant", { content: assistantParts }))
2295
+ if (assistantContent.length > 0) {
2296
+ messages.push(makeMessage("assistant", { content: assistantContent }))
2283
2297
  }
2284
2298
 
2285
2299
  if (toolParts.length > 0) {
@@ -441,16 +441,19 @@ export const StreamPart = <T extends Toolkit.Any | Toolkit.WithHandler<any>>(
441
441
  export type ToolCallParts<
442
442
  Tools extends Record<string, Tool.Any>,
443
443
  ParametersMode extends ToolParametersMode = "decoded"
444
- > = ToolCallPartForName<Tools, ParametersMode, keyof Tools>
444
+ > = ParametersMode extends "deferred" ?
445
+ ToolCallPartForName<Tools, "opaque", keyof Tools> | ToolCallPart<string, unknown>
446
+ : ToolCallPartForName<Tools, ParametersMode, keyof Tools>
445
447
 
446
448
  /**
447
- * Controls whether tool parameters are decoded, encoded, or opaque.
449
+ * Controls tool parameter decoding. `"deferred"` allows undeclared tool names
450
+ * with opaque parameters, leaving validation to the caller.
448
451
  *
449
452
  * @stability unstable
450
453
  * @category utility types
451
454
  * @since 4.0.0
452
455
  */
453
- export type ToolParametersMode = "decoded" | "encoded" | "opaque"
456
+ export type ToolParametersMode = "decoded" | "encoded" | "opaque" | "deferred"
454
457
 
455
458
  type ToolCallPartForName<
456
459
  Tools extends Record<string, Tool.Any>,
@@ -291,10 +291,7 @@ export const make = Effect.gen(function*() {
291
291
  id: ""
292
292
  })
293
293
 
294
- const maybeSuspended = Option.filter(
295
- maybeReply,
296
- (reply) => reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended"
297
- )
294
+ const maybeSuspended = Option.filter(maybeReply, isSuspendedReply)
298
295
 
299
296
  if (Option.isNone(maybeSuspended)) return
300
297
  if (
@@ -328,10 +325,14 @@ export const make = Effect.gen(function*() {
328
325
  yield* sharding.reset(requestId.value)
329
326
  }, Effect.scoped)
330
327
 
331
- // A child or deferred may complete while a run is still unwinding. Subscribe
332
- // to the run's reply before reading storage so the wake cannot fall between
333
- // the run's last read and its suspended reply.
334
- const waitForRunReply = Effect.fnUntraced(function*(workflow: Workflow.Any, request: Entity.Request<any>) {
328
+ // Subscribe to the run's next reply before reading storage, so a reply
329
+ // cannot fall between the read and the subscription. Returns `true` when the
330
+ // stored reply already settles the wait, `false` when a new reply arrived.
331
+ const awaitRunReply = Effect.fnUntraced(function*(
332
+ workflow: Workflow.Any,
333
+ request: Envelope.Request<any>,
334
+ isSettled: (reply: WithExitEncoded<Workflow.ResultEncoded<any, any>>) => boolean
335
+ ) {
335
336
  const waiter = yield* storage.registerReplyHandler(
336
337
  new Message.OutgoingRequest<any>({
337
338
  envelope: request,
@@ -343,12 +344,45 @@ export const make = Effect.gen(function*() {
343
344
  })
344
345
  ).pipe(Effect.forkScoped({ startImmediately: true }))
345
346
  const reply = yield* replyForRequestId(request.requestId)
346
- if (Option.isSome(reply)) return
347
- yield* Fiber.join(waiter).pipe(
347
+ if (Option.isSome(reply) && isSettled(reply.value)) return true
348
+ yield* Fiber.join(waiter)
349
+ return false
350
+ }, Effect.scoped)
351
+
352
+ // A child or deferred may complete while a run is still unwinding. Wait for
353
+ // the run's reply so the wake cannot fall between the run's last read and
354
+ // its suspended reply.
355
+ const waitForRunReply = (workflow: Workflow.Any, request: Entity.Request<any>) =>
356
+ awaitRunReply(workflow, request, () => true).pipe(
357
+ Effect.asVoid,
348
358
  Effect.catchTag("EntityNotAssignedToRunner", () => ClusterAbandon.interrupt),
349
359
  Effect.catchCause((cause) => ClusterAbandon.isCause(cause) ? ClusterAbandon.interrupt : Effect.failCause(cause))
350
360
  )
351
- }, Effect.scoped)
361
+
362
+ // Wake an external caller once its run publishes a reply other than
363
+ // `Suspended`. Replies are only observed on the runner that saves them, so
364
+ // callers on other runners, and any failure here, leave delivery to the
365
+ // caller's retry schedule.
366
+ const awaitComplete = (workflow: Workflow.Any, executionId: string) =>
367
+ Effect.gen(function*() {
368
+ const address = entityAddressFor({ workflow, entityType: `Workflow/${workflow._tag}`, executionId })
369
+ if (!sharding.hasShardId(address.shardId)) return yield* Effect.never
370
+ const requestId = yield* storage.requestIdForPrimaryKey({ address, tag: "run", id: "" })
371
+ if (Option.isNone(requestId)) return yield* Effect.never
372
+ const request = Envelope.makeRequest<any>({
373
+ requestId: requestId.value,
374
+ address,
375
+ tag: "run",
376
+ payload: {},
377
+ headers: Headers.empty
378
+ })
379
+ // A replay that suspends again re-arms the wait, so it does not spend a
380
+ // step of the caller's retry schedule.
381
+ let settled = false
382
+ while (!settled) {
383
+ settled = yield* awaitRunReply(workflow, request, (reply) => !isSuspendedReply(reply))
384
+ }
385
+ }).pipe(Effect.catchCause(() => Effect.never))
352
386
 
353
387
  const interrupt = Effect.fnUntraced(
354
388
  function*(workflow: Workflow.Any, executionId: string) {
@@ -367,7 +401,7 @@ export const make = Effect.gen(function*() {
367
401
 
368
402
  const nonSuspendedReply = Option.filter(
369
403
  reply,
370
- (reply) => reply.exit._tag !== "Success" || reply.exit.value._tag !== "Suspended"
404
+ (reply) => !isSuspendedReply(reply)
371
405
  )
372
406
  if (Option.isSome(nonSuspendedReply)) {
373
407
  return Option.none()
@@ -617,6 +651,8 @@ export const make = Effect.gen(function*() {
617
651
 
618
652
  resume: (workflow, executionId) => ensureSuccess(resume(workflow, executionId)),
619
653
 
654
+ awaitComplete,
655
+
620
656
  activityExecute: Effect.fnUntraced(
621
657
  function*(activity, attempt) {
622
658
  const services = yield* Effect.context<WorkflowEngine.WorkflowInstance>()
@@ -809,6 +845,9 @@ const DeferredRpc = Rpc.make("deferred", {
809
845
  .annotate(ClusterSchema.Persisted, true)
810
846
  .annotate(ClusterSchema.Uninterruptible, true)
811
847
 
848
+ const isSuspendedReply = (reply: WithExitEncoded<Workflow.ResultEncoded<any, any>>) =>
849
+ reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended"
850
+
812
851
  const decodeDeferredWithExit = Schema.decodeSync(Schema.toCodecJson(Reply.WithExit.schema(DeferredRpc)))
813
852
 
814
853
  const ResumeRpc = Rpc.make("resume", {