diff --git a/samples/csharp/hosted-agents/bring-your-own/invocations/HelloWorld/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/invocations/HelloWorld/agent.yaml index 31bad1f59..d9e00d020 100644 --- a/samples/csharp/hosted-agents/bring-your-own/invocations/HelloWorld/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/invocations/HelloWorld/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: hello-world-dotnet-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/csharp/hosted-agents/bring-your-own/invocations/human-in-the-loop/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/invocations/human-in-the-loop/agent.yaml index 69fd05eed..004bd8040 100644 --- a/samples/csharp/hosted-agents/bring-your-own/invocations/human-in-the-loop/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/invocations/human-in-the-loop/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: human-in-the-loop-dotnet-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/csharp/hosted-agents/bring-your-own/invocations/notetaking-agent/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/invocations/notetaking-agent/agent.yaml index 19e52c2b7..e7f63060c 100644 --- a/samples/csharp/hosted-agents/bring-your-own/invocations/notetaking-agent/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/invocations/notetaking-agent/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: notetaking-agent-dotnet-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/csharp/hosted-agents/bring-your-own/invocations_ws/HelloWorld/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/invocations_ws/HelloWorld/agent.yaml index f88c3aa34..96853b572 100644 --- a/samples/csharp/hosted-agents/bring-your-own/invocations_ws/HelloWorld/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/invocations_ws/HelloWorld/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: hello-world-dotnet-invocations-ws protocols: - protocol: invocations_ws - version: 1.0.0 + version: 2.0.0 resources: cpu: "1" memory: 2Gi diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/HelloWorld/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/responses/HelloWorld/agent.yaml index 85a975528..beee42c28 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/HelloWorld/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/responses/HelloWorld/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: hello-world-dotnet-responses protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/background-agent/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/responses/background-agent/agent.yaml index 93329464c..76b8242ca 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/background-agent/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/responses/background-agent/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: background-agent-dotnet-responses protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/browser-automation/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/responses/browser-automation/agent.yaml index 1e8cc7afe..508b26589 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/browser-automation/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/responses/browser-automation/agent.yaml @@ -13,7 +13,7 @@ metadata: - .NET protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/env-vars-agent/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/responses/env-vars-agent/agent.yaml index 8af0d6d9d..58ed7c674 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/env-vars-agent/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/responses/env-vars-agent/agent.yaml @@ -17,7 +17,7 @@ metadata: - .NET protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/notetaking-agent/agent.yaml b/samples/csharp/hosted-agents/bring-your-own/responses/notetaking-agent/agent.yaml index e8fc2e2b5..54d344f65 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/notetaking-agent/agent.yaml +++ b/samples/csharp/hosted-agents/bring-your-own/responses/notetaking-agent/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: notetaking-agent-dotnet-responses protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/ag-ui/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/ag-ui/agent.yaml index 9957b514d..2c3784e34 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/ag-ui/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/ag-ui/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: ag-ui-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/claude-agent-sdk/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/claude-agent-sdk/agent.yaml index 63a0a3756..8306fd245 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/claude-agent-sdk/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/claude-agent-sdk/agent.yaml @@ -13,7 +13,7 @@ metadata: - Bring Your Own protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/diagnostic-agent/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/diagnostic-agent/agent.yaml index 652dbd6b3..3f30f4efb 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/diagnostic-agent/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/diagnostic-agent/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: diagnostic-agent-python-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/event-grid-trigger/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/event-grid-trigger/agent.yaml index 174aea166..67a8f75c1 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/event-grid-trigger/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/event-grid-trigger/agent.yaml @@ -5,7 +5,7 @@ description: | BYO Invocations agent that Event Grid POSTs to directly using the system topic's system-assigned managed identity with audience https://ai.azure.com. Receives Storage BlobCreated events, downloads the blob with the per-agent Microsoft Entra identity, and writes a summary back to a sibling container. protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/github-copilot/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/github-copilot/agent.yaml index b873734f6..336bca5ce 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/github-copilot/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/github-copilot/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: github-copilot-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: '0.25' memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/hello-world/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/hello-world/agent.yaml index 4ec537837..0a9fc6949 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/hello-world/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/hello-world/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: hello-world-python-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/human-in-the-loop/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/human-in-the-loop/agent.yaml index bf393246d..8ac889a69 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/human-in-the-loop/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/human-in-the-loop/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: human-in-the-loop-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/langgraph-chat/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/langgraph-chat/agent.yaml index 840f0aff5..dc07829b2 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/langgraph-chat/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/langgraph-chat/agent.yaml @@ -14,7 +14,7 @@ metadata: - Python protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/notetaking-agent/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/notetaking-agent/agent.yaml index 1e04ebfa2..ab660d0f8 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/notetaking-agent/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/notetaking-agent/agent.yaml @@ -2,7 +2,7 @@ kind: hosted name: notetaking-agent-invocations-python protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/toolbox/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations/toolbox/agent.yaml index f2756ccaf..d9d90f815 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/toolbox/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations/toolbox/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: toolbox-python-invocations protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations/toolbox/main.py b/samples/python/hosted-agents/bring-your-own/invocations/toolbox/main.py index f3ca87b21..d1ef9537a 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/toolbox/main.py +++ b/samples/python/hosted-agents/bring-your-own/invocations/toolbox/main.py @@ -118,6 +118,12 @@ _TOOLBOX_FEATURES = os.getenv( "FOUNDRY_AGENT_TOOLBOX_FEATURES", "Toolboxes=V1Preview") +# Platform-injected per-request call identifier (container protocol v2.0.0). +# Extracted from the inbound invocation request and forwarded verbatim on every +# egress call to the Foundry toolbox MCP proxy so the platform can correlate the +# downstream tool calls with the originating invocation. Never parse this value. +_FOUNDRY_CALL_ID_HEADER = "x-agent-foundry-call-id" + _credential = DefaultAzureCredential() _project_client = AIProjectClient(endpoint=_endpoint, credential=_credential) _responses_client = _project_client.get_openai_client().responses @@ -142,13 +148,16 @@ def __init__(self, endpoint: str, token_provider): self._session_id: str | None = None self._req_id = 0 - def _headers(self) -> dict: + def _headers(self, call_id: str | None = None) -> dict: h = { "Content-Type": "application/json", "Authorization": f"Bearer {self._get_token()}", } if _TOOLBOX_FEATURES: h["Foundry-Features"] = _TOOLBOX_FEATURES + # Forward the per-request call ID extracted from the inbound invocation. + if call_id: + h[_FOUNDRY_CALL_ID_HEADER] = call_id if self._session_id: h["mcp-session-id"] = self._session_id return h @@ -157,12 +166,12 @@ def _next_id(self) -> int: self._req_id += 1 return self._req_id - def initialize(self) -> str: + def initialize(self, call_id: str | None = None) -> str: """Send MCP initialize + initialized notification.""" with httpx.Client(timeout=60) as client: resp = client.post( self.endpoint, - headers=self._headers(), + headers=self._headers(call_id), json={ "jsonrpc": "2.0", "id": self._next_id(), @@ -181,29 +190,29 @@ def initialize(self) -> str: # Send initialized notification client.post( self.endpoint, - headers=self._headers(), + headers=self._headers(call_id), json={"jsonrpc": "2.0", "method": "notifications/initialized"}, ) return data.get("result", {}).get("serverInfo", {}).get("name", "unknown") - def list_tools(self) -> list[dict]: + def list_tools(self, call_id: str | None = None) -> list[dict]: """Call tools/list and return tool definitions.""" with httpx.Client(timeout=60) as client: resp = client.post( self.endpoint, - headers=self._headers(), + headers=self._headers(call_id), json={"jsonrpc": "2.0", "id": self._next_id( ), "method": "tools/list", "params": {}}, ) resp.raise_for_status() return resp.json().get("result", {}).get("tools", []) - def call_tool(self, name: str, arguments: dict) -> str: + def call_tool(self, name: str, arguments: dict, call_id: str | None = None) -> str: """Call a tool and return the text result.""" with httpx.Client(timeout=120) as client: resp = client.post( self.endpoint, - headers=self._headers(), + headers=self._headers(call_id), json={ "jsonrpc": "2.0", "id": self._next_id(), @@ -235,13 +244,13 @@ def call_tool(self, name: str, arguments: dict) -> str: _tools_initialized = False -def _ensure_tools(): +def _ensure_tools(call_id: str | None = None): global _mcp_client, _tool_definitions, _tools_initialized if _tools_initialized: return _mcp_client = _McpToolboxClient(TOOLBOX_ENDPOINT, _token_provider) - server_name = _mcp_client.initialize() - mcp_tools = _mcp_client.list_tools() + server_name = _mcp_client.initialize(call_id) + mcp_tools = _mcp_client.list_tools(call_id) logger.info("Toolbox '%s' connected: %d tool(s) discovered", server_name, len(mcp_tools)) for t in mcp_tools: @@ -265,9 +274,9 @@ def _ensure_tools(): _MAX_TOOL_ROUNDS = 10 -def _call_model(input_items: list[dict]) -> object: +def _call_model(input_items: list[dict], call_id: str | None = None) -> object: """Call the model with tool definitions and return the response.""" - _ensure_tools() + _ensure_tools(call_id) return _responses_client.create( model=_model, instructions=_SYSTEM_PROMPT, @@ -277,7 +286,7 @@ def _call_model(input_items: list[dict]) -> object: ) -def _run_agent_loop(input_items: list[dict]) -> str: +def _run_agent_loop(input_items: list[dict], call_id: str | None = None) -> str: """Execute the agentic tool-calling loop synchronously. Calls the model, checks for tool calls, executes them, feeds results @@ -285,7 +294,7 @@ def _run_agent_loop(input_items: list[dict]) -> str: the max rounds limit. """ for _ in range(_MAX_TOOL_ROUNDS): - response = _call_model(input_items) + response = _call_model(input_items, call_id) # Check if the model wants to call tools tool_calls = [ @@ -302,7 +311,7 @@ def _run_agent_loop(input_items: list[dict]) -> str: try: arguments = json.loads(tc.arguments) if isinstance( tc.arguments, str) else tc.arguments - result_text = _mcp_client.call_tool(tc.name, arguments) + result_text = _mcp_client.call_tool(tc.name, arguments, call_id) logger.info("Tool '%s' returned %d chars", tc.name, len(result_text)) except Exception as e: @@ -326,10 +335,10 @@ def _run_agent_loop(input_items: list[dict]) -> str: return "(Reached maximum tool call rounds)" -async def _stream_agent_reply(input_items: list[dict]): +async def _stream_agent_reply(input_items: list[dict], call_id: str | None = None): """Run the agent loop in a thread and yield the result as SSE events.""" loop = asyncio.get_running_loop() - result = await loop.run_in_executor(None, _run_agent_loop, input_items) + result = await loop.run_in_executor(None, _run_agent_loop, input_items, call_id) yield f"data: {json.dumps({'type': 'token', 'content': result})}\n\n" @@ -374,8 +383,12 @@ async def handle_invoke(request: Request): session_id = request.state.session_id invocation_id = request.state.invocation_id - logger.info("Processing invocation %s (session %s)", - invocation_id, session_id) + # Extract the per-request call ID (container protocol v2.0.0) directly from + # the inbound request headers so it can be forwarded on toolbox egress calls. + call_id = request.headers.get(_FOUNDRY_CALL_ID_HEADER) + + logger.info("Processing invocation %s (session %s, call_id %s)", + invocation_id, session_id, call_id) # Retrieve or create conversation history for this session. history = _sessions.setdefault(session_id, []) @@ -385,7 +398,7 @@ async def handle_invoke(request: Request): async def event_generator(): full_reply = "" try: - async for delta in _stream_agent_reply(input_items): + async for delta in _stream_agent_reply(input_items, call_id): # Parse the SSE data to extract the content for history try: event_data = json.loads(delta.split( diff --git a/samples/python/hosted-agents/bring-your-own/invocations_ws/duplex-live-agent/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations_ws/duplex-live-agent/agent.yaml index b4f6b7a9c..0466adfdb 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations_ws/duplex-live-agent/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations_ws/duplex-live-agent/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: duplex-live-agent protocols: - protocol: invocations_ws - version: 1.0.0 + version: 2.0.0 resources: cpu: "1" memory: 1Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations_ws/hello-world/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations_ws/hello-world/agent.yaml index 1309edc5f..06f577749 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations_ws/hello-world/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations_ws/hello-world/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: hello-world protocols: - protocol: invocations_ws - version: 1.0.0 + version: 2.0.0 resources: cpu: "1" memory: 2Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations_ws/livekit-server/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations_ws/livekit-server/agent.yaml index d08bcf855..1a941bce0 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations_ws/livekit-server/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations_ws/livekit-server/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: livekit-server protocols: - protocol: invocations_ws - version: 1.0.0 + version: 2.0.0 resources: cpu: "2" memory: 4Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-webrtc/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-webrtc/agent.yaml index 429a5eefa..9465fbc83 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-webrtc/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-webrtc/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: pipecat-ws-webrtc protocols: - protocol: invocations_ws - version: 1.0.0 + version: 2.0.0 resources: cpu: "2" memory: 4Gi diff --git a/samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-ws-server/agent.yaml b/samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-ws-server/agent.yaml index 0925dfb3a..a18424d0d 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-ws-server/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-ws-server/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: pipecat-ws-server protocols: - protocol: invocations_ws - version: 1.0.0 + version: 2.0.0 resources: cpu: "2" memory: 4Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/background-agent/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/background-agent/agent.yaml index 49a6a4c86..22da8d763 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/background-agent/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/background-agent/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: background-agent-responses protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/agent.yaml index 1951d60ea..e167eee81 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/agent.yaml @@ -13,7 +13,7 @@ metadata: - Python protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/main.py b/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/main.py index e07abb695..7951092a1 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/main.py +++ b/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/main.py @@ -53,6 +53,13 @@ ) from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.ai.projects import AIProjectClient + +try: + # Container protocol v2.0.0 exposes the inbound per-request context + # (call_id, session_id, ...) via a ContextVar populated by the runtime. + from azure.ai.agentserver.core import get_request_context +except ImportError: # pragma: no cover - older core without request context + get_request_context = None import asyncio import json import logging @@ -122,6 +129,12 @@ # Feature-flag header value (e.g. "Toolboxes=V1Preview"). _TOOLBOX_FEATURES = os.getenv("FOUNDRY_AGENT_TOOLBOX_FEATURES", "Toolboxes=V1Preview") +# Platform-injected per-request call identifier (container protocol v2.0.0). +# Extracted from the inbound responses request via ``get_request_context()`` and +# forwarded verbatim on every egress call to the Foundry toolbox MCP proxy so the +# platform can correlate the downstream tool calls with the originating request. +_FOUNDRY_CALL_ID_HEADER = "x-agent-foundry-call-id" + _credential = DefaultAzureCredential() _project_client = AIProjectClient(endpoint=_endpoint, credential=_credential) _responses_client = _project_client.get_openai_client().responses @@ -146,13 +159,16 @@ def __init__(self, endpoint: str, token_provider): self._session_id: str | None = None self._req_id = 0 - def _headers(self) -> dict: + def _headers(self, call_id: str | None = None) -> dict: h = { "Content-Type": "application/json", "Authorization": f"Bearer {self._get_token()}", } if _TOOLBOX_FEATURES: h["Foundry-Features"] = _TOOLBOX_FEATURES + # Forward the per-request call ID extracted from the inbound request. + if call_id: + h[_FOUNDRY_CALL_ID_HEADER] = call_id if self._session_id: h["mcp-session-id"] = self._session_id return h @@ -161,12 +177,12 @@ def _next_id(self) -> int: self._req_id += 1 return self._req_id - def initialize(self) -> str: + def initialize(self, call_id: str | None = None) -> str: """Send MCP initialize + initialized notification.""" with httpx.Client(timeout=60) as client: resp = client.post( self.endpoint, - headers=self._headers(), + headers=self._headers(call_id), json={ "jsonrpc": "2.0", "id": self._next_id(), @@ -185,29 +201,29 @@ def initialize(self) -> str: # Send initialized notification client.post( self.endpoint, - headers=self._headers(), + headers=self._headers(call_id), json={"jsonrpc": "2.0", "method": "notifications/initialized"}, ) return data.get("result", {}).get("serverInfo", {}).get("name", "unknown") - def list_tools(self) -> list[dict]: + def list_tools(self, call_id: str | None = None) -> list[dict]: """Call tools/list and return tool definitions.""" with httpx.Client(timeout=60) as client: resp = client.post( self.endpoint, - headers=self._headers(), + headers=self._headers(call_id), json={"jsonrpc": "2.0", "id": self._next_id( ), "method": "tools/list", "params": {}}, ) resp.raise_for_status() return resp.json().get("result", {}).get("tools", []) - def call_tool(self, name: str, arguments: dict) -> str: + def call_tool(self, name: str, arguments: dict, call_id: str | None = None) -> str: """Call a tool and return the text result.""" with httpx.Client(timeout=120) as client: resp = client.post( self.endpoint, - headers=self._headers(), + headers=self._headers(call_id), json={ "jsonrpc": "2.0", "id": self._next_id(), @@ -239,7 +255,7 @@ def call_tool(self, name: str, arguments: dict) -> str: _tools_initialized = False -def _ensure_tools(): +def _ensure_tools(call_id: str | None = None): global _mcp_client, _tool_definitions, _tools_initialized if _tools_initialized: return @@ -257,8 +273,8 @@ def _ensure_tools(): for attempt in range(1, 6): try: _mcp_client = _McpToolboxClient(TOOLBOX_ENDPOINT, _token_provider) - server_name = _mcp_client.initialize() - mcp_tools = _mcp_client.list_tools() + server_name = _mcp_client.initialize(call_id) + mcp_tools = _mcp_client.list_tools(call_id) if mcp_tools: break logger.warning( @@ -294,9 +310,9 @@ def _ensure_tools(): _MAX_TOOL_ROUNDS = 10 -def _call_model(input_items: list[dict]) -> object: +def _call_model(input_items: list[dict], call_id: str | None = None) -> object: """Call the model with tool definitions and return the response.""" - _ensure_tools() + _ensure_tools(call_id) return _responses_client.create( model=_model, instructions=_SYSTEM_PROMPT, @@ -306,7 +322,7 @@ def _call_model(input_items: list[dict]) -> object: ) -def _run_agent_loop(input_items: list[dict]) -> str: +def _run_agent_loop(input_items: list[dict], call_id: str | None = None) -> str: """Execute the agentic tool-calling loop synchronously. Calls the model, checks for tool calls, executes them, feeds results @@ -314,7 +330,7 @@ def _run_agent_loop(input_items: list[dict]) -> str: the max rounds limit. """ for _ in range(_MAX_TOOL_ROUNDS): - response = _call_model(input_items) + response = _call_model(input_items, call_id) # Check if the model wants to call tools tool_calls = [ @@ -330,7 +346,7 @@ def _run_agent_loop(input_items: list[dict]) -> str: try: arguments = json.loads(tc.arguments) if isinstance( tc.arguments, str) else tc.arguments - result_text = _mcp_client.call_tool(tc.name, arguments) + result_text = _mcp_client.call_tool(tc.name, arguments, call_id) logger.info("Tool '%s' returned %d chars", tc.name, len(result_text)) except Exception as e: @@ -431,12 +447,21 @@ async def handler( history = [] input_items = _build_input(user_input, history) - logger.info("Processing request %s", context.response_id) + # Extract the per-request call ID (container protocol v2.0.0) from the + # inbound request context so it can be forwarded on toolbox egress calls. + call_id = None + if get_request_context is not None: + try: + call_id = get_request_context().call_id + except Exception: # noqa: BLE001 + call_id = None + + logger.info("Processing request %s (call_id %s)", context.response_id, call_id) loop = asyncio.get_running_loop() try: assistant_reply = await asyncio.wait_for( - loop.run_in_executor(None, _run_agent_loop, input_items), + loop.run_in_executor(None, _run_agent_loop, input_items, call_id), timeout=240.0, ) except asyncio.TimeoutError: diff --git a/samples/python/hosted-agents/bring-your-own/responses/browser-automation/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/browser-automation/agent.yaml index c0ba53a68..69aec6f22 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/browser-automation/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/browser-automation/agent.yaml @@ -13,7 +13,7 @@ metadata: - Browser Automation protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/env-vars-agent/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/env-vars-agent/agent.yaml index 8579f31ed..29a16997d 100755 --- a/samples/python/hosted-agents/bring-your-own/responses/env-vars-agent/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/env-vars-agent/agent.yaml @@ -17,7 +17,7 @@ metadata: - Python protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/hello-world/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/hello-world/agent.yaml index 15bc22852..27ec6a7fd 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/hello-world/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/hello-world/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: hello-world-python-responses protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/langgraph-chat/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/langgraph-chat/agent.yaml index 27ca1a137..ba2bdf4e5 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/langgraph-chat/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/langgraph-chat/agent.yaml @@ -2,7 +2,7 @@ kind: hosted name: langgraph-chat-responses protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/agent.yaml index d57e28bf9..ecf8d52fd 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/agent.yaml @@ -7,7 +7,7 @@ metadata: - LangGraph protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/main.py b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/main.py index c6e8959bd..05d66933f 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/main.py +++ b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/main.py @@ -50,6 +50,13 @@ from azure.identity import DefaultAzureCredential, get_bearer_token_provider from langchain_azure_ai.tools import AzureAIProjectToolbox +try: + # Container protocol v2.0.0 exposes the inbound per-request context + # (call_id, session_id, ...) via a ContextVar populated by the runtime. + from azure.ai.agentserver.core import get_request_context +except ImportError: # pragma: no cover - older core without request context + get_request_context = None + # ── Agent name and logger ──────────────────────────────────────────────────── @@ -125,6 +132,12 @@ def auth_flow(self, request): # Feature-flag header value (e.g. "Toolboxes=V1Preview"). _TOOLBOX_FEATURES = os.getenv("FOUNDRY_AGENT_TOOLBOX_FEATURES", "Toolboxes=V1Preview") +# Platform-injected per-request call identifier (container protocol v2.0.0). +# Extracted from the inbound responses request via ``get_request_context()`` and +# forwarded on the egress toolbox MCP calls so the platform can correlate the +# downstream tool calls with the originating request. +_FOUNDRY_CALL_ID_HEADER = "x-agent-foundry-call-id" + def _toolbox_name_from_endpoint(endpoint: str) -> str | None: """Extract toolbox name from endpoint URL path.""" @@ -153,7 +166,7 @@ def create_agent(model, tools): return create_react_agent(model, tools, prompt=SYSTEM_PROMPT) -async def quickstart(): +async def quickstart(call_id: str | None = None): """Build and return a LangGraph agent wired to a Foundry toolbox. Uses AzureAIProjectToolbox from langchain-azure-ai to resolve and load @@ -170,6 +183,12 @@ async def quickstart(): logger.info(f"Connecting to toolbox: {TOOLBOX_ENDPOINT}") extra_headers = {"Foundry-Features": _TOOLBOX_FEATURES} if _TOOLBOX_FEATURES else {} + # Forward the inbound per-request call ID on toolbox egress calls. Note: + # AzureAIProjectToolbox only accepts static extra_headers at construction and + # the agent/toolbox is cached (see _get_agent), so the call ID captured on the + # first request is reused for the lifetime of the cached toolbox. + if call_id: + extra_headers[_FOUNDRY_CALL_ID_HEADER] = call_id toolbox = AzureAIProjectToolbox( project_endpoint=PROJECT_ENDPOINT, toolbox_name=toolbox_name, @@ -311,7 +330,7 @@ def _get_input_text(request: CreateResponse) -> str | None: _agent_lock = asyncio.Lock() -async def _get_agent(): +async def _get_agent(call_id: str | None = None): global _agent, _mcp_client if _agent is not None: return _agent @@ -326,7 +345,7 @@ async def _get_agent(): last_exc: Exception | None = None for attempt in range(1, 6): try: - agent, mcp_client = await quickstart() + agent, mcp_client = await quickstart(call_id) bound_tools = getattr(agent, "tools", None) or [] if not bound_tools: logger.warning( @@ -344,7 +363,7 @@ async def _get_agent(): await asyncio.sleep(min(2 ** attempt, 15)) if last_exc is not None: raise last_exc - _agent, _mcp_client = await quickstart() + _agent, _mcp_client = await quickstart(call_id) return _agent @@ -373,7 +392,17 @@ async def handle_response( return try: - agent = await _get_agent() + # Extract the per-request call ID (container protocol v2.0.0) from the + # inbound request context so it can be forwarded on toolbox egress calls. + call_id = None + if get_request_context is not None: + try: + call_id = get_request_context().call_id + except Exception: # noqa: BLE001 + call_id = None + logger.info("Processing request %s (call_id %s)", + context.response_id, call_id) + agent = await _get_agent(call_id) result = await asyncio.wait_for( agent.ainvoke({"messages": [("user", user_input)]}), timeout=240.0, diff --git a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox/agent.yaml index 8b5512050..f794d645c 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox/agent.yaml @@ -7,7 +7,7 @@ metadata: - LangGraph protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox/main.py b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox/main.py index 74f94a895..a85aec1b8 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox/main.py +++ b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox/main.py @@ -50,6 +50,13 @@ from azure.identity import DefaultAzureCredential, get_bearer_token_provider from langchain_azure_ai.tools import AzureAIProjectToolbox +try: + # Container protocol v2.0.0 exposes the inbound per-request context + # (call_id, session_id, ...) via a ContextVar populated by the runtime. + from azure.ai.agentserver.core import get_request_context +except ImportError: # pragma: no cover - older core without request context + get_request_context = None + # ── Agent name and logger ──────────────────────────────────────────────────── @@ -125,6 +132,12 @@ def auth_flow(self, request): # Feature-flag header value (e.g. "Toolboxes=V1Preview"). _TOOLBOX_FEATURES = os.getenv("FOUNDRY_AGENT_TOOLBOX_FEATURES", "Toolboxes=V1Preview") +# Platform-injected per-request call identifier (container protocol v2.0.0). +# Extracted from the inbound responses request via ``get_request_context()`` and +# forwarded on the egress toolbox MCP calls so the platform can correlate the +# downstream tool calls with the originating request. +_FOUNDRY_CALL_ID_HEADER = "x-agent-foundry-call-id" + def _toolbox_name_from_endpoint(endpoint: str) -> str | None: """Extract toolbox name from endpoint URL path.""" @@ -153,7 +166,7 @@ def create_agent(model, tools): return create_react_agent(model, tools, prompt=SYSTEM_PROMPT) -async def quickstart(): +async def quickstart(call_id: str | None = None): """Build and return a LangGraph agent wired to a Foundry toolbox. Uses AzureAIProjectToolbox from langchain-azure-ai to resolve and load @@ -170,6 +183,12 @@ async def quickstart(): logger.info(f"Connecting to toolbox: {TOOLBOX_ENDPOINT}") extra_headers = {"Foundry-Features": _TOOLBOX_FEATURES} if _TOOLBOX_FEATURES else {} + # Forward the inbound per-request call ID on toolbox egress calls. Note: + # AzureAIProjectToolbox only accepts static extra_headers at construction and + # the agent/toolbox is cached (see _get_agent), so the call ID captured on the + # first request is reused for the lifetime of the cached toolbox. + if call_id: + extra_headers[_FOUNDRY_CALL_ID_HEADER] = call_id toolbox = AzureAIProjectToolbox( project_endpoint=PROJECT_ENDPOINT, toolbox_name=toolbox_name, @@ -311,7 +330,7 @@ def _get_input_text(request: CreateResponse) -> str | None: _agent_lock = asyncio.Lock() -async def _get_agent(): +async def _get_agent(call_id: str | None = None): global _agent, _mcp_client if _agent is not None: return _agent @@ -320,7 +339,7 @@ async def _get_agent(): if _agent is not None: return _agent - _agent, _mcp_client = await quickstart() + _agent, _mcp_client = await quickstart(call_id) return _agent @@ -349,7 +368,17 @@ async def handle_response( return try: - agent = await _get_agent() + # Extract the per-request call ID (container protocol v2.0.0) from the + # inbound request context so it can be forwarded on toolbox egress calls. + call_id = None + if get_request_context is not None: + try: + call_id = get_request_context().call_id + except Exception: # noqa: BLE001 + call_id = None + logger.info("Processing request %s (call_id %s)", + context.response_id, call_id) + agent = await _get_agent(call_id) result = await asyncio.wait_for( agent.ainvoke({"messages": [("user", user_input)]}), timeout=240.0, diff --git a/samples/python/hosted-agents/bring-your-own/responses/notetaking-agent/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/notetaking-agent/agent.yaml index 66eb2598e..bd389268d 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/notetaking-agent/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/notetaking-agent/agent.yaml @@ -2,7 +2,7 @@ kind: hosted name: notetaking-agent-responses-python protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/openai-agents-sdk/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/openai-agents-sdk/agent.yaml index 63b7e42fd..676356a08 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/openai-agents-sdk/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/openai-agents-sdk/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: openai-agents-sdk-invocations protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/optimization-customer-support/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/optimization-customer-support/agent.yaml index 11b587251..22e5d2c1d 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/optimization-customer-support/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/optimization-customer-support/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: optimization-customer-support-python-responses protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "1" memory: 2Gi diff --git a/samples/python/hosted-agents/bring-your-own/responses/optimization-hello-world/agent.yaml b/samples/python/hosted-agents/bring-your-own/responses/optimization-hello-world/agent.yaml index efcdc2433..d6ff57371 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/optimization-hello-world/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/responses/optimization-hello-world/agent.yaml @@ -3,7 +3,7 @@ kind: hosted name: optimization-hello-world-python-responses protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "1" memory: 2Gi diff --git a/samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/agent.yaml b/samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/agent.yaml index 6b80b8373..7ffc62c73 100644 --- a/samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/agent.yaml @@ -5,7 +5,7 @@ description: | Integrates with Work IQ, Document Understanding, and Procedural Memory. protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 0.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/voicelive/handoff-langgraph-responses-voicelive/agent.yaml b/samples/python/hosted-agents/bring-your-own/voicelive/handoff-langgraph-responses-voicelive/agent.yaml index 5550da5bb..d1005eed1 100644 --- a/samples/python/hosted-agents/bring-your-own/voicelive/handoff-langgraph-responses-voicelive/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/voicelive/handoff-langgraph-responses-voicelive/agent.yaml @@ -14,7 +14,7 @@ metadata: - Python protocols: - protocol: responses - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.5" memory: 1Gi diff --git a/samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/agent.yaml b/samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/agent.yaml index 146558d58..163d558fd 100644 --- a/samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/agent.yaml @@ -5,7 +5,7 @@ metadata: voiceLiveCompatible: "true" protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.25" memory: 1.5Gi diff --git a/samples/python/hosted-agents/bring-your-own/voicelive/hotel-booking-invocations-voicelive/agent.yaml b/samples/python/hosted-agents/bring-your-own/voicelive/hotel-booking-invocations-voicelive/agent.yaml index 3e3d7cd20..0f2232ddc 100644 --- a/samples/python/hosted-agents/bring-your-own/voicelive/hotel-booking-invocations-voicelive/agent.yaml +++ b/samples/python/hosted-agents/bring-your-own/voicelive/hotel-booking-invocations-voicelive/agent.yaml @@ -14,7 +14,7 @@ metadata: voiceLiveCompatible: "true" protocols: - protocol: invocations - version: 1.0.0 + version: 2.0.0 resources: cpu: "0.5" memory: 1Gi