Repository navigation
Add MaximumFunctionCallsPerRequest to toolautocall (#1202) - #1331
Lokesh Nelapati (Lokesh118) wants to merge 4 commits into
Conversation
This comment has been minimized.
This comment has been minimized.
ffb3fcd to
29f7445
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🟡 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
|
@microsoft-github-policy-service agree |
29f7445 to
f6f93ad
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
4d1b78d to
91c3ca5
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
|
||
| // 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- Approval Resumption & Re-entries: When incoming messages contain approval content (
hasAnyApprovalContent(messages) == true)—whether from human approval across turns or an auto-approval loop intoolapproval—toolautocallcontinues from the session's recorded consumed count. - Independent Runs: A new user prompt (
hasAnyApprovalContent(messages) == false) resets any leftover state in the session and starts with a fresh budget of0. - Cleanup: At the end of a run, if no pending approval requests were produced, a
defercleans up the state from the session.
Please let me know your thoughts on the updated design, thanks!
There was a problem hiding this comment.
Quim Muntal (@qmuntal) could you please review the updated PR? Thanks.
1b3edd2 to
4d2bb3d
Compare
This comment has been minimized.
This comment has been minimized.
4d2bb3d to
571f436
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🟡 Changes recommended
Tool-call budget accounting is lost across approval resumptions and loop iterations, allowing extra executions beyond the configured limit.
5 open issues
Restore the saved budget after binding approval responses · New Keep the tool budget active for the entire agent run · New Preserve budget state across native and hosted-tool approvals · New Associate deferred budgets with their pending approval groups · New Document that the limit is checked between tool batches · New
4 resolved since last review
Review effort: Max
571f436 to
7393a6d
Compare
This comment has been minimized.
This comment has been minimized.
7393a6d to
20faef3
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🟡 Changes recommended
Tool-call budgets are incorrectly preserved and scoped across loop re-entry and approval resumptions, allowing execution limits to be bypassed or fresh-turn budgets to be incorrectly reduced.
4 open issues
5 resolved since last review
Review effort: Max
| } else if session != nil && hasAnyApprovalResponse(messages) { | ||
| var state pendingApprovalRequestsState | ||
| if ok, err := session.Get(pendingApprovalRequestsStateKey, &state); err == nil && ok { | ||
| restoredApprovalConsumed = state.Consumed |
| newMsg, executedCount, errCount, err = f.processFunctionCalls(ctx, tools, functionCallContents, errCount) | ||
| totalFunctionCalls += executedCount |
| if state.Consumed != consumed { | ||
| state.Consumed = consumed | ||
| changed = true |
API consistency review
Changed Go contract
Upstream evidence reviewed
AssessmentThe new option is opt-in with a default of unlimited, which matches Python's
|


Description
Adds
MaximumFunctionCallsPerRequesttotoolautocall.Configto 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'smax_function_calls).Summary of Changes
Configuration & Documentation:
MaximumFunctionCallsPerRequest *inttotoolautocall.Config.MaximumFunctionCallsPerRequest must be 0 or greater).nilpreserves the default unlimited behavior.0disables automatic function calls while allowing the model to return a final response without schema tools.Accounting, Batch Handling & Review Follow-ups:
totalFunctionCalls) across iterations.functionInvocationStatusNotFound(unknown tools) from execution attempts so models can retry real tools whenTerminateOnUnknownCalls=false.FunctionResultContentin streamed updates and are not locally executed).prepareOptionsForLastIterationwhen its associated schema tool is stripped even if non-schema tools remain.pendingApprovalRequestsStateupon suspension and restored upon resumption, ensuring independent turns (including sequential runs usingcontext.Background()) start with a fresh budget without session pollution or context hacks.Tests:
TestFunctionInvoking_NegativeMaximumFunctionCallsReturnsErrorTestFunctionInvoking_ZeroMaximumFunctionCallsDisablesAutomaticCallsAndReturnsFinalResponseTestFunctionInvoking_MaximumFunctionCallsCapsSingleCallsPerIterationTestFunctionInvoking_MaximumFunctionCallsAllowsCurrentBatchToFinishTestFunctionInvoking_MaximumFunctionCallsCountsFailedExecutionsTestFunctionInvoking_MaximumFunctionCallsCountsPanickingExecutionsTestFunctionInvoking_MaximumFunctionCallsIgnoresNotFoundCallsTestFunctionInvoking_MaximumFunctionCallsIgnoresProviderHandledCallsTestFunctionInvoking_MaximumFunctionCallsIncludesApprovedCallsTestFunctionInvoking_ResumedApprovalWithZeroBudgetSkipsExecutionTestFunctionInvoking_ZeroBudgetClearsRequiredToolChoiceWhenSchemaToolRemovedTestFunctionInvoking_ResumedApprovalWithOnlyRequestIDRestoresNonzeroBudgetTestFunctionInvoking_PendingApprovalBudgetPreservedAcrossUnrelatedRunTestFunctionInvoking_HostedToolApprovalPreservesLocalBudgetWithoutCountingHostedCallsTestFunctionInvoking_MaximumFunctionCallsIndependentRunsWithBackgroundContextTestFunctionInvoking_StaleSessionConsumedIgnoredWithoutApprovalResponseTestFunctionInvoking_SafeToolsNotConvertedToApprovalsAtBudgetLimitTestToolApproval_SharesMaximumFunctionCallsAcrossAutoApprovedReEntriesTestToolApproval_SharesMaximumFunctionCallsAcrossHumanApprovalResumptionTestLoop_EachIterationReceivesConfiguredPerRequestBudgetgo test ./...) and race detector (go test -race ./agent/harness/...).Fixes #1202