mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
fix(delegation): route native-SDK providers through runtime resolver; fail on '(empty)' sentinel
Two related bugs caused subagent delegation to silently return empty summaries with 0 tokens when the user configured delegation.provider=bedrock alongside delegation.base_url=https://bedrock-runtime.<region>.amazonaws.com. Root cause #1 — misrouting in _resolve_delegation_credentials(): The configured_base_url branch unconditionally forced provider='custom' and api_mode='chat_completions', only specializing for chatgpt.com, anthropic, and kimi hosts. Bedrock (and other native-SDK providers) fell through as 'custom' + chat_completions, which then POSTed OpenAI-shaped JSON at Bedrock's native API. Bedrock rejected the payload and returned nothing, which looked like an empty LLM response to the child agent. Fix: when provider is one of {bedrock, vertex, google, google-genai}, skip the base_url short-circuit and fall through to resolve_runtime_provider(), which knows how to construct the proper SDK client. base_url can still be forwarded through that path for regional overrides. Root cause #2 — '(empty)' sentinel accepted as success: After N retries of empty LLM responses, run_agent.py emits the literal string '(empty)' as final_response. _run_single_child then hit `elif summary:` — '(empty)' is truthy, so status became 'completed' and the parent surfaced a blank result with no error. Users saw api_calls=4, tokens=0, duration~0.4s, status=completed. Fix: treat final_response.strip() == '(empty)' as a failure so the parent surfaces it instead of silently accepting zero-content 'success'. Both paths were reproduced in a live Hermes TUI session on us-west-2 Bedrock (provider=bedrock, model=us.anthropic.claude-sonnet-4-6) and are covered by new tests in tests/tools/test_delegate.py.
This commit is contained in:
parent
852c9b3cb2
commit
8f21311906
2 changed files with 70 additions and 2 deletions
|
|
@ -918,6 +918,31 @@ class TestDelegateObservability(unittest.TestCase):
|
|||
result = json.loads(delegate_task(goal="Test max iter", parent_agent=parent))
|
||||
self.assertEqual(result["results"][0]["exit_reason"], "max_iterations")
|
||||
|
||||
def test_empty_sentinel_marks_status_failed(self):
|
||||
"""Regression: a child that returns the literal '(empty)' sentinel
|
||||
(emitted by run_agent.py when the LLM returns empty responses after
|
||||
retries — e.g. transport misrouting) must be reported as failed, not
|
||||
silently accepted as a completed delegation. Otherwise the parent
|
||||
surfaces an empty string as if the subagent succeeded."""
|
||||
parent = _make_mock_parent(depth=0)
|
||||
|
||||
with patch("run_agent.AIAgent") as MockAgent:
|
||||
mock_child = MagicMock()
|
||||
mock_child.model = "claude-sonnet-4-6"
|
||||
mock_child.session_prompt_tokens = 0
|
||||
mock_child.session_completion_tokens = 0
|
||||
mock_child.run_conversation.return_value = {
|
||||
"final_response": "(empty)",
|
||||
"completed": True,
|
||||
"interrupted": False,
|
||||
"api_calls": 4,
|
||||
"messages": [],
|
||||
}
|
||||
MockAgent.return_value = mock_child
|
||||
|
||||
result = json.loads(delegate_task(goal="Test empty sentinel", parent_agent=parent))
|
||||
self.assertEqual(result["results"][0]["status"], "failed")
|
||||
|
||||
|
||||
class TestSubagentCostRollup(unittest.TestCase):
|
||||
"""Port of Kilo-Org/kilocode#9448 — parent's session_estimated_cost_usd
|
||||
|
|
@ -1341,6 +1366,32 @@ class TestDelegationCredentialResolution(unittest.TestCase):
|
|||
creds = _resolve_delegation_credentials(cfg, parent)
|
||||
self.assertIsNone(creds["provider"])
|
||||
|
||||
@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
|
||||
def test_bedrock_provider_with_base_url_uses_runtime_resolver(self, mock_resolve):
|
||||
"""Regression: provider=bedrock + base_url set must NOT fall through the
|
||||
direct-base_url branch (which would force provider='custom' +
|
||||
chat_completions and silently misroute OpenAI JSON to the Bedrock
|
||||
native endpoint, returning empty responses)."""
|
||||
mock_resolve.return_value = {
|
||||
"provider": "bedrock",
|
||||
"base_url": "https://bedrock-runtime.us-west-2.amazonaws.com",
|
||||
"api_key": "aws-resolved-key",
|
||||
"api_mode": "bedrock_converse",
|
||||
}
|
||||
parent = _make_mock_parent(depth=0)
|
||||
cfg = {
|
||||
"model": "us.anthropic.claude-sonnet-4-6",
|
||||
"provider": "bedrock",
|
||||
"base_url": "https://bedrock-runtime.us-west-2.amazonaws.com",
|
||||
}
|
||||
creds = _resolve_delegation_credentials(cfg, parent)
|
||||
# Must use Bedrock, not 'custom'
|
||||
self.assertEqual(creds["provider"], "bedrock")
|
||||
self.assertEqual(creds["api_mode"], "bedrock_converse")
|
||||
mock_resolve.assert_called_once()
|
||||
self.assertEqual(mock_resolve.call_args.kwargs.get("requested"), "bedrock")
|
||||
|
||||
|
||||
|
||||
class TestDelegationProviderIntegration(unittest.TestCase):
|
||||
"""Integration tests: delegation config → _run_single_child → AIAgent construction."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue