agent-framework-core 1.2.1__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/PKG-INFO +1 -1
  2. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_types.py +53 -7
  3. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_agent.py +21 -6
  4. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_agent_executor.py +3 -2
  5. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_runner.py +6 -1
  6. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_workflow.py +58 -31
  7. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_workflow_executor.py +2 -2
  8. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/foundry/__init__.py +9 -0
  9. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/foundry/__init__.pyi +12 -0
  10. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/observability.py +104 -44
  11. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/pyproject.toml +1 -1
  12. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/LICENSE +0 -0
  13. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/README.md +0 -0
  14. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/__init__.py +0 -0
  15. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_agents.py +0 -0
  16. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_clients.py +0 -0
  17. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_compaction.py +0 -0
  18. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_docstrings.py +0 -0
  19. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_evaluation.py +0 -0
  20. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_feature_stage.py +0 -0
  21. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_mcp.py +0 -0
  22. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_middleware.py +0 -0
  23. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_serialization.py +0 -0
  24. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_sessions.py +0 -0
  25. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_settings.py +0 -0
  26. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_skills.py +0 -0
  27. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_telemetry.py +0 -0
  28. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_tools.py +0 -0
  29. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/__init__.py +0 -0
  30. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_agent_utils.py +0 -0
  31. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_checkpoint.py +0 -0
  32. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_checkpoint_encoding.py +0 -0
  33. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_const.py +0 -0
  34. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_conversation_history.py +0 -0
  35. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_edge.py +0 -0
  36. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_edge_runner.py +0 -0
  37. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_events.py +0 -0
  38. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_executor.py +0 -0
  39. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_function_executor.py +0 -0
  40. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_functional.py +0 -0
  41. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_message_utils.py +0 -0
  42. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_model_utils.py +0 -0
  43. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_request_info_mixin.py +0 -0
  44. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_runner_context.py +0 -0
  45. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_state.py +0 -0
  46. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_typing_utils.py +0 -0
  47. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_validation.py +0 -0
  48. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_viz.py +0 -0
  49. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_workflow_builder.py +0 -0
  50. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/_workflows/_workflow_context.py +0 -0
  51. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/a2a/__init__.py +0 -0
  52. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/a2a/__init__.pyi +0 -0
  53. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/ag_ui/__init__.py +0 -0
  54. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/ag_ui/__init__.pyi +0 -0
  55. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/amazon/__init__.py +0 -0
  56. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/amazon/__init__.pyi +0 -0
  57. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/anthropic/__init__.py +0 -0
  58. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/anthropic/__init__.pyi +0 -0
  59. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/azure/__init__.py +0 -0
  60. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/azure/__init__.pyi +0 -0
  61. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/chatkit/__init__.py +0 -0
  62. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/chatkit/__init__.pyi +0 -0
  63. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/declarative/__init__.py +0 -0
  64. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/declarative/__init__.pyi +0 -0
  65. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/devui/__init__.py +0 -0
  66. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/devui/__init__.pyi +0 -0
  67. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/exceptions.py +0 -0
  68. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/github/__init__.py +0 -0
  69. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/github/__init__.pyi +0 -0
  70. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/google/__init__.py +0 -0
  71. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/google/__init__.pyi +0 -0
  72. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/lab/__init__.py +0 -0
  73. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/mem0/__init__.py +0 -0
  74. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/mem0/__init__.pyi +0 -0
  75. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/microsoft/__init__.py +0 -0
  76. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/microsoft/__init__.pyi +0 -0
  77. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/ollama/__init__.py +0 -0
  78. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/ollama/__init__.pyi +0 -0
  79. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/openai/__init__.py +0 -0
  80. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/openai/__init__.pyi +0 -0
  81. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/orchestrations/__init__.py +0 -0
  82. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/orchestrations/__init__.pyi +0 -0
  83. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/py.typed +0 -0
  84. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/redis/__init__.py +0 -0
  85. {agent_framework_core-1.2.1 → agent_framework_core-1.2.2}/agent_framework/redis/__init__.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-core
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Microsoft Agent Framework for building AI Agents with Python. This is the core package that has all the core abstractions and implementations.
5
5
  Author-email: Microsoft <af-support@microsoft.com>
6
6
  Requires-Python: >=3.10
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import base64
6
+ import contextlib
6
7
  import json
7
8
  import logging
8
9
  import re
@@ -2890,6 +2891,7 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
2890
2891
  self._inner_stream_source: ResponseStream[Any, Any] | Awaitable[ResponseStream[Any, Any]] | None = None
2891
2892
  self._wrap_inner: bool = False
2892
2893
  self._map_update: Callable[[Any], UpdateT | Awaitable[UpdateT]] | None = None
2894
+ self._pull_context_manager_factories: list[Callable[[], contextlib.AbstractContextManager[Any]]] = []
2893
2895
 
2894
2896
  def map(
2895
2897
  self,
@@ -3008,11 +3010,18 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
3008
3010
  return self
3009
3011
 
3010
3012
  async def __anext__(self) -> UpdateT:
3011
- if self._iterator is None:
3012
- stream = await self._get_stream()
3013
- self._iterator = stream.__aiter__()
3014
3013
  try:
3015
- update: UpdateT = await self._iterator.__anext__()
3014
+ with contextlib.ExitStack() as stack:
3015
+ for factory in self._pull_context_manager_factories:
3016
+ stack.enter_context(factory())
3017
+ # Resolve the underlying stream inside the pull contexts so that any
3018
+ # spans/contexts created during stream resolution (e.g. inner chat
3019
+ # completion spans created on the first pull of a wrapped agent stream)
3020
+ # inherit the active context (e.g. an outer agent invoke span).
3021
+ if self._iterator is None:
3022
+ stream = await self._get_stream()
3023
+ self._iterator = stream.__aiter__()
3024
+ update: UpdateT = await self._iterator.__anext__()
3016
3025
  except StopAsyncIteration:
3017
3026
  self._consumed = True
3018
3027
  await self._run_cleanup_hooks()
@@ -3038,9 +3047,25 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
3038
3047
  update = hooked
3039
3048
  return update
3040
3049
 
3050
+ async def _resolve_stream_with_pull_contexts(self) -> AsyncIterable[UpdateT]:
3051
+ """Resolve the underlying stream while activating any registered pull context managers.
3052
+
3053
+ Used by ``__await__`` and ``get_final_response`` so that any spans/contexts created
3054
+ during stream resolution (e.g. when the source is an Awaitable that internally
3055
+ creates child telemetry spans) inherit the same active context as iterator pulls.
3056
+ ``__anext__`` resolves the stream inside its own ExitStack and so calls ``_get_stream``
3057
+ directly.
3058
+ """
3059
+ if self._stream is not None:
3060
+ return await self._get_stream()
3061
+ with contextlib.ExitStack() as stack:
3062
+ for factory in self._pull_context_manager_factories:
3063
+ stack.enter_context(factory())
3064
+ return await self._get_stream()
3065
+
3041
3066
  def __await__(self) -> Any:
3042
3067
  async def _wrap() -> ResponseStream[UpdateT, FinalT]:
3043
- await self._get_stream()
3068
+ await self._resolve_stream_with_pull_contexts()
3044
3069
  return self
3045
3070
 
3046
3071
  return _wrap().__await__()
@@ -3064,10 +3089,12 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
3064
3089
  """
3065
3090
  if self._wrap_inner:
3066
3091
  if self._inner_stream is None:
3067
- # Use _get_stream() to resolve the awaitable - this properly handles
3092
+ # Use _resolve_stream_with_pull_contexts() so that any spans/contexts
3093
+ # created while resolving the awaitable (e.g. inner telemetry spans)
3094
+ # inherit the same active context as iterator pulls. This also handles
3068
3095
  # the case where _stream_source and _inner_stream_source are the same
3069
3096
  # coroutine (e.g., from from_awaitable), avoiding double-await errors.
3070
- await self._get_stream()
3097
+ await self._resolve_stream_with_pull_contexts()
3071
3098
  if self._inner_stream is None:
3072
3099
  raise RuntimeError("Inner stream not available")
3073
3100
  if not self._finalized and not self._consumed:
@@ -3177,6 +3204,25 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
3177
3204
  self._cleanup_hooks.append(hook)
3178
3205
  return self
3179
3206
 
3207
+ def with_pull_context_manager(
3208
+ self,
3209
+ cm_factory: Callable[[], contextlib.AbstractContextManager[Any]],
3210
+ ) -> ResponseStream[UpdateT, FinalT]:
3211
+ """Register a context manager factory invoked around each underlying iterator pull.
3212
+
3213
+ The factory is called once per ``__anext__`` and the returned context manager wraps
3214
+ the await of the underlying iterator. This is useful for state that needs to be
3215
+ active while the inner async work runs - for example, attaching an OpenTelemetry
3216
+ span to the current context so child spans created by inner code (HTTP clients,
3217
+ tool execution) are correctly parented.
3218
+
3219
+ Because the context manager is entered and exited within the same ``__anext__``
3220
+ invocation, attach/detach style operations remain symmetric in the same async
3221
+ context regardless of where the stream is iterated.
3222
+ """
3223
+ self._pull_context_manager_factories.append(cm_factory)
3224
+ return self
3225
+
3180
3226
  async def _run_cleanup_hooks(self) -> None:
3181
3227
  if self._cleanup_run:
3182
3228
  return
@@ -437,6 +437,13 @@ class WorkflowAgent(BaseAgent):
437
437
  yield event
438
438
 
439
439
  elif checkpoint_id is not None:
440
+ # Restore the prior workflow state from the checkpoint. Shared
441
+ # state (e.g. accumulated conversation history maintained by the
442
+ # workflow's executors) survives across turns because Workflow.run
443
+ # no longer wipes state per call. Callers who want to deliver a
444
+ # new user message after restore should make a second
445
+ # `workflow.run(message=...)` call - they are NOT mutually
446
+ # exclusive on the same instance, but each must be its own call.
440
447
  if streaming:
441
448
  async for event in self.workflow.run(
442
449
  stream=True,
@@ -528,6 +535,7 @@ class WorkflowAgent(BaseAgent):
528
535
  raw_representations.append(output_event)
529
536
  else:
530
537
  data = output_event.data
538
+
531
539
  if isinstance(data, AgentResponseUpdate):
532
540
  # We cannot support AgentResponseUpdate in non-streaming mode. This is because the message
533
541
  # sequence cannot be guaranteed when there are streaming updates in between non-streaming
@@ -628,16 +636,23 @@ class WorkflowAgent(BaseAgent):
628
636
  A list of AgentResponseUpdate objects. Empty list if the event is not relevant.
629
637
  """
630
638
  if event.type == "output":
631
- # Convert workflow output to agent response updates.
632
- # Handle different data types appropriately.
633
639
  data = event.data
634
640
  executor_id = event.executor_id
635
641
 
636
642
  if isinstance(data, AgentResponseUpdate):
637
- # Pass through AgentResponseUpdate directly (streaming from AgentExecutor)
638
- if not data.author_name:
639
- data.author_name = executor_id
640
- return [data]
643
+ # Construct a fresh AgentResponseUpdate so we don't mutate a payload
644
+ # that AgentExecutor still holds a reference to in its `updates` list.
645
+ return [
646
+ AgentResponseUpdate(
647
+ contents=list(data.contents),
648
+ role=data.role,
649
+ author_name=data.author_name or executor_id,
650
+ response_id=data.response_id,
651
+ message_id=data.message_id,
652
+ created_at=data.created_at,
653
+ raw_representation=data.raw_representation,
654
+ )
655
+ ]
641
656
  if isinstance(data, AgentResponse):
642
657
  # Convert each message in AgentResponse to an AgentResponseUpdate
643
658
  updates: list[AgentResponseUpdate] = []
@@ -156,8 +156,9 @@ class AgentExecutor(Executor):
156
156
  the agent run.
157
157
  - "custom": use the provided context_filter function to determine which messages to include
158
158
  as context for the agent run.
159
- context_filter: An optional function for filtering conversation context when context_mode is set
160
- to "custom".
159
+ context_filter: A function that takes the full conversation (list of Messages) as input and returns
160
+ a filtered list of Messages to be used as context for the agent run. This is required
161
+ if context_mode is set to "custom".
161
162
  """
162
163
  # Prefer provided id; else use agent.name if present; else generate deterministic prefix
163
164
  exec_id = id or resolve_agent_id(agent)
@@ -278,7 +278,12 @@ class Runner:
278
278
  "Please rebuild the original workflow before resuming."
279
279
  )
280
280
 
281
- # Restore state
281
+ # Restore state. Clear first so import_state (which merges) does
282
+ # not leak stale keys from a prior run on this Workflow instance.
283
+ # This matters more now that Workflow.run() no longer wipes state
284
+ # per call - the only reset point for shared state on a reused
285
+ # instance is at restore time.
286
+ self._state.clear()
282
287
  self._state.import_state(checkpoint.state)
283
288
  # Restore executor states using the restored state
284
289
  await self._restore_executor_states()
@@ -299,7 +299,7 @@ class Workflow(DictConvertible):
299
299
  async def _run_workflow_with_tracing(
300
300
  self,
301
301
  initial_executor_fn: Callable[[], Awaitable[None]] | None = None,
302
- reset_context: bool = True,
302
+ is_continuation: bool = False,
303
303
  streaming: bool = False,
304
304
  function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
305
305
  client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
@@ -310,13 +310,19 @@ class Workflow(DictConvertible):
310
310
  of external callers to maintain context across different workflow runs.
311
311
 
312
312
  Args:
313
- initial_executor_fn: Optional function to execute initial executor
314
- reset_context: Whether to reset the context for a new run
315
- streaming: Whether to enable streaming mode for agents
313
+ initial_executor_fn: Optional function to execute initial executor.
314
+ is_continuation: True when this run is a continuation of prior
315
+ work (a checkpoint restore or a responses-only replay) rather
316
+ than a fresh new turn delivered via the start executor with
317
+ ``message=...``. Continuations preserve per-run accounting
318
+ (iteration counter and run kwargs) from the prior turn;
319
+ fresh-message runs reset them. Shared workflow state is
320
+ preserved in both cases.
321
+ streaming: Whether to enable streaming mode for agents.
316
322
  function_invocation_kwargs: Optional kwargs to store in State for function
317
- invocations in subagents
323
+ invocations in subagents.
318
324
  client_kwargs: Optional kwargs to store in State for chat client
319
- invocations in subagents
325
+ invocations in subagents.
320
326
 
321
327
  Yields:
322
328
  WorkflowEvent: The events generated during the workflow execution.
@@ -345,16 +351,26 @@ class Workflow(DictConvertible):
345
351
  in_progress = WorkflowEvent.status(WorkflowRunState.IN_PROGRESS)
346
352
  yield in_progress # noqa: RUF070
347
353
 
348
- # Reset context for a new run if supported
349
- if reset_context:
354
+ # Per-run reset for fresh-message runs only. We deliberately
355
+ # do NOT clear shared workflow state (`_state.clear()`) or the
356
+ # runner context's in-flight messages (`reset_for_new_run()`)
357
+ # here - state and pending work persist across `run()` calls
358
+ # so that a `WorkflowAgent` can deliver multi-turn input on
359
+ # the same instance and have prior turns' context survive.
360
+ # Iteration counting and per-run kwargs ARE per-run though,
361
+ # so they're reset here.
362
+ if not is_continuation:
350
363
  self._runner.reset_iteration_count()
351
- self._runner.context.reset_for_new_run()
352
- self._state.clear()
353
364
 
354
365
  # Store run kwargs in State so executors can access them.
355
- # Only overwrite when new kwargs are explicitly provided or state was
356
- # just cleared (fresh run). On continuation (reset_context=False) with
357
- # no new kwargs, preserve the kwargs from the original run.
366
+ # Per-run kwargs semantics:
367
+ # - On a fresh message run, prior kwargs go away (set to {}
368
+ # by default, or to the new kwargs if provided). This
369
+ # prevents stale kwargs from a prior turn leaking into the
370
+ # current turn.
371
+ # - On a continuation (checkpoint restore or responses), the
372
+ # prior run's kwargs are preserved unless the caller
373
+ # explicitly provides new kwargs.
358
374
  if function_invocation_kwargs is not None or client_kwargs is not None:
359
375
  combined_kwargs: dict[str, Any] = {}
360
376
  if function_invocation_kwargs is not None:
@@ -366,11 +382,12 @@ class Workflow(DictConvertible):
366
382
  client_kwargs, "client_kwargs"
367
383
  )
368
384
  self._state.set(WORKFLOW_RUN_KWARGS_KEY, combined_kwargs)
369
- elif reset_context:
385
+ elif not is_continuation:
370
386
  self._state.set(WORKFLOW_RUN_KWARGS_KEY, {})
371
387
  self._state.commit() # Commit immediately so kwargs are available
372
388
 
373
- # Set streaming mode after reset
389
+ # Set streaming mode (always set explicitly per run since
390
+ # reset_for_new_run() no longer runs to clear it).
374
391
  self._runner_context.set_streaming(streaming)
375
392
 
376
393
  # Execute initial setup if provided
@@ -585,13 +602,31 @@ class Workflow(DictConvertible):
585
602
  if checkpoint_storage is not None:
586
603
  self._runner.context.set_runtime_checkpoint_storage(checkpoint_storage)
587
604
 
588
- initial_executor_fn, reset_context = self._resolve_execution_mode(
589
- message, responses, checkpoint_id, checkpoint_storage
590
- )
605
+ # Async validation: a fresh-message run is only allowed when the
606
+ # runner context has fully drained from any prior run. If it still
607
+ # has in-flight executor messages, the prior run didn't complete -
608
+ # the caller must either resume from a checkpoint or wait for the
609
+ # prior run to drain. (Pending request_info events are intentionally
610
+ # NOT blocked here: a follow-up run with message=... is the normal
611
+ # way to deliver a response to those pending requests, e.g. via
612
+ # WorkflowAgent._process_pending_requests.)
613
+ # NOTE: _validate_run_params already enforces that ``message`` is
614
+ # mutually exclusive with both ``checkpoint_id`` and ``responses``,
615
+ # so we don't need to re-check those here.
616
+ if message is not None and await self._runner.context.has_messages():
617
+ raise RuntimeError(
618
+ "Cannot start a new run with 'message' while in-flight executor "
619
+ "messages remain from a prior run. Resume from a checkpoint "
620
+ "(checkpoint_id=...) or wait for the prior run to complete. "
621
+ "Workflows that need to recover from a mid-run failure must use "
622
+ "checkpointing; there is no in-process recovery path."
623
+ )
624
+
625
+ initial_executor_fn = self._resolve_execution_mode(message, responses, checkpoint_id, checkpoint_storage)
591
626
 
592
627
  async for event in self._run_workflow_with_tracing(
593
628
  initial_executor_fn=initial_executor_fn,
594
- reset_context=reset_context,
629
+ is_continuation=(message is None),
595
630
  streaming=streaming,
596
631
  function_invocation_kwargs=function_invocation_kwargs,
597
632
  client_kwargs=client_kwargs,
@@ -674,12 +709,8 @@ class Workflow(DictConvertible):
674
709
  responses: Mapping[str, Any] | None,
675
710
  checkpoint_id: str | None,
676
711
  checkpoint_storage: CheckpointStorage | None,
677
- ) -> tuple[Callable[[], Awaitable[None]], bool]:
678
- """Determine the initial executor function and reset_context flag based on parameters.
679
-
680
- Returns:
681
- A tuple of (initial_executor_fn, reset_context).
682
- """
712
+ ) -> Callable[[], Awaitable[None]]:
713
+ """Determine the initial executor function based on parameters."""
683
714
  if responses is not None:
684
715
  if checkpoint_id is not None:
685
716
  # Combined: restore checkpoint then send responses
@@ -689,13 +720,9 @@ class Workflow(DictConvertible):
689
720
  else:
690
721
  # Send responses only (requires pending requests in workflow state)
691
722
  initial_executor_fn = functools.partial(self._send_responses_internal, responses)
692
- return initial_executor_fn, False
723
+ return initial_executor_fn
693
724
  # Regular run or checkpoint restoration
694
- initial_executor_fn = functools.partial(
695
- self._execute_with_message_or_checkpoint, message, checkpoint_id, checkpoint_storage
696
- )
697
- reset_context = message is not None and checkpoint_id is None
698
- return initial_executor_fn, reset_context
725
+ return functools.partial(self._execute_with_message_or_checkpoint, message, checkpoint_id, checkpoint_storage)
699
726
 
700
727
  async def _restore_and_send_responses(
701
728
  self,
@@ -361,7 +361,7 @@ class WorkflowExecutor(Executor):
361
361
  return any(is_instance_of(message.data, input_type) for input_type in self.workflow.input_types)
362
362
 
363
363
  @handler
364
- async def process_workflow(self, input_data: object, ctx: WorkflowContext[Any]) -> None:
364
+ async def process_workflow(self, input_data: object, ctx: WorkflowContext[Any, Any]) -> None:
365
365
  """Execute the sub-workflow with raw input data.
366
366
 
367
367
  This handler starts a new sub-workflow execution. When the sub-workflow
@@ -428,7 +428,7 @@ class WorkflowExecutor(Executor):
428
428
  async def handle_message_wrapped_request_response(
429
429
  self,
430
430
  response: SubWorkflowResponseMessage,
431
- ctx: WorkflowContext[Any],
431
+ ctx: WorkflowContext[Any, Any],
432
432
  ) -> None:
433
433
  """Handle response from parent for a forwarded request.
434
434
 
@@ -4,6 +4,7 @@
4
4
 
5
5
  This module lazily re-exports objects from:
6
6
  - ``agent-framework-anthropic``
7
+ - ``agent-framework-azure-contentunderstanding``
7
8
  - ``agent-framework-foundry``
8
9
  - ``agent-framework-foundry-local``
9
10
  """
@@ -12,7 +13,15 @@ import importlib
12
13
  from typing import Any
13
14
 
14
15
  _IMPORTS: dict[str, tuple[str, str]] = {
16
+ "AnalysisSection": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
15
17
  "AnthropicFoundryClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
18
+ "ContentUnderstandingContextProvider": (
19
+ "agent_framework_azure_contentunderstanding",
20
+ "agent-framework-azure-contentunderstanding",
21
+ ),
22
+ "DocumentStatus": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
23
+ "FileSearchBackend": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
24
+ "FileSearchConfig": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
16
25
  "FoundryAgent": ("agent_framework_foundry", "agent-framework-foundry"),
17
26
  "FoundryAgentOptions": ("agent_framework_foundry", "agent-framework-foundry"),
18
27
  "FoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
@@ -4,6 +4,13 @@
4
4
  # Install the relevant packages for full type support.
5
5
 
6
6
  from agent_framework_anthropic import AnthropicFoundryClient, RawAnthropicFoundryClient
7
+ from agent_framework_azure_contentunderstanding import ( # pyright: ignore[reportMissingImports]
8
+ AnalysisSection, # pyright: ignore[reportUnknownVariableType]
9
+ ContentUnderstandingContextProvider, # pyright: ignore[reportUnknownVariableType]
10
+ DocumentStatus, # pyright: ignore[reportUnknownVariableType]
11
+ FileSearchBackend, # pyright: ignore[reportUnknownVariableType]
12
+ FileSearchConfig, # pyright: ignore[reportUnknownVariableType]
13
+ )
7
14
  from agent_framework_foundry import (
8
15
  FoundryAgent,
9
16
  FoundryChatClient,
@@ -31,7 +38,12 @@ from agent_framework_foundry_local import (
31
38
  )
32
39
 
33
40
  __all__ = [
41
+ "AnalysisSection",
34
42
  "AnthropicFoundryClient",
43
+ "ContentUnderstandingContextProvider",
44
+ "DocumentStatus",
45
+ "FileSearchBackend",
46
+ "FileSearchConfig",
35
47
  "FoundryAgent",
36
48
  "FoundryChatClient",
37
49
  "FoundryChatOptions",
@@ -26,6 +26,7 @@ from time import perf_counter, time_ns
26
26
  from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, TypedDict, cast, overload
27
27
 
28
28
  from dotenv import load_dotenv
29
+ from opentelemetry import context as otel_context
29
30
  from opentelemetry import metrics, trace
30
31
 
31
32
  from . import __version__ as version_info
@@ -1277,27 +1278,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
1277
1278
  )
1278
1279
 
1279
1280
  if stream:
1280
- result_stream = cast(
1281
- ResponseStream[ChatResponseUpdate, ChatResponse[Any]],
1282
- super_get_response(
1283
- messages=messages,
1284
- stream=True,
1285
- options=opts,
1286
- compaction_strategy=compaction_strategy,
1287
- tokenizer=tokenizer,
1288
- function_invocation_kwargs=function_invocation_kwargs,
1289
- client_kwargs=merged_client_kwargs,
1290
- ),
1291
- )
1281
+ span = _start_streaming_span(attributes, OtelAttr.REQUEST_MODEL)
1292
1282
 
1293
- # Create span directly without trace.use_span() context attachment.
1294
- # Streaming spans are closed asynchronously in cleanup hooks, which run
1295
- # in a different async context than creation — using use_span() would
1296
- # cause "Failed to detach context" errors from OpenTelemetry.
1297
- operation = attributes.get(OtelAttr.OPERATION, "operation")
1298
- span_name = attributes.get(OtelAttr.REQUEST_MODEL, "unknown")
1299
- span = get_tracer().start_span(f"{operation} {span_name}")
1300
- span.set_attributes(attributes)
1301
1283
  if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages:
1302
1284
  _capture_messages(
1303
1285
  span=span,
@@ -1319,6 +1301,24 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
1319
1301
  def _record_duration() -> None:
1320
1302
  duration_state["duration"] = perf_counter() - start_time
1321
1303
 
1304
+ try:
1305
+ result_stream = cast(
1306
+ ResponseStream[ChatResponseUpdate, ChatResponse[Any]],
1307
+ super_get_response(
1308
+ messages=messages,
1309
+ stream=True,
1310
+ options=opts,
1311
+ compaction_strategy=compaction_strategy,
1312
+ tokenizer=tokenizer,
1313
+ function_invocation_kwargs=function_invocation_kwargs,
1314
+ client_kwargs=merged_client_kwargs,
1315
+ ),
1316
+ )
1317
+ except Exception as exception:
1318
+ capture_exception(span=span, exception=exception, timestamp=time_ns())
1319
+ _close_span()
1320
+ raise
1321
+
1322
1322
  async def _finalize_stream() -> None:
1323
1323
  from ._types import ChatResponse
1324
1324
 
@@ -1357,11 +1357,18 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
1357
1357
  finally:
1358
1358
  _close_span()
1359
1359
 
1360
- # Register a weak reference callback to close the span if stream is garbage collected
1361
- # without being consumed. This ensures spans don't leak if users don't consume streams.
1362
- wrapped_stream: ResponseStream[ChatResponseUpdate, ChatResponse[Any]] = result_stream.with_cleanup_hook(
1363
- _record_duration
1364
- ).with_cleanup_hook(_finalize_stream)
1360
+ # The pull context manager attaches the span around each underlying iterator pull so
1361
+ # that child spans created during the pull (e.g. HTTP requests, inner tool execution)
1362
+ # are parented under this chat span. Attach and detach happen in the same async
1363
+ # context as the pull, avoiding cross-context cleanup issues. The weakref finalizer
1364
+ # ensures the span is closed even if the stream is garbage collected without being
1365
+ # consumed.
1366
+ wrapped_stream: ResponseStream[ChatResponseUpdate, ChatResponse[Any]] = (
1367
+ result_stream
1368
+ .with_cleanup_hook(_record_duration)
1369
+ .with_cleanup_hook(_finalize_stream)
1370
+ .with_pull_context_manager(lambda: _activate_span(span))
1371
+ )
1365
1372
  weakref.finalize(wrapped_stream, _close_span)
1366
1373
  return wrapped_stream
1367
1374
 
@@ -1543,23 +1550,8 @@ class AgentTelemetryLayer:
1543
1550
  inner_accumulated_usage_token = INNER_ACCUMULATED_USAGE.set({})
1544
1551
 
1545
1552
  if stream:
1546
- try:
1547
- run_result: object = execute()
1548
- if isinstance(run_result, ResponseStream):
1549
- result_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = run_result # pyright: ignore[reportUnknownVariableType]
1550
- elif isinstance(run_result, Awaitable):
1551
- result_stream = ResponseStream.from_awaitable(run_result) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
1552
- else:
1553
- raise RuntimeError("Streaming telemetry requires a ResponseStream result.")
1554
- except Exception:
1555
- INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS.reset(inner_response_telemetry_captured_fields_token)
1556
- INNER_ACCUMULATED_USAGE.reset(inner_accumulated_usage_token)
1557
- raise
1553
+ span = _start_streaming_span(attributes, OtelAttr.AGENT_NAME)
1558
1554
 
1559
- operation = attributes.get(OtelAttr.OPERATION, "operation")
1560
- span_name = attributes.get(OtelAttr.AGENT_NAME, "unknown")
1561
- span = get_tracer().start_span(f"{operation} {span_name}")
1562
- span.set_attributes(attributes)
1563
1555
  if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages:
1564
1556
  _capture_messages(
1565
1557
  span=span,
@@ -1581,6 +1573,21 @@ class AgentTelemetryLayer:
1581
1573
  def _record_duration() -> None:
1582
1574
  duration_state["duration"] = perf_counter() - start_time
1583
1575
 
1576
+ try:
1577
+ run_result: object = execute()
1578
+ if isinstance(run_result, ResponseStream):
1579
+ result_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = run_result # pyright: ignore[reportUnknownVariableType]
1580
+ elif isinstance(run_result, Awaitable):
1581
+ result_stream = ResponseStream.from_awaitable(run_result) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
1582
+ else:
1583
+ raise RuntimeError("Streaming telemetry requires a ResponseStream result.")
1584
+ except Exception as exception:
1585
+ capture_exception(span=span, exception=exception, timestamp=time_ns())
1586
+ INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS.reset(inner_response_telemetry_captured_fields_token)
1587
+ INNER_ACCUMULATED_USAGE.reset(inner_accumulated_usage_token)
1588
+ _close_span()
1589
+ raise
1590
+
1584
1591
  async def _finalize_stream() -> None:
1585
1592
  from ._types import AgentResponse
1586
1593
 
@@ -1620,9 +1627,18 @@ class AgentTelemetryLayer:
1620
1627
  INNER_ACCUMULATED_USAGE.reset(inner_accumulated_usage_token)
1621
1628
  _close_span()
1622
1629
 
1623
- wrapped_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = result_stream.with_cleanup_hook(
1624
- _record_duration
1625
- ).with_cleanup_hook(_finalize_stream)
1630
+ # The pull context manager attaches the span around each underlying iterator pull so
1631
+ # that child spans created during the pull (e.g. inner chat completion spans from the
1632
+ # underlying ChatTelemetryLayer) are parented under this agent invoke span. Attach and
1633
+ # detach happen in the same async context as the pull, avoiding cross-context cleanup
1634
+ # issues. The weakref finalizer ensures the span is closed even if the stream is
1635
+ # garbage collected without being consumed.
1636
+ wrapped_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = (
1637
+ result_stream
1638
+ .with_cleanup_hook(_record_duration)
1639
+ .with_cleanup_hook(_finalize_stream)
1640
+ .with_pull_context_manager(lambda: _activate_span(span))
1641
+ )
1626
1642
  weakref.finalize(wrapped_stream, _close_span)
1627
1643
  return wrapped_stream
1628
1644
 
@@ -1809,6 +1825,27 @@ def get_function_span(
1809
1825
  )
1810
1826
 
1811
1827
 
1828
+ @contextlib.contextmanager
1829
+ def _activate_span(span: trace.Span) -> Generator[None]:
1830
+ """Attach ``span`` as the current span in the OpenTelemetry context.
1831
+
1832
+ Designed to be used as a per-pull context manager registered on a
1833
+ ``ResponseStream`` via ``with_pull_context_manager``: it attaches the span
1834
+ before each underlying iterator pull and detaches immediately after, so
1835
+ child spans created during the pull (HTTP clients, inner chat completions,
1836
+ tool execution) are correctly parented under ``span``.
1837
+
1838
+ Because attach and detach happen within the same ``__anext__`` invocation
1839
+ (and therefore the same async task / contextvars context), there is no risk
1840
+ of "Failed to detach context" warnings from cross-context cleanup.
1841
+ """
1842
+ token = otel_context.attach(trace.set_span_in_context(span))
1843
+ try:
1844
+ yield
1845
+ finally:
1846
+ otel_context.detach(token)
1847
+
1848
+
1812
1849
  @contextlib.contextmanager
1813
1850
  def _get_span(
1814
1851
  attributes: dict[str, Any],
@@ -1831,6 +1868,29 @@ def _get_span(
1831
1868
  yield current_span
1832
1869
 
1833
1870
 
1871
+ def _start_streaming_span(attributes: dict[str, Any], span_name_attribute: str) -> trace.Span:
1872
+ """Start a non-current span for a streaming operation.
1873
+
1874
+ Unlike :func:`_get_span`, the returned span is not attached to the current
1875
+ OpenTelemetry context. The caller is responsible for:
1876
+
1877
+ - Ending the span via cleanup hooks on the wrapped
1878
+ :class:`~agent_framework._types.ResponseStream`.
1879
+ - Activating the span around each iterator pull via
1880
+ :func:`_activate_span` registered with ``with_pull_context_manager`` so
1881
+ that child spans created during stream production inherit it as parent.
1882
+
1883
+ Streaming spans are closed asynchronously in cleanup hooks that run in a
1884
+ different async context than creation, so attaching the span at creation
1885
+ time would cause "Failed to detach context" errors from OpenTelemetry.
1886
+ """
1887
+ operation = attributes.get(OtelAttr.OPERATION, "operation")
1888
+ span_name = attributes.get(span_name_attribute, "unknown")
1889
+ span = get_tracer().start_span(f"{operation} {span_name}")
1890
+ span.set_attributes(attributes)
1891
+ return span
1892
+
1893
+
1834
1894
  def _get_instructions_from_options(options: Any) -> str | list[str] | None:
1835
1895
  """Extract instructions from options dict."""
1836
1896
  if options is None:
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
4
4
  authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
- version = "1.2.1"
7
+ version = "1.2.2"
8
8
  license-files = ["LICENSE"]
9
9
  urls.homepage = "https://aka.ms/agent-framework"
10
10
  urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"