agent-framework-core 1.1.0__tar.gz → 1.2.0__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 (86) hide show
  1. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/PKG-INFO +1 -1
  2. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/__init__.py +18 -0
  3. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_evaluation.py +19 -1
  4. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_feature_stage.py +1 -0
  5. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_telemetry.py +57 -22
  6. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_tools.py +88 -12
  7. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_events.py +7 -0
  8. agent_framework_core-1.2.0/agent_framework/_workflows/_functional.py +1551 -0
  9. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_workflow.py +5 -5
  10. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/a2a/__init__.py +2 -1
  11. agent_framework_core-1.2.0/agent_framework/a2a/__init__.pyi +5 -0
  12. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/foundry/__init__.py +1 -0
  13. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/github/__init__.py +2 -0
  14. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/github/__init__.pyi +2 -0
  15. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/pyproject.toml +1 -1
  16. agent_framework_core-1.1.0/agent_framework/a2a/__init__.pyi +0 -9
  17. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/LICENSE +0 -0
  18. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/README.md +0 -0
  19. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_agents.py +0 -0
  20. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_clients.py +0 -0
  21. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_compaction.py +0 -0
  22. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_docstrings.py +0 -0
  23. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_mcp.py +0 -0
  24. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_middleware.py +0 -0
  25. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_serialization.py +0 -0
  26. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_sessions.py +0 -0
  27. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_settings.py +0 -0
  28. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_skills.py +0 -0
  29. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_types.py +0 -0
  30. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/__init__.py +0 -0
  31. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_agent.py +0 -0
  32. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_agent_executor.py +0 -0
  33. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_agent_utils.py +0 -0
  34. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_checkpoint.py +0 -0
  35. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_checkpoint_encoding.py +0 -0
  36. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_const.py +0 -0
  37. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_conversation_history.py +0 -0
  38. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_edge.py +0 -0
  39. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_edge_runner.py +0 -0
  40. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_executor.py +0 -0
  41. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_function_executor.py +0 -0
  42. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_message_utils.py +0 -0
  43. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_model_utils.py +0 -0
  44. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_request_info_mixin.py +0 -0
  45. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_runner.py +0 -0
  46. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_runner_context.py +0 -0
  47. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_state.py +0 -0
  48. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_typing_utils.py +0 -0
  49. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_validation.py +0 -0
  50. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_viz.py +0 -0
  51. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_workflow_builder.py +0 -0
  52. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_workflow_context.py +0 -0
  53. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/_workflows/_workflow_executor.py +0 -0
  54. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/ag_ui/__init__.py +0 -0
  55. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/ag_ui/__init__.pyi +0 -0
  56. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/amazon/__init__.py +0 -0
  57. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/amazon/__init__.pyi +0 -0
  58. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/anthropic/__init__.py +0 -0
  59. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/anthropic/__init__.pyi +0 -0
  60. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/azure/__init__.py +0 -0
  61. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/azure/__init__.pyi +0 -0
  62. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/chatkit/__init__.py +0 -0
  63. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/chatkit/__init__.pyi +0 -0
  64. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/declarative/__init__.py +0 -0
  65. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/declarative/__init__.pyi +0 -0
  66. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/devui/__init__.py +0 -0
  67. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/devui/__init__.pyi +0 -0
  68. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/exceptions.py +0 -0
  69. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/foundry/__init__.pyi +0 -0
  70. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/google/__init__.py +0 -0
  71. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/google/__init__.pyi +0 -0
  72. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/lab/__init__.py +0 -0
  73. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/mem0/__init__.py +0 -0
  74. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/mem0/__init__.pyi +0 -0
  75. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/microsoft/__init__.py +0 -0
  76. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/microsoft/__init__.pyi +0 -0
  77. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/observability.py +0 -0
  78. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/ollama/__init__.py +0 -0
  79. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/ollama/__init__.pyi +0 -0
  80. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/openai/__init__.py +0 -0
  81. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/openai/__init__.pyi +0 -0
  82. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/orchestrations/__init__.py +0 -0
  83. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/orchestrations/__init__.pyi +0 -0
  84. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/py.typed +0 -0
  85. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/agent_framework/redis/__init__.py +0 -0
  86. {agent_framework_core-1.1.0 → agent_framework_core-1.2.0}/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.1.0
3
+ Version: 1.2.0
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
@@ -125,6 +125,7 @@ from ._telemetry import (
125
125
  prepend_agent_framework_to_user_agent,
126
126
  )
127
127
  from ._tools import (
128
+ SKIP_PARSING,
128
129
  FunctionInvocationConfiguration,
129
130
  FunctionInvocationLayer,
130
131
  FunctionTool,
@@ -212,6 +213,15 @@ from ._workflows._executor import (
212
213
  handler,
213
214
  )
214
215
  from ._workflows._function_executor import FunctionExecutor, executor
216
+ from ._workflows._functional import (
217
+ FunctionalWorkflow,
218
+ FunctionalWorkflowAgent,
219
+ RunContext,
220
+ StepWrapper,
221
+ get_run_context,
222
+ step,
223
+ workflow,
224
+ )
215
225
  from ._workflows._request_info_mixin import response_handler
216
226
  from ._workflows._runner import Runner
217
227
  from ._workflows._runner_context import (
@@ -258,6 +268,7 @@ __all__ = [
258
268
  "GROUP_INDEX_KEY",
259
269
  "GROUP_KIND_KEY",
260
270
  "GROUP_TOKEN_COUNT_KEY",
271
+ "SKIP_PARSING",
261
272
  "SUMMARIZED_BY_SUMMARY_ID_KEY",
262
273
  "SUMMARY_OF_GROUP_IDS_KEY",
263
274
  "SUMMARY_OF_MESSAGE_IDS_KEY",
@@ -330,6 +341,8 @@ __all__ = [
330
341
  "FunctionMiddleware",
331
342
  "FunctionMiddlewareTypes",
332
343
  "FunctionTool",
344
+ "FunctionalWorkflow",
345
+ "FunctionalWorkflowAgent",
333
346
  "GeneratedEmbeddings",
334
347
  "GraphConnectivityError",
335
348
  "HistoryProvider",
@@ -352,6 +365,7 @@ __all__ = [
352
365
  "ResponseStream",
353
366
  "Role",
354
367
  "RoleLiteral",
368
+ "RunContext",
355
369
  "Runner",
356
370
  "RunnerContext",
357
371
  "SecretString",
@@ -364,6 +378,7 @@ __all__ = [
364
378
  "SkillScriptRunner",
365
379
  "SkillsProvider",
366
380
  "SlidingWindowStrategy",
381
+ "StepWrapper",
367
382
  "SubWorkflowRequestMessage",
368
383
  "SubWorkflowResponseMessage",
369
384
  "SummarizationStrategy",
@@ -422,6 +437,7 @@ __all__ = [
422
437
  "evaluator",
423
438
  "executor",
424
439
  "function_middleware",
440
+ "get_run_context",
425
441
  "handler",
426
442
  "included_messages",
427
443
  "included_token_count",
@@ -437,6 +453,7 @@ __all__ = [
437
453
  "register_state_type",
438
454
  "resolve_agent_id",
439
455
  "response_handler",
456
+ "step",
440
457
  "tool",
441
458
  "tool_call_args_match",
442
459
  "tool_called_check",
@@ -445,4 +462,5 @@ __all__ = [
445
462
  "validate_tool_mode",
446
463
  "validate_tools",
447
464
  "validate_workflow_graph",
465
+ "workflow",
448
466
  ]
@@ -1659,6 +1659,7 @@ async def evaluate_workflow(
1659
1659
  workflow: Workflow,
1660
1660
  workflow_result: WorkflowRunResult | None = None,
1661
1661
  queries: str | Sequence[str] | None = None,
1662
+ expected_output: str | Sequence[str] | None = None,
1662
1663
  evaluators: Evaluator | Callable[..., Any] | Sequence[Evaluator | Callable[..., Any]],
1663
1664
  eval_name: str | None = None,
1664
1665
  include_overall: bool = True,
@@ -1683,6 +1684,11 @@ async def evaluate_workflow(
1683
1684
  workflow: The workflow instance.
1684
1685
  workflow_result: A completed ``WorkflowRunResult``.
1685
1686
  queries: Test queries to run through the workflow.
1687
+ expected_output: Ground-truth expected output(s), one per query. A
1688
+ single string is wrapped into a one-element list. When provided,
1689
+ must be the same length as ``queries``. Each value is stamped on
1690
+ the corresponding ``EvalItem.expected_output`` for evaluators
1691
+ that compare against a reference answer (e.g. similarity).
1686
1692
  evaluators: One or more ``Evaluator`` instances.
1687
1693
  eval_name: Display name for the evaluation.
1688
1694
  include_overall: Whether to evaluate the workflow's final output.
@@ -1720,10 +1726,20 @@ async def evaluate_workflow(
1720
1726
  # Normalize singular query to list
1721
1727
  if isinstance(queries, str):
1722
1728
  queries = [queries]
1729
+ if isinstance(expected_output, str):
1730
+ expected_output = [expected_output]
1723
1731
 
1724
1732
  if workflow_result is None and queries is None:
1725
1733
  raise ValueError("Provide either 'workflow_result' or 'queries'.")
1726
1734
 
1735
+ if expected_output is not None and queries is None:
1736
+ raise ValueError(
1737
+ "Provide 'queries' when using 'expected_output';"
1738
+ " 'expected_output' is not supported with 'workflow_result' only."
1739
+ )
1740
+ if expected_output is not None and queries is not None and len(expected_output) != len(queries):
1741
+ raise ValueError(f"Got {len(queries)} queries but {len(expected_output)} expected_output values.")
1742
+
1727
1743
  if num_repetitions < 1:
1728
1744
  raise ValueError(f"num_repetitions must be >= 1, got {num_repetitions}.")
1729
1745
 
@@ -1737,7 +1753,7 @@ async def evaluate_workflow(
1737
1753
  if queries is not None:
1738
1754
  results_list: list[WRR] = []
1739
1755
  for _rep in range(num_repetitions):
1740
- for q in queries:
1756
+ for qi, q in enumerate(queries):
1741
1757
  result = await workflow.run(q)
1742
1758
  if not isinstance(result, WRR):
1743
1759
  raise TypeError(f"Expected WorkflowRunResult from workflow.run(), got {type(result).__name__}.")
@@ -1746,6 +1762,8 @@ async def evaluate_workflow(
1746
1762
  if include_overall:
1747
1763
  overall_item = _build_overall_item(q, result)
1748
1764
  if overall_item:
1765
+ if expected_output is not None:
1766
+ overall_item.expected_output = expected_output[qi]
1749
1767
  overall_items.append(overall_item)
1750
1768
  else:
1751
1769
  assert workflow_result is not None # noqa: S101 # nosec B101
@@ -48,6 +48,7 @@ class ExperimentalFeature(str, Enum):
48
48
 
49
49
  EVALS = "EVALS"
50
50
  FILE_HISTORY = "FILE_HISTORY"
51
+ FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
51
52
  SKILLS = "SKILLS"
52
53
  TOOLBOXES = "TOOLBOXES"
53
54
 
@@ -2,11 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import contextlib
5
6
  import logging
6
7
  import os
7
- from collections.abc import Generator
8
- from contextlib import contextmanager
9
- from contextvars import ContextVar
10
8
  from typing import Any, Final
11
9
 
12
10
  from . import __version__ as version_info
@@ -29,34 +27,71 @@ USER_AGENT_KEY: Final[str] = "User-Agent"
29
27
  HTTP_USER_AGENT: Final[str] = "agent-framework-python"
30
28
  AGENT_FRAMEWORK_USER_AGENT = f"{HTTP_USER_AGENT}/{version_info}" # type: ignore[has-type]
31
29
 
32
- _user_agent_prefixes: ContextVar[tuple[str, ...]] = ContextVar("_user_agent_prefixes", default=())
30
+ # This environment variable is reserved by the Foundry hosting environment to
31
+ # indicate that the agent is running in a hosted environment.
32
+ _FOUNDRY_HOSTING_ENV_VAR = "FOUNDRY_HOSTING_ENVIRONMENT"
33
+ # This prefix is added to the user agent string when the agent is running in a hosted environment.
34
+ _HOSTED_USER_AGENT_PREFIX = "foundry-hosting"
33
35
 
36
+ _user_agent_prefixes: set[str] = set()
37
+ _hosted_env_detected: bool = False
34
38
 
35
- @contextmanager
36
- def user_agent_prefix(prefix: str) -> Generator[None]:
37
- """Context manager that adds a prefix to the user agent string for the current scope.
38
39
 
39
- This is useful for upstream layers that want to identify themselves in telemetry
40
- for the duration of a request without permanently mutating global state.
40
+ def _add_user_agent_prefix(prefix: str) -> None:
41
+ """Permanently add a prefix to the user agent string.
42
+
43
+ This is used by hosting layers to identify themselves in telemetry.
44
+ Once added, the prefix applies to all subsequent user agent strings.
41
45
 
42
46
  Args:
43
47
  prefix: The prefix to add (e.g. "foundry-hosting").
44
48
  """
45
- current = _user_agent_prefixes.get()
46
- token = _user_agent_prefixes.set((*current, prefix)) if prefix and prefix not in current else None
47
- try:
48
- yield
49
- finally:
50
- if token is not None:
51
- _user_agent_prefixes.reset(token)
49
+ if prefix:
50
+ _user_agent_prefixes.add(prefix)
51
+
52
+
53
+ def _detect_hosted_environment() -> None:
54
+ """Detect if running in a hosted environment and add the user agent prefix.
52
55
 
56
+ Checks the ``FOUNDRY_HOSTING_ENVIRONMENT`` env var first, then falls back
57
+ to checking whether the agent server SDK is installed (via
58
+ ``importlib.util.find_spec``) before importing it, to avoid unnecessary
59
+ import overhead for non-hosted scenarios.
60
+ """
61
+ global _hosted_env_detected
62
+ if _hosted_env_detected:
63
+ return
64
+
65
+ if (env_value := os.environ.get(_FOUNDRY_HOSTING_ENV_VAR)) is not None:
66
+ # Env var exists — trust its value and skip the fallback.
67
+ if env_value:
68
+ _add_user_agent_prefix(_HOSTED_USER_AGENT_PREFIX)
69
+ _hosted_env_detected = True
70
+ return
53
71
 
54
- def _get_user_agent() -> str:
55
- """Return the full user agent string including any context-scoped prefixes."""
56
- prefixes = _user_agent_prefixes.get()
57
- if not prefixes:
72
+ # Env var not set — fall back to AgentConfig as a second layer of defense.
73
+ # Use find_spec to avoid the cost of a full import when the SDK is not installed.
74
+ import importlib.util
75
+
76
+ try:
77
+ if importlib.util.find_spec("azure.ai.agentserver.core") is None:
78
+ return
79
+ except (ModuleNotFoundError, ValueError):
80
+ return
81
+ with contextlib.suppress(ImportError, AttributeError):
82
+ from azure.ai.agentserver.core import AgentConfig # pyright: ignore[reportMissingImports]
83
+
84
+ if AgentConfig.from_env().is_hosted:
85
+ _add_user_agent_prefix(_HOSTED_USER_AGENT_PREFIX)
86
+ _hosted_env_detected = True
87
+
88
+
89
+ def get_user_agent() -> str:
90
+ """Return the full user agent string including any registered prefixes."""
91
+ _detect_hosted_environment()
92
+ if not _user_agent_prefixes:
58
93
  return AGENT_FRAMEWORK_USER_AGENT
59
- return f"{'/'.join(prefixes)}/{AGENT_FRAMEWORK_USER_AGENT}"
94
+ return f"{'/'.join(sorted(_user_agent_prefixes))}/{AGENT_FRAMEWORK_USER_AGENT}"
60
95
 
61
96
 
62
97
  def prepend_agent_framework_to_user_agent(headers: dict[str, Any] | None = None) -> dict[str, Any]:
@@ -89,7 +124,7 @@ def prepend_agent_framework_to_user_agent(headers: dict[str, Any] | None = None)
89
124
  """
90
125
  if not IS_TELEMETRY_ENABLED:
91
126
  return headers or {}
92
- user_agent = _get_user_agent()
127
+ user_agent = get_user_agent()
93
128
  if not headers:
94
129
  return {USER_AGENT_KEY: user_agent}
95
130
  headers[USER_AGENT_KEY] = f"{user_agent} {headers[USER_AGENT_KEY]}" if USER_AGENT_KEY in headers else user_agent
@@ -94,6 +94,33 @@ ApprovalMode: TypeAlias = Literal["always_require", "never_require"]
94
94
  ChatClientT = TypeVar("ChatClientT", bound="SupportsChatGetResponse[Any]")
95
95
  ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel)
96
96
 
97
+
98
+ class _SkipParsingSentinel:
99
+ """Sentinel signaling that :meth:`FunctionTool.invoke` should return the raw value.
100
+
101
+ When passed as ``result_parser`` to :class:`FunctionTool` (or the ``@tool`` decorator),
102
+ the default :meth:`FunctionTool.parse_result` is bypassed and the wrapped function's
103
+ return value is returned unchanged from :meth:`FunctionTool.invoke`. Callers may also
104
+ request the raw value on a per-call basis by passing ``skip_parsing=True`` to
105
+ :meth:`FunctionTool.invoke`.
106
+
107
+ Use the module-level ``SKIP_PARSING`` singleton — do not instantiate this class.
108
+ """
109
+
110
+ _instance: ClassVar[_SkipParsingSentinel | None] = None
111
+
112
+ def __new__(cls) -> _SkipParsingSentinel:
113
+ if cls._instance is None:
114
+ cls._instance = super().__new__(cls)
115
+ return cls._instance
116
+
117
+ def __repr__(self) -> str:
118
+ return "SKIP_PARSING"
119
+
120
+
121
+ SKIP_PARSING: Final[_SkipParsingSentinel] = _SkipParsingSentinel()
122
+ """Sentinel for ``FunctionTool(result_parser=...)`` meaning "do not parse the result"."""
123
+
97
124
  # region Helpers
98
125
 
99
126
 
@@ -279,7 +306,7 @@ class FunctionTool(SerializationMixin):
279
306
  additional_properties: dict[str, Any] | None = None,
280
307
  func: Callable[..., Any] | None = None,
281
308
  input_model: type[BaseModel] | Mapping[str, Any] | None = None,
282
- result_parser: Callable[[Any], str | list[Content]] | None = None,
309
+ result_parser: Callable[[Any], str | list[Content]] | _SkipParsingSentinel | None = None,
283
310
  **kwargs: Any,
284
311
  ) -> None:
285
312
  """Initialize the FunctionTool.
@@ -327,9 +354,11 @@ class FunctionTool(SerializationMixin):
327
354
  result_parser: An optional callable with signature ``Callable[[Any], str]`` that
328
355
  overrides the default result parsing behavior. When provided, this callable
329
356
  is used to convert the raw function return value to a string instead of the
330
- built-in :meth:`parse_result` logic. Depending on your function, it may be
331
- easiest to just do the serialization directly in the function body rather
332
- than providing a custom ``result_parser``.
357
+ built-in :meth:`parse_result` logic. Pass the :data:`SKIP_PARSING` sentinel
358
+ instead of a callable to opt out of parsing entirely; in that case
359
+ :meth:`invoke` returns the wrapped function's raw return value. Depending
360
+ on your function, it may be easiest to just do the serialization directly
361
+ in the function body rather than providing a custom ``result_parser``.
333
362
  **kwargs: Additional keyword arguments.
334
363
  """
335
364
  # Core attributes (formerly from BaseTool)
@@ -508,31 +537,65 @@ class FunctionTool(SerializationMixin):
508
537
  self.invocation_exception_count += 1
509
538
  raise
510
539
 
540
+ @overload
511
541
  async def invoke(
512
542
  self,
513
543
  *,
514
544
  arguments: BaseModel | Mapping[str, Any] | None = None,
515
545
  context: FunctionInvocationContext | None = None,
516
546
  tool_call_id: str | None = None,
547
+ skip_parsing: Literal[True],
517
548
  **kwargs: Any,
518
- ) -> list[Content]:
549
+ ) -> Any: ...
550
+
551
+ @overload
552
+ async def invoke(
553
+ self,
554
+ *,
555
+ arguments: BaseModel | Mapping[str, Any] | None = None,
556
+ context: FunctionInvocationContext | None = None,
557
+ tool_call_id: str | None = None,
558
+ skip_parsing: Literal[False] = False,
559
+ **kwargs: Any,
560
+ ) -> list[Content]: ...
561
+
562
+ async def invoke(
563
+ self,
564
+ *,
565
+ arguments: BaseModel | Mapping[str, Any] | None = None,
566
+ context: FunctionInvocationContext | None = None,
567
+ tool_call_id: str | None = None,
568
+ skip_parsing: bool = False,
569
+ **kwargs: Any,
570
+ ) -> list[Content] | Any:
519
571
  """Run the AI function with the provided arguments as a Pydantic model.
520
572
 
521
573
  The raw return value of the wrapped function is automatically parsed into a
522
574
  ``list[Content]`` using :meth:`parse_result` or the custom ``result_parser``
523
- if one was provided. Every result — text, rich media, or serialized objects —
524
- is represented uniformly as Content items.
575
+ configured on the tool. Every result — text, rich media, or serialized
576
+ objects — is represented uniformly as Content items.
577
+
578
+ Parsing can be skipped in two ways: configure the tool with
579
+ ``result_parser=SKIP_PARSING`` to always skip parsing, or pass
580
+ ``skip_parsing=True`` per call. Either way the wrapped function's raw value
581
+ is returned. This is intended for callers (e.g. sandboxed runtimes) that
582
+ consume the value from Python directly and would otherwise undo the
583
+ ``Content`` wrapping.
525
584
 
526
585
  Keyword Args:
527
586
  arguments: A mapping or model instance containing the arguments for the function.
528
587
  context: Explicit function invocation context carrying runtime kwargs.
529
588
  tool_call_id: Optional tool call identifier used for telemetry and tracing.
589
+ skip_parsing: When ``True``, bypass parsing and return the wrapped function's
590
+ raw value instead of a ``list[Content]``. Defaults to ``False``.
530
591
  kwargs: Direct function argument values. When provided, every keyword
531
592
  must match a declared tool parameter. Runtime data must be passed
532
593
  via ``context``.
533
594
 
534
595
  Returns:
535
- A list of Content items representing the tool output.
596
+ ``list[Content]`` by default. The raw function return value (``Any``) when
597
+ ``skip_parsing=True`` (or the tool was constructed with
598
+ ``result_parser=SKIP_PARSING``).
536
599
 
537
600
  Raises:
538
601
  TypeError: If arguments is not mapping-like or fails schema checks.
@@ -544,7 +607,9 @@ class FunctionTool(SerializationMixin):
544
607
  from ._types import Content
545
608
  from .observability import OBSERVABILITY_SETTINGS
546
609
 
547
- parser = self.result_parser or FunctionTool.parse_result
610
+ configured_parser = self.result_parser
611
+ skip_parsing = skip_parsing or configured_parser is SKIP_PARSING
612
+ parser = configured_parser if callable(configured_parser) else FunctionTool.parse_result
548
613
 
549
614
  parameter_names = set(self.parameters().get("properties", {}).keys())
550
615
  direct_argument_kwargs = (
@@ -616,6 +681,10 @@ class FunctionTool(SerializationMixin):
616
681
  logger.debug(f"Function arguments: {observable_kwargs}")
617
682
  res = self.__call__(**call_kwargs)
618
683
  result = await res if inspect.isawaitable(res) else res
684
+ if skip_parsing:
685
+ logger.info(f"Function {self.name} succeeded.")
686
+ logger.debug(f"Function result: {type(result).__name__}")
687
+ return result
619
688
  try:
620
689
  parsed = parser(result)
621
690
  except Exception:
@@ -671,6 +740,13 @@ class FunctionTool(SerializationMixin):
671
740
  logger.error(f"Function failed. Error: {exception}")
672
741
  raise
673
742
  else:
743
+ if skip_parsing:
744
+ logger.info(f"Function {self.name} succeeded.")
745
+ if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED: # type: ignore[name-defined]
746
+ result_str = str(result)
747
+ span.set_attribute(OtelAttr.TOOL_RESULT, result_str)
748
+ logger.debug(f"Function result: {result_str}")
749
+ return result
674
750
  try:
675
751
  parsed = parser(result)
676
752
  except Exception:
@@ -1067,7 +1143,7 @@ def tool(
1067
1143
  max_invocations: int | None = None,
1068
1144
  max_invocation_exceptions: int | None = None,
1069
1145
  additional_properties: dict[str, Any] | None = None,
1070
- result_parser: Callable[[Any], str | list[Content]] | None = None,
1146
+ result_parser: Callable[[Any], str | list[Content]] | _SkipParsingSentinel | None = None,
1071
1147
  ) -> FunctionTool: ...
1072
1148
 
1073
1149
 
@@ -1083,7 +1159,7 @@ def tool(
1083
1159
  max_invocations: int | None = None,
1084
1160
  max_invocation_exceptions: int | None = None,
1085
1161
  additional_properties: dict[str, Any] | None = None,
1086
- result_parser: Callable[[Any], str | list[Content]] | None = None,
1162
+ result_parser: Callable[[Any], str | list[Content]] | _SkipParsingSentinel | None = None,
1087
1163
  ) -> Callable[[Callable[..., Any]], FunctionTool]: ...
1088
1164
 
1089
1165
 
@@ -1098,7 +1174,7 @@ def tool(
1098
1174
  max_invocations: int | None = None,
1099
1175
  max_invocation_exceptions: int | None = None,
1100
1176
  additional_properties: dict[str, Any] | None = None,
1101
- result_parser: Callable[[Any], str | list[Content]] | None = None,
1177
+ result_parser: Callable[[Any], str | list[Content]] | _SkipParsingSentinel | None = None,
1102
1178
  ) -> FunctionTool | Callable[[Callable[..., Any]], FunctionTool]:
1103
1179
  """Decorate a function to turn it into a FunctionTool that can be passed to models and executed automatically.
1104
1180
 
@@ -120,6 +120,7 @@ WorkflowEventType = Literal[
120
120
  "executor_invoked", # Executor handler was called (use .executor_id, .data)
121
121
  "executor_completed", # Executor handler completed (use .executor_id, .data)
122
122
  "executor_failed", # Executor handler raised error (use .executor_id, .details)
123
+ "executor_bypassed", # Executor skipped via cache hit during replay (use .executor_id, .data)
123
124
  # Orchestration event types (use .data for typed payload)
124
125
  "group_chat", # Group chat orchestrator events (use .data as GroupChatRequestSentEvent | GroupChatResponseReceivedEvent) # noqa: E501
125
126
  "handoff_sent", # Handoff routing events (use .data as HandoffSentEvent)
@@ -148,6 +149,7 @@ class WorkflowEvent(Generic[DataT]):
148
149
  - `WorkflowEvent.executor_invoked(executor_id)` - executor handler called
149
150
  - `WorkflowEvent.executor_completed(executor_id)` - executor handler completed
150
151
  - `WorkflowEvent.executor_failed(executor_id, details)` - executor handler failed
152
+ - `WorkflowEvent.executor_bypassed(executor_id)` - executor skipped via cache hit
151
153
 
152
154
  The generic parameter DataT represents the type of the event's data payload:
153
155
  - Lifecycle events: `WorkflowEvent[None]` (data is None)
@@ -318,6 +320,11 @@ class WorkflowEvent(Generic[DataT]):
318
320
  """Create an 'executor_failed' event when an executor handler raises an error."""
319
321
  return WorkflowEvent("executor_failed", executor_id=executor_id, data=details, details=details)
320
322
 
323
+ @classmethod
324
+ def executor_bypassed(cls, executor_id: str, data: DataT | None = None) -> WorkflowEvent[DataT]:
325
+ """Create an 'executor_bypassed' event when a step is skipped via cache hit during replay."""
326
+ return cls("executor_bypassed", executor_id=executor_id, data=data)
327
+
321
328
  # ==========================================================================
322
329
  # Property for type-safe access
323
330
  # ==========================================================================