feat(assistant): #91 steps humains en direct + outils web_search/fetch_url (labels backend, SSE step, garde SSRF)
CI / lint (push) Successful in 1m22s
CI / security (push) Successful in 55s
CI / test (push) Successful in 3m0s
CI / build (push) Successful in 51s
CI / e2e (push) Successful in 10m41s

This commit is contained in:
2026-09-15 17:46:51 -04:00
parent 18b7dee2f6
commit 19d5b931c4
14 changed files with 745 additions and 32 deletions
+41 -16
View File
@@ -2058,22 +2058,35 @@ class BooksLM {
// ── Tool activity & confirmations (agent mode) ──────────────────────
/** Agent tool calls collapse into a discreet “N steps” line (Notion-style). */
_renderToolActivity(toolCalls) {
const wrap = document.createElement('details');
wrap.className = 'bookslm-tool-trace';
const summary = document.createElement('summary');
summary.textContent = t('ai.steps_count', { count: toolCalls.length });
wrap.appendChild(summary);
for (const call of toolCalls) {
const line = document.createElement('div');
line.className = 'bookslm-tool-line' + (call.ok === false ? ' failed' : '');
line.textContent = `${call.ok === false ? '⚠' : '🔧'} ${t('ai.tool_call', { name: call.name })}`;
if (call.ok === false) line.title = t('ai.tool_call_failed', { name: call.name });
wrap.appendChild(line);
/** Human one-liner for a recorded step (backend label key or legacy name). */
_stepText(call) {
const step = call.step;
if (step && step.key) {
const key = `ai.step.${step.key}`;
const label = t(key, step.params || {});
if (label && label !== key) return label;
}
return t('ai.tool_call', { name: call.name });
}
/** Agent tool calls collapse into a discreet “N steps” line (Notion-style). */
_renderToolActivity(toolCalls) {
const wrap = document.createElement('details');
wrap.className = 'bookslm-tool-trace';
const summary = document.createElement('summary');
const countKey = toolCalls.length > 1 ? 'ai.steps_count_plural' : 'ai.steps_count';
summary.textContent = t(countKey, { count: toolCalls.length });
wrap.appendChild(summary);
for (const call of toolCalls) {
const line = document.createElement('div');
line.className = 'bookslm-tool-line' + (call.ok === false ? ' failed' : '');
line.textContent = `${call.ok === false ? '⚠' : '·'} ${this._stepText(call)}`;
if (call.ok === false) line.title = t('ai.tool_call_failed', { name: call.name });
else if (call.step && call.step.params && call.step.params.value) line.title = call.step.params.value;
wrap.appendChild(line);
}
return wrap;
}
return wrap;
}
_renderConfirmationCard(msg) {
const conf = msg.confirmation;
@@ -2393,10 +2406,22 @@ class BooksLM {
}
if (currentEvent === 'tool') {
assistantMsg.toolCalls.push({ name: data.name, ok: data.ok !== false, arguments: data.arguments || {} });
assistantMsg.toolCalls.push({
name: data.name,
ok: data.ok !== false,
arguments: data.arguments || {},
step: data.step || null,
});
this._setActivity('working', t('ai.activity_tool', { name: data.name }));
return;
}
// Intermediate reasoning note streamed by the agent (Notion-style
// ". thought" line inside the steps block).
if (currentEvent === 'step' && data.step) {
assistantMsg.toolCalls.push({ name: '', ok: true, arguments: {}, step: data.step });
this._setActivity('working', t('ai.activity_thinking'));
return;
}
if (currentEvent === 'confirmation') {
assistantMsg.confirmation = {
pending: data.pending || data,