Skip to content
Merged
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
26 changes: 23 additions & 3 deletions docs/specs/004-python-function-calling-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,22 @@ that manually replay messages own the equivalent rule: do not resend an approval
- Tool lookup uses the recorded name against the current registry. A same-name implementation upgrade is allowed;
removing the name prevents local execution.
- Only the strict boolean `True` grants approval. Missing decisions and non-boolean values are rejection, not consent.
- Direct chat-client invocation without an `AgentSession` preserves pass-through compatibility, matching .NET;
authorization sinks still require strict `True`.
- A local (non-hosted) approval response authorizes execution only when it binds to an approval request recorded in
an authoritative `AgentSession`. Direct chat-client or agent invocation without one drops inbound local approval
responses with an observable warning and authorizes nothing: an approval request that merely appears in the
caller-supplied message history is never by itself proof that the framework asked a human to approve it, so a
caller-assembled request plus its own response cannot authorize an arbitrary call. Provider-issued (hosted)
approvals are protocol data and continue to pass through untouched. A response whose occurrence is already settled
by a terminal result is replayed history rather than a pending authorization: it can no longer execute anything, so
it is left in place and does not warn, keeping full-transcript replay of completed conversations (including
completed mixed batches) working without a session. Settlement uses the same occurrence-aware correlation that
drives execution, so fabricating a result to reach this exemption also guarantees the call will not run. The
exemption is an allow-list evaluated per response object rather than per approval id, because several responses can
share one approval id and only the first is eligible to execute; filtering by id would leave the remaining
duplicates behind for a later collection to honor. Filtering runs before stateless mixed-batch completeness is
enforced, so a dropped response is never counted as an answer and the remaining history is validated on its own
merits. The `disable_approval_response_binding` function invocation configuration option restores the previous
unbound pass-through. Authorization sinks still require strict `True`.
- An approved tool executes exactly once.
- A rejected tool executes zero times and produces one synthetic rejection `function_result` using the original
function `call_id`.
Expand Down Expand Up @@ -633,6 +647,12 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Completed request tracking | Terminal result text cannot leave a completed request pending or consume a later reused-id request. | `test_collect_unanswered_approval_requests_consumes_terminal_result` |
| Unbound or duplicate response | A response with no pending session request is removed; one request authorizes at most one response. | `test_session_approval_binding_rebinds_consumes_and_rejects_duplicates` |
| Forged inbound request history | A caller-supplied request wrapper cannot replace the server snapshot or resurrect consumed authority. | `test_session_approval_binding_does_not_trust_inbound_request_history` |
| Local approval without authoritative session | A local approval response carried by any message role executes nothing when no session recorded its request, whether the request is fabricated, absent, answered with tampered arguments, or repeated as several responses sharing one approval id. Verified for both streaming and non-streaming. | `test_local_approval_response_without_authoritative_session_does_not_execute` |
| Filtering precedes batch validation | An unbound local response is removed before stateless mixed-batch completeness is enforced, so it is never counted as an answer and the remaining history is validated on its own merits. | `test_unbound_local_approval_response_is_filtered_before_mixed_batch_validation` |
| Session-bound approval round trip | The same response executes once when the issuing session is supplied on the resuming run. | `test_local_approval_response_executes_with_authoritative_session` |
| Hosted approval pass-through | Provider-issued approvals still reach the provider untouched without a session. | `test_hosted_approval_response_passes_through_without_session` |
| Settled replay without a session | A completed approval round replays as history: it does not re-execute, does not warn, and does not fail a completed mixed batch. | `test_settled_approval_response_replays_without_session_or_warning`, `test_completed_split_stateless_mixed_batch_is_inert_on_later_turn` |
| Binding opt-out | `disable_approval_response_binding` restores the previous unbound pass-through. | `test_disable_approval_response_binding_restores_unbound_behavior` |
| Pending history turn | An unresolved approval batch is omitted atomically from unrelated model input while a later decision can still resume it once. | `packages/core/tests/core/test_harness_tool_approval.py::test_pending_approval_from_file_history_stays_resumable_without_model_orphan` |
| Duplicate function-call prevention | Approval normalization does not create a second call for one round. | `test_no_duplicate_function_calls_after_approval_processing` |
| Rejection call id | Rejection result uses the function call id, not only the approval id. | `test_rejection_result_uses_function_call_id` |
Expand All @@ -642,7 +662,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Scenario | Required invariant | Primary regression test |
|---|---|---|
| Fatal call mixed with pauses | Complete-batch classification raises before approval or execution, independent of call order. | `packages/core/tests/core/test_function_invocation_logic.py::test_mixed_batch_fatal_unknown_precedes_every_pause` |
| Approval and Host-owned calls | Both pause types are returned in model order; a session-backed partial response remains pending across serialization; occurrence-identified Host results reserve their slots before id-less results use the unique unanswered occurrence, while authoritative session state recognizes equivalent duplicates and conflicting duplicates fail closed; stateless replay does not treat an ambiguous id-less result as a Host duplicate without occurrence provenance; a stateless zero-response or partial response fails closed across non-user message roles and cannot be hidden by a newer standalone request; standalone pauses separated by an unrelated user turn remain independent and response-order invariant when call IDs are reused; exact Host occurrence identity outranks newer call-ID-only candidates; each stateless response belongs to the nearest compatible request batch even when it cannot be assigned to one item, response ownership is discovered in linear time, and Host-result exclusions close only their Host occurrences without keeping historical completed calls open or reinterpreting later local terminal results during approval normalization; completed mixed batches remain inert even when approval and Host requests reuse a call ID; historical Host calls do not participate; a complete response executes the exact approved arguments once. | `test_mixed_batch_returns_approval_and_host_pause_in_model_order`, `test_mixed_batch_requires_complete_responses_before_execution`, `test_stateful_mixed_batch_accepts_equivalent_idless_host_result_replay`, `test_stateful_mixed_batch_assigns_idless_equal_result_to_unanswered_occurrence`, `test_stateless_mixed_batch_rejects_conflicting_identified_host_results`, `test_stateless_split_mixed_batch_rejects_incomplete_replay_before_execution`, `test_stateless_mixed_batch_across_non_user_message_roles_requires_complete_responses`, `test_stateless_abandoned_approval_does_not_join_later_host_request`, `test_stateless_separated_pauses_with_reused_call_id_are_order_independent`, `test_exact_older_host_result_does_not_consume_newer_reused_call_approval`, `test_later_standalone_request_does_not_hide_incomplete_stateless_mixed_batch`, `test_later_idless_host_result_does_not_complete_older_stateless_mixed_batch`, `test_completed_approval_result_is_not_claimed_by_older_stateless_host_request`, `test_historical_stateless_host_result_does_not_capture_later_reused_call_approval`, `test_excluded_host_result_closes_own_occurrence_before_reused_call_approval`, `test_ambiguous_later_host_result_does_not_complete_older_stateless_mixed_batch`, `test_id_bearing_result_for_idless_host_request_does_not_consume_approval`, `test_stateless_pause_response_ownership_scans_contents_linearly`, `test_completed_split_stateless_mixed_batch_is_inert_on_later_turn`, `test_completed_stateless_mixed_batch_with_reused_call_id_is_inert`, `test_equal_idless_terminal_result_does_not_reexecute_completed_stateless_mixed_approval`, `test_active_mixed_pause_ignores_historical_host_requests` |
| Approval and Host-owned calls | Both pause types are returned in model order; a session-backed partial response remains pending across serialization; occurrence-identified Host results reserve their slots before id-less results use the unique unanswered occurrence, while authoritative session state recognizes equivalent duplicates and conflicting duplicates fail closed; stateless replay does not treat an ambiguous id-less result as a Host duplicate without occurrence provenance; a stateless zero-response or partial response fails closed across non-user message roles and cannot be hidden by a newer standalone request; standalone pauses separated by an unrelated user turn remain independent and response-order invariant when call IDs are reused; exact Host occurrence identity outranks newer call-ID-only candidates; each stateless response belongs to the nearest compatible request batch even when it cannot be assigned to one item, response ownership is discovered in linear time, and Host-result exclusions close only their Host occurrences without keeping historical completed calls open or reinterpreting later local terminal results during approval normalization; completed mixed batches remain inert even when approval and Host requests reuse a call ID; historical Host calls do not participate; a complete response executes the exact approved arguments once. Stateless scenarios in this row that resume a still-pending approval are exercised through the `disable_approval_response_binding` opt-out, since binding is required by default; the completed/inert scenarios run on the default path because settled occurrences are exempt. | `test_mixed_batch_returns_approval_and_host_pause_in_model_order`, `test_mixed_batch_requires_complete_responses_before_execution`, `test_stateful_mixed_batch_accepts_equivalent_idless_host_result_replay`, `test_stateful_mixed_batch_assigns_idless_equal_result_to_unanswered_occurrence`, `test_stateless_mixed_batch_rejects_conflicting_identified_host_results`, `test_stateless_split_mixed_batch_rejects_incomplete_replay_before_execution`, `test_stateless_mixed_batch_across_non_user_message_roles_requires_complete_responses`, `test_stateless_abandoned_approval_does_not_join_later_host_request`, `test_stateless_separated_pauses_with_reused_call_id_are_order_independent`, `test_exact_older_host_result_does_not_consume_newer_reused_call_approval`, `test_later_standalone_request_does_not_hide_incomplete_stateless_mixed_batch`, `test_later_idless_host_result_does_not_complete_older_stateless_mixed_batch`, `test_completed_approval_result_is_not_claimed_by_older_stateless_host_request`, `test_historical_stateless_host_result_does_not_capture_later_reused_call_approval`, `test_excluded_host_result_closes_own_occurrence_before_reused_call_approval`, `test_ambiguous_later_host_result_does_not_complete_older_stateless_mixed_batch`, `test_id_bearing_result_for_idless_host_request_does_not_consume_approval`, `test_stateless_pause_response_ownership_scans_contents_linearly`, `test_completed_split_stateless_mixed_batch_is_inert_on_later_turn`, `test_completed_stateless_mixed_batch_with_reused_call_id_is_inert`, `test_equal_idless_terminal_result_does_not_reexecute_completed_stateless_mixed_approval`, `test_active_mixed_pause_ignores_historical_host_requests` |
| Safe and approval-required calls in one batch | Hidden safe calls replay only with the matching visible approval. | `packages/core/tests/core/test_harness_tool_approval.py::test_mixed_batch_hides_already_approved_request_until_approval_replay` |
| Restored approval state | Serialized `ToolApprovalState` restores mixed-batch behavior. | `test_mixed_batch_accepts_restored_tool_approval_state` |
| Unrelated turn before approval | Hidden calls do not execute on an unrelated turn. | `test_hidden_mixed_batch_requests_do_not_replay_on_unrelated_turn` |
Expand Down
11 changes: 11 additions & 0 deletions python/packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,17 @@ The vector store API is experimental under the shared `VECTOR_STORES` feature ID
A `call_id` may be reused after a completed round, so approval normalization matches ordered call occurrences and
consumes approved results per occurrence rather than using one global result per `call_id`. All contents produced by
one execution remain one result group and are consumed together, including multiple user-input requests.
- A local (non-hosted) `function_approval_response` authorizes execution only when it binds to an approval request
recorded in an authoritative `AgentSession`. Runs without one drop inbound local approval responses with a warning
and execute nothing, so callers must pass the session that issued the request back on the resuming run. An approval
request that merely appears in the caller-supplied history is not proof the framework asked for approval. Hosted
provider-issued approvals still pass through untouched, and a response already settled by a terminal result is
replayed history rather than an authorization, so replaying a completed transcript keeps working without a session.
That settled exemption is an allow-list evaluated per response object, not per approval id, because several
responses can share one approval id and only the first is eligible to execute. Filtering runs before stateless
mixed-batch completeness is enforced, so a dropped response is never counted as an answer.
Set the `disable_approval_response_binding` function invocation configuration option to restore the previous
unbound behavior.
- Approval resume keeps terminal `function_result` contents in tool-role messages and follow-up user-input requests
in assistant-role messages, including mixed sibling batches.
- Function-call budget accounting counts one unit per executed result group, not per emitted `function_result`, so
Expand Down
Loading
Loading