@junctionpanel/server 0.1.31 → 0.1.32
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/server/client/daemon-client.d.ts +1 -0
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +27 -0
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +2 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +63 -4
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
- package/dist/server/server/agent/agent-projections.js +9 -2
- package/dist/server/server/agent/agent-projections.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +13 -2
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +30 -30
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.js +33 -1
- package/dist/server/server/agent/agent-storage.js.map +1 -1
- package/dist/server/server/agent/codex-config.d.ts +12 -0
- package/dist/server/server/agent/codex-config.d.ts.map +1 -0
- package/dist/server/server/agent/codex-config.js +42 -0
- package/dist/server/server/agent/codex-config.js.map +1 -0
- package/dist/server/server/agent/mcp-server.js +8 -8
- package/dist/server/server/agent/mcp-server.js.map +1 -1
- package/dist/server/server/agent/provider-launch-config.d.ts +2 -2
- package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -1
- package/dist/server/server/agent/provider-launch-config.js +32 -5
- package/dist/server/server/agent/provider-launch-config.js.map +1 -1
- package/dist/server/server/agent/provider-manifest.js +10 -10
- package/dist/server/server/agent/provider-manifest.js.map +1 -1
- package/dist/server/server/agent/providers/claude/model-catalog.d.ts +17 -25
- package/dist/server/server/agent/providers/claude/model-catalog.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/model-catalog.js +228 -40
- package/dist/server/server/agent/providers/claude/model-catalog.js.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts +2 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.js +201 -36
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +30 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +309 -49
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.d.ts +17 -5
- package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.js +1040 -482
- package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/session.d.ts +1 -0
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +35 -0
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/shared/messages.d.ts +1550 -1298
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +19 -0
- package/dist/server/shared/messages.js.map +1 -1
- package/package.json +3 -2
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
2
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import
|
|
6
|
+
import { Readable as NodeReadable, Writable as NodeWritable } from "node:stream";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { applyProviderEnv, isProviderCommandAvailable, resolveProviderCommandPrefix, } from "../provider-launch-config.js";
|
|
8
|
-
import { writeImageAttachment } from "./image-attachments.js";
|
|
9
9
|
import { ToolEditInputSchema, ToolEditOutputSchema, ToolReadInputSchema, ToolReadOutputSchema, ToolSearchInputSchema, ToolShellInputSchema, ToolShellOutputSchema, ToolWriteInputSchema, ToolWriteOutputSchema, toEditToolDetail, toReadToolDetail, toSearchToolDetail, toShellToolDetail, toWriteToolDetail, } from "./tool-call-detail-primitives.js";
|
|
10
10
|
import { coerceToolCallId, nonEmptyString } from "./tool-call-mapper-utils.js";
|
|
11
11
|
const GEMINI_PROVIDER = "gemini";
|
|
12
12
|
const GEMINI_MODES = ["default", "auto_edit", "yolo", "plan"];
|
|
13
13
|
const GEMINI_GLOBAL_DIR = path.join(homedir(), ".gemini");
|
|
14
14
|
const GEMINI_TMP_DIR = path.join(GEMINI_GLOBAL_DIR, "tmp");
|
|
15
|
+
const GEMINI_HISTORY_FALLBACK_IDLE_MS = 1000;
|
|
15
16
|
const GEMINI_CAPABILITIES = {
|
|
16
17
|
supportsStreaming: true,
|
|
17
18
|
supportsSessionPersistence: true,
|
|
18
19
|
supportsDynamicModes: true,
|
|
19
20
|
supportsMcpServers: true,
|
|
20
|
-
supportsReasoningStream:
|
|
21
|
+
supportsReasoningStream: true,
|
|
21
22
|
supportsToolInvocations: true,
|
|
22
23
|
};
|
|
23
24
|
const DEFAULT_GEMINI_MODE = "default";
|
|
@@ -91,59 +92,6 @@ const GEMINI_RUNTIME_MODES = [
|
|
|
91
92
|
description: "Read-only planning mode",
|
|
92
93
|
},
|
|
93
94
|
];
|
|
94
|
-
const GeminiStreamInitSchema = z.object({
|
|
95
|
-
type: z.literal("init"),
|
|
96
|
-
session_id: z.string().trim().min(1),
|
|
97
|
-
model: z.string().trim().min(1).optional(),
|
|
98
|
-
});
|
|
99
|
-
const GeminiStreamMessageSchema = z.object({
|
|
100
|
-
type: z.literal("message"),
|
|
101
|
-
role: z.enum(["user", "assistant"]),
|
|
102
|
-
content: z.string(),
|
|
103
|
-
delta: z.boolean().optional(),
|
|
104
|
-
});
|
|
105
|
-
const GeminiStreamToolUseSchema = z.object({
|
|
106
|
-
type: z.literal("tool_use"),
|
|
107
|
-
tool_name: z.string().trim().min(1),
|
|
108
|
-
tool_id: z.string().trim().min(1),
|
|
109
|
-
parameters: z.unknown().optional(),
|
|
110
|
-
});
|
|
111
|
-
const GeminiStreamToolResultSchema = z.object({
|
|
112
|
-
type: z.literal("tool_result"),
|
|
113
|
-
tool_id: z.string().trim().min(1),
|
|
114
|
-
status: z.string().trim().min(1),
|
|
115
|
-
output: z.unknown().optional(),
|
|
116
|
-
error: z.unknown().optional(),
|
|
117
|
-
});
|
|
118
|
-
const GeminiStreamErrorSchema = z.object({
|
|
119
|
-
type: z.literal("error"),
|
|
120
|
-
severity: z.string().optional(),
|
|
121
|
-
message: z.string().trim().min(1),
|
|
122
|
-
});
|
|
123
|
-
const GeminiStreamResultSchema = z.object({
|
|
124
|
-
type: z.literal("result"),
|
|
125
|
-
status: z.string().trim().min(1),
|
|
126
|
-
stats: z
|
|
127
|
-
.object({
|
|
128
|
-
total_tokens: z.number().finite().optional(),
|
|
129
|
-
input_tokens: z.number().finite().optional(),
|
|
130
|
-
output_tokens: z.number().finite().optional(),
|
|
131
|
-
cached: z.number().finite().optional(),
|
|
132
|
-
input: z.number().finite().optional(),
|
|
133
|
-
duration_ms: z.number().finite().optional(),
|
|
134
|
-
tool_calls: z.number().finite().optional(),
|
|
135
|
-
})
|
|
136
|
-
.passthrough()
|
|
137
|
-
.optional(),
|
|
138
|
-
});
|
|
139
|
-
const GeminiStreamEventSchema = z.discriminatedUnion("type", [
|
|
140
|
-
GeminiStreamInitSchema,
|
|
141
|
-
GeminiStreamMessageSchema,
|
|
142
|
-
GeminiStreamToolUseSchema,
|
|
143
|
-
GeminiStreamToolResultSchema,
|
|
144
|
-
GeminiStreamErrorSchema,
|
|
145
|
-
GeminiStreamResultSchema,
|
|
146
|
-
]);
|
|
147
95
|
const GeminiRecordedThoughtSchema = z
|
|
148
96
|
.object({
|
|
149
97
|
thought: z.string().optional(),
|
|
@@ -180,6 +128,54 @@ const GeminiRecordedSessionSchema = z
|
|
|
180
128
|
messages: z.array(GeminiRecordedMessageSchema),
|
|
181
129
|
})
|
|
182
130
|
.passthrough();
|
|
131
|
+
class AsyncEventQueue {
|
|
132
|
+
constructor() {
|
|
133
|
+
this.items = [];
|
|
134
|
+
this.waiters = [];
|
|
135
|
+
this.closed = false;
|
|
136
|
+
this.error = null;
|
|
137
|
+
}
|
|
138
|
+
push(item) {
|
|
139
|
+
if (this.closed) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.items.push(item);
|
|
143
|
+
this.waiters.shift()?.();
|
|
144
|
+
}
|
|
145
|
+
close() {
|
|
146
|
+
if (this.closed) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.closed = true;
|
|
150
|
+
for (const notify of this.waiters.splice(0)) {
|
|
151
|
+
notify();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
fail(error) {
|
|
155
|
+
if (this.closed) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.error = error;
|
|
159
|
+
this.closed = true;
|
|
160
|
+
for (const notify of this.waiters.splice(0)) {
|
|
161
|
+
notify();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async shift() {
|
|
165
|
+
while (this.items.length === 0) {
|
|
166
|
+
if (this.closed) {
|
|
167
|
+
if (this.error) {
|
|
168
|
+
throw this.error;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
this.waiters.push(resolve);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return this.items.shift() ?? null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
183
179
|
function resolveGeminiBinary() {
|
|
184
180
|
try {
|
|
185
181
|
const geminiPath = execSync("which gemini", { encoding: "utf8" }).trim();
|
|
@@ -192,6 +188,56 @@ function resolveGeminiBinary() {
|
|
|
192
188
|
}
|
|
193
189
|
throw new Error("Gemini CLI not found. Please install gemini globally so Junction can launch the provider.");
|
|
194
190
|
}
|
|
191
|
+
function assertGeminiAcpSupport(runtimeSettings) {
|
|
192
|
+
const launchPrefix = resolveProviderCommandPrefix(runtimeSettings?.command, resolveGeminiBinary);
|
|
193
|
+
const env = applyProviderEnv(process.env, runtimeSettings);
|
|
194
|
+
const result = spawnSync(launchPrefix.command, [...launchPrefix.args, "--help"], {
|
|
195
|
+
env,
|
|
196
|
+
encoding: "utf8",
|
|
197
|
+
});
|
|
198
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
199
|
+
if (output.includes("--acp")) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
throw new Error("Installed Gemini CLI does not support --acp. Upgrade Gemini CLI to a version that includes ACP support.");
|
|
203
|
+
}
|
|
204
|
+
function stringifyUnknown(value) {
|
|
205
|
+
if (typeof value === "string") {
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
if (value instanceof Error) {
|
|
209
|
+
return value.message;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
return JSON.stringify(value);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return String(value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function asRecord(value) {
|
|
219
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function formatGeminiAcpError(error) {
|
|
225
|
+
if (error instanceof acp.RequestError) {
|
|
226
|
+
const directMessage = nonEmptyString(error.message);
|
|
227
|
+
const dataRecord = asRecord(error.data);
|
|
228
|
+
const nestedMessage = nonEmptyString(asRecord(dataRecord?.error)?.message) ??
|
|
229
|
+
nonEmptyString(dataRecord?.message) ??
|
|
230
|
+
null;
|
|
231
|
+
const message = nestedMessage ?? directMessage ?? "Gemini ACP request failed";
|
|
232
|
+
if (error.code === -32002 ||
|
|
233
|
+
message.toLowerCase().includes("authentication required")) {
|
|
234
|
+
return "Gemini CLI authentication is required. Run `gemini` locally and sign in before using Gemini in Junction.";
|
|
235
|
+
}
|
|
236
|
+
return message;
|
|
237
|
+
}
|
|
238
|
+
const message = nonEmptyString(stringifyUnknown(error));
|
|
239
|
+
return message ?? "Gemini ACP request failed";
|
|
240
|
+
}
|
|
195
241
|
export function normalizeGeminiMode(modeId) {
|
|
196
242
|
if (!modeId) {
|
|
197
243
|
return DEFAULT_GEMINI_MODE;
|
|
@@ -204,62 +250,35 @@ export function normalizeGeminiMode(modeId) {
|
|
|
204
250
|
}
|
|
205
251
|
throw new Error(`Unknown Gemini mode '${modeId}'. Valid modes: ${GEMINI_MODES.join(", ")}`);
|
|
206
252
|
}
|
|
207
|
-
|
|
208
|
-
export async function normalizeGeminiPromptInput(prompt, options) {
|
|
209
|
-
if (typeof prompt === "string") {
|
|
210
|
-
return prompt;
|
|
211
|
-
}
|
|
212
|
-
const writeAttachment = options?.writeImageAttachment ?? writeImageAttachment;
|
|
213
|
-
const textBlocks = [];
|
|
214
|
-
const imageReferences = [];
|
|
215
|
-
for (const block of prompt) {
|
|
216
|
-
if (block.type === "text") {
|
|
217
|
-
textBlocks.push(block.text);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
const filePath = await writeAttachment(block.mimeType, block.data);
|
|
222
|
-
imageReferences.push(`@${filePath}`);
|
|
223
|
-
}
|
|
224
|
-
catch (error) {
|
|
225
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
226
|
-
throw new Error(`Failed to prepare Gemini image attachment: ${message}`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
const sections = [];
|
|
230
|
-
if (textBlocks.length > 0) {
|
|
231
|
-
sections.push(textBlocks.join("\n\n"));
|
|
232
|
-
}
|
|
233
|
-
else if (imageReferences.length > 0) {
|
|
234
|
-
sections.push(GEMINI_IMAGE_ONLY_PROMPT);
|
|
235
|
-
}
|
|
236
|
-
if (imageReferences.length > 0) {
|
|
237
|
-
sections.push([
|
|
238
|
-
imageReferences.length === 1
|
|
239
|
-
? "Attached image file:"
|
|
240
|
-
: "Attached image files:",
|
|
241
|
-
...imageReferences,
|
|
242
|
-
].join("\n"));
|
|
243
|
-
}
|
|
244
|
-
return sections.join("\n\n");
|
|
245
|
-
}
|
|
246
|
-
function mapGeminiUsage(stats) {
|
|
247
|
-
if (!stats) {
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
253
|
+
export function getRequestedGeminiSessionState(config) {
|
|
250
254
|
return {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
modeId: config.modeId ? normalizeGeminiMode(config.modeId) : null,
|
|
256
|
+
modelId: typeof config.model === "string" && config.model.trim().length > 0
|
|
257
|
+
? config.model.trim()
|
|
258
|
+
: null,
|
|
254
259
|
};
|
|
255
260
|
}
|
|
261
|
+
export function toGeminiPromptBlocks(prompt) {
|
|
262
|
+
if (typeof prompt === "string") {
|
|
263
|
+
return [{ type: "text", text: prompt }];
|
|
264
|
+
}
|
|
265
|
+
return prompt.map((block) => block.type === "text"
|
|
266
|
+
? { type: "text", text: block.text }
|
|
267
|
+
: {
|
|
268
|
+
type: "image",
|
|
269
|
+
data: block.data,
|
|
270
|
+
mimeType: block.mimeType,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
256
273
|
function normalizeToolStatus(status) {
|
|
257
274
|
const normalized = status?.trim().toLowerCase() ?? "";
|
|
258
|
-
if (normalized === "
|
|
275
|
+
if (normalized === "completed" ||
|
|
276
|
+
normalized === "success" ||
|
|
277
|
+
normalized === "done") {
|
|
259
278
|
return "completed";
|
|
260
279
|
}
|
|
261
|
-
if (normalized === "
|
|
262
|
-
normalized === "
|
|
280
|
+
if (normalized === "failed" ||
|
|
281
|
+
normalized === "error" ||
|
|
263
282
|
normalized === "failure") {
|
|
264
283
|
return "failed";
|
|
265
284
|
}
|
|
@@ -271,59 +290,122 @@ function normalizeToolStatus(status) {
|
|
|
271
290
|
}
|
|
272
291
|
return "running";
|
|
273
292
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
type
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
293
|
+
function toolCallContentToText(content) {
|
|
294
|
+
if (!content || content.length === 0) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const segments = [];
|
|
298
|
+
for (const part of content) {
|
|
299
|
+
if (part.type === "content" && part.content.type === "text") {
|
|
300
|
+
const text = part.content.text.trim();
|
|
301
|
+
if (text.length > 0) {
|
|
302
|
+
segments.push(text);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (segments.length === 0) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return segments.join("\n\n");
|
|
284
310
|
}
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
311
|
+
function detailFromAcpDiffContent(content) {
|
|
312
|
+
if (!content) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
for (const part of content) {
|
|
316
|
+
if (part.type !== "diff") {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
type: "edit",
|
|
321
|
+
filePath: part.path,
|
|
322
|
+
...(typeof part.oldText === "string" ? { oldString: part.oldText } : {}),
|
|
323
|
+
newString: part.newText,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
function deriveGeminiToolDetail(toolName, input, output, options) {
|
|
329
|
+
const fromDiff = detailFromAcpDiffContent(options?.content);
|
|
330
|
+
if (fromDiff) {
|
|
331
|
+
return fromDiff;
|
|
332
|
+
}
|
|
333
|
+
const normalizedKind = options?.kind?.trim().toLowerCase() ?? "";
|
|
334
|
+
const normalizedName = toolName.trim().toLowerCase();
|
|
335
|
+
const contentText = toolCallContentToText(options?.content);
|
|
336
|
+
const locationPath = options?.locations?.[0]?.path;
|
|
337
|
+
if (normalizedKind === "execute" ||
|
|
338
|
+
normalizedName === "run_shell_command" ||
|
|
339
|
+
normalizedName === "shell" ||
|
|
340
|
+
normalizedName === "bash" ||
|
|
341
|
+
normalizedName === "exec_command") {
|
|
291
342
|
const parsedInput = ToolShellInputSchema.safeParse(input);
|
|
292
343
|
const parsedOutput = ToolShellOutputSchema.safeParse(output);
|
|
293
|
-
return (toShellToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ??
|
|
344
|
+
return (toShellToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? {
|
|
345
|
+
type: "shell",
|
|
346
|
+
command: toolName,
|
|
347
|
+
...(contentText ? { output: contentText } : {}),
|
|
348
|
+
});
|
|
294
349
|
}
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
350
|
+
if (normalizedKind === "read" ||
|
|
351
|
+
normalizedName === "read_file" ||
|
|
352
|
+
normalizedName === "read" ||
|
|
353
|
+
normalizedName === "view") {
|
|
298
354
|
const parsedInput = ToolReadInputSchema.safeParse(input);
|
|
299
355
|
const parsedOutput = ToolReadOutputSchema.safeParse(output);
|
|
300
|
-
return (toReadToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ??
|
|
356
|
+
return (toReadToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? {
|
|
357
|
+
type: "read",
|
|
358
|
+
filePath: locationPath ?? toolName,
|
|
359
|
+
...(contentText ? { content: contentText } : {}),
|
|
360
|
+
});
|
|
301
361
|
}
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
362
|
+
if (normalizedKind === "edit" ||
|
|
363
|
+
normalizedKind === "delete" ||
|
|
364
|
+
normalizedKind === "move" ||
|
|
365
|
+
normalizedName === "replace" ||
|
|
366
|
+
normalizedName === "edit" ||
|
|
367
|
+
normalizedName === "apply_patch" ||
|
|
368
|
+
normalizedName === "apply_diff") {
|
|
369
|
+
const parsedInput = ToolEditInputSchema.safeParse(input);
|
|
370
|
+
const parsedOutput = ToolEditOutputSchema.safeParse(output);
|
|
371
|
+
return (toEditToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? {
|
|
372
|
+
type: "edit",
|
|
373
|
+
filePath: locationPath ?? toolName,
|
|
374
|
+
...(contentText ? { unifiedDiff: contentText } : {}),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (normalizedName === "write_file" ||
|
|
378
|
+
normalizedName === "write" ||
|
|
379
|
+
normalizedName === "create_file") {
|
|
305
380
|
const parsedInput = ToolWriteInputSchema.safeParse(input);
|
|
306
381
|
const parsedOutput = ToolWriteOutputSchema.safeParse(output);
|
|
307
|
-
return (toWriteToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ??
|
|
382
|
+
return (toWriteToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? {
|
|
383
|
+
type: "write",
|
|
384
|
+
filePath: locationPath ?? toolName,
|
|
385
|
+
...(contentText ? { content: contentText } : {}),
|
|
386
|
+
});
|
|
308
387
|
}
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
normalized === "glob" ||
|
|
319
|
-
normalized === "search_text" ||
|
|
320
|
-
normalized === "search" ||
|
|
321
|
-
normalized === "web_search" ||
|
|
322
|
-
normalized === "google_search" ||
|
|
323
|
-
normalized === "web_fetch") {
|
|
388
|
+
if (normalizedKind === "search" ||
|
|
389
|
+
normalizedKind === "fetch" ||
|
|
390
|
+
normalizedName === "grep" ||
|
|
391
|
+
normalizedName === "glob" ||
|
|
392
|
+
normalizedName === "search_text" ||
|
|
393
|
+
normalizedName === "search" ||
|
|
394
|
+
normalizedName === "web_search" ||
|
|
395
|
+
normalizedName === "google_search" ||
|
|
396
|
+
normalizedName === "web_fetch") {
|
|
324
397
|
const parsedInput = ToolSearchInputSchema.safeParse(input);
|
|
325
|
-
return (toSearchToolDetail(parsedInput.success ? parsedInput.data : null) ??
|
|
326
|
-
|
|
398
|
+
return (toSearchToolDetail(parsedInput.success ? parsedInput.data : null) ?? {
|
|
399
|
+
type: "search",
|
|
400
|
+
query: toolName,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (contentText) {
|
|
404
|
+
return {
|
|
405
|
+
type: "plain_text",
|
|
406
|
+
label: toolName,
|
|
407
|
+
text: contentText,
|
|
408
|
+
};
|
|
327
409
|
}
|
|
328
410
|
return {
|
|
329
411
|
type: "unknown",
|
|
@@ -339,7 +421,11 @@ export function toGeminiToolTimelineItem(params) {
|
|
|
339
421
|
input: params.input,
|
|
340
422
|
});
|
|
341
423
|
const status = normalizeToolStatus(params.status);
|
|
342
|
-
const detail = deriveGeminiToolDetail(params.toolName, params.input ?? null, params.output ?? null
|
|
424
|
+
const detail = deriveGeminiToolDetail(params.toolName, params.input ?? null, params.output ?? null, {
|
|
425
|
+
kind: params.kind,
|
|
426
|
+
content: params.content ?? null,
|
|
427
|
+
locations: params.locations ?? null,
|
|
428
|
+
});
|
|
343
429
|
if (status === "failed") {
|
|
344
430
|
return {
|
|
345
431
|
type: "tool_call",
|
|
@@ -348,6 +434,9 @@ export function toGeminiToolTimelineItem(params) {
|
|
|
348
434
|
status,
|
|
349
435
|
detail,
|
|
350
436
|
error: params.error ?? { message: "Tool call failed" },
|
|
437
|
+
metadata: {
|
|
438
|
+
...(params.kind ? { kind: params.kind } : {}),
|
|
439
|
+
},
|
|
351
440
|
};
|
|
352
441
|
}
|
|
353
442
|
return {
|
|
@@ -357,6 +446,9 @@ export function toGeminiToolTimelineItem(params) {
|
|
|
357
446
|
status,
|
|
358
447
|
detail,
|
|
359
448
|
error: null,
|
|
449
|
+
metadata: {
|
|
450
|
+
...(params.kind ? { kind: params.kind } : {}),
|
|
451
|
+
},
|
|
360
452
|
};
|
|
361
453
|
}
|
|
362
454
|
function toGeminiModelDefinitions() {
|
|
@@ -365,66 +457,9 @@ function toGeminiModelDefinitions() {
|
|
|
365
457
|
id: model.id,
|
|
366
458
|
label: model.label,
|
|
367
459
|
description: model.description,
|
|
368
|
-
...("isDefault" in model && model.isDefault
|
|
460
|
+
...("isDefault" in model && model.isDefault ? { isDefault: true } : {}),
|
|
369
461
|
}));
|
|
370
462
|
}
|
|
371
|
-
async function resolveProjectTempDirForCwd(cwd) {
|
|
372
|
-
const normalizedCwd = await normalizeComparablePath(cwd);
|
|
373
|
-
let entries;
|
|
374
|
-
try {
|
|
375
|
-
entries = await readdir(GEMINI_TMP_DIR);
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
for (const entry of entries) {
|
|
381
|
-
const candidateDir = path.join(GEMINI_TMP_DIR, entry);
|
|
382
|
-
const markerPath = path.join(candidateDir, ".project_root");
|
|
383
|
-
try {
|
|
384
|
-
const marker = (await readFile(markerPath, "utf8")).trim();
|
|
385
|
-
const normalizedMarker = await normalizeComparablePath(marker);
|
|
386
|
-
if (normalizedMarker === normalizedCwd) {
|
|
387
|
-
return candidateDir;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
// ignore non-project entries
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
async function listKnownGeminiProjects() {
|
|
397
|
-
let entries;
|
|
398
|
-
try {
|
|
399
|
-
entries = await readdir(GEMINI_TMP_DIR);
|
|
400
|
-
}
|
|
401
|
-
catch {
|
|
402
|
-
return [];
|
|
403
|
-
}
|
|
404
|
-
const projects = [];
|
|
405
|
-
for (const entry of entries) {
|
|
406
|
-
const tempDir = path.join(GEMINI_TMP_DIR, entry);
|
|
407
|
-
const markerPath = path.join(tempDir, ".project_root");
|
|
408
|
-
try {
|
|
409
|
-
const cwd = (await readFile(markerPath, "utf8")).trim();
|
|
410
|
-
if (cwd.length > 0) {
|
|
411
|
-
projects.push({ cwd, tempDir });
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
catch {
|
|
415
|
-
// ignore
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return projects;
|
|
419
|
-
}
|
|
420
|
-
async function normalizeComparablePath(inputPath) {
|
|
421
|
-
try {
|
|
422
|
-
return await realpath(inputPath);
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
return path.resolve(inputPath);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
463
|
async function readGeminiSessionFile(filePath) {
|
|
429
464
|
try {
|
|
430
465
|
const raw = await readFile(filePath, "utf8");
|
|
@@ -479,6 +514,19 @@ function getGeminiSessionTitle(session) {
|
|
|
479
514
|
}
|
|
480
515
|
return null;
|
|
481
516
|
}
|
|
517
|
+
function extractRecordedThoughtText(thought) {
|
|
518
|
+
const directText = nonEmptyString(thought.text) ?? nonEmptyString(thought.thought);
|
|
519
|
+
if (directText) {
|
|
520
|
+
return directText;
|
|
521
|
+
}
|
|
522
|
+
const record = thought;
|
|
523
|
+
const subject = nonEmptyString(record.subject);
|
|
524
|
+
const description = nonEmptyString(record.description);
|
|
525
|
+
if (subject && description) {
|
|
526
|
+
return `**${subject}**\n${description}`;
|
|
527
|
+
}
|
|
528
|
+
return subject ?? description ?? "";
|
|
529
|
+
}
|
|
482
530
|
export function buildGeminiHistoryTimeline(session) {
|
|
483
531
|
const items = [];
|
|
484
532
|
for (const message of session.messages) {
|
|
@@ -501,7 +549,7 @@ export function buildGeminiHistoryTimeline(session) {
|
|
|
501
549
|
items.push({ type: "assistant_message", text: assistantText });
|
|
502
550
|
}
|
|
503
551
|
for (const thought of message.thoughts ?? []) {
|
|
504
|
-
const text =
|
|
552
|
+
const text = extractRecordedThoughtText(thought);
|
|
505
553
|
if (text) {
|
|
506
554
|
items.push({ type: "reasoning", text });
|
|
507
555
|
}
|
|
@@ -522,6 +570,58 @@ export function buildGeminiHistoryTimeline(session) {
|
|
|
522
570
|
}
|
|
523
571
|
return items;
|
|
524
572
|
}
|
|
573
|
+
async function listKnownGeminiProjects() {
|
|
574
|
+
let entries;
|
|
575
|
+
try {
|
|
576
|
+
entries = await readdir(GEMINI_TMP_DIR);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
const projects = [];
|
|
582
|
+
for (const entry of entries) {
|
|
583
|
+
const tempDir = path.join(GEMINI_TMP_DIR, entry);
|
|
584
|
+
const markerPath = path.join(tempDir, ".project_root");
|
|
585
|
+
try {
|
|
586
|
+
const cwd = (await readFile(markerPath, "utf8")).trim();
|
|
587
|
+
if (cwd.length > 0) {
|
|
588
|
+
projects.push({ cwd, tempDir });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
// ignore
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return projects;
|
|
596
|
+
}
|
|
597
|
+
async function resolveGeminiProjectTempDirForCwd(cwd) {
|
|
598
|
+
const projects = await listKnownGeminiProjects();
|
|
599
|
+
return projects.find((project) => project.cwd === cwd)?.tempDir ?? null;
|
|
600
|
+
}
|
|
601
|
+
async function loadGeminiRecordedSessionForCwd(cwd, sessionId) {
|
|
602
|
+
const tempDir = await resolveGeminiProjectTempDirForCwd(cwd);
|
|
603
|
+
if (!tempDir) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
const chatsDir = path.join(tempDir, "chats");
|
|
607
|
+
let chatFiles;
|
|
608
|
+
try {
|
|
609
|
+
chatFiles = await readdir(chatsDir);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
for (const fileName of chatFiles) {
|
|
615
|
+
if (!fileName.startsWith("session-") || !fileName.endsWith(".json")) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const session = await readGeminiSessionFile(path.join(chatsDir, fileName));
|
|
619
|
+
if (session?.sessionId === sessionId) {
|
|
620
|
+
return session;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
525
625
|
async function listGeminiSessionsForProject(params) {
|
|
526
626
|
const chatsDir = path.join(params.tempDir, "chats");
|
|
527
627
|
let chatFiles;
|
|
@@ -560,41 +660,281 @@ async function listGeminiSessionsForProject(params) {
|
|
|
560
660
|
});
|
|
561
661
|
}
|
|
562
662
|
return sessions
|
|
563
|
-
.sort((
|
|
663
|
+
.sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
|
|
564
664
|
.slice(0, params.limit ?? sessions.length);
|
|
565
665
|
}
|
|
566
|
-
function
|
|
666
|
+
function toAcpHeaders(headers) {
|
|
667
|
+
if (!headers) {
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
return Object.entries(headers).map(([name, value]) => ({ name, value }));
|
|
671
|
+
}
|
|
672
|
+
function toAcpEnvVars(env) {
|
|
673
|
+
if (!env) {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
return Object.entries(env).map(([name, value]) => ({ name, value }));
|
|
677
|
+
}
|
|
678
|
+
function toGeminiAcpMcpServer(name, config) {
|
|
567
679
|
if (config.type === "stdio") {
|
|
568
680
|
return {
|
|
681
|
+
name,
|
|
569
682
|
command: config.command,
|
|
570
|
-
|
|
571
|
-
|
|
683
|
+
args: config.args ?? [],
|
|
684
|
+
env: toAcpEnvVars(config.env),
|
|
572
685
|
};
|
|
573
686
|
}
|
|
574
687
|
return {
|
|
575
688
|
type: config.type,
|
|
689
|
+
name,
|
|
576
690
|
url: config.url,
|
|
577
|
-
|
|
691
|
+
headers: toAcpHeaders(config.headers),
|
|
578
692
|
};
|
|
579
693
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
overlay.experimental = { plan: true };
|
|
694
|
+
function toGeminiAcpMcpServers(mcpServers) {
|
|
695
|
+
if (!mcpServers) {
|
|
696
|
+
return [];
|
|
584
697
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
698
|
+
return Object.entries(mcpServers).map(([name, config]) => toGeminiAcpMcpServer(name, config));
|
|
699
|
+
}
|
|
700
|
+
function toAgentMode(mode) {
|
|
701
|
+
return {
|
|
702
|
+
id: mode.id,
|
|
703
|
+
label: mode.name,
|
|
704
|
+
...(nonEmptyString(mode.description) ? { description: mode.description } : {}),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function toAgentModelDefinition(model) {
|
|
708
|
+
return {
|
|
709
|
+
provider: GEMINI_PROVIDER,
|
|
710
|
+
id: model.modelId,
|
|
711
|
+
label: model.name,
|
|
712
|
+
...(nonEmptyString(model.description) ? { description: model.description } : {}),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function toolCallTitleToInteractionSubtype(title) {
|
|
716
|
+
const normalized = title?.trim() ?? "";
|
|
717
|
+
if (normalized.startsWith("Asking user:")) {
|
|
718
|
+
return "ask_user_unsupported";
|
|
590
719
|
}
|
|
591
|
-
if (
|
|
720
|
+
if (normalized.startsWith("Requesting plan approval for:")) {
|
|
721
|
+
return "exit_plan_mode_unsupported";
|
|
722
|
+
}
|
|
723
|
+
return "tool";
|
|
724
|
+
}
|
|
725
|
+
function extractGeminiPlanPath(title) {
|
|
726
|
+
const normalized = title?.trim() ?? "";
|
|
727
|
+
const prefix = "Requesting plan approval for:";
|
|
728
|
+
if (!normalized.startsWith(prefix)) {
|
|
592
729
|
return null;
|
|
593
730
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
return
|
|
731
|
+
return nonEmptyString(normalized.slice(prefix.length).trim()) ?? null;
|
|
732
|
+
}
|
|
733
|
+
export function classifyGeminiPermissionRequest(params) {
|
|
734
|
+
return toolCallTitleToInteractionSubtype(params.toolCall.title);
|
|
735
|
+
}
|
|
736
|
+
export function toGeminiPermissionRequest(params) {
|
|
737
|
+
const interactionSubtype = classifyGeminiPermissionRequest(params);
|
|
738
|
+
const toolCallTitle = nonEmptyString(params.toolCall.title) ?? params.toolCall.toolCallId;
|
|
739
|
+
const optionMetadata = params.options.map((option) => ({
|
|
740
|
+
optionId: option.optionId,
|
|
741
|
+
name: option.name,
|
|
742
|
+
kind: option.kind,
|
|
743
|
+
}));
|
|
744
|
+
const planPath = extractGeminiPlanPath(toolCallTitle);
|
|
745
|
+
const detail = toGeminiToolTimelineItem({
|
|
746
|
+
toolName: toolCallTitle,
|
|
747
|
+
callId: params.toolCall.toolCallId,
|
|
748
|
+
status: params.toolCall.status,
|
|
749
|
+
input: params.toolCall.rawInput,
|
|
750
|
+
output: params.toolCall.rawOutput,
|
|
751
|
+
kind: params.toolCall.kind,
|
|
752
|
+
content: params.toolCall.content ?? null,
|
|
753
|
+
locations: params.toolCall.locations ?? null,
|
|
754
|
+
}).detail;
|
|
755
|
+
const metadata = {
|
|
756
|
+
geminiInteractionSubtype: interactionSubtype,
|
|
757
|
+
permissionOptions: optionMetadata,
|
|
758
|
+
toolKind: params.toolCall.kind ?? null,
|
|
759
|
+
toolCallId: params.toolCall.toolCallId,
|
|
760
|
+
toolCallTitle,
|
|
761
|
+
toolCallContent: params.toolCall.content ?? [],
|
|
762
|
+
toolCallLocations: params.toolCall.locations ?? [],
|
|
763
|
+
...(planPath ? { geminiPlanPath: planPath } : {}),
|
|
764
|
+
};
|
|
765
|
+
if (params.toolCall.rawInput !== undefined) {
|
|
766
|
+
metadata.toolCallRawInput = params.toolCall.rawInput;
|
|
767
|
+
}
|
|
768
|
+
if (params.toolCall.rawOutput !== undefined) {
|
|
769
|
+
metadata.toolCallRawOutput = params.toolCall.rawOutput;
|
|
770
|
+
}
|
|
771
|
+
if (interactionSubtype === "ask_user_unsupported") {
|
|
772
|
+
return {
|
|
773
|
+
id: params.toolCall.toolCallId,
|
|
774
|
+
provider: GEMINI_PROVIDER,
|
|
775
|
+
name: "ask_user",
|
|
776
|
+
kind: "other",
|
|
777
|
+
title: "Gemini needs answers from ask_user",
|
|
778
|
+
description: "Gemini ACP does not include the payload needed to answer ask_user prompts from Junction.",
|
|
779
|
+
detail,
|
|
780
|
+
metadata,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (interactionSubtype === "exit_plan_mode_unsupported") {
|
|
784
|
+
return {
|
|
785
|
+
id: params.toolCall.toolCallId,
|
|
786
|
+
provider: GEMINI_PROVIDER,
|
|
787
|
+
name: "exit_plan_mode",
|
|
788
|
+
kind: "other",
|
|
789
|
+
title: "Gemini requested plan approval",
|
|
790
|
+
description: "Gemini ACP does not include the payload needed to approve or revise exit_plan_mode from Junction.",
|
|
791
|
+
detail,
|
|
792
|
+
metadata,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
id: params.toolCall.toolCallId,
|
|
797
|
+
provider: GEMINI_PROVIDER,
|
|
798
|
+
name: toolCallTitle,
|
|
799
|
+
kind: "tool",
|
|
800
|
+
title: toolCallTitle,
|
|
801
|
+
description: "Gemini needs approval before continuing.",
|
|
802
|
+
detail,
|
|
803
|
+
metadata,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function toolStateToTimelineItem(state) {
|
|
807
|
+
return toGeminiToolTimelineItem({
|
|
808
|
+
toolName: state.title,
|
|
809
|
+
callId: state.toolCallId,
|
|
810
|
+
status: state.status,
|
|
811
|
+
input: state.rawInput,
|
|
812
|
+
output: state.rawOutput,
|
|
813
|
+
kind: state.kind,
|
|
814
|
+
content: state.content,
|
|
815
|
+
locations: state.locations,
|
|
816
|
+
error: normalizeToolStatus(state.status) === "failed"
|
|
817
|
+
? state.rawOutput ?? { message: "Tool call failed" }
|
|
818
|
+
: null,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
function sessionUpdateToEvents(update, toolStates, options) {
|
|
822
|
+
switch (update.sessionUpdate) {
|
|
823
|
+
case "user_message_chunk": {
|
|
824
|
+
if (!options?.includeUserMessages || update.content.type !== "text") {
|
|
825
|
+
return [];
|
|
826
|
+
}
|
|
827
|
+
const text = update.content.text.trim();
|
|
828
|
+
if (text.length === 0) {
|
|
829
|
+
return [];
|
|
830
|
+
}
|
|
831
|
+
return [
|
|
832
|
+
{
|
|
833
|
+
type: "timeline",
|
|
834
|
+
provider: GEMINI_PROVIDER,
|
|
835
|
+
item: {
|
|
836
|
+
type: "user_message",
|
|
837
|
+
text,
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
];
|
|
841
|
+
}
|
|
842
|
+
case "agent_message_chunk": {
|
|
843
|
+
if (update.content.type !== "text") {
|
|
844
|
+
return [];
|
|
845
|
+
}
|
|
846
|
+
const text = update.content.text;
|
|
847
|
+
if (text.length === 0) {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
return [
|
|
851
|
+
{
|
|
852
|
+
type: "timeline",
|
|
853
|
+
provider: GEMINI_PROVIDER,
|
|
854
|
+
item: {
|
|
855
|
+
type: "assistant_message",
|
|
856
|
+
text,
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
];
|
|
860
|
+
}
|
|
861
|
+
case "agent_thought_chunk": {
|
|
862
|
+
if (update.content.type !== "text") {
|
|
863
|
+
return [];
|
|
864
|
+
}
|
|
865
|
+
const text = update.content.text;
|
|
866
|
+
if (text.length === 0) {
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
return [
|
|
870
|
+
{
|
|
871
|
+
type: "timeline",
|
|
872
|
+
provider: GEMINI_PROVIDER,
|
|
873
|
+
item: {
|
|
874
|
+
type: "reasoning",
|
|
875
|
+
text,
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
];
|
|
879
|
+
}
|
|
880
|
+
case "tool_call": {
|
|
881
|
+
const state = {
|
|
882
|
+
toolCallId: update.toolCallId,
|
|
883
|
+
title: update.title,
|
|
884
|
+
status: update.status ?? "in_progress",
|
|
885
|
+
kind: update.kind ?? null,
|
|
886
|
+
content: update.content ?? null,
|
|
887
|
+
locations: update.locations ?? null,
|
|
888
|
+
rawInput: update.rawInput,
|
|
889
|
+
rawOutput: update.rawOutput,
|
|
890
|
+
};
|
|
891
|
+
toolStates.set(update.toolCallId, state);
|
|
892
|
+
return [
|
|
893
|
+
{
|
|
894
|
+
type: "timeline",
|
|
895
|
+
provider: GEMINI_PROVIDER,
|
|
896
|
+
item: toolStateToTimelineItem(state),
|
|
897
|
+
},
|
|
898
|
+
];
|
|
899
|
+
}
|
|
900
|
+
case "tool_call_update": {
|
|
901
|
+
const previous = toolStates.get(update.toolCallId);
|
|
902
|
+
const state = {
|
|
903
|
+
toolCallId: update.toolCallId,
|
|
904
|
+
title: nonEmptyString(update.title) ?? previous?.title ?? update.toolCallId,
|
|
905
|
+
status: update.status ?? previous?.status ?? "in_progress",
|
|
906
|
+
kind: update.kind ?? previous?.kind ?? null,
|
|
907
|
+
content: update.content ?? previous?.content ?? null,
|
|
908
|
+
locations: update.locations ?? previous?.locations ?? null,
|
|
909
|
+
rawInput: update.rawInput !== undefined ? update.rawInput : previous?.rawInput,
|
|
910
|
+
rawOutput: update.rawOutput !== undefined ? update.rawOutput : previous?.rawOutput,
|
|
911
|
+
};
|
|
912
|
+
toolStates.set(update.toolCallId, state);
|
|
913
|
+
return [
|
|
914
|
+
{
|
|
915
|
+
type: "timeline",
|
|
916
|
+
provider: GEMINI_PROVIDER,
|
|
917
|
+
item: toolStateToTimelineItem(state),
|
|
918
|
+
},
|
|
919
|
+
];
|
|
920
|
+
}
|
|
921
|
+
case "plan":
|
|
922
|
+
return [
|
|
923
|
+
{
|
|
924
|
+
type: "timeline",
|
|
925
|
+
provider: GEMINI_PROVIDER,
|
|
926
|
+
item: {
|
|
927
|
+
type: "todo",
|
|
928
|
+
items: update.entries.map((entry) => ({
|
|
929
|
+
text: entry.content,
|
|
930
|
+
completed: entry.status === "completed",
|
|
931
|
+
})),
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
];
|
|
935
|
+
default:
|
|
936
|
+
return [];
|
|
937
|
+
}
|
|
598
938
|
}
|
|
599
939
|
export class GeminiAgentClient {
|
|
600
940
|
constructor(logger, runtimeSettings) {
|
|
@@ -605,7 +945,9 @@ export class GeminiAgentClient {
|
|
|
605
945
|
}
|
|
606
946
|
async createSession(config) {
|
|
607
947
|
const geminiConfig = this.assertConfig(config);
|
|
608
|
-
|
|
948
|
+
const session = new GeminiAgentSession(geminiConfig, this.logger, this.runtimeSettings);
|
|
949
|
+
await session.initialize();
|
|
950
|
+
return session;
|
|
609
951
|
}
|
|
610
952
|
async resumeSession(handle, overrides) {
|
|
611
953
|
const cwd = overrides?.cwd ?? handle.metadata?.cwd;
|
|
@@ -617,7 +959,9 @@ export class GeminiAgentClient {
|
|
|
617
959
|
cwd,
|
|
618
960
|
...overrides,
|
|
619
961
|
});
|
|
620
|
-
|
|
962
|
+
const session = new GeminiAgentSession(config, this.logger, this.runtimeSettings, handle.sessionId);
|
|
963
|
+
await session.initialize();
|
|
964
|
+
return session;
|
|
621
965
|
}
|
|
622
966
|
async listModels(_options) {
|
|
623
967
|
return toGeminiModelDefinitions();
|
|
@@ -630,11 +974,21 @@ export class GeminiAgentClient {
|
|
|
630
974
|
})));
|
|
631
975
|
return sessions
|
|
632
976
|
.flat()
|
|
633
|
-
.sort((
|
|
977
|
+
.sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
|
|
634
978
|
.slice(0, options?.limit ?? 20);
|
|
635
979
|
}
|
|
636
980
|
async isAvailable() {
|
|
637
|
-
|
|
981
|
+
try {
|
|
982
|
+
const available = isProviderCommandAvailable(this.runtimeSettings?.command, resolveGeminiBinary, applyProviderEnv(process.env, this.runtimeSettings));
|
|
983
|
+
if (!available) {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
assertGeminiAcpSupport(this.runtimeSettings);
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
638
992
|
}
|
|
639
993
|
assertConfig(config) {
|
|
640
994
|
if (config.provider !== GEMINI_PROVIDER) {
|
|
@@ -647,24 +1001,53 @@ class GeminiAgentSession {
|
|
|
647
1001
|
constructor(config, logger, runtimeSettings, sessionId) {
|
|
648
1002
|
this.provider = GEMINI_PROVIDER;
|
|
649
1003
|
this.capabilities = GEMINI_CAPABILITIES;
|
|
650
|
-
this.
|
|
651
|
-
this.
|
|
652
|
-
this.
|
|
1004
|
+
this.connection = null;
|
|
1005
|
+
this.child = null;
|
|
1006
|
+
this.closePromise = null;
|
|
1007
|
+
this.initialized = false;
|
|
1008
|
+
this.closed = false;
|
|
1009
|
+
this.currentModeId = null;
|
|
1010
|
+
this.currentModelId = null;
|
|
1011
|
+
this.requestedModeId = null;
|
|
1012
|
+
this.requestedModelId = null;
|
|
1013
|
+
this.availableModes = GEMINI_RUNTIME_MODES;
|
|
1014
|
+
this.availableModels = toGeminiModelDefinitions();
|
|
653
1015
|
this.pendingPermissions = new Map();
|
|
1016
|
+
this.toolStates = new Map();
|
|
1017
|
+
this.activeTurnQueue = null;
|
|
1018
|
+
this.historyEvents = [];
|
|
1019
|
+
this.historyReadyPromise = null;
|
|
1020
|
+
this.resolveHistoryReady = null;
|
|
1021
|
+
this.historyCaptureActive = false;
|
|
1022
|
+
this.historyIdleTimer = null;
|
|
1023
|
+
this.expectedHistoryEventCount = null;
|
|
1024
|
+
this.capturedHistoryEventCount = 0;
|
|
1025
|
+
this.currentRunInterrupted = false;
|
|
654
1026
|
this.config = config;
|
|
655
1027
|
this.logger = logger.child({ sessionProvider: GEMINI_PROVIDER });
|
|
656
1028
|
this.runtimeSettings = runtimeSettings;
|
|
657
|
-
this.currentMode = normalizeGeminiMode(config.modeId);
|
|
658
1029
|
this.sessionId = sessionId ?? null;
|
|
1030
|
+
const requestedState = getRequestedGeminiSessionState(config);
|
|
1031
|
+
this.requestedModeId = requestedState.modeId;
|
|
1032
|
+
this.requestedModelId = requestedState.modelId;
|
|
1033
|
+
this.currentModeId = requestedState.modeId;
|
|
1034
|
+
this.currentModelId = requestedState.modelId;
|
|
659
1035
|
this.config.thinkingOptionId = undefined;
|
|
660
1036
|
}
|
|
661
1037
|
get id() {
|
|
662
1038
|
return this.sessionId;
|
|
663
1039
|
}
|
|
1040
|
+
async initialize() {
|
|
1041
|
+
if (this.initialized) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
assertGeminiAcpSupport(this.runtimeSettings);
|
|
1045
|
+
await this.openConnection();
|
|
1046
|
+
this.initialized = true;
|
|
1047
|
+
}
|
|
664
1048
|
async run(prompt, options) {
|
|
665
1049
|
const timeline = [];
|
|
666
1050
|
let finalText = "";
|
|
667
|
-
let usage;
|
|
668
1051
|
let canceled = false;
|
|
669
1052
|
for await (const event of this.stream(prompt, options)) {
|
|
670
1053
|
if (event.type === "timeline") {
|
|
@@ -673,9 +1056,6 @@ class GeminiAgentSession {
|
|
|
673
1056
|
finalText += event.item.text;
|
|
674
1057
|
}
|
|
675
1058
|
}
|
|
676
|
-
else if (event.type === "turn_completed") {
|
|
677
|
-
usage = event.usage;
|
|
678
|
-
}
|
|
679
1059
|
else if (event.type === "turn_failed") {
|
|
680
1060
|
throw new Error(event.error);
|
|
681
1061
|
}
|
|
@@ -686,249 +1066,156 @@ class GeminiAgentSession {
|
|
|
686
1066
|
return {
|
|
687
1067
|
sessionId: this.sessionId ?? "",
|
|
688
1068
|
finalText,
|
|
689
|
-
usage,
|
|
690
1069
|
timeline,
|
|
691
1070
|
...(canceled ? { canceled } : {}),
|
|
692
1071
|
};
|
|
693
1072
|
}
|
|
694
1073
|
async *stream(prompt, _options) {
|
|
695
|
-
|
|
696
|
-
|
|
1074
|
+
await this.initialize();
|
|
1075
|
+
if (!this.connection || !this.sessionId) {
|
|
1076
|
+
throw new Error("Gemini ACP session is not ready");
|
|
697
1077
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const overlay = await writeGeminiOverlayConfig(modeId, this.config.mcpServers);
|
|
701
|
-
if (overlay) {
|
|
702
|
-
await this.cleanupOverlay();
|
|
703
|
-
this.overlayDir = overlay.dir;
|
|
1078
|
+
if (this.activeTurnQueue) {
|
|
1079
|
+
throw new Error("Gemini session is already running");
|
|
704
1080
|
}
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
if (overlay) {
|
|
708
|
-
env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = overlay.settingsPath;
|
|
709
|
-
}
|
|
710
|
-
const args = [
|
|
711
|
-
...launchPrefix.args,
|
|
712
|
-
"-p",
|
|
713
|
-
promptText,
|
|
714
|
-
"--output-format",
|
|
715
|
-
"stream-json",
|
|
716
|
-
"--approval-mode",
|
|
717
|
-
modeId,
|
|
718
|
-
];
|
|
719
|
-
const modelId = typeof this.config.model === "string" && this.config.model.trim().length > 0
|
|
720
|
-
? this.config.model.trim()
|
|
721
|
-
: null;
|
|
722
|
-
if (modelId) {
|
|
723
|
-
args.push("--model", modelId);
|
|
724
|
-
}
|
|
725
|
-
if (this.sessionId) {
|
|
726
|
-
args.push("--resume", this.sessionId);
|
|
727
|
-
}
|
|
728
|
-
const child = spawn(launchPrefix.command, args, {
|
|
729
|
-
cwd: this.config.cwd,
|
|
730
|
-
env,
|
|
731
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
732
|
-
});
|
|
733
|
-
this.currentChild = child;
|
|
1081
|
+
const queue = new AsyncEventQueue();
|
|
1082
|
+
this.activeTurnQueue = queue;
|
|
734
1083
|
this.currentRunInterrupted = false;
|
|
735
|
-
|
|
736
|
-
child.stderr.on("data", (chunk) => {
|
|
737
|
-
const text = chunk.toString("utf8");
|
|
738
|
-
if (text.length > 0) {
|
|
739
|
-
stderrChunks.push(text);
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
const closePromise = new Promise((resolve, reject) => {
|
|
743
|
-
child.once("error", reject);
|
|
744
|
-
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
745
|
-
});
|
|
746
|
-
const lineReader = readline.createInterface({
|
|
747
|
-
input: child.stdout,
|
|
748
|
-
crlfDelay: Infinity,
|
|
749
|
-
});
|
|
750
|
-
const pendingToolCalls = new Map();
|
|
751
|
-
let sawResult = false;
|
|
752
|
-
let failureMessage = null;
|
|
753
|
-
yield {
|
|
1084
|
+
queue.push({
|
|
754
1085
|
type: "turn_started",
|
|
755
1086
|
provider: GEMINI_PROVIDER,
|
|
756
|
-
};
|
|
1087
|
+
});
|
|
1088
|
+
void this.connection
|
|
1089
|
+
.prompt({
|
|
1090
|
+
sessionId: this.sessionId,
|
|
1091
|
+
prompt: toGeminiPromptBlocks(prompt),
|
|
1092
|
+
})
|
|
1093
|
+
.then((response) => {
|
|
1094
|
+
if (response.stopReason === "cancelled") {
|
|
1095
|
+
queue.push({
|
|
1096
|
+
type: "turn_canceled",
|
|
1097
|
+
provider: GEMINI_PROVIDER,
|
|
1098
|
+
reason: "Interrupted",
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
queue.push({
|
|
1103
|
+
type: "turn_completed",
|
|
1104
|
+
provider: GEMINI_PROVIDER,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
queue.close();
|
|
1108
|
+
})
|
|
1109
|
+
.catch((error) => {
|
|
1110
|
+
queue.push({
|
|
1111
|
+
type: "turn_failed",
|
|
1112
|
+
provider: GEMINI_PROVIDER,
|
|
1113
|
+
error: formatGeminiAcpError(error),
|
|
1114
|
+
});
|
|
1115
|
+
queue.close();
|
|
1116
|
+
});
|
|
757
1117
|
try {
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
if (!
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
let parsedJson;
|
|
764
|
-
try {
|
|
765
|
-
parsedJson = JSON.parse(line);
|
|
766
|
-
}
|
|
767
|
-
catch {
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
const parsedEvent = GeminiStreamEventSchema.safeParse(parsedJson);
|
|
771
|
-
if (!parsedEvent.success) {
|
|
772
|
-
continue;
|
|
773
|
-
}
|
|
774
|
-
const event = parsedEvent.data;
|
|
775
|
-
if (event.type === "init") {
|
|
776
|
-
this.sessionId = event.session_id;
|
|
777
|
-
if (event.model) {
|
|
778
|
-
this.config.model = event.model;
|
|
779
|
-
}
|
|
780
|
-
yield {
|
|
781
|
-
type: "thread_started",
|
|
782
|
-
provider: GEMINI_PROVIDER,
|
|
783
|
-
sessionId: event.session_id,
|
|
784
|
-
};
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
if (event.type === "message") {
|
|
788
|
-
if (event.role === "assistant" && event.content.length > 0) {
|
|
789
|
-
yield {
|
|
790
|
-
type: "timeline",
|
|
791
|
-
provider: GEMINI_PROVIDER,
|
|
792
|
-
item: {
|
|
793
|
-
type: "assistant_message",
|
|
794
|
-
text: event.content,
|
|
795
|
-
},
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
continue;
|
|
799
|
-
}
|
|
800
|
-
if (event.type === "tool_use") {
|
|
801
|
-
pendingToolCalls.set(event.tool_id, {
|
|
802
|
-
toolName: event.tool_name,
|
|
803
|
-
input: event.parameters,
|
|
804
|
-
});
|
|
805
|
-
yield {
|
|
806
|
-
type: "timeline",
|
|
807
|
-
provider: GEMINI_PROVIDER,
|
|
808
|
-
item: toGeminiToolTimelineItem({
|
|
809
|
-
toolName: event.tool_name,
|
|
810
|
-
callId: event.tool_id,
|
|
811
|
-
status: "running",
|
|
812
|
-
input: event.parameters,
|
|
813
|
-
}),
|
|
814
|
-
};
|
|
815
|
-
continue;
|
|
816
|
-
}
|
|
817
|
-
if (event.type === "tool_result") {
|
|
818
|
-
const pending = pendingToolCalls.get(event.tool_id);
|
|
819
|
-
const toolName = pending?.toolName ?? "tool";
|
|
820
|
-
yield {
|
|
821
|
-
type: "timeline",
|
|
822
|
-
provider: GEMINI_PROVIDER,
|
|
823
|
-
item: toGeminiToolTimelineItem({
|
|
824
|
-
toolName,
|
|
825
|
-
callId: event.tool_id,
|
|
826
|
-
status: event.status,
|
|
827
|
-
input: pending?.input,
|
|
828
|
-
output: event.output,
|
|
829
|
-
error: event.error,
|
|
830
|
-
}),
|
|
831
|
-
};
|
|
832
|
-
pendingToolCalls.delete(event.tool_id);
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
if (event.type === "error") {
|
|
836
|
-
failureMessage = event.message;
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
if (event.type === "result") {
|
|
840
|
-
sawResult = true;
|
|
841
|
-
if (event.status !== "success") {
|
|
842
|
-
failureMessage = failureMessage ?? `Gemini CLI result status: ${event.status}`;
|
|
843
|
-
yield {
|
|
844
|
-
type: "turn_failed",
|
|
845
|
-
provider: GEMINI_PROVIDER,
|
|
846
|
-
error: failureMessage,
|
|
847
|
-
};
|
|
848
|
-
continue;
|
|
849
|
-
}
|
|
850
|
-
yield {
|
|
851
|
-
type: "turn_completed",
|
|
852
|
-
provider: GEMINI_PROVIDER,
|
|
853
|
-
usage: mapGeminiUsage(event.stats),
|
|
854
|
-
};
|
|
1118
|
+
while (true) {
|
|
1119
|
+
const nextEvent = await queue.shift();
|
|
1120
|
+
if (!nextEvent) {
|
|
1121
|
+
break;
|
|
855
1122
|
}
|
|
1123
|
+
yield nextEvent;
|
|
856
1124
|
}
|
|
857
1125
|
}
|
|
858
1126
|
finally {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
code: 1,
|
|
862
|
-
signal: null,
|
|
863
|
-
error,
|
|
864
|
-
}));
|
|
865
|
-
this.currentChild = null;
|
|
866
|
-
if (!sawResult) {
|
|
867
|
-
if (this.currentRunInterrupted || signal) {
|
|
868
|
-
yield {
|
|
869
|
-
type: "turn_canceled",
|
|
870
|
-
provider: GEMINI_PROVIDER,
|
|
871
|
-
reason: "Interrupted",
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
else {
|
|
875
|
-
const stderr = stderrChunks.join("").trim();
|
|
876
|
-
const message = failureMessage ??
|
|
877
|
-
stderr ??
|
|
878
|
-
(typeof code === "number" && code !== 0
|
|
879
|
-
? `Gemini CLI exited with code ${code}`
|
|
880
|
-
: "Gemini CLI ended without a result event");
|
|
881
|
-
yield {
|
|
882
|
-
type: "turn_failed",
|
|
883
|
-
provider: GEMINI_PROVIDER,
|
|
884
|
-
error: message,
|
|
885
|
-
};
|
|
886
|
-
this.logger.warn({ code, signal, message }, "Gemini run failed");
|
|
887
|
-
}
|
|
1127
|
+
if (this.activeTurnQueue === queue) {
|
|
1128
|
+
this.activeTurnQueue = null;
|
|
888
1129
|
}
|
|
889
1130
|
this.currentRunInterrupted = false;
|
|
890
1131
|
}
|
|
891
1132
|
}
|
|
892
1133
|
async *streamHistory() {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const session = await this.loadRecordedSession(this.sessionId);
|
|
897
|
-
if (!session) {
|
|
898
|
-
return;
|
|
1134
|
+
await this.initialize();
|
|
1135
|
+
if (this.historyReadyPromise) {
|
|
1136
|
+
await this.historyReadyPromise;
|
|
899
1137
|
}
|
|
900
|
-
for (const
|
|
901
|
-
yield
|
|
902
|
-
type: "timeline",
|
|
903
|
-
provider: GEMINI_PROVIDER,
|
|
904
|
-
item,
|
|
905
|
-
};
|
|
1138
|
+
for (const event of this.historyEvents) {
|
|
1139
|
+
yield event;
|
|
906
1140
|
}
|
|
907
1141
|
}
|
|
908
1142
|
async getRuntimeInfo() {
|
|
1143
|
+
await this.initialize();
|
|
909
1144
|
return {
|
|
910
1145
|
provider: GEMINI_PROVIDER,
|
|
911
1146
|
sessionId: this.sessionId,
|
|
912
|
-
model: this.
|
|
1147
|
+
model: this.currentModelId,
|
|
913
1148
|
thinkingOptionId: null,
|
|
914
|
-
modeId: this.
|
|
1149
|
+
modeId: this.currentModeId,
|
|
1150
|
+
extra: {
|
|
1151
|
+
availableModels: this.availableModels.map((model) => ({
|
|
1152
|
+
id: model.id,
|
|
1153
|
+
label: model.label,
|
|
1154
|
+
description: model.description ?? null,
|
|
1155
|
+
})),
|
|
1156
|
+
},
|
|
915
1157
|
};
|
|
916
1158
|
}
|
|
917
1159
|
async getAvailableModes() {
|
|
918
|
-
|
|
1160
|
+
await this.initialize();
|
|
1161
|
+
return this.availableModes;
|
|
919
1162
|
}
|
|
920
1163
|
async getCurrentMode() {
|
|
921
|
-
|
|
1164
|
+
await this.initialize();
|
|
1165
|
+
return this.currentModeId;
|
|
922
1166
|
}
|
|
923
1167
|
async setMode(modeId) {
|
|
924
|
-
this.
|
|
925
|
-
|
|
1168
|
+
await this.initialize();
|
|
1169
|
+
const normalizedMode = normalizeGeminiMode(modeId);
|
|
1170
|
+
const availableModeIds = new Set(this.availableModes.map((mode) => mode.id));
|
|
1171
|
+
if (!availableModeIds.has(normalizedMode)) {
|
|
1172
|
+
throw new Error(`Gemini mode '${normalizedMode}' is unavailable for this session. Available modes: ${this.availableModes
|
|
1173
|
+
.map((mode) => mode.id)
|
|
1174
|
+
.join(", ")}`);
|
|
1175
|
+
}
|
|
1176
|
+
if (!this.connection || !this.sessionId) {
|
|
1177
|
+
throw new Error("Gemini ACP session is not ready");
|
|
1178
|
+
}
|
|
1179
|
+
await this.connection.setSessionMode({
|
|
1180
|
+
sessionId: this.sessionId,
|
|
1181
|
+
modeId: normalizedMode,
|
|
1182
|
+
});
|
|
1183
|
+
this.requestedModeId = normalizedMode;
|
|
1184
|
+
this.currentModeId = normalizedMode;
|
|
1185
|
+
this.config.modeId = normalizedMode;
|
|
926
1186
|
}
|
|
927
1187
|
getPendingPermissions() {
|
|
928
|
-
return Array.from(this.pendingPermissions.values());
|
|
1188
|
+
return Array.from(this.pendingPermissions.values(), (entry) => entry.request);
|
|
929
1189
|
}
|
|
930
|
-
async respondToPermission(requestId,
|
|
931
|
-
|
|
1190
|
+
async respondToPermission(requestId, response) {
|
|
1191
|
+
const pending = this.pendingPermissions.get(requestId);
|
|
1192
|
+
if (!pending) {
|
|
1193
|
+
throw new Error(`No pending Gemini permission request with id '${requestId}'`);
|
|
1194
|
+
}
|
|
1195
|
+
const selectedOptionId = response.behavior === "allow"
|
|
1196
|
+
? nonEmptyString(response.updatedInput?.optionId)
|
|
1197
|
+
: nonEmptyString(response.updatedInput?.optionId);
|
|
1198
|
+
const option = pending.options.find((entry) => entry.optionId === selectedOptionId) ??
|
|
1199
|
+
pending.options.find((entry) => response.behavior === "allow"
|
|
1200
|
+
? entry.kind.startsWith("allow")
|
|
1201
|
+
: entry.kind.startsWith("reject")) ??
|
|
1202
|
+
null;
|
|
1203
|
+
if (!option) {
|
|
1204
|
+
throw new Error(`Gemini permission '${requestId}' does not have a matching option`);
|
|
1205
|
+
}
|
|
1206
|
+
this.pendingPermissions.delete(requestId);
|
|
1207
|
+
pending.resolve({
|
|
1208
|
+
outcome: {
|
|
1209
|
+
outcome: "selected",
|
|
1210
|
+
optionId: option.optionId,
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
this.emitLiveEvent({
|
|
1214
|
+
type: "permission_resolved",
|
|
1215
|
+
provider: GEMINI_PROVIDER,
|
|
1216
|
+
requestId,
|
|
1217
|
+
resolution: response,
|
|
1218
|
+
});
|
|
932
1219
|
}
|
|
933
1220
|
describePersistence() {
|
|
934
1221
|
if (!this.sessionId) {
|
|
@@ -937,65 +1224,336 @@ class GeminiAgentSession {
|
|
|
937
1224
|
return {
|
|
938
1225
|
provider: GEMINI_PROVIDER,
|
|
939
1226
|
sessionId: this.sessionId,
|
|
940
|
-
nativeHandle: this.sessionId,
|
|
941
1227
|
metadata: {
|
|
942
1228
|
cwd: this.config.cwd,
|
|
943
1229
|
},
|
|
944
1230
|
};
|
|
945
1231
|
}
|
|
946
1232
|
async interrupt() {
|
|
947
|
-
this.
|
|
948
|
-
|
|
949
|
-
if (!child) {
|
|
1233
|
+
await this.initialize();
|
|
1234
|
+
if (!this.connection || !this.sessionId) {
|
|
950
1235
|
return;
|
|
951
1236
|
}
|
|
952
|
-
|
|
1237
|
+
this.currentRunInterrupted = true;
|
|
1238
|
+
for (const [requestId, pending] of this.pendingPermissions.entries()) {
|
|
1239
|
+
this.pendingPermissions.delete(requestId);
|
|
1240
|
+
pending.resolve({
|
|
1241
|
+
outcome: {
|
|
1242
|
+
outcome: "cancelled",
|
|
1243
|
+
},
|
|
1244
|
+
});
|
|
1245
|
+
this.emitLiveEvent({
|
|
1246
|
+
type: "permission_resolved",
|
|
1247
|
+
provider: GEMINI_PROVIDER,
|
|
1248
|
+
requestId,
|
|
1249
|
+
resolution: {
|
|
1250
|
+
behavior: "deny",
|
|
1251
|
+
message: "Interrupted",
|
|
1252
|
+
},
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
await this.connection.cancel({
|
|
1256
|
+
sessionId: this.sessionId,
|
|
1257
|
+
});
|
|
953
1258
|
}
|
|
954
1259
|
async close() {
|
|
955
|
-
|
|
956
|
-
|
|
1260
|
+
if (this.closed) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
this.closed = true;
|
|
1264
|
+
this.finishHistoryCapture();
|
|
1265
|
+
for (const [requestId, pending] of this.pendingPermissions.entries()) {
|
|
1266
|
+
this.pendingPermissions.delete(requestId);
|
|
1267
|
+
pending.resolve({
|
|
1268
|
+
outcome: {
|
|
1269
|
+
outcome: "cancelled",
|
|
1270
|
+
},
|
|
1271
|
+
});
|
|
1272
|
+
this.emitLiveEvent({
|
|
1273
|
+
type: "permission_resolved",
|
|
1274
|
+
provider: GEMINI_PROVIDER,
|
|
1275
|
+
requestId,
|
|
1276
|
+
resolution: {
|
|
1277
|
+
behavior: "deny",
|
|
1278
|
+
message: "Session closed",
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
this.activeTurnQueue?.close();
|
|
1283
|
+
this.activeTurnQueue = null;
|
|
1284
|
+
if (this.child?.stdin) {
|
|
1285
|
+
this.child.stdin.end();
|
|
1286
|
+
}
|
|
1287
|
+
if (this.child && !this.child.killed) {
|
|
1288
|
+
this.child.kill();
|
|
1289
|
+
}
|
|
1290
|
+
await this.closePromise?.catch(() => { });
|
|
1291
|
+
this.connection = null;
|
|
1292
|
+
this.child = null;
|
|
957
1293
|
}
|
|
958
1294
|
async setModel(modelId) {
|
|
959
|
-
this.
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1295
|
+
await this.initialize();
|
|
1296
|
+
const normalizedModelId = typeof modelId === "string" && modelId.trim().length > 0 ? modelId.trim() : null;
|
|
1297
|
+
if (!normalizedModelId) {
|
|
1298
|
+
throw new Error("Gemini model id cannot be empty");
|
|
1299
|
+
}
|
|
1300
|
+
const availableModelIds = new Set(this.availableModels.map((model) => model.id));
|
|
1301
|
+
if (!availableModelIds.has(normalizedModelId)) {
|
|
1302
|
+
throw new Error(`Gemini model '${normalizedModelId}' is unavailable for this session. Available models: ${this.availableModels
|
|
1303
|
+
.map((model) => model.id)
|
|
1304
|
+
.join(", ")}`);
|
|
1305
|
+
}
|
|
1306
|
+
if (!this.connection || !this.sessionId) {
|
|
1307
|
+
throw new Error("Gemini ACP session is not ready");
|
|
1308
|
+
}
|
|
1309
|
+
await this.connection.unstable_setSessionModel({
|
|
1310
|
+
sessionId: this.sessionId,
|
|
1311
|
+
modelId: normalizedModelId,
|
|
1312
|
+
});
|
|
1313
|
+
this.requestedModelId = normalizedModelId;
|
|
1314
|
+
this.currentModelId = normalizedModelId;
|
|
1315
|
+
this.config.model = normalizedModelId;
|
|
963
1316
|
}
|
|
964
|
-
async setThinkingOption(_thinkingOptionId) {
|
|
965
|
-
|
|
1317
|
+
async setThinkingOption(_thinkingOptionId) { }
|
|
1318
|
+
async openConnection() {
|
|
1319
|
+
const launchPrefix = resolveProviderCommandPrefix(this.runtimeSettings?.command, resolveGeminiBinary);
|
|
1320
|
+
const env = applyProviderEnv(process.env, this.runtimeSettings);
|
|
1321
|
+
const child = spawn(launchPrefix.command, [...launchPrefix.args, "--acp"], {
|
|
1322
|
+
cwd: this.config.cwd,
|
|
1323
|
+
env,
|
|
1324
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1325
|
+
});
|
|
1326
|
+
this.child = child;
|
|
1327
|
+
const stderrChunks = [];
|
|
1328
|
+
child.stderr.on("data", (chunk) => {
|
|
1329
|
+
const text = chunk.toString("utf8");
|
|
1330
|
+
if (text.length > 0) {
|
|
1331
|
+
stderrChunks.push(text);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
this.closePromise = new Promise((resolve) => {
|
|
1335
|
+
child.once("close", (code, signal) => {
|
|
1336
|
+
const stderr = stderrChunks.join("").trim();
|
|
1337
|
+
this.child = null;
|
|
1338
|
+
this.connection = null;
|
|
1339
|
+
if (!this.closed) {
|
|
1340
|
+
const message = stderr ||
|
|
1341
|
+
(typeof code === "number" && code !== 0
|
|
1342
|
+
? `Gemini ACP process exited with code ${code}`
|
|
1343
|
+
: signal
|
|
1344
|
+
? `Gemini ACP process exited with signal ${signal}`
|
|
1345
|
+
: "Gemini ACP process closed");
|
|
1346
|
+
if (this.activeTurnQueue) {
|
|
1347
|
+
this.activeTurnQueue.push({
|
|
1348
|
+
type: "turn_failed",
|
|
1349
|
+
provider: GEMINI_PROVIDER,
|
|
1350
|
+
error: message,
|
|
1351
|
+
});
|
|
1352
|
+
this.activeTurnQueue.close();
|
|
1353
|
+
}
|
|
1354
|
+
this.logger.warn({ code, signal, stderr }, "Gemini ACP process closed");
|
|
1355
|
+
}
|
|
1356
|
+
resolve();
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
const stream = acp.ndJsonStream(NodeWritable.toWeb(child.stdin), NodeReadable.toWeb(child.stdout));
|
|
1360
|
+
this.connection = new acp.ClientSideConnection(() => ({
|
|
1361
|
+
sessionUpdate: async (notification) => {
|
|
1362
|
+
await this.handleSessionUpdate(notification);
|
|
1363
|
+
},
|
|
1364
|
+
requestPermission: async (request) => this.handlePermissionRequest(request),
|
|
1365
|
+
}), stream);
|
|
1366
|
+
try {
|
|
1367
|
+
await this.connection.initialize({
|
|
1368
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
1369
|
+
clientCapabilities: {
|
|
1370
|
+
fs: {
|
|
1371
|
+
readTextFile: false,
|
|
1372
|
+
writeTextFile: false,
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
clientInfo: {
|
|
1376
|
+
name: "junction",
|
|
1377
|
+
title: "Junction",
|
|
1378
|
+
version: "local",
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
1381
|
+
if (this.sessionId) {
|
|
1382
|
+
const recordedSession = await loadGeminiRecordedSessionForCwd(this.config.cwd, this.sessionId);
|
|
1383
|
+
this.beginHistoryCapture(recordedSession ? buildGeminiHistoryTimeline(recordedSession).length : null);
|
|
1384
|
+
const response = await this.connection.loadSession({
|
|
1385
|
+
sessionId: this.sessionId,
|
|
1386
|
+
cwd: this.config.cwd,
|
|
1387
|
+
mcpServers: toGeminiAcpMcpServers(this.config.mcpServers),
|
|
1388
|
+
});
|
|
1389
|
+
this.applySessionSetup(response);
|
|
1390
|
+
if (this.expectedHistoryEventCount === 0) {
|
|
1391
|
+
this.finishHistoryCapture();
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
this.scheduleHistoryCaptureFinish();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
const response = await this.connection.newSession({
|
|
1399
|
+
cwd: this.config.cwd,
|
|
1400
|
+
mcpServers: toGeminiAcpMcpServers(this.config.mcpServers),
|
|
1401
|
+
});
|
|
1402
|
+
this.sessionId = response.sessionId;
|
|
1403
|
+
this.applySessionSetup(response);
|
|
1404
|
+
}
|
|
1405
|
+
await this.applyRequestedOverrides();
|
|
1406
|
+
}
|
|
1407
|
+
catch (error) {
|
|
1408
|
+
await this.close().catch(() => { });
|
|
1409
|
+
throw new Error(formatGeminiAcpError(error));
|
|
1410
|
+
}
|
|
966
1411
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1412
|
+
applySessionSetup(response) {
|
|
1413
|
+
if (response.modes) {
|
|
1414
|
+
this.availableModes = response.modes.availableModes.map((mode) => toAgentMode(mode));
|
|
1415
|
+
this.currentModeId = response.modes.currentModeId;
|
|
1416
|
+
this.config.modeId = response.modes.currentModeId;
|
|
971
1417
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1418
|
+
if (response.models) {
|
|
1419
|
+
this.availableModels = response.models.availableModels.map((model) => toAgentModelDefinition(model));
|
|
1420
|
+
this.currentModelId = response.models.currentModelId;
|
|
1421
|
+
this.config.model = response.models.currentModelId;
|
|
976
1422
|
}
|
|
977
|
-
|
|
978
|
-
|
|
1423
|
+
}
|
|
1424
|
+
async applyRequestedOverrides() {
|
|
1425
|
+
const requestedModeId = this.requestedModeId;
|
|
1426
|
+
if (requestedModeId && requestedModeId !== this.currentModeId) {
|
|
1427
|
+
const availableModeIds = new Set(this.availableModes.map((mode) => mode.id));
|
|
1428
|
+
if (!availableModeIds.has(requestedModeId)) {
|
|
1429
|
+
throw new Error(`Gemini mode '${requestedModeId}' is unavailable for this session. Available modes: ${this.availableModes
|
|
1430
|
+
.map((mode) => mode.id)
|
|
1431
|
+
.join(", ")}`);
|
|
1432
|
+
}
|
|
1433
|
+
if (!this.connection || !this.sessionId) {
|
|
1434
|
+
throw new Error("Gemini ACP session is not ready");
|
|
1435
|
+
}
|
|
1436
|
+
await this.connection.setSessionMode({
|
|
1437
|
+
sessionId: this.sessionId,
|
|
1438
|
+
modeId: requestedModeId,
|
|
1439
|
+
});
|
|
1440
|
+
this.currentModeId = requestedModeId;
|
|
1441
|
+
this.config.modeId = requestedModeId;
|
|
979
1442
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1443
|
+
const requestedModelId = this.requestedModelId;
|
|
1444
|
+
if (requestedModelId && requestedModelId !== this.currentModelId) {
|
|
1445
|
+
const availableModelIds = new Set(this.availableModels.map((model) => model.id));
|
|
1446
|
+
if (!availableModelIds.has(requestedModelId)) {
|
|
1447
|
+
throw new Error(`Gemini model '${requestedModelId}' is unavailable for this session. Available models: ${this.availableModels
|
|
1448
|
+
.map((model) => model.id)
|
|
1449
|
+
.join(", ")}`);
|
|
983
1450
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
if (session?.sessionId === sessionId) {
|
|
987
|
-
return session;
|
|
1451
|
+
if (!this.connection || !this.sessionId) {
|
|
1452
|
+
throw new Error("Gemini ACP session is not ready");
|
|
988
1453
|
}
|
|
1454
|
+
await this.connection.unstable_setSessionModel({
|
|
1455
|
+
sessionId: this.sessionId,
|
|
1456
|
+
modelId: requestedModelId,
|
|
1457
|
+
});
|
|
1458
|
+
this.currentModelId = requestedModelId;
|
|
1459
|
+
this.config.model = requestedModelId;
|
|
989
1460
|
}
|
|
990
|
-
return null;
|
|
991
1461
|
}
|
|
992
|
-
|
|
993
|
-
|
|
1462
|
+
beginHistoryCapture(expectedEventCount) {
|
|
1463
|
+
this.historyEvents = [];
|
|
1464
|
+
this.expectedHistoryEventCount = expectedEventCount;
|
|
1465
|
+
this.capturedHistoryEventCount = 0;
|
|
1466
|
+
this.historyCaptureActive = true;
|
|
1467
|
+
this.historyReadyPromise = new Promise((resolve) => {
|
|
1468
|
+
this.resolveHistoryReady = resolve;
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
scheduleHistoryCaptureFinish() {
|
|
1472
|
+
if (!this.historyCaptureActive) {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (this.historyIdleTimer) {
|
|
1476
|
+
clearTimeout(this.historyIdleTimer);
|
|
1477
|
+
}
|
|
1478
|
+
this.historyIdleTimer = setTimeout(() => {
|
|
1479
|
+
this.finishHistoryCapture();
|
|
1480
|
+
}, GEMINI_HISTORY_FALLBACK_IDLE_MS);
|
|
1481
|
+
}
|
|
1482
|
+
finishHistoryCapture() {
|
|
1483
|
+
if (this.historyIdleTimer) {
|
|
1484
|
+
clearTimeout(this.historyIdleTimer);
|
|
1485
|
+
this.historyIdleTimer = null;
|
|
1486
|
+
}
|
|
1487
|
+
if (!this.historyCaptureActive) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
this.historyCaptureActive = false;
|
|
1491
|
+
this.expectedHistoryEventCount = null;
|
|
1492
|
+
this.capturedHistoryEventCount = 0;
|
|
1493
|
+
this.resolveHistoryReady?.();
|
|
1494
|
+
this.resolveHistoryReady = null;
|
|
1495
|
+
}
|
|
1496
|
+
async handleSessionUpdate(notification) {
|
|
1497
|
+
if (notification.sessionId !== this.sessionId) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (notification.update.sessionUpdate === "current_mode_update") {
|
|
1501
|
+
this.currentModeId = notification.update.currentModeId;
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const includeUserMessages = this.historyCaptureActive;
|
|
1505
|
+
const events = sessionUpdateToEvents(notification.update, this.toolStates, {
|
|
1506
|
+
includeUserMessages,
|
|
1507
|
+
});
|
|
1508
|
+
if (events.length === 0) {
|
|
994
1509
|
return;
|
|
995
1510
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1511
|
+
if (this.historyCaptureActive && !this.activeTurnQueue) {
|
|
1512
|
+
this.historyEvents.push(...events);
|
|
1513
|
+
this.capturedHistoryEventCount += events.length;
|
|
1514
|
+
if (this.expectedHistoryEventCount !== null &&
|
|
1515
|
+
this.capturedHistoryEventCount >= this.expectedHistoryEventCount) {
|
|
1516
|
+
this.finishHistoryCapture();
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
this.scheduleHistoryCaptureFinish();
|
|
1520
|
+
}
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
for (const event of events) {
|
|
1524
|
+
this.emitLiveEvent(event);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
emitLiveEvent(event) {
|
|
1528
|
+
this.activeTurnQueue?.push(event);
|
|
1529
|
+
}
|
|
1530
|
+
async handlePermissionRequest(params) {
|
|
1531
|
+
if (this.currentRunInterrupted) {
|
|
1532
|
+
return {
|
|
1533
|
+
outcome: {
|
|
1534
|
+
outcome: "cancelled",
|
|
1535
|
+
},
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const request = toGeminiPermissionRequest(params);
|
|
1539
|
+
const options = params.options.map((option) => ({
|
|
1540
|
+
optionId: option.optionId,
|
|
1541
|
+
name: option.name,
|
|
1542
|
+
kind: option.kind,
|
|
1543
|
+
}));
|
|
1544
|
+
return await new Promise((resolve, reject) => {
|
|
1545
|
+
this.pendingPermissions.set(request.id, {
|
|
1546
|
+
request,
|
|
1547
|
+
options,
|
|
1548
|
+
resolve,
|
|
1549
|
+
reject,
|
|
1550
|
+
});
|
|
1551
|
+
this.emitLiveEvent({
|
|
1552
|
+
type: "permission_requested",
|
|
1553
|
+
provider: GEMINI_PROVIDER,
|
|
1554
|
+
request,
|
|
1555
|
+
});
|
|
1556
|
+
});
|
|
999
1557
|
}
|
|
1000
1558
|
}
|
|
1001
1559
|
//# sourceMappingURL=gemini-agent.js.map
|