.. include:: /Includes.rst.txt .. _administration-tools: ============= Running tools ============= *Tools* are small, admin-curated PHP functions the model may call mid-generation. Where a normal completion answers in one shot, a **tool run** is a bounded *agent loop*: the model may ask to call a tool, nr-llm executes it, feeds the result back, and re-asks — until the model answers or an iteration cap is reached. The v1 consumer is the interactive :ref:`Tool Playground `. The :ref:`Tool Playground ` — the only surface that runs the agent TOOL loop — is **admin-only** (editors run one-shot tasks and decide approvals in :guilabel:`Web > AI Tasks`, which never executes tools directly). The runtime itself applies a two-tier gate: each tool declares ``requiresAdmin()``, and :php:`ToolLoopService` drops admin-only tools when the acting backend user is not an administrator. Most built-in tools require admin because a tool runs with full TYPO3 privileges, has no per-record authorization, and its return value egresses both to the configured LLM provider **and** to the rendered backend output; only a few read-only, scope-limited tools are offered to non-admin users. .. note:: The runtime design and its security and cost rationale are recorded in :ref:`ADR-038 `. Skill ingest and injection — which can steer *which* tools a run may use and *what arguments* the model chooses — are :ref:`ADR-035 ` / :ref:`ADR-036 ` and the :ref:`Managing skills ` guide. .. _administration-tools-builtin: The built-in tools ================== nr-llm ships forty-one read-only tools and five writing tools. Each is a reference implementation of the security contract: model-chosen arguments are validated and scoped, volumes are capped, and secret-bearing output is either redacted or gated behind a separate ``_raw`` variant. Thirty-eight ship **enabled**; the three unredacted ``_raw`` variants (``get_env_raw``, ``get_php_info_raw`` and ``list_be_users_raw``) and all five writing tools (``update_page_metadata``, ``set_file_alternative_text``, ``move_content_element``, ``create_content_element_draft``, ``create_translation_draft``) ship **disabled** and must be enabled deliberately. Many require admin; the read-only structure, content and file tools (``get_pagetree``, ``get_tca``, ``get_full_tca``, ``get_table_schema``, ``get_flexform_schema``, ``fluid_resolve``, ``search_records``, ``get_page_content``, ``read_records``, ``get_record_history``, ``resolve_url``, ``validate_tca``, ``list_fal_storages``, ``browse_fal_folder``, ``search_fal_files``, ``get_fal_references``, ``find_missing_files``) are offered to non-admin backend users — those self-enforce the acting user's TYPO3 permissions (page-show rights, ``tables_select``) inside the tool, so a non-admin only ever sees what the backend already grants them (see :ref:`ADR-042 `). The two tools below are the fullest illustrations of the contract: ``fetch_logs`` Returns the most recent ``sys_log`` entries, newest first, with an optional PSR ``level`` filter and a ``limit`` (default 20, **hard-capped at 50**). Personally-identifying fields — the client IP, the backend user id and the serialized payload — are **redacted by omission**, because the result egresses to the external provider. ``read_fal_asset_meta`` Returns read-only metadata (file name, MIME type, size, title, alternative text) for a single managed file (``sys_file``) by its ``uid``. The uid is model-chosen and therefore injection-steerable, so the lookup is **storage-scoped** (default: the default storage). A uid in a non-permitted storage returns the same neutral "not found or not permitted" string as a missing uid — the model cannot enumerate arbitrary files. The remaining tools follow the same pattern: ``list_fal_storages`` The file storages this run may touch (uid, name, driver, status flags). The effective set is the configured allow-list, intersected for non-admins with their file mounts; the server-side base path is never part of the output. ``browse_fal_folder`` One FAL folder: subfolders (with file count), then files with size and MIME type. Storage-relative identifiers only; anything unresolvable collapses into one neutral denial. Capped at 100 entries. ``search_fal_files`` Substring search over file name and metadata title/alternative within the accessible storages. ``%``/``_`` in the query match literally; missing files are excluded. ``get_fal_references`` Where a file is used: ``sys_file_reference`` rows as ``table:uid (field)``, hidden references marked. Soft references (RTE links, plain URLs) are not tracked — stated in the output so "no references" is never read as "safe to delete". Non-admins only see references from tables they may read. ``find_missing_files`` ``sys_file`` records whose physical file is gone (``missing = 1``) — the "broken image" diagnosis. The total count is always reported next to the capped listing. ``get_env`` / ``get_env_raw`` Process environment variables. ``get_env`` redacts secret-looking values (password, token, key, secret, salt, DSN, …); ``get_env_raw`` returns them unredacted (database password, encryption key) and ships disabled. ``get_php_info`` / ``get_php_info_raw`` PHP runtime configuration. ``get_php_info`` is redacted; ``get_php_info_raw`` returns the full, secret-bearing ``phpinfo`` detail and ships disabled. ``get_pagetree`` The backend page tree (uid, title, doktype) as a depth-indented outline; deleted and hidden pages are excluded — structure only, no content. ``get_tca`` The TYPO3 TCA schema: with no argument it lists the configured table names; with a ``table`` argument it returns that table's field definitions. ``list_be_groups`` The backend user groups (uid, title). ``list_be_users`` / ``list_be_users_raw`` Backend users. ``list_be_users`` omits credentials (password hashes and MFA secrets are never included); ``list_be_users_raw`` returns the full non-credential profile columns and ships disabled. ``search_records`` Full-text search across the tables that define TCA ``searchFields``. Returns compact ``table:uid`` hits with a short excerpt around the match. Credential and nr-llm configuration tables are never searched; non-admins are limited to their ``tables_select`` tables and to hits on pages they may show. ``get_page_content`` One page's header data plus its content elements in column/sorting order (uid, colPos, CType, header, a short bodytext excerpt). Non-admins need page-show permission; only admins see hidden elements (marked ``[hidden]``). ``read_records`` Generic equality-filtered read of one TCA table — never raw SQL. Fields are validated against the TCA and credential-like columns are silently dropped; the same table gates as ``search_records`` apply. ``get_record_history`` One record's change history from ``sys_history``, newest first: when, which backend user, which action, and per modification the changed fields as old → new values. Values of credential-like fields are never rendered — only the fact that they changed. Same table gates as ``read_records``, and non-admins additionally need page-show access on the record's page. ``resolve_url`` Map a URL (or path) of this instance to the page that serves it: site, language, page uid/title/slug and route arguments. Routing only — no request is sent; foreign hosts cannot match by construction. Non-admins need page-show permission on the resolved page. ``get_typoscript`` The resolved frontend TypoScript (setup or constants) effective on a page, with a dotted ``path`` drill-down and capped output. Admin-only — constants routinely carry API keys — and credential-like values render as ``[redacted]`` on top of that. ``get_tsconfig`` The rootline-merged Page TSconfig effective on a page, with the same ``path`` drill-down, output cap and redaction as ``get_typoscript``. Admin-only. ``get_last_exception`` The newest exception/error from the TYPO3 file logs with its parsed stack trace and the surrounding source lines of the project frames inlined. ``index`` steps back through older errors, ``search`` filters by message, class or component. Admin-only. ``read_source`` A line-numbered range of one project source file. Paths must resolve inside the project root; dotfiles, ``var/*`` (except ``var/log``), ``config/system``, ``settings.php``/``additional.php``, key material and credential paths are structurally unreadable. Admin-only. ``search_code`` Literal-substring (or opt-in regex) search across the project's source files, returning ``path:line`` hits. Vendor, ``var`` and dot directories are never searched; matched credential lines are value-redacted. Admin-only. ``probe_url`` One GET against a URL of *this* instance: status, key headers, timing and a short body excerpt — and on a 5xx the matching exception from the TYPO3 logs is appended automatically. Foreign hosts and non-http(s) schemes are denied; redirects are reported, not followed. Admin-only. ``get_full_tca`` The TCA index: the names and titles of all accessible tables, each with a pointer to ``get_table_schema``. A navigation aid so the model can traverse the schema without the whole (multi-megabyte) TCA being sent at once. The same table gates as ``get_table_schema`` apply. Optional ``filter`` and ``extension`` narrow the list. ``get_table_schema`` One table's schema in a readable form: control settings plus, per field, its type and — for relational fields — the foreign table and relation kind (the value over ``get_tca``). Sensitive tables are denied for every user; credential-like columns show name and type only. ``get_flexform_schema`` The data structure of a TCA FlexForm field, rendered as sheets and fields. When the field selects one of several structures by a pointer, the available keys are listed so a follow-up call can pass ``ds_pointer``. Same table gates as ``get_table_schema``. ``fluid_resolve`` Which physical Fluid file backs a template, partial or layout name in an extension: the candidate paths in override order with an exists flag and the winning path — to debug a wrong or missing template. Paths only. (Resolves an extension's own ``Resources/Private`` paths; TypoScript override root paths need a live rendering context and are not reflected.) ``validate_tca`` Structural TCA checks: ``ctrl.label``/``ctrl.type`` naming undefined columns, ``foreign_table`` references to unknown tables, ``showitem`` entries referencing undefined columns or palettes. One table or all accessible tables; findings name schema keys, never record data. ``check_typoscript`` Scans the TypoScript effective on a page (constants **and** setup) for syntax errors — invalid lines, unbalanced braces, ``@import`` matching no file — using the same core scanner as the backend's TypoScript module. Reports source and line number only, never the offending line's content (a constants line may carry an API key). Admin-only. ``list_extensions`` The installed (active) extensions: key, version, composer name and title — no package paths. Admin-only. ``get_site_config`` Without arguments the configured sites (identifier, base, root page); with ``identifier`` that site's configuration flattened to dotted ``key: value`` lines. Credential-like keys (camelCase included, e.g. ``apiKey``) render as ``[redacted]``. Admin-only. ``list_scheduler_tasks`` The scheduler tasks with next execution, disabled flag and a last-run-failed marker. The serialized task object is **never unserialized**; degrades gracefully when EXT:scheduler is absent. Admin-only. ``get_system_status`` One compact block: TYPO3/PHP/database versions, application context, composer mode, OS family, timezone — no paths, no hostnames. Admin-only. ``list_deprecations`` The newest distinct messages from the deprecation log, deduplicated with a ×count suffix and project paths relativized — the upgrade work list. Admin-only. ``list_middlewares`` A PSR-15 middleware stack (frontend or backend) in execution order with identifiers and classes. Admin-only. ``site_rag_query`` Curated evidence about the website's own **public** content for a question: source id, title, URL and match excerpt per source, retrieved from the best available search index — EXT:solr, ke_search, indexed_search or a database fallback — and labelled with the answering backend (:ref:`ADR-049 `). Index-level filtering is always public-only (what the anonymous visitor could read). ``site_fetch_source`` The full indexed text behind a ``site_rag_query`` source id, capped at 8000 characters — for reading a promising source beyond its excerpt. .. _administration-tools-writing: The writing tools ================= Five tools change anything at all: ``update_page_metadata``, ``set_file_alternative_text``, ``move_content_element``, ``create_content_element_draft`` and ``create_translation_draft``. All five write through the TYPO3 DataHandler, as the acting backend user, in the live workspace only, on exactly **one** record per call (:ref:`ADR-135 `, :ref:`ADR-146 `). What holds for all of them: - They ship **disabled**, sit in their own ``editing`` group, and are not offered until both the group and the tool are enabled. - **Every call pauses for a human decision** (:ref:`ADR-134 `). The approval card names the record and the values, together with the values the write would REPLACE (:ref:`ADR-136 `); nothing is written until somebody presses Approve, and the approver must themselves be permitted to run the tool (:ref:`ADR-133 `). - Permissions are enforced twice: the tool checks the acting user's own rights first, and the DataHandler enforces its own rules on top. A non-admin therefore writes only what the backend already lets them write. - A field the user lacks the "exclude field" grant for is dropped by the DataHandler **without an error**. Every one of them re-reads the record afterwards and reports that as a failure rather than as a successful write. - A call the tool would refuse is refused **whole** rather than applied in part, and a record the acting user may not reach is refused with the same words as a record that does not exist — so a refusal never confirms that a uid exists. ``update_page_metadata`` Sets a fixed set of descriptive fields on one page. Editable: ``title``, ``subtitle``, ``nav_title``, ``abstract``, ``description``, ``keywords`` and — when EXT:seo is installed — ``seo_title``, ``og_title``, ``og_description``, ``twitter_title``, ``twitter_description``. Anything else (``slug``, ``hidden``, ``doktype``, ``fe_group``, ``perms_*``, ``no_index``, the image relations …) is refused. Authorised by the acting user's page-edit right; the DataHandler then enforces ``tables_modify`` and the field-level grants. ``set_file_alternative_text`` Sets the alternative text (``sys_file_metadata.alternative``) of one managed file, identified by its ``sys_file`` uid — the accessibility gap an editor most often leaves behind. It writes that one field and nothing else. Authorised by the same storage allow-list and file mounts as the read-only FAL tools; core's own file-metadata permission check (a **writable** file mount) then applies inside the DataHandler, so a read-only mount is refused there. Two limits worth knowing before enabling it: - It **never creates** a metadata record. A file that carries none is refused, in the same words as a file the user may not reach — so the model cannot tell "not yours" from "not indexed". - It writes the **live**, **default-language** record only and takes no language argument. A translation, and a draft version of the same record in a workspace, are both left alone. Translated alternative texts stay a backend job (:ref:`ADR-135 `). An empty string is accepted and is the correct value for a decorative image. ``move_content_element`` Moves one content element to a page and a column. The element keeps its uid, its content, its language, its history and its references — only its place changes, which is what makes it the least committal write in the set. Both ends are authorised: the acting user needs content-edit rights on the **source** page as well as on the target, because moving an element out of a page edits that page's content too. An ``after_content_uid`` anchor must sit on the target page and in the same language; a wrong anchor is refused rather than silently corrected. The destination column is always stated explicitly on the wire, so the element lands where the approval card said it would even when the anchor element sits in a column the caller did not expect. ``create_content_element_draft`` Creates one content element on a page — the first tool that brings a record into being. Three things bound it: - It is **always hidden**. There is no argument to switch that off: publishing is a separate act with a separate audience, and the approval that let the tool run approved a draft. - The content type is an allow-list (``header``, ``text``, ``textmedia``, ``bullets``) intersected with what the installation's TCA actually declares. Types whose payload is configuration rather than prose — ``list`` (a plugin), ``html``, ``shortcut`` — are out of reach. - The field set is fixed: headline, body text, column, language, position. This is not a generic record API. ``bodytext`` reaches the DataHandler and its RTE transformation exactly as an editor's input does. It is bounded in length and not otherwise filtered — an editor may write the same markup by hand, and a tool enforcing a stricter rule than the CMS would be enforcing a rule that does not exist. ``create_translation_draft`` Translates one page or content element into another language by running core's own ``localize`` command, so connected-mode translations, inline children and every localisation hook behave exactly as they do in the backend. The result is **hidden**, which is the part core does not do: ``localize`` copies the source's visibility, so a translation of a live page would go live the moment it was created. An existing translation stops the call and is named in the refusal. The ``overwrite`` argument is the only way past it: it **deletes** that translation first — recoverably (``deleted = 1``) and in ``sys_log`` — and the approval card says so on its own line. Whether the target language exists for the record's site is core's check, not a second implementation here. .. _administration-tools-register: Registering a tool ================== A tool is a PHP class that implements :php:`Netresearch\\NrLlm\\Service\\Tool\\ToolInterface`: ``getSpec(): ToolSpec`` Returns the declaration the model receives — a name, a description, and a JSON-Schema ``parameters`` block. Build it with ``ToolSpec::function($name, $description, $parameters)``. ``execute(array $arguments): string`` Runs the tool with the model-provided arguments and returns a plain string that is fed back into the conversation as a tool turn. ``getGroup(): string`` The tool's *group* — a short, stable identifier used to enable or disable whole families of tools at once. Built-ins use ``content``, ``editing``, ``structure``, ``system``, ``accounts`` and ``configuration``; third-party tools declare their own group (recommended: the providing extension's key). See :ref:`administration-tools-groups`. The interface carries ``#[AutoconfigureTag('nr_llm.tool')]``, so a class is **auto-registered simply by implementing it** — no central registration file to edit. :php:`ToolRegistry` collects every tagged tool through a DI iterator and indexes it by spec name; two tools with the **same** ``name`` is a developer error and fails fast at container build. When you write a tool, honour the security contract: treat ``$arguments`` as attacker-influenced (the model is steerable by injected skill prose), **validate and scope** every input (cap volumes, scope identifier lookups), and never return secrets — the result leaves the instance. .. _administration-tools-manage: Managing tools ============== The :guilabel:`Admin Tools > LLM > Tools` module lists every registered tool with its global enable state and lets an admin toggle it. A **disabled** tool is refused on every run, everywhere — the runtime gate is fail-closed, so a disabled tool can never be offered to the model regardless of a skill's ``allowed-tools`` or the per-run selection in the playground. Some built-in tools (for example ``get_env_raw`` and ``get_php_info_raw``) ship **disabled by default** because they return unredacted, secret-bearing output; enable them only deliberately. .. figure:: /Images/ToolsModule.png :alt: The Tools management module listing each built-in tool with an Enabled or Disabled badge and an Enable/Disable toggle :class: with-border with-shadow :zoom: lightbox The Tools module — each registered tool with its global enable state and a toggle. The ``_raw`` variants show as :guilabel:`Disabled`, the redacted tools as :guilabel:`Enabled`; the :guilabel:`Default` badge marks a tool sitting at its shipped state. A tool that carries an **editor action** declaration (:ref:`ADR-152 `) reads differently in that list: it shows an icon, its translated name, one sentence written for a human, and the record types it addresses — instead of the wire name and the description written for the language model. All five writing tools declare one, and the wire name stays visible as the technical detail the toggle acts on. A read-only tool is unchanged. The declaration is presentation only. It does not decide whether a tool writes — that is the tool's declared effect — and it changes nothing about how a call is fenced, approved or audited. What editors see ---------------- The declaration is what the **Editor Action Center** (:ref:`ADR-158 `) renders. It lives in the editor module :guilabel:`Web > AI tasks` and appears in two places: as an :guilabel:`AI actions` catalogue reachable from that module, and as an :guilabel:`AI actions` entry in the context menu of a record — a page or a content element — which opens the catalogue narrowed to the actions that address that record. An editor is offered an action only when all of the following hold, and every one of them is an administrator's decision: * the writing tool is enabled in this module (all five ship **disabled**); * its group — ``editing`` — is enabled, and where the default LLM configuration restricts tool groups, ``editing`` is among them; * the tool's data class is within the configured provider's trust-zone ceiling; * the backend user holds ``tasks_use`` and has the module ticked in their group; * the backend user may use the default LLM configuration itself — where that configuration restricts :guilabel:`Allowed backend groups`, the user is in one of them (see :ref:`administration-permissions`). That last point is checked again when the action is started, so an editor outside those groups cannot start a run by naming the action directly either. Starting an action creates an ordinary agent run restricted to that one tool. Because the tool declares a write, the run suspends before it touches anything and the change appears on an approval card with its preview — the editor is redirected straight to that inbox. Nothing is written until someone approves. The record an action is offered on is the record its arguments name, which is not always the record it writes: :guilabel:`Create content element draft` is offered on a **page**, because the page is what it must be told, and the element it creates is the result. Where an action needs something the selected record cannot supply — :guilabel:`Move content element` needs a target page — the editor names it in the note, and the approval card shows the destination that was resolved from it. Files have no context-menu entry yet: the file list identifies a file by its combined identifier rather than by uid, so :guilabel:`Set alternative text` is listed in the catalogue but has no per-record entry point. Several records at once ----------------------- Once a record is selected — that is, when the catalogue was opened from a record's context menu — each action there also offers :guilabel:`Run this on several records` (:ref:`ADR-162 `). The catalogue opened from the module menu has no record and therefore no bulk entry point either; an action needs a subject, and this module picks none. That page takes a list of record numbers from the same table, seeded with the record that was selected, and shows, before anything starts, which of them the action can run on, which are skipped and why, and what the batch is expected to cost in requests, tokens and money. At most 100 entries of that list are read at all. A longer paste is cut there and the page says so, because everything past the cut would otherwise become a table row and a record number in a message no one can read. Starting it creates **one ordinary run per record**. There is no bulk mode: each record gets its own approval card with its own preview, and an approver decides them one at a time. At most 20 records are started in one press, because the runs execute inside the one backend request. The AI budget is checked once per run, so a batch can run out of budget partway through. When that happens the batch stops and names the records the stop kept from starting — the record it stopped on was run, and is reported separately. Nothing is left half-written: the runs that did start are proposals awaiting approval, not changes. Runs that ended for some other reason are reported by kind — failed, stopped by a guardrail, cancelled, or simply finished without proposing a change — so a batch in which everything failed does not read like one in which nothing needed changing. The estimate on that page is deliberately rough and says so: it does not count the system prompt and skills the runtime adds, and its upper price assumes every request returns the configured token ceiling. It shows no price range at all unless the model record carries both an input and an output price and the configuration sets an output ceiling — an absent range means "unknown", which ``0.00`` would not. Treat it as an order of magnitude, not an invoice. .. _administration-tools-groups: Tool groups =========== Every tool belongs to a **group** (its ``getGroup()`` value). The built-in groups carry a translated name in the module header beside their identifier; a group a third-party extension brings has no translated name and shows its identifier alone. The built-in taxonomy: ================= ============================================================ Group Tools ================= ============================================================ ``content`` ``search_records``, ``get_page_content``, ``read_records``, ``get_record_history`` ``structure`` ``get_pagetree``, ``get_tca``, ``read_fal_asset_meta``, ``get_full_tca``, ``get_table_schema``, ``get_flexform_schema``, ``resolve_url``, ``validate_tca`` ``system`` ``get_env`` (+ raw), ``get_php_info`` (+ raw), ``fetch_logs``, ``probe_url``, ``list_extensions``, ``list_scheduler_tasks``, ``get_system_status``, ``list_deprecations``, ``list_middlewares`` ``accounts`` ``list_be_users`` (+ raw), ``list_be_groups`` ``configuration`` ``get_typoscript``, ``get_tsconfig``, ``fluid_resolve``, ``check_typoscript``, ``get_site_config`` ``code`` ``get_last_exception``, ``read_source``, ``search_code`` ``files`` ``list_fal_storages``, ``browse_fal_folder``, ``search_fal_files``, ``get_fal_references``, ``find_missing_files`` ``rag`` ``site_rag_query``, ``site_fetch_source`` ``editing`` ``update_page_metadata``, ``set_file_alternative_text``, ``move_content_element``, ``create_content_element_draft``, ``create_translation_draft`` — the only WRITING group ================= ============================================================ Groups can be switched on three levels, and the result cascades **fail-closed** — a tool is offered only when *every* level permits it: #. **Centrally** in the Tools module: each group header carries an Enable/Disable group toggle. A disabled group refuses all of its tools — including same-group tools installed later — and a per-tool override can **not** re-enable a tool inside a disabled group (predictable, fail-closed). Per-tool toggles keep working but take effect only once the group is enabled again. #. **Per configuration**: the :guilabel:`Allowed tool groups` field on an LLM configuration restricts agent runs with that configuration to tools of the selected groups (empty = all groups). This intersects with a skill's ``allowed-tools`` declaration. #. **Per run** in the playground: the tool checkboxes are grouped, and each group checkbox (de)selects its children. Third-party extensions declare their own group per tool; the recommended value is the extension key, so an admin can disable an extension's whole tool family with one toggle. The design is recorded in :ref:`ADR-043 `. .. _administration-tools-egress: Network egress policy per group =============================== Network egress is governed **per tool group** and is **fail-closed** (:ref:`ADR-061 `). Each group has a declared egress scope; a group with no declaration may make **no** outbound request: =============== ============================================================ Scope Meaning =============== ============================================================ ``none`` No outbound network request (the default for every group). ``own_site`` Only the instance's own configured site hosts, resolved through ``SiteFinder`` — the exact allow-listing ``probe_url`` applies, now lifted to the group boundary. =============== ============================================================ Only the ``system`` group (which carries ``probe_url``, the one built-in that fetches over the network) is granted ``own_site``; every other group is ``none``. There is no "any host" scope, so a newly installed or mis-declared tool group can never egress to an arbitrary target. The diagnostics tools that share the ``system`` group (``get_env``, ``fetch_logs`` …) never make a network request, so the grant does not loosen them. .. _administration-tools-playground: Using the Tool Playground ========================= The playground lives in :guilabel:`Admin Tools > LLM > Playground` and is admin-only. It is a sibling of the :ref:`Tools ` management module: the playground *runs* the loop, while the Tools module governs *which* tools exist and are enabled. .. figure:: /Images/ToolPlaygroundShell.png :alt: The Tool Playground module with the LLM configuration picker, an empty prompt box, the Run button, and the Available tools panel :class: with-border with-shadow :zoom: lightbox The playground shell — the configuration picker, prompt box and the :guilabel:`Tools available to this run` panel, which lists every registered tool with the default-enabled ones pre-checked and the disabled ``_raw`` variants unchecked. .. tip:: **Small local models work best with a narrow tool set.** With every group enabled, the model is offered every enabled tool declaration at once (several dozen). Small models such as the seeded ``qwen3:4b`` often fail to pick the right tool from a set that large, or reason past the token budget without calling any. Untick the groups that are irrelevant to the question — restricted to one or two groups, the same model picks the right tool. Larger hosted models cope with the full set. 1. Pick an **LLM configuration** from the dropdown. Its vault-stored API key, model, temperature and system prompt are what the loop actually runs on — the playground never falls back to a default model. 2. Type a **prompt**. Optionally open the override panels to **force-inject skills** (added on top of the configuration's own), **force-add snippets** (inserted as leading system messages), override the **system prompt**, cap the **max rounds**, or tick **capture raw provider response**. 3. Click :guilabel:`Run` — or :guilabel:`Dry run` to assemble the prompt and inspect exactly what *would* be sent without calling the model. 4. Read the **inspector** — live from the moment you click Run. A summary strip reports rounds, tool calls, the prompt/completion token split, estimated cost, wall time and status. The step list is the nr_llm ↔ LLM dialog in order: each round opens with a :guilabel:`Context budget` step, then its outbound **request** (the messages sent and the tools offered) appears the instant it goes out, a waiting indicator shows while the model works, then the **response** and each tool execution stream in. Select a step to open its detail — requests carry :guilabel:`Messages sent` and :guilabel:`Tools offered`; responses carry :guilabel:`Structured`, :guilabel:`Raw JSON` and :guilabel:`Thinking`. The model's **final answer** closes the run. 5. The :guilabel:`Context budget` step says where the window went for the round that follows it, so you can act on the component that is yours to change rather than only learn that history was dropped. It has two tabs: :guilabel:`Where the window went` The window, the output reserve, the safety margin and the resulting budget, then four component lines — transcript, tool schema, system prompt (incl. snippets) and skills — that **sum to the estimated total**, plus what is left. On this surface the system-prompt line always reads *counted in the transcript* and the skills line always reads 0: the agent loop builds both into the transcript before the fit measures it, so their content is already on the transcript line. The table says so under the figures. A send whose reserve exceeds the whole window is handed to the provider unmeasured, and the step reports *no accounting* instead of a window of zero. :guilabel:`Injected context` Every snippet and skill this run injects, by **name only**, with the data class it declared (:ref:`ADR-144 `) or *not classified*, and the strictest class across all of them. The list covers the snippets and skills you force-injected for this one run as well as the configuration's own — the input-context gate itself still answers for the configuration alone, so a forced source is shown here and is not gated. .. figure:: /Images/ToolPlaygroundRun.png :alt: A completed tool run — the summary strip, the ordered step list and the selected step's detail tabs for a two-iteration agent loop :class: with-border with-shadow :zoom: lightbox A completed run — the summary strip (rounds, tool calls, token split, wall time, status), the ordered step list of the nr_llm ↔ LLM dialog, and the selected step's detail: here round 1 requested the ``list_be_users`` tool, whose result is fed back so round 2 can answer. The :guilabel:`Tools available to this run` list lets you narrow a single run to a subset of the globally-enabled tools (the full list and the global enable/disable controls live in the :ref:`Tools ` module). Raw-response capture is off unless you tick it, so ordinary runs never retain the provider's raw payload. Every displayed string — tool arguments, tool results (which may include ``sys_log`` content), and the final answer — is rendered escaped; HTML is only ever shown inside a sandboxed preview, never injected into the page. Each run is bounded by the iteration cap (default 5) and, when the configuration's backend user has a budget, by the per-iteration budget pre-flight. If the cap is hit with tools still pending, a final tool-free completion synthesises a closing answer and the run is marked *truncated*. The aggregated **token** usage is reported; the monetary cost is recorded in the usage table by the middleware pipeline. .. _administration-tools-ollama: Ollama model-capability dependency ================================== Tool calling depends on the **model**, not just the provider. For Ollama, only function-calling-capable models — for example ``llama3.1``, ``mistral``, ``qwen2.5`` — return tool calls. A model without function-calling support simply **answers the prompt directly and never calls a tool**; the loop ends gracefully on the first plain answer. If a configured Ollama model never seems to use the available tools, verify it is one of the function-calling models for your Ollama version. .. _administration-tools-allowed: Gating tools with ``allowed-tools`` in a skill ============================================== A skill's ``SKILL.md`` front-matter may carry an ``allowed-tools`` key that gates which tools the skills attached to a configuration (or task) grant for a run. The resolution is **fail-closed on declaration**, computed over the configuration's *effective* skills (enabled, non-orphaned, at or above the instance trust floor, deduped): - **Absent** (no skill declares ``allowed-tools``) — no opinion; all registered tools are offered. - **Declared list** — the **union** of the declared lists across the effective skills; only those tools are offered (intersected with what is actually registered, so an unknown name is dropped). - **Declared empty** (``allowed-tools: []``) — declares **zero** tools; if no other effective skill widens the set, the run gets no tools and is a single plain completion. A disabled or orphaned skill never grants tools. The allow-list is enforced both when the tools are offered to the model and again when a tool call is executed, so a prompt injection cannot reach a tool the skills did not grant. The effective set is *not* the same as what ends up in the prompt. The skill-block byte budget (``skills.maxBytes``, see :ref:`Managing skills `) is applied after the allow-list has been computed, while the block is assembled. A skill dropped for the budget therefore still contributes its ``allowed-tools``, but its usage rules never reach the model. That order is deliberate: a budget-aware union would let the drop of the last *declaring* skill collapse the allow-list to "no restriction" — every registered tool — which is a looser gate, not a tighter one. Keep ``skills.maxBytes`` above the composed size of the skills you rely on if you want their prose to arrive alongside the tools it describes; every drop is logged as a warning. See :ref:`ADR-038 ` for the runtime design and security rationale.