@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 +14 -1
- package/dist/Doctor.d.ts.map +1 -1
- package/dist/Doctor.js +62 -6
- package/dist/Doctor.js.map +1 -1
- package/dist/NodeGeminiAcpBridge.d.ts +73 -0
- package/dist/NodeGeminiAcpBridge.d.ts.map +1 -0
- package/dist/NodeGeminiAcpBridge.js +231 -0
- package/dist/NodeGeminiAcpBridge.js.map +1 -0
- package/package.json +4 -3
- package/src/Doctor.ts +69 -6
- package/src/NodeGeminiAcpBridge.ts +395 -0
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
|
|
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
|
package/dist/Doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Doctor.d.ts","sourceRoot":"","sources":["../src/Doctor.ts"],"names":[],"mappings":"
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
147
|
+
const bridge = geminiBridgePrerequisites(environment, readPiModelsJson());
|
|
148
|
+
if (geminiRelevant || bridge !== undefined) {
|
|
102
149
|
lines.push("");
|
|
103
150
|
lines.push("prerequisites:");
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
lines.push(
|
|
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("");
|
package/dist/Doctor.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
63
|
-
"@llm4ts/flow": "2.
|
|
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
|
-
|
|
132
|
-
|
|
185
|
+
const bridge = geminiBridgePrerequisites(environment, readPiModelsJson())
|
|
186
|
+
if (geminiRelevant || bridge !== undefined) {
|
|
133
187
|
lines.push("")
|
|
134
188
|
lines.push("prerequisites:")
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
lines.push(
|
|
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
|
+
})
|