| 65 | |
| 66 | @dataclass |
| 67 | class ToolContext: |
| 68 | workspace_root: Path |
| 69 | permission_context: ToolPermissionContext = field( |
| 70 | default_factory=lambda: ToolPermissionContext(mode="bypassPermissions") |
| 71 | ) |
| 72 | cwd: Path | None = None |
| 73 | read_file_fingerprints: dict[Path, tuple[int, int] | tuple[int, int, bool]] = field(default_factory=dict) |
| 74 | task_manager: TaskManager = field(default_factory=TaskManager) |
| 75 | mcp_clients: dict[str, Any] = field(default_factory=dict) |
| 76 | lsp_client: Any | None = None |
| 77 | todos: list[dict[str, Any]] = field(default_factory=list) |
| 78 | tasks: dict[str, dict[str, Any]] = field(default_factory=dict) |
| 79 | # Chapter-10 / Chunk B / WI-1.3 — typed runtime-task registry. Houses |
| 80 | # ``LocalShellTaskState`` / ``LocalAgentTaskState`` / etc. as |
| 81 | # ``TaskStateBase`` subclasses. Replaces the un-typed |
| 82 | # ``background_bash_tasks`` and ``_internal=True`` agent entries that |
| 83 | # used to live on ``tasks``. ``runtime_tasks`` is the source of truth |
| 84 | # for the chapter-10 task state machine; ``tasks`` continues to host |
| 85 | # ``tasks_v2``/todo entries for the unrelated TaskCreate system. |
| 86 | runtime_tasks: RuntimeTaskRegistry = field(default_factory=RuntimeTaskRegistry) |
| 87 | # WI-5.1: per-message tool-result aggregate counter. The execution |
| 88 | # pipeline (Step 11) reads + increments this each time a tool result |
| 89 | # is mapped to its API form; when the running total exceeds |
| 90 | # ``MAX_TOOL_RESULTS_PER_MESSAGE_CHARS`` (default 200K) the next |
| 91 | # result is persisted to disk regardless of its individual size. |
| 92 | # Reset to 0 between messages by the turn-loop dispatcher. |
| 93 | # |
| 94 | # ``_aggregate_lock`` synchronizes the read-decide-write across |
| 95 | # concurrent tool dispatches (critic B6). The query loop's concurrent dispatch |
| 96 | # uses ``asyncio.to_thread`` to fan out concurrency-safe tools (Read, |
| 97 | # Grep, Glob) — without this lock, N threads would all read 0, all |
| 98 | # decide their block is under the cap, and the per-message budget |
| 99 | # would be silently bypassed. The full read+decide+write runs |
| 100 | # serialized so the persistence decision uses the LIVE counter and |
| 101 | # the cap is strictly enforced. Cost: the rare persist-to-disk path |
| 102 | # serializes against the lock, but persists are O(1) per turn in |
| 103 | # typical workloads (the common case under-threshold returns the |
| 104 | # block without I/O). |
| 105 | tool_result_chars_so_far: int = 0 |
| 106 | _aggregate_lock: threading.Lock = field(default_factory=threading.Lock) |
| 107 | # Session-cumulative tokens spent on client-side advisor calls. |
| 108 | # ``src/tool_system/tools/advisor.py`` accumulates here on every |
| 109 | # consultation; the status surface reads them to display |
| 110 | # ``advisor: <in>/<out>`` next to the worker's |
| 111 | # token counts. Distinct from ``tool_result_chars_so_far`` (which |
| 112 | # is a per-message budget tied to API-result persistence) — these |
| 113 | # are per-session totals for UI display. |
| 114 | advisor_input_tokens: int = 0 |
| 115 | advisor_output_tokens: int = 0 |
| 116 | # Chapter-10 / Chunk F / WI-6.1 — agent-name registry. Maps the |
| 117 | # human-readable ``name`` (passed via Agent({name: "researcher"})) |
| 118 | # to the random ``agent_id`` returned by the spawn. SendMessage |
| 119 | # consults this registry first when resolving a ``to:`` field; |
| 120 | # falling back to "treat ``to`` as a raw agent_id" when the name |
| 121 | # isn't registered preserves the legacy code path. |
| 122 | # |
| 123 | # Per Chunk-F-Phase-6 critic concern C1 (Phase-7 fix): the registry |
| 124 | # is a typed ``AgentNameRegistry`` (not a bare dict) so the |