Source code for tool_context

"""Shared context object passed to tools that need platform access.

Tools opt-in to receiving context by declaring a ``ctx`` parameter in
their ``run()`` signature.  The :class:`ToolRegistry` inspects the
handler at call-time and injects the context automatically -- the LLM
never sees or fills this parameter.
"""

from __future__ import annotations

import uuid as _uuid
from dataclasses import dataclass, field
from typing import Any, TYPE_CHECKING


[docs] @dataclass class ToolCallRecord: """One tool call's full execution trace. Stored as a hidden Redis message after each inference turn so the agent can retrieve exact inputs/outputs on demand via the ``retrieve_tool_call_log`` tool. """ record_id: str = field(default_factory=lambda: _uuid.uuid4().hex) """Unique ID for this individual record (used as Redis key suffix).""" tool_name: str = "" """Name of the tool that was called.""" raw_arguments_json: str = "" """Exact JSON payload generated by the LLM (before parsing).""" result_output: str = "" """Full execution output returned by the tool handler.""" success: bool = True """Whether the tool call completed without error.""" execution_start: float = 0.0 """Wall-clock ``time.time()`` when execution began.""" execution_end: float = 0.0 """Wall-clock ``time.time()`` when execution finished.""" duration_ms: float = 0.0 """Elapsed wall-clock milliseconds.""" order_index: int = 0 """Sequential index within the turn (global across all rounds).""" round_number: int = 0 """Which tool-call round in the ``chat()`` loop (0-indexed).""" turn_summary_id: str = "" """Back-reference to the parent Stargazer-System-Log summary (set after write)."""
if TYPE_CHECKING: from config import Config from conversation import ConversationManager from knowledge_graph import KnowledgeGraphManager from message_cache import MessageCache from platforms.base import PlatformAdapter from task_manager import TaskManager from threadweave import ThreadweaveManager from tools import ToolRegistry
[docs] @dataclass class ToolContext: """Runtime context injected into tool handlers that opt in to it. A single mutable bag of per-message state and shared service handles that the tool-execution layer hands to any tool whose ``run()`` declares a ``ctx`` parameter. It exists so tools can reach platform adapters, Redis, the knowledge graph, the conversation/message caches, the live OpenRouterClient, and sibling managers without importing them directly (which would create circular imports) and without the LLM ever seeing these fields — the registry inspects the handler signature and injects this object behind the model's back. Beyond carrying read-only handles, several fields are write-back channels: tool handlers append to ``injected_tools``, ``sent_files``, ``sent_rich_messages``, and ``tools_executed`` during a round, and :meth:`OpenRouterClient.chat` reads/clears them between rounds to merge newly injected tools and to surface media the bot emitted. Because parallel tools in one model round share the same instance and run under ``asyncio.gather``, these mutations must stay append-only/atomic. Instances are constructed by the message-processing paths that drive tool calls — e.g. ``message_processor/generate_and_send.py``, ``message_processor/channel_heartbeat.py``, ``core/outbound_consumer.py``, and ``kg_agentic_extraction.py`` — and by the swarm/test harnesses. The individual fields are documented by their per-attribute docstrings below. """ platform: str = "" """Short identifier for the originating platform (``"discord"``, ``"matrix"``, ...).""" channel_id: str = "" """Platform-specific channel / room identifier.""" user_id: str = "" """Platform-specific sender identifier.""" user_name: str = "" """Human-readable display name of the sender.""" guild_id: str = "" """Discord-specific guild (server) ID. Empty on non-Discord platforms.""" adapter: PlatformAdapter | None = None """The :class:`PlatformAdapter` instance for the originating platform. For Discord this is a :class:`DiscordPlatform` whose ``.client`` property exposes the underlying :class:`discord.Client`. """ message_id: str = "" """Platform-specific ID of the message that triggered this response.""" config: Config | None = None """Global bot :class:`Config`. Gives tools access to ``redis_url``, ``model``, and other settings.""" redis: Any = None """Shared async Redis client (``redis.asyncio.Redis``). ``None`` when Redis is not configured. Tools should guard access with an ``if ctx.redis is not None`` check. """ message_cache: MessageCache | None = None """Shared :class:`MessageCache` instance for retrieving cached messages as :class:`~message_cache.CachedMessage` objects. ``None`` when Redis is not configured. """ kg_manager: KnowledgeGraphManager | None = None """Shared :class:`KnowledgeGraphManager` for the knowledge graph system. ``None`` when Redis is not configured. """ task_manager: TaskManager | None = None """Shared :class:`TaskManager` for checking background task results. Injected by the message processor so tools like ``check_task`` can access it. """ threadweave: ThreadweaveManager | None = None """Shared :class:`ThreadweaveManager` for the Threadweave persistent knowledge system (DNA Vault, Persistent Weave, Shadow Memory). ``None`` when Redis is not configured. """ tool_registry: ToolRegistry | None = None """The live :class:`ToolRegistry` that holds all loaded tools. Allows meta-tools like ``list_all_tools`` and ``reload_tools`` to inspect or modify the running tool set. """ conversation_manager: ConversationManager | None = None """Shared :class:`ConversationManager` for per-channel conversation histories. Allows tools to read or manipulate the in-memory context window for any channel. """ openrouter: Any = None """The :class:`OpenRouterClient` instance for the current session. Used for shared HTTP/registry access. Effective tool-round limits for the active :meth:`~openrouter_client.OpenRouterClient.chat` call are scoped via :mod:`contextvars` (see :func:`~openrouter_client.get_tool_round_limit_box`); ``extend_tool_loop`` mutates that per-invocation state, not ``openrouter`` fields. """ all_adapters: list[Any] = field(default_factory=list) """All running :class:`PlatformAdapter` instances. Allows cross-platform tools (e.g. ``list_active_servers``) to query every connected platform, not just the one that triggered the current message. """ adapters_by_name: dict[str, Any] = field(default_factory=dict) """All platform adapters keyed by name (e.g. ``"discord"``, ``"discord-self"``, ``"matrix"``). Allows tools to look up a specific adapter directly:: selfbot = ctx.adapters_by_name.get("discord-self") """ disclosed_skill_ids: list[str] = field(default_factory=list) """Agent Skill ids disclosed in tier-1 catalog for this message (for ``activate_skill``).""" injected_tools: list[str] | None = None """Per-round staging list for ``request_tool_injection``. Tool handlers append here; after each tool round :meth:`OpenRouterClient.chat` merges these names into the live ``tool_names`` list passed to the LLM and then clears this field. Each round starts with ``injected_tools = []`` so parallel tools in one round share one list safely. The **model** keeps seeing merged tools on **every later round** of the same ``chat()`` call because the client extends the in-memory ``tool_names`` list — not because this attribute stays populated. When several tools run in the same model round they share this :class:`ToolContext` and execute concurrently via ``asyncio.gather``. Prefer ``list.extend`` / ``append``; avoid non-atomic read-modify-write patterns. Backgrounded tools (task manager) may still race with later rounds on the same ``ctx``—keep mutations minimal. """ injected_tools_session: list[str] = field(default_factory=list) """All tool names merged via injection during the current ``chat()`` loop. Appended when the client merges ``injected_tools`` into ``tool_names``; cleared at the start of ``chat()`` when ``record_executed_tools`` is true (same as ``tools_executed``). Use this to introspect which extra tools stayed active for the whole generation. """ sent_files: list[dict[str, Any]] = field(default_factory=list) """Media files sent to the channel during tool execution. Each entry has keys: ``data`` (raw ``bytes``), ``filename`` (``str``), ``mimetype`` (``str``), and optionally ``file_url`` (``str``). After the LLM call, the message processor converts these into real multimodal content parts (``image_url``, ``input_audio``, etc.) via :func:`~platforms.media_common.media_to_content_parts` and appends them to conversation history so the bot can see its own visual and audio outputs on subsequent turns. Parallel tools in one LLM round share this list; appends are safe, but do not assume ordering of entries matches tool-call order unless you serialize tool execution. """ sent_rich_messages: list[dict[str, Any]] = field(default_factory=list) """Rich Discord messages sent during tool execution. Each entry has keys: ``text`` (serialized LLM-visible content) and optionally ``message_id``. The message processor appends these to assistant history/cache so embed-only sends remain visible later. """ tools_executed: list[str] = field(default_factory=list) """Tool names executed during the current top-level :meth:`OpenRouterClient.chat` loop, in first-seen order (each name at most once). Populated by the client when ``record_executed_tools`` is true; cleared at the start of each such call. Nested ``chat()`` calls (e.g. subagents) should pass ``record_executed_tools=False`` to avoid polluting this list. """ observability_request_id: str = "" """Correlation id for one user message → LLM → reply (Redis observability).""" room_context: dict[str, Any] | None = None """Prompt ``room_context`` for the active channel (when built). Used by tools that need parity with the main system prompt, e.g. swarm subagent rendering of ``dominant_emotions`` / ``narrative_tone``. """ tool_call_records: list[ToolCallRecord] = field(default_factory=list) """Per-turn tool call execution traces. Populated by :meth:`OpenRouterClient.chat` after each tool round completes (post-gather barrier). Consumed by :func:`~message_processor.generate_and_send.run_generate_and_send` to write hidden Redis messages and the ``Stargazer-System-Log`` summary. Cleared at the start of each ``chat()`` call when ``record_executed_tools`` is true. """ persona_pref_manager: Any = None """Shared :class:`~persona_preferences.PersonaPreferenceManager` for the persona preference memory system. Allows persona preference tools (record, review, evolve, retract) to access the manager without a circular import. ``None`` when the feature is disabled or Redis is not configured. """ visual_memory: Any = None """Shared :class:`~visual_memory.VisualMemoryEngine` for the visual pattern recognition memory system. Allows visual memory tools (repost_image, label_visual_entity) to access the engine for image retrieval and entity management. ``None`` when visual memory is disabled or not initialized. """