import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils"; import { asBoolean, asNumber, asString, ensureAbsoluteDirectory, ensureCommandResolvable, ensurePathInEnv, filterConflictingArgs, redactEnvForLogs, renderTemplate, runChildProcess, } from "@agentcompany/adapter-utils/server-utils"; import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js"; import { describeGeminiFailure, detectGeminiAuthRequired, isGeminiTurnLimitResult, isGeminiUnknownSessionError, parseGeminiJsonl, } from "./parse.js"; import { firstNonEmptyLine } from "@agentcompany/adapter-utils/test-utils"; import { resolveGeminiBillingType, renderEnvNote, renderApiAccessNote, ensureGeminiSkillsInjected, parseManagedSkills, parseExtraArgs, buildCommonAgentCompanyEnv, resolveSessionResumeState, } from "./execute-helpers.js"; export async function execute(ctx: AdapterExecutionContext): Promise { const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx; const promptTemplate = asString( config.promptTemplate, "{{context.taskTitle}}\n\n{{context.taskDescription}}", ); const command = asString(config.command, "gemini"); const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim(); const sandbox = asBoolean(config.sandbox, false); const { env, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef } = await buildCommonAgentCompanyEnv( { runId, agent, config, context, authToken }, { ensureAbsoluteDirectory }, ); const managedSkills = parseManagedSkills(config); await ensureGeminiSkillsInjected(managedSkills, onLog); const billingType = resolveGeminiBillingType(env); const runtimeEnv = ensurePathInEnv(env); await ensureCommandResolvable(command, cwd, runtimeEnv); const timeoutSec = asNumber(config.timeoutSec, 0); const graceSec = asNumber(config.graceSec, 20); const extraArgs = parseExtraArgs(config); const { sessionId, runtimeSessionId, runtimeSessionCwd, canResumeSession } = resolveSessionResumeState(runtime, cwd); if (runtimeSessionId && !canResumeSession) { await onLog( "stderr", `[agent-company] Gemini session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`, ); } const instructionsFilePath = asString(config.instructionsFilePath, "").trim(); const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : ""; let instructionsPrefix = ""; if (instructionsFilePath) { try { const instructionsContents = await fs.readFile(instructionsFilePath, "utf8"); instructionsPrefix = `${instructionsContents}\n\n` + `The above agent instructions were loaded from ${instructionsFilePath}. ` + `Resolve any relative file references from ${instructionsDir}.\n\n`; await onLog( "stderr", `[agent-company] Loaded agent instructions file: ${instructionsFilePath}\n`, ); } catch (err) { const reason = err instanceof Error ? err.message : String(err); await onLog( "stderr", `[agent-company] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`, ); } } const renderedPrompt = renderTemplate(promptTemplate, { agentId: agent.id, companyId: agent.companyId, runId, company: { id: agent.companyId }, agent, run: { id: runId, source: "on_demand" }, context, }); const envNote = renderEnvNote(env); const apiAccessNote = renderApiAccessNote(env); const prompt = `${instructionsPrefix}${envNote}${apiAccessNote}${renderedPrompt}`; const SAFE_ARG_LIMIT = 30_000; const useStdin = prompt.length > SAFE_ARG_LIMIT; const commandNotes = (() => { const notes: string[] = [useStdin ? "Prompt passed via stdin (exceeds command-line length limit)." : "Prompt is passed to Gemini as the final positional argument."]; notes.push("Added --approval-mode yolo for unattended execution."); if (!instructionsFilePath) return notes; if (instructionsPrefix.length > 0) { notes.push( `Loaded agent instructions from ${instructionsFilePath}`, `Prepended instructions + path directive to prompt (relative references from ${instructionsDir}).`, ); return notes; } notes.push( `Configured instructionsFilePath ${instructionsFilePath}, but file could not be read; continuing without injected instructions.`, ); return notes; })(); let promptFilePath: string | null = null; if (useStdin) { promptFilePath = path.join(os.tmpdir(), `agentcompany-gemini-prompt-${runId}.txt`); await fs.writeFile(promptFilePath, prompt, "utf8"); } const buildArgs = (resumeSessionId: string | null) => { const args = ["--output-format", "stream-json"]; if (resumeSessionId) args.push("--resume", resumeSessionId); if (model && model !== DEFAULT_GEMINI_LOCAL_MODEL) args.push("--model", model); args.push("--approval-mode", "yolo"); if (sandbox) { args.push("--sandbox"); } else { args.push("--sandbox=none"); } if (extraArgs.length > 0) { const managedFlags = [ "--approval-mode", "--output-format", "--model", "--sandbox", ]; const cleaned = filterConflictingArgs(extraArgs, managedFlags); if (cleaned.length > 0) args.push(...cleaned); } if (!useStdin) { args.push(prompt); } return args; }; const runAttempt = async (resumeSessionId: string | null) => { const args = buildArgs(resumeSessionId); if (onMeta) { await onMeta({ adapterType: "gemini_local", command, cwd, commandNotes, commandArgs: args.map((value, index) => ( index === args.length - 1 ? `` : value )), env: redactEnvForLogs(env), prompt, context, }); } const proc = await runChildProcess(runId, command, args, { cwd, env, timeoutSec, graceSec, onLog, ...(useStdin ? { stdin: prompt } : {}), }); return { proc, parsed: parseGeminiJsonl(proc.stdout), }; }; const toResult = ( attempt: { proc: { exitCode: number | null; signal: string | null; timedOut: boolean; stdout: string; stderr: string; }; parsed: ReturnType; }, clearSessionOnMissingSession = false, isRetry = false, ): AdapterExecutionResult => { const authMeta = detectGeminiAuthRequired({ parsed: attempt.parsed.resultEvent, stdout: attempt.proc.stdout, stderr: attempt.proc.stderr, }); if (attempt.proc.timedOut) { return { exitCode: attempt.proc.exitCode, signal: attempt.proc.signal, timedOut: true, errorMessage: `Timed out after ${timeoutSec}s`, errorCode: authMeta.requiresAuth ? "gemini_auth_required" : null, clearSession: clearSessionOnMissingSession, }; } const clearSessionForTurnLimit = isGeminiTurnLimitResult(attempt.parsed.resultEvent, attempt.proc.exitCode); const canFallbackToRuntimeSession = !isRetry; const resolvedSessionId = attempt.parsed.sessionId ?? (canFallbackToRuntimeSession ? (runtimeSessionId ?? runtime.sessionId ?? null) : null); const resolvedSessionParams = resolvedSessionId ? ({ sessionId: resolvedSessionId, cwd, ...(workspaceId ? { workspaceId } : {}), ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}), ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}), } as Record) : null; const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : ""; const stderrLine = firstNonEmptyLine(attempt.proc.stderr); const structuredFailure = attempt.parsed.resultEvent ? describeGeminiFailure(attempt.parsed.resultEvent) : null; const fallbackErrorMessage = parsedError || structuredFailure || stderrLine || `Gemini exited with code ${attempt.proc.exitCode ?? -1}`; return { exitCode: attempt.proc.exitCode, signal: attempt.proc.signal, timedOut: false, errorMessage: (attempt.proc.exitCode ?? 0) === 0 ? null : fallbackErrorMessage, errorCode: (attempt.proc.exitCode ?? 0) !== 0 && authMeta.requiresAuth ? "gemini_auth_required" : null, usage: attempt.parsed.usage, sessionId: resolvedSessionId, sessionParams: resolvedSessionParams, sessionDisplayId: resolvedSessionId, provider: "google", model, billingType, costUsd: attempt.parsed.costUsd, resultJson: attempt.parsed.resultEvent ?? { stdout: attempt.proc.stdout, stderr: attempt.proc.stderr, }, summary: attempt.parsed.summary, question: attempt.parsed.question, clearSession: clearSessionForTurnLimit || Boolean(clearSessionOnMissingSession && !resolvedSessionId), }; }; try { const initial = await runAttempt(sessionId); if ( sessionId && !initial.proc.timedOut && (initial.proc.exitCode ?? 0) !== 0 && isGeminiUnknownSessionError(initial.proc.stdout, initial.proc.stderr) ) { await onLog( "stderr", `[agent-company] Gemini resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`, ); const retry = await runAttempt(null); return toResult(retry, true, true); } return toResult(initial); } finally { if (promptFilePath) { await fs.unlink(promptFilePath).catch(() => {}); } } }