Unified · Split
| @@ -13,5 +13,5 @@ App wiring is now split by responsibility: · 12 unmodified lines | |||
| 13 | - `src/config.ts`: Effect config service (`PORT`) | 13 | - `src/config.ts`: Effect config service (`PORT`) |
| 14 | - `src/run-executor.ts`: run execution service for an injected runtime adapter | 14 | - `src/run-executor.ts`: run execution service for an injected runtime adapter |
| 15 | - `src/mcp-handler.ts`: MCP transport wiring to run client | 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 | - `src/main.ts`: Layer composition + process entrypoint | 17 | - `src/main.ts`: Layer composition + process entrypoint |
| @@ -9,6 +9,8 @@ · 8 unmodified lines | |||
| 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" | 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 10 | }, | 10 | }, |
| 11 | "dependencies": { | 11 | "dependencies": { |
| 12 | "@effect/platform": "catalog:", | ||
| 13 | "@effect/platform-bun": "^0.86.0", | ||
| 12 | "@executor-v2/engine": "workspace:*", | 14 | "@executor-v2/engine": "workspace:*", |
| 13 | "@executor-v2/mcp-gateway": "workspace:*", | 15 | "@executor-v2/mcp-gateway": "workspace:*", |
| 14 | "@executor-v2/sdk": "workspace:*", | 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 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 9 | import * as Layer from "effect/Layer"; | ||
| 2 | 10 | ||
| 3 | import { PmConfig } from "./config"; | 11 | import { PmConfig } from "./config"; |
| 4 | import { PmMcpHandler } from "./mcp-handler"; | 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 | export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () { | 25 | export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () { |
| 8 | const { port } = yield* PmConfig; | 26 | const { port } = yield* PmConfig; |
| 9 | const { handleMcp } = yield* PmMcpHandler; | 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 | PmRunExecutorLive, | 10 | PmRunExecutorLive, |
| 11 | PmToolProviderRegistryLive, | 11 | PmToolProviderRegistryLive, |
| 12 | } from "./run-executor"; | 12 | } from "./run-executor"; |
| 13 | import { | 13 | import { PmToolCallHandlerLive } from "./tool-call-handler"; |
| 14 | PmToolCallHandlerLive, | ||
| 15 | PmToolCallHttpHandlerLive, | ||
| 16 | } from "./tool-call-handler"; | ||
| 17 | 14 | ||
| 18 | const runtimeAdapter = makeLocalInProcessRuntimeAdapter(); | 15 | const runtimeAdapter = makeLocalInProcessRuntimeAdapter(); |
| 19 | const PmMcpDependenciesLive = Layer.merge( | 16 | const PmMcpDependenciesLive = Layer.merge( |
| @@ -21,14 +18,10 @@ const PmMcpDependenciesLive = Layer.merge( · 1 unmodified line | |||
| 21 | PmToolProviderRegistryLive, | 18 | PmToolProviderRegistryLive, |
| 22 | ); | 19 | ); |
| 23 | 20 | ||
| 24 | const PmToolCallDependenciesLive = PmToolCallHttpHandlerLive.pipe( | ||
| 25 | Layer.provide(PmToolCallHandlerLive), | ||
| 26 | ); | ||
| 27 | |||
| 28 | const PmAppLive = Layer.mergeAll( | 21 | const PmAppLive = Layer.mergeAll( |
| 29 | PmConfigLive, | 22 | PmConfigLive, |
| 30 | PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)), | 23 | PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)), |
| 31 | PmToolCallDependenciesLive, | 24 | PmToolCallHandlerLive, |
| 32 | ); | 25 | ); |
| 33 | 26 | ||
| 34 | const program = startPmHttpServer().pipe(Effect.provide(PmAppLive)); | 27 | const program = startPmHttpServer().pipe(Effect.provide(PmAppLive)); |
| @@ -1,12 +1,10 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | ||
| 4 | 3 | ||
| 5 | import { | 4 | import { |
| 5 | handleToolCallBody, | ||
| 6 | PmToolCallHandler, | 6 | PmToolCallHandler, |
| 7 | PmToolCallHandlerLive, | 7 | PmToolCallHandlerLive, |
| 8 | PmToolCallHttpHandler, | ||
| 9 | PmToolCallHttpHandlerLive, | ||
| 10 | } from "./tool-call-handler"; | 8 | } from "./tool-call-handler"; |
| 11 | 9 | ||
| 12 | describe("PM runtime tool-call handling", () => { | 10 | describe("PM runtime tool-call handling", () => { |
| @@ -29,40 +27,20 @@ describe("PM runtime tool-call handling", () => { · 16 unmodified lines | |||
| 29 | }).pipe(Effect.provide(PmToolCallHandlerLive)), | 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 | Effect.gen(function* () { | 31 | Effect.gen(function* () { |
| 34 | const httpHandler = yield* PmToolCallHttpHandler; | 32 | const result = yield* handleToolCallBody({ |
| 35 | 33 | runId: "run_2", | |
| 36 | const response = yield* Effect.tryPromise(() => | 34 | callId: "call_2", |
| 37 | httpHandler.handleToolCallHttp( | 35 | toolPath: "tools.example.weather", |
| 38 | new Request("http://127.0.0.1/runtime/tool-call", { | 36 | input: { city: "London" }, |
| 39 | method: "POST", | 37 | }); |
| 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 | }; | ||
| 58 | 38 | ||
| 59 | expect(payload.ok).toBe(false); | 39 | expect(result.ok).toBe(false); |
| 60 | expect(payload.kind).toBe("failed"); | 40 | if (!result.ok) { |
| 61 | expect(payload.error).toContain("tools.example.weather"); | 41 | expect(result.kind).toBe("failed"); |
| 62 | }).pipe( | 42 | expect(result.error).toContain("tools.example.weather"); |
| 63 | Effect.provide( | 43 | } |
| 64 | PmToolCallHttpHandlerLive.pipe(Layer.provide(PmToolCallHandlerLive)), | 44 | }).pipe(Effect.provide(PmToolCallHandlerLive)), |
| 65 | ), | ||
| 66 | ), | ||
| 67 | ); | 45 | ); |
| 68 | }); | 46 | }); |
| @@ -2,11 +2,11 @@ import type { · 1 unmodified line | |||
| 2 | RuntimeToolCallRequest, | 2 | RuntimeToolCallRequest, |
| 3 | RuntimeToolCallResult, | 3 | RuntimeToolCallResult, |
| 4 | } from "@executor-v2/sdk"; | 4 | } from "@executor-v2/sdk"; |
| 5 | import { HttpServerRequest, HttpServerResponse } from "@effect/platform"; | ||
| 5 | import * as Context from "effect/Context"; | 6 | import * as Context from "effect/Context"; |
| 6 | import * as Data from "effect/Data"; | 7 | import * as Data from "effect/Data"; |
| 7 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 8 | import * as Layer from "effect/Layer"; | 9 | import * as Layer from "effect/Layer"; |
| 9 | import * as Runtime from "effect/Runtime"; | ||
| 10 | import * as Schema from "effect/Schema"; | 10 | import * as Schema from "effect/Schema"; |
| 11 | 11 | ||
| 12 | export type PmToolCallHandlerService = { | 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 = { | 80 | export const handleToolCallBody = Effect.fn( |
| 81 | handleToolCallHttp: (request: Request) => Promise<Response>; | 81 | "@executor-v2/app-pm/tool-call.handle-body", |
| 82 | }; | 82 | )(function* (body: unknown) { |
| 83 | 83 | const handler = yield* PmToolCallHandler; | |
| 84 | export class PmToolCallHttpHandler extends Context.Tag("@executor-v2/app-pm/PmToolCallHttpHandler")< | 84 | const input = yield* decodeRuntimeToolCallRequest(body).pipe( |
| 85 | PmToolCallHttpHandler, | 85 | Effect.mapError(decodeRequestBodyError), |
| 86 | PmToolCallHttpHandlerService | 86 | ); |
| 87 | >() {} | ||
| 88 | 87 | ||
| 89 | export const PmToolCallHttpHandlerLive = Layer.effect( | 88 | return yield* handler.handleToolCall(input); |
| 90 | PmToolCallHttpHandler, | 89 | }); |
| 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 | ); | ||
| 119 | 90 | ||
| 120 | return PmToolCallHttpHandler.of({ | 91 | export const handleToolCallHttp = Effect.gen(function* () { |
| 121 | handleToolCallHttp, | 92 | const body = yield* HttpServerRequest.schemaBodyJson(Schema.Unknown); |
| 122 | }); | 93 | const result = yield* handleToolCallBody(body); |
| 123 | }), | 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 | "apps/pm": { | 37 | "apps/pm": { |
| 38 | "name": "@executor-v2/app-pm", | 38 | "name": "@executor-v2/app-pm", |
| 39 | "dependencies": { | 39 | "dependencies": { |
| 40 | "@effect/platform": "catalog:", | ||
| 41 | "@effect/platform-bun": "^0.86.0", | ||
| 40 | "@executor-v2/engine": "workspace:*", | 42 | "@executor-v2/engine": "workspace:*", |
| 41 | "@executor-v2/mcp-gateway": "workspace:*", | 43 | "@executor-v2/mcp-gateway": "workspace:*", |
| 42 | "@executor-v2/sdk": "workspace:*", | 44 | "@executor-v2/sdk": "workspace:*", |