@llm4ts/core 2.2.0 → 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.
@@ -0,0 +1,67 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type * as Scope from "effect/Scope";
3
+ import * as Stream from "effect/Stream";
4
+ import { ProviderError, type LlmError } from "../Errors.ts";
5
+ import type { ProcessExecutorShape } from "../ProcessExecutor.ts";
6
+ import { type JsonRecord, type JsonValue } from "./CliSupport.ts";
7
+ /**
8
+ * ACP (Agent Client Protocol, `gemini --experimental-acp`) message shapes
9
+ * are best-effort against public docs (geminicli.com/docs/cli/acp-mode,
10
+ * agentclientprotocol.com) — there is no installed `gemini` binary in CI to
11
+ * verify against (the repo's CI constraint is exactly "no installed
12
+ * provider CLIs"). Tests here exercise this module against a scripted fake
13
+ * peer, the same posture ClaudeAgentSession.test.ts takes against a fake
14
+ * `claude` process. A first live run against a real `gemini` is an
15
+ * integration smoke test, not a substitute for that determinism.
16
+ */
17
+ export declare const geminiAcpArgv: (model: string | undefined) => ReadonlyArray<string>;
18
+ export declare const acpInitializeParams: JsonRecord;
19
+ export declare const acpNewSessionParams: (cwd: string, mcpServerUrl: string) => JsonRecord;
20
+ export declare const acpPromptParams: (sessionId: string, text: string) => JsonRecord;
21
+ export declare const acpInitializeLine: (id: number) => string;
22
+ export declare const acpNewSessionLine: (id: number, cwd: string, mcpServerUrl: string) => string;
23
+ export declare const acpPromptLine: (id: number, sessionId: string, text: string) => string;
24
+ export declare const acpPermissionResponseLine: (id: JsonValue, approved: boolean) => string;
25
+ export declare const acpUnsupportedFsResponseLine: (id: JsonValue) => string;
26
+ export declare class AcpTextChunk {
27
+ readonly text: string;
28
+ readonly _tag = "TextChunk";
29
+ constructor(text: string);
30
+ }
31
+ export declare class AcpToolCallObserved {
32
+ readonly id: string;
33
+ readonly name: string;
34
+ readonly input: JsonValue;
35
+ readonly _tag = "ToolCallObserved";
36
+ constructor(id: string, name: string, input: JsonValue);
37
+ }
38
+ export declare class AcpPermissionRequested {
39
+ readonly id: string;
40
+ readonly toolCallId: string;
41
+ readonly name: string;
42
+ readonly _tag = "PermissionRequested";
43
+ constructor(id: string, toolCallId: string, name: string);
44
+ }
45
+ export type AcpSessionEvent = AcpTextChunk | AcpToolCallObserved | AcpPermissionRequested;
46
+ export interface AcpPromptResult {
47
+ readonly text: string;
48
+ readonly stopReason: string;
49
+ }
50
+ export interface AcpApprovalPolicy {
51
+ readonly decide: (toolName: string, input: JsonValue) => Effect.Effect<boolean>;
52
+ }
53
+ /**
54
+ * The safety decision belongs to whichever side actually executes the tool
55
+ * (pi, over the MCP bridge) — gemini's own permission prompt is a formality
56
+ * for a call this session never runs itself, so the default is to approve.
57
+ */
58
+ export declare const alwaysApprove: AcpApprovalPolicy;
59
+ export interface GeminiAcpSessionShape {
60
+ readonly events: Stream.Stream<AcpSessionEvent, LlmError>;
61
+ readonly newSession: (cwd: string, mcpServerUrl: string) => Effect.Effect<string, LlmError>;
62
+ readonly prompt: (sessionId: string, text: string) => Effect.Effect<AcpPromptResult, LlmError>;
63
+ readonly cancel: Effect.Effect<void>;
64
+ }
65
+ export declare const parseAcpSessionUpdate: (params: JsonValue | undefined) => AcpSessionEvent | undefined;
66
+ export declare const openGeminiAcpSession: (executor: ProcessExecutorShape, argv: readonly string[], cwd: string, envVars?: Readonly<Record<string, string>> | undefined, approval?: AcpApprovalPolicy | undefined) => Effect.Effect<GeminiAcpSessionShape, ProviderError | import("../Errors.ts").UsageLimitError | import("../Errors.ts").AuthenticationError | import("../Errors.ts").InvalidRequestError | import("../Errors.ts").ParseError | import("../Errors.ts").ToolError | import("../Errors.ts").ConfigError | import("../Errors.ts").RateLimitError | import("../Errors.ts").TimeoutError | import("../Errors.ts").TurnLimitError, Scope.Scope>;
67
+ //# sourceMappingURL=GeminiAcpSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiAcpSession.d.ts","sourceRoot":"","sources":["../../src/providers/GeminiAcpSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAIvC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACjE,OAAO,EAML,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAA;AAExB;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,aAAa,CAAC,MAAM,CAI7E,CAAA;AAOD,eAAO,MAAM,mBAAmB,EAAE,UAIjC,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,EAAE,cAAc,MAAM,KAAG,UAGtE,CAAA;AAEF,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,EAAE,MAAM,MAAM,KAAG,UAGhE,CAAA;AAKF,eAAO,MAAM,iBAAiB,GAAI,IAAI,MAAM,KAAG,MACQ,CAAA;AAEvD,eAAO,MAAM,iBAAiB,GAAI,IAAI,MAAM,EAAE,KAAK,MAAM,EAAE,cAAc,MAAM,KAAG,MACP,CAAA;AAE3E,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,EAAE,WAAW,MAAM,EAAE,MAAM,MAAM,KAAG,MACJ,CAAA;AAExE,eAAO,MAAM,yBAAyB,GAAI,IAAI,SAAS,EAAE,UAAU,OAAO,KAAG,MAKzE,CAAA;AAEJ,eAAO,MAAM,4BAA4B,GAAI,IAAI,SAAS,KAAG,MAKzD,CAAA;AAEJ,qBAAa,YAAY;IAEX,QAAQ,CAAC,IAAI,EAAE,MAAM;IADjC,QAAQ,CAAC,IAAI,eAAc;gBACN,IAAI,EAAE,MAAM;CAClC;AAED,qBAAa,mBAAmB;IAG5B,QAAQ,CAAC,EAAE,EAAE,MAAM;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS;IAJ3B,QAAQ,CAAC,IAAI,sBAAqB;gBAEvB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS;CAE5B;AAED,qBAAa,sBAAsB;IAG/B,QAAQ,CAAC,EAAE,EAAE,MAAM;IACnB,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM;IAJvB,QAAQ,CAAC,IAAI,yBAAwB;gBAE1B,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM;CAExB;AAED,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,mBAAmB,GAAG,sBAAsB,CAAA;AAEzF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CAChF;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,iBAE3B,CAAA;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAA;IACzD,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC3F,QAAQ,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAA;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACrC;AA4BD,eAAO,MAAM,qBAAqB,GAChC,QAAQ,SAAS,GAAG,SAAS,KAC5B,eAAe,GAAG,SAIpB,CAAA;AAED,eAAO,MAAM,oBAAoB,mlBAiLhC,CAAA"}
@@ -0,0 +1,239 @@
1
+ import * as Deferred from "effect/Deferred";
2
+ import * as Effect from "effect/Effect";
3
+ import * as PubSub from "effect/PubSub";
4
+ import * as Queue from "effect/Queue";
5
+ import * as Ref from "effect/Ref";
6
+ import * as Stream from "effect/Stream";
7
+ import { ProviderError } from "../Errors.js";
8
+ import { isJsonRecord, jsonField, jsonStringField, jsonText, parseJsonLine } from "./CliSupport.js";
9
+ /**
10
+ * ACP (Agent Client Protocol, `gemini --experimental-acp`) message shapes
11
+ * are best-effort against public docs (geminicli.com/docs/cli/acp-mode,
12
+ * agentclientprotocol.com) — there is no installed `gemini` binary in CI to
13
+ * verify against (the repo's CI constraint is exactly "no installed
14
+ * provider CLIs"). Tests here exercise this module against a scripted fake
15
+ * peer, the same posture ClaudeAgentSession.test.ts takes against a fake
16
+ * `claude` process. A first live run against a real `gemini` is an
17
+ * integration smoke test, not a substitute for that determinism.
18
+ */
19
+ export const geminiAcpArgv = (model) => [
20
+ "gemini",
21
+ "--experimental-acp",
22
+ ...(model === undefined ? [] : ["-m", model])
23
+ ];
24
+ const clientCapabilities = {
25
+ fs: { readTextFile: false, writeTextFile: false },
26
+ terminal: false
27
+ };
28
+ export const acpInitializeParams = {
29
+ protocolVersion: 1,
30
+ clientInfo: { name: "llm4ts", version: "1" },
31
+ clientCapabilities
32
+ };
33
+ export const acpNewSessionParams = (cwd, mcpServerUrl) => ({
34
+ cwd,
35
+ mcpServers: [{ type: "http", url: mcpServerUrl }]
36
+ });
37
+ export const acpPromptParams = (sessionId, text) => ({
38
+ sessionId,
39
+ prompt: [{ type: "text", text }]
40
+ });
41
+ const jsonRpcRequest = (id, method, params) => jsonText({ jsonrpc: "2.0", id, method, params });
42
+ export const acpInitializeLine = (id) => jsonRpcRequest(id, "initialize", acpInitializeParams);
43
+ export const acpNewSessionLine = (id, cwd, mcpServerUrl) => jsonRpcRequest(id, "session/new", acpNewSessionParams(cwd, mcpServerUrl));
44
+ export const acpPromptLine = (id, sessionId, text) => jsonRpcRequest(id, "session/prompt", acpPromptParams(sessionId, text));
45
+ export const acpPermissionResponseLine = (id, approved) => jsonText({
46
+ jsonrpc: "2.0",
47
+ id,
48
+ result: { outcome: approved ? "Approved" : "Denied" }
49
+ });
50
+ export const acpUnsupportedFsResponseLine = (id) => jsonText({
51
+ jsonrpc: "2.0",
52
+ id,
53
+ error: { code: -32601, message: "fs access not supported by this client" }
54
+ });
55
+ export class AcpTextChunk {
56
+ text;
57
+ _tag = "TextChunk";
58
+ constructor(text) {
59
+ this.text = text;
60
+ }
61
+ }
62
+ export class AcpToolCallObserved {
63
+ id;
64
+ name;
65
+ input;
66
+ _tag = "ToolCallObserved";
67
+ constructor(id, name, input) {
68
+ this.id = id;
69
+ this.name = name;
70
+ this.input = input;
71
+ }
72
+ }
73
+ export class AcpPermissionRequested {
74
+ id;
75
+ toolCallId;
76
+ name;
77
+ _tag = "PermissionRequested";
78
+ constructor(id, toolCallId, name) {
79
+ this.id = id;
80
+ this.toolCallId = toolCallId;
81
+ this.name = name;
82
+ }
83
+ }
84
+ /**
85
+ * The safety decision belongs to whichever side actually executes the tool
86
+ * (pi, over the MCP bridge) — gemini's own permission prompt is a formality
87
+ * for a call this session never runs itself, so the default is to approve.
88
+ */
89
+ export const alwaysApprove = {
90
+ decide: () => Effect.succeed(true)
91
+ };
92
+ const contentTextOf = (value) => jsonStringField(jsonField(value, "content"), "text");
93
+ /** `content_chunk` and `agent_message_chunk` are both seen across ACP revisions for a text delta. */
94
+ const textChunkUpdate = (update) => {
95
+ const type = jsonStringField(update, "type");
96
+ return type === "content_chunk" || type === "agent_message_chunk"
97
+ ? contentTextOf(update)
98
+ : undefined;
99
+ };
100
+ const toolCallUpdate = (update) => {
101
+ if (jsonStringField(update, "type") !== "tool_call") {
102
+ return undefined;
103
+ }
104
+ const toolCall = jsonField(update, "toolCall");
105
+ const id = jsonStringField(toolCall, "id");
106
+ return id === undefined
107
+ ? undefined
108
+ : new AcpToolCallObserved(id, jsonStringField(toolCall, "name") ?? "", jsonField(toolCall, "input") ?? {});
109
+ };
110
+ export const parseAcpSessionUpdate = (params) => {
111
+ const update = jsonField(params, "update");
112
+ const text = textChunkUpdate(update);
113
+ return text !== undefined && text.length > 0 ? new AcpTextChunk(text) : toolCallUpdate(update);
114
+ };
115
+ export const openGeminiAcpSession = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.open")(function* (executor, argv, cwd, envVars = {}, approval = alwaysApprove) {
116
+ const [stdin, stdout] = yield* executor.runBidirectional(argv, cwd, envVars);
117
+ const events = yield* PubSub.unbounded();
118
+ // Accumulated directly by the background reader below rather than via a
119
+ // subscription `prompt` opens on demand: a subscription opened only when
120
+ // `prompt` runs can race a canned/fast-replaying stdout (real or faked in
121
+ // tests) that the reader has already drained by then. One prompt in
122
+ // flight at a time (the single-active-session assumption) makes
123
+ // reset-then-read at each call's boundary sufficient.
124
+ const accumulatedText = yield* Ref.make("");
125
+ const nextId = yield* Ref.make(0);
126
+ const pending = yield* Ref.make(new Map());
127
+ const allocateId = Ref.updateAndGet(nextId, (current) => current + 1);
128
+ const registerPending = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.registerPending")(function* (id) {
129
+ const deferred = yield* Deferred.make();
130
+ yield* Ref.update(pending, (current) => {
131
+ const next = new Map(current);
132
+ next.set(id, deferred);
133
+ return next;
134
+ });
135
+ return deferred;
136
+ });
137
+ const resolvePending = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.resolvePending")(function* (id, result, error) {
138
+ const current = yield* Ref.get(pending);
139
+ const deferred = current.get(id);
140
+ if (deferred === undefined) {
141
+ return;
142
+ }
143
+ yield* Ref.update(pending, (map) => {
144
+ const next = new Map(map);
145
+ next.delete(id);
146
+ return next;
147
+ });
148
+ if (error !== undefined) {
149
+ yield* Deferred.fail(deferred, ProviderError.make({
150
+ message: `Gemini ACP error: ${jsonStringField(error, "message") ?? jsonText(error)}`
151
+ }));
152
+ return;
153
+ }
154
+ yield* Deferred.succeed(deferred, result ?? {});
155
+ });
156
+ const respondToPermission = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.respondToPermission")(function* (requestId, toolName, input) {
157
+ const approved = yield* approval.decide(toolName, input);
158
+ yield* Queue.offer(stdin, acpPermissionResponseLine(requestId, approved));
159
+ });
160
+ const handleAgentRequest = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.handleAgentRequest")(function* (id, method, params) {
161
+ switch (method) {
162
+ case "session/request_permission": {
163
+ const toolCall = jsonField(params, "toolCall");
164
+ const toolCallId = jsonStringField(toolCall, "id") ?? "";
165
+ const toolName = jsonStringField(toolCall, "name") ?? "";
166
+ yield* PubSub.publish(events, new AcpPermissionRequested(typeof id === "string" ? id : jsonText(id), toolCallId, toolName));
167
+ yield* respondToPermission(id, toolName, jsonField(toolCall, "input") ?? {});
168
+ return;
169
+ }
170
+ case "fs/read_text_file":
171
+ case "fs/write_text_file":
172
+ yield* Queue.offer(stdin, acpUnsupportedFsResponseLine(id));
173
+ return;
174
+ default:
175
+ return;
176
+ }
177
+ });
178
+ const handleLine = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.handleLine")(function* (line) {
179
+ const json = parseJsonLine(line);
180
+ if (json === undefined || !isJsonRecord(json)) {
181
+ return;
182
+ }
183
+ const id = jsonField(json, "id");
184
+ const method = jsonStringField(json, "method");
185
+ if (method === "session/update") {
186
+ const event = parseAcpSessionUpdate(jsonField(json, "params"));
187
+ if (event !== undefined) {
188
+ if (event._tag === "TextChunk") {
189
+ yield* Ref.update(accumulatedText, (current) => current + event.text);
190
+ }
191
+ yield* PubSub.publish(events, event);
192
+ }
193
+ return;
194
+ }
195
+ if (method !== undefined) {
196
+ if (id !== undefined) {
197
+ yield* handleAgentRequest(id, method, jsonField(json, "params"));
198
+ }
199
+ return;
200
+ }
201
+ if (typeof id === "number") {
202
+ yield* resolvePending(id, jsonField(json, "result"), jsonField(json, "error"));
203
+ }
204
+ });
205
+ yield* stdout.pipe(Stream.mapEffect(handleLine), Stream.runDrain, Effect.catch(() => Effect.void), Effect.forkScoped);
206
+ const call = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.call")(function* (method, params) {
207
+ const id = yield* allocateId;
208
+ const deferred = yield* registerPending(id);
209
+ yield* Queue.offer(stdin, jsonRpcRequest(id, method, params));
210
+ return yield* Deferred.await(deferred);
211
+ });
212
+ const newSession = (cwd, mcpServerUrl) => Effect.gen(function* () {
213
+ yield* call("initialize", acpInitializeParams);
214
+ const result = yield* call("session/new", acpNewSessionParams(cwd, mcpServerUrl));
215
+ const sessionId = jsonStringField(result, "sessionId");
216
+ if (sessionId === undefined) {
217
+ return yield* ProviderError.make({
218
+ message: "Gemini ACP session/new did not return a sessionId"
219
+ });
220
+ }
221
+ return sessionId;
222
+ });
223
+ const prompt = (sessionId, text) => Effect.gen(function* () {
224
+ yield* Ref.set(accumulatedText, "");
225
+ const result = yield* call("session/prompt", acpPromptParams(sessionId, text));
226
+ const finalText = yield* Ref.get(accumulatedText);
227
+ return {
228
+ text: finalText,
229
+ stopReason: jsonStringField(result, "stopReason") ?? "Completed"
230
+ };
231
+ });
232
+ return {
233
+ events: Stream.fromPubSub(events),
234
+ newSession,
235
+ prompt,
236
+ cancel: Queue.shutdown(stdin)
237
+ };
238
+ });
239
+ //# sourceMappingURL=GeminiAcpSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiAcpSession.js","sourceRoot":"","sources":["../../src/providers/GeminiAcpSession.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/core",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Effect-native LLM contracts, providers, tools, evaluation, and observability",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,6 +46,7 @@
46
46
  "./providers/ConnectorFactories": "./dist/providers/ConnectorFactories.js",
47
47
  "./providers/CopilotConnector": "./dist/providers/CopilotConnector.js",
48
48
  "./providers/CursorConnector": "./dist/providers/CursorConnector.js",
49
+ "./providers/GeminiAcpSession": "./dist/providers/GeminiAcpSession.js",
49
50
  "./providers/GeminiApiProvider": "./dist/providers/GeminiApiProvider.js",
50
51
  "./providers/GeminiCliProvider": "./dist/providers/GeminiCliProvider.js",
51
52
  "./providers/GeminiModels": "./dist/providers/GeminiModels.js",
@@ -0,0 +1,344 @@
1
+ import * as Deferred from "effect/Deferred"
2
+ import * as Effect from "effect/Effect"
3
+ import * as PubSub from "effect/PubSub"
4
+ import * as Queue from "effect/Queue"
5
+ import * as Ref from "effect/Ref"
6
+ import type * as Scope from "effect/Scope"
7
+ import * as Stream from "effect/Stream"
8
+ import { ProviderError, type LlmError } from "../Errors.ts"
9
+ import type { ProcessExecutorShape } from "../ProcessExecutor.ts"
10
+ import {
11
+ isJsonRecord,
12
+ jsonField,
13
+ jsonStringField,
14
+ jsonText,
15
+ parseJsonLine,
16
+ type JsonRecord,
17
+ type JsonValue
18
+ } from "./CliSupport.ts"
19
+
20
+ /**
21
+ * ACP (Agent Client Protocol, `gemini --experimental-acp`) message shapes
22
+ * are best-effort against public docs (geminicli.com/docs/cli/acp-mode,
23
+ * agentclientprotocol.com) — there is no installed `gemini` binary in CI to
24
+ * verify against (the repo's CI constraint is exactly "no installed
25
+ * provider CLIs"). Tests here exercise this module against a scripted fake
26
+ * peer, the same posture ClaudeAgentSession.test.ts takes against a fake
27
+ * `claude` process. A first live run against a real `gemini` is an
28
+ * integration smoke test, not a substitute for that determinism.
29
+ */
30
+ export const geminiAcpArgv = (model: string | undefined): ReadonlyArray<string> => [
31
+ "gemini",
32
+ "--experimental-acp",
33
+ ...(model === undefined ? [] : ["-m", model])
34
+ ]
35
+
36
+ const clientCapabilities: JsonRecord = {
37
+ fs: { readTextFile: false, writeTextFile: false },
38
+ terminal: false
39
+ }
40
+
41
+ export const acpInitializeParams: JsonRecord = {
42
+ protocolVersion: 1,
43
+ clientInfo: { name: "llm4ts", version: "1" },
44
+ clientCapabilities
45
+ }
46
+
47
+ export const acpNewSessionParams = (cwd: string, mcpServerUrl: string): JsonRecord => ({
48
+ cwd,
49
+ mcpServers: [{ type: "http", url: mcpServerUrl }]
50
+ })
51
+
52
+ export const acpPromptParams = (sessionId: string, text: string): JsonRecord => ({
53
+ sessionId,
54
+ prompt: [{ type: "text", text }]
55
+ })
56
+
57
+ const jsonRpcRequest = (id: number, method: string, params: JsonRecord): string =>
58
+ jsonText({ jsonrpc: "2.0", id, method, params })
59
+
60
+ export const acpInitializeLine = (id: number): string =>
61
+ jsonRpcRequest(id, "initialize", acpInitializeParams)
62
+
63
+ export const acpNewSessionLine = (id: number, cwd: string, mcpServerUrl: string): string =>
64
+ jsonRpcRequest(id, "session/new", acpNewSessionParams(cwd, mcpServerUrl))
65
+
66
+ export const acpPromptLine = (id: number, sessionId: string, text: string): string =>
67
+ jsonRpcRequest(id, "session/prompt", acpPromptParams(sessionId, text))
68
+
69
+ export const acpPermissionResponseLine = (id: JsonValue, approved: boolean): string =>
70
+ jsonText({
71
+ jsonrpc: "2.0",
72
+ id,
73
+ result: { outcome: approved ? "Approved" : "Denied" }
74
+ })
75
+
76
+ export const acpUnsupportedFsResponseLine = (id: JsonValue): string =>
77
+ jsonText({
78
+ jsonrpc: "2.0",
79
+ id,
80
+ error: { code: -32601, message: "fs access not supported by this client" }
81
+ })
82
+
83
+ export class AcpTextChunk {
84
+ readonly _tag = "TextChunk"
85
+ constructor(readonly text: string) {}
86
+ }
87
+
88
+ export class AcpToolCallObserved {
89
+ readonly _tag = "ToolCallObserved"
90
+ constructor(
91
+ readonly id: string,
92
+ readonly name: string,
93
+ readonly input: JsonValue
94
+ ) {}
95
+ }
96
+
97
+ export class AcpPermissionRequested {
98
+ readonly _tag = "PermissionRequested"
99
+ constructor(
100
+ readonly id: string,
101
+ readonly toolCallId: string,
102
+ readonly name: string
103
+ ) {}
104
+ }
105
+
106
+ export type AcpSessionEvent = AcpTextChunk | AcpToolCallObserved | AcpPermissionRequested
107
+
108
+ export interface AcpPromptResult {
109
+ readonly text: string
110
+ readonly stopReason: string
111
+ }
112
+
113
+ export interface AcpApprovalPolicy {
114
+ readonly decide: (toolName: string, input: JsonValue) => Effect.Effect<boolean>
115
+ }
116
+
117
+ /**
118
+ * The safety decision belongs to whichever side actually executes the tool
119
+ * (pi, over the MCP bridge) — gemini's own permission prompt is a formality
120
+ * for a call this session never runs itself, so the default is to approve.
121
+ */
122
+ export const alwaysApprove: AcpApprovalPolicy = {
123
+ decide: () => Effect.succeed(true)
124
+ }
125
+
126
+ export interface GeminiAcpSessionShape {
127
+ readonly events: Stream.Stream<AcpSessionEvent, LlmError>
128
+ readonly newSession: (cwd: string, mcpServerUrl: string) => Effect.Effect<string, LlmError>
129
+ readonly prompt: (sessionId: string, text: string) => Effect.Effect<AcpPromptResult, LlmError>
130
+ readonly cancel: Effect.Effect<void>
131
+ }
132
+
133
+ const contentTextOf = (value: JsonValue | undefined): string | undefined =>
134
+ jsonStringField(jsonField(value, "content"), "text")
135
+
136
+ /** `content_chunk` and `agent_message_chunk` are both seen across ACP revisions for a text delta. */
137
+ const textChunkUpdate = (update: JsonValue | undefined): string | undefined => {
138
+ const type = jsonStringField(update, "type")
139
+ return type === "content_chunk" || type === "agent_message_chunk"
140
+ ? contentTextOf(update)
141
+ : undefined
142
+ }
143
+
144
+ const toolCallUpdate = (update: JsonValue | undefined): AcpToolCallObserved | undefined => {
145
+ if (jsonStringField(update, "type") !== "tool_call") {
146
+ return undefined
147
+ }
148
+ const toolCall = jsonField(update, "toolCall")
149
+ const id = jsonStringField(toolCall, "id")
150
+ return id === undefined
151
+ ? undefined
152
+ : new AcpToolCallObserved(
153
+ id,
154
+ jsonStringField(toolCall, "name") ?? "",
155
+ jsonField(toolCall, "input") ?? {}
156
+ )
157
+ }
158
+
159
+ export const parseAcpSessionUpdate = (
160
+ params: JsonValue | undefined
161
+ ): AcpSessionEvent | undefined => {
162
+ const update = jsonField(params, "update")
163
+ const text = textChunkUpdate(update)
164
+ return text !== undefined && text.length > 0 ? new AcpTextChunk(text) : toolCallUpdate(update)
165
+ }
166
+
167
+ export const openGeminiAcpSession = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.open")(
168
+ function* (
169
+ executor: ProcessExecutorShape,
170
+ argv: ReadonlyArray<string>,
171
+ cwd: string,
172
+ envVars: Readonly<Record<string, string>> = {},
173
+ approval: AcpApprovalPolicy = alwaysApprove
174
+ ): Effect.fn.Return<GeminiAcpSessionShape, LlmError, Scope.Scope> {
175
+ const [stdin, stdout] = yield* executor.runBidirectional(argv, cwd, envVars)
176
+ const events = yield* PubSub.unbounded<AcpSessionEvent>()
177
+ // Accumulated directly by the background reader below rather than via a
178
+ // subscription `prompt` opens on demand: a subscription opened only when
179
+ // `prompt` runs can race a canned/fast-replaying stdout (real or faked in
180
+ // tests) that the reader has already drained by then. One prompt in
181
+ // flight at a time (the single-active-session assumption) makes
182
+ // reset-then-read at each call's boundary sufficient.
183
+ const accumulatedText = yield* Ref.make("")
184
+ const nextId = yield* Ref.make(0)
185
+ const pending = yield* Ref.make<ReadonlyMap<number, Deferred.Deferred<JsonValue, LlmError>>>(
186
+ new Map()
187
+ )
188
+
189
+ const allocateId = Ref.updateAndGet(nextId, (current) => current + 1)
190
+
191
+ const registerPending = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.registerPending")(
192
+ function* (id: number): Effect.fn.Return<Deferred.Deferred<JsonValue, LlmError>> {
193
+ const deferred = yield* Deferred.make<JsonValue, LlmError>()
194
+ yield* Ref.update(pending, (current) => {
195
+ const next = new Map(current)
196
+ next.set(id, deferred)
197
+ return next
198
+ })
199
+ return deferred
200
+ }
201
+ )
202
+
203
+ const resolvePending = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.resolvePending")(
204
+ function* (id: number, result: JsonValue | undefined, error: JsonValue | undefined) {
205
+ const current = yield* Ref.get(pending)
206
+ const deferred = current.get(id)
207
+ if (deferred === undefined) {
208
+ return
209
+ }
210
+ yield* Ref.update(pending, (map) => {
211
+ const next = new Map(map)
212
+ next.delete(id)
213
+ return next
214
+ })
215
+ if (error !== undefined) {
216
+ yield* Deferred.fail(
217
+ deferred,
218
+ ProviderError.make({
219
+ message: `Gemini ACP error: ${jsonStringField(error, "message") ?? jsonText(error)}`
220
+ })
221
+ )
222
+ return
223
+ }
224
+ yield* Deferred.succeed(deferred, result ?? {})
225
+ }
226
+ )
227
+
228
+ const respondToPermission = Effect.fn(
229
+ "@llm4ts/core/providers/GeminiAcpSession.respondToPermission"
230
+ )(function* (requestId: JsonValue, toolName: string, input: JsonValue) {
231
+ const approved = yield* approval.decide(toolName, input)
232
+ yield* Queue.offer(stdin, acpPermissionResponseLine(requestId, approved))
233
+ })
234
+
235
+ const handleAgentRequest = Effect.fn(
236
+ "@llm4ts/core/providers/GeminiAcpSession.handleAgentRequest"
237
+ )(function* (id: JsonValue, method: string, params: JsonValue | undefined) {
238
+ switch (method) {
239
+ case "session/request_permission": {
240
+ const toolCall = jsonField(params, "toolCall")
241
+ const toolCallId = jsonStringField(toolCall, "id") ?? ""
242
+ const toolName = jsonStringField(toolCall, "name") ?? ""
243
+ yield* PubSub.publish(
244
+ events,
245
+ new AcpPermissionRequested(
246
+ typeof id === "string" ? id : jsonText(id),
247
+ toolCallId,
248
+ toolName
249
+ )
250
+ )
251
+ yield* respondToPermission(id, toolName, jsonField(toolCall, "input") ?? {})
252
+ return
253
+ }
254
+ case "fs/read_text_file":
255
+ case "fs/write_text_file":
256
+ yield* Queue.offer(stdin, acpUnsupportedFsResponseLine(id))
257
+ return
258
+ default:
259
+ return
260
+ }
261
+ })
262
+
263
+ const handleLine = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.handleLine")(function* (
264
+ line: string
265
+ ) {
266
+ const json = parseJsonLine(line)
267
+ if (json === undefined || !isJsonRecord(json)) {
268
+ return
269
+ }
270
+ const id = jsonField(json, "id")
271
+ const method = jsonStringField(json, "method")
272
+
273
+ if (method === "session/update") {
274
+ const event = parseAcpSessionUpdate(jsonField(json, "params"))
275
+ if (event !== undefined) {
276
+ if (event._tag === "TextChunk") {
277
+ yield* Ref.update(accumulatedText, (current) => current + event.text)
278
+ }
279
+ yield* PubSub.publish(events, event)
280
+ }
281
+ return
282
+ }
283
+
284
+ if (method !== undefined) {
285
+ if (id !== undefined) {
286
+ yield* handleAgentRequest(id, method, jsonField(json, "params"))
287
+ }
288
+ return
289
+ }
290
+
291
+ if (typeof id === "number") {
292
+ yield* resolvePending(id, jsonField(json, "result"), jsonField(json, "error"))
293
+ }
294
+ })
295
+
296
+ yield* stdout.pipe(
297
+ Stream.mapEffect(handleLine),
298
+ Stream.runDrain,
299
+ Effect.catch(() => Effect.void),
300
+ Effect.forkScoped
301
+ )
302
+
303
+ const call = Effect.fn("@llm4ts/core/providers/GeminiAcpSession.call")(function* (
304
+ method: string,
305
+ params: JsonRecord
306
+ ): Effect.fn.Return<JsonValue, LlmError> {
307
+ const id = yield* allocateId
308
+ const deferred = yield* registerPending(id)
309
+ yield* Queue.offer(stdin, jsonRpcRequest(id, method, params))
310
+ return yield* Deferred.await(deferred)
311
+ })
312
+
313
+ const newSession = (cwd: string, mcpServerUrl: string): Effect.Effect<string, LlmError> =>
314
+ Effect.gen(function* () {
315
+ yield* call("initialize", acpInitializeParams)
316
+ const result = yield* call("session/new", acpNewSessionParams(cwd, mcpServerUrl))
317
+ const sessionId = jsonStringField(result, "sessionId")
318
+ if (sessionId === undefined) {
319
+ return yield* ProviderError.make({
320
+ message: "Gemini ACP session/new did not return a sessionId"
321
+ })
322
+ }
323
+ return sessionId
324
+ })
325
+
326
+ const prompt = (sessionId: string, text: string): Effect.Effect<AcpPromptResult, LlmError> =>
327
+ Effect.gen(function* () {
328
+ yield* Ref.set(accumulatedText, "")
329
+ const result = yield* call("session/prompt", acpPromptParams(sessionId, text))
330
+ const finalText = yield* Ref.get(accumulatedText)
331
+ return {
332
+ text: finalText,
333
+ stopReason: jsonStringField(result, "stopReason") ?? "Completed"
334
+ }
335
+ })
336
+
337
+ return {
338
+ events: Stream.fromPubSub(events),
339
+ newSession,
340
+ prompt,
341
+ cancel: Queue.shutdown(stdin)
342
+ }
343
+ }
344
+ )