Files
nisse/nisse-dev/tests/model_contract.py
T
dongarandClaude Opus 5.5 e351d0a74e
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m37s
CI / e2e (pull_request) Successful in 4m5s
nisse-core: group modules into packages by architecture section
A pure move; behaviour unchanged.

- models (L9, L7): llm, catalog, probe, render, render_anthropic, context
- log (L7, L6): canonical, views
- state (L8): store (was state), reconcile, worker, cancel, dump
- knowledge (L10, L6.4): vault, memory
- conversation (L5): transport, matrix
- execution (L13): ssh (was execution), testing
- authority (P3, P4, L4): actions
- gitea, oplog, envfile stay at the top until each gains a second module.

Every import, monkeypatch target, script and doc reference rewritten.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
2026-10-13 21:16:09 +00:00

66 lines
2.8 KiB
Python

"""What a real model API insists on, for scripted test models.
Both formats the agent sends require a tool call's result to come right
after it: in Anthropic's, the next message is the user's and starts with a
tool_result for every tool_use, before any text; in OpenAI's, the assistant
message with tool_calls is followed directly by a tool message for each. A
scripted model that doesn't check this lets a broken transcript pass the
unit tests and be rejected live; this makes it fail here.
"""
from typing import Any
from nisse_core.models.render import OpenAIChatRenderer
from nisse_core.models.render_anthropic import AnthropicMessagesRenderer
def assert_renderable(events: list[Any]) -> None:
_anthropic(AnthropicMessagesRenderer().render(list(events), None, "m")["messages"])
_openai(OpenAIChatRenderer().render(list(events), None, "m")["messages"])
def _anthropic(messages: list[dict[str, Any]]) -> None:
# A request ends on the user's side: current models refuse assistant
# prefill ("The conversation must end with a user message"), found live
# on 2026-10-07 when the driver's prose nudge left its own reply last.
assert not messages or messages[-1]["role"] == "user", (
f"anthropic messages.{len(messages) - 1}: the conversation ends with an "
"assistant message (prefill), which the API refuses"
)
for index, message in enumerate(messages):
if message["role"] != "assistant":
continue
used = [b["id"] for b in message["content"] if b.get("type") == "tool_use"]
if not used:
continue
following = messages[index + 1] if index + 1 < len(messages) else None
assert following is not None and following["role"] == "user", (
f"anthropic messages.{index}: tool_use with no user message after it"
)
blocks = following["content"]
leading = []
for block in blocks:
if block.get("type") != "tool_result":
break
leading.append(block["tool_use_id"])
assert set(used) <= set(leading), (
f"anthropic messages.{index}: tool_use ids {sorted(set(used) - set(leading))} "
"lack a tool_result at the start of the next message"
)
def _openai(messages: list[dict[str, Any]]) -> None:
for index, message in enumerate(messages):
calls = [c["id"] for c in message.get("tool_calls") or []]
if not calls:
continue
answered = []
for after in messages[index + 1 :]:
if after["role"] != "tool":
break
answered.append(after["tool_call_id"])
assert set(calls) <= set(answered), (
f"openai messages.{index}: tool_calls {sorted(set(calls) - set(answered))} "
"are not answered by the tool messages right after them"
)