History

use effect in pm

Rhys Sullivan committed 8 months ago

Commit 31348e61 parent c4376dc7 files · +81 −118Browse files

Unified · Split

7 files
  1. v2/apps/pm/README.md +1 −1
  2. v2/apps/pm/package.json +2 −0
  3. v2/apps/pm/src/http-server.ts +34 −29
  4. v2/apps/pm/src/main.ts +2 −9
  5. v2/apps/pm/src/tool-call-handler.test.ts +14 −36
  6. v2/apps/pm/src/tool-call-handler.ts +26 −43
  7. v2/bun.lock +2 −0
v2/apps/pm/README.md+1−1
@@ -13,5 +13,5 @@ App wiring is now split by responsibility: · 12 unmodified lines
1313 - `src/config.ts`: Effect config service (`PORT`)
1414 - `src/run-executor.ts`: run execution service for an injected runtime adapter
1515 - `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
1717 - `src/main.ts`: Layer composition + process entrypoint
v2/apps/pm/package.json+2−0
@@ -9,6 +9,8 @@ · 8 unmodified lines
99 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
1010 },
1111 "dependencies": {
12+ "@effect/platform": "catalog:",
13+ "@effect/platform-bun": "^0.86.0",
1214 "@executor-v2/engine": "workspace:*",
1315 "@executor-v2/mcp-gateway": "workspace:*",
1416 "@executor-v2/sdk": "workspace:*",
v2/apps/pm/src/http-server.ts+34−29
@@ -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";
18 import * as Effect from "effect/Effect";
9+import * as Layer from "effect/Layer";
210
311 import { PmConfig } from "./config";
412 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+ });
624
725 export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () {
826 const { port } = yield* PmConfig;
927 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− });
3628
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+ );
3843
39− return server;
44+ return yield* Layer.launch(httpLive);
4045 });
v2/apps/pm/src/main.ts+2−9
@@ -10,10 +10,7 @@ import { · 9 unmodified lines
1010 PmRunExecutorLive,
1111 PmToolProviderRegistryLive,
1212 } from "./run-executor";
13−import {
14− PmToolCallHandlerLive,
15− PmToolCallHttpHandlerLive,
16−} from "./tool-call-handler";
13+import { PmToolCallHandlerLive } from "./tool-call-handler";
1714
1815 const runtimeAdapter = makeLocalInProcessRuntimeAdapter();
1916 const PmMcpDependenciesLive = Layer.merge(
@@ -21,14 +18,10 @@ const PmMcpDependenciesLive = Layer.merge( · 1 unmodified line
2118 PmToolProviderRegistryLive,
2219 );
2320
24−const PmToolCallDependenciesLive = PmToolCallHttpHandlerLive.pipe(
25− Layer.provide(PmToolCallHandlerLive),
26−);
27−
2821 const PmAppLive = Layer.mergeAll(
2922 PmConfigLive,
3023 PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)),
31− PmToolCallDependenciesLive,
24+ PmToolCallHandlerLive,
3225 );
3326
3427 const program = startPmHttpServer().pipe(Effect.provide(PmAppLive));
v2/apps/pm/src/tool-call-handler.test.ts+14−36
@@ -1,12 +1,10 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
3−import * as Layer from "effect/Layer";
43
54 import {
5+ handleToolCallBody,
66 PmToolCallHandler,
77 PmToolCallHandlerLive,
8− PmToolCallHttpHandler,
9− PmToolCallHttpHandlerLive,
108 } from "./tool-call-handler";
119
1210 describe("PM runtime tool-call handling", () => {
@@ -29,40 +27,20 @@ describe("PM runtime tool-call handling", () => { · 16 unmodified lines
2927 }).pipe(Effect.provide(PmToolCallHandlerLive)),
3028 );
3129
32− it.effect("decodes callback request payload and returns JSON result", () =>
30+ it.effect("decodes callback request payload and returns failed callback result", () =>
3331 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+ });
5838
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)),
6745 );
6846 });
v2/apps/pm/src/tool-call-handler.ts+26−43
@@ -2,11 +2,11 @@ import type { · 1 unmodified line
22 RuntimeToolCallRequest,
33 RuntimeToolCallResult,
44 } from "@executor-v2/sdk";
5+import { HttpServerRequest, HttpServerResponse } from "@effect/platform";
56 import * as Context from "effect/Context";
67 import * as Data from "effect/Data";
78 import * as Effect from "effect/Effect";
89 import * as Layer from "effect/Layer";
9−import * as Runtime from "effect/Runtime";
1010 import * as Schema from "effect/Schema";
1111
1212 export type PmToolCallHandlerService = {
@@ -77,48 +77,31 @@ export const PmToolCallHandlerLive = Layer.succeed( · 64 unmodified lines
7777 }),
7878 );
7979
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+ );
8887
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+});
11990
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+ ),
124107 );
v2/bun.lock+2−0
@@ -37,6 +37,8 @@ · 36 unmodified lines
3737 "apps/pm": {
3838 "name": "@executor-v2/app-pm",
3939 "dependencies": {
40+ "@effect/platform": "catalog:",
41+ "@effect/platform-bun": "^0.86.0",
4042 "@executor-v2/engine": "workspace:*",
4143 "@executor-v2/mcp-gateway": "workspace:*",
4244 "@executor-v2/sdk": "workspace:*",