Files
turnstone/tests/test_optimizer_seam.py
T
Patrick Buckley bc3fa60011 fix(providers): segregate inline reasoning at the drain seam
Passthrough servers (parserless vLLM/llama.cpp, LM Studio, bare
gateways) emit reasoning as literal <think>/<reasoning> blocks inside
content, and only three of nine drained lanes stripped them: web_fetch
tool results persisted raw think blocks into every following turn
(#940), judge verdicts parsed through tag noise, and a draft verdict
inside a think block could shadow the real one at the output guard.

One rule at the seam now. drain_stream accumulates content in RUNS
bounded by interleaving signals (provider-parsed reasoning deltas,
tool-call deltas) with the interactive consumer's within-chunk ordering
— reasoning, then content, then the tool-call close — and splits each
run through split_inline_reasoning, the one-shot form of the
interactive lane's ThinkTagSplitter: a pure raw split, exactly
equivalent to the streaming form on every catalog case. One trim policy
exists and the drain owns it: blank edge lines are trimmed once over
the joined runs when a tag was consumed, so tag residue dies at the
edges while genuine inter-run paragraph separators survive. Extracted
text is appended to result.reasoning after any server-parsed reasoning
with a blank-line boundary and rides the native lane as the
reasoning_text synth block. Orphan CLOSE tags deliberately pass through
byte-identical: a close whose open never arrived is indistinguishable
from prose QUOTING the tag, and drained lanes routinely quote
third-party text — reclassifying would let a malicious page containing
the literal tag destroy the extraction that cites it. The title lane
keeps a local rfind peel as display-string formatting. The citations
footer folds only onto non-blank content — sourcing for an answer that
does not exist is dropped rather than handed to emptiness checks as a
footer-only "answer".

Every private strip is deleted: the title lane's strip, the summarizer
strip, _strip_reasoning itself, and the optimizer's five regexes
(_strip_markdown_fence is now the one fence rule, applied to normalized
model output only, never to or-fallback values). Think-only and
whitespace-only responses drain to blank content, and every lane's
no-answer fallback gates on blankness: web_fetch returns an honest
extraction-error card, the intent judge takes the empty-retry ladder,
the task-agent synthesis reports "(no output)", and the optimizer keeps
the current observer system and prompt verbatim on no-answer passes.
Final-say reads (optimizer analyst, eval final_content, the notify
hook) use trajectory.final_assistant_text — the last assistant turn
only, never an earlier narration presented as the conclusion — while
last_assistant_text is the salvage walk (task_agent partial-work
recovery), skipping tool-call-only, all-reasoning, and whitespace-only
turns. Perception memoizes every completed description immediately,
including an empty one — one perceive per key, ever — under a
commit-lock guard so an empty result never overwrites a concurrently
memoized real description; an all-reasoning perception model pins the
placeholder until restart, and the remediation is server-side (a
reasoning parser or the template thinking toggle on the perception
alias). A true double-reasoning shape (inline-extracted text alongside
a native reasoning block) logs chars-only at the drain, where it is
distinguishable from the routine reasoning_delta mirror.

The dialect's semantics are pinned as one table
(tests/_reasoning_dialect.py) driven through shared fixtures
(think_tag_stream, seam_provider): one-shot conformance, the exact
one-shot/streaming equivalence property, the drain seam rules including
quoted-tag safety, run-boundary and separator-preservation pins,
per-lane pins for all nine lanes, and the empty-content assistant wire
shape.

Closes #965. Closes #940.
2026-08-05 00:23:11 -07:00

128 lines
4.4 KiB
Python

"""#965 per-lane pins for the optimizer: content arrives IR-clean from the
drain seam, and the emptiness fallbacks are load-bearing.
The optimizer's five private regex strips are gone; these rows pin (a) that
tagged model output still yields clean prompts/systems (the seam does the
work now), and (b) the two ``or``-fallback flips: an all-reasoning pass
keeps the CURRENT observer system / prompt verbatim instead of wiping the
observer system or evaluating an empty-prompt evolution node.
"""
from __future__ import annotations
from tests._session_helpers import seam_provider
from turnstone.optimizer import _observe_and_update_optimizer, _propose_prompt_modification
_OBSERVER_SYSTEM = "You observe optimization runs and refine the optimizer system."
def test_observer_tagged_output_yields_clean_system() -> None:
out = _observe_and_update_optimizer(
client=object(),
model="m",
optimizer_system=_OBSERVER_SYSTEM,
iterations=[],
provider=seam_provider("<think>weighing the history</think>Refined observer text."),
)
assert out == "Refined observer text."
def test_observer_think_only_keeps_current_system_verbatim() -> None:
# Before the seam, the private regex emptied the text AFTER the
# ``or``-fallback check and the observer system was WIPED.
out = _observe_and_update_optimizer(
client=object(),
model="m",
optimizer_system=_OBSERVER_SYSTEM,
iterations=[],
provider=seam_provider("<think>no conclusion reached</think>"),
)
assert out == _OBSERVER_SYSTEM
def test_proposal_tagged_output_yields_clean_prompt() -> None:
out = _propose_prompt_modification(
client=object(),
model="m",
current_prompt="Old prompt.",
test_cases=[],
iteration_result={"cases": {}},
history=[],
provider=seam_provider("<think>rework section two</think>New prompt text."),
)
assert out == "New prompt text."
def test_proposal_think_only_keeps_current_prompt() -> None:
# An all-reasoning pass reads as "no changes" downstream — never an
# empty-prompt evolution-tree node.
out = _propose_prompt_modification(
client=object(),
model="m",
current_prompt="Old prompt.",
test_cases=[],
iteration_result={"cases": {}},
history=[],
provider=seam_provider("<think>hmm</think>"),
)
assert out == "Old prompt."
def test_observer_whitespace_only_content_keeps_current_system() -> None:
# Falsiness is checked AFTER normalization: whitespace-only content
# (tag-free, so seam-byte-identical and truthy) must not strip to ""
# past the fallback and wipe the observer system.
out = _observe_and_update_optimizer(
client=object(),
model="m",
optimizer_system=_OBSERVER_SYSTEM,
iterations=[],
provider=seam_provider("\n\n \n"),
)
assert out == _OBSERVER_SYSTEM
def test_proposal_whitespace_only_content_keeps_current_prompt() -> None:
out = _propose_prompt_modification(
client=object(),
model="m",
current_prompt="Old prompt.",
test_cases=[],
iteration_result={"cases": {}},
history=[],
provider=seam_provider("\n\n"),
)
assert out == "Old prompt."
def test_proposal_fallback_prompt_is_never_fence_stripped() -> None:
# The fence-strip normalizes MODEL output only; a no-answer pass keeps
# a fence-bearing current prompt VERBATIM, never reduced to its fence
# innards.
fenced_prompt = "Do the task.\n```python\nexample()\n```\nBe precise."
out = _propose_prompt_modification(
client=object(),
model="m",
current_prompt=fenced_prompt,
test_cases=[],
iteration_result={"cases": {}},
history=[],
provider=seam_provider("<think>no conclusion</think>"),
)
assert out == fenced_prompt
def test_proposal_model_output_fence_is_unwrapped() -> None:
# The fence rule applies to MODEL output (only): a fenced proposal
# yields its innards, and prose outside the fences is discarded.
out = _propose_prompt_modification(
client=object(),
model="m",
current_prompt="Old prompt.",
test_cases=[],
iteration_result={"cases": {}},
history=[],
provider=seam_provider("Here you go:\n```\nNew prompt text.\n```\nHope that helps!"),
)
assert out == "New prompt text."