Skip to content

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

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

bugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)pythonUsage: [Issues, PRs], Target: Python

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions