Visibility¶
In multi-agent pipelines, not every agent’s output should be shown to the end-user. A 5-agent pipeline that streams all 5 responses creates a confusing, noisy experience. The visibility system lets you control which agents produce user-facing output and which remain internal.
The Real Problem¶
Consider a customer support pipeline:
classifier >> router >> specialist >> responder >> auditor
Without visibility control, the user sees:
“I classified this as a billing issue” (classifier – internal reasoning)
“Routing to billing specialist” (router – infrastructure noise)
“The customer’s account shows…” (specialist – internal analysis)
“Dear customer, we’ve resolved…” (responder – this is what the user should see)
“Audit: compliant” (auditor – internal)
Only response #4 matters. The rest is internal noise that confuses users and leaks implementation details.
Topology-Inferred Visibility¶
infer_visibility walks the pipeline IR and classifies each agent automatically. Terminal agents (those with no successor) are marked "user", intermediate agents are marked "internal":
from adk_fluent import Agent
from adk_fluent._visibility import infer_visibility
pipeline = (
Agent("classifier").model("m").instruct("Classify.")
>> Agent("handler").model("m").instruct("Handle.")
)
vis = infer_visibility(pipeline.to_ir())
# {"classifier": "internal", "handler": "user"}
Policies Reference¶
Policy |
Behavior |
Use case |
|---|---|---|
|
Terminal agents = user-facing, intermediate = internal |
Production: clean user experience |
|
All agents user-facing |
Debugging: see every agent’s output |
|
All events pass through with metadata tags |
Monitoring: log everything, decide later |
Pipeline-Level Policies¶
Set a policy on the entire pipeline with a single method call:
pipeline = (
Agent("a").model("m").instruct("Step 1.")
>> Agent("b").model("m").instruct("Step 2.")
)
pipeline.transparent() # Debug: see everything
pipeline.filtered() # Production: only terminal output
pipeline.annotated() # Monitoring: tag events with visibility metadata
Per-Agent Overrides¶
Override the topology-inferred classification on individual agents:
# Force an intermediate agent to be user-facing (e.g., progress updates)
agent = Agent("progress").model("m").instruct("Report progress.").show()
# Force a terminal agent to be internal (e.g., cleanup step)
agent = Agent("cleaner").model("m").instruct("Clean up.").hide()
Overrides take precedence over both the inferred topology and the pipeline-level policy.
VisibilityPlugin¶
VisibilityPlugin is an ADK BasePlugin that runs on event callbacks. It reads the inferred visibility map and either annotates or filters events:
annotate mode – all events pass through with
adk_fluent.visibilityandadk_fluent.is_user_facingmetadata attachedfilter mode – internal events have their content stripped so they never reach the user
Error events always pass through regardless of mode.
from adk_fluent._visibility import infer_visibility, VisibilityPlugin
vis_map = infer_visibility(pipeline.to_ir())
plugin = VisibilityPlugin(vis_map, mode="filter")
Interplay with Other Modules¶
Visibility + Transfer Control¶
Visibility and transfer control work on different axes:
Visibility controls what the user sees
Transfer control controls what agents can do (
.isolate(),.stay(),.no_peers())
They compose independently:
from adk_fluent import Agent
# This agent is hidden from the user AND can't transfer to other agents
internal_validator = (
Agent("validator")
.model("gemini-2.5-flash")
.instruct("Validate the output.")
.hide() # User doesn't see validation reasoning
.isolate() # Validator can't hand off to other agents
)
See Transfer Control.
Visibility + Context Engineering¶
Context engineering controls what the LLM sees. Visibility controls what the user sees. They’re complementary:
from adk_fluent import Agent, C
# Classifier: hidden from user, sees no conversation history
classifier = (
Agent("classifier")
.model("gemini-2.5-flash")
.instruct("Classify the intent.")
.context(C.none()) # LLM doesn't see history
.hide() # User doesn't see classification
)
A common pattern: intermediate agents should both .hide() (from user) and use C.none() or C.from_state() (from LLM). This prevents noise in both directions.
See Context Engineering.
Visibility + Streaming¶
When using .stream(), visibility determines which agents’ chunks reach the stream:
filtered: only terminal agents’ chunks appeartransparent: all agents’ chunks appear (useful for debugging)annotate: all chunks appear with metadata tags
# Only the final agent's output streams to the user
pipeline = (
Agent("analyzer").instruct("Analyze.").hide()
>> Agent("writer").instruct("Write.").show()
)
async for chunk in pipeline.stream("Explain quantum computing"):
print(chunk, end="") # Only writer's output
See Execution.
Visibility + Middleware¶
Middleware sees all agents regardless of visibility. M.log() captures events from hidden agents too – visibility only affects user-facing output:
from adk_fluent._middleware import M
pipeline = (
Agent("hidden").instruct("Internal.").hide()
>> Agent("visible").instruct("User-facing.")
).middleware(M.log())
# M.log() captures both hidden and visible agents' events
See Middleware.
Complete Example¶
A 3-agent draft-review-edit pipeline where only the editor’s output is shown to the user:
from adk_fluent import Agent
from adk_fluent._visibility import infer_visibility
pipeline = (
Agent("drafter")
.model("gemini-2.5-flash")
.instruct("Write a first draft.")
>> Agent("reviewer")
.model("gemini-2.5-flash")
.instruct("Review the draft and suggest improvements.")
>> Agent("editor")
.model("gemini-2.5-flash")
.instruct("Apply the review feedback and produce the final text.")
)
vis = infer_visibility(pipeline.to_ir())
# {"drafter": "internal", "reviewer": "internal", "editor": "user"}
# Production mode -- only the editor's output reaches the user
pipeline.filtered()
# Debug mode -- see all three agents' output
# pipeline.transparent()
Best Practices¶
Default to
filteredin production. Users should only see the final, polished outputUse
transparentduring development. Seeing every agent’s reasoning helps debug pipeline logicUse
.show()for progress agents. If an intermediate agent reports progress (“Searching 3 sources…”), make it user-facing explicitlyUse
.hide()for cleanup/audit agents. Terminal agents that perform validation or logging shouldn’t be user-facingPair
.hide()withC.none()for utility agents. If an agent is hidden from the user, it probably shouldn’t see conversation history either
See also
Transfer Control –
.isolate(),.stay(),.no_peers()for agent handoff controlContext Engineering – controlling what the LLM sees
Execution –
.stream()and how visibility affects streamingMiddleware – middleware sees all agents regardless of visibility