Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6e0446d
[agentserver] Support container protocol v2.0.0 (per-request call ID)
vangarp Jun 20, 2026
608a797
[agentserver] Bump core dependency floor to 2.0.0b7
vangarp Jun 20, 2026
0949b19
[agentserver] Forward only call ID (not user-id) on outbound 1P calls
vangarp Jun 20, 2026
c483f98
[agentserver] Fix spell-check: rename pctx_token -> platform_ctx_token
vangarp Jun 20, 2026
1ca8273
[agentserver] ghcopilot toolbox call-id (§8) + rename RequestContext …
vangarp Jun 21, 2026
df4999f
[agentserver] Add multi-user (per-request call ID) README examples; f…
vangarp Jun 21, 2026
f94ad77
Disable mindependency for responses/invocations (unpublished core 2.0…
vangarp Jun 21, 2026
ffa6e04
Re-establish platform request context for streaming response bodies
vangarp Jun 21, 2026
a4783a5
Fix misleading comment in _toolbox.py: only call ID is forwarded to 1…
Copilot Jun 21, 2026
8f3dd74
fix: use correct class name in CHANGELOG and import FOUNDRY_CALL_ID f…
Copilot Jun 21, 2026
94daa1b
Remove extra blank line in _toolbox.py (PEP 8 / black compliance)
Copilot Jun 21, 2026
5e2b9d8
Merge branch 'main' into vangarp/agentserver-core-2.0.0-support
vangarp Jun 28, 2026
733fa54
Revert githubcopilot package changes
vangarp Jun 28, 2026
18385cc
Add api.md for agentserver core, responses, and invocations
vangarp Jun 28, 2026
0d7cebd
Rename responses scripts/ to _scripts/ so apistub skips it
vangarp Jun 28, 2026
bd7aafa
Remove api.md and api.metadata.yml from agentserver packages
vangarp Jun 28, 2026
10c924d
Revert "Remove api.md and api.metadata.yml from agentserver packages"
vangarp Jun 28, 2026
8bffe63
Add 'xhigh' to responses cspell ignoreWords
vangarp Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Release History

## 2.0.0b7 (Unreleased)

### Features Added

- Container protocol version `2.0.0` support: added the platform identity header constants `x-agent-user-id` (`USER_ID`) — the global, cross-agent per-user partition key — and `x-agent-foundry-call-id` (`FOUNDRY_CALL_ID`) — the opaque per-request call identifier — to the `_platform_headers` module.
- Added `FOUNDRY_AGENT_ID` environment variable support exposing the agent's stable GUID via `AgentConfig.agent_guid` and the `resolve_agent_guid()` helper.
- Added a request-scoped platform context: `FoundryAgentRequestContext`, `get_request_context()`, `set_request_context()`, and `reset_request_context()`. Protocol packages bind the inbound per-request call ID and user ID so that handler code (and the SDK HTTP pipeline) can read them. `FoundryAgentRequestContext.platform_headers()` builds the headers to forward on outbound Foundry 1P calls — the per-request call ID only; `x-agent-user-id` is **not** forwarded (it is not accepted/trusted by 1P services and is used only for container-side state partitioning).

### Breaking Changes

- Replaced the `x-agent-user-isolation-key` / `x-agent-chat-isolation-key` header constants (`USER_ISOLATION_KEY` / `CHAT_ISOLATION_KEY`) with `x-agent-user-id` (`USER_ID`) and `x-agent-foundry-call-id` (`FOUNDRY_CALL_ID`) per container protocol version `2.0.0`.

## 2.0.0b6 (2026-06-12)

### Bugs Fixed
Expand Down
30 changes: 30 additions & 0 deletions sdk/agentserver/azure-ai-agentserver-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ async def handle(request):
app.run()
```

### Per-request identity (multi-user sessions)

On container protocol `2.0.0` a single agent session can serve **multiple users**. Each request carries `x-agent-user-id` (the user — partition state by it) and an opaque `x-agent-foundry-call-id` (the per-request caller identity). Read both via `get_request_context()`; the SDK forwards **only** the call ID on outbound Foundry calls — `x-agent-user-id` is never echoed. Forwarding the call ID lets a tool server resolve which user made the request and act on their behalf.

```python
import os

import httpx
from azure.ai.agentserver.core import get_request_context


def foundry_headers() -> dict[str, str]:
# Echoes x-agent-foundry-call-id only; x-agent-user-id is never forwarded.
return dict(get_request_context().platform_headers())


async def call_toolbox(query: str) -> str:
user_id = get_request_context().user_id # for the container's OWN per-user state
# Attach the call ID PER CALL — a toolbox MCP session is long-lived and serves many
# users/turns, so never bake one call's ID into the client's static headers.
async with httpx.AsyncClient() as mcp:
resp = await mcp.post(
f"{os.environ['FOUNDRY_PROJECT_ENDPOINT']}/toolboxes/github/mcp",
headers={"Authorization": f"Bearer {get_agent_token()}", **foundry_headers()},
json={"jsonrpc": "2.0", "method": "tools/call",
"params": {"name": "list_my_assigned_issues", "arguments": {}}},
)
return resp.text # the toolbox resolved the caller from the call ID and acted as that user
```

### Subclassing AgentServerHost

For custom protocol implementations, subclass `AgentServerHost` and add routes:
Expand Down
205 changes: 205 additions & 0 deletions sdk/agentserver/azure-ai-agentserver-core/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
```py
namespace azure.ai.agentserver.core

def azure.ai.agentserver.core.build_server_version(sdk_name: str, version: str) -> str: ...


def azure.ai.agentserver.core.configure_observability(
*,
connection_string: Optional[str] = ...,
enable_sensitive_data: bool = False,
log_level: Optional[str] = ...
) -> None: ...


def azure.ai.agentserver.core.create_error_response(
code: str,
message: str,
*,
details: Optional[list[dict[str, Any]]] = ...,
error_type: Optional[str] = ...,
headers: Optional[dict[str, str]] = ...,
status_code: int
) -> JSONResponse: ...


def azure.ai.agentserver.core.detach_context(token: Any) -> None: ...


def azure.ai.agentserver.core.end_span(span: Any, exc: Optional[BaseException] = None) -> None: ...


def azure.ai.agentserver.core.flush_spans(timeout_millis: int = 5000) -> None: ...


def azure.ai.agentserver.core.get_request_context() -> FoundryAgentRequestContext: ...


def azure.ai.agentserver.core.record_error(span: Any, exc: BaseException) -> None: ...


def azure.ai.agentserver.core.reset_request_context(token: Token[FoundryAgentRequestContext]) -> None: ...


def azure.ai.agentserver.core.set_current_span(span: Any) -> Any: ...


def azure.ai.agentserver.core.set_request_context(context: FoundryAgentRequestContext) -> Token[FoundryAgentRequestContext]: ...


async def azure.ai.agentserver.core.trace_stream:async(iterator: AsyncIterable[_Content], span: Any) -> AsyncIterator[_Content]: ...


class azure.ai.agentserver.core.AgentConfig:

def __init__(
self,
*,
agent_guid: str = "",
agent_id: str,
agent_name: str,
agent_version: str,
appinsights_connection_string: str,
is_hosted: bool,
otlp_endpoint: str,
port: int,
project_endpoint: str,
project_id: str,
session_id: str,
sse_keepalive_interval: int,
ws_ping_interval: float = 0.0
) -> None: ...

@classmethod
def from_env(cls) -> Self: ...


class azure.ai.agentserver.core.AgentServerHost(Starlette):
property routes: list[BaseRoute] # Read-only

async def __call__(
self,
scope: Scope,
receive: Receive,
send: Send
) -> None: ...

def __init__(
self,
*,
access_log: Optional[Logger] = _SENTINEL_ACCESS_LOG,
access_log_format: Optional[str] = ...,
applicationinsights_connection_string: Optional[str] = ...,
configure_observability: Optional[Callable[, None]] = _tracing.configure_observability,
graceful_shutdown_timeout: Optional[int] = ...,
log_level: Optional[str] = ...,
routes: Optional[list[Route]] = ...,
**kwargs: Any
) -> None: ...

def add_exception_handler(
self,
exc_class_or_status_code: int | type[Exception],
handler: ExceptionHandler
) -> None: ...

def add_middleware(
self,
middleware_class: _MiddlewareFactory[P],
*args: args,
**kwargs: kwargs
) -> None: ...

def add_route(
self,
path: str,
route: Callable[[Request], Awaitable[Response] | Response],
methods: list[str] | None = None,
name: str | None = None,
include_in_schema: bool = True
) -> None: ...

def build_middleware_stack(self) -> ASGIApp: ...

def host(
self,
host: str,
app: ASGIApp,
name: str | None = None
) -> None: ...

def mount(
self,
path: str,
app: ASGIApp,
name: str | None = None
) -> None: ...

def register_server_version(self, version_segment: str) -> None: ...

def run(
self,
host: str = "0.0.0.0",
port: Optional[int] = None
) -> None: ...

async def run_async(
self,
host: str = "0.0.0.0",
port: Optional[int] = None
) -> None: ...

def shutdown_handler(self, fn: Callable[[], Awaitable[None]]) -> Callable[[], Awaitable[None]]: ...

@staticmethod
async def sse_keepalive_stream(iterator: AsyncIterable[_Content], interval: int) -> AsyncIterator[_Content]: ...

def url_path_for(
self,
name: str,
/,
**path_params: Any
) -> URLPath: ...


class azure.ai.agentserver.core.FoundryAgentRequestContext:
call_id: str | None
session_id: str | None
user_id: str | None

def __init__(
self,
*,
call_id: str | None = ...,
session_id: str | None = ...,
user_id: str | None = ...
) -> None: ...

def platform_headers(self) -> dict[str, str]: ...


class azure.ai.agentserver.core.InboundRequestLoggingMiddleware:

async def __call__(
self,
scope: Scope,
receive: Receive,
send: Send
) -> None: ...

def __init__(self, app: ASGIApp) -> None: ...


class azure.ai.agentserver.core.RequestIdMiddleware:

async def __call__(
self,
scope: Scope,
receive: Receive,
send: Send
) -> None: ...

def __init__(self, app: ASGIApp) -> None: ...


```
3 changes: 3 additions & 0 deletions sdk/agentserver/azure-ai-agentserver-core/api.metadata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
apiMdSha256: cbbfb5e77c255601f97cc31505d7e0962ac3eeb665c690659064b2bd4940071e
parserVersion: 0.3.28
pythonVersion: 3.11.9
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
from azure.ai.agentserver.core import (
AgentConfig,
AgentServerHost,
FoundryAgentRequestContext,
configure_observability,
create_error_response,
detach_context,
end_span,
flush_spans,
get_request_context,
record_error,
set_current_span,
trace_stream,
Expand All @@ -27,6 +29,12 @@
from ._config import AgentConfig
from ._errors import create_error_response
from ._middleware import InboundRequestLoggingMiddleware
from ._request_context import (
FoundryAgentRequestContext,
get_request_context,
reset_request_context,
set_request_context,
)
from ._request_id import RequestIdMiddleware
from ._server_version import build_server_version
from ._tracing import (
Expand All @@ -44,15 +52,19 @@
"AgentConfig",
"AgentServerHost",
"InboundRequestLoggingMiddleware",
"FoundryAgentRequestContext",
"RequestIdMiddleware",
"build_server_version",
"configure_observability",
"create_error_response",
"detach_context",
"end_span",
"flush_spans",
"get_request_context",
"record_error",
"reset_request_context",
"set_current_span",
"set_request_context",
"trace_stream",
]
__version__ = VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# ======================================================================

_ENV_FOUNDRY_AGENT_NAME = "FOUNDRY_AGENT_NAME"
_ENV_FOUNDRY_AGENT_ID = "FOUNDRY_AGENT_ID"
_ENV_FOUNDRY_AGENT_VERSION = "FOUNDRY_AGENT_VERSION"
_ENV_FOUNDRY_AGENT_INSTANCE_CLIENT_ID = "FOUNDRY_AGENT_INSTANCE_CLIENT_ID"
_ENV_FOUNDRY_AGENT_BLUEPRINT_CLIENT_ID = "FOUNDRY_AGENT_BLUEPRINT_CLIENT_ID"
Expand Down Expand Up @@ -60,6 +61,7 @@ class AgentConfig: # pylint: disable=too-many-instance-attributes
:param agent_name: Agent name from ``FOUNDRY_AGENT_NAME``.
:param agent_version: Agent version from ``FOUNDRY_AGENT_VERSION``.
:param agent_id: Combined identifier (``"name:version"`` or ``"name"`` or ``""``).
:param agent_guid: Agent stable identifier (GUID) from ``FOUNDRY_AGENT_ID``.
:param is_hosted: Whether the agent is running in a Foundry-hosted container environment,
derived from ``FOUNDRY_HOSTING_ENVIRONMENT``.
:param project_endpoint: Foundry project endpoint from ``FOUNDRY_PROJECT_ENDPOINT``.
Expand All @@ -79,6 +81,7 @@ def __init__(
agent_name: str,
agent_version: str,
agent_id: str,
agent_guid: str = "",
is_hosted: bool,
project_endpoint: str,
project_id: str,
Expand All @@ -92,6 +95,7 @@ def __init__(
self.agent_name = agent_name
self.agent_version = agent_version
self.agent_id = agent_id
self.agent_guid = agent_guid
self.is_hosted = is_hosted
self.project_endpoint = project_endpoint
self.project_id = project_id
Expand Down Expand Up @@ -123,6 +127,7 @@ def from_env(cls) -> Self:
agent_name=agent_name,
agent_version=agent_version,
agent_id=agent_id,
agent_guid=os.environ.get(_ENV_FOUNDRY_AGENT_ID, ""),
is_hosted=bool(os.environ.get(_ENV_FOUNDRY_HOSTING_ENVIRONMENT, "")),
project_endpoint=os.environ.get(_ENV_FOUNDRY_PROJECT_ENDPOINT, ""),
project_id=os.environ.get(_ENV_FOUNDRY_PROJECT_ARM_ID, ""),
Expand Down Expand Up @@ -315,6 +320,15 @@ def resolve_agent_id() -> str:
return agent_name


def resolve_agent_guid() -> str:
"""Resolve the agent stable GUID from the ``FOUNDRY_AGENT_ID`` environment variable.

:return: The agent GUID, or an empty string if not set.
:rtype: str
"""
return os.environ.get(_ENV_FOUNDRY_AGENT_ID, "")


def resolve_agent_blueprint_id() -> str:
"""Resolve the agent blueprint client ID from the ``FOUNDRY_AGENT_BLUEPRINT_CLIENT_ID`` environment variable.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Constants:

# Foundry identity
FOUNDRY_AGENT_NAME = "FOUNDRY_AGENT_NAME"
FOUNDRY_AGENT_ID = "FOUNDRY_AGENT_ID"
FOUNDRY_AGENT_VERSION = "FOUNDRY_AGENT_VERSION"
FOUNDRY_PROJECT_ENDPOINT = "FOUNDRY_PROJECT_ENDPOINT"
FOUNDRY_PROJECT_ARM_ID = "FOUNDRY_PROJECT_ARM_ID"
Expand Down
Loading
Loading