Unified · Split
| @@ -13,5 +13,5 @@ App wiring is now split by responsibility: · 12 unmodified lines | ||
| 13 | 13 | - `src/config.ts`: Effect config service (`PORT`) |
| 14 | 14 | - `src/run-executor.ts`: run execution service for an injected runtime adapter |
| 15 | 15 | - `src/mcp-handler.ts`: MCP transport wiring to run client |
| 16 | −- `src/http-server.ts`: Bun server startup | |
| 16 | +- `src/http-server.ts`: Effect HTTP server startup | |
| 17 | 17 | - `src/main.ts`: Layer composition + process entrypoint |
| @@ -9,6 +9,8 @@ · 8 unmodified lines | ||
| 9 | 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 10 | 10 | }, |
| 11 | 11 | "dependencies": { |
| 12 | + "@effect/platform": "catalog:", | |
| 13 | + "@effect/platform-bun": "^0.86.0", | |
| 12 | 14 | "@executor-v2/engine": "workspace:*", |
| 13 | 15 | "@executor-v2/mcp-gateway": "workspace:*", |
| 14 | 16 | "@executor-v2/sdk": "workspace:*", |
| @@ -1,40 +1,45 @@ | ||
| 1 | +import { | |
| 2 | + HttpRouter, | |
| 3 | + HttpServer, | |
| 4 | + HttpServerRequest, | |
| 5 | + HttpServerResponse, | |
| 6 | +} from "@effect/platform"; | |
| 7 | +import { BunHttpServer, BunHttpServerRequest } from "@effect/platform-bun"; | |
| 1 | 8 | import * as Effect from "effect/Effect"; |
| 9 | +import * as Layer from "effect/Layer"; | |
| 2 | 10 | |
| 3 | 11 | import { PmConfig } from "./config"; |
| 4 | 12 | import { PmMcpHandler } from "./mcp-handler"; |
| 5 | −import { PmToolCallHttpHandler } from "./tool-call-handler"; | |
| 13 | +import { handleToolCallHttp } from "./tool-call-handler"; | |
| 14 | + | |
| 15 | +const fromWebHandler = (handler: (request: Request) => Promise<Response>) => | |
| 16 | + Effect.gen(function* () { | |
| 17 | + const request = yield* HttpServerRequest.HttpServerRequest; | |
| 18 | + const response = yield* Effect.promise(() => | |
| 19 | + handler(BunHttpServerRequest.toRequest(request)) | |
| 20 | + ); | |
| 21 | + | |
| 22 | + return HttpServerResponse.raw(response); | |
| 23 | + }); | |
| 6 | 24 | |
| 7 | 25 | export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () { |
| 8 | 26 | const { port } = yield* PmConfig; |
| 9 | 27 | const { handleMcp } = yield* PmMcpHandler; |
| 10 | − const { handleToolCallHttp } = yield* PmToolCallHttpHandler; | |
| 11 | − | |
| 12 | − const server = Bun.serve({ | |
| 13 | − port, | |
| 14 | − routes: { | |
| 15 | − "/healthz": { | |
| 16 | − GET: () => Response.json({ ok: true, service: "pm" }, { status: 200 }), | |
| 17 | − }, | |
| 18 | − "/mcp": { | |
| 19 | − GET: handleMcp, | |
| 20 | − POST: handleMcp, | |
| 21 | − DELETE: handleMcp, | |
| 22 | − }, | |
| 23 | − "/v1/mcp": { | |
| 24 | − GET: handleMcp, | |
| 25 | − POST: handleMcp, | |
| 26 | − DELETE: handleMcp, | |
| 27 | − }, | |
| 28 | − "/runtime/tool-call": { | |
| 29 | − POST: handleToolCallHttp, | |
| 30 | − }, | |
| 31 | − "/v1/runtime/tool-call": { | |
| 32 | − POST: handleToolCallHttp, | |
| 33 | − }, | |
| 34 | − }, | |
| 35 | − }); | |
| 36 | 28 | |
| 37 | − yield* Effect.logInfo(`executor-v2 PM listening on http://127.0.0.1:${server.port}`); | |
| 29 | + const httpLive = HttpRouter.empty.pipe( | |
| 30 | + HttpRouter.get("/healthz", HttpServerResponse.json({ ok: true, service: "pm" })), | |
| 31 | + HttpRouter.get("/mcp", fromWebHandler(handleMcp)), | |
| 32 | + HttpRouter.post("/mcp", fromWebHandler(handleMcp)), | |
| 33 | + HttpRouter.del("/mcp", fromWebHandler(handleMcp)), | |
| 34 | + HttpRouter.get("/v1/mcp", fromWebHandler(handleMcp)), | |
| 35 | + HttpRouter.post("/v1/mcp", fromWebHandler(handleMcp)), | |
| 36 | + HttpRouter.del("/v1/mcp", fromWebHandler(handleMcp)), | |
| 37 | + HttpRouter.post("/runtime/tool-call", handleToolCallHttp), | |
| 38 | + HttpRouter.post("/v1/runtime/tool-call", handleToolCallHttp), | |
| 39 | + HttpServer.serve(), | |
| 40 | + HttpServer.withLogAddress, | |
| 41 | + Layer.provide(BunHttpServer.layer({ port })), | |
| 42 | + ); | |
| 38 | 43 | |
| 39 | − return server; | |
| 44 | + return yield* Layer.launch(httpLive); | |
| 40 | 45 | }); |
| @@ -10,10 +10,7 @@ import { · 9 unmodified lines | ||
| 10 | 10 | PmRunExecutorLive, |
| 11 | 11 | PmToolProviderRegistryLive, |
| 12 | 12 | } from "./run-executor"; |
| 13 | −import { | |
| 14 | − PmToolCallHandlerLive, | |
| 15 | − PmToolCallHttpHandlerLive, | |
| 16 | −} from "./tool-call-handler"; | |
| 13 | +import { PmToolCallHandlerLive } from "./tool-call-handler"; | |
| 17 | 14 | |
| 18 | 15 | const runtimeAdapter = makeLocalInProcessRuntimeAdapter(); |
| 19 | 16 | const PmMcpDependenciesLive = Layer.merge( |
| @@ -21,14 +18,10 @@ const PmMcpDependenciesLive = Layer.merge( · 1 unmodified line | ||
| 21 | 18 | PmToolProviderRegistryLive, |
| 22 | 19 | ); |
| 23 | 20 | |
| 24 | −const PmToolCallDependenciesLive = PmToolCallHttpHandlerLive.pipe( | |
| 25 | − Layer.provide(PmToolCallHandlerLive), | |
| 26 | −); | |
| 27 | − | |
| 28 | 21 | const PmAppLive = Layer.mergeAll( |
| 29 | 22 | PmConfigLive, |
| 30 | 23 | PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)), |
| 31 | − PmToolCallDependenciesLive, | |
| 24 | + PmToolCallHandlerLive, | |
| 32 | 25 | ); |
| 33 | 26 | |
| 34 | 27 | const program = startPmHttpServer().pipe(Effect.provide(PmAppLive)); |
| @@ -1,12 +1,10 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | import * as Effect from "effect/Effect"; |
| 3 | −import * as Layer from "effect/Layer"; | |
| 4 | 3 | |
| 5 | 4 | import { |
| 5 | + handleToolCallBody, | |
| 6 | 6 | PmToolCallHandler, |
| 7 | 7 | PmToolCallHandlerLive, |
| 8 | − PmToolCallHttpHandler, | |
| 9 | − PmToolCallHttpHandlerLive, | |
| 10 | 8 | } from "./tool-call-handler"; |
| 11 | 9 | |
| 12 | 10 | describe("PM runtime tool-call handling", () => { |
| @@ -29,40 +27,20 @@ describe("PM runtime tool-call handling", () => { · 16 unmodified lines | ||
| 29 | 27 | }).pipe(Effect.provide(PmToolCallHandlerLive)), |
| 30 | 28 | ); |
| 31 | 29 | |
| 32 | − it.effect("decodes callback request payload and returns JSON result", () => | |
| 30 | + it.effect("decodes callback request payload and returns failed callback result", () => | |
| 33 | 31 | Effect.gen(function* () { |
| 34 | − const httpHandler = yield* PmToolCallHttpHandler; | |
| 35 | − | |
| 36 | − const response = yield* Effect.tryPromise(() => | |
| 37 | − httpHandler.handleToolCallHttp( | |
| 38 | − new Request("http://127.0.0.1/runtime/tool-call", { | |
| 39 | − method: "POST", | |
| 40 | − headers: { "content-type": "application/json" }, | |
| 41 | − body: JSON.stringify({ | |
| 42 | − runId: "run_2", | |
| 43 | − callId: "call_2", | |
| 44 | − toolPath: "tools.example.weather", | |
| 45 | − input: { city: "London" }, | |
| 46 | − }), | |
| 47 | − }), | |
| 48 | − ), | |
| 49 | − ); | |
| 50 | − | |
| 51 | − expect(response.status).toBe(200); | |
| 52 | − | |
| 53 | − const payload = (yield* Effect.tryPromise(() => response.json())) as { | |
| 54 | − ok: boolean; | |
| 55 | − kind?: string; | |
| 56 | − error?: string; | |
| 57 | − }; | |
| 32 | + const result = yield* handleToolCallBody({ | |
| 33 | + runId: "run_2", | |
| 34 | + callId: "call_2", | |
| 35 | + toolPath: "tools.example.weather", | |
| 36 | + input: { city: "London" }, | |
| 37 | + }); | |
| 58 | 38 | |
| 59 | − expect(payload.ok).toBe(false); | |
| 60 | − expect(payload.kind).toBe("failed"); | |
| 61 | − expect(payload.error).toContain("tools.example.weather"); | |
| 62 | − }).pipe( | |
| 63 | − Effect.provide( | |
| 64 | − PmToolCallHttpHandlerLive.pipe(Layer.provide(PmToolCallHandlerLive)), | |
| 65 | − ), | |
| 66 | − ), | |
| 39 | + expect(result.ok).toBe(false); | |
| 40 | + if (!result.ok) { | |
| 41 | + expect(result.kind).toBe("failed"); | |
| 42 | + expect(result.error).toContain("tools.example.weather"); | |
| 43 | + } | |
| 44 | + }).pipe(Effect.provide(PmToolCallHandlerLive)), | |
| 67 | 45 | ); |
| 68 | 46 | }); |
| @@ -2,11 +2,11 @@ import type { · 1 unmodified line | ||
| 2 | 2 | RuntimeToolCallRequest, |
| 3 | 3 | RuntimeToolCallResult, |
| 4 | 4 | } from "@executor-v2/sdk"; |
| 5 | +import { HttpServerRequest, HttpServerResponse } from "@effect/platform"; | |
| 5 | 6 | import * as Context from "effect/Context"; |
| 6 | 7 | import * as Data from "effect/Data"; |
| 7 | 8 | import * as Effect from "effect/Effect"; |
| 8 | 9 | import * as Layer from "effect/Layer"; |
| 9 | −import * as Runtime from "effect/Runtime"; | |
| 10 | 10 | import * as Schema from "effect/Schema"; |
| 11 | 11 | |
| 12 | 12 | export type PmToolCallHandlerService = { |
| @@ -77,48 +77,31 @@ export const PmToolCallHandlerLive = Layer.succeed( · 64 unmodified lines | ||
| 77 | 77 | }), |
| 78 | 78 | ); |
| 79 | 79 | |
| 80 | −export type PmToolCallHttpHandlerService = { | |
| 81 | − handleToolCallHttp: (request: Request) => Promise<Response>; | |
| 82 | −}; | |
| 83 | − | |
| 84 | −export class PmToolCallHttpHandler extends Context.Tag("@executor-v2/app-pm/PmToolCallHttpHandler")< | |
| 85 | − PmToolCallHttpHandler, | |
| 86 | − PmToolCallHttpHandlerService | |
| 87 | −>() {} | |
| 80 | +export const handleToolCallBody = Effect.fn( | |
| 81 | + "@executor-v2/app-pm/tool-call.handle-body", | |
| 82 | +)(function* (body: unknown) { | |
| 83 | + const handler = yield* PmToolCallHandler; | |
| 84 | + const input = yield* decodeRuntimeToolCallRequest(body).pipe( | |
| 85 | + Effect.mapError(decodeRequestBodyError), | |
| 86 | + ); | |
| 88 | 87 | |
| 89 | −export const PmToolCallHttpHandlerLive = Layer.effect( | |
| 90 | − PmToolCallHttpHandler, | |
| 91 | − Effect.gen(function* () { | |
| 92 | − const handler = yield* PmToolCallHandler; | |
| 93 | − const runtime = yield* Effect.runtime<never>(); | |
| 94 | − const runPromise = Runtime.runPromise(runtime); | |
| 95 | − | |
| 96 | − const handleToolCallHttp = (request: Request): Promise<Response> => | |
| 97 | − runPromise( | |
| 98 | − Effect.tryPromise({ | |
| 99 | − try: () => request.json(), | |
| 100 | − catch: decodeRequestBodyError, | |
| 101 | − }).pipe( | |
| 102 | − Effect.flatMap((body) => decodeRuntimeToolCallRequest(body)), | |
| 103 | − Effect.flatMap((input) => handler.handleToolCall(input)), | |
| 104 | − Effect.map((result) => Response.json(result, { status: 200 })), | |
| 105 | − Effect.catchAll((error) => | |
| 106 | − Effect.succeed( | |
| 107 | − Response.json( | |
| 108 | − { | |
| 109 | − ok: false, | |
| 110 | − kind: "failed", | |
| 111 | − error: errorToText(error), | |
| 112 | − } satisfies RuntimeToolCallResult, | |
| 113 | − { status: 400 }, | |
| 114 | − ), | |
| 115 | − ), | |
| 116 | − ), | |
| 117 | − ), | |
| 118 | − ); | |
| 88 | + return yield* handler.handleToolCall(input); | |
| 89 | +}); | |
| 119 | 90 | |
| 120 | − return PmToolCallHttpHandler.of({ | |
| 121 | − handleToolCallHttp, | |
| 122 | − }); | |
| 123 | − }), | |
| 91 | +export const handleToolCallHttp = Effect.gen(function* () { | |
| 92 | + const body = yield* HttpServerRequest.schemaBodyJson(Schema.Unknown); | |
| 93 | + const result = yield* handleToolCallBody(body); | |
| 94 | + | |
| 95 | + return yield* HttpServerResponse.json(result, { status: 200 }); | |
| 96 | +}).pipe( | |
| 97 | + Effect.catchAll((error) => | |
| 98 | + HttpServerResponse.json( | |
| 99 | + { | |
| 100 | + ok: false, | |
| 101 | + kind: "failed", | |
| 102 | + error: errorToText(error), | |
| 103 | + } satisfies RuntimeToolCallResult, | |
| 104 | + { status: 400 }, | |
| 105 | + ), | |
| 106 | + ), | |
| 124 | 107 | ); |
| @@ -37,6 +37,8 @@ · 36 unmodified lines | ||
| 37 | 37 | "apps/pm": { |
| 38 | 38 | "name": "@executor-v2/app-pm", |
| 39 | 39 | "dependencies": { |
| 40 | + "@effect/platform": "catalog:", | |
| 41 | + "@effect/platform-bun": "^0.86.0", | |
| 40 | 42 | "@executor-v2/engine": "workspace:*", |
| 41 | 43 | "@executor-v2/mcp-gateway": "workspace:*", |
| 42 | 44 | "@executor-v2/sdk": "workspace:*", |