Skip to content

Commit ec752e1

Browse files
Harsheet ShahCopilot
andcommitted
Forward x-agent-foundry-call-id header on toolbox egress calls
Extract the per-request x-agent-foundry-call-id header from the inbound invocation/responses call (container protocol 2.0.0) and forward it on the egress request to the Foundry toolbox MCP proxy for all four bring-your-own toolbox samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c77eecb commit ec752e1

4 files changed

Lines changed: 144 additions & 48 deletions

File tree

  • samples/python/hosted-agents/bring-your-own

samples/python/hosted-agents/bring-your-own/invocations/toolbox/main.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@
118118
_TOOLBOX_FEATURES = os.getenv(
119119
"FOUNDRY_AGENT_TOOLBOX_FEATURES", "Toolboxes=V1Preview")
120120

121+
# Platform-injected per-request call identifier (container protocol v2.0.0).
122+
# Extracted from the inbound invocation request and forwarded verbatim on every
123+
# egress call to the Foundry toolbox MCP proxy so the platform can correlate the
124+
# downstream tool calls with the originating invocation. Never parse this value.
125+
_FOUNDRY_CALL_ID_HEADER = "x-agent-foundry-call-id"
126+
121127
_credential = DefaultAzureCredential()
122128
_project_client = AIProjectClient(endpoint=_endpoint, credential=_credential)
123129
_responses_client = _project_client.get_openai_client().responses
@@ -142,13 +148,16 @@ def __init__(self, endpoint: str, token_provider):
142148
self._session_id: str | None = None
143149
self._req_id = 0
144150

145-
def _headers(self) -> dict:
151+
def _headers(self, call_id: str | None = None) -> dict:
146152
h = {
147153
"Content-Type": "application/json",
148154
"Authorization": f"Bearer {self._get_token()}",
149155
}
150156
if _TOOLBOX_FEATURES:
151157
h["Foundry-Features"] = _TOOLBOX_FEATURES
158+
# Forward the per-request call ID extracted from the inbound invocation.
159+
if call_id:
160+
h[_FOUNDRY_CALL_ID_HEADER] = call_id
152161
if self._session_id:
153162
h["mcp-session-id"] = self._session_id
154163
return h
@@ -157,12 +166,12 @@ def _next_id(self) -> int:
157166
self._req_id += 1
158167
return self._req_id
159168

160-
def initialize(self) -> str:
169+
def initialize(self, call_id: str | None = None) -> str:
161170
"""Send MCP initialize + initialized notification."""
162171
with httpx.Client(timeout=60) as client:
163172
resp = client.post(
164173
self.endpoint,
165-
headers=self._headers(),
174+
headers=self._headers(call_id),
166175
json={
167176
"jsonrpc": "2.0",
168177
"id": self._next_id(),
@@ -181,29 +190,29 @@ def initialize(self) -> str:
181190
# Send initialized notification
182191
client.post(
183192
self.endpoint,
184-
headers=self._headers(),
193+
headers=self._headers(call_id),
185194
json={"jsonrpc": "2.0", "method": "notifications/initialized"},
186195
)
187196
return data.get("result", {}).get("serverInfo", {}).get("name", "unknown")
188197

189-
def list_tools(self) -> list[dict]:
198+
def list_tools(self, call_id: str | None = None) -> list[dict]:
190199
"""Call tools/list and return tool definitions."""
191200
with httpx.Client(timeout=60) as client:
192201
resp = client.post(
193202
self.endpoint,
194-
headers=self._headers(),
203+
headers=self._headers(call_id),
195204
json={"jsonrpc": "2.0", "id": self._next_id(
196205
), "method": "tools/list", "params": {}},
197206
)
198207
resp.raise_for_status()
199208
return resp.json().get("result", {}).get("tools", [])
200209

201-
def call_tool(self, name: str, arguments: dict) -> str:
210+
def call_tool(self, name: str, arguments: dict, call_id: str | None = None) -> str:
202211
"""Call a tool and return the text result."""
203212
with httpx.Client(timeout=120) as client:
204213
resp = client.post(
205214
self.endpoint,
206-
headers=self._headers(),
215+
headers=self._headers(call_id),
207216
json={
208217
"jsonrpc": "2.0",
209218
"id": self._next_id(),
@@ -235,13 +244,13 @@ def call_tool(self, name: str, arguments: dict) -> str:
235244
_tools_initialized = False
236245

237246

238-
def _ensure_tools():
247+
def _ensure_tools(call_id: str | None = None):
239248
global _mcp_client, _tool_definitions, _tools_initialized
240249
if _tools_initialized:
241250
return
242251
_mcp_client = _McpToolboxClient(TOOLBOX_ENDPOINT, _token_provider)
243-
server_name = _mcp_client.initialize()
244-
mcp_tools = _mcp_client.list_tools()
252+
server_name = _mcp_client.initialize(call_id)
253+
mcp_tools = _mcp_client.list_tools(call_id)
245254
logger.info("Toolbox '%s' connected: %d tool(s) discovered",
246255
server_name, len(mcp_tools))
247256
for t in mcp_tools:
@@ -265,9 +274,9 @@ def _ensure_tools():
265274
_MAX_TOOL_ROUNDS = 10
266275

267276

268-
def _call_model(input_items: list[dict]) -> object:
277+
def _call_model(input_items: list[dict], call_id: str | None = None) -> object:
269278
"""Call the model with tool definitions and return the response."""
270-
_ensure_tools()
279+
_ensure_tools(call_id)
271280
return _responses_client.create(
272281
model=_model,
273282
instructions=_SYSTEM_PROMPT,
@@ -277,15 +286,15 @@ def _call_model(input_items: list[dict]) -> object:
277286
)
278287

279288

280-
def _run_agent_loop(input_items: list[dict]) -> str:
289+
def _run_agent_loop(input_items: list[dict], call_id: str | None = None) -> str:
281290
"""Execute the agentic tool-calling loop synchronously.
282291
283292
Calls the model, checks for tool calls, executes them, feeds results
284293
back, and repeats until the model produces a text response or we hit
285294
the max rounds limit.
286295
"""
287296
for _ in range(_MAX_TOOL_ROUNDS):
288-
response = _call_model(input_items)
297+
response = _call_model(input_items, call_id)
289298

290299
# Check if the model wants to call tools
291300
tool_calls = [
@@ -302,7 +311,7 @@ def _run_agent_loop(input_items: list[dict]) -> str:
302311
try:
303312
arguments = json.loads(tc.arguments) if isinstance(
304313
tc.arguments, str) else tc.arguments
305-
result_text = _mcp_client.call_tool(tc.name, arguments)
314+
result_text = _mcp_client.call_tool(tc.name, arguments, call_id)
306315
logger.info("Tool '%s' returned %d chars",
307316
tc.name, len(result_text))
308317
except Exception as e:
@@ -326,10 +335,10 @@ def _run_agent_loop(input_items: list[dict]) -> str:
326335
return "(Reached maximum tool call rounds)"
327336

328337

329-
async def _stream_agent_reply(input_items: list[dict]):
338+
async def _stream_agent_reply(input_items: list[dict], call_id: str | None = None):
330339
"""Run the agent loop in a thread and yield the result as SSE events."""
331340
loop = asyncio.get_running_loop()
332-
result = await loop.run_in_executor(None, _run_agent_loop, input_items)
341+
result = await loop.run_in_executor(None, _run_agent_loop, input_items, call_id)
333342
yield f"data: {json.dumps({'type': 'token', 'content': result})}\n\n"
334343

335344

@@ -374,8 +383,12 @@ async def handle_invoke(request: Request):
374383
session_id = request.state.session_id
375384
invocation_id = request.state.invocation_id
376385

377-
logger.info("Processing invocation %s (session %s)",
378-
invocation_id, session_id)
386+
# Extract the per-request call ID (container protocol v2.0.0) directly from
387+
# the inbound request headers so it can be forwarded on toolbox egress calls.
388+
call_id = request.headers.get(_FOUNDRY_CALL_ID_HEADER)
389+
390+
logger.info("Processing invocation %s (session %s, call_id %s)",
391+
invocation_id, session_id, call_id)
379392

380393
# Retrieve or create conversation history for this session.
381394
history = _sessions.setdefault(session_id, [])
@@ -385,7 +398,7 @@ async def handle_invoke(request: Request):
385398
async def event_generator():
386399
full_reply = ""
387400
try:
388-
async for delta in _stream_agent_reply(input_items):
401+
async for delta in _stream_agent_reply(input_items, call_id):
389402
# Parse the SSE data to extract the content for history
390403
try:
391404
event_data = json.loads(delta.split(

samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/main.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@
5353
)
5454
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
5555
from azure.ai.projects import AIProjectClient
56+
57+
try:
58+
# Container protocol v2.0.0 exposes the inbound per-request context
59+
# (call_id, session_id, ...) via a ContextVar populated by the runtime.
60+
from azure.ai.agentserver.core import get_request_context
61+
except ImportError: # pragma: no cover - older core without request context
62+
get_request_context = None
5663
import asyncio
5764
import json
5865
import logging
@@ -122,6 +129,12 @@
122129
# Feature-flag header value (e.g. "Toolboxes=V1Preview").
123130
_TOOLBOX_FEATURES = os.getenv("FOUNDRY_AGENT_TOOLBOX_FEATURES", "Toolboxes=V1Preview")
124131

132+
# Platform-injected per-request call identifier (container protocol v2.0.0).
133+
# Extracted from the inbound responses request via ``get_request_context()`` and
134+
# forwarded verbatim on every egress call to the Foundry toolbox MCP proxy so the
135+
# platform can correlate the downstream tool calls with the originating request.
136+
_FOUNDRY_CALL_ID_HEADER = "x-agent-foundry-call-id"
137+
125138
_credential = DefaultAzureCredential()
126139
_project_client = AIProjectClient(endpoint=_endpoint, credential=_credential)
127140
_responses_client = _project_client.get_openai_client().responses
@@ -146,13 +159,16 @@ def __init__(self, endpoint: str, token_provider):
146159
self._session_id: str | None = None
147160
self._req_id = 0
148161

149-
def _headers(self) -> dict:
162+
def _headers(self, call_id: str | None = None) -> dict:
150163
h = {
151164
"Content-Type": "application/json",
152165
"Authorization": f"Bearer {self._get_token()}",
153166
}
154167
if _TOOLBOX_FEATURES:
155168
h["Foundry-Features"] = _TOOLBOX_FEATURES
169+
# Forward the per-request call ID extracted from the inbound request.
170+
if call_id:
171+
h[_FOUNDRY_CALL_ID_HEADER] = call_id
156172
if self._session_id:
157173
h["mcp-session-id"] = self._session_id
158174
return h
@@ -161,12 +177,12 @@ def _next_id(self) -> int:
161177
self._req_id += 1
162178
return self._req_id
163179

164-
def initialize(self) -> str:
180+
def initialize(self, call_id: str | None = None) -> str:
165181
"""Send MCP initialize + initialized notification."""
166182
with httpx.Client(timeout=60) as client:
167183
resp = client.post(
168184
self.endpoint,
169-
headers=self._headers(),
185+
headers=self._headers(call_id),
170186
json={
171187
"jsonrpc": "2.0",
172188
"id": self._next_id(),
@@ -185,29 +201,29 @@ def initialize(self) -> str:
185201
# Send initialized notification
186202
client.post(
187203
self.endpoint,
188-
headers=self._headers(),
204+
headers=self._headers(call_id),
189205
json={"jsonrpc": "2.0", "method": "notifications/initialized"},
190206
)
191207
return data.get("result", {}).get("serverInfo", {}).get("name", "unknown")
192208

193-
def list_tools(self) -> list[dict]:
209+
def list_tools(self, call_id: str | None = None) -> list[dict]:
194210
"""Call tools/list and return tool definitions."""
195211
with httpx.Client(timeout=60) as client:
196212
resp = client.post(
197213
self.endpoint,
198-
headers=self._headers(),
214+
headers=self._headers(call_id),
199215
json={"jsonrpc": "2.0", "id": self._next_id(
200216
), "method": "tools/list", "params": {}},
201217
)
202218
resp.raise_for_status()
203219
return resp.json().get("result", {}).get("tools", [])
204220

205-
def call_tool(self, name: str, arguments: dict) -> str:
221+
def call_tool(self, name: str, arguments: dict, call_id: str | None = None) -> str:
206222
"""Call a tool and return the text result."""
207223
with httpx.Client(timeout=120) as client:
208224
resp = client.post(
209225
self.endpoint,
210-
headers=self._headers(),
226+
headers=self._headers(call_id),
211227
json={
212228
"jsonrpc": "2.0",
213229
"id": self._next_id(),
@@ -239,7 +255,7 @@ def call_tool(self, name: str, arguments: dict) -> str:
239255
_tools_initialized = False
240256

241257

242-
def _ensure_tools():
258+
def _ensure_tools(call_id: str | None = None):
243259
global _mcp_client, _tool_definitions, _tools_initialized
244260
if _tools_initialized:
245261
return
@@ -257,8 +273,8 @@ def _ensure_tools():
257273
for attempt in range(1, 6):
258274
try:
259275
_mcp_client = _McpToolboxClient(TOOLBOX_ENDPOINT, _token_provider)
260-
server_name = _mcp_client.initialize()
261-
mcp_tools = _mcp_client.list_tools()
276+
server_name = _mcp_client.initialize(call_id)
277+
mcp_tools = _mcp_client.list_tools(call_id)
262278
if mcp_tools:
263279
break
264280
logger.warning(
@@ -294,9 +310,9 @@ def _ensure_tools():
294310
_MAX_TOOL_ROUNDS = 10
295311

296312

297-
def _call_model(input_items: list[dict]) -> object:
313+
def _call_model(input_items: list[dict], call_id: str | None = None) -> object:
298314
"""Call the model with tool definitions and return the response."""
299-
_ensure_tools()
315+
_ensure_tools(call_id)
300316
return _responses_client.create(
301317
model=_model,
302318
instructions=_SYSTEM_PROMPT,
@@ -306,15 +322,15 @@ def _call_model(input_items: list[dict]) -> object:
306322
)
307323

308324

309-
def _run_agent_loop(input_items: list[dict]) -> str:
325+
def _run_agent_loop(input_items: list[dict], call_id: str | None = None) -> str:
310326
"""Execute the agentic tool-calling loop synchronously.
311327
312328
Calls the model, checks for tool calls, executes them, feeds results
313329
back, and repeats until the model produces a text response or we hit
314330
the max rounds limit.
315331
"""
316332
for _ in range(_MAX_TOOL_ROUNDS):
317-
response = _call_model(input_items)
333+
response = _call_model(input_items, call_id)
318334

319335
# Check if the model wants to call tools
320336
tool_calls = [
@@ -330,7 +346,7 @@ def _run_agent_loop(input_items: list[dict]) -> str:
330346
try:
331347
arguments = json.loads(tc.arguments) if isinstance(
332348
tc.arguments, str) else tc.arguments
333-
result_text = _mcp_client.call_tool(tc.name, arguments)
349+
result_text = _mcp_client.call_tool(tc.name, arguments, call_id)
334350
logger.info("Tool '%s' returned %d chars",
335351
tc.name, len(result_text))
336352
except Exception as e:
@@ -431,12 +447,21 @@ async def handler(
431447
history = []
432448
input_items = _build_input(user_input, history)
433449

434-
logger.info("Processing request %s", context.response_id)
450+
# Extract the per-request call ID (container protocol v2.0.0) from the
451+
# inbound request context so it can be forwarded on toolbox egress calls.
452+
call_id = None
453+
if get_request_context is not None:
454+
try:
455+
call_id = get_request_context().call_id
456+
except Exception: # noqa: BLE001
457+
call_id = None
458+
459+
logger.info("Processing request %s (call_id %s)", context.response_id, call_id)
435460

436461
loop = asyncio.get_running_loop()
437462
try:
438463
assistant_reply = await asyncio.wait_for(
439-
loop.run_in_executor(None, _run_agent_loop, input_items),
464+
loop.run_in_executor(None, _run_agent_loop, input_items, call_id),
440465
timeout=240.0,
441466
)
442467
except asyncio.TimeoutError:

0 commit comments

Comments
 (0)