Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
7a9eead
chore: bump band-sdk-core to 2.3.0 for AgentFailure
AlexanderZ-Band Sep 6, 2026
daaf3e9
feat: add send_failure and to_failure_event to AgentToolsProtocol
AlexanderZ-Band Sep 6, 2026
2771d9a
feat: add DeliveryFailedError/deliver_reply misclassification guard
AlexanderZ-Band Sep 6, 2026
2aea71d
fix: surface Anthropic provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
3fb643e
fix: surface Gemini provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
c990b3f
fix: surface Google ADK provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
6a834e8
fix: surface Claude SDK provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
4437cf7
test: pre-wire send_failure into not-yet-migrated adapter test fixtures
AlexanderZ-Band Sep 6, 2026
4833a52
fix: surface Copilot SDK provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
e85b559
fix: surface LangGraph provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
6a6db89
fix: surface Letta provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
312ba96
fix: surface A2A provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
ef6f45a
fix: surface CrewAI provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
634ac13
fix: surface CrewAI Flow provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
8b42272
fix: surface Pydantic AI provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
e078156
fix: surface Parlant provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
fde418a
fix: surface Strands provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
0a38f43
fix: surface Agno provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
59cd1d4
fix: surface OpenCode provider failures via send_failure
AlexanderZ-Band Sep 6, 2026
baf5248
refactor: surface Codex provider errors as AgentFailure
AlexanderZ-Band Sep 6, 2026
4baa482
feat: surface A2A Gateway failures as structured AgentFailure metadata
AlexanderZ-Band Sep 6, 2026
55c90db
refactor: surface ACP client provider errors as AgentFailure
AlexanderZ-Band Sep 6, 2026
355ddab
fix: address code-review findings on the AgentFailure migration
AlexanderZ-Band Sep 6, 2026
43692d1
refactor: simplify error-surfacing migration per /simplify review
AlexanderZ-Band Sep 6, 2026
eb28bd8
fix: correctness bugs surfaced by /code-review max --fix
AlexanderZ-Band Sep 6, 2026
99b660a
fix: fail the turn on a terminal provider failure instead of swallowi…
AlexanderZ-Band Sep 7, 2026
ad677a0
refactor: dedup and simplify error-surfacing code per /simplify review
AlexanderZ-Band Sep 7, 2026
a5d3142
fix: fail the turn on every remaining reported-but-unraised provider …
AlexanderZ-Band Sep 7, 2026
8f7ca0f
Merge remote-tracking branch 'origin/main' into int-1385-band-sdk-pyt…
AlexanderZ-Band Sep 9, 2026
86990dc
fix(claude_sdk): stop leaking raw exception text in session-resume fa…
AlexanderZ-Band Sep 9, 2026
181edfb
fix(opencode): use the shared generic provider-failure message
AlexanderZ-Band Sep 9, 2026
83fb9fe
refactor(codex,copilot_sdk): share one send_event_safe helper
AlexanderZ-Band Sep 9, 2026
6e82782
test(letta): assert on the structured AgentFailure, not just event count
AlexanderZ-Band Sep 9, 2026
61885df
fix(crewai): restore the missing-reply log line dropped by the send_f…
AlexanderZ-Band Sep 9, 2026
dde72e8
fix(strands): log before reporting a silent turn; assert on structure…
AlexanderZ-Band Sep 9, 2026
b5a2da4
fix(pydantic_ai): log before reporting a silent turn
AlexanderZ-Band Sep 9, 2026
b21ed1d
fix(a2a): log a peer task's terminal failure before raising
AlexanderZ-Band Sep 9, 2026
913723a
test(agno): assert on the structured AgentFailure via reported_failur…
AlexanderZ-Band Sep 9, 2026
3b01fcb
test(parlant): cover the DeliveryFailedError misreporting guard
AlexanderZ-Band Sep 9, 2026
0cb8119
test: drop history-narration from two docstrings
AlexanderZ-Band Sep 9, 2026
b00e015
fix(codex): log a turn/completed structured failure before reporting it
AlexanderZ-Band Sep 9, 2026
fc87987
fix(protocols): correct send_event_safe's docstring; add contract tests
AlexanderZ-Band Sep 9, 2026
0f8ac32
refactor(a2a): dedupe state_name(state); consolidate tests onto repor…
AlexanderZ-Band Sep 9, 2026
86c2f39
test(agno): consolidate redundant error-event assertions onto reporte…
AlexanderZ-Band Sep 9, 2026
58d3a91
test(letta): consolidate redundant error-event assertions onto report…
AlexanderZ-Band Sep 9, 2026
004b4fc
test(testing): cover reported_failures()'s ordering and metadata filt…
AlexanderZ-Band Sep 9, 2026
956e0f7
fix(tests): disable crewai's own telemetry for the test session
AlexanderZ-Band Sep 9, 2026
c237ec4
fix(tests): restore HOME after launcher tests call main() in-process
AlexanderZ-Band Sep 9, 2026
80146b7
fix: address code-review findings on Codex turn-failure reporting
AlexanderZ-Band Sep 14, 2026
0b71865
fix: address /my-code-review findings across adapter failure reporting
AlexanderZ-Band Sep 14, 2026
e609f8d
fix: close two regressions from the prior fix pass
AlexanderZ-Band Sep 14, 2026
87500af
Merge remote-tracking branch 'origin/main' into int-1385-band-sdk-pyt…
AlexanderZ-Band Sep 14, 2026
ad36d9a
fix: keep ACP session on bookkeeping failure
AlexanderZ-Band Sep 14, 2026
f530ba8
fix: isolate adapter failure recovery by room
AlexanderZ-Band Sep 15, 2026
255d78f
fix: preserve adapter failure boundaries
AlexanderZ-Band Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ jobs:
from band import Agent, BandLink, AgentRuntime
from band.config import load_agent_config
from band_sdk_core import (
AgentFailure,
ClaimRegistry,
ParticipantRoster,
RetryTracker,
Expand Down Expand Up @@ -291,6 +292,13 @@ jobs:
"room_id": "room-1",
"message_id": "msg-1",
}
failure = AgentFailure("wheel-smoke", "failure")
assert failure.to_dict() == {
"provider": "wheel-smoke",
"code": None,
"message": "failure",
"detail": None,
}
print('Core imports successful')
PYEOF

Expand Down
1 change: 0 additions & 1 deletion docs/adapters/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ These `CodexAdapterConfig(...)` flags add more telemetry detail:
| `emit_turn_lifecycle_events` | `bool` | `False` | Emit enriched turn lifecycle events at turn start and completion. |
| `emit_diff_events` | `bool` | `False` | Include file diffs in event metadata, capped at 64 KB. |
| `emit_token_usage_events` | `bool` | `False` | Track and emit token usage per session. |
| `structured_errors` | `bool` | `True` | Emit structured error events instead of plain text errors. |

Enabling both `emit_turn_task_markers` and `emit_turn_lifecycle_events` produces two task events per completed turn. Pick one; lifecycle events contain richer metadata.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ classifiers = [

dependencies = [
"band-client-rest==0.0.27",
"band-sdk-core==2.2.0",
"band-sdk-core==2.3.0",
"phoenix-channels-python-client>=0.2.4",
"python-dotenv>=1.2.2",
"pydantic>=2.0",
Expand Down
44 changes: 21 additions & 23 deletions src/band/adapters/agno.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@

from agno.media import Image
from agno.tools.function import ToolResult
from band_sdk_core import AgentFailure
from typing_extensions import Unpack

from band.core.protocols import AgentToolsProtocol
from band.core.protocols import GENERIC_PROVIDER_FAILURE_MESSAGE, AgentToolsProtocol
from band.core.simple_adapter import SimpleAdapter
from band.core.tool_filter import filter_tool_schemas
from band.core.types import (
Expand Down Expand Up @@ -465,18 +466,18 @@ async def _run_agent(
:meth:`_run_streamed`), matching the other adapters' live reporting.
Otherwise it runs non-streaming, exactly as before.
"""
agent = self._agent
if agent is None:
raise RuntimeError("AgnoAdapter was used before on_started()")
session_id = self._session_id_factory(room_id)
logger.debug(
"Room %s msg %s: running Agno agent (%d input messages, session_id=%s)",
room_id,
msg_id,
len(messages),
session_id,
)
try:
agent = self._agent
if agent is None:
raise RuntimeError("AgnoAdapter was used before on_started()")
session_id = self._session_id_factory(room_id)
logger.debug(
"Room %s msg %s: running Agno agent (%d input messages, session_id=%s)",
room_id,
msg_id,
len(messages),
session_id,
)
with _bind_room_tools(tools):
if Emit.TOOL_CALLS in self.features.emit:
response = await self._run_streamed(
Expand All @@ -494,22 +495,19 @@ async def _run_agent(
# the turn as failed rather than as a silent empty reply.
if response is not None and response.status == RunStatus.error:
raise AgnoRunError(_error_summary(response.content))
except Exception:
except Exception as e:
# Keep the user-facing payload generic; the full traceback is in the
# agent log via logger.exception. Exception text can include DB
# strings, paths, and tokens that must not surface in chat.
# strings, paths, and tokens that must not surface in chat. Only
# the coarse RunStatus.error code -- never response.content -- is
# safe to attach.
logger.exception(
"Room %s msg %s: error running Agno agent", room_id, msg_id
)
try:
await tools.send_event(
content="Internal error while processing message; see agent logs.",
message_type="error",
)
except Exception:
logger.exception(
"Room %s msg %s: failed to report error event", room_id, msg_id
)
code = RunStatus.error.value if isinstance(e, AgnoRunError) else None
await tools.send_failure(
AgentFailure("agno", GENERIC_PROVIDER_FAILURE_MESSAGE, code)
)
raise

if response is None:
Expand Down
26 changes: 15 additions & 11 deletions src/band/adapters/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
import warnings
from typing import Any, ClassVar, cast

from anthropic import AsyncAnthropic
from anthropic import APIStatusError, AsyncAnthropic
from anthropic.types import Message, MessageParam, TextBlock, ToolParam, ToolUseBlock
from band_sdk_core import AgentFailure
from typing_extensions import Unpack

from band.core.exceptions import BandConfigError
from band.core.protocols import AgentToolsProtocol
from band.core.protocols import GENERIC_PROVIDER_FAILURE_MESSAGE, AgentToolsProtocol
from band.core.simple_adapter import SimpleAdapter
from band.core.types import (
Capability,
Expand Down Expand Up @@ -62,6 +63,17 @@ def _image_tool_result_content(result: dict[str, Any]) -> list[dict[str, Any]]:
]


def _to_agent_failure(e: Exception) -> AgentFailure:
Comment thread
AlexanderZ-Band marked this conversation as resolved.
"""Parse a turn-ending exception into the shared provider-failure shape.

``APIStatusError`` carries an HTTP status and response body that a plain
exception's message alone does not.
"""
if isinstance(e, APIStatusError):
return AgentFailure("anthropic", str(e), str(e.status_code), e.body)
return AgentFailure("anthropic", GENERIC_PROVIDER_FAILURE_MESSAGE)


class AnthropicAdapter(SimpleAdapter[AnthropicMessages]):
"""
Anthropic SDK adapter using SimpleAdapter pattern.
Expand Down Expand Up @@ -279,7 +291,7 @@ async def on_message(
)
except Exception as e:
logger.error("Error calling Anthropic: %s", e, exc_info=True)
await self._report_error(tools, str(e))
await tools.send_failure(_to_agent_failure(e))
raise # Re-raise so message is marked as failed

turn_usage = turn_usage + self._usage_from_response(response)
Expand Down Expand Up @@ -512,11 +524,3 @@ async def _process_tool_calls(
)

return tool_results

# --- Copied from BaseFrameworkAgent._report_error ---
async def _report_error(self, tools: AgentToolsProtocol, error: str) -> None:
"""Send error event (best effort)."""
try:
await tools.send_event(content=f"Error: {error}", message_type="error")
except Exception as e:
logger.warning("Failed to send error event: %s", e)
68 changes: 52 additions & 16 deletions src/band/adapters/claude_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@
except ImportError:
_CLAUDE_SDK_AVAILABLE = False

from band_sdk_core import AgentFailure
from typing_extensions import Unpack

from band.core.protocols import AgentToolsProtocol
from band.core.protocols import (
GENERIC_PROVIDER_FAILURE_MESSAGE,
AgentToolsProtocol,
TurnResultAlreadyReported,
)
from band.core.simple_adapter import SimpleAdapter
from band.core.types import (
Capability,
Expand Down Expand Up @@ -128,6 +133,8 @@
# same constant instead of a second, driftable number.
_CLAUDE_SDK_MAX_BUFFER_BYTES = MAX_INLINE_IMAGE_BYTES * 2

_PROVIDER = "claude_sdk"

# Approval flow types (mirrors Codex adapter patterns)
ApprovalMode = Literal["auto_accept", "auto_decline", "manual"]
ApprovalDecision = Literal["accept", "decline"]
Expand Down Expand Up @@ -611,10 +618,27 @@ async def on_message(
stored_session_id,
resume_exc,
)
client = await self._session_manager.get_or_create_session(
room_id, resume_session_id=None
)
try:
client = await self._session_manager.get_or_create_session(
room_id, resume_session_id=None
)
except Exception as fresh_exc:
logger.exception(
"Room %s: Fresh session creation also failed: %s",
room_id,
fresh_exc,
)
await tools.send_failure(
AgentFailure(_PROVIDER, GENERIC_PROVIDER_FAILURE_MESSAGE)
)
raise
else:
logger.exception(
"Room %s: Session creation failed: %s", room_id, resume_exc
)
await tools.send_failure(
AgentFailure(_PROVIDER, GENERIC_PROVIDER_FAILURE_MESSAGE)
)
raise

# Add chat_id context (Claude needs this for tool calls) -- the label
Expand Down Expand Up @@ -685,6 +709,11 @@ async def on_message(
# Process streaming response (MCP tools handle execution)
await self._process_response(client, room_id, tools)

except TurnResultAlreadyReported:
# _on_turn_complete already reported this failure via
# send_failure; propagate without reporting it a second time.
raise

except CLIConnectionError as e:
# CLI process is dead — evict the cached session so the next
# message creates a fresh one instead of reusing the corpse.
Expand All @@ -695,12 +724,16 @@ async def on_message(
)
await self._invalidate_session(room_id)

await self._report_error(tools, str(e))
await tools.send_failure(
AgentFailure(_PROVIDER, GENERIC_PROVIDER_FAILURE_MESSAGE)
)
raise

except Exception as e:
logger.exception("Error processing message: %s", e)
await self._report_error(tools, str(e))
await tools.send_failure(
AgentFailure(_PROVIDER, GENERIC_PROVIDER_FAILURE_MESSAGE)
)
raise

logger.debug("Message %s processed successfully", msg.id)
Expand Down Expand Up @@ -933,11 +966,22 @@ async def _on_turn_complete(
# outright) doesn't linger and grow this room's entry unbounded.
notified = self._notified_declines.pop(room_id, None)
if sdk_message.is_error:
await self._report_error(tools, self._result_error_detail(sdk_message))
code = (
str(sdk_message.api_error_status)
if sdk_message.api_error_status is not None
else None
)
detail = self._result_error_detail(sdk_message)
await tools.send_failure(
AgentFailure(_PROVIDER, detail, code, sdk_message.errors)
)
raise TurnResultAlreadyReported(detail)
elif not replied_this_turn and not self._declined_the_reply(
sdk_message.permission_denials, notified
):
await self._report_error(tools, missing_reply_error("Claude SDK"))
detail = missing_reply_error("Claude SDK")
await tools.send_failure(AgentFailure(_PROVIDER, detail))
raise TurnResultAlreadyReported(detail)

def _declined_the_reply(
self, permission_denials: list[Any] | None, notified: set[str] | None
Expand Down Expand Up @@ -1127,14 +1171,6 @@ async def on_cleanup(self, room_id: str) -> None:
self._pending_tool_names.pop(room_id, None)
logger.debug("Room %s: Cleaned up Claude SDK session", room_id)

# --- Copied from BaseFrameworkAgent._report_error ---
async def _report_error(self, tools: AgentToolsProtocol, error: str) -> None:
"""Send error event (best effort)."""
try:
await tools.send_event(content=f"Error: {error}", message_type="error")
except Exception:
logger.debug("Failed to send error event", exc_info=True)

async def cleanup_all(self) -> None:
"""Cleanup all sessions (call on stop)."""
# Decline all pending approvals across rooms
Expand Down
Loading
Loading