Engine Application
Observability
Actuator Endpoints
management.endpoints.web.exposure.include=health,prometheus,metrics exposes:
| Endpoint | Use |
|---|---|
|
The probes the Kubernetes deployment uses — see the engine
chart. The Elasticsearch health indicator follows |
|
Metrics in Prometheus format. Scraped by the |
|
Individual meters, for ad-hoc inspection. |
All metrics carry an application tag with the application name.
Metrics
Meters are declared in engine/metrics/ and tagged per pipeline (withPipelineTags adds the
pipeline key, locale, device type and similar dimensions), so they can be sliced per agent or
channel.
| Meter | Meaning |
|---|---|
|
Gauges of currently open conversations, backed by the transport handlers' session maps. |
|
Counters of created sessions, turns, users and devices. |
|
Element throughput, tagged additionally by element type, and error count. |
|
Audio volume in and out, in samples. |
|
Element throughput between the engine and the realtime model. |
|
Animation data produced for avatar rendering. |
|
Response latency measured from the last interim transcript — how quickly the agent reacts to speech still in progress. |
|
The same latencies measured from the final transcript. |
|
Timing of the planning and interaction phases of a turn. |
LLMMetrics counts input_tokens, output_tokens and total_tokens; NettyMemoryMetrics
reports the Netty allocator’s memory use, which is the resource that matters most when many audio
streams are open.
The latency timers are recorded from the per-turn timing log when a turn is saved, so they reflect what the user experienced rather than internal call durations.
Tracing
The engine is instrumented with the OpenTelemetry Java agent, attached through JAVA_TOOL_OPTIONS
and pointed at a collector with OTEL_EXPORTER_OTLP_ENDPOINT. In a deployment the chart sets both,
along with the service name, resource attributes and logback MDC capture, when
app.serviceMonitor is enabled. Locally, see Telemetry.
Reactive chains are annotated with Reactor checkpoint() calls at the pipeline boundaries
(Pipeline/{sessionId}, SessionFetch/{sessionId}, PipelineEgress/{sessionId} and the
processor chain), which is what makes a stack trace from deep inside a reactive operator
attributable to a session. Reactor scheduler metrics are enabled at startup.
Two logging switches exist for expensive diagnostics and are off by default —
logging.level.r for reactive chain logging and logging.level.SpeechRecognizer for recognition
logging. Neither should be enabled in production.
Log statements carry structured attributes, most importantly session.id, so a conversation can
be followed across components.
Provider Health Checks
Beyond the Actuator health of the application itself, the engine can probe its external providers:
/api/health/textSpeechSynthesizer/, /api/health/speechRecognizer/,
/api/health/realtimeModel/ and /api/health/llm/ (see Internal
Controllers). Each checker performs a minimal real request against every configured component,
or against one named component, and reports a HealthCheckResult per provider — which is how a
provider outage is distinguished from an engine problem.
The LLM checker works per model provider rather than per component: OpenAi, OpenAiReasoning,
AzureOpenAi, AzureOpenAiReasoning, Anthropic and GoogleGenAi, each with its default
promethist.ai.llm.* configuration and that configuration’s timeout. A probe succeeds on the first
non-blank streamed text. It calls the provider model directly instead of through the wrapper of
SpringAiLLMService, so probes produce no LLM logs.
Usage Records
EDRService writes event detail records (EDR documents) capturing active users and active
conversation duration. They are the basis for usage metering and, together with
SubscriptionService, for enforcing a tenant’s seat and minute allowance.
LLM Logs
Every model invocation is stored as an LLMLog document by LLMLoggerService: prompt, response,
tool calls, model parameters and token usage, with sensitive information masked. Each log carries the
sessionId, turnId, and the session’s projectRef and agentRef. Logs expire after 30 days. They are
readable per session through /api/session/{sessionId}/log/llm and are what the debugger works with.
Session Debugger
The debugger is an internal tool for inspecting and re-running what a conversation actually did.
Its API is under /api/debug and it is backed by:
| Service | Role |
|---|---|
Transforms the raw LLM logs of a session into a turn and component timeline. |
|
|
Re-run a recorded invocation against a model, optionally with a different configuration, so a change can be evaluated against a real conversation. |
Supplies tool callbacks during replay, so recorded tool calls are simulated rather than executed again. |
|
Replays recorded realtime sessions against a realtime provider. |
|
Judges a replayed response against the original with a judge prompt, which is how regressions are detected in bulk. |
|
Pairs the pipeline manager’s log entries with the internal realtime model’s entries, using several anchors rather than timestamps alone. |
|
|
Authorize access against the session’s project ACL, and provide the front end’s OAuth bootstrap
configuration ( |
The debugger front end is served at /agent-debugger/index.html and has two modes: Sessions, the
timeline and replay of one session, and Tool Calls, described below.
Tool Call Analysis
The Tool Calls mode shows how the agents of a project call their tools, so agent designers can judge
whether tools, in particular handovers, are called when they should be. It starts by opening a project:
GET /api/debug/projects/{projectRef} confirms the project exists and that the caller may debug it.
The open project and section are kept in the URL, as
?view=tool-calls&tab=criteria|search&projectRef={projectRef}, so a link reopens the same view. The
project has two sections:
-
Criteria — the project’s criteria, with the state of their revisions, and editing, publishing, discarding a draft and deleting them.
-
Search — the statistics. The scope is the sessions of the project started within a period, optionally narrowed to an agent and revision, to LLM components and to tools. Component and tool filters take comma-separated names in which
matches any characters, sohandover_selects every tool of thehandovertool box. Test-mode sessions are excluded unless requested, and at most 5,000 newest sessions are analysed at once.
LLMDebuggerToolCallService selects the sessions on session and reads their llmLog documents through an
aggregation that keeps only the offered tool names (tools[].name) and the tool trace
(toolExecutionTrace), with arguments and responses cut to their first 2,000 characters. The unit of
analysis is a turn and a tool: a tool is offered in a turn if any log of the turn offered it, and
called if any log of the turn called it. A log without a turn id forms a turn of its own.
Reading the logs is the expensive part, so the sessions and log digests of a project and period
(with its agent, revision and test-mode choice) are kept in memory for five minutes: drilling down,
paging, filtering by component or tool and classifying reuse them, while Show reads them again
(refresh=true). The logs are found through the sessionId_created index of llmLog.
For every tool the statistics give the number of calls, the sessions it was called in (and the share of all sessions in scope), the turns it was called in out of all turns of those sessions, failed calls, loops and, when criteria exist, the relevance verdicts.
-
Outcome of each call is derived by
ToolCallOutcome:UnknownToolwhen the tool box answered that no such tool exists or the name was not offered,Failedwhen the call threw or returned a known failure response (a missing skill, an invalid URL),Emptyfor an empty result, otherwiseSucceeded. OnlyFailedandUnknownToolcount as failures. -
Loop is a turn in which the same tool was called three or more times.
-
Relevance compares whether the tool was called with whether it should have been, judged against
ToolCallCriteria: true positive, false positive (called but should not have been), false negative (should have been called but was not) and true negative. The false positive rate is computed over evaluated calls, the false negative rate over evaluated turns that needed the tool.
Every number opens the turns behind it, each with the user input, the calls and their outcome, and the judge’s verdict and reasoning; a turn opens in the Sessions mode on the component that called the tool.
Criteria and Classification
ToolCallCriteria is the specification of when a tool should and should not be called, for a
tool name or pattern in a project, optionally narrowed to one agent. Criteria are versioned by ref
and revision and edited through a draft, managed by ToolCallCriteriaService:
-
saving writes the single
Draftrevision, created one above the latest revision and overwritten by later saves (POST /api/debug/tool-call-criteria); -
publishing turns the draft into the one
Publishedrevision and archives the previous one (POST /api/debug/tool-call-criteria/{ref}/publish); only the published revision is used for classification, so a draft takes effect once it is published; -
discarding drops the draft (
DELETE /api/debug/tool-call-criteria/{ref}/draft); criteria that were never published disappear with it; -
deleting drops the draft and adds an
Archivedrevision on top (DELETE /api/debug/tool-call-criteria/{ref}), which removes the criteria from the project and from classification while keeping their history and stored classifications.
Each criteria has a call scope:
-
Per turn (the default) judges every turn on its own: should the tool have been called in response to that turn’s user input?
-
Per session judges the whole session at once: should the tool have been called at least once in the conversation? It suits tools that have to happen once at the right moment, such as a handover. The unit is a session and a tool — offered if any of its turns offered the tool, called if it was called at least once — and the judge reads the session transcript from the
turncollection (the user input and the agent response of every turn, as the platform’s other conversation-level judgements read it). A session that is still open is not judged until it ends.
When several published criteria of the same scope match a tool, criteria narrowed to the session’s
agent win over project-wide ones, an exact name wins over a pattern, and a longer pattern wins over a
shorter one. Each scope is resolved on its own, so a tool can be judged per turn and per session at
the same time, and sessions of different agents can be judged by different criteria. The statistics
therefore show one line per criteria in the false positive, false negative and evaluated columns, as
X (n.n %) of Y classified turns or … classified sessions — Y being every turn (session) classified
against that criteria — each naming its criteria and linking to it; the drill-down of a
line lists the turns, or the sessions, that criteria judged. Under each false positive and false negative
line, the count is split by the judge’s confidence (high X (n.n %) · medium … · low …), each percentage
of all units classified against the criteria.
Classification is run on demand. ToolCallClassificator is the contract: it receives one turn, or one
whole session — the conversation, the offered tools and the calls — together with the criteria of each
tool, and returns a verdict per tool. It does not depend on llmLog, so it can later run inside the pipeline on a live turn.
LLMToolCallClassificator implements it with an LLM judge: all tools of a turn are decided in one
call, the judge only decides whether each tool should have been called, and the
verdict follows from that decision and whether the tool was called. The judge model is set by
promethist.debugger.tool-call-classification.model (gpt-5.1); it is engine configuration and the
same for all criteria, so verdicts are comparable across tools and projects.
Judge calls are not written to llmLog.
pipeline.classificator.tool-call selects the implementation: llm (the default) is the LLM judge
above, jev is JevToolCallClassificator, backed by TypeSafe AI Jev, a hosted model that answers typed
questions instead of generating text and is much faster and cheaper per turn. It needs
spring.ai.typesafe.api-key. Each turn is one Jev call whose state is the turn’s conversation, ending at
its last user message (a session’s is its whole conversation), and whose
questions are one yes/no question per tool, with the criteria’s should call and should not call as
the conditions for yes and no. The question carries the same rules as the LLM judge: should not call
takes precedence when both apply, and a situation the criteria do not cover warrants the call only when
the need is unambiguous. The answer, a value between 0 and 1, decides should call against
pipeline.classificator.jev.threshold (0.5, strictly between 0 and 1), and its distance from the
threshold gives the confidence. Jev returns no explanation, so the stored reasoning states the decision
and the score.
Each verdict is stored as a ToolCallClassification for the turn, the tool and the exact criteria revision,
which acts as a cache: a turn already classified against the current revision is not judged again, and
a new revision leaves older verdicts stale but stored. Delete Classification on a tool removes the
verdicts its row counts — those of the sessions in the current scope, against the published criteria
revisions — so they are judged again (POST /api/debug/tool-calls/classifications/delete). A run classifies at most 5,000 pending turns,
newest first.
The results of an older revision stay viewable: Show results on a revision in the criteria history
recomputes the statistics and the drill-down from the verdicts stored for that revision (filter parameter
criteriaRevision={criteriaRef}:{revision}, which puts that revision in place of the published one; drafts
have no results). The view is read-only — classifying and deleting are refused (409) — and Back to
current returns to the published revisions.