import { loadConfig, getUnderlyingProviderKey } from '@agi-cli/sdk'; import { getDb } from '@agi-cli/database'; import { sessions } from '@agi-cli/database/schema'; import { eq } from 'drizzle-orm'; import { resolveModel } from '../provider/index.ts'; import { resolveAgentConfig } from './registry.ts'; import { composeSystemPrompt } from '../prompt/builder.ts'; import { discoverProjectTools } from '@agi-cli/sdk'; import { adaptTools } from '../../tools/adapter.ts'; import { buildDatabaseTools } from '../../tools/database/index.ts'; import { debugLog, time } from '../debug/index.ts'; import { buildHistoryMessages } from '../message/history-builder.ts'; import { getMaxOutputTokens } from '../utils/token.ts'; import { setupToolContext } from '../tools/setup.ts'; import { getCompactionSystemPrompt } from '../message/compaction.ts'; import { detectOAuth, adaptRunnerCall } from '../provider/oauth-adapter.ts'; import type { RunOpts } from '../session/queue.ts'; import type { ToolAdapterContext } from '../../tools/adapter.ts'; export interface SetupResult { cfg: Awaited>; db: Awaited>; agentCfg: Awaited>; history: Awaited>; system: string; systemComponents: string[]; additionalSystemMessages: Array<{ role: 'system' | 'user'; content: string }>; model: Awaited>; maxOutputTokens: number | undefined; effectiveMaxOutputTokens: number | undefined; toolset: ReturnType; sharedCtx: ToolAdapterContext; firstToolTimer: ReturnType; firstToolSeen: () => boolean; providerOptions: Record; needsSpoof: boolean; } const THINKING_BUDGET = 16000; export async function setupRunner(opts: RunOpts): Promise { const cfgTimer = time('runner:loadConfig+db'); const cfg = await loadConfig(opts.projectRoot); const db = await getDb(cfg.projectRoot); cfgTimer.end(); const agentTimer = time('runner:resolveAgentConfig'); const agentCfg = await resolveAgentConfig(cfg.projectRoot, opts.agent); agentTimer.end({ agent: opts.agent }); const agentPrompt = agentCfg.prompt || ''; const historyTimer = time('runner:buildHistory'); let history: Awaited>; if (opts.isCompactCommand && opts.compactionContext) { debugLog('[RUNNER] Using minimal history for /compact command'); history = []; } else { history = await buildHistoryMessages(db, opts.sessionId); } historyTimer.end({ messages: history.length }); const sessionRows = await db .select() .from(sessions) .where(eq(sessions.id, opts.sessionId)) .limit(1); const contextSummary = sessionRows[0]?.contextSummary ?? undefined; if (contextSummary) { debugLog( `[RUNNER] Using context summary from compaction (${contextSummary.length} chars)`, ); } const isFirstMessage = !history.some((m) => m.role === 'assistant'); debugLog(`[RUNNER] isFirstMessage: ${isFirstMessage}`); debugLog(`[RUNNER] userContext provided: ${opts.userContext ? 'YES' : 'NO'}`); if (opts.userContext) { debugLog( `[RUNNER] userContext value: ${opts.userContext.substring(0, 100)}${opts.userContext.length > 100 ? '...' : ''}`, ); } const systemTimer = time('runner:composeSystemPrompt'); const { getAuth } = await import('@agi-cli/sdk'); const auth = await getAuth(opts.provider, cfg.projectRoot); const oauth = detectOAuth(opts.provider, auth); debugLog( `[RUNNER] needsSpoof (OAuth): ${oauth.needsSpoof}, isOpenAIOAuth: ${oauth.isOpenAIOAuth}`, ); debugLog( `[RUNNER] spoofPrompt: ${oauth.spoofPrompt ? `present (${opts.provider})` : 'none'}`, ); const composed = await composeSystemPrompt({ provider: opts.provider, model: opts.model, projectRoot: cfg.projectRoot, agentPrompt, oneShot: opts.oneShot, spoofPrompt: undefined, includeProjectTree: isFirstMessage, userContext: opts.userContext, contextSummary, }); const rawMaxOutputTokens = getMaxOutputTokens(opts.provider, opts.model); const adapted = adaptRunnerCall(oauth, composed, { provider: opts.provider, rawMaxOutputTokens, }); const { system } = adapted; const { systemComponents, additionalSystemMessages } = adapted; systemTimer.end(); debugLog( `[system] summary: ${JSON.stringify({ components: systemComponents, length: system.length, })}`, ); if (opts.isCompactCommand && opts.compactionContext) { debugLog('[RUNNER] Injecting compaction context for /compact command'); const compactPrompt = getCompactionSystemPrompt(); additionalSystemMessages.push({ role: 'system', content: compactPrompt, }); additionalSystemMessages.push({ role: 'user', content: `Please summarize this conversation:\n\n\n${opts.compactionContext}\n`, }); } const toolsTimer = time('runner:discoverTools'); const allTools = await discoverProjectTools(cfg.projectRoot); if (opts.agent === 'research') { const currentSession = sessionRows[0]; const parentSessionId = currentSession?.parentSessionId ?? null; const dbTools = buildDatabaseTools(cfg.projectRoot, parentSessionId); for (const dt of dbTools) { allTools.push(dt); } debugLog( `[tools] Added ${dbTools.length} database tools for research agent (parent: ${parentSessionId ?? 'none'})`, ); } toolsTimer.end({ count: allTools.length }); const allowedNames = new Set([...(agentCfg.tools || []), 'finish']); const gated = allTools.filter((tool) => allowedNames.has(tool.name)); debugLog(`[tools] ${gated.length} allowed tools`); debugLog(`[RUNNER] About to create model with provider: ${opts.provider}`); debugLog(`[RUNNER] About to create model ID: ${opts.model}`); const model = await resolveModel(opts.provider, opts.model, cfg, { sessionId: opts.sessionId, messageId: opts.assistantMessageId, }); debugLog( `[RUNNER] Model created: ${JSON.stringify({ id: model.modelId, provider: model.provider })}`, ); const maxOutputTokens = adapted.maxOutputTokens; debugLog(`[RUNNER] maxOutputTokens for ${opts.model}: ${maxOutputTokens}`); const { sharedCtx, firstToolTimer, firstToolSeen } = await setupToolContext( opts, db, ); const providerAuth = await getAuth(opts.provider, opts.projectRoot); const authType = providerAuth?.type; const toolset = adaptTools(gated, sharedCtx, opts.provider, authType); const providerOptions = { ...adapted.providerOptions }; let effectiveMaxOutputTokens = maxOutputTokens; if (opts.reasoningText) { const underlyingProvider = getUnderlyingProviderKey( opts.provider, opts.model, ); if (underlyingProvider === 'anthropic') { providerOptions.anthropic = { thinking: { type: 'enabled', budgetTokens: THINKING_BUDGET }, }; if (maxOutputTokens && maxOutputTokens > THINKING_BUDGET) { effectiveMaxOutputTokens = maxOutputTokens - THINKING_BUDGET; } } else if (underlyingProvider === 'openai') { providerOptions.openai = { ...((providerOptions.openai as Record) || {}), reasoningEffort: 'high', reasoningSummary: 'auto', }; } else if (underlyingProvider === 'google') { const isGemini3 = opts.model.includes('gemini-3'); providerOptions.google = { thinkingConfig: isGemini3 ? { thinkingLevel: 'high', includeThoughts: true } : { thinkingBudget: THINKING_BUDGET }, }; } else if (underlyingProvider === 'openai-compatible') { providerOptions.openaiCompatible = { reasoningEffort: 'high', }; } } return { cfg, db, agentCfg, history, system, systemComponents, additionalSystemMessages, model, maxOutputTokens, effectiveMaxOutputTokens, toolset, sharedCtx, firstToolTimer, firstToolSeen, providerOptions, needsSpoof: oauth.needsSpoof, }; } export function buildMessages( additionalSystemMessages: Array<{ role: string; content: string }>, history: Array<{ role: string; content: string | Array }>, isFirstMessage: boolean, ): Array<{ role: string; content: string | Array }> { const messagesWithSystemInstructions: Array<{ role: string; content: string | Array; }> = [...additionalSystemMessages, ...history]; if (!isFirstMessage) { messagesWithSystemInstructions.push({ role: 'user', content: 'SYSTEM REMINDER: You are continuing an existing session. When you have completed the task, you MUST stream a text summary of what you did to the user, and THEN call the `finish` tool. Do not call `finish` without a summary.', }); } return messagesWithSystemInstructions; }