diff --git a/backend/bookslm_routes.py b/backend/bookslm_routes.py
index 003642b..a0e4ec8 100644
--- a/backend/bookslm_routes.py
+++ b/backend/bookslm_routes.py
@@ -536,18 +536,6 @@ async def api_bookslm_agent(
# run no longer pauses on every subsequent mutating call.
ctx.confirmed = True
- async def _llm(msgs, tool_schemas):
- return await chat_completion(
- msgs,
- tools=tool_schemas,
- provider=req.provider,
- model=req.model,
- temperature=0.3,
- # Tool-call arguments can carry a whole file body (e.g. a generated
- # table): leave more room than the plain-chat default.
- max_tokens=8192,
- )
-
async def generate_sse():
import asyncio
@@ -561,6 +549,23 @@ async def api_bookslm_agent(
yield f"event: error\ndata: {error_data}\n\n"
return
+ # #187: resolve the provider like /chat does — the agent must use
+ # the same engine the SSE "provider" tag reports (the raw
+ # req.provider could name an unavailable provider and silently
+ # fall back to another one via _get_provider_config).
+ async def _llm(msgs, tool_schemas):
+ return await chat_completion(
+ msgs,
+ tools=tool_schemas,
+ provider=cfg_name,
+ model=req.model,
+ temperature=0.3,
+ # Tool-call arguments can carry a whole file body (e.g. a
+ # generated table): leave more room than the plain-chat
+ # default.
+ max_tokens=8192,
+ )
+
# Stream tool events live: each executed step is pushed on the
# queue by the loop callback and emitted as soon as it happens,
# so the UI can grow its « N steps » block while thinking.
diff --git a/backend/tools/registry.py b/backend/tools/registry.py
index 652802e..18f676f 100644
--- a/backend/tools/registry.py
+++ b/backend/tools/registry.py
@@ -117,9 +117,22 @@ def list_tools(*, scope: ToolScope | None = None) -> list[ToolSpec]:
return specs
+# ponytail: tool schemas are static after import (registration is decorator
+# only); keying the cache on len(_REGISTRY) invalidates it if a tool is ever
+# registered at runtime. Rebuilding 50 pydantic JSON schemas cost ~27 ms per
+# agent/MCP request.
+_SCHEMAS_CACHE: dict[Any, list[dict[str, Any]]] = {}
+
+
def get_tool_schemas(*, scope: ToolScope | None = None) -> list[dict[str, Any]]:
- """Return OpenAI-compatible schemas for registered tools."""
- return [spec.openai_schema() for spec in list_tools(scope=scope)]
+ """Return OpenAI-compatible schemas for registered tools (cached)."""
+ key = (scope, len(_REGISTRY))
+ cached = _SCHEMAS_CACHE.get(key)
+ if cached is None:
+ cached = [spec.openai_schema() for spec in list_tools(scope=scope)]
+ _SCHEMAS_CACHE.clear()
+ _SCHEMAS_CACHE[key] = cached
+ return [dict(s) for s in cached]
def _audit(ctx: ToolContext, spec: ToolSpec, arguments: dict[str, Any], *, ok: bool, error: str | None = None) -> None:
diff --git a/frontend/index.html b/frontend/index.html
index b005234..749838f 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4674,9 +4674,9 @@
conversation.
- Le bouton « mode agent » active les outils (lire, lister,
- chercher) ; les actions de modification demandent une
- confirmation avec aperçu des changements.
+ L'assistant utilise toujours ses outils (lire, lister,
+ chercher, modifier) ; les actions de modification demandent
+ une confirmation avec aperçu des changements.
Les actions s'affichent dans le fil : l'assistant regroupe les
diff --git a/frontend/js/ai-quick-actions.js b/frontend/js/ai-quick-actions.js
index 8fb734b..d112407 100644
--- a/frontend/js/ai-quick-actions.js
+++ b/frontend/js/ai-quick-actions.js
@@ -46,8 +46,8 @@ export const ACTION_CATALOG = Object.freeze([
{ id: 'checklist', cat: 'structure', icon: 'list-checks', labelKey: 'qa.checklist', promptKey: 'qa.checklist.prompt' },
{ id: 'plan', cat: 'structure', icon: 'list-ordered', labelKey: 'qa.plan', promptKey: 'qa.plan.prompt' },
{ id: 'memo', cat: 'structure', icon: 'scroll-text', labelKey: 'qa.memo', promptKey: 'qa.memo.prompt' },
- { id: 'frontmatter', cat: 'structure', icon: 'braces', labelKey: 'qa.frontmatter', promptKey: 'qa.frontmatter.prompt', agent: true },
- { id: 'frontmatter_update', cat: 'structure', icon: 'refresh-cw', labelKey: 'qa.frontmatter_update', promptKey: 'qa.frontmatter_update.prompt', agent: true },
+ { id: 'frontmatter', cat: 'structure', icon: 'braces', labelKey: 'qa.frontmatter', promptKey: 'qa.frontmatter.prompt' },
+ { id: 'frontmatter_update', cat: 'structure', icon: 'refresh-cw', labelKey: 'qa.frontmatter_update', promptKey: 'qa.frontmatter_update.prompt' },
{ id: 'backlinks', cat: 'structure', icon: 'link-2', labelKey: 'qa.backlinks', promptKey: 'qa.backlinks.prompt' },
{ id: 'sections', cat: 'structure', icon: 'heading', labelKey: 'qa.sections', promptKey: 'qa.sections.prompt' },
// ── Code & Scripts ───────────────────────────────────────────────────
diff --git a/frontend/js/bookslm.js b/frontend/js/bookslm.js
index bcf3f68..c087514 100644
--- a/frontend/js/bookslm.js
+++ b/frontend/js/bookslm.js
@@ -151,9 +151,8 @@ class BooksLM {
this._sessions = [];
this._currentSessionId = null;
this._pendingNewSession = false;
- // Agent mode: routes chat through /api/ai/bookslm/agent so the model can
- // call read/search tools and propose mutations (confirmation cards).
- this._agentMode = this._readAgentMode();
+ // #187: the assistant is always in agent mode (tools on /agent endpoint);
+ // the former header toggle was removed — images still use the /chat path.
// Ad-hoc context added with `@` (files/directories) and images attached
// by paste or by mentioning an image file.
this._adhocFiles = [];
@@ -188,33 +187,6 @@ class BooksLM {
this._extensions = null;
}
- _readAgentMode() {
- try {
- return localStorage.getItem('obsigate-bookslm-agent') === 'true';
- } catch {
- return false;
- }
- }
-
- _toggleAgentMode() {
- this._agentMode = !this._agentMode;
- try {
- localStorage.setItem('obsigate-bookslm-agent', this._agentMode ? 'true' : 'false');
- } catch { /* private mode */ }
- this._updateAgentToggle();
- }
-
- _updateAgentToggle() {
- if (!this._panel) return;
- const btn = this._panel.querySelector('.bookslm-btn-agent');
- if (!btn) return;
- btn.classList.toggle('active', this._agentMode);
- const label = this._agentMode ? t('ai.agent_mode_on') : t('ai.agent_mode_off');
- btn.title = label;
- btn.setAttribute('aria-label', label);
- btn.setAttribute('aria-pressed', this._agentMode ? 'true' : 'false');
- }
-
// ── Public API ──────────────────────────────────────────────────────
/**
@@ -966,7 +938,6 @@ class BooksLM {