{"slug": "delegation-masking-why-your-langchain-callbacks-lie-about-sub-agent-failures", "title": "Delegation Masking: Why Your LangChain Callbacks Lie About Sub-Agent Failures", "summary": "A developer discovered that LangChain's callback system masks sub-agent failures, a phenomenon called 'delegation masking.' When Agent A delegates to Agent B and Agent B fails silently, Agent A's callback layer logs success because it only sees the delegation function's return value, not the sub-agent's internal state. The developer proposes output validation and cross-agent execution correlation to catch these hidden failures.", "body_md": "You delegate a task from Agent A to Agent B in LangChain. Agent B fails. Agent A's callback chain fires 'success' anyway.\n\nThis is the observability blind spot most builders miss in agentic workflows: **delegation masking**. A sub-agent fails silently, but the parent agent's callback layer never knows because it only watches the delegation *call itself*, not what the delegated agent actually did.\n\nLet's walk the mechanism.\n\nWhen you wire up agent-to-agent delegation in LangChain, you're typically using the `tool`\n\ndecorator to wrap a sub-agent invocation:\n\n``` php\n@tool\ndef delegate_to_classification_agent(task: str) -> str:\n    \"\"\"Delegate classification to a specialized sub-agent.\"\"\"\n    result = classification_agent.invoke({\"input\": task})\n    return result.get(\"output\", \"\")\n```\n\nThe parent agent treats this as just another tool. It calls it, gets a result, moves on. The parent agent's callback chain (the layer that logs success/failure, fires alerts, measures latency) only sees the **function return value**, not the internal state of the sub-agent.\n\nHere's what can happen:\n\n**Agent B (sub-agent) fails to produce valid output.** Its internal chain breaks, maybe a tool call failed, or output parsing broke, or the LLM went silent. Agent B's callback chain logs the failure.\n\n**But the delegation function still returns something.** Maybe it returns an empty string, a cached fallback, or a generic error message. It doesn't raise an exception, it just returns.\n\n**Agent A's callback layer sees HTTP 200.** The delegation tool returned *something*, so the callback fires `on_tool_end`\n\nwith `status: \"success\"`\n\n. The parent agent logs success, moves on.\n\n**The actual failure is buried two layers deep.** Agent A's monitoring sees \"delegation succeeded.\" Only if someone digs into Agent B's logs does the failure surface.\n\nThis is a **callback visibility boundary**. The parent agent's instrumentation layer is one level too high to catch delegation failures.\n\nStandard token-counting observability misses this because both agents might report partial token usage (Agent B started, burned some tokens, then failed). A success callback fired, so metrics look nominal.\n\nWhat actually catches delegation failures:\n\n**1. Output validation at the delegation boundary**\n\nTrack what the delegation function *actually returned*:\n\n``` php\n@tool\ndef delegate_to_agent(task: str) -> str:\n    \"\"\"Delegate with observability.\"\"\"\n    result = sub_agent.invoke({\"input\": task})\n    output = result.get(\"output\", \"\")\n\n    # Signal: validate the output exists and is non-empty\n    if not output or output.strip() == \"\":\n        # This is a silent failure, the sub-agent ran but produced nothing\n        log_event(\"delegation_produced_empty_output\", {\n            \"delegated_agent\": \"classification_agent\",\n            \"input\": task,\n            \"sub_agent_status\": result.get(\"status\"),\n            \"token_count\": result.get(\"usage\", {}).get(\"output_tokens\", 0)\n        })\n\n    return output\n```\n\nThe signal is: **a delegation that returned empty or unchanged input**. The parent agent's callback sees \"success,\" but observability knows something went wrong.\n\n**2. Cross-agent execution correlation**\n\nWhen Agent A calls Agent B, log a **correlation ID** that links both agents' execution traces:\n\n``` python\nimport uuid\n\ncorrelation_id = str(uuid.uuid4())\n\n# In Agent A's tool:\nresult = sub_agent.invoke(\n    {\"input\": task, \"correlation_id\": correlation_id},\n    metadata={\"correlation_id\": correlation_id}\n)\n\n# In sub-agent's callback handler:\ndef on_tool_end(self, output: str, **kwargs):\n    corr_id = kwargs.get(\"metadata\", {}).get(\"correlation_id\")\n    if corr_id:\n        log_event(\"agent_execution\", {\n            \"correlation_id\": corr_id,\n            \"agent\": \"sub_agent\",\n            \"output_tokens\": ...,\n            \"status\": \"success\" or \"failed\"\n        })\n```\n\nNow you can query: *\"What sub-agent executions have a correlation_id but show empty output or error status?\"* That's where delegation failures hide.\n\n**3. Aggregate success rate per agent pair**\n\nTrack success rates at the **delegation edge**, not just per-agent:\n\n```\ndelegation_success_rate = (\n    executions where parent_agent=A and delegated_agent=B and output_validation_passed\n) / (total executions where parent_agent=A and delegated_agent=B)\n```\n\nIf Agent A to Agent B delegation shows 95% success in parent logs but only 70% pass output validation, you've found the callback masking.\n\nLangChain's callback layer is designed to instrument the **calling agent's perspective**, not the called agent's internal state. That's by design, clean separation of concerns. But it means delegation failures are invisible until they propagate (or don't).\n\nMost frameworks have the same boundary. CrewAI's task delegation, AutoGen's sub-agent calls, they all fire success callbacks when the *call itself* succeeds, regardless of what the called agent actually did.\n\nYou need **one layer deeper**:\n\n**Instrument sub-agents independently.** Log their execution status, output validity, token usage. Don't rely on the parent agent's callback to know what happened.\n\n**Validate delegation outputs.** Don't trust that a delegation function returning a value means the sub-agent actually succeeded. Check the output.\n\n**Correlate across agents.** Link parent and child agent executions so failures propagate upward in your observability dashboard, not just downward in logs.\n\nThe callback chain is essential, but it's not enough. **Delegation visibility requires you to see both sides of the boundary.**\n\n**The takeaway:** When agents delegate to other agents, callback success is not the same as execution success. Standard monitoring stays silent because the parent agent's callbacks only see the delegation call, not the delegated agent's actual work. Catch it by validating outputs, correlating executions across agents, and measuring success rates at the delegation edge.", "url": "https://wpnews.pro/news/delegation-masking-why-your-langchain-callbacks-lie-about-sub-agent-failures", "canonical_source": "https://dev.to/opsveritas/delegation-masking-why-your-langchain-callbacks-lie-about-sub-agent-failures-4l02", "published_at": "2026-07-28 08:38:58+00:00", "updated_at": "2026-07-28 09:05:04.718624+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "developer-tools", "large-language-models"], "entities": ["LangChain"], "alternates": {"html": "https://wpnews.pro/news/delegation-masking-why-your-langchain-callbacks-lie-about-sub-agent-failures", "markdown": "https://wpnews.pro/news/delegation-masking-why-your-langchain-callbacks-lie-about-sub-agent-failures.md", "text": "https://wpnews.pro/news/delegation-masking-why-your-langchain-callbacks-lie-about-sub-agent-failures.txt", "jsonld": "https://wpnews.pro/news/delegation-masking-why-your-langchain-callbacks-lie-about-sub-agent-failures.jsonld"}}