feat(delegate): show user's actual concurrency / spawn-depth limits in tool description (#22694)

The delegate_task tool description hardcoded 'default 3' / 'default 2' for
max_concurrent_children / max_spawn_depth, which misled the model on any
install that raised these limits — the schema text said 'default 3' even
when the user had set max_concurrent_children=15 / max_spawn_depth=3, so
the model would self-cap at 3 and never use the headroom.

Make the description dynamic. ToolEntry gains an optional
dynamic_schema_overrides callable; registry.get_definitions() merges its
output on top of the static schema before returning it. delegate_tool
registers a builder that reads the current delegation.* config and emits:

- 'up to N items concurrently for this user' (N = max_concurrent_children)
- 'Nested delegation IS enabled / OFF for this user (max_spawn_depth=N)'
- 'orchestrator children can themselves delegate up to M more level(s)'
- 'orchestrator_enabled=false' when the kill switch is set

The model_tools cache key already includes config.yaml mtime+size, so
edits to delegation.* in config invalidate the cached tool definitions
without an explicit hook. CLI_CONFIG staleness within a process is a
pre-existing limitation of _load_config and out of scope here.

Static description / tasks.description / role.description in
DELEGATE_TASK_SCHEMA are placeholders so module import doesn't trigger
cli.CLI_CONFIG load before the test conftest can redirect HERMES_HOME.
This commit is contained in:
Teknium 2026-05-09 11:07:53 -07:00 committed by GitHub
parent 000ddb8a93
commit 1f4200debf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 222 additions and 23 deletions

View file

@ -80,12 +80,12 @@ class ToolEntry:
__slots__ = (
"name", "toolset", "schema", "handler", "check_fn",
"requires_env", "is_async", "description", "emoji",
"max_result_size_chars",
"max_result_size_chars", "dynamic_schema_overrides",
)
def __init__(self, name, toolset, schema, handler, check_fn,
requires_env, is_async, description, emoji,
max_result_size_chars=None):
max_result_size_chars=None, dynamic_schema_overrides=None):
self.name = name
self.toolset = toolset
self.schema = schema
@ -96,6 +96,14 @@ class ToolEntry:
self.description = description
self.emoji = emoji
self.max_result_size_chars = max_result_size_chars
# Optional zero-arg callable returning a dict of schema overrides
# applied at get_definitions() time. Use for fields that depend on
# runtime config (e.g. delegate_task's description must reflect the
# user's current delegation.max_concurrent_children / max_spawn_depth
# so the model isn't told the wrong limits). The callable is invoked
# on every get_definitions() call; results are merged shallow on top
# of the base schema before the {"type": "function", ...} wrap.
self.dynamic_schema_overrides = dynamic_schema_overrides
# ---------------------------------------------------------------------------
@ -235,6 +243,7 @@ class ToolRegistry:
description: str = "",
emoji: str = "",
max_result_size_chars: int | float | None = None,
dynamic_schema_overrides: Callable = None,
):
"""Register a tool. Called at module-import time by each tool file."""
with self._lock:
@ -272,6 +281,7 @@ class ToolRegistry:
description=description or schema.get("description", ""),
emoji=emoji,
max_result_size_chars=max_result_size_chars,
dynamic_schema_overrides=dynamic_schema_overrides,
)
if check_fn and toolset not in self._toolset_checks:
self._toolset_checks[toolset] = check_fn
@ -337,6 +347,22 @@ class ToolRegistry:
continue
# Ensure schema always has a "name" field — use entry.name as fallback
schema_with_name = {**entry.schema, "name": entry.name}
# Apply runtime-dynamic overrides (e.g. delegate_task description
# depends on current delegation.max_concurrent_children /
# max_spawn_depth). Caller side (model_tools.get_tool_definitions)
# already keys its memo on config.yaml mtime + size, so changes
# to delegation.* in config invalidate the cache automatically.
if entry.dynamic_schema_overrides is not None:
try:
overrides = entry.dynamic_schema_overrides()
if isinstance(overrides, dict):
schema_with_name.update(overrides)
except Exception as exc:
logger.warning(
"dynamic_schema_overrides for tool %s raised %s; "
"using static schema",
name, exc,
)
result.append({"type": "function", "function": schema_with_name})
return result