Skip to content

subagent.allowed_tools does not normalize model-facing tool names and can fail with "tools are required for this operation" #130

@Andy963

Description

@Andy963

Summary

subagent currently forwards allowed_tools as raw strings, without normalizing
model-facing tool names (for example schedule_list) back to runtime registry
names (for example schedule.list).

In practice, this can cause a subagent call that looks valid at the prompt/model
layer to fail later with a confusing runtime error: RuntimeError: invalid_input: tools are required for this operation.

Traceback (most recent call last):

  File "/home/andy/whisper/bot.py", line 32, in <module>
    main()
    └ <function main at 0x7fcad4394180>

  File "/home/andy/whisper/bot.py", line 28, in main
    bub_main()
    └ <function main at 0x7fcad41c65c0>

  File "/home/andy/whisper/app/main.py", line 67, in main
    run_gateway(workspace, enabled_channels=["telegram"])
    │           └ PosixPath('/home/andy/whisper')
    └ <function run_gateway at 0x7fcad0a123e0>

  File "/home/andy/whisper/adapter/bub/main.py", line 175, in run_gateway
    asyncio.run(_run())
    │       │   └ <function run_gateway.<locals>._run at 0x7fcad05f2fc0>
    │       └ <function run at 0x7fcad3fec220>
    └ <module 'asyncio' from '/usr/lib/python3.12/asyncio/__init__.py'>

  File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           │      │   └ <coroutine object run_gateway.<locals>._run at
0x7fcad0013740>
           │      └ <function Runner.run at 0x7fcad402dbc0>
           └ <asyncio.runners.Runner object at 0x7fcad0a63e30>
  File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           │    │     │                  └ <Task pending name='Task-1'
coro=<run_gateway.<locals>._run() running at /home/andy/whisper/adapter/bub/
main.py:169> wait_for...
           │    │     └ <function BaseEventLoop.run_until_complete at
0x7fcad402b7e0>
           │    └ <_UnixSelectorEventLoop running=True closed=False debug=False>
           └ <asyncio.runners.Runner object at 0x7fcad0a63e30>
  File "/usr/lib/python3.12/asyncio/base_events.py", line 674, in
run_until_complete
    self.run_forever()
    │    └ <function BaseEventLoop.run_forever at 0x7fcad402b740>
    └ <_UnixSelectorEventLoop running=True closed=False debug=False>
  File "/usr/lib/python3.12/asyncio/base_events.py", line 641, in run_forever
    self._run_once()
    │    └ <function BaseEventLoop._run_once at 0x7fcad402d580>
    └ <_UnixSelectorEventLoop running=True closed=False debug=False>
  File "/usr/lib/python3.12/asyncio/base_events.py", line 1987, in _run_once
    handle._run()
    │      └ <function Handle._run at 0x7fcad3fbda80>
    └ <Handle Task.task_wakeup(<Future finis...details=None)>)>
  File "/usr/lib/python3.12/asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
    │    │            │    │           │    └ <member '_args' of 'Handle'
objects>
    │    │            │    │           └ <Handle Task.task_wakeup(<Future
finis...details=None)>)>
    │    │            │    └ <member '_callback' of 'Handle' objects>
    │    │            └ <Handle Task.task_wakeup(<Future finis...details=None)>)>
    │    └ <member '_context' of 'Handle' objects>
    └ <Handle Task.task_wakeup(<Future finis...details=None)>)>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/bub/framework.py",
line 107, in process_inbound
    model_output = await self._hook_runtime.call_first(
                         │    │             └ <function HookRuntime.call_first at
0x7fcad0c5fec0>
                         │    └ <bub.hook_runtime.HookRuntime object at
0x7fcad0f59760>
                         └ <bub.framework.BubFramework object at 0x7fcad09dfec0>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/bub/
hook_runtime.py", line 25, in call_first
    value = await self._invoke_impl_async(
                  │    └ <function HookRuntime._invoke_impl_async at
0x7fcad0a80360>
                  └ <bub.hook_runtime.HookRuntime object at 0x7fcad0f59760>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/bub/
hook_runtime.py", line 130, in _invoke_impl_async
    value = await value
                  └ <coroutine object WhisperBuiltinImpl.run_model at
0x7fcad00bea70>

  File "/home/andy/whisper/adapter/bub/builtin.py", line 451, in run_model
    return await self.agent.run(prompt=prompt, session_id=session_id,
state=state)
                 │    │     │          │                  │                 └
{'_runtime_workspace': '/home/andy/whisper', 'session_id': 'telegram:786273482',
'_runtime_agent': <adapter.bub.builtin.Whisp...
                 │    │     │          │                  └ 'telegram:786273482'
                 │    │     │          └ 'channel=$telegram|
chat_id=786273482\n---\n{"message": "删除那个向目标迈进一步的定时任务",
"message_id": 16680, "type": "text", "username": "...
                 │    │     └ <function WhisperRuntimeAgent.run at
0x7fcad09e7ce0>
                 │    └ <adapter.bub.builtin.WhisperRuntimeAgent object at
0x7fcad0b19df0>
                 └ <adapter.bub.builtin.WhisperBuiltinImpl object at
0x7fcad0c6e030>

  File "/home/andy/whisper/adapter/bub/builtin.py", line 379, in run
    result = await super().run(

  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/bub/builtin/
agent.py", line 71, in run
    return await self._agent_loop(
                 │    └ <function Agent._agent_loop at 0x7fcad099f7e0>
                 └ <adapter.bub.builtin.WhisperRuntimeAgent object at
0x7fcad0b19df0>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/bub/builtin/
agent.py", line 131, in _agent_loop
    output = await self._run_tools_once(
                   │    └ <function Agent._run_tools_once at 0x7fcad099f920>
                   └ <adapter.bub.builtin.WhisperRuntimeAgent object at
0x7fcad0b19df0>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/bub/builtin/
agent.py", line 228, in _run_tools_once
    return await tape.run_tools_async(
                 │    └ <function Tape.run_tools_async at 0x7fcad0c51e40>
                 └ <Tape name=6a26aa2d706d5cad__d76b7480e7f83a6d>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/republic/tape/
session.py", line 270, in run_tools_async
    return await self._client.run_tools_async(
                 │    │       └ <function ChatClient.run_tools_async at
0x7fcad0c5c4a0>
                 │    └ <republic.clients.chat.ChatClient object at
0x7fcacfe13770>
                 └ <Tape name=6a26aa2d706d5cad__d76b7480e7f83a6d>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/republic/clients/
chat.py", line 1408, in run_tools_async
    return await self._execute_async(
                 │    └ <function ChatClient._execute_async at 0x7fcad0c53240>
                 └ <republic.clients.chat.ChatClient object at 0x7fcacfe13770>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/republic/clients/
chat.py", line 564, in _execute_async
    return await self._core.run_chat_async(
                 │    │     └ <function LLMCore.run_chat_async at 0x7fcad0c194e0>
                 │    └ <republic.core.execution.LLMCore object at
0x7fcacfe13950>
                 └ <republic.clients.chat.ChatClient object at 0x7fcacfe13770>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/republic/core/
execution.py", line 798, in run_chat_async
    result = await result
                   └ <coroutine object
ChatClient._handle_tools_auto_response_async at 0x7fcace57ba00>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/republic/clients/
chat.py", line 1156, in _handle_tools_auto_response_async
    execution = await self._tool_executor.execute_async(
                      │    │              └ <function ToolExecutor.execute_async
at 0x7fcad0c50d60>
                      │    └ <republic.tools.executor.ToolExecutor object at
0x7fcacfe13980>
                      └ <republic.clients.chat.ChatClient object at
0x7fcacfe13770>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/republic/tools/
executor.py", line 62, in execute_async
    result = await self._handle_tool_response_async(tool_response, tool_map,
context)
                   │    │                           │              │         └
ToolContext(tape='6a26aa2d706d5cad__d76b7480e7f83a6d',
run_id='f45f737aaf504d98b7b99a4cff50ba44', meta={'provider': 'openai',...
                   │    │                           │              └ {'bash':
Tool(name='bash', description='Run a shell command. Use background=true to keep
it running and fetch output later vi...
                   │    │                           └ {'function': {'name':
                   └ <coroutine object run_subagent at 0x7fcace27cee0>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/bub/builtin/
tools.py", line 270, in run_subagent
    return await agent.run(
                 │     └ <function WhisperRuntimeAgent.run at 0x7fcad09e7ce0>
                 └ <adapter.bub.builtin.WhisperRuntimeAgent object at
                 └ <adapter.bub.builtin.WhisperRuntimeAgent object at
0x7fcad0b19df0>
  File "/home/andy/whisper/.venv/lib/python3.12/site-packages/bub/builtin/
agent.py", line 194, in _agent_loop
    raise RuntimeError(outcome.error)
                       │       └ 'invalid_input: tools are required for this
operation.'
                       └ _ToolAutoOutcome(kind='error', text='',
error='invalid_input: tools are required for this operation.')

RuntimeError: invalid_input: tools are required for this operation.

Reproduction

A typical call looked like this:

tool.call.start name=subagent { prompt="...", session="temp",
allowed_tools=["schedule_list", "schedule_remove"] }

This later failed with:

RuntimeError: invalid_input: tools are required for this operation.

Full stack trace ends in bub.builtin.agent.Agent._agent_loop raising:

invalid_input: tools are required for this operation.

Why this is surprising

Bub already exposes model-facing tool names by replacing . with _ in
model_tools() / prompt rendering.

That means a caller can reasonably pass schedule_list instead of schedule.list.

However, subagent.allowed_tools currently does not normalize those names back to
the runtime registry form before calling agent.run(...).

Current implementation

In src/bub/builtin/tools.py, run_subagent() currently does:

if param.allowed_tools:
allowed_tools = set(param.allowed_tools) - {"subagent"}
else:
allowed_tools = set(REGISTRY.keys()) - {"subagent"}

But the runtime registry uses dotted names such as:

schedule.list
schedule.remove

So values like:

schedule_list
schedule_remove

do not actually match registered runtime tool names.

Actual behavior

When allowed_tools contains model-facing aliases or otherwise unnormalized names,
the subagent ends up with an invalid or effectively empty tool set for the
requested operation.

The eventual failure is delayed and surfaces as:

invalid_input: tools are required for this operation.

This makes the problem hard to diagnose from logs.

Expected behavior

subagent.allowed_tools should use the same canonical normalization as the model/
tool surface:

  • accept runtime names like schedule.list
  • accept model-facing aliases like schedule_list
  • trim surrounding whitespace
  • reject unknown tool names explicitly and early, instead of failing later with a
    generic runtime error
  • apply the same normalization to exclude / internal filtering paths as well

Impact

This affects subagent workflows that pass tool allowlists derived from:

  • prompt-visible tool names
  • model-generated tool names
  • user-configured allowlists that use _ aliases instead of . runtime names

The resulting failure message is misleading, so this looks like a real correctness

  • debuggability issue rather than just a UX papercut.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions