2026-10-18 23:02:44 +00:00
|
|
|
import { estimateTextTokens, estimateTokens } from "./context";
|
|
|
|
|
import { runToolCalls, type ToolUseBlock } from "./tool-runner";
|
2026-10-11 00:11:20 +00:00
|
|
|
import type {
|
|
|
|
|
AgentSkill,
|
|
|
|
|
AgentUI,
|
|
|
|
|
ContentBlock,
|
|
|
|
|
LLMProvider,
|
|
|
|
|
Message,
|
|
|
|
|
ModeInitResult,
|
|
|
|
|
PermissionPolicy,
|
2026-10-15 21:49:58 +00:00
|
|
|
PermissionSource,
|
2026-10-11 00:11:20 +00:00
|
|
|
SlashCommand,
|
|
|
|
|
Tool,
|
|
|
|
|
} from "./types";
|
|
|
|
|
|
|
|
|
|
export interface AgentSession {
|
|
|
|
|
provider: LLMProvider;
|
|
|
|
|
system: string;
|
|
|
|
|
tools: Map<string, Tool>;
|
|
|
|
|
commands: SlashCommand[];
|
|
|
|
|
skills: AgentSkill[];
|
|
|
|
|
permissions?: PermissionPolicy;
|
2026-10-15 21:49:58 +00:00
|
|
|
/** Fichiers dont viennent les règles de permissions (affichage avec /permissions). */
|
|
|
|
|
permissionSources: PermissionSource[];
|
2026-10-11 00:11:20 +00:00
|
|
|
env?: Record<string, string>;
|
|
|
|
|
messages: Message[];
|
2026-10-18 23:02:44 +00:00
|
|
|
/** Résumé compacté qui sert de contexte de départ (présent dans `system`) ; absent : contexte MAIA.md / CLAUDE.md. */
|
|
|
|
|
startContext?: string;
|
2026-10-15 22:54:08 +00:00
|
|
|
/** Taille du contexte (tokens) après la dernière réponse du LLM : valeur renvoyée par le serveur, sinon estimation. */
|
|
|
|
|
contextTokens: number;
|
2026-10-18 23:02:44 +00:00
|
|
|
/** true : contextTokens vient du serveur (exact) ; false : estimation. */
|
|
|
|
|
contextExact: boolean;
|
|
|
|
|
/** Facteur de calibration de l'estimation (tokens réels / tokens estimés), mesuré à chaque réponse du serveur. */
|
|
|
|
|
tokenScale: number;
|
2026-10-12 07:16:26 +00:00
|
|
|
/** Contexte ajouté par l'utilisateur hors conversation (ex. résultat d'une commande "!"), joint à sa prochaine demande. */
|
|
|
|
|
pendingContext: string[];
|
2026-10-11 00:11:20 +00:00
|
|
|
ui: AgentUI;
|
|
|
|
|
cwd: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createSession(opts: {
|
|
|
|
|
provider: LLMProvider;
|
|
|
|
|
init: ModeInitResult;
|
|
|
|
|
ui: AgentUI;
|
|
|
|
|
cwd: string;
|
2026-10-18 23:02:44 +00:00
|
|
|
startContext?: string;
|
2026-10-11 00:11:20 +00:00
|
|
|
}): 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,
|
2026-10-15 22:54:08 +00:00
|
|
|
permissionSources: opts.init.permissionSources ?? [],
|
2026-10-11 00:11:20 +00:00
|
|
|
env: opts.init.env,
|
|
|
|
|
messages: [],
|
2026-10-18 23:02:44 +00:00
|
|
|
startContext: opts.startContext,
|
2026-10-15 22:54:08 +00:00
|
|
|
contextTokens: 0,
|
2026-10-18 23:02:44 +00:00
|
|
|
contextExact: false,
|
|
|
|
|
tokenScale: 1,
|
2026-10-12 07:16:26 +00:00
|
|
|
pendingContext: [],
|
2026-10-11 00:11:20 +00:00
|
|
|
ui: opts.ui,
|
|
|
|
|
cwd: opts.cwd,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MAX_ITERATIONS = 25;
|
2026-10-18 23:02:44 +00:00
|
|
|
const MIN_CALIBRATION_TOKENS = 200; // en dessous, le rapport réel/estimé est trop instable pour calibrer
|
|
|
|
|
const MIN_SCALE = 0.25;
|
|
|
|
|
const MAX_SCALE = 4;
|
2026-10-11 00:11:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Un tour complet : message utilisateur → (LLM → outils)* → réponse finale. */
|
|
|
|
|
export async function runTurn(
|
|
|
|
|
session: AgentSession,
|
|
|
|
|
userText: string,
|
|
|
|
|
signal?: AbortSignal,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const { ui } = session;
|
2026-10-12 07:16:26 +00:00
|
|
|
// 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 }] });
|
2026-10-11 00:11:20 +00:00
|
|
|
|
|
|
|
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
|
|
|
const assistant: ContentBlock[] = [];
|
|
|
|
|
let text = "";
|
2026-10-15 22:54:08 +00:00
|
|
|
let usedTokens: number | null = null;
|
2026-10-18 23:02:44 +00:00
|
|
|
const sentEstimate = rawSessionEstimate(session); // taille estimée de la requête, pour calibrer l'estimation
|
2026-10-11 00:11:20 +00:00
|
|
|
|
|
|
|
|
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 });
|
2026-10-15 22:54:08 +00:00
|
|
|
} else if (ev.type === "usage") {
|
|
|
|
|
// Requête + réponse : c'est la taille du contexte une fois la réponse ajoutée à la conversation
|
|
|
|
|
usedTokens = ev.inputTokens + ev.outputTokens;
|
2026-10-18 23:02:44 +00:00
|
|
|
if (sentEstimate >= MIN_CALIBRATION_TOKENS && ev.inputTokens > 0) {
|
|
|
|
|
session.tokenScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, ev.inputTokens / sentEstimate));
|
|
|
|
|
}
|
2026-10-11 00:11:20 +00:00
|
|
|
}
|
|
|
|
|
// "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 });
|
2026-10-15 22:54:08 +00:00
|
|
|
session.contextTokens = usedTokens ?? estimateSessionTokens(session);
|
2026-10-18 23:02:44 +00:00
|
|
|
session.contextExact = usedTokens !== null;
|
|
|
|
|
ui.onContextUsage?.(session.contextTokens, session.contextExact);
|
2026-10-11 00:11:20 +00:00
|
|
|
|
|
|
|
|
const calls = assistant.filter((b): b is ToolUseBlock => b.type === "tool_use");
|
|
|
|
|
if (calls.length === 0) return;
|
|
|
|
|
|
2026-10-18 23:02:44 +00:00
|
|
|
// 1 appel : premier plan ; 2 appels ou plus : une tâche de fond par outil, on attend toutes les fins
|
|
|
|
|
const results = await runToolCalls(session, calls, signal);
|
2026-10-11 00:11:20 +00:00
|
|
|
session.messages.push({ role: "user", content: results });
|
|
|
|
|
}
|
|
|
|
|
session.ui.onToolResult("agent", `Arrêt : plus de ${MAX_ITERATIONS} itérations`, true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 23:02:44 +00:00
|
|
|
type Estimable = Pick<AgentSession, "system" | "messages" | "tools">;
|
2026-10-11 00:11:20 +00:00
|
|
|
|
2026-10-18 23:02:44 +00:00
|
|
|
function rawSessionEstimate(session: Estimable): number {
|
|
|
|
|
return estimateTokens(session.system, session.messages, [...session.tools.values()].map((t) => t.spec));
|
|
|
|
|
}
|
2026-10-11 00:11:20 +00:00
|
|
|
|
2026-10-18 23:02:44 +00:00
|
|
|
/** Estimation calibrée du contexte de la conversation (prompt système, outils, messages), sans le contexte en attente. */
|
|
|
|
|
export function estimateSessionTokens(session: Estimable & Pick<AgentSession, "tokenScale">): number {
|
|
|
|
|
return Math.round(rawSessionEstimate(session) * session.tokenScale);
|
2026-10-11 00:11:20 +00:00
|
|
|
}
|
2026-10-15 22:54:08 +00:00
|
|
|
|
2026-10-18 23:02:44 +00:00
|
|
|
/**
|
|
|
|
|
* Taille du contexte de la session en cours : dernier compte connu (serveur ou estimation) + contexte en attente
|
|
|
|
|
* (résultats de commandes « ! », fichiers cités) qui sera joint à la prochaine demande.
|
|
|
|
|
*/
|
|
|
|
|
export function sessionContextTokens(
|
|
|
|
|
session: Pick<AgentSession, "contextTokens" | "pendingContext" | "tokenScale">,
|
|
|
|
|
): number {
|
|
|
|
|
const pending = session.pendingContext.reduce((sum, text) => sum + estimateTextTokens(text), 0);
|
|
|
|
|
return session.contextTokens + Math.round(pending * session.tokenScale);
|
2026-10-15 22:54:08 +00:00
|
|
|
}
|