Skip to content

fix: resolve infinite loop when combining outputSchema and tools also added unit tests#412

Open
b2rpt wants to merge 1 commit into
google:mainfrom
b2rpt:fix/output-schema-tools-loop
Open

fix: resolve infinite loop when combining outputSchema and tools also added unit tests#412
b2rpt wants to merge 1 commit into
google:mainfrom
b2rpt:fix/output-schema-tools-loop

Conversation

@b2rpt

@b2rpt b2rpt commented Jun 6, 2026

Copy link
Copy Markdown

Please ensure you have read the [contribution guide](https://google.github.io/adk-
docs/contributing-guide/) before creating a pull request.

### Link to Issue or Description of Change

**1. Link to an existing issue (if applicable):**

- Closes: #411
- Related: https://github.com/google/adk-js/issues/411

**2. Or, if no issue exists, describe the change:**

**Problem:**
When an agent is configured with both `outputSchema` and tools:
1. The `BasicLlmRequestProcessor` sets `responseMimeType: "application/json"` and

responseSchema on the model configuration.
2. In the Gemini API, combining strict JSON mode and Function Calling (tools) in the same
call is architecturally unsupported:
- Stable models immediately return a 400 Bad Request API error.
- Preview models (such as gemini-3.1-flash-lite) get confused, calling tools
recursively and getting trapped in an infinite loop.

**Solution:**
We implemented the **Tool-Based Structured Output** pattern (aligning with `adk-python`):
1. **API-level bypass**: If tools are present, we bypass setting `responseMimeType:

"application/json"andresponseSchema at the Gemini API level (BasicLlmRequestProcessor), allowing standard function calling without API conflicts. 2. **Dynamic Tool Registration**: In LlmAgent.ts, we dynamically inject a special set_model_responsetool into the agent's toolset whenoutputSchemaand tools are both present. 3. **Filter Bypass**: Ensured that theset_model_responsetool is never filtered out by plugin-drivenallowedToolsrestrictions. 4. **System Instruction Injection**: Appended a system instruction instructing the model that it must submit its final structured output by calling theset_model_responsetool. 5. **Turn Interception**: Intercepted theset_model_responsecall in the agent's post- processing lifecycle to parse the arguments and format the final text JSON response, settingskipSummarization = true to break the agent execution loop and exit the turn immediately. Added safe-checks for TS compiler compatibility (toolContext` null-checks).

---

### Testing Plan

**Unit Tests:**

- [x] I have added or updated unit tests for my change.
- [x] All unit tests pass locally.

**Summary of unit test results:**
Run successfully via Vitest (`vitest --project unit:core --project unit:dev --run`):
- `basic_llm_request_processor_test.ts`: Passed (9 tests, including new verification that

outputSchema is not set at API level when tools are present).
- instructions_llm_request_processor_test.ts: Passed (5 tests, including new
verification that instructions to call set_model_response are successfully appended).
- Total test suite: 1,787 test cases successfully executed and passed.

**Manual End-to-End (E2E) Tests:**
To manually verify, run an agent configured with an `outputSchema` and one or more custom

tools. The agent will successfully execute the custom tools and then return a single
structured JSON response by calling set_model_response at the end of the turn without
triggering a 400 Bad Request or entering recursive tool call loops.

---

### Checklist

- [x] I have read the [CONTRIBUTING.md](https://github.com/google/adk-

js/blob/main/CONTRIBUTING.md) document.
- [x] I have performed a self-review of my own code.
- [x] I have commented my code, particularly in hard-to-understand areas.
- [x] I have added tests that prove my fix is effective or that my feature works.
- [x] New and existing unit tests pass locally with my changes.
- [x] I have manually tested my changes end-to-end.
- [ ] Any dependent changes have been merged and published in downstream modules.

@google-cla

google-cla Bot commented Jun 6, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant