Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/specs/004-python-function-calling-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,17 @@ that manually replay messages own the equivalent rule: do not resend an approval
not invent one.
- A completed function call/result pair is inert on later turns.
- Informational-only and declaration-only calls are not executed as local tools.
- For automatic local execution, provider arguments are JSON-parsed before function middleware. Schema-compatible
arguments retain the existing normalized mapping contract. A provisional validation failure is not terminal:
middleware receives the raw mapping and may repair it before calling `call_next()`. The innermost handler validates
changed or previously invalid arguments immediately before the tool body and writes the normalized mapping back to
`FunctionInvocationContext.arguments`; unchanged provisionally normalized arguments are reused without running
validators twice. Middleware that short-circuits without `call_next()` skips final validation and tool execution.
- Argument-repair middleware must precede security or policy middleware so enforcement observes the effective,
normalized invocation. Built-in security middleware normalizes hidden-value expansions before policy inspection.
Changing arguments after security middleware has processed them fails closed with `MiddlewareFailure`.
- Argument-validation failures after middleware retain the established `Argument parsing failed` result contract.
Exceptions raised by middleware or the tool body retain the separate `Function failed` contract.

### Reasoning-bound calls

Expand Down Expand Up @@ -414,6 +425,11 @@ that manually replay messages own the equivalent rule: do not resend an approval
- If policy middleware detects that the exact resolved invocation changed after approval, the old response executes
nothing and yields a caller-visible, session-persisted replacement request for the same occurrence; execution
requires a second approval and happens exactly once.
- If function middleware repairs an approval-bound call, the old response likewise executes nothing and yields a
caller-visible, session-persisted replacement request containing the repaired approval-visible arguments. The
replacement retains the call occurrence identity, rotates request-generation identity, and requires a second
approval before exactly-once execution. Security middleware transformations that preserve an approval-visible
placeholder do not disclose the resolved value or trigger a spurious replacement.
- If session-bound middleware no longer holds the reviewed authority because it expired or was evicted, the matched
response executes nothing and produces a replacement approval request with the same occurrence identity and a fresh
request generation. The replacement is caller-visible, becomes the authoritative pending session snapshot, and
Expand Down Expand Up @@ -583,6 +599,8 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Rejected execution | Rejection is a normal terminal result, not an exception to the caller. | `test_unapproved_tool_execution_raises_exception` |
| Approved tool exception | Generic and detailed error modes preserve one result and one execution. | `test_approved_function_call_with_error_without_detailed_errors`, `test_approved_function_call_with_error_with_detailed_errors` |
| Approved validation error | Validation failure returns one result without invoking the function body. | `test_approved_function_call_with_validation_error` |
| Pre-validation middleware repair | Schema-compatible calls retain normalized middleware arguments without duplicate validation. Prepared-value reuse does not require validator outputs to be copyable, and unchanged NaNs remain stable. When provisional validation fails, function middleware observes raw parsed arguments, may repair them before final validation, and the body receives normalized values; short-circuiting skips final validation and execution. Repair after security middleware fails closed using recursive type-aware, float-bit-exact comparison, including invalid and short-circuited mutations. Security inspects exact normalized values, and validation errors after hidden-value resolution do not disclose resolved values or mapping keys, including validator `TypeError` paths. | `test_function_middleware_keeps_normalized_arguments_for_valid_calls`, `test_prepared_arguments_support_noncopyable_validator_output`, `test_nan_prepared_and_approval_snapshots_are_stable`, `test_function_middleware_repairs_raw_arguments_before_validation`, `test_function_middleware_can_short_circuit_before_argument_validation`, `test_invalid_arguments_produced_by_middleware_keep_argument_error_contract`, `packages/core/tests/test_security.py::TestVariableArgumentPolicy::test_argument_mutation_after_security_middleware_fails_closed`, `test_security_snapshot_accepts_unchanged_nan`, `test_argument_mutation_after_security_short_circuit_fails_closed`, `test_security_policy_observes_custom_validator_transform_once`, `test_hidden_argument_validation_error_does_not_disclose_resolved_value`, `test_hidden_mapping_key_is_not_disclosed_by_validation_error`, `test_hidden_value_is_not_disclosed_by_validator_type_error`, `test_hidden_argument_can_be_normalized_after_security_check` |
| Approved middleware repair | Approval binds to the normalized middleware-entry representation, so ordinary Pydantic coercion still completes in one approval round. A changed approval-bound call executes zero times under the old grant, returns a persisted occurrence-bound replacement request in both response modes, and executes once only after the replacement is approved. Recursive type-aware, float-bit-exact comparison treats booleans and numbers, and positive and negative zero, as distinct while keeping unchanged NaNs stable. The same replacement rule applies when middleware short-circuits instead of calling the tool. Security expansion preserves approval-visible placeholders. | `test_approved_coercing_arguments_execute_without_replacement`, `test_approved_argument_repair_requires_replacement_approval`, `test_approved_argument_repair_short_circuit_requires_replacement_approval`, `test_approval_snapshot_distinguishes_exact_values`, `packages/core/tests/test_security.py::TestVariableArgumentPolicy::test_hidden_argument_resolution_does_not_require_reapproval` |
| Approved success | Successful approved execution returns one result. | `test_approved_function_call_successful_execution` |
| Consecutive error cap | Error threshold stops repeated failures, submits collected results, and makes only the required final no-tool model call. | `test_function_invocation_config_max_consecutive_errors`, `test_streaming_function_invocation_config_max_consecutive_errors`, `test_approval_resume_error_limit_forces_final_no_tool_response` |
| Unknown call handling | Configured false returns an error result; configured true raises. | `test_function_invocation_config_terminate_on_unknown_calls_false`, `test_function_invocation_config_terminate_on_unknown_calls_true`, streaming equivalents |
Expand Down
35 changes: 29 additions & 6 deletions python/packages/core/agent_framework/_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,15 @@ class FunctionInvocationContext:

Attributes:
function: The function being invoked.
arguments: The validated arguments for the function.
arguments: The function arguments. In the automatic function-calling loop,
schema-compatible provider arguments retain the existing normalized
mapping contract. If provisional normalization rejects provider
arguments, middleware instead receives the raw JSON-parsed mapping and
may repair it before calling ``call_next()``. The innermost handler
validates changed or previously invalid arguments immediately before
execution, then stores the normalized mapping back on this attribute.
Middleware that short-circuits without calling ``call_next()`` skips
final validation and function execution.
session: The agent session for this invocation, if any.
metadata: Metadata dictionary for sharing data between function middleware.
result: Function execution result. This attribute carries no guaranteed type.
Expand Down Expand Up @@ -438,7 +446,9 @@ def __init__(

Args:
function: The function being invoked.
arguments: The validated arguments for the function.
arguments: The function arguments. Automatic invocation supplies a normalized
mapping when provisional validation succeeds, otherwise the raw JSON-parsed
mapping so middleware can repair it before final validation.
session: The agent session for this invocation, if any.
metadata: Metadata dictionary for sharing data between function middleware.
result: Function execution result. Observed and overridden values do not
Expand Down Expand Up @@ -714,8 +724,16 @@ class FunctionMiddleware(ABC):
"""Abstract base class for function middleware that can intercept function invocations.

Function middleware allows you to intercept and modify function/tool invocations before
and after execution. You can validate arguments, cache results, log invocations, or
override function execution.
and after execution. On entry, schema-compatible calls retain normalized arguments.
When provisional normalization rejects provider arguments, middleware receives the raw
JSON-parsed mapping so it can repair provider-specific deviations before calling
``call_next()``. The innermost handler validates changed or previously invalid arguments
immediately before execution and updates ``context.arguments`` with normalized values.
You can also cache results, log invocations, or override function execution.

Argument-repair middleware must run before security or policy middleware so those
layers inspect the effective invocation. Changing arguments after security middleware
has processed them fails closed with :class:`MiddlewareFailure`.

Note:
FunctionMiddleware is an abstract base class. You must subclass it and implement
Expand Down Expand Up @@ -768,8 +786,13 @@ async def process(

Args:
context: Function invocation context containing function, arguments, and metadata.
MiddlewareTypes can set context.result to override execution, or observe
the actual execution result after calling call_next().
Before ``call_next()``, automatic invocation exposes normalized
arguments for schema-compatible calls and raw JSON-parsed arguments
when provisional validation failed. Middleware may inspect or replace
either mapping. After ``call_next()`` reaches the function, arguments
contain their validated, normalized values. MiddlewareTypes can set
context.result to override execution, or observe the actual execution
result after calling call_next().
call_next: Function to call the next middleware or final function execution.
Does not return anything - all data flows through the context.

Expand Down
Loading
Loading