When you need per-call control, or when auto_patch() is not suitable:
import langsightfrom google import genails = langsight.init() # reads LANGSIGHT_URL, LANGSIGHT_API_KEY, LANGSIGHT_PROJECT_IDasync with langsight.session(agent_name="analyst") as session_id: # agent_name and session_id inherited from context — no need to pass them client = ls.wrap_llm(genai.Client()) # Async — your existing code, unchanged response = await client.aio.models.generate_content( model="gemini-2.5-flash", contents=[...], config=config, ) # Auto-traced: LLM generation span + llm_intent spans per tool decision
The wrapper intercepts client.models.generate_content() (sync) and client.aio.models.generate_content() (async). All other attributes and methods forward to the original client unchanged.
agent_name, session_id, and trace_id are optional when called inside langsight.session() — the context is inherited automatically. Only pass them explicitly when you need to override the context values.
The recommended pattern for orchestrator → analyst → procurement workflows:
Every agent needs input= and sess.set_output() — not just the orchestrator. Without them, the Input/Output panel in the session graph will be empty for that agent’s node, even though the session itself is tracked correctly.
import langsightfrom google import genailangsight.auto_patch() # patches all Gemini clients + MCP sessions globallyasync def orchestrator(question: str): async with langsight.session(agent_name="orchestrator", input=question) as sess: client = genai.Client() response = await client.aio.models.generate_content( model="gemini-2.5-flash", contents=[question], config=config, ) sess.set_output(response.text) # ← captures final answer in dashboard return response.textasync def analyst(question: str, session_id: str): # input= and sess.set_output() required for Input/Output to appear in the # session graph right panel for this agent's node async with langsight.session(agent_name="analyst", session_id=session_id, input=question) as sess: client = genai.Client() data = await catalog_mcp_session.call_tool("search", {"q": question}) result = process(data) sess.set_output(result) # ← without this, Output is blank in dashboard return resultasync def procurement(findings: str, session_id: str): async with langsight.session(agent_name="procurement", session_id=session_id, input=findings) as sess: client = genai.Client() plan = await generate_plan(findings) sess.set_output(plan) return plan
Rule of thumb: every session() block that represents a meaningful agent turn should have:
input= — what the agent received (user question, upstream findings, etc.)
sess.set_output(result) — what the agent produced
This is what populates the Input / Output panel in the session detail view for each node.
Sharing a single session_id across sub-agents
Sub-agents that are awaited directly (not spawned via asyncio.create_task()) inherit the parent session context automatically. But for Input/Output to appear per-agent, pass session_id explicitly and use input= + sess.set_output():
async with langsight.session(agent_name="orchestrator", input=question) as sess: session_id = str(sess) result = await analyst(question, session_id=session_id) sess.set_output(result)async def analyst(question: str, session_id: str): async with langsight.session(agent_name="analyst", session_id=session_id, input=question) as sess: answer = await do_work(question) sess.set_output(answer) return answer
If you omit session_id=session_id, each sub-agent creates its own independent session and the spans will not appear in the parent session’s graph.
wrap_llm() and wrap() work independently. Use both when your agent calls Gemini directly and also uses MCP servers. Inside a session() block, agent_name and session_id are inherited automatically — no threading needed:
async with langsight.session(agent_name="analyst") as session_id: # Trace LLM calls — agent_name and session_id inherited from context client = ls.wrap_llm(genai.Client()) # Trace MCP tool calls — same traced_mcp = ls.wrap(mcp_session, server_name="postgres-mcp")