From 7527961b19c002d3236f8022506d9ae6441010f2 Mon Sep 17 00:00:00 2001 From: coldxiangyu Date: Sun, 19 Apr 2026 11:07:34 +0800 Subject: [PATCH] fix(cron): drop top-level oneOf so OpenAI Codex/Responses accept tool schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #3125 added a top-level `oneOf` branch to `_CRON_PARAMETERS` to advertise per-action required fields. OpenAI Codex/Responses rejects `oneOf`/`anyOf`/`allOf`/`enum`/`not` at the root of function parameters, so any agent that registers the cron tool now fails to start with: HTTP 400: Invalid schema for function 'cron': schema must have type 'object' and not have 'oneOf'/'anyOf'/'allOf'/'enum'/'not' at the top level. Remove the top-level `oneOf`. The original intent of #3125 (stop LLMs from looping on the #3113 contract mismatch) is preserved by: - `validate_params` — runtime-enforces `message` for `action='add'` and `job_id` for `action='remove'` - field descriptions — each schema field already flags "REQUIRED when action='...'" so the LLM sees the contract The regression test is updated to lock the invariant in the other direction: the top-level schema must not contain `oneOf`/`anyOf`/`allOf`/`not`, and the REQUIRED hints must stay on `message` and `job_id`. Verified: - tests/cron/ 70 passed - tests/agent/test_loop_cron_timezone.py + tests/providers/ 232 passed Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- nanobot/agent/tools/cron.py | 26 ++++---------------------- tests/cron/test_cron_tool_list.py | 30 +++++++++++++----------------- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/nanobot/agent/tools/cron.py b/nanobot/agent/tools/cron.py index 111bb1c8..7124a2a7 100644 --- a/nanobot/agent/tools/cron.py +++ b/nanobot/agent/tools/cron.py @@ -43,30 +43,12 @@ _CRON_PARAMETERS = tool_parameters_schema( required=["action"], description=( "Action-specific parameters: add requires a non-empty message plus one schedule " - "(every_seconds, cron_expr, or at); remove requires job_id; list only needs action." + "(every_seconds, cron_expr, or at); remove requires job_id; list only needs action. " + "Per-action requirements are enforced at runtime (see field descriptions) so the " + "top-level schema stays compatible with providers (e.g. OpenAI Codex/Responses) that " + "reject oneOf/anyOf/allOf/enum/not at the root of function parameters." ), ) -_CRON_PARAMETERS["oneOf"] = [ - { - "properties": { - "action": {"enum": ["add"]}, - "message": {"type": "string", "minLength": 1}, - }, - "required": ["action", "message"], - }, - { - "properties": { - "action": {"enum": ["list"]}, - }, - "required": ["action"], - }, - { - "properties": { - "action": {"enum": ["remove"]}, - }, - "required": ["action", "job_id"], - }, -] @tool_parameters(_CRON_PARAMETERS) diff --git a/tests/cron/test_cron_tool_list.py b/tests/cron/test_cron_tool_list.py index a3ee9b1a..5ffd4691 100644 --- a/tests/cron/test_cron_tool_list.py +++ b/tests/cron/test_cron_tool_list.py @@ -348,24 +348,20 @@ def test_add_job_can_disable_delivery(tmp_path) -> None: def test_cron_schema_advertises_action_specific_requirements(tmp_path) -> None: tool = _make_tool(tmp_path) + # Only ``action`` is required at the schema root — per-action requirements + # are enforced at runtime via ``validate_params`` and surfaced to the LLM + # through field descriptions. We intentionally do NOT set top-level + # ``oneOf``/``anyOf``/``allOf``/``enum``/``not``: OpenAI Codex/Responses + # reject those at the root of function parameters (#3265 regression). assert tool.parameters["required"] == ["action"] - assert tool.parameters["oneOf"] == [ - { - "properties": { - "action": {"enum": ["add"]}, - "message": {"type": "string", "minLength": 1}, - }, - "required": ["action", "message"], - }, - { - "properties": {"action": {"enum": ["list"]}}, - "required": ["action"], - }, - { - "properties": {"action": {"enum": ["remove"]}}, - "required": ["action", "job_id"], - }, - ] + for disallowed in ("oneOf", "anyOf", "allOf", "not"): + assert disallowed not in tool.parameters, ( + f"Top-level '{disallowed}' is rejected by OpenAI Codex/Responses tool schemas" + ) + message_desc = tool.parameters["properties"]["message"]["description"] + assert "REQUIRED" in message_desc and "action='add'" in message_desc + job_id_desc = tool.parameters["properties"]["job_id"]["description"] + assert "REQUIRED" in job_id_desc and "action='remove'" in job_id_desc def test_validate_params_requires_message_only_for_add(tmp_path) -> None: