Repository navigation
Conversation
Add --session-turns N to `dokimi arms run`. Each question is asked as its opening question followed by its `follow_ups` list, with the conversation kept across turns. Every turn records its token usage (input, output, cache read and write, cost), and the arm totals are the sum over turns. --turn-reminder TEXT adds a `<arm>+reminder` variant of each arm that appends the reminder to every user turn, so the cost of a reminder can be read against the same arm without it. Sessions are measurement only: --judge is refused with --session-turns, and agentic arms are refused in a session. A question with fewer follow-ups than N-1 fails before any answer is paid for. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at c2ccd81. The session design is sound: the history is copied per call, the reminder stays out of the assistant turns, and the judge and agentic refusals fire before any answer is paid for. Four things are worth fixing before merge, all inline: the cost of a failed session, the empty-reply path, the pairing commands ignoring session runs, and untested CLI wiring and options.
What I ran, on a Linux review host at c2ccd81:
make ci-local: the test job passes (7082 passed, 21 skipped) and so do the changed-line coverage, changelog, schema-check, schema-compat, package-smoke and gitleaks jobs. The ruff, mypy, zizmor, pre-commit, bandit, deptry, mkdocs and pip-audit jobs did not run in my venv:make devstops at the tools install on this base, which #179 fixed onmain. Static checks, mypy and the strict docs build pass on GitHub's CI at this head.- Reproductions for the code findings, with the LLM transport mocked (
fake_llm). - A mutation run: 66 mutants of the new code against
tests/test_session_runner.py, 8 of which pass the full file and are findings, plus 1 equivalent mutant. The 13 tests fail on the base modules except the guard test, as the description says.
Checked, no issues
- Schema versioning:
arms.v1.jsonhas noadditionalProperties: falseon the record or arm, sosession,session_turnsand the armsessionblock are additive. A success run and a mid-session failure both validate. - Matrix and subjects: those parsers do not define the new flags, so
forwarded_run_configforwards nothing andgetattr(..., None)applies. - Usage sums:
n_calls, cache read and write and cost add correctly over turns;cost_summaryreads a successful session like any other arm. - Docs and fragment: the
python cli.py arms runform matches the other examples on that page; the fragment name and format match the others. No new page, so nonav:change.
…sees session settings A session that failed mid-way dropped token_usage/metrics so cost_summary lost the completed turns; an empty reply stopped the session but was recorded as a success. run_config now records session_turns/turn_reminder and plugin-delta and the matrix pair key refuse to mix session and single-turn runs. Adds tests for the arms run wiring and per-call session options. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #174 +/- ##
==========================================
+ Coverage 91.51% 91.70% +0.18%
==========================================
Files 216 224 +8
Lines 27074 27629 +555
Branches 4457 4551 +94
==========================================
+ Hits 24777 25337 +560
+ Misses 1632 1624 -8
- Partials 665 668 +3
🚀 New features to boost your workflow:
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 41fc167. The four fixes from my earlier review are in and hold up: the error arms keep their spend, an empty reply is an errored arm, run_config records the session settings, and the CLI wiring and per-call options are tested. One bug comes from the spend fix itself, and two test gaps are left. Two nits on the docs.
What I ran, on a Linux review host at 41fc167:
make ci-local: 27 passed, 1 failed, 7 skipped. The test job passes (7114 passed, 1 skipped) and so do changed-line coverage, mypy, deptry, schema-compat, package-smoke, windows-smoke, mkdocs and gitleaks. pip-audit fails ongitpython 3.2.0(CVE-2026-114145, fix in 3.2.1), which comes from the lock files this PR does not touch.- Reproductions for the code findings, with the LLM transport mocked (
fake_llm). - A mutation run: 35 mutants of the new code against
tests/test_session_runner.py, 4 of which pass the full file (3 are findings below, 1 is a harmlessraw_answerduplicate).
Checked, no issues
- Empty reply:
empty_reply: turn Nis the index of the empty turn, and its spend is kept. A failing or empty first turn gives a validarms.v1artifact and the report renders it. - Old artifacts:
plugin_delta,matrix_rollupandmodel_compare.replicate_keytreat a missingsession_turnsandturn_reminderas equal toNone, so two old runs still pair. - Reminder arm:
--turn-remindergives[baseline, baseline+reminder], validates against the schema and renders in the report.
| usage = session["usage"] | ||
| return {"token_usage": usage.as_token_usage_dict(), |
There was a problem hiding this comment.
A failed session records raw_answer_chars and final_answer_chars as 0, which skews the plugin-delta answer-length comparison.
_session_spend calls as_metrics_dict(answer_chars=0, final_chars=0), but the turns that completed did produce text. plugin_delta._answer_metrics (plugin_delta.py:225-241) averages those two fields over every arm that has a metrics dict. It does not look at error, and a plain failed arm has no metrics, so this is new with the spend fix.
Repro (fake_llm, --session-turns 3, every good reply is "x" * 100): in the control run q1 answers 100 chars at turn 0 and then returns an empty reply, q2 succeeds. In the plugin run both succeed. compare_plugin_runs returns raw_answer_chars: {baseline_avg: 50.0, plugin_avg: 100.0, delta: 50.0} (same for final_answer_chars). Every real answer is 100 chars on both sides, so the +50 is made up by the failed arm.
Suggestion: usage.as_metrics_dict() with no char arguments. The keys are then omitted (usage.py:126), _answer_metrics skips them, and cost_summary still sees metrics. Add a test that a failed session's metrics has no raw_answer_chars.
| def test_a_session_run_is_not_paired_with_a_single_turn_run(): | ||
| from dokimi.eval.plugin_delta import PluginDeltaError, compare_plugin_runs | ||
|
|
||
| with pytest.raises(PluginDeltaError, match="different session_turns: None vs 3"): | ||
| compare_plugin_runs(_artifact(), _artifact(session_turns=3, plugin_set="caveman")) | ||
|
|
||
|
|
||
| def test_the_matrix_pairing_key_separates_session_runs(): | ||
| from dokimi.eval.matrix_rollup import _pair_key | ||
|
|
||
| assert _pair_key(_artifact()) != _pair_key(_artifact(session_turns=3)) | ||
| assert _pair_key(_artifact(session_turns=3)) == _pair_key(_artifact(session_turns=3)) |
There was a problem hiding this comment.
turn_reminder in the pairing checks is untested.
_artifact() always sets turn_reminder=None, and only session_turns differs between the two runs. Mutation check: both of these pass the full file (25 passed):
plugin_delta.py:76with the tuple cut to("session_turns",)matrix_rollup.py:128with theturn_reminderline removed
Then a run with a reminder and one without, at the same turn count, are paired as if comparable. Their request text differs on every user turn.
Suggestion: give _artifact a turn_reminder argument, and add compare_plugin_runs(_artifact(3, reminder=None), _artifact(3, reminder="R", plugin_set="caveman")) raising different turn_reminder, and _pair_key differing for the same case.
| if cfg.has_context: | ||
| context = join_injected(cfg.injected_context) | ||
| opening = (CONTEXT_PROMPT_PREFIX.format(context=context[:MAX_CONTEXT_CHARS]) | ||
| + CONTEXT_PROMPT_SUFFIX.format(question=turns[0])) |
There was a problem hiding this comment.
The opening turn of a context-bearing arm is not tested against the first question.
Mutation check: CONTEXT_PROMPT_SUFFIX.format(question=turns[-1]) here passes all 25 tests. test_opening_is_prepared_from_the_first_turn_not_the_last covers prepare(turns[0]) and the plain ANSWER_PROMPT_PLAIN branch (both mutants fail), but not this branch. With the mutant, a skill or context arm opens a 3-turn session with the last follow-up and the history is in the wrong order, for those arms only.
Suggestion: in the context-cap test (test_the_opening_turn_caps_injected_context_as_the_plain_run_does), also assert that the opening ends with the first question and does not contain the last follow-up.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
I pushed fixes for the two doc nits from my review, one commit each:
GitHub's Tests now fail at 1a20b18 on every Python version, in Still open: the bug and the two risks from my review. |
Part of #4.
Summary
dokimi arms runcan now ask each question as a multi-turn session, with the history kept and the token usage recorded per turn.Off by default. Without
--session-turns, the runner and its output are unchanged.--judgeis refused with--session-turns, because the judge would score the last answer against the first question.mcp:,skill-agent:,skill-catalog:) are refused in a session withsession_unsupported.follow_upsfails before any answer is paid for. Every record of a run has the same number of turns.stopped_reason: empty_reply.dokimi/eval/session.py, soarm_runner.pygrows by one hook (_answer_session_arm) and the output shape gains asessionblock only in session mode.Evidence
Tests (new file
tests/test_session_runner.py, 13 tests) use thefake_llmadapter only. Nothing reaches a provider.Fail-on-old:
dokimi/eval/arm_runner.pyanddokimi/commands/arms.pyreplaced with the intel/main versions, the rest of the branch kept:Local checks on the rebased head (rebased onto intel/main caae46a):
Merge Danger
Door: two-way. The flags are opt-in and additive. Reverting the commit restores the old runner.
Blast Radius: low. The code runs only when
--session-turnsis passed.Behaviour changes for callers:
arms runflags.--session-turns N(N >= 2) and--turn-reminder TEXT.--turn-reminderwithout--session-turnsis an error.follow_upsis a new optional question field. Questions without it run as before. A session run needs N-1 entries on every question.--judgewith--session-turnsfails up front, before any call is made.token_usageandmetricsof a session arm are the sums over all turns, so a session arm's cost is not comparable with a single-turn arm's cost for the same question.Checklist
pytestpassesruff,mypy, and schema checks passdocs/benchmarking-and-comparison.md(new "Multi-turn sessions" section)changelog.d/)🤖 Generated with Claude Code