Skip to content

Commit 20cedbf

Browse files
cornelcroiclaude
andauthored
fix: raise proper Exception in ChainAgent instead of a string (#248) (#550)
raise/throw of a bare string is a TypeError in Python and produces an unhelpful non-Error in TypeScript — neither can be caught by type, neither carries a stack trace, and neither chains the original cause. - Python: `raise f"..."` → `raise Exception(f"...") from error` - TypeScript: `throw \`...\`` → `throw new Error(\`...\`)` - Add test_chain_agent.py covering the fix (exception is a real Exception, __cause__ is the original error) plus basic routing and edge cases Fixes #248 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 74cd097 commit 20cedbf

3 files changed

Lines changed: 105 additions & 2 deletions

File tree

python/src/agent_squad/agents/chain_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def process_request(
6262

6363
except Exception as error:
6464
Logger.error(f"Error processing request with agent {agent.name}:{str(error)}")
65-
raise f"Error processing request with agent {agent.name}:{str(error)}" from error
65+
raise Exception(f"Error processing request with agent {agent.name}:{str(error)}") from error
6666

6767
return final_response
6868

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Unit tests for ChainAgent."""
2+
import pytest
3+
from unittest.mock import AsyncMock, MagicMock
4+
from agent_squad.agents.chain_agent import ChainAgent, ChainAgentOptions
5+
from agent_squad.types import ConversationMessage, ParticipantRole
6+
7+
8+
def _make_agent(name: str, response_text: str = "ok") -> MagicMock:
9+
agent = MagicMock()
10+
agent.name = name
11+
agent.process_request = AsyncMock(return_value=ConversationMessage(
12+
role=ParticipantRole.ASSISTANT.value,
13+
content=[{"text": response_text}],
14+
))
15+
return agent
16+
17+
18+
def _options(agents, **kwargs):
19+
return ChainAgentOptions(
20+
name="chain",
21+
description="test chain",
22+
agents=agents,
23+
**kwargs,
24+
)
25+
26+
27+
# ---------------------------------------------------------------------------
28+
# Basic routing
29+
# ---------------------------------------------------------------------------
30+
31+
@pytest.mark.asyncio
32+
async def test_single_agent_returns_response():
33+
agent = _make_agent("a", "hello")
34+
chain = ChainAgent(_options([agent]))
35+
result = await chain.process_request("hi", "u", "s", [])
36+
assert result.content[0]["text"] == "hello"
37+
38+
39+
@pytest.mark.asyncio
40+
async def test_output_of_first_agent_fed_to_second():
41+
a1 = _make_agent("a1", "step1")
42+
a2 = _make_agent("a2", "step2")
43+
chain = ChainAgent(_options([a1, a2]))
44+
await chain.process_request("start", "u", "s", [])
45+
a2.process_request.assert_awaited_once()
46+
call_input = a2.process_request.call_args[0][0]
47+
assert call_input == "step1"
48+
49+
50+
# ---------------------------------------------------------------------------
51+
# Exception handling — the core bug fix
52+
# ---------------------------------------------------------------------------
53+
54+
@pytest.mark.asyncio
55+
async def test_agent_exception_raises_exception_not_string():
56+
agent = MagicMock()
57+
agent.name = "bad_agent"
58+
agent.process_request = AsyncMock(side_effect=RuntimeError("boom"))
59+
chain = ChainAgent(_options([agent]))
60+
61+
with pytest.raises(Exception) as exc_info:
62+
await chain.process_request("hi", "u", "s", [])
63+
64+
# Must be a real Exception, not a string
65+
assert isinstance(exc_info.value, Exception)
66+
assert "bad_agent" in str(exc_info.value)
67+
assert "boom" in str(exc_info.value)
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_exception_chained_from_original():
72+
original = RuntimeError("root cause")
73+
agent = MagicMock()
74+
agent.name = "bad_agent"
75+
agent.process_request = AsyncMock(side_effect=original)
76+
chain = ChainAgent(_options([agent]))
77+
78+
with pytest.raises(Exception) as exc_info:
79+
await chain.process_request("hi", "u", "s", [])
80+
81+
assert exc_info.value.__cause__ is original
82+
83+
84+
# ---------------------------------------------------------------------------
85+
# Edge cases
86+
# ---------------------------------------------------------------------------
87+
88+
def test_empty_agents_raises_value_error():
89+
with pytest.raises(ValueError):
90+
ChainAgent(_options([]))
91+
92+
93+
@pytest.mark.asyncio
94+
async def test_default_output_used_when_agent_returns_no_text():
95+
agent = MagicMock()
96+
agent.name = "empty"
97+
agent.process_request = AsyncMock(return_value=ConversationMessage(
98+
role=ParticipantRole.ASSISTANT.value,
99+
content=[{"no_text": True}],
100+
))
101+
chain = ChainAgent(_options([agent], default_output="fallback"))
102+
result = await chain.process_request("hi", "u", "s", [])
103+
assert result.content[0]["text"] == "fallback"

typescript/src/agents/chainAgent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class ChainAgent extends Agent {
8686
}
8787
} catch (error) {
8888
Logger.logger.error(`Error processing request with agent ${agent.name}:`, error);
89-
throw `Error processing request with agent ${agent.name}:${String(error)}`;
89+
throw new Error(`Error processing request with agent ${agent.name}:${String(error)}`);
9090
}
9191
}
9292

0 commit comments

Comments
 (0)