@llm4ts/runner 2.2.1 → 2.3.0

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/dist/Doctor.d.ts CHANGED
@@ -22,5 +22,18 @@ export interface PrerequisiteReport {
22
22
  * environment is reported as a caveat rather than a failure.
23
23
  */
24
24
  export declare const geminiPrerequisites: (environment: Readonly<Record<string, string | undefined>>) => PrerequisiteReport;
25
- export declare const makeDoctorProgram: (registry?: ConnectorRegistryShape, environment?: Readonly<Record<string, string | undefined>>) => Effect.Effect<string>;
25
+ export declare const defaultGeminiBridgePort = "8731";
26
+ /**
27
+ * `~/.pi/agent/models.json` (ADR 0016). Read for reporting only — this file
28
+ * belongs to `pi`, outside llm4ts's package graph, and llm4ts never writes
29
+ * it. Missing is not an error here: the file simply doesn't exist yet.
30
+ */
31
+ export declare const defaultReadPiModelsJson: () => string | undefined;
32
+ /**
33
+ * Whether pi's custom-provider config points at this machine's Gemini ACP
34
+ * bridge — detect-and-report only, never auto-fixed (ADR 0016). `undefined`
35
+ * means the check doesn't apply: nothing asked for the bridge.
36
+ */
37
+ export declare const geminiBridgePrerequisites: (environment: Readonly<Record<string, string | undefined>>, modelsJson: string | undefined) => PrerequisiteReport | undefined;
38
+ export declare const makeDoctorProgram: (registry?: ConnectorRegistryShape, environment?: Readonly<Record<string, string | undefined>>, readPiModelsJson?: () => string | undefined) => Effect.Effect<string>;
26
39
  //# sourceMappingURL=Doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Doctor.d.ts","sourceRoot":"","sources":["../src/Doctor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AAuB5E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,GAC9B,aAAa,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,KACxD,kBA0BF,CAAA;AAwBD,eAAO,MAAM,iBAAiB,GAC5B,WAAU,sBAA8D,EACxE,cAAa,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,KACtE,MAAM,CAAC,MAAM,CAAC,MAAM,CAwCnB,CAAA"}
1
+ {"version":3,"file":"Doctor.d.ts","sourceRoot":"","sources":["../src/Doctor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AAuB5E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,GAC9B,aAAa,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,KACxD,kBA0BF,CAAA;AAwBD,eAAO,MAAM,uBAAuB,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QAAO,MAAM,GAAG,SAMnD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GACpC,aAAa,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EACzD,YAAY,MAAM,GAAG,SAAS,KAC7B,kBAAkB,GAAG,SAyBvB,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,WAAU,sBAA8D,EACxE,cAAa,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,EACvE,mBAAkB,MAAM,MAAM,GAAG,SAAmC,KACnE,MAAM,CAAC,MAAM,CAAC,MAAM,CAiDnB,CAAA"}
package/dist/Doctor.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import * as Effect from "effect/Effect";
2
5
  import { ConnectorIds } from "@llm4ts/core/Models";
3
6
  import { nodeFlowRunnerDependencies } from "./FlowRunner.js";
@@ -76,7 +79,51 @@ const wrap = (text, width, indent) => {
76
79
  return out;
77
80
  };
78
81
  const selectedCoder = (environment) => environment.LLM4TS_CODER ?? "claude (default)";
79
- export const makeDoctorProgram = (registry = nodeFlowRunnerDependencies().registry, environment = process.env) => Effect.map(registry.healthCheckAll, (statuses) => {
82
+ export const defaultGeminiBridgePort = "8731";
83
+ /**
84
+ * `~/.pi/agent/models.json` (ADR 0016). Read for reporting only — this file
85
+ * belongs to `pi`, outside llm4ts's package graph, and llm4ts never writes
86
+ * it. Missing is not an error here: the file simply doesn't exist yet.
87
+ */
88
+ export const defaultReadPiModelsJson = () => {
89
+ try {
90
+ return readFileSync(join(homedir(), ".pi", "agent", "models.json"), "utf8");
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ };
96
+ /**
97
+ * Whether pi's custom-provider config points at this machine's Gemini ACP
98
+ * bridge — detect-and-report only, never auto-fixed (ADR 0016). `undefined`
99
+ * means the check doesn't apply: nothing asked for the bridge.
100
+ */
101
+ export const geminiBridgePrerequisites = (environment, modelsJson) => {
102
+ if (!truthy(environment.LLM4TS_GEMINI_BRIDGE)) {
103
+ return undefined;
104
+ }
105
+ const port = isSet(environment.LLM4TS_GEMINI_BRIDGE_PORT)
106
+ ? environment.LLM4TS_GEMINI_BRIDGE_PORT.trim()
107
+ : defaultGeminiBridgePort;
108
+ const needle = `127.0.0.1:${port}`;
109
+ const hint = `add a custom provider to ~/.pi/agent/models.json with baseUrl ` +
110
+ `"http://${needle}" so pi draws inference from the gemini-cli bridge instead ` +
111
+ `of a model API key (ADR 0016) — llm4ts never writes this file for you.`;
112
+ if (modelsJson === undefined) {
113
+ return { satisfied: false, summary: "~/.pi/agent/models.json not found", hint };
114
+ }
115
+ return modelsJson.includes(needle)
116
+ ? {
117
+ satisfied: true,
118
+ summary: `a provider in ~/.pi/agent/models.json points at ${needle}`
119
+ }
120
+ : {
121
+ satisfied: false,
122
+ summary: `no provider in ~/.pi/agent/models.json points at ${needle}`,
123
+ hint
124
+ };
125
+ };
126
+ export const makeDoctorProgram = (registry = nodeFlowRunnerDependencies().registry, environment = process.env, readPiModelsJson = defaultReadPiModelsJson) => Effect.map(registry.healthCheckAll, (statuses) => {
80
127
  const lines = [];
81
128
  lines.push("llm4ts doctor");
82
129
  lines.push("");
@@ -97,13 +144,22 @@ export const makeDoctorProgram = (registry = nodeFlowRunnerDependencies().regist
97
144
  const coder = selectedCoder(environment);
98
145
  const geminiRelevant = coder.startsWith("gemini") ||
99
146
  Array.from(statuses.keys()).some((id) => id.value === ConnectorIds.GeminiCli.value || id.value === ConnectorIds.GeminiApi.value);
100
- if (geminiRelevant) {
101
- const gemini = geminiPrerequisites(environment);
147
+ const bridge = geminiBridgePrerequisites(environment, readPiModelsJson());
148
+ if (geminiRelevant || bridge !== undefined) {
102
149
  lines.push("");
103
150
  lines.push("prerequisites:");
104
- lines.push(` ${gemini.satisfied ? "✔" : "?"} gemini: ${gemini.summary}`);
105
- if (gemini.hint !== undefined) {
106
- lines.push(...wrap(gemini.hint, 78, " "));
151
+ if (geminiRelevant) {
152
+ const gemini = geminiPrerequisites(environment);
153
+ lines.push(` ${gemini.satisfied ? "✔" : "?"} gemini: ${gemini.summary}`);
154
+ if (gemini.hint !== undefined) {
155
+ lines.push(...wrap(gemini.hint, 78, " "));
156
+ }
157
+ }
158
+ if (bridge !== undefined) {
159
+ lines.push(` ${bridge.satisfied ? "✔" : "?"} pi-gemini-bridge: ${bridge.summary}`);
160
+ if (bridge.hint !== undefined) {
161
+ lines.push(...wrap(bridge.hint, 78, " "));
162
+ }
107
163
  }
108
164
  }
109
165
  lines.push("");
@@ -1 +1 @@
1
- {"version":3,"file":"Doctor.js","sourceRoot":"","sources":["../src/Doctor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Doctor.js","sourceRoot":"","sources":["../src/Doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import * as Deferred from "effect/Deferred";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Option from "effect/Option";
4
+ import * as Ref from "effect/Ref";
5
+ import type * as Scope from "effect/Scope";
6
+ import { InvalidRequestError, ProviderError, type LlmError } from "@llm4ts/core/Errors";
7
+ import type { ProcessExecutorShape } from "@llm4ts/core/ProcessExecutor";
8
+ import { type GeminiAcpSessionShape } from "@llm4ts/core/providers/GeminiAcpSession";
9
+ import { type JsonRecord, type JsonValue } from "@llm4ts/core/providers/CliSupport";
10
+ import { type McpTool } from "@llm4ts/flow/McpServer";
11
+ /**
12
+ * Speaks the Anthropic Messages API on `/v1/messages` (what `pi`'s
13
+ * `~/.pi/agent/models.json` custom-provider entry points at) and MCP over
14
+ * HTTP on `/mcp` (what an ACP `session/new` registers with gemini). See
15
+ * ADR 0016 for why: pi's tool loop — not gemini's — executes every tool
16
+ * call, so a tool call gemini makes through the registered MCP tools pauses
17
+ * here until pi's *next* `/v1/messages` request supplies the result.
18
+ *
19
+ * One active ACP session per bridge instance (ADR 0016's simplifying
20
+ * assumption): correlation needs no session-id routing, just one pending
21
+ * tool call and one "what happens next" slot at a time.
22
+ */
23
+ interface AnthropicToolDef {
24
+ readonly name: string;
25
+ readonly description: string;
26
+ readonly inputSchema: JsonRecord;
27
+ }
28
+ interface ToolResultPayload {
29
+ readonly content: string;
30
+ readonly isError: boolean;
31
+ }
32
+ interface PendingToolCall {
33
+ readonly id: string;
34
+ readonly name: string;
35
+ readonly resultDeferred: Deferred.Deferred<ToolResultPayload, LlmError>;
36
+ }
37
+ export type TurnOutcome = {
38
+ readonly _tag: "Text";
39
+ readonly text: string;
40
+ } | {
41
+ readonly _tag: "ToolUse";
42
+ readonly id: string;
43
+ readonly name: string;
44
+ readonly input: JsonValue;
45
+ };
46
+ export interface BridgeState {
47
+ readonly session: GeminiAcpSessionShape;
48
+ readonly cwd: string;
49
+ readonly mcpServerUrl: string;
50
+ readonly sessionId: Ref.Ref<Option.Option<string>>;
51
+ readonly tools: Ref.Ref<ReadonlyArray<AnthropicToolDef>>;
52
+ readonly pendingToolCall: Ref.Ref<Option.Option<PendingToolCall>>;
53
+ readonly turnOutcome: Ref.Ref<Option.Option<Deferred.Deferred<TurnOutcome, LlmError>>>;
54
+ readonly nextToolCallId: Ref.Ref<number>;
55
+ readonly forkBackground: <A>(effect: Effect.Effect<A, LlmError>) => Effect.Effect<void>;
56
+ }
57
+ export declare const makeBridgeState: (session: GeminiAcpSessionShape, cwd: string, mcpServerUrl: string, forkBackground: <A>(effect: Effect.Effect<A, LlmError>) => Effect.Effect<void>) => Effect.Effect<BridgeState, never, never>;
58
+ export declare const anthropicToolToMcpTool: (state: BridgeState, def: AnthropicToolDef) => McpTool;
59
+ export declare const handleMessagesRequest: (state: BridgeState, body: import("effect/Schema").Json) => Effect.Effect<Readonly<Record<string, import("effect/Schema").Json>>, InvalidRequestError | ProviderError | import("@llm4ts/core/Errors").UsageLimitError | import("@llm4ts/core/Errors").AuthenticationError | import("@llm4ts/core/Errors").ParseError | import("@llm4ts/core/Errors").ToolError | import("@llm4ts/core/Errors").ConfigError | import("@llm4ts/core/Errors").RateLimitError | import("@llm4ts/core/Errors").TimeoutError | import("@llm4ts/core/Errors").TurnLimitError, never>;
60
+ export interface GeminiAcpBridgeConfig {
61
+ readonly port: number;
62
+ readonly cwd: string;
63
+ readonly model?: string;
64
+ readonly executor: ProcessExecutorShape;
65
+ readonly envVars?: Readonly<Record<string, string>>;
66
+ }
67
+ export interface RunningGeminiAcpBridge {
68
+ readonly port: number;
69
+ readonly baseUrl: string;
70
+ }
71
+ export declare const runGeminiAcpBridge: (config: GeminiAcpBridgeConfig) => Effect.Effect<RunningGeminiAcpBridge, InvalidRequestError | ProviderError | import("@llm4ts/core/Errors").UsageLimitError | import("@llm4ts/core/Errors").AuthenticationError | import("@llm4ts/core/Errors").ParseError | import("@llm4ts/core/Errors").ToolError | import("@llm4ts/core/Errors").ConfigError | import("@llm4ts/core/Errors").RateLimitError | import("@llm4ts/core/Errors").TimeoutError | import("@llm4ts/core/Errors").TurnLimitError, Scope.Scope>;
72
+ export {};
73
+ //# sourceMappingURL=NodeGeminiAcpBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NodeGeminiAcpBridge.d.ts","sourceRoot":"","sources":["../src/NodeGeminiAcpBridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,231 @@
1
+ import { createServer } from "node:http";
2
+ import * as Deferred from "effect/Deferred";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Option from "effect/Option";
5
+ import * as Ref from "effect/Ref";
6
+ import { InvalidRequestError, ProviderError } from "@llm4ts/core/Errors";
7
+ import { geminiAcpArgv, openGeminiAcpSession } from "@llm4ts/core/providers/GeminiAcpSession";
8
+ import { isJsonRecord, jsonArray, jsonBooleanField, jsonField, jsonStringField } from "@llm4ts/core/providers/CliSupport";
9
+ import { FlowLlmError } from "@llm4ts/flow/FlowError";
10
+ import { handleMcpRequest } from "@llm4ts/flow/McpServer";
11
+ export const makeBridgeState = Effect.fn("@llm4ts/runner/NodeGeminiAcpBridge.makeBridgeState")(function* (session, cwd, mcpServerUrl, forkBackground) {
12
+ return {
13
+ session,
14
+ cwd,
15
+ mcpServerUrl,
16
+ sessionId: yield* Ref.make(Option.none()),
17
+ tools: yield* Ref.make([]),
18
+ pendingToolCall: yield* Ref.make(Option.none()),
19
+ turnOutcome: yield* Ref.make(Option.none()),
20
+ nextToolCallId: yield* Ref.make(0),
21
+ forkBackground
22
+ };
23
+ });
24
+ const resolveTurnOutcomeSuccess = (state, outcome) => Effect.gen(function* () {
25
+ const current = yield* Ref.get(state.turnOutcome);
26
+ if (Option.isSome(current)) {
27
+ yield* Deferred.succeed(current.value, outcome);
28
+ }
29
+ });
30
+ const resolveTurnOutcomeFailure = (state, error) => Effect.gen(function* () {
31
+ const current = yield* Ref.get(state.turnOutcome);
32
+ if (Option.isSome(current)) {
33
+ yield* Deferred.fail(current.value, error);
34
+ }
35
+ });
36
+ export const anthropicToolToMcpTool = (state, def) => ({
37
+ name: def.name,
38
+ description: def.description,
39
+ inputSchema: def.inputSchema,
40
+ call: (arguments_) => Effect.gen(function* () {
41
+ const id = yield* Ref.updateAndGet(state.nextToolCallId, (current) => current + 1);
42
+ const toolCallId = `toolu_${id}`;
43
+ const resultDeferred = yield* Deferred.make();
44
+ yield* Ref.set(state.pendingToolCall, Option.some({ id: toolCallId, name: def.name, resultDeferred }));
45
+ yield* resolveTurnOutcomeSuccess(state, {
46
+ _tag: "ToolUse",
47
+ id: toolCallId,
48
+ name: def.name,
49
+ input: arguments_
50
+ });
51
+ const outcome = yield* Deferred.await(resultDeferred);
52
+ if (outcome.isError) {
53
+ return yield* ProviderError.make({ message: outcome.content });
54
+ }
55
+ return outcome.content;
56
+ }).pipe(Effect.mapError((error) => FlowLlmError.from(error)))
57
+ });
58
+ const parseToolDefs = (body) => jsonArray(jsonField(body, "tools")).flatMap((tool) => {
59
+ const name = jsonStringField(tool, "name");
60
+ if (name === undefined) {
61
+ return [];
62
+ }
63
+ const inputSchema = jsonField(tool, "input_schema");
64
+ return [
65
+ {
66
+ name,
67
+ description: jsonStringField(tool, "description") ?? "",
68
+ inputSchema: inputSchema !== undefined && isJsonRecord(inputSchema) ? inputSchema : {}
69
+ }
70
+ ];
71
+ });
72
+ const lastMessage = (body) => {
73
+ const messages = jsonArray(jsonField(body, "messages"));
74
+ return messages[messages.length - 1];
75
+ };
76
+ const contentBlocksText = (value) => typeof value === "string"
77
+ ? value
78
+ : jsonArray(value)
79
+ .map((block) => jsonStringField(block, "text") ?? "")
80
+ .join("\n");
81
+ const toolResultInLastMessage = (body) => {
82
+ const message = lastMessage(body);
83
+ if (jsonStringField(message, "role") !== "user") {
84
+ return undefined;
85
+ }
86
+ for (const block of jsonArray(jsonField(message, "content"))) {
87
+ if (jsonStringField(block, "type") !== "tool_result") {
88
+ continue;
89
+ }
90
+ const id = jsonStringField(block, "tool_use_id");
91
+ if (id === undefined) {
92
+ continue;
93
+ }
94
+ return {
95
+ id,
96
+ content: contentBlocksText(jsonField(block, "content")),
97
+ isError: jsonBooleanField(block, "is_error") === true
98
+ };
99
+ }
100
+ return undefined;
101
+ };
102
+ const userTextInLastMessage = (body) => contentBlocksText(jsonField(lastMessage(body), "content"));
103
+ const anthropicResponse = (model, outcome) => ({
104
+ id: `msg_${Date.now()}`,
105
+ type: "message",
106
+ role: "assistant",
107
+ model,
108
+ content: outcome._tag === "Text"
109
+ ? [{ type: "text", text: outcome.text }]
110
+ : [{ type: "tool_use", id: outcome.id, name: outcome.name, input: outcome.input }],
111
+ stop_reason: outcome._tag === "Text" ? "end_turn" : "tool_use",
112
+ // ACP does not currently surface per-request token counts to this bridge;
113
+ // zero, not omitted, since the Anthropic Messages response shape requires
114
+ // the field. Real usage accounting for this path is future work.
115
+ usage: { input_tokens: 0, output_tokens: 0 }
116
+ });
117
+ export const handleMessagesRequest = Effect.fn("@llm4ts/runner/NodeGeminiAcpBridge.handleMessagesRequest")(function* (state, body) {
118
+ const continuation = toolResultInLastMessage(body);
119
+ let pendingToResolve;
120
+ if (continuation !== undefined) {
121
+ const pending = yield* Ref.get(state.pendingToolCall);
122
+ if (Option.isNone(pending) || pending.value.id !== continuation.id) {
123
+ return yield* InvalidRequestError.make({
124
+ message: `no pending tool call matches tool_result id ${continuation.id}`
125
+ });
126
+ }
127
+ yield* Ref.set(state.pendingToolCall, Option.none());
128
+ pendingToResolve = pending.value;
129
+ }
130
+ // Registered before anything that could resolve it (a resumed tool call
131
+ // unblocking gemini's still-in-flight prompt, or — the first turn — the
132
+ // forked prompt() call itself) can run: both can complete fast enough to
133
+ // race an outcome slot only opened afterward, silently dropping it
134
+ // forever with nothing left awaiting it.
135
+ const outcomeDeferred = yield* Deferred.make();
136
+ yield* Ref.set(state.turnOutcome, Option.some(outcomeDeferred));
137
+ if (pendingToResolve !== undefined) {
138
+ yield* Deferred.succeed(pendingToResolve.resultDeferred, {
139
+ content: continuation?.content ?? "",
140
+ isError: continuation?.isError ?? false
141
+ });
142
+ }
143
+ else {
144
+ yield* Ref.set(state.tools, parseToolDefs(body));
145
+ const existing = yield* Ref.get(state.sessionId);
146
+ const sessionId = existing._tag === "Some"
147
+ ? existing.value
148
+ : yield* Effect.tap(state.session.newSession(state.cwd, state.mcpServerUrl), (id) => Ref.set(state.sessionId, Option.some(id)));
149
+ const text = userTextInLastMessage(body);
150
+ yield* state.forkBackground(state.session.prompt(sessionId, text).pipe(Effect.matchEffect({
151
+ onSuccess: (result) => resolveTurnOutcomeSuccess(state, { _tag: "Text", text: result.text }),
152
+ onFailure: (error) => resolveTurnOutcomeFailure(state, error)
153
+ })));
154
+ }
155
+ const outcome = yield* Deferred.await(outcomeDeferred);
156
+ yield* Ref.set(state.turnOutcome, Option.none());
157
+ return anthropicResponse(jsonStringField(body, "model") ?? "gemini", outcome);
158
+ });
159
+ const readBody = (request) => Effect.callback((resume) => {
160
+ const chunks = [];
161
+ request.on("data", (chunk) => chunks.push(chunk));
162
+ request.on("end", () => resume(Effect.succeed(Buffer.concat(chunks).toString("utf8"))));
163
+ request.on("error", (error) => resume(ProviderError.make({ message: "failed to read request body", cause: error })));
164
+ });
165
+ const parseJson = (raw) => JSON.parse(raw);
166
+ const parseJsonBody = (raw) => Effect.try({
167
+ try: () => parseJson(raw),
168
+ catch: () => InvalidRequestError.make({ message: "request body is not valid JSON" })
169
+ });
170
+ const writeJson = (response, status, body) => {
171
+ const text = JSON.stringify(body);
172
+ response.writeHead(status, { "Content-Type": "application/json" });
173
+ response.end(text);
174
+ };
175
+ const errorStatus = (error) => error._tag === "InvalidRequestError" ? 400 : error._tag === "AuthenticationError" ? 401 : 502;
176
+ const bindError = (port, cause) => ProviderError.make({
177
+ message: `Gemini ACP bridge could not bind 127.0.0.1:${port} — another llm4ts run may already ` +
178
+ "own it (concurrent runs against one bridge are unsupported; see ADR 0016)",
179
+ cause
180
+ });
181
+ const acquireServer = (port, listener) => Effect.acquireRelease(Effect.callback((resume) => {
182
+ const server = createServer(listener);
183
+ const onError = (cause) => resume(Effect.fail(bindError(port, cause)));
184
+ server.once("error", onError);
185
+ server.listen(port, "127.0.0.1", () => {
186
+ server.off("error", onError);
187
+ resume(Effect.succeed(server));
188
+ });
189
+ }), (server) => Effect.callback((resume) => {
190
+ if (!server.listening) {
191
+ resume(Effect.void);
192
+ return;
193
+ }
194
+ server.close(() => resume(Effect.void));
195
+ }));
196
+ export const runGeminiAcpBridge = Effect.fn("@llm4ts/runner/NodeGeminiAcpBridge.run")(function* (config) {
197
+ const mcpServerUrl = `http://127.0.0.1:${config.port}/mcp`;
198
+ const session = yield* openGeminiAcpSession(config.executor, geminiAcpArgv(config.model), config.cwd, config.envVars ?? {});
199
+ const forkBackground = (effect) => Effect.sync(() => {
200
+ Effect.runFork(effect);
201
+ });
202
+ const state = yield* makeBridgeState(session, config.cwd, mcpServerUrl, forkBackground);
203
+ const requestListener = (request, response) => {
204
+ const path = request.url ?? "/";
205
+ if (request.method !== "POST" || (path !== "/v1/messages" && path !== "/mcp")) {
206
+ writeJson(response, 404, { error: { message: "not found" } });
207
+ return;
208
+ }
209
+ const handled = Effect.gen(function* () {
210
+ const raw = yield* readBody(request);
211
+ const body = yield* parseJsonBody(raw);
212
+ if (path === "/mcp") {
213
+ const tools = yield* Ref.get(state.tools);
214
+ const mcpTools = tools.map((tool) => anthropicToolToMcpTool(state, tool));
215
+ const reply = yield* handleMcpRequest(body, mcpTools);
216
+ return { status: 200, body: reply ?? {} };
217
+ }
218
+ const reply = yield* handleMessagesRequest(state, body);
219
+ return { status: 200, body: reply };
220
+ }).pipe(Effect.catch((error) => Effect.succeed({
221
+ status: errorStatus(error),
222
+ body: { error: { message: error.message } }
223
+ })));
224
+ Effect.runPromise(handled).then(({ status, body }) => writeJson(response, status, body), (defect) => writeJson(response, 500, { error: { message: String(defect) } }));
225
+ };
226
+ const server = yield* acquireServer(config.port, requestListener);
227
+ const address = server.address();
228
+ const port = address !== null && typeof address !== "string" ? address.port : config.port;
229
+ return { port, baseUrl: `http://127.0.0.1:${port}` };
230
+ });
231
+ //# sourceMappingURL=NodeGeminiAcpBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NodeGeminiAcpBridge.js","sourceRoot":"","sources":["../src/NodeGeminiAcpBridge.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/runner",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Node runner, CLI, terminal, and MCP composition for llm4ts",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,6 +22,7 @@
22
22
  "./FlowRunner": "./dist/FlowRunner.js",
23
23
  "./Kits": "./dist/Kits.js",
24
24
  "./McpStdio": "./dist/McpStdio.js",
25
+ "./NodeGeminiAcpBridge": "./dist/NodeGeminiAcpBridge.js",
25
26
  "./NodeHttpClient": "./dist/NodeHttpClient.js",
26
27
  "./NodeGeminiCliExecutor": "./dist/NodeGeminiCliExecutor.js",
27
28
  "./NodePlainFileStore": "./dist/NodePlainFileStore.js",
@@ -59,8 +60,8 @@
59
60
  "typescript"
60
61
  ],
61
62
  "dependencies": {
62
- "@llm4ts/core": "2.2.1",
63
- "@llm4ts/flow": "2.2.1"
63
+ "@llm4ts/core": "2.3.0",
64
+ "@llm4ts/flow": "2.3.0"
64
65
  },
65
66
  "peerDependencies": {
66
67
  "effect": "4.0.0-rc.115"
package/src/Doctor.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import { join } from "node:path"
1
4
  import * as Effect from "effect/Effect"
2
5
  import type { HealthStatus } from "@llm4ts/core/Models"
3
6
  import { ConnectorIds } from "@llm4ts/core/Models"
@@ -98,9 +101,60 @@ const wrap = (text: string, width: number, indent: string): ReadonlyArray<string
98
101
  const selectedCoder = (environment: Readonly<Record<string, string | undefined>>): string =>
99
102
  environment.LLM4TS_CODER ?? "claude (default)"
100
103
 
104
+ export const defaultGeminiBridgePort = "8731"
105
+
106
+ /**
107
+ * `~/.pi/agent/models.json` (ADR 0016). Read for reporting only — this file
108
+ * belongs to `pi`, outside llm4ts's package graph, and llm4ts never writes
109
+ * it. Missing is not an error here: the file simply doesn't exist yet.
110
+ */
111
+ export const defaultReadPiModelsJson = (): string | undefined => {
112
+ try {
113
+ return readFileSync(join(homedir(), ".pi", "agent", "models.json"), "utf8")
114
+ } catch {
115
+ return undefined
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Whether pi's custom-provider config points at this machine's Gemini ACP
121
+ * bridge — detect-and-report only, never auto-fixed (ADR 0016). `undefined`
122
+ * means the check doesn't apply: nothing asked for the bridge.
123
+ */
124
+ export const geminiBridgePrerequisites = (
125
+ environment: Readonly<Record<string, string | undefined>>,
126
+ modelsJson: string | undefined
127
+ ): PrerequisiteReport | undefined => {
128
+ if (!truthy(environment.LLM4TS_GEMINI_BRIDGE)) {
129
+ return undefined
130
+ }
131
+ const port = isSet(environment.LLM4TS_GEMINI_BRIDGE_PORT)
132
+ ? environment.LLM4TS_GEMINI_BRIDGE_PORT.trim()
133
+ : defaultGeminiBridgePort
134
+ const needle = `127.0.0.1:${port}`
135
+ const hint =
136
+ `add a custom provider to ~/.pi/agent/models.json with baseUrl ` +
137
+ `"http://${needle}" so pi draws inference from the gemini-cli bridge instead ` +
138
+ `of a model API key (ADR 0016) — llm4ts never writes this file for you.`
139
+ if (modelsJson === undefined) {
140
+ return { satisfied: false, summary: "~/.pi/agent/models.json not found", hint }
141
+ }
142
+ return modelsJson.includes(needle)
143
+ ? {
144
+ satisfied: true,
145
+ summary: `a provider in ~/.pi/agent/models.json points at ${needle}`
146
+ }
147
+ : {
148
+ satisfied: false,
149
+ summary: `no provider in ~/.pi/agent/models.json points at ${needle}`,
150
+ hint
151
+ }
152
+ }
153
+
101
154
  export const makeDoctorProgram = (
102
155
  registry: ConnectorRegistryShape = nodeFlowRunnerDependencies().registry,
103
- environment: Readonly<Record<string, string | undefined>> = process.env
156
+ environment: Readonly<Record<string, string | undefined>> = process.env,
157
+ readPiModelsJson: () => string | undefined = defaultReadPiModelsJson
104
158
  ): Effect.Effect<string> =>
105
159
  Effect.map(registry.healthCheckAll, (statuses) => {
106
160
  const lines: Array<string> = []
@@ -128,13 +182,22 @@ export const makeDoctorProgram = (
128
182
  (id) =>
129
183
  id.value === ConnectorIds.GeminiCli.value || id.value === ConnectorIds.GeminiApi.value
130
184
  )
131
- if (geminiRelevant) {
132
- const gemini = geminiPrerequisites(environment)
185
+ const bridge = geminiBridgePrerequisites(environment, readPiModelsJson())
186
+ if (geminiRelevant || bridge !== undefined) {
133
187
  lines.push("")
134
188
  lines.push("prerequisites:")
135
- lines.push(` ${gemini.satisfied ? "✔" : "?"} gemini: ${gemini.summary}`)
136
- if (gemini.hint !== undefined) {
137
- lines.push(...wrap(gemini.hint, 78, " "))
189
+ if (geminiRelevant) {
190
+ const gemini = geminiPrerequisites(environment)
191
+ lines.push(` ${gemini.satisfied ? "✔" : "?"} gemini: ${gemini.summary}`)
192
+ if (gemini.hint !== undefined) {
193
+ lines.push(...wrap(gemini.hint, 78, " "))
194
+ }
195
+ }
196
+ if (bridge !== undefined) {
197
+ lines.push(` ${bridge.satisfied ? "✔" : "?"} pi-gemini-bridge: ${bridge.summary}`)
198
+ if (bridge.hint !== undefined) {
199
+ lines.push(...wrap(bridge.hint, 78, " "))
200
+ }
138
201
  }
139
202
  }
140
203
 
@@ -0,0 +1,395 @@
1
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
2
+ import * as Deferred from "effect/Deferred"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Option from "effect/Option"
5
+ import * as Ref from "effect/Ref"
6
+ import type * as Scope from "effect/Scope"
7
+ import { InvalidRequestError, ProviderError, type LlmError } from "@llm4ts/core/Errors"
8
+ import type { ProcessExecutorShape } from "@llm4ts/core/ProcessExecutor"
9
+ import {
10
+ geminiAcpArgv,
11
+ openGeminiAcpSession,
12
+ type GeminiAcpSessionShape
13
+ } from "@llm4ts/core/providers/GeminiAcpSession"
14
+ import {
15
+ isJsonRecord,
16
+ jsonArray,
17
+ jsonBooleanField,
18
+ jsonField,
19
+ jsonStringField,
20
+ type JsonRecord,
21
+ type JsonValue
22
+ } from "@llm4ts/core/providers/CliSupport"
23
+ import { FlowLlmError } from "@llm4ts/flow/FlowError"
24
+ import { handleMcpRequest, type McpTool } from "@llm4ts/flow/McpServer"
25
+
26
+ /**
27
+ * Speaks the Anthropic Messages API on `/v1/messages` (what `pi`'s
28
+ * `~/.pi/agent/models.json` custom-provider entry points at) and MCP over
29
+ * HTTP on `/mcp` (what an ACP `session/new` registers with gemini). See
30
+ * ADR 0016 for why: pi's tool loop — not gemini's — executes every tool
31
+ * call, so a tool call gemini makes through the registered MCP tools pauses
32
+ * here until pi's *next* `/v1/messages` request supplies the result.
33
+ *
34
+ * One active ACP session per bridge instance (ADR 0016's simplifying
35
+ * assumption): correlation needs no session-id routing, just one pending
36
+ * tool call and one "what happens next" slot at a time.
37
+ */
38
+
39
+ interface AnthropicToolDef {
40
+ readonly name: string
41
+ readonly description: string
42
+ readonly inputSchema: JsonRecord
43
+ }
44
+
45
+ interface ToolResultPayload {
46
+ readonly content: string
47
+ readonly isError: boolean
48
+ }
49
+
50
+ interface PendingToolCall {
51
+ readonly id: string
52
+ readonly name: string
53
+ readonly resultDeferred: Deferred.Deferred<ToolResultPayload, LlmError>
54
+ }
55
+
56
+ export type TurnOutcome =
57
+ | { readonly _tag: "Text"; readonly text: string }
58
+ | {
59
+ readonly _tag: "ToolUse"
60
+ readonly id: string
61
+ readonly name: string
62
+ readonly input: JsonValue
63
+ }
64
+
65
+ export interface BridgeState {
66
+ readonly session: GeminiAcpSessionShape
67
+ readonly cwd: string
68
+ readonly mcpServerUrl: string
69
+ readonly sessionId: Ref.Ref<Option.Option<string>>
70
+ readonly tools: Ref.Ref<ReadonlyArray<AnthropicToolDef>>
71
+ readonly pendingToolCall: Ref.Ref<Option.Option<PendingToolCall>>
72
+ readonly turnOutcome: Ref.Ref<Option.Option<Deferred.Deferred<TurnOutcome, LlmError>>>
73
+ readonly nextToolCallId: Ref.Ref<number>
74
+ readonly forkBackground: <A>(effect: Effect.Effect<A, LlmError>) => Effect.Effect<void>
75
+ }
76
+
77
+ export const makeBridgeState = Effect.fn("@llm4ts/runner/NodeGeminiAcpBridge.makeBridgeState")(
78
+ function* (
79
+ session: GeminiAcpSessionShape,
80
+ cwd: string,
81
+ mcpServerUrl: string,
82
+ forkBackground: <A>(effect: Effect.Effect<A, LlmError>) => Effect.Effect<void>
83
+ ): Effect.fn.Return<BridgeState> {
84
+ return {
85
+ session,
86
+ cwd,
87
+ mcpServerUrl,
88
+ sessionId: yield* Ref.make<Option.Option<string>>(Option.none()),
89
+ tools: yield* Ref.make<ReadonlyArray<AnthropicToolDef>>([]),
90
+ pendingToolCall: yield* Ref.make<Option.Option<PendingToolCall>>(Option.none()),
91
+ turnOutcome: yield* Ref.make<Option.Option<Deferred.Deferred<TurnOutcome, LlmError>>>(
92
+ Option.none()
93
+ ),
94
+ nextToolCallId: yield* Ref.make(0),
95
+ forkBackground
96
+ }
97
+ }
98
+ )
99
+
100
+ const resolveTurnOutcomeSuccess = (state: BridgeState, outcome: TurnOutcome): Effect.Effect<void> =>
101
+ Effect.gen(function* () {
102
+ const current = yield* Ref.get(state.turnOutcome)
103
+ if (Option.isSome(current)) {
104
+ yield* Deferred.succeed(current.value, outcome)
105
+ }
106
+ })
107
+
108
+ const resolveTurnOutcomeFailure = (state: BridgeState, error: LlmError): Effect.Effect<void> =>
109
+ Effect.gen(function* () {
110
+ const current = yield* Ref.get(state.turnOutcome)
111
+ if (Option.isSome(current)) {
112
+ yield* Deferred.fail(current.value, error)
113
+ }
114
+ })
115
+
116
+ export const anthropicToolToMcpTool = (state: BridgeState, def: AnthropicToolDef): McpTool => ({
117
+ name: def.name,
118
+ description: def.description,
119
+ inputSchema: def.inputSchema,
120
+ call: (arguments_) =>
121
+ Effect.gen(function* () {
122
+ const id = yield* Ref.updateAndGet(state.nextToolCallId, (current) => current + 1)
123
+ const toolCallId = `toolu_${id}`
124
+ const resultDeferred = yield* Deferred.make<ToolResultPayload, LlmError>()
125
+ yield* Ref.set(
126
+ state.pendingToolCall,
127
+ Option.some({ id: toolCallId, name: def.name, resultDeferred })
128
+ )
129
+ yield* resolveTurnOutcomeSuccess(state, {
130
+ _tag: "ToolUse",
131
+ id: toolCallId,
132
+ name: def.name,
133
+ input: arguments_
134
+ })
135
+ const outcome = yield* Deferred.await(resultDeferred)
136
+ if (outcome.isError) {
137
+ return yield* ProviderError.make({ message: outcome.content })
138
+ }
139
+ return outcome.content
140
+ }).pipe(Effect.mapError((error) => FlowLlmError.from(error)))
141
+ })
142
+
143
+ const parseToolDefs = (body: JsonValue): ReadonlyArray<AnthropicToolDef> =>
144
+ jsonArray(jsonField(body, "tools")).flatMap((tool) => {
145
+ const name = jsonStringField(tool, "name")
146
+ if (name === undefined) {
147
+ return []
148
+ }
149
+ const inputSchema = jsonField(tool, "input_schema")
150
+ return [
151
+ {
152
+ name,
153
+ description: jsonStringField(tool, "description") ?? "",
154
+ inputSchema: inputSchema !== undefined && isJsonRecord(inputSchema) ? inputSchema : {}
155
+ }
156
+ ]
157
+ })
158
+
159
+ const lastMessage = (body: JsonValue): JsonValue | undefined => {
160
+ const messages = jsonArray(jsonField(body, "messages"))
161
+ return messages[messages.length - 1]
162
+ }
163
+
164
+ const contentBlocksText = (value: JsonValue | undefined): string =>
165
+ typeof value === "string"
166
+ ? value
167
+ : jsonArray(value)
168
+ .map((block) => jsonStringField(block, "text") ?? "")
169
+ .join("\n")
170
+
171
+ const toolResultInLastMessage = (
172
+ body: JsonValue
173
+ ): { readonly id: string; readonly content: string; readonly isError: boolean } | undefined => {
174
+ const message = lastMessage(body)
175
+ if (jsonStringField(message, "role") !== "user") {
176
+ return undefined
177
+ }
178
+ for (const block of jsonArray(jsonField(message, "content"))) {
179
+ if (jsonStringField(block, "type") !== "tool_result") {
180
+ continue
181
+ }
182
+ const id = jsonStringField(block, "tool_use_id")
183
+ if (id === undefined) {
184
+ continue
185
+ }
186
+ return {
187
+ id,
188
+ content: contentBlocksText(jsonField(block, "content")),
189
+ isError: jsonBooleanField(block, "is_error") === true
190
+ }
191
+ }
192
+ return undefined
193
+ }
194
+
195
+ const userTextInLastMessage = (body: JsonValue): string =>
196
+ contentBlocksText(jsonField(lastMessage(body), "content"))
197
+
198
+ const anthropicResponse = (model: string, outcome: TurnOutcome): JsonRecord => ({
199
+ id: `msg_${Date.now()}`,
200
+ type: "message",
201
+ role: "assistant",
202
+ model,
203
+ content:
204
+ outcome._tag === "Text"
205
+ ? [{ type: "text", text: outcome.text }]
206
+ : [{ type: "tool_use", id: outcome.id, name: outcome.name, input: outcome.input }],
207
+ stop_reason: outcome._tag === "Text" ? "end_turn" : "tool_use",
208
+ // ACP does not currently surface per-request token counts to this bridge;
209
+ // zero, not omitted, since the Anthropic Messages response shape requires
210
+ // the field. Real usage accounting for this path is future work.
211
+ usage: { input_tokens: 0, output_tokens: 0 }
212
+ })
213
+
214
+ export const handleMessagesRequest = Effect.fn(
215
+ "@llm4ts/runner/NodeGeminiAcpBridge.handleMessagesRequest"
216
+ )(function* (state: BridgeState, body: JsonValue): Effect.fn.Return<JsonRecord, LlmError> {
217
+ const continuation = toolResultInLastMessage(body)
218
+ let pendingToResolve: PendingToolCall | undefined
219
+ if (continuation !== undefined) {
220
+ const pending = yield* Ref.get(state.pendingToolCall)
221
+ if (Option.isNone(pending) || pending.value.id !== continuation.id) {
222
+ return yield* InvalidRequestError.make({
223
+ message: `no pending tool call matches tool_result id ${continuation.id}`
224
+ })
225
+ }
226
+ yield* Ref.set(state.pendingToolCall, Option.none())
227
+ pendingToResolve = pending.value
228
+ }
229
+
230
+ // Registered before anything that could resolve it (a resumed tool call
231
+ // unblocking gemini's still-in-flight prompt, or — the first turn — the
232
+ // forked prompt() call itself) can run: both can complete fast enough to
233
+ // race an outcome slot only opened afterward, silently dropping it
234
+ // forever with nothing left awaiting it.
235
+ const outcomeDeferred = yield* Deferred.make<TurnOutcome, LlmError>()
236
+ yield* Ref.set(state.turnOutcome, Option.some(outcomeDeferred))
237
+
238
+ if (pendingToResolve !== undefined) {
239
+ yield* Deferred.succeed(pendingToResolve.resultDeferred, {
240
+ content: continuation?.content ?? "",
241
+ isError: continuation?.isError ?? false
242
+ })
243
+ } else {
244
+ yield* Ref.set(state.tools, parseToolDefs(body))
245
+ const existing = yield* Ref.get(state.sessionId)
246
+ const sessionId =
247
+ existing._tag === "Some"
248
+ ? existing.value
249
+ : yield* Effect.tap(state.session.newSession(state.cwd, state.mcpServerUrl), (id) =>
250
+ Ref.set(state.sessionId, Option.some(id))
251
+ )
252
+ const text = userTextInLastMessage(body)
253
+ yield* state.forkBackground(
254
+ state.session.prompt(sessionId, text).pipe(
255
+ Effect.matchEffect({
256
+ onSuccess: (result) =>
257
+ resolveTurnOutcomeSuccess(state, { _tag: "Text", text: result.text }),
258
+ onFailure: (error) => resolveTurnOutcomeFailure(state, error)
259
+ })
260
+ )
261
+ )
262
+ }
263
+
264
+ const outcome = yield* Deferred.await(outcomeDeferred)
265
+ yield* Ref.set(state.turnOutcome, Option.none())
266
+
267
+ return anthropicResponse(jsonStringField(body, "model") ?? "gemini", outcome)
268
+ })
269
+
270
+ const readBody = (request: IncomingMessage): Effect.Effect<string, ProviderError> =>
271
+ Effect.callback((resume) => {
272
+ const chunks: Array<Buffer> = []
273
+ request.on("data", (chunk: Buffer) => chunks.push(chunk))
274
+ request.on("end", () => resume(Effect.succeed(Buffer.concat(chunks).toString("utf8"))))
275
+ request.on("error", (error) =>
276
+ resume(ProviderError.make({ message: "failed to read request body", cause: error }))
277
+ )
278
+ })
279
+
280
+ const parseJson = (raw: string): JsonValue => JSON.parse(raw)
281
+
282
+ const parseJsonBody = (raw: string): Effect.Effect<JsonValue, InvalidRequestError> =>
283
+ Effect.try({
284
+ try: () => parseJson(raw),
285
+ catch: () => InvalidRequestError.make({ message: "request body is not valid JSON" })
286
+ })
287
+
288
+ const writeJson = (response: ServerResponse, status: number, body: JsonValue): void => {
289
+ const text = JSON.stringify(body)
290
+ response.writeHead(status, { "Content-Type": "application/json" })
291
+ response.end(text)
292
+ }
293
+
294
+ const errorStatus = (error: LlmError | InvalidRequestError): number =>
295
+ error._tag === "InvalidRequestError" ? 400 : error._tag === "AuthenticationError" ? 401 : 502
296
+
297
+ export interface GeminiAcpBridgeConfig {
298
+ readonly port: number
299
+ readonly cwd: string
300
+ readonly model?: string
301
+ readonly executor: ProcessExecutorShape
302
+ readonly envVars?: Readonly<Record<string, string>>
303
+ }
304
+
305
+ export interface RunningGeminiAcpBridge {
306
+ readonly port: number
307
+ readonly baseUrl: string
308
+ }
309
+
310
+ const bindError = (port: number, cause: unknown): ProviderError =>
311
+ ProviderError.make({
312
+ message:
313
+ `Gemini ACP bridge could not bind 127.0.0.1:${port} — another llm4ts run may already ` +
314
+ "own it (concurrent runs against one bridge are unsupported; see ADR 0016)",
315
+ cause
316
+ })
317
+
318
+ const acquireServer = (
319
+ port: number,
320
+ listener: (request: IncomingMessage, response: ServerResponse) => void
321
+ ): Effect.Effect<Server, ProviderError, Scope.Scope> =>
322
+ Effect.acquireRelease(
323
+ Effect.callback<Server, ProviderError>((resume) => {
324
+ const server = createServer(listener)
325
+ const onError = (cause: Error): void => resume(Effect.fail(bindError(port, cause)))
326
+ server.once("error", onError)
327
+ server.listen(port, "127.0.0.1", () => {
328
+ server.off("error", onError)
329
+ resume(Effect.succeed(server))
330
+ })
331
+ }),
332
+ (server) =>
333
+ Effect.callback<void>((resume) => {
334
+ if (!server.listening) {
335
+ resume(Effect.void)
336
+ return
337
+ }
338
+ server.close(() => resume(Effect.void))
339
+ })
340
+ )
341
+
342
+ export const runGeminiAcpBridge = Effect.fn("@llm4ts/runner/NodeGeminiAcpBridge.run")(function* (
343
+ config: GeminiAcpBridgeConfig
344
+ ): Effect.fn.Return<RunningGeminiAcpBridge, LlmError, Scope.Scope> {
345
+ const mcpServerUrl = `http://127.0.0.1:${config.port}/mcp`
346
+ const session = yield* openGeminiAcpSession(
347
+ config.executor,
348
+ geminiAcpArgv(config.model),
349
+ config.cwd,
350
+ config.envVars ?? {}
351
+ )
352
+ const forkBackground = <A>(effect: Effect.Effect<A, LlmError>): Effect.Effect<void> =>
353
+ Effect.sync(() => {
354
+ Effect.runFork(effect)
355
+ })
356
+ const state = yield* makeBridgeState(session, config.cwd, mcpServerUrl, forkBackground)
357
+
358
+ const requestListener = (request: IncomingMessage, response: ServerResponse): void => {
359
+ const path = request.url ?? "/"
360
+ if (request.method !== "POST" || (path !== "/v1/messages" && path !== "/mcp")) {
361
+ writeJson(response, 404, { error: { message: "not found" } })
362
+ return
363
+ }
364
+
365
+ const handled = Effect.gen(function* () {
366
+ const raw = yield* readBody(request)
367
+ const body = yield* parseJsonBody(raw)
368
+ if (path === "/mcp") {
369
+ const tools = yield* Ref.get(state.tools)
370
+ const mcpTools = tools.map((tool) => anthropicToolToMcpTool(state, tool))
371
+ const reply = yield* handleMcpRequest(body, mcpTools)
372
+ return { status: 200, body: reply ?? {} }
373
+ }
374
+ const reply = yield* handleMessagesRequest(state, body)
375
+ return { status: 200, body: reply }
376
+ }).pipe(
377
+ Effect.catch((error) =>
378
+ Effect.succeed({
379
+ status: errorStatus(error),
380
+ body: { error: { message: error.message } }
381
+ })
382
+ )
383
+ )
384
+
385
+ Effect.runPromise(handled).then(
386
+ ({ status, body }: { status: number; body: JsonValue }) => writeJson(response, status, body),
387
+ (defect: unknown) => writeJson(response, 500, { error: { message: String(defect) } })
388
+ )
389
+ }
390
+
391
+ const server = yield* acquireServer(config.port, requestListener)
392
+ const address = server.address()
393
+ const port = address !== null && typeof address !== "string" ? address.port : config.port
394
+ return { port, baseUrl: `http://127.0.0.1:${port}` }
395
+ })