Skip to content

Python: [Bug]: require_script_approval=True fails with 400 "Expected toolResult blocks" for code-defined (@script) skills #5672

@zhouyibing

Description

@zhouyibing

Description

Environment

  • agent_framework version: 1.2.2
  • Python: 3.10
  • Client: OpenAIChatCompletionClient (Anthropic-compatible endpoint)

Description

When require_script_approval=True is set on SkillsProvider and the skill uses a
code-defined script (registered via the @skill.script decorator), the approval
callback fails with a 400 Bad Request:

openai.BadRequestError: Error code: 400
{"message":"Expected toolResult blocks at messages.0.content for the following Ids:    
tooluse_xxx"}

Steps to Reproduce

from agent_framework import Skill, SkillsProvider
from agent_framework.openai import OpenAIChatCompletionClient
import asyncio, json

unit_converter_skill = Skill(
    name="unit-converter",
    description="Convert between common units using a conversion factor",
    content="Use the convert script to perform unit conversions.",
)

@unit_converter_skill.script(name="convert", description="Convert a value: result =    
value × factor")
def convert_units(value: float, factor: float) -> str:
    result = round(value * factor, 4)
    return json.dumps({"value": value, "factor": factor, "result": result})

skills_provider = SkillsProvider(
    skill_paths=[],
    skills=[unit_converter_skill],
    require_script_approval=True,
    instruction_template=(
        "Skills:\n{skills}\n{runner_instructions}"
    ),
)

agent = OpenAIChatCompletionClient("model", base_url="...", api_key="...").as_agent(   
    name="Agent",
    instructions="You are a helpful assistant.",
    context_providers=[skills_provider],
)

async def main():
    session = agent.create_session()
    result = await agent.run("Convert 26.2 miles to km, factor=1.60934",
session=session)

    # Approval loop
    while result.user_input_requests:
        approvals = [
            r.to_function_approval_response(approved=True)
            for r in result.user_input_requests
            if r.function_call is not None
        ]
        result = await agent.run(approvals, session=session)  # ← 400 here

asyncio.run(main())

Expected Behavior

After sending the approval response, the framework executes the inline convert_units
function in-process and returns the result to the API as a proper function_result
block.

Actual Behavior

The second agent.run(approvals) call raises a 400 error: Expected toolResult blocks at messages.0.content.

Root Cause Analysis

After reading the source code in _tools.py, I identified two compounding defects:

Defect 1 — Wrong object type passed to executor (_process_function_requests,

~line 2066)

approved_responses contains function_approval_response objects, but they are passed directly to execute_function_calls as function_calls=. The executor expects
function_call objects. The fix should extract the embedded function_call from each
approval response:

# Current (buggy)
approved_responses = [resp for resp in fcc_todo.values() if resp.approved]
results = await execute_function_calls(function_calls=approved_responses, ...)

# Should be
function_calls_to_execute = [resp.function_call for resp in approved_responses]        
results = await execute_function_calls(function_calls=function_calls_to_execute, ...)  

Defect 2 — tool_map missing run_skill_script in the approval turn

(_auto_invoke_function, ~line 1468)

When agent.run(approvals) is called (a turn with no user text), the SkillsProvider
tools (run_skill_script, load_skill, etc.) are not re-registered in the tool_map. As a result, tool_map.get("run_skill_script") returns None, and the function falls through to the "hosted tool" branch, returning the original
function_approval_response object instead of a function_result.

This means approved_function_results in _replace_approval_contents_with_results
ends up containing function_approval_response objects, not function_result objects. The API then correctly rejects the message.

Workaround

Remove require_script_approval=True (and script_runner) from SkillsProvider.
Code-defined scripts are executed in-process without needing a runner, so approval
gating does not apply to them cleanly at present.

Notes

  • File-based skills (with skill.path set) were not tested; the bug is specific to
    code-defined scripts registered via @skill.script.
  • The framework documentation correctly states that code-defined scripts "are always
    executed in-process and do not use a script runner," but require_script_approval
    still intercepts them and triggers the broken approval flow.

Code Sample

Error Messages / Stack Traces

Package Versions

agent-framework-core 1.2.2

Python Version

3.10.18

Additional Context

No response

Metadata

Metadata

Labels

bugSomething isn't workingpython

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions