"""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
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.
"""