-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Python: Update agentserver to 2.1.0 #7621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,7 +64,6 @@ | |
| from ._feature_usage import FeatureIndex | ||
| from ._request_context import ( | ||
| validate_foundry_request_context, | ||
| validate_path_segment, | ||
| ) | ||
| from ._state_store import ( | ||
| AgentSessionStoreProvider, | ||
|
|
@@ -372,29 +371,10 @@ async def _handle_inner_agent( | |
| try: | ||
| approval_storage = self._function_approval_storage_provider.get_store(config=self.config) | ||
| session_storage = self._session_storage_provider.get_store(config=self.config) | ||
| # Agent sessions are either tied to the conversation_id (for multi-turn conversation mode) | ||
| # or the previous_response_id (for response chaining). If neither is present, a new session | ||
| # is created for this request and stored under the current response_id. The current response_id | ||
| # will become the previous_response_id for the next request in a response chain, allowing the | ||
| # session to be retrieved. | ||
| if (previous_response_id := request.get("previous_response_id")) is not None: | ||
| session = await session_storage.get(previous_response_id) | ||
| if session is None: | ||
| raise RuntimeError( | ||
| f"Cannot find an existing agent session for previous_response_id={previous_response_id}. " | ||
| "Ensure that the previous response was created successfully and that the ID is correct." | ||
| ) | ||
| elif (conversation_id := context.conversation_id) is not None: | ||
| session = await session_storage.get(conversation_id) | ||
| if session is None: | ||
| # Note that we cannot determine if the session was deleted or never existed, | ||
| # so we log a warning and create a new session. | ||
| logger.info( | ||
| "Cannot find an existing agent session for id=%s. Creating a new session.", | ||
| conversation_id, | ||
| ) | ||
| session = self._agent.create_session() | ||
| else: | ||
|
|
||
| context_id = context.conversation_chain_id | ||
| session = await session_storage.get(context_id) | ||
|
Comment on lines
+375
to
+376
|
||
| if session is None: | ||
| session = self._agent.create_session() | ||
| except Exception as ex: | ||
| logger.error("Failed to prepare state storage: %s", ex, exc_info=(type(ex), ex, ex.__traceback__)) | ||
|
|
@@ -456,7 +436,7 @@ async def _handle_inner_agent( | |
| if self._uses_hosted_responses_history: | ||
| session.state.pop(_HOSTED_RESPONSES_HISTORY_SOURCE_ID, None) | ||
| try: | ||
| await session_storage.set(context.conversation_id or context.response_id, session) | ||
| await session_storage.set(context_id, session) | ||
| except Exception as save_error: | ||
| save_failure = save_error | ||
| if request_interrupted: | ||
|
|
@@ -518,42 +498,19 @@ async def _handle_inner_workflow( | |
| # any future async resources owned by the workflow are entered here. | ||
| await self._ensure_agent_ready() | ||
|
|
||
| context_id = context.conversation_chain_id | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new SDK safely hashes malformed context values into opaque IDs, but the three existing checkpoint-context tests still require these inputs to produce |
||
| checkpoint_storage = self._checkpoint_storage_provider.get_store(config=self.config, context_id=context_id) | ||
|
|
||
|
Comment on lines
+501
to
+503
|
||
| # Determine the latest checkpoint (if any) so we can resume the | ||
| # workflow's prior state for this turn. The directory is keyed by | ||
| # the inbound context id (conversation_id when set, otherwise | ||
| # previous_response_id). Multi-turn declarative workflows need the | ||
| # workflow's internal state (e.g. Conversation.messages, | ||
| # the platform derived context_id. Multi-turn declarative workflows | ||
| # need the workflow's internal state (e.g. Conversation.messages, | ||
| # intermediate Local.* variables) to survive across user turns; | ||
| # the only place that state lives is the workflow checkpoint, so | ||
| # on every turn we restore the latest checkpoint and feed the new | ||
| # input back into the start executor as a continuation rather than | ||
| # a fresh run. | ||
| latest_checkpoint_id: str | None = None | ||
| restore_storage: CheckpointStorage | None = None | ||
| if context_id is not None: | ||
| validate_path_segment(context_id, kind="context id") | ||
| restore_storage = self._checkpoint_storage_provider.get_store( | ||
| config=self.config, | ||
| context_id=context_id, | ||
| ) | ||
| latest_checkpoint = await restore_storage.get_latest(workflow_name=self._agent.workflow.name) | ||
| if latest_checkpoint is not None: | ||
| latest_checkpoint_id = latest_checkpoint.checkpoint_id | ||
|
|
||
| # Storage that will receive checkpoints written during this turn. | ||
| # When the caller chains with previous_response_id, the next turn | ||
| # will reference the current response_id as its previous_response_id, | ||
| # so new checkpoints must land under the current response_id (or the | ||
| # conversation_id when set). When conversation_id is set, this | ||
| # matches restore_storage; when only previous_response_id was | ||
| # supplied, restore_storage points at the *prior* response's | ||
| # directory and write_storage points at the *current* response's. | ||
| write_context_id = context.conversation_id or context.response_id | ||
| validate_path_segment(write_context_id, kind="context id") | ||
| write_storage = self._checkpoint_storage_provider.get_store( | ||
| config=self.config, | ||
| context_id=write_context_id, | ||
| ) | ||
| latest_checkpoint = await checkpoint_storage.get_latest(workflow_name=self._agent.workflow.name) | ||
|
|
||
|
|
||
| # Multi-turn pattern: when we have a prior checkpoint, restore it | ||
| # first (drive the workflow back to idle with prior state intact), | ||
|
|
@@ -571,11 +528,11 @@ async def _handle_inner_workflow( | |
| # ``run(input_messages, ...)`` call may contain ``function_call_output`` | ||
| # items (carried as FunctionResult/FunctionApprovalResponse content) | ||
| # that fulfill them via :meth:`WorkflowAgent._process_pending_requests`. | ||
| if latest_checkpoint_id is not None: | ||
| if latest_checkpoint is not None: | ||
| async for _ in self._agent.run( | ||
| stream=True, | ||
| checkpoint_id=latest_checkpoint_id, | ||
| checkpoint_storage=restore_storage, | ||
| checkpoint_id=latest_checkpoint.checkpoint_id, | ||
| checkpoint_storage=checkpoint_storage, | ||
| ): | ||
| pass | ||
|
|
||
|
|
@@ -585,7 +542,7 @@ async def _handle_inner_workflow( | |
| async for update in self._agent.run( | ||
| input_messages, | ||
| stream=True, | ||
| checkpoint_storage=write_storage, | ||
| checkpoint_storage=checkpoint_storage, | ||
| ): | ||
| for content in update.contents: | ||
| for event in tracker.handle(content): | ||
|
|
@@ -600,27 +557,12 @@ async def _handle_inner_workflow( | |
| # Close any remaining active builder | ||
| for event in tracker.close(): | ||
| yield event | ||
|
|
||
| await self._delete_not_latest_checkpoints(write_storage, self._agent.workflow.name) | ||
| yield response_event_stream.emit_completed() | ||
| except Exception as ex: | ||
| logger.exception("Failed to produce response for workflow agent") | ||
| for event in self._emit_failure(response_event_stream, tracker, ex): | ||
| yield event | ||
|
|
||
| @staticmethod | ||
| async def _delete_not_latest_checkpoints(checkpoint_storage: CheckpointStorage, workflow_name: str) -> None: | ||
| """Delete all checkpoints except the latest one. | ||
|
|
||
| We only need the last checkpoint for each invocation. | ||
| """ | ||
| latest_checkpoint = await checkpoint_storage.get_latest(workflow_name=workflow_name) | ||
| if latest_checkpoint is not None: | ||
| all_checkpoints = await checkpoint_storage.list_checkpoints(workflow_name=workflow_name) | ||
| for checkpoint in all_checkpoints: | ||
| if checkpoint.checkpoint_id != latest_checkpoint.checkpoint_id: | ||
| await checkpoint_storage.delete(checkpoint.checkpoint_id) | ||
|
|
||
| @staticmethod | ||
| def _emit_failure( | ||
| response_event_stream: ResponseEventStream, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This key no longer restores regular-agent state for
previous_response_idrequests in the actual endpoint path: the existing generated-ID chain test observes[1, 1, 1]instead of restoring the prior session, and a missing previous response silently creates a fresh session. That breaks the core multi-turn workflow and discards conversation state without notifying the caller. Please ensure the first response and its continuations resolve to the same persisted key, while preserving an error for a genuinely unknown previous response.