import type { AgentSkill, AgentUI, ContentBlock, LLMProvider, Message, ModeInitResult, PermissionPolicy, SlashCommand, Tool, } from "./types"; export interface AgentSession { provider: LLMProvider; system: string; tools: Map; commands: SlashCommand[]; skills: AgentSkill[]; permissions?: PermissionPolicy; env?: Record; messages: Message[]; /** Contexte ajouté par l'utilisateur hors conversation (ex. résultat d'une commande "!"), joint à sa prochaine demande. */ pendingContext: string[]; ui: AgentUI; cwd: string; } export function createSession(opts: { provider: LLMProvider; init: ModeInitResult; ui: AgentUI; cwd: string; }): AgentSession { return { provider: opts.provider, system: opts.init.systemPrompt, tools: new Map(opts.init.tools.map((t) => [t.spec.name, t])), commands: opts.init.commands ?? [], skills: opts.init.skills ?? [], permissions: opts.init.permissions, env: opts.init.env, messages: [], pendingContext: [], ui: opts.ui, cwd: opts.cwd, }; } const MAX_ITERATIONS = 25; type ToolUseBlock = Extract; /** Un tour complet : message utilisateur → (LLM → outils)* → réponse finale. */ export async function runTurn( session: AgentSession, userText: string, signal?: AbortSignal, ): Promise { const { ui } = session; // Le contexte en attente est joint à la demande (un seul message : certains providers exigent l'alternance user/assistant) const context = session.pendingContext.splice(0); const text = context.length > 0 ? `${context.join("\n\n")}\n\n${userText}` : userText; session.messages.push({ role: "user", content: [{ type: "text", text }] }); for (let i = 0; i < MAX_ITERATIONS; i++) { const assistant: ContentBlock[] = []; let text = ""; ui.onThinking(true); let first = true; try { for await (const ev of session.provider.chat( { system: session.system, messages: session.messages, tools: [...session.tools.values()].map((t) => t.spec), }, signal, )) { if (first) { ui.onThinking(false); first = false; } if (ev.type === "text_delta") { text += ev.text; ui.onText(ev.text); } else if (ev.type === "tool_use") { assistant.push({ type: "tool_use", id: ev.id, name: ev.name, input: ev.input }); } // "done" : la fin du flux suffit, les appels d'outils décident de la suite } } finally { if (first) ui.onThinking(false); } if (text) { ui.onTextEnd(); assistant.unshift({ type: "text", text }); } session.messages.push({ role: "assistant", content: assistant }); const calls = assistant.filter((b): b is ToolUseBlock => b.type === "tool_use"); if (calls.length === 0) return; const results: ContentBlock[] = []; for (const call of calls) { results.push(await executeToolCall(session, call, signal)); } session.messages.push({ role: "user", content: results }); } session.ui.onToolResult("agent", `Arrêt : plus de ${MAX_ITERATIONS} itérations`, true); } async function executeToolCall( session: AgentSession, call: ToolUseBlock, signal?: AbortSignal, ): Promise { const { ui } = session; const tool = session.tools.get(call.name); const result = (content: string, isError = false): ContentBlock => ({ type: "tool_result", toolUseId: call.id, content, isError, }); if (!tool) return result(`Outil inconnu : ${call.name}`, true); const summary = tool.describe?.(call.input) ?? JSON.stringify(call.input).slice(0, 120); ui.onToolCall(call.name, summary); // Les règles de l'agent (deny > ask > allow) priment sur la permission par défaut de l'outil const verdict = session.permissions?.check(call.name, call.input, { cwd: session.cwd }); if (verdict?.decision === "deny") { const why = verdict.reason ? ` (${verdict.reason})` : ""; ui.onToolResult(call.name, `Refusé par la configuration${why}`, true); return result(`Action refusée par les règles de permissions${why}. Ne réessaie pas telle quelle.`, true); } const needsConfirm = verdict ? verdict.decision === "ask" : tool.permission === "ask"; if (needsConfirm) { const ok = await ui.confirm(`Autoriser ${call.name} : ${summary} ?`); if (!ok) { ui.onToolResult(call.name, "Refusé par l'utilisateur", true); return result("L'utilisateur a refusé cette action.", true); } } try { const output = await tool.execute(call.input, { cwd: session.cwd, model: session.provider.model, signal, env: session.env }); ui.onToolResult(call.name, output, false); return result(output); } catch (err) { const msg = (err as Error).message; ui.onToolResult(call.name, msg, true); return result(msg, true); } }