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.
- package/AGENTS.md +24 -0
- package/CLAUDE.md +24 -0
- package/ai-docs/package.json +2 -2
- package/ai-docs/src/52_graphql/10_basics.ts +111 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +116 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +122 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -0
- package/dist/Context.d.ts.map +1 -1
- package/dist/Context.js +9 -0
- package/dist/Context.js.map +1 -1
- package/dist/RequestResolver.d.ts.map +1 -1
- package/dist/RequestResolver.js +4 -10
- package/dist/RequestResolver.js.map +1 -1
- package/dist/Scheduler.d.ts +9 -0
- package/dist/Scheduler.d.ts.map +1 -1
- package/dist/Scheduler.js +9 -0
- package/dist/Scheduler.js.map +1 -1
- package/dist/Schema.d.ts +8 -0
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +7 -0
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaGetter.d.ts +4 -0
- package/dist/SchemaGetter.d.ts.map +1 -1
- package/dist/SchemaGetter.js +52 -33
- package/dist/SchemaGetter.js.map +1 -1
- package/dist/Sink.d.ts.map +1 -1
- package/dist/Sink.js +38 -17
- package/dist/Sink.js.map +1 -1
- package/dist/Stream.d.ts +11 -0
- package/dist/Stream.d.ts.map +1 -1
- package/dist/Stream.js +16 -1
- package/dist/Stream.js.map +1 -1
- package/dist/Utils.js +8 -3
- package/dist/Utils.js.map +1 -1
- package/dist/ai/AnthropicStructuredOutput.js +0 -2
- package/dist/ai/AnthropicStructuredOutput.js.map +1 -1
- package/dist/ai/Chat.d.ts +2 -2
- package/dist/ai/Chat.d.ts.map +1 -1
- package/dist/ai/LanguageModel.d.ts +38 -7
- package/dist/ai/LanguageModel.d.ts.map +1 -1
- package/dist/ai/LanguageModel.js +25 -7
- package/dist/ai/LanguageModel.js.map +1 -1
- package/dist/ai/McpServer.d.ts +55 -0
- package/dist/ai/McpServer.d.ts.map +1 -1
- package/dist/ai/McpServer.js +132 -22
- package/dist/ai/McpServer.js.map +1 -1
- package/dist/ai/Prompt.d.ts.map +1 -1
- package/dist/ai/Prompt.js +28 -5
- package/dist/ai/Prompt.js.map +1 -1
- package/dist/ai/Response.d.ts +4 -3
- package/dist/ai/Response.d.ts.map +1 -1
- package/dist/ai/Response.js.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.js +46 -8
- package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
- package/dist/cluster/Sharding.d.ts.map +1 -1
- package/dist/cluster/Sharding.js +6 -4
- package/dist/cluster/Sharding.js.map +1 -1
- package/dist/encoding/Sse.d.ts +6 -0
- package/dist/encoding/Sse.d.ts.map +1 -1
- package/dist/encoding/Sse.js +62 -70
- package/dist/encoding/Sse.js.map +1 -1
- package/dist/graphql/GraphQL.d.ts +349 -0
- package/dist/graphql/GraphQL.d.ts.map +1 -0
- package/dist/graphql/GraphQL.js +207 -0
- package/dist/graphql/GraphQL.js.map +1 -0
- package/dist/graphql/GraphQLClient.d.ts +562 -0
- package/dist/graphql/GraphQLClient.d.ts.map +1 -0
- package/dist/graphql/GraphQLClient.js +498 -0
- package/dist/graphql/GraphQLClient.js.map +1 -0
- package/dist/graphql/GraphQLClientError.d.ts +445 -0
- package/dist/graphql/GraphQLClientError.d.ts.map +1 -0
- package/dist/graphql/GraphQLClientError.js +479 -0
- package/dist/graphql/GraphQLClientError.js.map +1 -0
- package/dist/graphql/GraphQLGroup.d.ts +118 -0
- package/dist/graphql/GraphQLGroup.d.ts.map +1 -0
- package/dist/graphql/GraphQLGroup.js +109 -0
- package/dist/graphql/GraphQLGroup.js.map +1 -0
- package/dist/graphql/GraphQLMiddleware.d.ts +249 -0
- package/dist/graphql/GraphQLMiddleware.d.ts.map +1 -0
- package/dist/graphql/GraphQLMiddleware.js +130 -0
- package/dist/graphql/GraphQLMiddleware.js.map +1 -0
- package/dist/graphql/GraphQLProtocol.d.ts +731 -0
- package/dist/graphql/GraphQLProtocol.d.ts.map +1 -0
- package/dist/graphql/GraphQLProtocol.js +719 -0
- package/dist/graphql/GraphQLProtocol.js.map +1 -0
- package/dist/graphql/GraphQLResolver.d.ts +453 -0
- package/dist/graphql/GraphQLResolver.d.ts.map +1 -0
- package/dist/graphql/GraphQLResolver.js +552 -0
- package/dist/graphql/GraphQLResolver.js.map +1 -0
- package/dist/graphql/index.d.ts +40 -0
- package/dist/graphql/index.d.ts.map +1 -0
- package/dist/graphql/index.js +41 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/http/HttpClient.d.ts +15 -0
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/HttpClient.js +24 -10
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/http/HttpEffect.d.ts.map +1 -1
- package/dist/http/HttpEffect.js +14 -7
- package/dist/http/HttpEffect.js.map +1 -1
- package/dist/http/HttpMiddleware.d.ts.map +1 -1
- package/dist/http/HttpMiddleware.js +31 -32
- package/dist/http/HttpMiddleware.js.map +1 -1
- package/dist/http/HttpRouter.d.ts +35 -0
- package/dist/http/HttpRouter.d.ts.map +1 -1
- package/dist/http/HttpRouter.js +35 -0
- package/dist/http/HttpRouter.js.map +1 -1
- package/dist/http/HttpServerResponse.d.ts +8 -6
- package/dist/http/HttpServerResponse.d.ts.map +1 -1
- package/dist/http/HttpServerResponse.js +27 -16
- package/dist/http/HttpServerResponse.js.map +1 -1
- package/dist/http/Multipart.d.ts.map +1 -1
- package/dist/http/Multipart.js +15 -1
- package/dist/http/Multipart.js.map +1 -1
- package/dist/http-api/HttpApiScalar.d.ts +5 -0
- package/dist/http-api/HttpApiScalar.d.ts.map +1 -1
- package/dist/http-api/HttpApiScalar.js.map +1 -1
- package/dist/internal/dateTime.js +40 -35
- package/dist/internal/dateTime.js.map +1 -1
- package/dist/internal/effect.js +8 -3
- package/dist/internal/effect.js.map +1 -1
- package/dist/internal/graphql.d.ts +2 -0
- package/dist/internal/graphql.d.ts.map +1 -0
- package/dist/internal/graphql.js +195 -0
- package/dist/internal/graphql.js.map +1 -0
- package/dist/internal/graphqlHeaders.d.ts +4 -0
- package/dist/internal/graphqlHeaders.d.ts.map +1 -0
- package/dist/internal/graphqlHeaders.js +11 -0
- package/dist/internal/graphqlHeaders.js.map +1 -0
- package/dist/internal/request.js +20 -1
- package/dist/internal/request.js.map +1 -1
- package/dist/internal/schema/toCodeDocument.js +4 -1
- package/dist/internal/schema/toCodeDocument.js.map +1 -1
- package/dist/internal/schema/toJsonSchemaDocument.js +29 -5
- package/dist/internal/schema/toJsonSchemaDocument.js.map +1 -1
- package/dist/internal/version.js +1 -1
- package/dist/net/Host.d.ts +49 -0
- package/dist/net/Host.d.ts.map +1 -1
- package/dist/net/Host.js +55 -0
- package/dist/net/Host.js.map +1 -1
- package/dist/net/NetAddress.d.ts +8 -0
- package/dist/net/NetAddress.d.ts.map +1 -1
- package/dist/net/NetAddress.js +8 -0
- package/dist/net/NetAddress.js.map +1 -1
- package/dist/rpc/RpcServer.d.ts.map +1 -1
- package/dist/rpc/RpcServer.js +36 -9
- package/dist/rpc/RpcServer.js.map +1 -1
- package/dist/socket/Socket.d.ts +6 -0
- package/dist/socket/Socket.d.ts.map +1 -1
- package/dist/socket/Socket.js.map +1 -1
- package/dist/workflow/DurableDeferred.d.ts.map +1 -1
- package/dist/workflow/DurableDeferred.js +32 -3
- package/dist/workflow/DurableDeferred.js.map +1 -1
- package/dist/workflow/WorkflowEngine.d.ts +24 -2
- package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
- package/dist/workflow/WorkflowEngine.js +11 -2
- package/dist/workflow/WorkflowEngine.js.map +1 -1
- package/package.json +6 -3
- package/src/Context.ts +9 -0
- package/src/RequestResolver.ts +4 -10
- package/src/Scheduler.ts +9 -0
- package/src/Schema.ts +8 -0
- package/src/SchemaGetter.ts +47 -35
- package/src/Sink.ts +45 -18
- package/src/Stream.ts +33 -4
- package/src/Utils.ts +8 -5
- package/src/ai/AnthropicStructuredOutput.ts +2 -2
- package/src/ai/Chat.ts +2 -2
- package/src/ai/LanguageModel.ts +96 -24
- package/src/ai/McpServer.ts +244 -90
- package/src/ai/Prompt.ts +24 -10
- package/src/ai/Response.ts +6 -3
- package/src/cluster/ClusterWorkflowEngine.ts +51 -12
- package/src/cluster/Sharding.ts +12 -9
- package/src/encoding/Sse.ts +69 -79
- package/src/graphql/GraphQL.ts +494 -0
- package/src/graphql/GraphQLClient.ts +912 -0
- package/src/graphql/GraphQLClientError.ts +542 -0
- package/src/graphql/GraphQLGroup.ts +167 -0
- package/src/graphql/GraphQLMiddleware.ts +253 -0
- package/src/graphql/GraphQLProtocol.ts +1286 -0
- package/src/graphql/GraphQLResolver.ts +1010 -0
- package/src/graphql/index.ts +48 -0
- package/src/http/HttpClient.ts +54 -10
- package/src/http/HttpEffect.ts +11 -5
- package/src/http/HttpMiddleware.ts +33 -34
- package/src/http/HttpRouter.ts +35 -0
- package/src/http/HttpServerResponse.ts +28 -16
- package/src/http/Multipart.ts +17 -1
- package/src/http-api/HttpApiScalar.ts +5 -0
- package/src/internal/dateTime.ts +13 -10
- package/src/internal/effect.ts +8 -3
- package/src/internal/graphql.ts +263 -0
- package/src/internal/graphqlHeaders.ts +11 -0
- package/src/internal/request.ts +31 -3
- package/src/internal/schema/toCodeDocument.ts +7 -1
- package/src/internal/schema/toJsonSchemaDocument.ts +34 -2
- package/src/internal/version.ts +1 -1
- package/src/net/Host.ts +72 -0
- package/src/net/NetAddress.ts +9 -0
- package/src/rpc/RpcServer.ts +55 -15
- package/src/socket/Socket.ts +6 -0
- package/src/workflow/DurableDeferred.ts +43 -5
- package/src/workflow/WorkflowEngine.ts +48 -4
package/src/ai/McpServer.ts
CHANGED
|
@@ -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
|
-
|
|
1256
|
-
Effect.
|
|
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
|
|
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
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
Effect.
|
|
1851
|
+
if (admission.acknowledge) {
|
|
1852
|
+
return yield* Effect.catchCause(
|
|
1853
|
+
Effect.map(httpEffect, toResponse),
|
|
1854
|
+
() => Effect.succeed(HttpServerResponse.empty({ status: 202 }))
|
|
1683
1855
|
)
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
}
|
|
1706
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
2282
|
-
messages.push(makeMessage("assistant", { content:
|
|
2295
|
+
if (assistantContent.length > 0) {
|
|
2296
|
+
messages.push(makeMessage("assistant", { content: assistantContent }))
|
|
2283
2297
|
}
|
|
2284
2298
|
|
|
2285
2299
|
if (toolParts.length > 0) {
|
package/src/ai/Response.ts
CHANGED
|
@@ -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
|
-
> =
|
|
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
|
|
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
|
-
//
|
|
332
|
-
//
|
|
333
|
-
// the
|
|
334
|
-
const
|
|
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)
|
|
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
|
-
|
|
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
|
|
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", {
|