Skip to content

feat: support @function_tool on instance methods (closes #94)#3693

Open
fede-kamel wants to merge 4 commits into
openai:mainfrom
fede-kamel:feat/function-tool-methods
Open

feat: support @function_tool on instance methods (closes #94)#3693
fede-kamel wants to merge 4 commits into
openai:mainfrom
fede-kamel:feat/function-tool-methods

Conversation

@fede-kamel

Copy link
Copy Markdown

Summary

Makes @function_tool work on instance methods. Today, decorating a method and passing the bound method fails because self ends up in the JSON schema (and isn't supplied at call time):

class Tools:
    @function_tool
    def get_random_number(self) -> int: ...

agent = Agent(..., tools=[Tools().get_random_number])
# -> BadRequestError: schema for 'self' must have a 'type' key

This implements the descriptor protocol on FunctionTool: when a method-backed tool is accessed via an instance, __get__ returns a copy bound to that instance. Binding rebuilds the tool from the bound method (whose signature omits self), so the schema and invocation are correct and self is passed automatically. Bound tools are cached per instance.

  • Module-level @function_tool functions are unaffected (only tools whose first parameter is self get a binder; class/non-instance access returns the tool unchanged).
  • Reuses the existing tool-construction path, so descriptions, guardrails, approval, timeouts, strict-schema, etc. are preserved on the bound tool.

This addresses @rm-openai's "I'll take a look" on the issue and removes the need for the function_tool(instance.method) workaround.

Test plan

  • New tests/test_function_tool_methods.py: self excluded from schema; invocation supplies self; distinct instances bind independently; per-instance caching; class-level access returns the unbound tool; module-level functions unaffected; and an end-to-end Runner.run with a FakeModel calling the bound method.
  • tests/test_function_tool.py + tests/test_function_schema.py (75 tests) still pass — no regression.
  • make format, make lint, make typecheck (mypy + pyright) clean. Docs example added under Tools › Function tools.

Issue number

Closes #94

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation
  • I've run make lint and make format
  • I've made sure tests pass

Make FunctionTool a descriptor: when a method decorated with @function_tool
is accessed via an instance, return a copy bound to that instance. The bound
method's signature omits self, so the JSON schema and invocation are correct
and self is supplied automatically. Module-level function tools and class
access are unaffected; bound tools are cached per instance.

Closes openai#94

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 395cbcb004

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/tool.py Outdated
Comment on lines +432 to +435
bound = cache.get(instance)
if bound is None:
bound = self._bind_to_instance(instance)
cache[instance] = bound

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid weak-key caching of bound method tools

This per-instance cache breaks for common tool-holder classes and can also leak instances. WeakKeyDictionary requires keys to be weak-referenceable and hashable, so a @dataclass tool class with default eq=True or a __slots__ class without __weakref__ raises TypeError just by evaluating instance.tool; for normal objects, the cached value is a bound FunctionTool whose closure retains MethodType(..., instance), so the weak key never becomes collectible. Consider avoiding the global per-instance cache or adding a fallback/cache design that does not strongly retain the instance.

Useful? React with 👍 / 👎.

Comment thread src/agents/tool.py Outdated
defer_loading=defer_loading,
sync_invoker=is_sync_function_tool,
)
_attach_self_binder(function_tool, the_func, _create_function_tool)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bind methods before validating context parameters

Attaching the binder only after the unbound method has already gone through function_schema means instance tools cannot use the existing context-parameter feature. A method like def lookup(self, ctx: RunContextWrapper[Any], x: int) -> str is rejected during class definition because the schema builder sees ctx as a non-first context parameter after self, even though the bound method's valid signature would have ctx first. The method should be bound or have self skipped before schema validation for method-backed tools.

Useful? React with 👍 / 👎.

- Drop the per-instance WeakKeyDictionary cache: it raised TypeError for
  unhashable / __slots__ tool-holder classes and strongly retained
  instances via the bound closure. __get__ now rebuilds the bound tool per
  access (cheap; typically accessed once when building the agent).
- Support methods that take RunContextWrapper: function_schema gains an
  opt-in skip_self so a leading self is skipped before context-position
  validation, fixing 'context param at non-first position' for
  def m(self, ctx, x). Gated by a __qualname__-based method check so a
  module-level function whose first arg is literally 'self' is unaffected.
- Tests for context-taking methods and the module-level self guard.
@seratch seratch added feature:core duplicate This issue or pull request already exists labels Jun 25, 2026
@seratch seratch changed the title Support @function_tool on instance methods (closes #94) feat: support @function_tool on instance methods (closes #94) Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

duplicate This issue or pull request already exists feature:core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add method_tool Functionality

2 participants