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
66 lines
2.8 KiB
Python
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"
|
|
)
|