Skip to content

Voice asks which device you mean, LLM agents can ask Jev, and 1.16.0 - #31

Merged
AboveColin merged 42 commits into
mainfrom
feature/1.16
Sep 24, 2026
Merged

AboveColin merged 42 commits into
mainfrom
feature/1.16

Conversation

@AboveColin

Copy link
Copy Markdown
Owner

Release 1.16.0. manifest.json carries 1.16.0 and pins jevclient==1.2.0, which is on PyPI. HACS reads the version from the release tag, so the merge commit needs the tag 1.16.0. hacs.json keeps Home Assistant 2026.9.0 as the minimum. The config entry stays at version 1.2, so there is no migration.

What changes

  • Voice asks which device you mean when two fit the name, and the reply picks one or is handled as a new command. A room you name settles it first, then the room of the satellite that heard the command.
  • A voice command that acts says the sentence Home Assistant's own agent says, such as "Turned on the light". Where there is none, it says "Done."
  • The Assist dialog and the pipeline debug view show what Jev answered for each command.
  • A voice command or an action is refused before it is sent when its estimate does not fit the daily budget. The estimate counts the 250 tokens every request pays, and a small call no longer sets the ratio for a large one.
  • "Turn off all the lights" counts as every device of one kind.
  • A hidden device named in full is not swapped for an exposed one with a shorter name.
  • Other LLM agents can ask Jev yes/no and pick-one questions. The option is off by default.
  • jev.calibrate finds the threshold that fits a yes/no sensor from its recorded history. It costs no tokens.
  • Docs for calibration, the LLM tools, the ask-back and a month of cost.

Gate

Run on the branch tip:

Check Result
ruff check, ruff format --check pass
mypy --strict no issues in 22 source files
pytest 474 passed, 12 skipped
coverage 97% total, config_flow.py 100%
mkdocs build --strict pass
hassfest 0 invalid integrations

CI passed on each phase PR, #20 to #30.

Live check

I installed the merged code on a test instance that runs Home Assistant 2026.9.3, on its existing entry and usage storage.

  • Two lights called Lamp: the question came 3 of 3 times. Replies that picked a device ran the command on it. Replies that were a new command ran as new commands.
  • With a device placed in a room, "turn on the lamp" turned on the Lamp in that room, 4 of 4. A device in a third room, or in no room, got the question.
  • A hidden "Desk lamp" next to an exposed "Lamp": "turn on the desk lamp" went to the fallback, 3 of 3.
  • "Turn off all the lights" acted. "Turn on all the lights" was refused with whole-house commands off.
  • A budget refusal gave its message as an error reply. The budget sensor stayed off when only the voice command was refused.
  • Estimates against bills: 348 against 271 for a small action, 3,018 against 2,642 for 6 KB.
  • jev.calibrate returned a threshold. ai_task.generate_data answered.

Known

"Toggle the lamp in the office" split 0.53 toggle against 0.47 turn_on and went to the fallback. That is below the 0.6 floor, and this release does not change it.

AboveColin and others added 30 commits September 23, 2026 21:53
…ly budget

The voice agent only checked whether the budget was already spent, so the
last command of the day went through in full and could end it over budget.
It now estimates the command from its request size, as contexts and AI Task
do, holds the estimate while the call runs, and reports the size to the
tracker so the estimate learns from voice requests too.
Each command adds one agent detail to the conversation trace: the decision,
the latency and tokens, and every answer with its probabilities. The debug
view then shows why a command picked a device or fell back, without a
diagnostics download.
When the action is clear and the entity answer is split between two exposed
devices, the agent asks "Do you mean A or B?" and keeps the microphone open.
The reply gets one small question about those two devices, and the first
command then runs on the one it picked. A reply that picks neither is handled
as a new command. The question lasts as long as the chat session.
… budget

jev.noul, jev.choice, jev.score and jev.ask never checked the budget. A script
that looped on an action spent without a limit. They now make the same check
as contexts, AI Task and voice: estimate the request, refuse it with a message
that names the estimate, what is left and the budget, and hold the estimate
while the call runs.
A 138 byte action was estimated at 70 tokens and billed 278. The estimate
was bytes over a ratio, with no fixed part, and the ratio came from the
last call alone. After that small call, a 6 KB request was estimated at
14,327 tokens, billed 3,277 one call earlier, and refused on every try.

The estimate is now 250 tokens plus the body over the ratio, and only a
call whose body was billed at least 250 tokens updates the ratio.
Jev ran the action through the intent layer, which says nothing, and the
Assist dialog showed no reply at all. Home Assistant's own agent picks a
sentence from home-assistant-intents by what the command targeted. Jev
now reads the same key off the slots it sent and renders that sentence,
and says "Done." where the package has none, as for a toggle.
The everything option read "The whole house, with no room or device
named". For "turn off all the lights" the model put 0.44 to 0.52 on it,
below the 0.6 floor, and the command fell back. With the option naming
every device of one kind, "all the lights", "every light" and "the
lights" acted 6 of 6 times live. Commands naming an exposed device or a
room still named it, 8 of 8, and "turn on all the lights" was still
refused by the whole-house gate.
…n error

The check refuses a command whose estimate does not fit, which happens
with tokens left. The line said the budget "is spent". It now says not
enough is left, in all 13 languages.

With no fallback agent, the reply was an action_done response. The Assist
dialog and a satellite read that as a command that went through. It is
now an error, no_intent_match for a sentence not understood and
failed_to_handle for the budget, a rejected key and no answer, as the
default agent answers.
# Conflicts:
#	tests/test_conversation.py
chat_log.async_trace() writes to the conversation trace, and nothing in
Home Assistant 2026.9.3 reads that: pipeline_debug/get showed only
run-start, intent-start, intent-end and run-end.

The agent now sends the record to the chat log's delta listener as
thinking_content. The pipeline stores it as an intent-progress event, and
the Assist dialog appends thinking_content under the reply. On a
development instance pipeline_debug/get held the event for an action and
for a refusal, with the decision, the slots, the top three options of
each answer, the model, the input tokens and the latency.

The delta goes to the listener only and is not added to the chat log, so
a fallback agent reading the same conversation does not see it.
# Conflicts:
#	custom_components/jev/conversation.py
On a development instance with two lights both called "Lamp", "turn on
the lamp" came back as one of them at 1.00, every time. The entity
answer never split, so the ask-back never asked, and the agent acted on
a guess that looked sure.

The candidates now come from the exposed names of the chosen device's
kind. The whole name said beats a name that only shares a word with the
command, so "the lamp" is Lamp beside a Desk lamp, and "the desk lamp" is
the Desk lamp. Two names that fit equally well get the question. More
than two fall back. A room the model is sure of narrows the list first,
so "the lamp in the office" acts. When no name fits the words at all, the
model's answer stands as before.

A reply that picks a device reads the command again without the check,
so it does not ask the same question twice.
A test window with the door open for two minutes returned hours_true 0.0
beside times_true 1, which read as the never-true refusal failing to
fire. Both hour counts now keep two places, so the same window reads
0.03.
The ask-back section described the probability split. It now describes
the name comparison. It also restores the "What it costs" heading that
the section had replaced.

The diagnostics section named the debug view as the place for the agent
detail. The note now shows in the Assist dialog and as an intent-progress
event in the pipeline debug view.

The refusal row and the budget sensor now say what the budget actually
refuses: a command whose estimate does not fit, with tokens left.
Home Assistant's own agent prefers the satellite's area when a name fits
devices in several rooms. Jev asked instead. It now reads the satellite
entity's area, then its device's area, the same lookup, and asks only
when that room does not hold exactly one of the devices.
With an unexposed "Desk lamp" and an exposed "Lamp", "turn on the desk
lamp" came back from the model as Lamp and turned it on, 3 of 3 times on a
development instance. The model can only pick from what it was shown, and
Lamp was the closest option.

The snapshot now keeps the names of entities the model is not shown: not
exposed, a lock or entrance cover, or past the cap. They stay in Home
Assistant. When the command says one of them in more words than it says
the chosen device, the agent falls back and records "named a device that
is not exposed".
A voice command is refused before it is sent if it would pass the daily budget
AboveColin and others added 12 commits September 24, 2026 11:49
The Assist debug view shows what Jev answered for each command
Voice asks which device you mean when two fit the name
A hidden device named in full is not swapped for an exposed one
Actions are refused before they are sent if they would pass the daily budget
Other LLM agents can ask Jev yes/no and pick-one questions, when the options allow it
# Conflicts:
#	custom_components/jev/conversation.py
#	tests/test_conversation.py
A new action finds the threshold that fits a yes/no sensor, from its recorded history
The budget estimate counts the part of the bill every request pays
A voice command that acts says what it did
"Turn off all the lights" counts as every device of one kind
Docs for calibration, the LLM tools, the ask-back and a month of cost
@AboveColin
AboveColin merged commit 561c2e7 into main Sep 24, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant