From 90f376136ed428e60b08189ee30094cff8acc43f Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 9 Apr 2026 07:21:24 -0700 Subject: [PATCH] fix: always on tools --- core/framework/orchestrator/context.py | 55 ++++++++++++++++++++------ 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/core/framework/orchestrator/context.py b/core/framework/orchestrator/context.py index 8ede7244..aa977170 100644 --- a/core/framework/orchestrator/context.py +++ b/core/framework/orchestrator/context.py @@ -10,6 +10,7 @@ This module centralizes: from __future__ import annotations import asyncio +import logging from dataclasses import dataclass, field from typing import Any @@ -18,6 +19,24 @@ from framework.orchestrator.goal import Goal from framework.orchestrator.node import DataBuffer, NodeContext, NodeProtocol, NodeSpec from framework.tracker.decision_tracker import DecisionTracker +logger = logging.getLogger(__name__) + +# Tool names that are ALWAYS available to every node, regardless of +# the node's explicit tool policy. These are framework essentials that +# agents need unconditionally. +_ALWAYS_AVAILABLE_TOOLS: frozenset[str] = frozenset( + { + "read_file", + "write_file", + "edit_file", + "list_directory", + "search_files", + "hashline_edit", + "set_output", + "escalate", + } +) + @dataclass class GraphContext: @@ -128,28 +147,38 @@ def _resolve_available_tools( """Select tools available to the current node. Respects ``node_spec.tool_access_policy``: - - ``"all"`` -- all tools from the registry (no filtering). - - ``"explicit"`` -- only tools whose name appears in ``node_spec.tools``. - If the list is empty, **no tools** are given (default-deny). - - ``"none"`` -- no tools at all. + - ``"explicit"`` -- only tools whose name appears in ``node_spec.tools`` + PLUS framework-default tools (read_file, set_output, etc.). + If the list is empty, only defaults are given. + - ``"none"`` -- only framework-default tools (read_file, set_output, etc.). + + Framework-default tools (``_ALWAYS_AVAILABLE_TOOLS``) are always included + regardless of policy — agents need file I/O and output/escalate to function. """ if override_tools is not None: - return list(override_tools) + # Merge override with always-available, dedup by name + names = {t.name for t in override_tools} + extra = [t for t in tools if t.name in _ALWAYS_AVAILABLE_TOOLS and t.name not in names] + return list(override_tools) + extra policy = getattr(node_spec, "tool_access_policy", "explicit") + # Always include framework-default tools + always_tools = [t for t in tools if t.name in _ALWAYS_AVAILABLE_TOOLS] + if policy == "none": - return [] + return always_tools - if policy == "all": - return list(tools) - - # "explicit" (default): only tools named in node_spec.tools. + # "explicit" (default): declared tools + framework defaults if not node_spec.tools: - return [] + return always_tools - return [tool for tool in tools if tool.name in node_spec.tools] + declared = set(node_spec.tools) + declared_tools = [ + t for t in tools if t.name in declared and t.name not in _ALWAYS_AVAILABLE_TOOLS + ] + return always_tools + declared_tools def _derive_input_data(buffer: DataBuffer, input_keys: list[str]) -> dict[str, Any]: @@ -284,7 +313,7 @@ def build_node_context_from_graph_context( resolved_override_tools = override_tools if resolved_override_tools is None and gc.is_continuous and gc.cumulative_tools: if node_spec.tool_access_policy == "explicit" and node_spec.tools: - declared = set(node_spec.tools) + declared = set(node_spec.tools) | _ALWAYS_AVAILABLE_TOOLS resolved_override_tools = [t for t in gc.cumulative_tools if t.name in declared] else: resolved_override_tools = list(gc.cumulative_tools)