fix(gateway): reject null + DO-NOT-PASS-NULL remediate for decision/reflect

Smoke-6 found the agent calling note(scope='decision', context=null,
chosen=null, rationale=null) eight times in a row. Root cause split
across two surfaces:

1. The MCP tool schema declared these fields as `str | None = None`,
   producing a JSON schema of `anyOf [string, null]`. minimax-m2.7 read
   that and decided null was a valid value — passed it on every retry.

2. The remediate text used `<placeholder>` syntax for the example
   without telling the agent "don't pass null" explicitly.

Fixes:
- roboco/api/schemas/v2/do.py NoteRequest: context, chosen, rationale,
  what_done, what_learned, what_struggled now typed `str = ""` (no None).
  Pydantic on the route rejects literal null with 422 BEFORE the gateway
  sees it. Empty string still counts as missing at the gate.
- roboco/mcp/do_server.py note(): matching signature changes so the
  MCP tool schema declares the fields as `string` not `anyOf[string,null]`.
- roboco/services/gateway/content_actions.py: remediate text now opens
  with "DO NOT pass null" and the example uses concrete values (redis vs
  postgres) instead of <angle bracket> placeholders. Reflect remediate
  also gets the don't-pass-null intro and concrete values.

8 new tests pin "schema rejects null for each of the 6 string fields"
plus "empty defaults work for unscoped notes".
This commit is contained in:
Renn F
2026-05-14 05:27:45 +02:00
parent 21007e122f
commit 3bbaf0d645
4 changed files with 135 additions and 26 deletions
+6 -6
View File
@@ -179,14 +179,14 @@ def note(
scope: str = "note",
task_id: str | None = None,
title: str | None = None,
context: str | None = None,
context: str = "",
options: list[dict[str, str]] | None = None,
chosen: str | None = None,
rationale: str | None = None,
chosen: str = "",
rationale: str = "",
consequences: list[str] | None = None,
what_done: str | None = None,
what_learned: str | None = None,
what_struggled: str | None = None,
what_done: str = "",
what_learned: str = "",
what_struggled: str = "",
next_steps: list[str] | None = None,
) -> dict[str, Any]:
"""Write a journal entry. scope in note|decision|reflect|learning|struggle.