What isn't the dev agent's moves to nisse-core, unchanged in behaviour, with its tests: canonical, render, render_anthropic, llm, catalog, probe, context, views, state, reconcile, worker, cancel, dump, transport, matrix, execution, testing, vault, memory, oplog, envfile and gitea (whole). - nisse_core.state.StateStore holds the tables every agent has; an agent's subclass adds its tables (version, migrations), roles and views. nisse_dev.state.StateStore adds the build VM blocks (v5). - views.project takes what each role is shown, rather than a fixed table. - Vault takes the agent's name (agent=), for provenance. - A test that nisse_core never imports an agent. - The loop, config, git, delegation, expert, tools and evals stay in nisse-dev until nisse-ops has a loop to share. Docs: the Development Plan marks M7.1 done and notes that Phase 6 waits on the agent host and Terraform; Architecture Part L names the package. 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.render import OpenAIChatRenderer
|
|
from nisse_core.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"
|
|
)
|