Fold one assistant message into the tracker. Mirrors ``updateProgressFromMessage`` in ``LocalAgentTask.tsx:68-96``. Updates token counts (per the cumulative-vs-per-turn split), pushes one ``ToolActivity`` per non-blacklisted ``tool_use`` block, and drops the oldest activity on cap-5
(
tracker: ProgressTracker,
message: "AssistantMessage",
*,
resolve_activity_description: ActivityDescriptionResolver | None = None,
)
| 121 | |
| 122 | |
| 123 | def update_progress_from_message( |
| 124 | tracker: ProgressTracker, |
| 125 | message: "AssistantMessage", |
| 126 | *, |
| 127 | resolve_activity_description: ActivityDescriptionResolver | None = None, |
| 128 | ) -> None: |
| 129 | """Fold one assistant message into the tracker. |
| 130 | |
| 131 | Mirrors ``updateProgressFromMessage`` in ``LocalAgentTask.tsx:68-96``. |
| 132 | Updates token counts (per the cumulative-vs-per-turn split), pushes |
| 133 | one ``ToolActivity`` per non-blacklisted ``tool_use`` block, and |
| 134 | drops the oldest activity on cap-5 overflow. |
| 135 | |
| 136 | Tolerant of missing/partial usage payloads — ``message.usage`` is |
| 137 | ``dict[str, Any] | None`` per ``src.types.messages``, and a stub |
| 138 | test message that omits it is a no-op for token accounting. |
| 139 | """ |
| 140 | # Local imports defer the cycle: ``progress`` doesn't need to know |
| 141 | # about message types at module load. |
| 142 | from src.types.content_blocks import ToolUseBlock |
| 143 | from src.types.messages import AssistantMessage |
| 144 | |
| 145 | if not isinstance(message, AssistantMessage): |
| 146 | return |
| 147 | |
| 148 | usage = message.usage if isinstance(message.usage, dict) else None |
| 149 | if usage is not None: |
| 150 | # Cumulative-per-call inputs: include cache read/creation token |
| 151 | # counts so the total matches what the API billed for. |
| 152 | input_tokens = int(usage.get("input_tokens", 0) or 0) |
| 153 | cache_creation = int(usage.get("cache_creation_input_tokens", 0) or 0) |
| 154 | cache_read = int(usage.get("cache_read_input_tokens", 0) or 0) |
| 155 | tracker.latest_input_tokens = input_tokens + cache_creation + cache_read |
| 156 | |
| 157 | tracker.cumulative_output_tokens += int(usage.get("output_tokens", 0) or 0) |
| 158 | |
| 159 | content = message.content |
| 160 | if not isinstance(content, list): |
| 161 | return |
| 162 | |
| 163 | for block in content: |
| 164 | if not isinstance(block, ToolUseBlock): |
| 165 | continue |
| 166 | tracker.tool_use_count += 1 |
| 167 | if block.name in _PREVIEW_BLACKLIST: |
| 168 | # Counted in tool_use_count (chapter does the same) but |
| 169 | # omitted from the preview ring. |
| 170 | continue |
| 171 | block_input = getattr(block, "input", None) |
| 172 | if not isinstance(block_input, dict): |
| 173 | block_input = {} |
| 174 | description: str | None = None |
| 175 | if resolve_activity_description is not None: |
| 176 | try: |
| 177 | description = resolve_activity_description(block.name, block_input) |
| 178 | except Exception: |
| 179 | # A resolver bug must not poison the tracker. |
| 180 | description = None |