Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions docs/specs/004-python-function-calling-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ Code-reading landmarks:
- `_process_model_function_calls(...)` handles only calls from a completed model response.
- `_try_execute_function_calls(...)` decides approval/declaration/execution behavior for a batch.
- `_replace_approval_contents_with_results(...)` is the occurrence-aware approval transcript normalizer.
- `FunctionInvocationLayer._update_function_invocation_continuation_state(...)` updates continuation state after
every service response. Provider layers may override it to carry provider-specific continuation metadata into
the next service call, but must delegate to the base implementation so generic conversation continuation remains
synchronized with the active `AgentSession`.

### Approval pause and resume

Expand Down
53 changes: 34 additions & 19 deletions python/packages/core/agent_framework/_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,38 @@ def _call_chat_client(
client_kwargs=context["client_kwargs"],
)

def _update_session_from_chat_response(
self,
session: AgentSession | None,
response: ChatResponse[Any],
) -> None:
"""Update session continuation state from a chat response."""
if (
session
and response.conversation_id
and not is_local_history_conversation_id(response.conversation_id)
and session.service_session_id != response.conversation_id
):
session.service_session_id = response.conversation_id

def _update_session_from_chat_response_update(
self,
session: AgentSession | None,
update: AgentResponseUpdate,
) -> None:
"""Update session continuation state from a streaming agent update."""
if session is None:
return
raw = update.raw_representation
conversation_id = getattr(raw, "conversation_id", None) if raw else None
if (
isinstance(conversation_id, str)
and conversation_id
and not is_local_history_conversation_id(conversation_id)
and session.service_session_id != conversation_id
):
session.service_session_id = conversation_id

async def _parse_non_streaming_response(
self,
context: _RunContext,
Expand All @@ -1174,13 +1206,7 @@ async def _parse_non_streaming_response(
message.author_name = context["agent_name"]

session = context["session"]
if (
session
and response.conversation_id
and not is_local_history_conversation_id(response.conversation_id)
and session.service_session_id != response.conversation_id
):
session.service_session_id = response.conversation_id
self._update_session_from_chat_response(session, response)

agent_response = _build_agent_response_from_chat_response(
response,
Expand Down Expand Up @@ -1232,18 +1258,7 @@ async def _post_hook(response: AgentResponse) -> None:

def _propagate_conversation_id(update: AgentResponseUpdate) -> AgentResponseUpdate:
"""Eagerly propagate conversation_id to session as updates arrive."""
session = context["session"]
if session is None:
return update
raw = update.raw_representation
conversation_id = getattr(raw, "conversation_id", None) if raw else None
if (
isinstance(conversation_id, str)
and conversation_id
and not is_local_history_conversation_id(conversation_id)
and session.service_session_id != conversation_id
):
session.service_session_id = conversation_id
self._update_session_from_chat_response_update(context["session"], update)
return update

def _suppress_response_id(update: AgentResponseUpdate) -> AgentResponseUpdate:
Expand Down
50 changes: 25 additions & 25 deletions python/packages/core/agent_framework/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1923,27 +1923,6 @@ def _update_conversation_id(
options["conversation_id"] = conversation_id


def _update_continuation_state(
kwargs: dict[str, Any],
response: ChatResponse[Any],
*,
session: AgentSession | None,
options: dict[str, Any] | None = None,
) -> None:
"""Update in-flight and persisted continuation state from a response."""
conversation_id = response.conversation_id
if conversation_id is None:
return

_update_conversation_id(kwargs, conversation_id, options)
if (
session is not None
and not response.has_internal_conversation_id()
and session.service_session_id != conversation_id
):
session.service_session_id = conversation_id


def _clear_internal_conversation_id(response: ChatResponse[Any]) -> ChatResponse[Any]:
if response.has_internal_conversation_id():
response.conversation_id = None
Expand Down Expand Up @@ -2867,6 +2846,27 @@ def __init__(
kwargs["middleware"] = chat_middleware
super().__init__(**kwargs)

def _update_function_invocation_continuation_state(
self,
kwargs: dict[str, Any],
response: ChatResponse[Any],
*,
session: AgentSession | None,
options: dict[str, Any] | None = None,
) -> None:
"""Update continuation state after a function-loop service call."""
conversation_id = response.conversation_id
if conversation_id is None:
return

_update_conversation_id(kwargs, conversation_id, options)
if (
session is not None
and not response.has_internal_conversation_id()
and session.service_session_id != conversation_id
):
session.service_session_id = conversation_id

def _get_function_middleware_pipeline(
self,
runtime_middleware: Sequence[FunctionMiddlewareTypes],
Expand Down Expand Up @@ -2953,7 +2953,7 @@ async def _get_response_with_function_invocation(
):
_ensure_function_invocation_limit_fallback_response(response)
aggregated_usage = add_usage_details(aggregated_usage, response.usage_details)
_update_continuation_state(
self._update_function_invocation_continuation_state(
request_kwargs,
response,
session=invocation_session,
Expand Down Expand Up @@ -3004,7 +3004,7 @@ async def _get_response_with_function_invocation(
)
_ensure_function_invocation_limit_fallback_response(response)
aggregated_usage = add_usage_details(aggregated_usage, response.usage_details)
_update_continuation_state(
self._update_function_invocation_continuation_state(
request_kwargs,
response,
session=invocation_session,
Expand Down Expand Up @@ -3094,7 +3094,7 @@ async def _stream_response_with_function_invocation(
fallback_added = False
if function_call_limit_reached:
fallback_added = _ensure_function_invocation_limit_fallback_response(response)
_update_continuation_state(
self._update_function_invocation_continuation_state(
request_kwargs,
response,
session=invocation_session,
Expand Down Expand Up @@ -3161,7 +3161,7 @@ async def _stream_response_with_function_invocation(
yield update
final_response = await final_inner_stream.get_final_response()
fallback_added = _ensure_function_invocation_limit_fallback_response(final_response)
_update_continuation_state(
self._update_function_invocation_continuation_state(
request_kwargs,
final_response,
session=invocation_session,
Expand Down
1 change: 1 addition & 0 deletions python/packages/core/agent_framework/foundry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"DocumentStatus": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
"FileSearchBackend": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
"FileSearchConfig": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
"FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryAgent": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryAgentOptions": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
Expand Down
2 changes: 2 additions & 0 deletions python/packages/core/agent_framework/foundry/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ from agent_framework_azure_contentunderstanding import (
FileSearchConfig,
)
from agent_framework_foundry import (
FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY,
FoundryAgent,
FoundryChatClient,
FoundryChatOptions,
Expand Down Expand Up @@ -50,6 +51,7 @@ from agent_framework_foundry_local import (
)

__all__ = [
"FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY",
"AgentSessionStoreProvider",
"AnalysisSection",
"AnthropicFoundryClient",
Expand Down
3 changes: 3 additions & 0 deletions python/packages/core/tests/core/test_foundry_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@

FoundryChatClient = _foundry.FoundryChatClient
FoundryMemoryProvider = _foundry.FoundryMemoryProvider
FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY = _foundry.FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY
FoundryAgentSessionStore = _foundry_hosting.FoundryAgentSessionStore
ResponsesHostServer = _foundry_hosting.ResponsesHostServer
FoundryLocalClient = _foundry_local.FoundryLocalClient


def test_foundry_namespace_exposes_cloud_and_local_symbols() -> None:
assert foundry.FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY is FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY
assert foundry.FoundryChatClient is FoundryChatClient
assert foundry.FoundryMemoryProvider is FoundryMemoryProvider
assert foundry.FoundryAgentSessionStore is FoundryAgentSessionStore
assert foundry.ResponsesHostServer is ResponsesHostServer
assert foundry.FoundryLocalClient is FoundryLocalClient
assert "FoundryChatClient" in dir(foundry)
assert "FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY" in dir(foundry)
assert "FoundryLocalClient" in dir(foundry)
assert "FoundryAgentSessionStore" in dir(foundry)
assert "ResponsesHostServer" in dir(foundry)
Expand Down
10 changes: 6 additions & 4 deletions python/packages/foundry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ project conversation and returns an `AgentSession` that can be passed to
`agent.run(...)` without reaching into the raw OpenAI client.

```python
from agent_framework.foundry import FoundryAgent
from agent_framework.foundry import FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY, FoundryAgent

agent = FoundryAgent(
project_endpoint=project_endpoint,
Expand All @@ -126,9 +126,11 @@ session = await agent.create_conversation()
response = await agent.run("Help me plan a trip to Seattle.", session=session)
```

This is separate from hosted-agent `isolation_key` sessions: the created
conversation ID is stored on `AgentSession.service_session_id`, while the local
`session_id` remains available for application/session storage.
For HostedAgents, start with a normal `AgentSession`. When no hosted-agent
session ID is supplied, the service creates one and the agent stores it in
`session.state[FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY]`. The response conversation
ID or response ID remains separate in `session.service_session_id` and is used
as the next request's continuation handle.

## Publishing an agent as a Foundry prompt agent

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import importlib.metadata

from ._agent import FoundryAgent, FoundryAgentOptions, RawFoundryAgent, RawFoundryAgentChatClient
from ._agent import (
FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY,
FoundryAgent,
FoundryAgentOptions,
RawFoundryAgent,
RawFoundryAgentChatClient,
)
from ._chat_client import FoundryChatClient, FoundryChatOptions, RawFoundryChatClient
from ._embedding_client import (
FoundryEmbeddingClient,
Expand All @@ -25,6 +31,7 @@
__version__ = "0.0.0"

__all__ = [
"FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY",
"FoundryAgent",
"FoundryAgentOptions",
"FoundryChatClient",
Expand Down
Loading
Loading