Skip to content

Add MaximumFunctionCallsPerRequest to toolautocall (#1202) - #1331

Open
Lokesh Nelapati (Lokesh118) wants to merge 4 commits into
microsoft:mainfrom
Lokesh118:issue-1202
Open

Lokesh Nelapati (Lokesh118) wants to merge 4 commits into
microsoft:mainfrom
Lokesh118:issue-1202

Conversation

@Lokesh118

@Lokesh118 Lokesh Nelapati (Lokesh118) commented Oct 27, 2026 •

Copy link
Copy Markdown

Description

Adds MaximumFunctionCallsPerRequest to toolautocall.Config to allow callers to cap the total cumulative number of tool invocations performed in a single provider tool-calling request (addressing #1202 and achieving parity with Python's max_function_calls).

Summary of Changes

  1. Configuration & Documentation:

    • Added MaximumFunctionCallsPerRequest *int to toolautocall.Config.
    • Validates that negative values return a configuration error (MaximumFunctionCallsPerRequest must be 0 or greater).
    • nil preserves the default unlimited behavior.
    • 0 disables automatic function calls while allowing the model to return a final response without schema tools.
    • Documented in godoc that the limit is evaluated before each batch of function calls; if the limit has not been reached, the active batch finishes even if it exceeds the limit. Once reached, subsequent rounds strip schema tools.
    • Documented suspension/resumption semantics: when human approval is required, execution suspends and pre-approval consumed calls carry forward to the resumed turn via the pending approval state. The budget is scoped to a single invocation of this middleware; independent calls — whether from separate user turns, loop middleware re-invocations, or any other upstream caller — each start with a fresh count.
  2. Accounting, Batch Handling & Review Follow-ups:

    • Tracks cumulative actual execution attempts (totalFunctionCalls) across iterations.
    • Includes executions that resulted in errors or panics.
    • Excludes functionInvocationStatusNotFound (unknown tools) from execution attempts so models can retry real tools when TerminateOnUnknownCalls=false.
    • Ignores provider-handled function calls (which already contain FunctionResultContent in streamed updates and are not locally executed).
    • Checks budget before dispatching approved calls on re-entry, emitting valid non-execution error results if budget is already exhausted so conversation history remains valid without unwanted tool execution.
    • Preserves safe tools as normal function calls during approval pausing instead of converting them into approval requests.
    • Clears named required tool choice in prepareOptionsForLastIteration when its associated schema tool is stripped even if non-schema tools remain.
    • Scopes budget accounting cleanly to approval transactions: consumed count is saved within pendingApprovalRequestsState upon suspension and restored upon resumption, ensuring independent turns (including sequential runs using context.Background()) start with a fresh budget without session pollution or context hacks.
    • Tracks budget suspension and restoration across native function and hosted-tool approvals without counting hosted executions against the local function call budget.
  3. Tests:

    • TestFunctionInvoking_NegativeMaximumFunctionCallsReturnsError
    • TestFunctionInvoking_ZeroMaximumFunctionCallsDisablesAutomaticCallsAndReturnsFinalResponse
    • TestFunctionInvoking_MaximumFunctionCallsCapsSingleCallsPerIteration
    • TestFunctionInvoking_MaximumFunctionCallsAllowsCurrentBatchToFinish
    • TestFunctionInvoking_MaximumFunctionCallsCountsFailedExecutions
    • TestFunctionInvoking_MaximumFunctionCallsCountsPanickingExecutions
    • TestFunctionInvoking_MaximumFunctionCallsIgnoresNotFoundCalls
    • TestFunctionInvoking_MaximumFunctionCallsIgnoresProviderHandledCalls
    • TestFunctionInvoking_MaximumFunctionCallsIncludesApprovedCalls
    • TestFunctionInvoking_ResumedApprovalWithZeroBudgetSkipsExecution
    • TestFunctionInvoking_ZeroBudgetClearsRequiredToolChoiceWhenSchemaToolRemoved
    • TestFunctionInvoking_ResumedApprovalWithOnlyRequestIDRestoresNonzeroBudget
    • TestFunctionInvoking_PendingApprovalBudgetPreservedAcrossUnrelatedRun
    • TestFunctionInvoking_HostedToolApprovalPreservesLocalBudgetWithoutCountingHostedCalls
    • TestFunctionInvoking_MaximumFunctionCallsIndependentRunsWithBackgroundContext
    • TestFunctionInvoking_StaleSessionConsumedIgnoredWithoutApprovalResponse
    • TestFunctionInvoking_SafeToolsNotConvertedToApprovalsAtBudgetLimit
    • TestToolApproval_SharesMaximumFunctionCallsAcrossAutoApprovedReEntries
    • TestToolApproval_SharesMaximumFunctionCallsAcrossHumanApprovalResumption
    • TestLoop_EachIterationReceivesConfiguredPerRequestBudget
    • Ran full test suite (go test ./...) and race detector (go test -race ./agent/harness/...).

Fixes #1202

@github-actions github-actions Bot added area:agent Changes files in the agent area parity-approved Go API consistency review found no parity issues public-api-change Pull Request changes public APIs labels Oct 27, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot removed the parity-approved Go API consistency review found no parity issues label Oct 29, 2026
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Tool-call budget enforcement can incorrectly block retries, allow approved calls beyond the limit, and leave an invalid forced tool choice when schema tools are removed.

4 open issues
What changed in this PR

Adds a configurable tool-call budget to toolautocall, complementing its existing iteration limit and addressing the cost-control use case in #1202.

Changes:

  • Adds MaximumFunctionCallsPerRequest, with negative-value validation, an unlimited default, and zero-budget handling.
  • Tracks calls across tool rounds, including failures and approved calls, and removes schema tools once the budget is reached while allowing active batches to finish.
  • Adds tests for limits, batch overshoot, failed executions, provider-handled calls, and approval resumption.
File Description
agent/​harness/​toolautocall/​autocall.go Adds configuration, call accounting, and budget-based loop termination.
agent/​harness/​toolautocall/​autocall_test.go Tests validation, budget boundaries, batch completion, and execution accounting.
agent/​harness/​toolautocall/​autocall_approval_test.go Tests counting approved calls toward the budget.

Review effort: Max

Comment thread agent/harness/toolautocall/autocall.go Resolved
Comment thread agent/harness/toolautocall/autocall.go Resolved
Comment thread agent/harness/toolautocall/autocall.go Resolved
Comment thread agent/harness/toolautocall/autocall.go Resolved
@Lokesh118

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@github-actions

This comment has been minimized.

@Lokesh118
Lokesh Nelapati (Lokesh118) marked this pull request as ready for review November 1, 2026 23:38
@Lokesh118
Lokesh Nelapati (Lokesh118) requested a review from a team as a code owner November 1, 2026 23:38
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment thread agent/agent.go Outdated

// RunToken returns an opaque unique identity for the current agent run,
// or nil if the context was not created by an agent run.
func RunToken(ctx context.Context) any {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't like passing params around in the context, unless the value they provide is high. I don't think this meets the bar. Any alternative design?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! Updated the design.

I have completely reverted the changes to agent/agent.go to match upstream/main, eliminating RunToken and context injection altogether.

Instead, toolautocall now manages the budget lifecycle strictly through agent.Session, aligning with the existing conventions in toolapproval, filememory, and todo:

  1. Approval Resumption & Re-entries: When incoming messages contain approval content (hasAnyApprovalContent(messages) == true)—whether from human approval across turns or an auto-approval loop in toolapproval—toolautocall continues from the session's recorded consumed count.
  2. Independent Runs: A new user prompt (hasAnyApprovalContent(messages) == false) resets any leftover state in the session and starts with a fresh budget of 0.
  3. Cleanup: At the end of a run, if no pending approval requests were produced, a defer cleans up the state from the session.

Please let me know your thoughts on the updated design, thanks!

@Lokesh118 Lokesh Nelapati (Lokesh118) Nov 4, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quim Muntal (@qmuntal) could you please review the updated PR? Thanks.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread agent/harness/toolautocall/autocall.go Resolved
Comment thread agent/harness/toolautocall/autocall.go Resolved
Comment thread agent/harness/toolautocall/autocall.go Resolved
Comment thread agent/harness/toolautocall/autocall.go Resolved
Comment thread agent/harness/toolautocall/autocall.go Resolved
@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Nov 4, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +312 to +315
} else if session != nil && hasAnyApprovalResponse(messages) {
var state pendingApprovalRequestsState
if ok, err := session.Get(pendingApprovalRequestsStateKey, &state); err == nil && ok {
restoredApprovalConsumed = state.Consumed
Comment thread agent/harness/toolautocall/autocall.go Resolved
Comment on lines +522 to +523
newMsg, executedCount, errCount, err = f.processFunctionCalls(ctx, tools, functionCallContents, errCount)
totalFunctionCalls += executedCount
Comment on lines +1058 to +1060
if state.Consumed != consumed {
state.Consumed = consumed
changed = true
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

API consistency review

Result Scope
aligned public API, user-visible behavior

Changed Go contract

  • New toolautocall.Config.MaximumFunctionCallsPerRequest *int. nil means unlimited, 0 disables local calls, and negative values return a config error.
  • Cumulative local execution count across iterations, including errors and panics. Unknown-tool and provider-handled calls are excluded. The count is carried across approval suspend/resume.
  • Schema tools (and a named required tool choice) are stripped once the limit is reached.

Upstream evidence reviewed

Implementation Source Contract checked
Python _tools.py FunctionInvocationConfiguration.max_function_calls Optional cumulative cap, default None (unlimited), best-effort batch semantics, then forces a text response
.NET FunctionInvokingChatClient.cs Only MaximumIterationsPerRequest and MaximumConsecutiveErrorsPerRequest exist; no cumulative function-call cap
.NET ToolApprovalAgentOptions.MaxAutoApprovalIterations Related per-run cap on re-invocations; not the same contract

Assessment

The new option is opt-in with a default of unlimited, which matches Python's max_function_calls default and semantics (cumulative count, best-effort batch, graceful degradation to a text response). I found no .NET counterpart in the reviewed snapshots, so this is a Go/Python capability that is ahead of .NET rather than a divergence from it. Maintainers may want to track a .NET follow-up. No actionable parity issues. The existing parity-approved and public-api-change labels are correct and unchanged.

Generated by Go API Consistency Review Agent · copilot · auto · 39.4 AIC · ⌖ 0.631 AIC · ⊞ 5.2K · ◷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Changes files in the agent area parity-approved Go API consistency review found no parity issues public-api-change Pull Request changes public APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a per-run limit for tool calls

3 participants