Troubleshooting¶
What fails, why, and how to recover. The SDK ships no logging module usage and no custom exception classes — failures surface as stdlib exceptions (ImportError, ValueError, IndexError, NotImplementedError) or propagate raw from the Anthropic SDK. Observability is via print() at failure points. There is no verbose / DEBUG flag.
Anthropic API failures¶
Symptom: anthropic.RateLimitError / APIStatusError / APIConnectionError propagates out of await swarm.process(task).
Root cause: Agent.think() calls self.client.messages.create(**kwargs) with no retry, no backoff, no timeout. Per-agent calls run concurrently via asyncio.gather(*coros, return_exceptions=True).
Recovery: depends on how many agents failed.
- One agent fails, others succeed — the exception is captured by
gather(return_exceptions=True), logged viaprint(f"Agent {name} failed: {result}"), and the failed agent is skipped. The swarm survives. Verified bytest_failed_agent_is_skipped_not_raised. - All agents fail —
responses == [], consensus returns an emptyConsensusResult(final_answer="", agent_count=0, agreement=0.0). No exception raised. Detect with: - Invalid key —
AsyncAnthropicraises on first call; same gather-skip path applies. There is no early auth check.
Structured output parse failure¶
Symptom: Agent answer comes back as raw text; confidence defaults to 0.5.
Root cause: Model didn't emit <answer>…</answer> tags, or truncated mid-tag.
Recovery: parse_structured_output() is tolerant by design — three fallbacks, never raises:
- No tags → raw text becomes
answer. - Open-but-unclosed
<answer>(small model + tightmax_tokens) → takes everything after the opening tag. - Missing
<confidence>/<reasoning>→ defaults to0.5/""; confidence clamped to[0.0, 1.0].
Verified by test_parse_tolerates_truncated_answer_tag.
If you're seeing this often, raise max_tokens on the offending AgentConfig (default is 4096, but tight benchmarks often use 256).
Signature / receipt tamper detection¶
Symptom: SignatureBackend.verify_receipt(receipt) returns False.
Root cause: verification returns False (never raises) for any of:
- Unsigned receipt
- Payload hash mismatch — re-hashing the payload detects post-signing tamper
- Wrong public key
InvalidSignaturefrom the crypto library- Malformed hex (broad
except Exception: return False)
Verified by test_payload_tamper_invalidates_signature, test_signature_tamper_invalidates, test_wrong_public_key_rejects, test_payload_hash_mismatch_rejects — mirrored for ML-DSA in test_signing_pq.py.
Recovery: treat any False as untrusted. Never display the answer as consensus. Re-issue or investigate. The signature doesn't tell you what changed, only that it did — diff the payload against a known-good copy.
Optional-dependency ImportError¶
Three lazy imports raise clear install hints:
| Missing | Error | Install |
|---|---|---|
coincurve (Nostr transport) |
ImportError("Nostr transport requires coincurve (BIP-340 Schnorr). Install with: pip install claudeway[nostr]") |
pip install claudeway[nostr] |
dilithium-py (post-quantum backend) |
ImportError("ML-DSA-65 backend requires dilithium-py… pip install claudeway[pq]") |
pip install claudeway[pq] |
mcp (MCP client) |
ImportError("MCP support requires the 'mcp' package… pip install claudeway[mcp]") |
pip install claudeway[mcp] |
Nostr relay rejection¶
Symptom: Relay returns ["OK", id, false, "reason"] or doesn't respond.
Root cause: event rejected (rate-limited, banned, malformed) or relay offline.
Recovery: the SDK only builds the event via to_nostr_event — it does not publish. The reference publisher at examples/buzz_wire_publish.py shows the pattern: try each relay, handle TimeoutError (5 s), print [REJECT reason] / [NORESP] / [FAIL], return the list of accepting relays. Claim success only when ≥1 relay accepted.
Tamper detection at the relay boundary uses nak verify (the reference Nostr CLI). Without nak, content/sig tamper is caught by recomputing the event id — see test_nostr_event_tamper_detected_without_nak.
Async→sync deadlock (CrewAI adapter)¶
Symptom: CrewAI tool call hangs forever.
Root cause: naive run_coroutine_threadsafe + fut.result() self-deadlocks when the caller is on the loop's own thread (pytest-asyncio, CrewAI's async runtime) — fut.result() blocks the thread that would run the coro.
Recovery: _run_sync() in adapters/crewai.py always spawns a fresh daemon thread with its own event loop and asyncio.run(coro), then t.join(). Worker exceptions are re-raised in the caller.
Never replace this with loop reuse. If you're writing a custom adapter that wraps async Claudeway calls into a sync surface, copy the _run_sync pattern verbatim.
Empty / invalid input¶
- Empty prompt to MAF executor →
ValueError("consensus executor received an empty prompt…"). - Empty prompt to LangGraph node →
ValueError("consensus node could not find a prompt…"). - Bad swarm_id to runtime →
ValueError(f"Swarm {swarm_id} not found"). - Out-of-range Merkle leaf →
IndexError(f"leaf_index {i} out of range…").
Defensive patterns to copy¶
- Cost-guarded early exit.
Debate.resolve()skips the 2nd round whenfirst_agreement >= agreement_threshold. Verified bytest_debate_early_exits_when_agreement_is_high. - Verify before trusting. Always
verify_receipt()before displaying; alwaysverify_inclusion()against a published root before claiming a receipt was logged. - Verify before publishing.
buzz_wire_publish.pycollectsacceptedrelays before claiming success. - Observer isolation.
on_eventcallbacks run intry/except; a buggy observer can never change consensus outcome. Verified bytest_on_event_observer_error_does_not_kill_agent. (Note: this also means observer bugs are silent — log aggressively inside your callback.)
Known footguns¶
- Agreement scoring is surface-form only. Three agents with substantively-identical but differently-worded prose score ~33% disagreement, falsely triggering a Debate round. Tracked as
TODO(semantic-agreement)inconsensus.py. - No retry, no timeout, no circuit breaker on Anthropic calls.
- Coordinator dependency failures are silent. Unresolvable sub-task dependencies are marked
failedwith{"error": "unresolvable dependency"}rather than hanging. Checksub_results[id]for"error"keys aftercoordinate(). max_task_tokensdeclared but not enforced. The field exists onSwarmConfig; no code path reads it.topologyis advisory only. No code branches on it.