Skip to content

fix(agent): unwrap pre-wrapped context-engine tool schemas - #48065

Closed
arminanton wants to merge 1 commit into
NousResearch:mainfrom
arminanton:fix/context-engine-tool-schema-doublewrap
Closed

fix(agent): unwrap pre-wrapped context-engine tool schemas#48065
arminanton wants to merge 1 commit into
NousResearch:mainfrom
arminanton:fix/context-engine-tool-schema-doublewrap

Conversation

@arminanton

Copy link
Copy Markdown
Contributor

What does this PR do?

agent_init defensively unwraps an already-enveloped context-engine tool schema before re-wrapping it, so a misbehaving engine can't brick every turn with an HTTP 400.

A ContextEngine.get_tool_schemas() is contracted to return bare schemas ({name, description, parameters}); the host wraps each one in the OpenAI envelope ({"type": "function", "function": {...}}) before adding it to agent.tools. If an engine mistakenly pre-wraps its schemas in that envelope, wrapping again yields {"function": {"function": {...}}} whose outer function.name is empty. The provider then rejects the entire request with tools[N].function.name: empty string, so every turn fails for the whole session once that engine is active.

The fix detects the envelope and unwraps to the inner bare schema before processing, so both bare and pre-wrapped inputs register with a correct name.

Related Issue

Same error signature as #11411 (empty string name → provider 400), but a different root cause and code path: #11411 is about empty-name function_call_output on the Responses-API input side; this is the outgoing tool-schema registration in agent_init. The empty-name 400 has multiple distinct sources; this PR closes the context-engine one.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/agent_init.py — new module-level helper _normalize_context_engine_schema() that returns the inner bare schema when the input is an OpenAI envelope (type == "function" + a function dict + no top-level name), and returns bare/non-dict inputs unchanged. The context-engine tool-collection loop now calls it before wrapping, replacing the previous inline _schema.get("name", "") with a normalize-then-read.
  • tests/agent/test_context_engine_tool_schema_unwrap.py — 8 new tests exercising the real helper from agent_init (not a re-implementation): bare passthrough, envelope unwrap, non-dict input, the name+type-but-not-envelope edge case, and the resulting registration shape (bare / pre-wrapped / mixed / no-empty-names-emitted).

How to Test

  1. Have a context engine return a pre-wrapped schema: {"type": "function", "function": {"name": "grep", ...}}.
  2. Initialize an agent with that engine active.
  3. Before: the tool registers as {"function": {"function": {...}}} with an empty outer name → the next request 400s with tools[N].function.name: empty string. After: the schema is unwrapped, the tool registers with function.name == "grep", and the request is valid.
pytest tests/agent/test_context_engine_tool_schema_unwrap.py -q

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation (helper docstring explaining the envelope/unwrap contract) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no config keys)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) — N/A (pure Python, no OS primitives; passes the Windows-footgun check)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

ContextEngine.get_tool_schemas() is contracted to return BARE schemas
({name, description, parameters}); the host wraps each in the OpenAI envelope
before adding it to agent.tools. If an engine mistakenly pre-wraps its schemas,
wrapping again yields {"function": {"function": {...}}} whose OUTER
function.name is empty, and the provider rejects the whole request with HTTP 400
"tools[N].function.name: empty string" — breaking every turn for the session.

Add _normalize_context_engine_schema() to detect the envelope and unwrap to the
inner bare schema before processing, so both bare and pre-wrapped inputs
register with a correct name. Same error signature as NousResearch#11411 but a different
root cause (that one is the Responses-API input path; this is the outgoing
tool-schema registration in agent_init).
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists labels Jun 17, 2026
@arminanton
arminanton marked this pull request as ready for review June 17, 2026 21:46
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…olution semantic review

Council: (3) per-hunk justification — every unmapped hunk blamed via git log -S to its
origin commit, mapped to a standing user instruction (exclusion) OR a shipped PR. 216/216
accounted, 0 uncovered. Found+resolved 11 initially-uncovered (all in shipped test-cluster
PRs NousResearch#48065/NousResearch#48101/NousResearch#49644/NousResearch#50032/NousResearch#50080/NousResearch#50078). (4) semantic-equivalence review of the
resolution patches: all 6 active ones re-anchor their PR's exact intent onto v0.17.0, no
silent behavior change; removed the DEAD agent_gemini_cloudcode_adapter patch (never
invoked — withdrawn file — and imported withdrawn google_user_agent).
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused regression analysis.

Automated hermes-sweeper review found that current main already provides this exact protection:

  • agent/memory_manager.py:49 normalizes OpenAI-enveloped schemas to their inner bare function schema and rejects nameless inputs.
  • agent/agent_init.py:1939-1958 applies that normalizer to every context-engine schema before adding its OpenAI envelope.
  • Commit 710cd48fb1d649925e159aa08d29993f828c40ee (fix(agent): validate context/memory tool schemas before wrapping) implemented this behavior and is contained in v2026.7.1.

Closing as implemented on main.

@teknium1 teknium1 closed this Jul 14, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants