agent-framework-core 1.6.0__tar.gz → 1.7.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 (94) hide show
  1. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/PKG-INFO +1 -1
  2. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/__init__.py +18 -0
  3. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_compaction.py +116 -0
  4. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_feature_stage.py +124 -10
  5. agent_framework_core-1.7.0/agent_framework/_harness/_agent.py +349 -0
  6. agent_framework_core-1.7.0/agent_framework/_harness/_background_agents.py +521 -0
  7. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_harness/_mode.py +50 -21
  8. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_harness/_todo.py +94 -28
  9. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_request_info_mixin.py +369 -369
  10. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/a2a/__init__.py +2 -1
  11. agent_framework_core-1.7.0/agent_framework/a2a/__init__.pyi +5 -0
  12. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/foundry/__init__.py +1 -0
  13. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/foundry/__init__.pyi +2 -0
  14. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/pyproject.toml +1 -1
  15. agent_framework_core-1.6.0/agent_framework/a2a/__init__.pyi +0 -5
  16. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/LICENSE +0 -0
  17. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/README.md +0 -0
  18. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_agents.py +0 -0
  19. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_clients.py +0 -0
  20. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_docstrings.py +0 -0
  21. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_evaluation.py +0 -0
  22. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_harness/__init__.py +0 -0
  23. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_harness/_memory.py +0 -0
  24. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_mcp.py +0 -0
  25. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_middleware.py +0 -0
  26. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_serialization.py +0 -0
  27. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_sessions.py +0 -0
  28. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_settings.py +0 -0
  29. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_skills.py +0 -0
  30. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_telemetry.py +0 -0
  31. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_tools.py +0 -0
  32. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_types.py +0 -0
  33. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/__init__.py +0 -0
  34. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_agent.py +0 -0
  35. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_agent_executor.py +0 -0
  36. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_agent_utils.py +0 -0
  37. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_checkpoint.py +0 -0
  38. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_checkpoint_encoding.py +0 -0
  39. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_const.py +0 -0
  40. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_conversation_history.py +0 -0
  41. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_edge.py +0 -0
  42. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_edge_runner.py +0 -0
  43. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_events.py +0 -0
  44. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_executor.py +0 -0
  45. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_function_executor.py +0 -0
  46. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_functional.py +0 -0
  47. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_message_utils.py +0 -0
  48. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_model_utils.py +0 -0
  49. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_runner.py +0 -0
  50. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_runner_context.py +0 -0
  51. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_state.py +0 -0
  52. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_typing_utils.py +0 -0
  53. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_validation.py +0 -0
  54. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_viz.py +0 -0
  55. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_workflow.py +0 -0
  56. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_workflow_builder.py +0 -0
  57. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_workflow_context.py +0 -0
  58. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/_workflows/_workflow_executor.py +0 -0
  59. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/ag_ui/__init__.py +0 -0
  60. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/ag_ui/__init__.pyi +0 -0
  61. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/amazon/__init__.py +0 -0
  62. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/amazon/__init__.pyi +0 -0
  63. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/anthropic/__init__.py +0 -0
  64. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/anthropic/__init__.pyi +0 -0
  65. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/azure/__init__.py +0 -0
  66. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/azure/__init__.pyi +0 -0
  67. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/chatkit/__init__.py +0 -0
  68. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/chatkit/__init__.pyi +0 -0
  69. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/declarative/__init__.py +0 -0
  70. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/declarative/__init__.pyi +0 -0
  71. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/devui/__init__.py +0 -0
  72. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/devui/__init__.pyi +0 -0
  73. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/exceptions.py +0 -0
  74. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/github/__init__.py +0 -0
  75. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/github/__init__.pyi +0 -0
  76. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/google/__init__.py +0 -0
  77. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/google/__init__.pyi +0 -0
  78. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/hyperlight/__init__.py +0 -0
  79. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/lab/__init__.py +0 -0
  80. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/mem0/__init__.py +0 -0
  81. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/mem0/__init__.pyi +0 -0
  82. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/microsoft/__init__.py +0 -0
  83. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/microsoft/__init__.pyi +0 -0
  84. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/observability.py +0 -0
  85. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/ollama/__init__.py +0 -0
  86. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/ollama/__init__.pyi +0 -0
  87. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/openai/__init__.py +0 -0
  88. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/openai/__init__.pyi +0 -0
  89. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/orchestrations/__init__.py +0 -0
  90. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/orchestrations/__init__.pyi +0 -0
  91. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/py.typed +0 -0
  92. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/redis/__init__.py +0 -0
  93. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/redis/__init__.pyi +0 -0
  94. {agent_framework_core-1.6.0 → agent_framework_core-1.7.0}/agent_framework/security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-core
3
- Version: 1.6.0
3
+ Version: 1.7.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
@@ -45,6 +45,7 @@ from ._compaction import (
45
45
  CharacterEstimatorTokenizer,
46
46
  CompactionProvider,
47
47
  CompactionStrategy,
48
+ ContextWindowCompactionStrategy,
48
49
  SelectiveToolCallCompactionStrategy,
49
50
  SlidingWindowStrategy,
50
51
  SummarizationStrategy,
@@ -79,6 +80,16 @@ from ._evaluation import (
79
80
  tool_calls_present,
80
81
  )
81
82
  from ._feature_stage import ExperimentalFeature, ReleaseCandidateFeature
83
+ from ._harness._agent import (
84
+ DEFAULT_HARNESS_INSTRUCTIONS,
85
+ create_harness_agent,
86
+ )
87
+ from ._harness._background_agents import (
88
+ DEFAULT_BACKGROUND_AGENTS_SOURCE_ID,
89
+ BackgroundAgentsProvider,
90
+ BackgroundTaskInfo,
91
+ BackgroundTaskStatus,
92
+ )
82
93
  from ._harness._memory import (
83
94
  DEFAULT_MEMORY_SOURCE_ID,
84
95
  MemoryContextProvider,
@@ -297,6 +308,8 @@ __all__ = [
297
308
  "AGENT_FRAMEWORK_USER_AGENT",
298
309
  "APP_INFO",
299
310
  "COMPACTION_STATE_KEY",
311
+ "DEFAULT_BACKGROUND_AGENTS_SOURCE_ID",
312
+ "DEFAULT_HARNESS_INSTRUCTIONS",
300
313
  "DEFAULT_MAX_ITERATIONS",
301
314
  "DEFAULT_MEMORY_SOURCE_ID",
302
315
  "DEFAULT_MODE_SOURCE_ID",
@@ -332,6 +345,9 @@ __all__ = [
332
345
  "AgentSession",
333
346
  "AggregatingSkillsSource",
334
347
  "Annotation",
348
+ "BackgroundAgentsProvider",
349
+ "BackgroundTaskInfo",
350
+ "BackgroundTaskStatus",
335
351
  "BaseAgent",
336
352
  "BaseChatClient",
337
353
  "BaseEmbeddingClient",
@@ -352,6 +368,7 @@ __all__ = [
352
368
  "CompactionStrategy",
353
369
  "Content",
354
370
  "ContextProvider",
371
+ "ContextWindowCompactionStrategy",
355
372
  "ContinuationToken",
356
373
  "ConversationSplit",
357
374
  "ConversationSplitter",
@@ -499,6 +516,7 @@ __all__ = [
499
516
  "apply_compaction",
500
517
  "chat_middleware",
501
518
  "create_edge_runner",
519
+ "create_harness_agent",
502
520
  "detect_media_type_from_base64",
503
521
  "evaluate_agent",
504
522
  "evaluate_workflow",
@@ -1277,6 +1277,121 @@ class CompactionProvider(ContextProvider):
1277
1277
  # whether excluded messages are loaded on the next turn.
1278
1278
 
1279
1279
 
1280
+ class ContextWindowCompactionStrategy:
1281
+ """Token-budget compaction derived from a model's context window size.
1282
+
1283
+ Computes an input budget from the model's context window and output token
1284
+ limits, then applies a two-phase compaction pipeline:
1285
+
1286
+ 1. **Tool result eviction** — collapses older tool-call groups into summaries
1287
+ when included tokens exceed ``tool_eviction_threshold`` of the input budget.
1288
+ 2. **Truncation** — removes oldest non-system groups when included tokens
1289
+ exceed ``truncation_threshold`` of the input budget.
1290
+
1291
+ The class uses two independent :class:`TokenBudgetComposedStrategy`
1292
+ instances — one per phase — so each fires only when its own threshold
1293
+ is exceeded.
1294
+
1295
+ Examples:
1296
+ .. code-block:: python
1297
+
1298
+ from agent_framework import ContextWindowCompactionStrategy, CompactionProvider
1299
+
1300
+ strategy = ContextWindowCompactionStrategy(
1301
+ max_context_window_tokens=128_000,
1302
+ max_output_tokens=16_384,
1303
+ )
1304
+ provider = CompactionProvider(before_strategy=strategy)
1305
+ """
1306
+
1307
+ DEFAULT_TOOL_EVICTION_THRESHOLD: float = 0.5
1308
+ """Default fraction of input budget at which tool result eviction triggers."""
1309
+
1310
+ DEFAULT_TRUNCATION_THRESHOLD: float = 0.8
1311
+ """Default fraction of input budget at which truncation triggers."""
1312
+
1313
+ def __init__(
1314
+ self,
1315
+ *,
1316
+ max_context_window_tokens: int,
1317
+ max_output_tokens: int,
1318
+ tokenizer: TokenizerProtocol | None = None,
1319
+ tool_eviction_threshold: float = DEFAULT_TOOL_EVICTION_THRESHOLD,
1320
+ truncation_threshold: float = DEFAULT_TRUNCATION_THRESHOLD,
1321
+ keep_last_tool_call_groups: int = 4,
1322
+ ) -> None:
1323
+ """Create a context-window compaction strategy.
1324
+
1325
+ Keyword Args:
1326
+ max_context_window_tokens: The model's maximum context window size
1327
+ in tokens (e.g. 128,000).
1328
+ max_output_tokens: The model's maximum output tokens per response
1329
+ (e.g. 16,384).
1330
+ tokenizer: Token counter for measuring message sizes. Defaults to
1331
+ :class:`CharacterEstimatorTokenizer` (4 chars/token heuristic).
1332
+ tool_eviction_threshold: Fraction of input budget (0.0, 1.0] at
1333
+ which tool result eviction triggers. Defaults to 0.5.
1334
+ truncation_threshold: Fraction of input budget (0.0, 1.0] at which
1335
+ truncation triggers. Must be ≥ ``tool_eviction_threshold``.
1336
+ Defaults to 0.8.
1337
+ keep_last_tool_call_groups: Number of most recent tool-call groups
1338
+ to retain verbatim during tool eviction. Older groups are
1339
+ collapsed into summaries. Defaults to 4.
1340
+
1341
+ Raises:
1342
+ ValueError: If thresholds are out of range or inconsistent.
1343
+ """
1344
+ if max_context_window_tokens <= 0:
1345
+ raise ValueError("max_context_window_tokens must be positive.")
1346
+ if max_output_tokens < 0 or max_output_tokens >= max_context_window_tokens:
1347
+ raise ValueError("max_output_tokens must be >= 0 and < max_context_window_tokens.")
1348
+ if not (0.0 < tool_eviction_threshold <= 1.0):
1349
+ raise ValueError("tool_eviction_threshold must be in (0.0, 1.0].")
1350
+ if not (0.0 < truncation_threshold <= 1.0):
1351
+ raise ValueError("truncation_threshold must be in (0.0, 1.0].")
1352
+ if truncation_threshold < tool_eviction_threshold:
1353
+ raise ValueError("truncation_threshold must be >= tool_eviction_threshold.")
1354
+
1355
+ resolved_tokenizer = tokenizer or CharacterEstimatorTokenizer()
1356
+ input_budget = max_context_window_tokens - max_output_tokens
1357
+ tool_eviction_tokens = int(input_budget * tool_eviction_threshold)
1358
+ truncation_tokens = int(input_budget * truncation_threshold)
1359
+
1360
+ self.max_context_window_tokens = max_context_window_tokens
1361
+ self.max_output_tokens = max_output_tokens
1362
+ self.input_budget_tokens = input_budget
1363
+ self.tool_eviction_threshold = tool_eviction_threshold
1364
+ self.truncation_threshold = truncation_threshold
1365
+
1366
+ self._tool_eviction = TokenBudgetComposedStrategy(
1367
+ token_budget=tool_eviction_tokens,
1368
+ tokenizer=resolved_tokenizer,
1369
+ strategies=[
1370
+ ToolResultCompactionStrategy(keep_last_tool_call_groups=keep_last_tool_call_groups),
1371
+ ],
1372
+ )
1373
+ self._truncation = TokenBudgetComposedStrategy(
1374
+ token_budget=truncation_tokens,
1375
+ tokenizer=resolved_tokenizer,
1376
+ strategies=[
1377
+ TruncationStrategy(
1378
+ max_n=truncation_tokens,
1379
+ compact_to=tool_eviction_tokens,
1380
+ tokenizer=resolved_tokenizer,
1381
+ ),
1382
+ ],
1383
+ )
1384
+
1385
+ async def __call__(self, messages: list[Message]) -> bool:
1386
+ """Apply the two-phase compaction pipeline.
1387
+
1388
+ Returns:
1389
+ True if compaction changed message inclusion; otherwise False.
1390
+ """
1391
+ changed = await self._tool_eviction(messages)
1392
+ return (await self._truncation(messages)) or changed
1393
+
1394
+
1280
1395
  __all__ = [
1281
1396
  "COMPACTION_STATE_KEY",
1282
1397
  "EXCLUDED_KEY",
@@ -1293,6 +1408,7 @@ __all__ = [
1293
1408
  "CharacterEstimatorTokenizer",
1294
1409
  "CompactionProvider",
1295
1410
  "CompactionStrategy",
1411
+ "ContextWindowCompactionStrategy",
1296
1412
  "GroupKind",
1297
1413
  "SelectiveToolCallCompactionStrategy",
1298
1414
  "SlidingWindowStrategy",
@@ -2,10 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import abc
5
6
  import asyncio.coroutines
7
+ import contextlib
6
8
  import functools
7
9
  import inspect
10
+ import os
8
11
  import sys
12
+ import typing
9
13
  import warnings
10
14
  from collections.abc import Callable
11
15
  from enum import Enum
@@ -54,6 +58,7 @@ class ExperimentalFeature(str, Enum):
54
58
  FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
55
59
  HARNESS = "HARNESS"
56
60
  SKILLS = "SKILLS"
61
+ TO_PROMPT_AGENT = "TO_PROMPT_AGENT"
57
62
 
58
63
 
59
64
  class ReleaseCandidateFeature(str, Enum):
@@ -75,6 +80,51 @@ class ExperimentalWarning(FeatureStageWarning):
75
80
  """Warning emitted when an experimental API is used."""
76
81
 
77
82
 
83
+ # Sentinel attribute used to detect (and reuse) a formatter we've already
84
+ # installed. This lets the install be idempotent across re-imports / reloads
85
+ # and keeps a stable reference to the previous formatter for testing or
86
+ # external restoration via ``warnings.formatwarning = original``.
87
+ _FEATURE_STAGE_FORMATTER_MARKER = "__feature_stage_formatter__"
88
+
89
+
90
+ def _install_feature_stage_formatter() -> None:
91
+ """Install a single-line formatter for FeatureStageWarning categories.
92
+
93
+ The stdlib default formatter emits two lines (header + source snippet)
94
+ which is noisy for our warnings — the offending class/function name is
95
+ already in the message, so a one-line ``file:lineno: Category: message``
96
+ is enough. Other warning categories are delegated to the previous
97
+ formatter so we never change behaviour for unrelated warnings.
98
+
99
+ The install is idempotent: if a formatter installed by this module is
100
+ already in place, we leave it alone so re-imports (and any third-party
101
+ formatter wrapped on top of ours) don't get wrapped multiple times.
102
+ """
103
+ current = warnings.formatwarning
104
+ if getattr(current, _FEATURE_STAGE_FORMATTER_MARKER, False):
105
+ return
106
+
107
+ def _formatwarning(
108
+ message: Warning | str,
109
+ category: type[Warning],
110
+ filename: str,
111
+ lineno: int,
112
+ line: str | None = None,
113
+ ) -> str:
114
+ if issubclass(category, FeatureStageWarning):
115
+ return f"{filename}:{lineno}: {category.__name__}: {message}\n"
116
+ return current(message, category, filename, lineno, line)
117
+
118
+ setattr(_formatwarning, _FEATURE_STAGE_FORMATTER_MARKER, True)
119
+ # Keep a reference to the wrapped formatter so callers (tests, embedders)
120
+ # can restore the previous behaviour if they need to.
121
+ _formatwarning.__wrapped__ = current # type: ignore[attr-defined]
122
+ warnings.formatwarning = _formatwarning
123
+
124
+
125
+ _install_feature_stage_formatter()
126
+
127
+
78
128
  def _normalize_feature_id(feature_id: str | Enum) -> str:
79
129
  return str(feature_id.value if isinstance(feature_id, Enum) else feature_id)
80
130
 
@@ -109,23 +159,91 @@ def _set_feature_stage_metadata(obj: Any, *, stage: FeatureStageName, feature_id
109
159
  setattr(obj, _FEATURE_ID_ATTR, feature_id)
110
160
 
111
161
 
162
+ _INTERNAL_FRAME_FILE = os.path.normcase(__file__)
163
+ # Module names whose frames we never want to surface as the caller. ``abc`` is
164
+ # the big one (its ``__new__`` shows up as ``<frozen abc>:106`` for ABC-driven
165
+ # subclass creation on modern CPython, so we cannot rely on filename matching).
166
+ # ``functools``/``typing``/``contextlib`` are added because they often wrap our
167
+ # decorators or appear in the metaclass call path.
168
+ _INTERNAL_FRAME_MODULES: frozenset[str] = frozenset({
169
+ abc.__name__,
170
+ functools.__name__,
171
+ typing.__name__,
172
+ contextlib.__name__,
173
+ })
174
+
175
+
176
+ def _is_internal_frame(frame: Any) -> bool:
177
+ if os.path.normcase(frame.f_code.co_filename) == _INTERNAL_FRAME_FILE:
178
+ return True
179
+ module_name = frame.f_globals.get("__name__", "")
180
+ if module_name in _INTERNAL_FRAME_MODULES:
181
+ return True
182
+ # Submodules of the skipped stdlib packages (``typing.ext``, ``functools``
183
+ # wrappers under ``concurrent.futures._base``, etc.) are also wrappers we
184
+ # don't want to surface.
185
+ return any(module_name.startswith(prefix + ".") for prefix in _INTERNAL_FRAME_MODULES)
186
+
187
+
188
+ def _resolve_user_frame() -> tuple[str, int, str] | None:
189
+ """Resolve the user frame that triggered an experimental warning.
190
+
191
+ Walk the stack and return ``(filename, lineno, module_name)`` for the first
192
+ frame outside this module and the wrapping/metaclass machinery.
193
+
194
+ Returns ``None`` if no such frame is found; callers fall back to plain
195
+ ``warnings.warn`` with a fixed stacklevel.
196
+ """
197
+ # Frame objects participate in reference cycles (``frame -> f_locals ->
198
+ # frame``) and can delay GC if held implicitly. Capture the user frame's
199
+ # data into plain values inside the try, and explicitly delete the frame
200
+ # references in finally so we never leak frames across this call. This
201
+ # follows CPython's own guidance for code that uses ``inspect.currentframe``.
202
+ frame = inspect.currentframe()
203
+ candidate: Any = None
204
+ try:
205
+ if frame is None:
206
+ return None
207
+ # Skip _resolve_user_frame itself + the warn helper that called it.
208
+ candidate = frame.f_back.f_back if frame.f_back and frame.f_back.f_back else None
209
+ while candidate is not None:
210
+ if not _is_internal_frame(candidate):
211
+ return (
212
+ candidate.f_code.co_filename,
213
+ candidate.f_lineno,
214
+ candidate.f_globals.get("__name__", "<unknown>"),
215
+ )
216
+ candidate = candidate.f_back
217
+ return None
218
+ finally:
219
+ del frame, candidate
220
+
221
+
112
222
  def _warn_on_feature_use(
113
223
  *,
114
224
  stage: FeatureStageName,
115
225
  feature_id: str,
116
226
  object_name: str,
117
227
  category: type[Warning],
118
- stacklevel: int,
119
228
  ) -> None:
120
229
  warning_key = (category, feature_id)
121
230
  if warning_key in _WARNED_FEATURES:
122
231
  return
123
232
 
124
- warnings.warn(
125
- _build_stage_warning_message(stage=stage, feature_id=feature_id, object_name=object_name),
126
- category=category,
127
- stacklevel=stacklevel,
128
- )
233
+ message = _build_stage_warning_message(stage=stage, feature_id=feature_id, object_name=object_name)
234
+ user_frame = _resolve_user_frame()
235
+ if user_frame is None:
236
+ # Last-resort fallback: emit at the immediate caller of this helper.
237
+ warnings.warn(message, category=category, stacklevel=2)
238
+ else:
239
+ filename, lineno, module = user_frame
240
+ warnings.warn_explicit(
241
+ message,
242
+ category=category,
243
+ filename=filename,
244
+ lineno=lineno,
245
+ module=module,
246
+ )
129
247
  _WARNED_FEATURES.add(warning_key)
130
248
 
131
249
 
@@ -150,7 +268,6 @@ def _add_runtime_warning(
150
268
  feature_id=feature_id,
151
269
  object_name=object_name,
152
270
  category=category,
153
- stacklevel=3,
154
271
  )
155
272
  if original_new is not object.__new__:
156
273
  return original_new(cls, *args, **kwargs)
@@ -171,7 +288,6 @@ def _add_runtime_warning(
171
288
  feature_id=feature_id,
172
289
  object_name=object_name,
173
290
  category=category,
174
- stacklevel=3,
175
291
  )
176
292
  return original_init_subclass_func(*args, **kwargs)
177
293
 
@@ -185,7 +301,6 @@ def _add_runtime_warning(
185
301
  feature_id=feature_id,
186
302
  object_name=object_name,
187
303
  category=category,
188
- stacklevel=3,
189
304
  )
190
305
  return original_init_subclass(*args, **kwargs)
191
306
 
@@ -200,7 +315,6 @@ def _add_runtime_warning(
200
315
  feature_id=feature_id,
201
316
  object_name=object_name,
202
317
  category=category,
203
- stacklevel=3,
204
318
  )
205
319
  return obj(*args, **kwargs)
206
320