Skip to content
4 changes: 4 additions & 0 deletions python/.github/skills/python-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Every `.py` file must start with:
- Use `Mapping` instead of `MutableMapping` for read-only input parameters
- Prefer `# type: ignore[...]` over unnecessary casts, or `isinstance` checks, when these are internally called and executed methods
But make sure the ignore is specific for both mypy and pyright so that we don't miss other mistakes
- Internal private helpers may be used across `agent_framework*` modules when intentional; use a targeted
`# pyright: ignore[reportPrivateUsage]` instead of making the helper public just to satisfy pyright.
- Do not add trivial pass-through or one-line helper functions solely to appease typing. Prefer targeted ignores,
casts, or clearer annotations over adding runtime overhead without a design benefit.

## Function Parameters

Expand Down
7 changes: 6 additions & 1 deletion python/CODING_STANDARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,12 @@ Use typing as a helper first and suppressions as a last resort:
(`# pyright: ignore[reportGeneralTypeIssues]`), file-level is allowed if there is a compelling reason for it, that should be documented right beneath the ignore.
Never change the global suppression flags unless the dev team okays it.
- **Private usage boundary**: Accessing private members across `agent_framework*` packages can be acceptable for this
codebase, but private member usage for non-Agent Framework dependencies should remain flagged.
codebase, but private member usage for non-Agent Framework dependencies should remain flagged. Do not make an
internal helper public merely to satisfy pyright private-usage checks inside the package; use a targeted
`# pyright: ignore[reportPrivateUsage]` when the internal dependency is intentional.
- **Avoid typing-only wrappers**: Do not introduce trivial pass-through functions solely to satisfy typing or private
usage checks. Prefer a targeted ignore, cast, or clearer annotation over an extra one-line function that adds runtime
overhead without improving the design.

## Function Parameter Guidelines

Expand Down
1 change: 1 addition & 0 deletions python/packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ agent_framework/
- **`MCPStdioTool`** / **`MCPStreamableHTTPTool`** / **`MCPWebsocketTool`** - Transport-specific subclasses.
- **Argument allowlist (`_prepare_call_kwargs`)** - Before each `tools/call`, kwargs are filtered to an **allowlist** built from the tool's declared parameters (`inputSchema.properties`) plus any user-configured extras. Framework runtime kwargs injected through the function-invocation pipeline (e.g. `thread`, `conversation_id`, `chat_options`, `options`, `response_format`) are stripped by default rather than forwarded. A tool that declares no usable `properties` (including schemas with `additionalProperties: true`) forwards only the configured extras. The `_MCP_FRAMEWORK_DENYLIST` is a safety net for framework-named params a server *declares* in its schema (those are dropped); names explicitly opted in via `additional_tool_argument_names` always win. The reserved `_meta` key is never forwarded as an argument; trusted caller/runtime `_meta` is validated as MCP request metadata, model-supplied `_meta` is discarded in generated MCP functions, and metadata precedence is caller/runtime < OpenTelemetry < tools/list metadata.
- **`allowed_tools`** (constructor arg on all `MCPTool` subclasses) - Restricts exposed MCP tools by raw remote MCP tool identity. Prefixed local names remain accepted only when the raw remote name already matches its normalized form; normalized/local aliases do not authorize a different raw remote name. If multiple raw remote tool names map to the same local function name, tool loading raises `ToolExecutionException` instead of first-one-wins shadowing.
- **Progressive MCP disclosure** (`use_progressive_disclosure`, `always_load`) - When enabled on any `MCPTool` subclass, the initial model-facing surface is loader tools (`list_mcp_tools` / `load_tool` / `unload_tool`, prefixed by `tool_name_prefix` when configured) plus allowed tools selected by `always_load` and tools loaded earlier on the same `MCPTool` instance. `list_mcp_tools` only reports tools that pass `allowed_tools`; filtered tools are not listed or loadable. Loader tool names are reserved in progressive mode: remote MCP tools whose local generated name collides with a loader name are omitted from the initial/listed surface, and explicit `load_tool` calls return a model-visible message pointing callers to `tool_name_prefix` or excluding the colliding tool. `load_tool` accepts one tool name or a list of tool names and uses `FunctionInvocationContext.add_tools(...)` so the selected generated MCP `FunctionTool`s become available on the next function-calling iteration while keeping existing approval mode, argument filtering, header-provider runtime kwargs, result parsing, OTel, and task behavior. `unload_tool` accepts one dynamically loaded tool name or a list of names and removes them from the live tool list and persisted progressive surface, but it does not remove tools configured in `always_load`. Invalid `always_load` entries are ignored like unmatched `allowed_tools` entries.
- **`additional_tool_argument_names`** (constructor arg on all `MCPTool` subclasses) - Opt extra argument names back into the allowlist. Accepts a `Sequence[str]` (applied to every tool) or a `Mapping[str, Sequence[str]]` keyed by **remote tool name**, where the reserved key `"*"` denotes global extras. It is configured only in user code at construction; there is **no per-call/runtime override**, so a model-issued tool call cannot change which names pass through. To use a server that accepts `additionalProperties: true`, list the extra names here and then either (1) manually extend that tool's `inputSchema` (via the `.functions` list after connecting) so the model is prompted to supply them, or (2) supply the values yourself via `function_invocation_kwargs`. If a normal forwarded argument name is supplied by both the model and `function_invocation_kwargs`, the model-supplied value wins; `_meta` is the exception and only trusted runtime/caller metadata is used.
- **Sampling guardrails** (`sampling_callback`) - Passing `client=` advertises `SamplingCapability` so the server can send `sampling/createMessage`. Because remote servers are untrusted (confused-deputy risk), the default `sampling_callback` is **deny-by-default** and applies, in order: a per-session rate limit (`sampling_max_requests`, default `_DEFAULT_SAMPLING_MAX_REQUESTS`), an approval gate (`sampling_approval_callback`), and a `maxTokens` cap (`sampling_max_tokens`, default `_DEFAULT_SAMPLING_MAX_TOKENS`). The approval callback (constructor arg on all subclasses; exported type alias `SamplingApprovalCallback`) receives the raw `CreateMessageRequestParams`, may be sync or async, and must return truthy to approve. When it is `None` (the default) every sampling request is denied; pass `lambda params: True` to restore legacy auto-approve as an explicit opt-in. Requests and denials are logged at WARNING (content is not logged). The per-session counter resets in `_reset_session_state`.
- **`MCPTaskOptions`** (experimental, `MCP_LONG_RUNNING_TASKS` feature, **frozen**) - Per-tool-instance options controlling the SEP-2663 long-running task lifecycle. When the server advertises a tool with `execution.taskSupport == "required"`, `MCPTool.call_tool` transparently routes through `call_tool_as_task`, which sends an augmented `tools/call`, polls `tasks/get` until terminal, and reinterprets `tasks/result` as a normal `CallToolResult`. Instances are immutable; replace via `MCPTool.task_options = MCPTaskOptions(...)`. Fields:
Expand Down
7 changes: 4 additions & 3 deletions python/packages/core/agent_framework/_feature_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,15 +226,16 @@ def _resolve_user_frame() -> tuple[str, int, str] | None:
def _warn_on_feature_use(
*,
stage: FeatureStageName,
feature_id: str,
feature_id: str | Enum,
object_name: str,
category: type[Warning],
) -> None:
warning_key = (category, feature_id)
normalized_feature_id = _normalize_feature_id(feature_id)
warning_key = (category, normalized_feature_id)
if warning_key in _WARNED_FEATURES:
return

message = _build_stage_warning_message(stage=stage, feature_id=feature_id, object_name=object_name)
message = _build_stage_warning_message(stage=stage, feature_id=normalized_feature_id, object_name=object_name)
user_frame = _resolve_user_frame()
if user_frame is None:
# Last-resort fallback: emit at the immediate caller of this helper.
Expand Down
Loading
Loading