nr-llm
Architecture Decision Records
Every significant design decision in nr-llm is recorded as an ADR — the context, the decision, and its consequences. Browse the full set below.
243 decisions.
Provider & Resilience 21
- ADR 001 Provider Abstraction Layer Accepted Introduces a ProviderInterface contract plus optional capability interfaces (vision, streaming, tools, documents), an AbstractProvider base class, and LlmServiceManager as the unified entry point, so multiple LLM providers share one consistent API.
- ADR 007 Multi-Provider Strategy Accepted Registers providers as a tagged service collection ordered by priority so the manager can select and iterate providers by rank.
- ADR 008 Error Handling Strategy Accepted Defines a hierarchical exception system rooted at ProviderException (connection, response, configuration, unsupported-feature, fallback-exhausted) plus extension-level configuration and argument exceptions.
- ADR 018 Multi-Provider Model Discovery Accepted Abstracts model discovery behind ModelDiscoveryInterface (testConnection + discover) with per-adapter dispatch so each provider's model list and connection test share one contract.
- ADR 021 Provider Fallback Chain Accepted A configuration's ordered fallback_chain of configuration identifiers is walked by FallbackMiddleware on narrowly-defined retryable failures (connection errors, HTTP 429), returning the first success or throwing FallbackChainExhaustedException.
- ADR 022 Attribute-Based Provider Registration Accepted Adds a #[AsLlmProvider(priority)] attribute scanned by ProviderCompilerPass to auto-tag providers at compile time, keeping providers private while an explicit YAML tag still wins when both are present.
- ADR 026 Provider Middleware Pipeline Accepted Introduces a PSR-15-inspired provider middleware pipeline (ProviderMiddlewareInterface with an immutable ProviderCallContext and callable $next), replacing the earlier PSR-14 event system.
- ADR 034 Remove the ExtensionConfiguration Default-Provider Fallback Accepted Removes the defaultProvider concept from LlmServiceManager (state, seed, accessors) making the database the single source of truth for provider selection; a breaking change where getProvider(null) now throws.
- ADR 063 Provider Resilience — Circuit Breaker, Health, Idempotency Accepted Adds a per-provider CircuitBreakerMiddleware (opens after consecutive tripping failures, half-open probe after cooldown) placed innermost, tripping only on the same connection/429 failures FallbackMiddleware treats as retryable.
- ADR 080 Typed Provider HTTP Exceptions (Authentication 401, Rate-Limit 429) Accepted Adds ProviderAuthenticationException (401) and ProviderRateLimitException (429) as final subclasses of a de-finalized ProviderResponseException, chosen by a shared AbstractProvider factory so buffered and streaming 4xx branches never drift, preserving backward compatibility via inheritance.
- ADR 095 One failure taxonomy for retry and circuit-breaker decisions Accepted One vocabulary. FailureClass (connection, rateLimit, auth, configuration, clientError, serverError, circuitOpen, unknown) answers the two questions once: isRetryable() and tripsCircuit(). FailureClassifier maps a throwable onto it — a pure, static, directly-tested function recognising the provider …
- ADR 125 Per-adapter collaborator classes Accepted Adapter-specific logic that is pure — no transport, no credential path, no response parsing — MAY be extracted into a collaborator class in a sub-namespace named after the adapter (here NetresearchNrLlmProviderOpenRouterModelRouter). Rules:
- ADR 160 One adapter contract, and honest capability provenance Accepted Tests/Unit/Provider/Contract/AbstractAdapterContractTestCase fixes seven things for all seven adapters: the identifier, the capability declaration, error normalisation per exception type, refusing to send without a credential, timeout behaviour, usage reporting, and — where declared — the shape of …
- ADR 189 A model capability is vocabulary, and only sometimes data Accepted A capability may be required in selection criteria only if some producer actually writes it. The enum stays the wider vocabulary; the constraint is on what criteria may demand, not on what the enum may contain.
- ADR 203 Tool calling on an OpenAI reasoning model goes through Responses Accepted The transport is chosen per request, from a model profile. A tool request goes to responses when the model's profile says chat/completions cannot serve its function tools — today the three GPT-6 models. Everything else stays on chat/completions, the GPT-5 and o-series reasoning models included …
- ADR 204 Reasoning effort is a request option, and the thinking switch sets it Accepted Reasoning effort is a request option of its own. A backed enum ReasoningEffort names the seven values the API defines. It reaches the provider through ChatOptions::withReasoningEffort() as the options key reasoning_effort.
- ADR 222 Gemini replay keeps native thought signatures Accepted Use the existing replay carrier with an explicit Gemini-owned item whose type is nrllm_gemini_generate_content and whose parts contain the original native array-valued parts in their original order. Preserve signatures and part boundaries without decoding or rebuilding them. Signed ordinary chat …
- ADR 223 Gemini separates native thoughts from text Accepted Parse the first candidate's text parts in order through one internal helper. String text with a strictly boolean thought: true marker belongs to native thinking; all other string text belongs to visible output. Non-text values retain the existing defensive handling. Concatenate each sequence …
- ADR 224 Gemini keeps known Struct roots as JSON objects Accepted Encode these two API-known roots as JSON objects when constructing the final Gemini request, including empty and numeric-key maps. Preserve their member values and current scalar/invalid result fallback. A decoded list result keeps its members at numeric object keys in the known Struct root. Do not …
- ADR 226 Gemini correlates native function-call IDs Accepted Keep public synthesized ToolCall IDs and capture their association with populated native IDs at the actual parser. Extend only the existing owned opaque capsule with optional tool_call_bindings entries containing tool_call_id and native_id. Preserve native parts. Validate the owning turn …
- ADR 241 A reasoning model gets room to think Accepted The output limit sent to a reasoning model of OpenAI is at least 16,000. OpenAiProvider sends the larger of the requested limit and OpenAiModelProfiles::REASONING_OUTPUT_FLOOR on every path: completion, tool round on either transport, vision and stream. A call that switches thinking off with an …
Core Services & Architecture 27
- ADR 002 Feature Services Architecture Accepted Adds dedicated high-level feature services (Completion, Embedding, Vision, Translation) that use LlmServiceManager internally, expose domain-specific methods, handle caching, and return typed response objects.
- ADR 004 PSR-14 Event System Superseded Originally dispatched BeforeRequest/AfterResponse PSR-14 events from LlmServiceManager to let consumers modify requests and process responses; superseded by the provider middleware pipeline (ADR-026).
- ADR 005 TYPO3 Caching Framework Integration Accepted Integrates with TYPO3's caching framework using the nrllm_responses cache with no hardcoded backend, keying entries on provider+model+input hash, so deterministic calls (embeddings, temperature-0 completions) can be cached while streaming and tool calls are not.
- ADR 016 Thinking/Reasoning Block Extraction Accepted Extracts model reasoning via a two-tier strategy — native structured thinking blocks (Anthropic) plus a regex <think>-tag fallback — surfaced as an optional thinking property on CompletionResponse.
- ADR 017 Safe Type Casting via SafeCastTrait Accepted Provides a reusable SafeCastTrait with static toStr/toInt/toFloat helpers that coerce mixed input to sensible defaults without PHPStan suppressions.
- ADR 028 Public Services Policy Accepted Documents the deliberate public-service set in Services.yaml as a policy of load-bearing categories, enforced by a unit test that keeps the count honest; its count/tail rationale is later superseded by ADR-065.
- ADR 032 Specialized Usage Tracking and Pricing Catalog Accepted Makes specialized services report real usage units (images, characters, audio seconds, tokens) and adds a static OpenAI price catalog with a DB override so image/speech/transcription calls get accurate cost tracking.
- ADR 053 One Marker Interface for All Thrown Exceptions Accepted Adds NrLlmExceptionInterface (extending Throwable) implemented by every exception the extension throws, with value-object normalization errors now throwing the nr_llm InvalidArgumentException and a reflection test enforcing the marker.
- ADR 058 Telemetry Middleware Accepted Adds a TelemetryMiddleware at the outermost priority and a UI-less tx_nrllm_telemetry table writing exactly one immutable row per pipeline run (latency, cache-hit, fallback attempts, success/error) on both success and failure.
- ADR 059 Decompose LlmServiceManager into Focused Collaborators Accepted Stage-1 decomposition extracting self-contained groups into private autowired collaborators (KeyedProviderRegistry, ConfigurationResolver, ...) while the manager stays final and keeps its unchanged interface via thin delegation.
- ADR 065 Reduce the Public Service Surface (ADR-028 Follow-up) Accepted Keeps public: true only where genuinely required (documented API contract, interface alias, concrete-only surface, standalone consumer API, or makeInstance reuse) and privatises everything public solely for test resolution.
- ADR 076 Document Understanding — Native-First, Rasterize Fallback Accepted Adds a stateless DocumentAnalysisService that sends a whole PDF in one chat call as a Base64 document block when the provider supports it, falling back to page-by-page rasterization for providers without document support.
- ADR 081 Agent run persistence and a durable event stream Accepted Persists each tool-loop agent run and its steps in two UI-less log tables, so a run can be resumed after a restart, queued, paused for approval, audited or replayed — instead of living only for a single HTTP request.
- ADR 082 Schema-validated structured outputs with one repair round-trip Accepted completeJson() now validates results against a business JSON-Schema and runs one repair round-trip on a malformed response (reusing the evaluation subsystem's structural matcher), so AI output is reliable to program against.
- ADR 083 Conversation sessions and memory Accepted Adds an explicit, persisted session model — two UI-less log tables read via a raw-SQL repository — so multi-turn conversations keep their history server-side instead of each consumer re-assembling and re-sending it.
- ADR 090 One extension until 1.0, with documented split seams Accepted Keeps nr-llm as a single extension through the 1.0 line rather than splitting its subsystems (tools/RAG, skills, guardrails) into separate packages now, while documenting the seams along which a future split could occur so the option stays open.
- ADR 096 The pipeline configuration lives on the call context Accepted The configuration moves onto the context. ProviderCallContext gains a nullable configuration plus provider / model / configurationIdentifier strings. MiddlewarePipeline::run(context, terminal) and ProviderMiddlewareInterface::handle(context, next) drop the separate configuration parameter; $next …
- ADR 097 Specialized services dispatch through the shared pipeline Accepted AbstractSpecializedService takes the MiddlewarePipeline as a required dependency and exposes runLifecycle(ProviderCallContext $context, callable $call), which runs the actual HTTP dispatch as the pipeline terminal. Each service builds a ProviderCallContext::forService(operation, provider, model) …
- ADR 100 Specialized usage recorded by tagged extractors in the pipeline Accepted A specialized service no longer writes usage. Before dispatch it attaches a SpecializedUsageIntent — the stable, dispatch-independent inputs it knows (model, resolved model / configuration uid, attribution uid, and the input- derived counters: characters, size, quality, batch size) — to the …
- ADR 127 A marked, versioned API surface Accepted Every class-level docblock carries one of three markers, and the marker — not the container visibility, not the documentation — is the authority on what semver covers:
- ADR 139 Context assembly is a seam, not a provider registry Accepted Context assembly stays a seam between the parts that already exist. No ContextProviderInterface, no registry, no context-type vocabulary in the core.
- ADR 147 No Symfony AI bridge while it is below 1.0 Accepted Do not add a Symfony AI adapter while ``symfony/ai-platform`` is below 1.0 — not as a hard dependency, and not as an optional one.
- ADR 159 One extension, confirmed at the 1.0 API freeze Accepted Stay one extension through 1.0. ADR-090's timing — "with or before the 1.0 release" — is met by this record, not by a split: the re-evaluation happened, and its outcome is that the split is still the wrong move.
- ADR 171 The personas nr_llm's code already assumes Proposed Issue #768 blocks a management grant on questions that are not about code: whether a customer has a person who writes AI prompts but must not hold API keys, whether an editor's lead needs to see their team's spend. Those questions cannot be answered from the repository, and every attempt so far has …
- ADR 208 A site glossary reaches both translators Accepted A glossary is a record, ``tx_nrllm_glossary``, keyed by site identifier, source language and target language. Editors maintain it through FormEngine, listed in the module AI > Authoring > Glossaries, which is built like the prompt snippet module. The module is admin-only, like its siblings.
- ADR 211 Typed decisions from an exchangeable backend Accepted A decision is a model operation like chat or embeddings. A model that decides natively is a Model record with the capability decision behind a provider adapter that implements DecisionCapableInterface; any chat model can answer the same questions through structured output. A new @api service …
- ADR 233 A language model revises a DeepL translation Accepted TranslationOptions::withPolish(bool $enabled = true, ?string $instruction = null) asks for a revision. It is off by default. translateWithTranslator() and translateBatchWithTranslator() honour it when the translator that ran is not the LLM translator; after the LLM translator a second pass would …
Agent Runtime 21
- ADR 091 Sessions are owned — an explicit actor context on every turn Accepted Every stateful entry point takes an explicit AiActorContext.
- ADR 092 A run records why it ended, and cannot be settled twice Accepted Status and reason are separate fields. AgentRunTerminationReason — completed, max_iterations, budget_exhausted, policy_denied, approval_denied, provider_failed, cancelled — is carried on ToolLoopResult from the loop's exit path and stored in a new termination_reason column. The status stays the …
- ADR 101 AgentRuntime — the agent-run lifecycle as a public service Accepted AgentRuntimeInterface (Classes/Service/Agent/) is the public application service for agent runs; AgentRuntime implements it and owns the ladder exactly once. The playground controller is now a UI adapter: it parses the request into an AgentRunRequest / ApprovalDecision and maps the returned …
- ADR 102 Queued agent runs over the TYPO3 message bus Accepted AgentRuntimeInterface (consumer contract: methods may be added in minor releases, ADR-101) gains the queued half of its lifecycle:
- ADR 103 Cooperative cancellation at step boundaries Accepted The run executor's trace hook gains a cancellation probe. Every step boundary — after a provider response, after each tool execution, before the next round — already records a step through the runtime's onRecord closure; the probe re-reads the run row there and, when it is CANCELLED, throws the …
- ADR 104 Worker heartbeat, stale-run reaper, retry and dead-letter Accepted Heartbeat. The runtime's trace hook (the same step-boundary closure the cancellation probe uses, ADR-103) renews the lease on every step for a worker run — an ownership-guarded UPDATE (WHERE status='running' AND claimed_by = :me). A renewal that affects no row means the worker lost the run (reaped …
- ADR 105 Typed user-input suspension (WAITING_FOR_INPUT) Accepted A new suspend kind, routed by the persisted status discriminator: WAITING_FOR_APPROVAL → approve(), WAITING_FOR_INPUT → ``submitInput()``. The two never share a code path after the row is loaded, so the persisted state needs no "kind" field.
- ADR 107 Agent-loop context-window management Accepted An optional ContextWindowManager collaborator on ToolLoopService bounds the transcript before each provider send. Absent it (the lean test wiring) the loop sends the full transcript exactly as before — every enforcement site is a no-op.
- ADR 121 Conversations are bounded where they are assembled Accepted Bound the transcript in ConversationService::send(), using the existing ContextWindowManager. The configuration for the turn is resolved before the transcript is assembled, because the bound depends on the model the request will actually go to.
- ADR 141 Every executing segment holds a lease, or writes stop Accepted Every segment that can execute a tool claims the run it executes, and a side-effecting tool that cannot be fenced does not run.
- ADR 153 A run's uuid is the correlation id of everything it does Accepted No new column: the run's uuid IS the correlation id. tx_nrllm_agentrun.uuid and tx_nrllm_telemetry.correlation_id are both an RFC 4122 uuid in a varchar(36). A correlation_id column on the run would have stored a second identifier for the same thing and needed a mapping nobody asked for. …
- ADR 157 The simulation covers the run, and answers for an actor Accepted One verdict, four axes, every axis visible. The simulation asks the tool gate, the input-context gate, routing eligibility and the approval requirement, and folds them: ALLOW when every axis permits and no human decision is needed, ALLOW + APPROVAL when a human decision is, BLOCK when any axis …
- ADR 176 A per-call outcome, kept apart from the approval gate Accepted A per-call outcome row, keyed on correlation_id — the same key ADR-174 writes cost and pre-routing facts against, so a quality figure and a cost figure join without a second identity.
- ADR 185 An observed outcome is derived from history, inside a window Accepted An observed outcome is derived once, after a window, from ``sys_history``, and says UNKNOWN whenever it cannot say better.
- ADR 188 Every conversation has a configuration from the moment it opens Accepted A session is bound when it is opened, or it is not opened.
- ADR 190 Cancellation crosses the transport boundary as a signal Accepted Cancellation crosses the boundary as a caller-owned signal, never as a promise. nr-vault's CancellableHttpClientInterface takes a CancellationSignalInterface — one method, isCancelled() — and returns a PSR-7 response. No transport type crosses it in either direction. This extension therefore never …
- ADR 191 A cancelled tool call is not a failed one Accepted A tool result states its outcome, and the boolean stays. ToolOutcome has three cases — OK, FAILED, CANCELLED — and ToolResult carries one. ToolResult::$isError is unchanged and remains true for both non-OK cases, so every consumer that reads it keeps the meaning it had; the outcome says WHICH of …
- ADR 219 Observe worker operations Accepted Keep Symfony Messenger and the existing reaper. Record a queue-entry timestamp on enqueue and each successful requeue. Unknown legacy timestamps remain zero in storage and are represented as unknown in operational output.
- ADR 225 Agent outcomes survive optional diagnostic failures Accepted Contain optional diagnostic emission in the executor, queued failure recovery and agent-run persister through one internal private callback guard. Invoke the existing severity method once with its existing message, context and ordering. Contain only its Throwable; emit no recursive fallback …
- ADR 228 A process run records its lineage; the runtime derives pins Accepted The invocation. AgentRunRequest gains two trailing optional arguments: invocation (SkillInvocation: a skill uid and an optional SubjectRecord of table, uid and language) and predecessorRunUuid. Both travel through the queue codec; a stored value that cannot be read refuses the run instead of …
- ADR 234 Background work is a stored run and a sweep Accepted One queue, the run table, and a second way to work it. There is no table of its own. ADR-162 rejected that for batches, and the reason holds for tasks: a second lifecycle would need its own claim, lease, retry, cancel and retention.
Approvals & Writing Tools 33
- ADR 109 Agent Runs approvals inbox (backend module) Accepted Ship a new admin-only backend submodule nrllm_runs ("Agent Runs") — an approvals inbox focused on the runs that need a human, plus a read-only list of recent terminal runs for context.
- ADR 111 Tool side effects and fail-closed audit for writes Accepted Classify a tool's side effect and act on it.
- ADR 112 Lease-before-op fence — no retry for an interrupted write Accepted Record the in-flight write BEFORE it runs, and refuse to retry a run reaped while that record stands.
- ADR 122 The side-effecting tool contract waits for a side-effecting tool Accepted Do not build the interface, the scope or the preview yet. A contract designed before the first writing tool guesses at the shape that tool needs, and this codebase has just spent three changes removing exactly that kind of guess: an argument that looked like enforcement and was read by nothing.
- ADR 132 Fail-closed approval audit and turn binding Accepted One digest definition. The computation moved out of the inbox view factory into PendingTurnDigest, a small @internal service both the rendering side and the verifying side use. A digest is a comparison; two implementations that drift apart silently compare different things. The hash covers the …
- ADR 133 An approver may only release a write they could run Accepted The approver passes the same gate the execution would. ResumeCoordinator::approve() resolves the APPROVER's live backend user — the ActingBackendUserResolver of ADR-083, applied to the deciding identity rather than the executing one — and asks ToolCallPolicy::decide() about every pending call that …
- ADR 134 A builtin's declared write effect implies human approval Accepted A tool counts as approval-bound in the loop's approval scan when it implements RequiresApprovalInterface; when it is a remote tool whose operator declaration says so; or when it implements ToolEffectInterface, its getEffect() is a write, and it is not remote.
- ADR 135 The first writing tool, and the contract it actually needed Accepted Ship UpdatePageMetadataTool (update_page_metadata): set a fixed allow-list of descriptive fields on exactly ONE page, through the DataHandler, as the acting backend user.
- ADR 136 The write preview is produced when the run suspends Accepted An opt-in ToolPreviewInterface — no base class, no ActionInterface, the same marker shape ToolEffectInterface and RequiresApprovalInterface already have. A tool that implements it returns human-readable lines describing what the pending call would do, and answers whether a given viewer may be shown …
- ADR 146 Three more editorial writers, and what the third one reviewed Accepted Three more purpose-built editorial tools, on the terms the first two established: disabled by default, in the editing group, with an explicit ToolEffect, a human approval before every call (ADR-134), a preview at suspend (ADR-136), a write through the DataHandler under the acting user's permissions …
- ADR 150 A submitter may only feed a tool they could run, on the turn they saw Accepted The submitter passes the same gate the execution would, on EVERY pending call. ResumeCoordinator::submitInput() resolves the SUBMITTER's live backend user and asks ToolCallPolicy::decide() about every call of the pending turn — not only the writing ones — plus the state's declared inputToolName. …
- ADR 172 Four-eyes approval is a per-configuration switch, default off Accepted A per-configuration switch, require_second_approver, default 0.
- ADR 173 A self-approved run says so, wherever the approval is shown Accepted The READOUT compares the two uids in one place, Domain\Enum\ApprovalAttribution, and two readers display its answer.
- ADR 180 The sixth writer creates a page, and the one-record rule holds Accepted create_page_draft is the sixth writing tool, on exactly the terms of the previous five: disabled by default, in the editing group, an explicit ToolEffect (NON_IDEMPOTENT_WRITE — two calls leave two pages), a human approval before every call (ADR-134), a preview at suspend (ADR-136), a write through …
- ADR 182 A write tool names the record it wrote Accepted A tool that creates or changes a record says which one, and nothing else.
- ADR 184 An approval binds to the state the preview showed Accepted A run that resumes writes against the state its approver was shown, or it does not write.
- ADR 187 AI-write provenance is announced, not implemented Accepted The extension says that an AI wrote a record. It never says what anyone should do about it.
- ADR 192 The eighth writer describes an asset, and stays out of the seventh's field Accepted update_fal_asset_meta is the eighth writing tool, on exactly the terms of the previous seven: disabled by default, in the editing group, an explicit ToolEffect (IDEMPOTENT_WRITE — setting named scalar fields to given values converges on repeat), a human approval before every call (ADR-134), a …
- ADR 193 A draft element does not turn a connected page into a mixed one Accepted create_content_element_draft refuses a call with language greater than zero when the target page already holds at least one tt_content row with that sys_language_uid and l18n_parent > 0. Deleted rows do not count, hidden rows do — as core counts them, and every draft this extension writes is hidden.
- ADR 194 Two more metadata fields, one of them a select Accepted twitter_card joins the allow-list of update_page_metadata. The list is still intersected with the live TCA, so an installation without EXT:seo is not offered the field. For a select column the writer reads the items the live TCA declares at call time and refuses a value that is not one of them …
- ADR 195 The ninth writer sets a page's social image, and stays out of the first's allow-list Accepted set_page_social_image is the ninth writing tool, on the terms of the previous eight: disabled by default, in the editing group, an explicit ToolEffect, a human approval before every call (ADR-134), a preview at suspend (ADR-136) that is a pure function of the arguments and the current state …
- ADR 196 The element draft offers the TCA's content types Accepted The table stays fixed: tt_content, one hidden element per call, through the DataHandler as the acting user, with the approval pause, the preview and the read-back of ADR-146 unchanged. What changes is where the type list and the field list come from.
- ADR 197 A generic record creator, only where no narrow writer exists Accepted create_record_draft is a builtin writer that creates ONE hidden record in a TCA table for which no narrow writer exists. It is a fallback, not a replacement, and every one of the following conditions is part of the decision. Removing one reopens ADR-135's argument.
- ADR 198 The assistant acts on existing pages and content elements Accepted Six builtin writers act on existing pages and tt_content rows. Each is narrow — one act on one record — and every rail below is part of the decision; removing one reopens ADR-135's argument.
- ADR 199 A copy is a hidden draft, and a page is copied without its branch Accepted The copy is core's copy. The rows it brings along are the ones the page module's copy would bring; the tool does not pick among them, and the approval card counts them — the translations, and for a page the content elements on it.
- ADR 200 A denied approval tells the model who declined it Accepted The result of a denied call leads with fixed tokens, then says in plain words who declined and what the model should tell the user:
- ADR 205 A column every content type carries excludes no type Accepted The form of core's header type is the shared form, and its columns do not decide. An excluding column of that form is left at its default, like an unfilled column; fields never accepted an excluding column and still does not. The fillable columns of that form — date, header_layout — stay settable …
- ADR 206 A hook that fails does not fail the write it ran after Accepted Every builtin writer runs ToolDataHandler, a DataHandler that tells a failure after the writes from one during them. It looks at the method the OUTERMOST run called when it failed:
- ADR 209 The translation draft translates its text Accepted After localize created and hid the record, the tool machine-translates its text.
- ADR 212 A generic file attacher, only where no narrow writer exists Accepted attach_file_to_record(table, record, file, field?, title?, alternative?, description?) references one existing sys_file from one file field of one record. It is the sibling of attach_file_to_content_element and keeps its terms (ADR-135, ADR-146, ADR-180):
- ADR 213 Approval preview lines are in the acting user's language Accepted The language of a preview line is the language of the run's acting backend user, the lang column of the user the run executes as, read from the ToolExecutionContext (ADR-083). Never the ambient $GLOBALS['LANG'], which belongs to whoever runs the call, and never the viewer's language.
- ADR 230 An alternative text for one use of an image Accepted set_file_reference_alternative_text(reference, alternative) sets alternative on one existing sys_file_reference, in place. It keeps the terms of the other writers (ADR-134, ADR-135, ADR-136, ADR-198, ADR-213, ADR-214):
- ADR 239 A record an AI wrote carries a field Accepted One listener ships: it records the fact and says nothing. MarkAiWrittenRecord sets tx_nrllm_ai_written on the record of an AfterAiRecordWrittenEvent. It renders nothing. The rest of ADR-187 stands: the event carries no payload, and other listeners remain free to do what the site wants.
Governance & Routing 18
- ADR 137 One candidate resolution for the primary's chain Accepted The candidate loop moves into one class, ProviderFallbackFallbackCandidateResolver, marked @internal. It owns the four ADR-021 rules and nothing else:
- ADR 138 Criteria-mode selection matches the operation, not only the criteria Accepted Thread the operation into the resolution. ModelSelectionServiceInterface::resolveModel() and ConfigurationCallPlanner::resolveModel() take a ?ProviderOperation. It has no default: every resolution belonging to a concrete call must name that call, and the one caller that genuinely has none — the …
- ADR 140 The effective-policy readout has no apply path Accepted A read-only view, in an existing module, reading through the runtime resolvers.
- ADR 142 One routing decision, with a reason per candidate Accepted Discovery, then eligibility, then ranking — and the boundary between the last two is the load-bearing part.
- ADR 143 Bound every send against the model that actually serves it Accepted The window comes from the resolved model. fit() takes the model that will actually serve the send and prefers it over the configuration's relation; the response reserve follows the same model. Passing nothing keeps the entity's model, which is the fixed-mode case and the behaviour every existing …
- ADR 145 Governance profiles describe a posture, they never apply it Accepted A profile is a definition. GovernanceProfile is an enum of named postures — local-only, controlled-cloud, enterprise-strict, development — and each one is a map of expected values. It enforces nothing, resolves nothing, and is never consulted at runtime. A profile that could enforce would be a …
- ADR 148 The routing readout is a second gate on the Governance tab Accepted A section on the Governance tab, not a tab of its own. ADR-145 already established that page as the place an operator asks "which rule applies here, and what would it say about this call". A second tab would need its own module route, its own doc-header entry and its own template, and would split …
- ADR 149 A criteria-mode trust zone comes from the resolved model Accepted The zone comes from the serving model when one is known. zoneFor() takes an optional resolved Model and reads the primary zone from ITS provider. Passing nothing keeps the configuration's own relation, which is the fixed-mode case and what every existing caller had.
- ADR 151 The context budget is a breakdown, not a number Accepted The fit reports where the window went, as data. ContextFitResult gains a ContextBudgetBreakdown: the window, the reserved output, the safety margin, the budget, and four component lines — transcript, tool schema, system prompt, skills — plus the estimated total and what is left.
- ADR 156 Persist the routing decision, and observe complexity without routing Accepted Two column groups on the one table, and one reader for both.
- ADR 164 A run's forced sources bind against the trust ceiling Accepted A forced source binds against the same ceiling as a configuration's own.
- ADR 165 A resumed run re-gates its forced sources Accepted The suspended state carries the forced set as uids. SuspendedRunState gains $forcedSnippetUids and $forcedSkillUids, written at both suspend sites and read on both resume paths. A row persisted before this ADR has neither key and rehydrates with none — the pre-ADR-165 behaviour, never a refusal to …
- ADR 166 Deactivating a source does not lower a ceiling Accepted "Inactive" means "not for new composition". It does not mean "already-injected text loses its classification". Those are two different questions, and the repository now answers them with two methods.
- ADR 167 Configuration access is a simulated axis Accepted Configuration access is the fifth axis, and it folds into the verdict. A configuration the actor may not use makes the verdict BLOCK, like every other refusing axis. No fourth verdict value: the fold's meaning is unchanged — any refusal blocks — and a separate outcome would have implied a …
- ADR 174 Per-call cost, and request facts measured before the model is chosen Accepted Two more column groups on the one row ADR-156 already writes, and nothing reads either of them inside the decision path.
- ADR 177 Caller-source attribution on the options path Accepted The caller identity travels on the options object — the same channel the idempotency key already uses, with the same wire-leak rule.
- ADR 178 Caller source on the cost path Accepted The caller identity travels the cost path as well: source_extension becomes a column on tx_nrllm_service_usage and part of that table's daily aggregation key, so two extensions calling the same model on the same day produce two rows instead of one.
- ADR 179 A forced source that is dropped is recorded on the run Accepted Recorded on the run, not only in a log. A log line answers "did it happen", which serves support. The question an operator actually has is "why did this run behave differently from the one I queued", and that is asked at the run. The record therefore travels on RunAugmentation from the rehydration …
MCP 7
- ADR 116 Central tooling authority — nr_llm owns builtin + MCP tools Accepted nr_llm is the single tooling authority for the AI stack. All tooling — builtin and MCP — is aggregated and executed there; consumers never reach an MCP server directly.
- ADR 154 An MCP server's liveness is observed, not inferred Accepted Two columns, written on every successful client round trip. last_contact and last_latency_ms on tx_nrllm_mcp_server. Any completed operation stamps them: a tool call, a catalogue import, a connection test. They answer "when did this installation last get an answer out of this server, and how long …
- ADR 161 One conformance suite for every MCP connection we support Accepted One suite, one case per connection. AbstractMcpConformanceTestCase holds every check. A concrete case supplies an McpConnectionProfile and nothing else, and the whole list has to pass for it. Two profiles ship: a stateless HTTP server that issues no session, and one that issues a session id — with …
- ADR 170 One deadline per MCP operation, spent across its legs Accepted The budget belongs to the operation, and travels with the call. McpClient opens one McpOperationDeadline per public method and passes it to every leg; McpHttpTransport grants each request what is left of it. The handshake spending six seconds is six seconds tools/call does not get.
- ADR 181 The client reads an event-stream framed answer, and holds no stream Accepted The transport offers both media types: Accept: application/json, text/event-stream.
- ADR 217 Delegated MCP authentication keeps the acting identity Accepted Add delegation as an opt-in authentication mode. Existing anonymous and server-Vault authentication keep their behaviour. Delegated servers name an exchange profile, audience and requested scopes. Configuration never stores a plaintext token. A malformed explicit mode is refused, not converted to …
- ADR 227 MCP replies belong to the request that accepts them Accepted Allocate opaque string request IDs from a transport-local random namespace and increasing sequence. Detect a changed execution process before reusing an inherited allocator, and refuse allocation failure or exhaustion before HTTP contact. The client keeps its existing per-operation session …
Editor Experience 11
- ADR 131 The editor-facing module Accepted One new backend module, nrllm_aitasks ("AI Tasks"), containing exactly what the editing role holds: run a prepared task, and decide agent runs waiting for approval. Everything else — configuration, providers, models, playground, wizards, analytics — stays admin-only.
- ADR 152 An editor action is a declaration, not a second executor Accepted An editor action is a produced, narrowly-bounded WRITING TOOL. It is declared metadata on top of the existing tool contract, and it executes on the existing tool / agent-runtime path.
- ADR 158 The Editor Action Center adds a catalogue, not a runtime Accepted The Editor Action Center adds a catalogue and one entry point. It adds no execution, no approval path and no authorisation rule.
- ADR 162 Bulk editor actions are N ordinary runs, not a bulk runtime Accepted A bulk editor action is N ordinary agent runs. There is no bulk runtime, no batch approval, no shared digest and no queue of its own.
- ADR 163 A use-case pack is data plus a small installer Accepted A pack is DATA. UseCasePack declares a configuration preset, task records, snippet records, a recommended governance posture and the tool groups its tasks benefit from. It has no behaviour. UseCasePackInstaller is the only thing that writes, it writes ordinary records through the existing services …
- ADR 168 A use-case pack declares editor actions, and only declares them Accepted A new advisory field. UseCasePack::$recommendedEditorActions is a list<string> of tool names. It is the fourth field of the same kind as the governance profile and the tool groups, and it is read by exactly one thing: the install plan.
- ADR 183 The AI section exists, and editor surfaces live in it Accepted A shared top-level section netresearch_ai holds five entries:
- ADR 186 A pack may ship snippets its own extension reads Accepted Two optional fields on PackSnippet, both defaulting to the behaviour that exists today, so no declared pack changes meaning.
- ADR 235 A vision editor action sends the image Accepted An action declares the fields it fills from an image. EditorAction gains visionFields. A non-empty list makes the action a vision action; the two tools name their fields.
- ADR 236 A batch over a folder is a draft per record Accepted A batch is still N ordinary runs. Everything ADR-162 derives from that holds: N turns, N digests, N verdicts, one catalogue call per record. Only the loop moves, from run() to defer().
- ADR 237 An empty field is filled after the save Accepted Nobody waits, and a model failure touches nothing. A hook after the database write stores one tool-free background run per empty, opted-in field (ADR-234) and the save ends. No model is called in the request. Whatever fails in the hook is logged and stops there, and the hook builds the agent …
Domain Model & Types 9
- ADR 003 Typed Response Objects Accepted Provider and service responses are returned as immutable readonly value objects (e.g. CompletionResponse) instead of loosely-typed arrays.
- ADR 006 Option Objects vs Arrays Superseded Introduced typed Option objects with fluent builders and factory presets (initially alongside array backward-compatibility) to replace ad-hoc option arrays; superseded by the object-only API (ADR-011).
- ADR 011 Object-Only Options API Accepted Removes array option support entirely; all methods take nullable typed Option objects (ChatOptions, EmbeddingOptions, VisionOptions) with factory presets.
- ADR 015 Type-Safe Domain Models via PHP 8.1+ Enums & Value Objects Accepted Uses PHP 8.1+ string-backed enums for all domain constants, each providing values()/isValid()/tryFromString() helpers plus domain methods like label() and getIconIdentifier().
- ADR 054 Typed Tool Turns on ChatMessage Instead of Wire Arrays Accepted Extends ChatMessage with two validated optional tail fields (toolCalls on assistant, toolCallId on tool) and named constructors, keeping it the single value object for all four roles instead of per-shape subclasses.
- ADR 108 Typed ToolResult with run-only artifacts Accepted Replace execute(array $arguments): string with execute(array $arguments): ToolResult — a final readonly value object carrying exactly one provider-facing string $content, a bool $isError, and a list<ToolArtifact> $artifacts that is run-scoped: it flows only to the trace, the inspector event stream …
- ADR 126 A named JSON-Schema subset, enforced strict Accepted The validator gains a second, STRICT mode next to the untouched lenient one. Its contract is a named subset, owned by StrictSchemaSubset:
- ADR 128 Provider-native structured output Accepted ChatOptions gains withResponseSchema(array) (declared outside the constructor, like suppressRequestCount — the @phpstan-consistent-constructor/ToolOptions constraint). Both completeStructured*() methods attach the already-pre-flighted schema; it reaches the adapter as response_schema in the flat …
- ADR 129 In-repo consumers of structured output Accepted Three consumers move onto the structured pipeline:
Configuration 11
- ADR 013 Three-Level Configuration Architecture (Provider-Model-Configuration) Accepted Separates concerns into three hierarchical levels — use-case Configuration references a Model, which belongs to a Provider — rather than embedding models in providers.
- ADR 031 Tagged Prompt Snippet Library Accepted Adds a lightweight PromptSnippet entity (identifier/name/description/fragment with free-form CSV tags) queried by case-insensitive exact tag tokens, separate from the heavier PromptTemplate.
- ADR 033 Specialized Models in the Model Registry Accepted Adds IMAGE/TEXT_TO_SPEECH/TRANSCRIPTION capabilities so image, TTS and transcription models are regular registry records, with fail-soft capability-based default-model resolution in the specialized services.
- ADR 055 Embeddings Join the Configuration Path; Dimensions Metadata Accepted Adds embedForConfiguration() mirroring the chat configuration path (adapter resolution, middleware, budget metadata, feature guard) with per-call options overriding stored defaults and cache keys keyed on the effective model.
- ADR 056 Configuration Presets — Consumer-Declared, Admin-Imported Records Accepted Consuming extensions declare needed configurations as presets via a DI tag (requirements/criteria only, never a provider/model/key); nr_llm lists undeclared-but-unimported presets as pending for one-click admin import.
- ADR 066 Criteria-Mode Configurations Resolve in the Service Layer Accepted getAdapterFromConfiguration() resolves the model via ModelSelectionService (direct model for fixed mode, criteria-selected for criteria mode) without writing the resolved model back onto the Extbase entity to avoid silently converting it to fixed-mode.
- ADR 069 Remove the Unusable PromptTemplate Stack Accepted Removes the never-usable PromptTemplate stack in full (entity, repository, service+interface, exception, table, tests/fixtures) since it had no TCA mapping and zero consumers, leaving Task's prompt_template string field and PromptSnippet untouched.
- ADR 070 User-less Configuration Resolution by Identifier Accepted Adds ConfigurationResolver::getActiveByIdentifier() throwing typed exceptions (NotFound/Inactive/AccessDenied) instead of returning null, and refuses beGroups-restricted records in user-less contexts.
- ADR 077 Plain Completion Joins the Named-Configuration Path Accepted Adds completeForConfiguration(prompt, configuration, options) mirroring chat's default-configuration branch (builds ChatMessages, injects skills, threads budget/idempotency metadata) taking typed ChatOptions rather than low-level arrays.
- ADR 124 A provider key can be set from the command line Accepted Add nrllm:provider:set-key <provider>, which does from a script exactly what the wizard does from the backend.
- ADR 244 A site names its configuration and its DeepL key Accepted Two site settings in the set netresearch/nr-llm: nrllm.configuration holds the identifier of an LLM configuration, nrllm.deepl.apiKeyIdentifier the identifier of an nr-vault secret. A key never sits in a site setting, because settings.yaml is versioned with the project; this is how a provider key …
Backend & UI 12
- ADR 014 AI-Powered Wizard System Accepted Adds three backend wizards (Setup, Configuration, Task) that turn natural-language descriptions into structured configurations/task chains via WizardGeneratorService, with graceful fallback when no LLM is available.
- ADR 019 Internationalization Strategy Accepted Follows TYPO3 XLIFF conventions (one label file per backend module, with German translations) for static UI strings and adds locale-aware placeholder substitution for dynamic LLM interactions.
- ADR 020 Backend Output Format Rendering Accepted Stores raw LLM output and renders format (HTML/Markdown/JSON/text) entirely client-side, with an ephemeral, non-persisted toggle operating on the cached raw content.
- ADR 024 Dashboard Widgets Accepted Ships two dashboard widgets (monthly AI cost, requests-by-provider over 7 days) reusing TYPO3's built-in widget classes, registered conditionally only when the Dashboard system extension is present.
- ADR 027 Split TaskController Accepted Splits the monolithic TaskController into four focused per-pathway controllers (list, wizard, execution, ...) plus service extraction and uniform typed responses.
- ADR 029 Usage Analytics Dashboard Accepted Ships a read-only usage-analytics module backed by a richer usage table (adding model and per-side token columns) and real cost computation derived from model pricing in UsageMiddleware.
- ADR 040 Playground Run Trace and Tool-Path Prompt Augmentation Accepted Adds an opt-in RunTrace recorder to ToolLoopService capturing per-round messages, tokens, timing and thinking for the admin playground, plus one-time prompt assembly, leaving production callers unaffected.
- ADR 117 Withdraw the backend capability permissions Accepted Remove the registration, the service and its interface rather than wiring the check in.
- ADR 118 Verify the specialized services from the backend Accepted Add backend endpoints that exercise translation and image generation, as verification surfaces rather than features.
- ADR 119 Where the backend modules live — Administration, for now Accepted Keep the modules under Administration for now. Do not treat this as settled.
- ADR 130 Capability grants for backend users Accepted Grants, not roles. BackendUserGrant is a string-backed enum mirroring ServiceAccountScope: each case documents exactly one enforcement point, there is no wildcard, and a case is only added together with its consumer — a grant nothing reads is worse than none. Roles are documentation-level presets …
- ADR 207 The DeepL character quota is shown on the test page Accepted The quota is shown in the translation card of the test page, through a fourth AJAX route on SpecializedTestController. No new module, no new card.
Security, Privacy & Budgets 22
- ADR 012 API Key Encryption at Application Level Superseded Encrypted stored API keys at application level using sodium_crypto_secretbox with a key derived from TYPO3's encryptionKey; later superseded (keys now live in nr-vault as UUIDs).
- ADR 023 Native Backend Capability Permissions Superseded Registers every ModelCapability enum value as a native TYPO3 BE group custom permission, with CapabilityPermissionService resolving checks (CLI/admin allowed, otherwise delegating to the backend user's permission).
- ADR 025 Per-User AI Budgets Accepted Adds a tx_nrllm_user_budget table of per-user ceilings (requests/tokens/cost × daily/monthly), enforced as a pure pre-flight BudgetService::check() that aggregates usage on demand from the existing usage table without a second write.
- ADR 030 Specialized Services Authenticate Through nr-vault Accepted Migrates keyed specialized services onto the nr-vault secure HTTP client, storing an API-key identifier (vault UUID) instead of the key and using placement hooks instead of manual auth-header building.
- ADR 037 Backend AJAX Admin Guard Accepted Adds one shared RequiresBackendAdminTrait::denyNonAdmin() returning a 403 JSON response for non-admins, applied at the very top of every AJAX-routed backend action.
- ADR 052 Usage Attribution Honours the Caller-Supplied beUserUid Accepted Adds an optional trailing beUserUid to trackUsage() and has UsageMiddleware read the same pipeline-context key the budget gate uses, so usage attribution and budget enforcement can no longer disagree.
- ADR 057 Speech and Image Services Carry Attribution in Options Accepted Extends options-based attribution (ADR-052) to specialized services via BudgetAwareOptionsInterface (optional beUserUid/plannedCost kept out of toArray to avoid leaking to remote APIs) — attribution only, no enforcement.
- ADR 064 Central Privacy Model Accepted Introduces one central PrivacyLevel enum (none/metadata/redacted/full, default metadata) read from extension configuration and applied at every content sink before a write, combining with strictest-wins.
- ADR 078 Budget Pre-Flight for the Specialized Image/Speech Services Accepted Adds a protected enforceBudget() to AbstractSpecializedService mirroring BudgetMiddleware, called after option resolution and before any HTTP dispatch in the image, TTS and transcription services so limits are enforced pre-flight.
- ADR 094 Tool data classes and provider trust zones Accepted Every tool declares what kind of data it returns. ToolDataClass is a total order from publicContent up to secretAdjacent. A tool's class comes from an explicit declaration or, failing that, from its group's default — each group taking the class of its worst plausible member. An unknown tool or an …
- ADR 099 Fail-closed HTTP egress for the specialized services Accepted The HTTP egress fails closed. AbstractSpecializedService tracks a private $withinLifecycle flag, set only while a runLifecycle() dispatch is executing (saved/restored around the terminal, so nesting is safe). assertWithinLifecycle() throws a \LogicException when the flag is false, and it is called …
- ADR 110 Service account scopes Accepted A service account carries an explicit, minimal set of scopes (NetresearchNrLlmDomainEnumServiceAccountScope). Each entry point a service account can reach checks the one scope it requires; a service account that does not declare that scope is denied. Backend users are unaffected — scopes govern …
- ADR 113 Fail-closed tool data-class enforcement switch Accepted The switch is fail-closed. The axis observes ONLY on a deliberate observe (matched case- and whitespace-insensitively). Every other case enforces:
- ADR 114 Encrypt queued and suspended agent-run state at rest Accepted Encrypt both columns at rest with an AgentStateCodec.
- ADR 115 Tool data-class enforcement is the default for new installs Accepted Ship enforce as the default and preserve observe for existing installs with an upgrade wizard, so the two cases are treated differently:
- ADR 123 One catalogue of secret shapes for every masking path Accepted Move the shapes into one place, Netresearch\NrLlm\Utility\SecretShapeRedactorTrait, next to the ErrorMessageSanitizerTrait it builds on, and have all three consumers read from it.
- ADR 144 Injected context carries a declared data class Accepted The same scale, reused. Snippets and skills carry a ToolDataClass value, the enum tool output already uses. A parallel enum with identical cases would be the duplication the module seams exist to prevent, and TrustZone::permits() already answers the comparison.
- ADR 155 The system prompt carries a declared data class Accepted One column, the ADR-144 shape. tx_nrllm_configuration gains system_prompt_data_class, a ToolDataClass value on the scale snippets, skills and tool output already share. Empty means UNDECLARED and constrains nothing, so every configuration that exists today keeps reaching every provider it reached …
- ADR 169 Record management belongs to TYPO3's permission model Accepted #768 asks how rootLevel => -1 is solved technically. The prior question is where the records should live.
- ADR 231 A monthly billing report with a markup Accepted A second page of the Analytics module, billing, shows one calendar month; exportBilling downloads the same month as CSV. Both reuse the queries of the Analytics page through UsageAnalyticsServiceInterface and refuse a non-administrator themselves, on top of the module's access => admin.
- ADR 232 An editor's budget status is public API Accepted The five classes are @api (ADR-127):
- ADR 242 A provider or a task tightens the privacy level Accepted A provider and a task can name a level, and it can only tighten. Both records get a privacy_level field, empty by default. The level a content is stored at is the strictest of the global level, the level of the provider and, for a queued run of a task, the level of the task, through …
Guardrails & Safety 10
- ADR 084 Human-in-the-loop tool approval Accepted Adds a human approval gate for side-effecting tools: a tool opts in via the empty RequiresApprovalInterface marker, and the agent loop suspends on such a call by throwing a control-flow signal that carries the serialised run state, persists it (status WAITING_FOR_APPROVAL), and resumes on approval or denial — the 41 read-only tools stay unchanged.
- ADR 085 Guardrail pipeline for provider responses Accepted Introduces a content-policy layer over provider responses: a GuardrailInterface returns an ALLOW/REDACT/RETRY/REQUIRE_APPROVAL/DENY verdict, auto-collected via the nr_llm.guardrail DI tag and run by GuardrailMiddleware at priority 115 (outermost). Ships secret-redaction and provider content-filter guardrails.
- ADR 086 Guardrail enforcement gaps Accepted Closes two gaps in the guardrail pipeline: the tool playground settles guardrail verdicts as policy outcomes (HTTP 200 guardrail_blocked / guardrail_approval_required) instead of a 500, and the streaming dispatcher screens the assembled completion at end-of-stream as a bounded, fail-soft audit (streamed chunks cannot be retracted).
- ADR 087 Input-side guardrails Accepted Extends guardrails to the outgoing prompt: an InputGuardrailInterface (tag nr_llm.input_guardrail) runs on the send path in LlmServiceManager, where a REDACT verdict rewrites the messages in place and DENY / REQUIRE_APPROVAL throw the same exceptions as the output side. Ships a secret-redaction input guardrail.
- ADR 088 Live streaming redaction Accepted Adds live REDACT for streamed output: each chunk re-redacts the whole raw buffer and emits only the stable prefix beyond a 128-byte holdback, so a secret — even one split across chunk boundaries — collapses to its marker before it is yielded. Opt-in via StreamRedactableInterface, bounded and multibyte-safe; DENY remains the end-of-stream audit's job.
- ADR 089 Guardrail boundary completeness — reasoning, system prompt, vision Accepted Extends secret redaction and the safety guardrails to boundaries an adversarial audit found unguarded — the model's reasoning/thinking output, the system prompt, and vision text inputs — so a secret cannot leak through a path the earlier output-side (ADR-085) and input-side (ADR-087) guards did not cover.
- ADR 098 Input-guardrail screening for specialized prompts Accepted InputGuardrailScreener gains screenText(string): string — the single- string sibling of screen(array $messages) — sharing one private screenContent() loop so both apply identical guardrails and verdict handling.
- ADR 106 Per-configuration guardrail policies Accepted A configuration selects which OPTIONAL guardrails apply; MANDATORY guardrails (secret redaction) always run and are never selectable.
- ADR 238 A term list blocks an output and says why Accepted A term list is a record, ``tx_nrllm_term_list``. It holds forbidden terms and required terms, one per line, in text fields for the reasons ADR-208 gives for the glossary. The module AI > Authoring > Term Lists lists them; FormEngine edits them.
- ADR 243 A rule set is a snippet and a term list Accepted No new record. A rule set is a tagged snippet for the context and a term list for the check. Both exist, and the trigger of ADR-139 has not fired: rules for a brand are text, which a snippet holds.
Streaming 4
- ADR 009 Streaming Implementation Accepted Uses PHP Generators to stream chat responses, yielding parsed SSE chunks so consumers can iterate output incrementally.
- ADR 041 Playground Live Run Streaming Accepted Streams the playground run as newline-delimited JSON (one event per RunStep via an opt-in onRecord closure) over the existing admin AJAX route, keeping the batch JSON response as the no-JS fallback.
- ADR 062 Streaming Request Lifecycle Accepted Introduces a dedicated StreamingDispatcher (a wrapping generator preserving the public Generator contract) owning a four-stage lifecycle whose budget pre-flight is eager so an over-budget caller is rejected at call time.
- ADR 240 The text of a tool round is streamed Accepted The call stays blocking; the text leaves earlier. A new contract, ToolStreamingCapableInterface, has one method beside chatCompletionWithTools(): the same arguments and a closure for the text, returning the same CompletionResponse. So the pipeline stays what it is. Budget, fallback, usage with real …
Tools & RAG 26
- ADR 010 Tool/Function Calling Design Accepted Adopts the OpenAI-compatible tool/function schema (type/function/name/description/parameters) as the common tool-definition format across providers.
- ADR 038 Tool Runtime (function-calling agent loop) Accepted Introduces a DI-tagged ToolInterface/ToolRegistry (getSpec/execute/isEnabledByDefault/requiresAdmin) as the authoritative allow-set, so an extension adds a tool just by tagging a class, driving a bounded function-calling agent loop.
- ADR 039 Global Per-Tool Availability State Accepted Adds a UI-less tx_nrllm_tool_state override table and ToolAvailabilityService computing each tool's effective enabled state (admin override, else its default) sitting between per-tool defaults and per-run allow-lists.
- ADR 042 Content and Configuration Read Tools for the Agent Accepted Adds five native read-only tools (search_records, get_page_content, read_records, get_typoscript, ...) implemented in-house without depending on mcp_server or ai_mate, using bound named-parameter equality filters (never raw SQL).
- ADR 043 Tool Groups with a Fail-Closed Enable Cascade Accepted Every tool declares a group (breaking getGroup() addition) and enablement cascades fail-closed across central group state, per-tool state and per-run allow-list, so a disabled group also covers later same-group tools.
- ADR 044 Error-Analysis Tools with Fail-Closed Guards Accepted Adds admin-only code-group tools (get_last_exception with inlined stack context, read_source, search_code) that inspect TYPO3 logs and project source via pure-PHP walks under hard budgets, sharing two guards.
- ADR 045 Schema and Resolution Tools Accepted Adds four read-only structure-group tools (get_full_tca index, get_table_schema with relation view, get_flexform_schema, ...) using a navigate-don't-dump approach that never serializes the whole TCA at once.
- ADR 046 History, URL and Validation Tools Accepted Adds four read-only tools (get_record_history, resolve_url via real SiteMatcher/PageRouter, validate_tca structural checks, ...) for auditing changes, routing-only URL mapping and live TCA validation.
- ADR 047 FAL Tools Accepted Adds five read-only tools in a new files group (list_fal_storages, browse_fal_folder, search_fal_files, ...) resolved exclusively through the FAL storage API, never exposing server-side base paths.
- ADR 048 Diagnostics Tools Accepted Adds six read-only admin-only tools (list_extensions, get_site_config with credential redaction, list_scheduler_tasks, ...) that enumerate instance configuration and attack surface without egressing package paths or raw YAML.
- ADR 049 RAG Site-Search Tools over Installed Search Indexes Accepted Adds a Service/Retrieval layer with a SearchBackendInterface and multiple tagged backends (Solr via HTTP select, ke_search, ...) exposing site_rag_query/site_fetch_source tools that read indexes others own, with public-access filtering.
- ADR 050 Retrieval and Embedding Scope — the Boundary with nr_ai_search Accepted Draws the scope boundary: nr_llm may read from indexes others maintain (or transient embed-and-compare) but must never own a persistent index it has to keep synchronised; cross-encoder reranking placement is later amended by ADR-075.
- ADR 051 Tool-Calling Feature Service — Narrow Consumer Interface Accepted Adds a ToolCallingService/Interface feature-service pair delegating to the manager's two tool-calling methods and adding beUserUid auto-population so per-user budget enforcement works without caller wiring.
- ADR 067 Solr Per-Language Core — No Language Filter Query Accepted Drops the fq=language filter from Solr search/fetchSource since language is already selected by the per-language read core, keeping only the public-access filter; shared-core language separation is a documented unsupported boundary.
- ADR 071 Public Keyword-Search Facade over the Retrieval Cascade Accepted Exposes a deliberately narrow KeywordSearchInterface (search + isAvailable) returning a stable KeywordHit DTO, so retrieval-cascade internals can evolve without touching the public contract.
- ADR 074 Reciprocal Rank Fusion as a Hosted Utility Accepted Hosts a pure final ReciprocalRankFusion class with a signature identical to the nr_ai_search original so consumers migrate by swapping the import; newable (not a DI service), so the audited public-service count is unchanged.
- ADR 075 Neutral Cross-Encoder Reranker Protocol Accepted Defines a neutral RerankerInterface (rerank(query, candidates)) with no consumer DTO crossing the boundary, plus an HttpReranker sidecar implementation that splits over-cap pools score-neutrally and throws typed errors — nr_llm never decides degradation.
- ADR 093 One tool gate, in the loop — not in the controller Accepted The loop is the chokepoint. ToolLoopService::resolveOfferedNames() now applies the per-configuration gate itself, alongside the global enablement intersection and the fail-closed admin filter it already applied. The caller's tool list is a request; the configuration is the grant. resume() runs the …
- ADR 120 The agent loop's tool gate is a required collaborator Accepted The composite policy becomes a required constructor argument. The allow-list resolver is deleted along with the now-unreachable legacy chain. The schema validator becomes non-nullable with a new JsonSchemaValidator() default — it is stateless and has no constructor, so there is no wiring under …
- ADR 201 A consumer can ask which tools a run will not be offered Accepted UnavailableToolsResolverInterface (@api, public in the container) runs the gate over every REGISTERED tool for one configuration and one backend user and returns the decisions that refuse, each carrying the tool name and a ToolDenialReason: toolDisabled, requiresAdmin, configurationGroup or …
- ADR 202 Reading a public web page through an address guard Accepted fetch_external_url reads ONE http(s) page and returns its readable text.
- ADR 210 A caller can ask which tools run without approval Accepted A new @api interface, UnattendedToolFilterInterface, with one method: unattended(list<string> $toolNames): list<string> returns the names, in their order, of the tools that run without an approval.
- ADR 215 Retrieval evaluation keeps its provenance Accepted Keep the existing registries and contracts. Neither EvaluatableRetrieverInterface nor GoldenQuestionSetProviderInterface gains a required member. An additive, optional provenance capability lets a retriever declare its corpus revision, embedding-model revision, chunking identity, pipeline identity …
- ADR 216 Invocation policies see the call they decide Accepted Keep the offerability contract. ToolCallPolicyInterface and its five gates remain source compatible. A separate invocation-policy contract decides a typed context immediately before execution. The decisions compose with AND: a new rule cannot make a tool available or undo an existing denial.
- ADR 218 Document source synchronisation belongs to the search companion Accepted Implement the directory source in the existing netresearch/nr-ai-search package. No new package, nr-llm dependency, table or persistent index is introduced here. Companion ADR-036 records its implementation decision.
- ADR 220 Evaluation results name their generator Accepted Distinguish the DB provider instance from the adapter key returned in CompletionResponse.provider. After a configuration-driven terminal succeeds, its actual resolved DB provider, final outbound model alias and response-reported model become reserved additive response metadata. The actual fallback …
Skills 7
- ADR 035 Skill Ingest (GitHub-hosted SKILL.md sources) Accepted Adds dedicated SkillSource and Skill Extbase entities to fetch, parse and review GitHub-hosted SKILL.md files, with an app-level GitHub host allowlist on top of the nr-vault SSRF guard.
- ADR 036 Skill Injection (attach + compose into prompts) Accepted Composes attached skill prose in a shared SkillInjectionService and prepends it to the user prompt (never the system role), applied only to text-generation paths (completion/translation/task) and never to embeddings, vision or speech.
- ADR 061 Skill Trust Levels, Signed Manifests, Injection Scanning Accepted Adds a SkillTrustLevel provenance scale gating injection/allowed-tools against a configurable minimum, plus manifest fingerprints and prompt-injection scanning, separate from support status.
- ADR 175 A forced skill binds by the same rule as a forced snippet Accepted Skills get the pair snippets already have. SkillRepository::findByUids() resolves enabled skills only; findExistingByUids() drops the enabled clause and nothing else. Both preserve the caller's order and both keep the deleted restriction, so a deleted record still resolves to nothing either way.
- ADR 214 Approved skill versions instruct, load on demand and run processes Accepted A skill version that an authorised person approved is composed as an instruction. The approval binds to a digest over everything the model reads of that version and to the source that vouches for it. Skills can be written in the backend, are listed in context and loaded when needed, and a …
- ADR 221 Skill synchronization holds a durable lease Accepted Synchronize only a persisted source that is neither physically absent nor soft-deleted by TYPO3. Missing or soft-deleted rows fail closed before remote contact or skill changes. Detached test fixtures must become real persisted sources; tests cannot define an unsafe production exception. The …
- ADR 229 A skill declares whether it needs a page, outside its version Accepted A frontmatter key page declares whether the skill works on one page. Its values are required (the skill works on one page and needs it), optional (it may work on a page) and none (it does not work on a page). The value is read case-insensitively after trimming. An absent key, a non-string value and …
Testing & Quality 4
- ADR 060 Quality Evaluation — Golden Sets, Grading and Regression Detection Accepted Adds an opt-in evaluation layer (DI-tagged golden prompt sets, pluggable graders, run/aggregate service, result persistence with regression detection, CLI) that never touches the request pipeline.
- ADR 072 Retrieval-Quality Evaluation — Golden Questions and Top-k Hit Rates Accepted Adds the retrieval counterpart of ADR-060 — DI-tagged golden question sets, a pluggable retriever contract, document-level top-1/top-3 hit-rate scoring and a CLI — reusing ADR-060's persistence/regression machinery, shipping methodology not questions.
- ADR 073 First-Party Test Doubles for Consumer-Facing Interfaces Accepted Ships maintained fakes (FakeToolCallingService, FakeEmbeddingService) in a runtime-autoloaded Testing namespace so consumers can use them without nr_llm's dev dependencies, with PHPStan enforcing they track the real interfaces.
- ADR 079 First-Party Fakes for Completion, Vision and Budget Services Accepted Extends ADR-073 with FakeCompletionService, FakeVisionService and FakeBudgetService (FIFO/canned returns plus call recording) as plain final classes implementing the real interfaces for consumer tests.