monkeybot 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- monkeybot/__init__.py +3 -0
- monkeybot/cli/__init__.py +3 -0
- monkeybot/cli/__main__.py +8 -0
- monkeybot/cli/audio_io.py +8 -0
- monkeybot/cli/gateway_manager.py +17 -0
- monkeybot/cli/main.py +22 -0
- monkeybot/cli/push_to_talk.py +12 -0
- monkeybot/cli/realtime_client.py +13 -0
- monkeybot/core/__init__.py +19 -0
- monkeybot/core/attachments/__init__.py +22 -0
- monkeybot/core/attachments/catalog.py +62 -0
- monkeybot/core/attachments/config.py +52 -0
- monkeybot/core/attachments/freeze.py +158 -0
- monkeybot/core/attachments/resolve.py +70 -0
- monkeybot/core/attachments/store.py +180 -0
- monkeybot/core/attachments/text.py +72 -0
- monkeybot/core/attachments/tools.py +54 -0
- monkeybot/core/bootstrap.py +242 -0
- monkeybot/core/config/__init__.py +71 -0
- monkeybot/core/config/realtime_config.py +150 -0
- monkeybot/core/config/runtime_env.py +262 -0
- monkeybot/core/config/settings.py +341 -0
- monkeybot/core/config/validation.py +249 -0
- monkeybot/core/config/yaml_loader.py +45 -0
- monkeybot/core/context/__init__.py +781 -0
- monkeybot/core/context/campaign_context.py +8 -0
- monkeybot/core/context/common.py +14 -0
- monkeybot/core/context/curator.py +255 -0
- monkeybot/core/context/epoch.py +226 -0
- monkeybot/core/context/memory_prompt.py +222 -0
- monkeybot/core/context/tool_output_policy.py +270 -0
- monkeybot/core/context/tool_result_ingress.py +290 -0
- monkeybot/core/context/tool_shapers.py +361 -0
- monkeybot/core/hooks/__init__.py +261 -0
- monkeybot/core/llm/__init__.py +4 -0
- monkeybot/core/llm/provider.py +296 -0
- monkeybot/core/llm/realtime_provider.py +203 -0
- monkeybot/core/llm/usage.py +57 -0
- monkeybot/core/logging_utils.py +24 -0
- monkeybot/core/mcp/__init__.py +1 -0
- monkeybot/core/mcp/mcp_client.py +1215 -0
- monkeybot/core/mcp/ports_mcp.py +109 -0
- monkeybot/core/memory/__init__.py +24 -0
- monkeybot/core/memory/hook.py +413 -0
- monkeybot/core/memory/index_format.py +104 -0
- monkeybot/core/memory/integrity.py +180 -0
- monkeybot/core/memory/organizer.py +270 -0
- monkeybot/core/memory/storage_ops.py +139 -0
- monkeybot/core/memory/subsystem.py +91 -0
- monkeybot/core/messages/__init__.py +16 -0
- monkeybot/core/messages/convert_provider.py +41 -0
- monkeybot/core/messages/tool_integrity.py +262 -0
- monkeybot/core/messages/transform_context.py +84 -0
- monkeybot/core/path_safety.py +11 -0
- monkeybot/core/persistence/__init__.py +17 -0
- monkeybot/core/persistence/backends.py +236 -0
- monkeybot/core/persistence/db.py +28 -0
- monkeybot/core/persistence/durable_runs.py +286 -0
- monkeybot/core/persistence/firestore.py +658 -0
- monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
- monkeybot/core/persistence/history.py +156 -0
- monkeybot/core/persistence/postgres.py +895 -0
- monkeybot/core/persistence/runs.py +76 -0
- monkeybot/core/persistence/scheduled_loops.py +435 -0
- monkeybot/core/persistence/session_turn_locks.py +94 -0
- monkeybot/core/persistence/sqlite.py +218 -0
- monkeybot/core/persistence/sqlite_backend.py +74 -0
- monkeybot/core/persistence/thread_summary.py +61 -0
- monkeybot/core/persistence/transcript.py +194 -0
- monkeybot/core/persistence/usage.py +149 -0
- monkeybot/core/prompts/__init__.py +1 -0
- monkeybot/core/prompts/harness_prompt.py +197 -0
- monkeybot/core/prompts/prompt.py +215 -0
- monkeybot/core/runtime/__init__.py +1 -0
- monkeybot/core/runtime/context_budget.py +267 -0
- monkeybot/core/runtime/events.py +819 -0
- monkeybot/core/runtime/input_admission.py +154 -0
- monkeybot/core/runtime/loop.py +2374 -0
- monkeybot/core/runtime/provider_stream_mapper.py +159 -0
- monkeybot/core/runtime/realtime_loop.py +654 -0
- monkeybot/core/runtime/utterance_buffer.py +179 -0
- monkeybot/core/subagents/__init__.py +1 -0
- monkeybot/core/subagents/subagent_proto.py +331 -0
- monkeybot/core/subagents/subagent_worker.py +441 -0
- monkeybot/core/subagents/worker_pool.py +403 -0
- monkeybot/core/testing/__init__.py +1 -0
- monkeybot/core/testing/mocks_provider.py +86 -0
- monkeybot/core/testing/mocks_realtime_provider.py +137 -0
- monkeybot/core/tools/__init__.py +1 -0
- monkeybot/core/tools/core_tool_executor.py +1548 -0
- monkeybot/core/tools/inspector.py +226 -0
- monkeybot/core/tools/loop_inspector.py +45 -0
- monkeybot/core/tools/patch.py +480 -0
- monkeybot/core/tools/permission.py +284 -0
- monkeybot/core/tools/sandbox_executor.py +255 -0
- monkeybot/core/tools/spill_inventory.py +35 -0
- monkeybot/core/tools/terminal.py +381 -0
- monkeybot/core/tools/text_normalize.py +25 -0
- monkeybot/core/tools/types.py +33 -0
- monkeybot/core/tools/workspace_service.py +710 -0
- monkeybot/core/tools/workspace_tools.py +116 -0
- monkeybot/core/types/__init__.py +1 -0
- monkeybot/core/types/content_blocks.py +644 -0
- monkeybot/core/types/interfaces.py +156 -0
- monkeybot/core/types/types_tools.py +29 -0
- monkeybot/core/workspace/__init__.py +8 -0
- monkeybot/core/workspace/factory.py +45 -0
- monkeybot/core/workspace/gcs.py +130 -0
- monkeybot/core/workspace/local.py +162 -0
- monkeybot/core/workspace/protocol.py +45 -0
- monkeybot/core/workspace/s3.py +151 -0
- monkeybot/core/workspace_layout.py +27 -0
- monkeybot/gateway/__init__.py +1 -0
- monkeybot/gateway/bootstrap.py +18 -0
- monkeybot/gateway/main.py +47 -0
- monkeybot/gateway/realtime/__init__.py +31 -0
- monkeybot/gateway/realtime/app.py +321 -0
- monkeybot/gateway/realtime/deps.py +52 -0
- monkeybot/gateway/realtime/errors.py +81 -0
- monkeybot/gateway/realtime/guardrails.py +88 -0
- monkeybot/gateway/realtime/manager.py +77 -0
- monkeybot/gateway/realtime/metrics.py +144 -0
- monkeybot/gateway/realtime/routes.py +864 -0
- monkeybot/gateway/realtime/session.py +232 -0
- monkeybot/gateway/realtime/wire.py +412 -0
- monkeybot/gateway/realtime_main.py +49 -0
- monkeybot/gateway/sse/__init__.py +1 -0
- monkeybot/gateway/sse/app.py +733 -0
- monkeybot/gateway/sse/loop_port.py +31 -0
- monkeybot/gateway/sse/models.py +177 -0
- monkeybot/gateway/sse/reply_body.py +91 -0
- monkeybot/gateway/sse/routes.py +1101 -0
- monkeybot/gateway/sse/scheduler_routes.py +200 -0
- monkeybot/gateway/sse/scheduler_wiring.py +96 -0
- monkeybot/gateway/sse/session_bus.py +226 -0
- monkeybot/gateway/sse/sse.py +46 -0
- monkeybot/gateway/sse/workspace_layout.py +7 -0
- monkeybot/observability/__init__.py +220 -0
- monkeybot/observability/_state.py +10 -0
- monkeybot/observability/instrumentation.py +153 -0
- monkeybot/observability/propagation.py +65 -0
- monkeybot/observability/spans.py +455 -0
- monkeybot/providers/__init__.py +19 -0
- monkeybot/providers/_openai_compat.py +450 -0
- monkeybot/providers/_utils.py +473 -0
- monkeybot/providers/bedrock.py +145 -0
- monkeybot/providers/claude.py +125 -0
- monkeybot/providers/gemini.py +677 -0
- monkeybot/providers/gemini_live.py +398 -0
- monkeybot/providers/huggingface.py +129 -0
- monkeybot/providers/nvidia.py +104 -0
- monkeybot/providers/ollama.py +152 -0
- monkeybot/providers/openai.py +127 -0
- monkeybot/providers/pricing.py +60 -0
- monkeybot/providers/sampling.py +44 -0
- monkeybot/providers/vertex_claude.py +148 -0
- monkeybot/scaffold/__init__.py +33 -0
- monkeybot/scheduler/__init__.py +13 -0
- monkeybot/scheduler/__main__.py +4 -0
- monkeybot/scheduler/engine.py +333 -0
- monkeybot/scheduler/http_invoker.py +61 -0
- monkeybot/scheduler/interval.py +77 -0
- monkeybot/scheduler/tick_result.py +34 -0
- monkeybot/scheduler/worker.py +87 -0
- monkeybot/subagents/__init__.py +1 -0
- monkeybot/subagents/worker/__init__.py +1 -0
- monkeybot/subagents/worker/__main__.py +22 -0
- monkeybot/web_search/__init__.py +82 -0
- monkeybot/web_search/backends/__init__.py +5 -0
- monkeybot/web_search/backends/duckduckgo.py +32 -0
- monkeybot/web_search/backends/firecrawl.py +43 -0
- monkeybot/web_search/backends/tavily.py +45 -0
- monkeybot/web_search/protocol.py +25 -0
- monkeybot/web_search/tool.py +56 -0
- monkeybot-2.1.1.dist-info/METADATA +318 -0
- monkeybot-2.1.1.dist-info/RECORD +178 -0
- monkeybot-2.1.1.dist-info/WHEEL +4 -0
- monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Dynamic context-window budgeting for tool results (Option B)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from monkeybot.core.context.common import ContextPressureTier, text_from_blocks
|
|
11
|
+
from monkeybot.core.context.tool_output_policy import resolve_tool_budget
|
|
12
|
+
from monkeybot.core.context.tool_result_ingress import (
|
|
13
|
+
sanitize_tool_result_text,
|
|
14
|
+
skip_tool_result_sanitize,
|
|
15
|
+
)
|
|
16
|
+
from monkeybot.core.context.tool_shapers import shape_tool_text
|
|
17
|
+
from monkeybot.core.logging_utils import kv
|
|
18
|
+
from monkeybot.core.types.content_blocks import ContentBlock, Text, ToolResponse
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_INVENTORY_MARKER = "[Spill inventory —"
|
|
23
|
+
_CHARS_PER_TOKEN = 4
|
|
24
|
+
|
|
25
|
+
_DIFF_GIT_RE = re.compile(r"^diff --git a/.+ b/(.+)$", re.MULTILINE)
|
|
26
|
+
|
|
27
|
+
_DEFAULT_LIGHT_RATIO = 0.50
|
|
28
|
+
_DEFAULT_MODERATE_RATIO = 0.70
|
|
29
|
+
_DEFAULT_AGGRESSIVE_RATIO = 0.85
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ratio_from_env(name: str, default: float) -> float:
|
|
33
|
+
raw = os.environ.get(name, "").strip()
|
|
34
|
+
if not raw:
|
|
35
|
+
return default
|
|
36
|
+
try:
|
|
37
|
+
val = float(raw)
|
|
38
|
+
except ValueError:
|
|
39
|
+
logger.warning(
|
|
40
|
+
"invalid env var value %s",
|
|
41
|
+
kv(name=name, value=raw, default=default),
|
|
42
|
+
)
|
|
43
|
+
return default
|
|
44
|
+
return min(0.99, max(0.05, val))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def pressure_light_ratio_from_env() -> float:
|
|
48
|
+
return _ratio_from_env("MONKEYBOT_PRESSURE_LIGHT_RATIO", _DEFAULT_LIGHT_RATIO)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def pressure_moderate_ratio_from_env() -> float:
|
|
52
|
+
return _ratio_from_env("MONKEYBOT_PRESSURE_MODERATE_RATIO", _DEFAULT_MODERATE_RATIO)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pressure_aggressive_ratio_from_env() -> float:
|
|
56
|
+
return _ratio_from_env("MONKEYBOT_PRESSURE_AGGRESSIVE_RATIO", _DEFAULT_AGGRESSIVE_RATIO)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def summarization_trigger_ratio_from_env() -> float:
|
|
60
|
+
"""Ratio of context window that triggers full LLM summarization."""
|
|
61
|
+
return pressure_aggressive_ratio_from_env()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def compute_context_pressure_tier(
|
|
65
|
+
used_tokens: int,
|
|
66
|
+
window_tokens: int,
|
|
67
|
+
*,
|
|
68
|
+
light_ratio: float | None = None,
|
|
69
|
+
moderate_ratio: float | None = None,
|
|
70
|
+
aggressive_ratio: float | None = None,
|
|
71
|
+
) -> ContextPressureTier | None:
|
|
72
|
+
"""Return pressure tier from ``used_tokens / window_tokens``, or None when low."""
|
|
73
|
+
if window_tokens <= 0:
|
|
74
|
+
return None
|
|
75
|
+
ratio = used_tokens / window_tokens
|
|
76
|
+
light = light_ratio if light_ratio is not None else pressure_light_ratio_from_env()
|
|
77
|
+
moderate = moderate_ratio if moderate_ratio is not None else pressure_moderate_ratio_from_env()
|
|
78
|
+
aggressive = (
|
|
79
|
+
aggressive_ratio if aggressive_ratio is not None else pressure_aggressive_ratio_from_env()
|
|
80
|
+
)
|
|
81
|
+
if ratio >= aggressive:
|
|
82
|
+
return "aggressive"
|
|
83
|
+
if ratio >= moderate:
|
|
84
|
+
return "moderate"
|
|
85
|
+
if ratio >= light:
|
|
86
|
+
return "light"
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _safety_fraction_for_tier(
|
|
91
|
+
base_fraction: float,
|
|
92
|
+
tier: ContextPressureTier | None,
|
|
93
|
+
) -> float:
|
|
94
|
+
if tier == "light":
|
|
95
|
+
return min(base_fraction, 0.65)
|
|
96
|
+
if tier == "moderate":
|
|
97
|
+
return min(base_fraction, 0.45)
|
|
98
|
+
if tier == "aggressive":
|
|
99
|
+
return min(base_fraction, 0.25)
|
|
100
|
+
return base_fraction
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def estimate_tokens(text: str) -> int:
|
|
104
|
+
"""Cheap local token estimate (no network)."""
|
|
105
|
+
if not text:
|
|
106
|
+
return 0
|
|
107
|
+
return max(1, len(text) // _CHARS_PER_TOKEN)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _tool_response_with_text(block: ToolResponse, text: str) -> ToolResponse:
|
|
111
|
+
original = text_from_blocks(list(block.result))
|
|
112
|
+
if text == original:
|
|
113
|
+
return block
|
|
114
|
+
return ToolResponse(
|
|
115
|
+
id=block.id,
|
|
116
|
+
tool_name=block.tool_name,
|
|
117
|
+
result=[Text(text=text)],
|
|
118
|
+
is_error=block.is_error,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _split_inventory_note(text: str) -> tuple[str, str | None]:
|
|
123
|
+
"""Return (body, inventory_note) when an inventory block is present."""
|
|
124
|
+
idx = text.rfind(_INVENTORY_MARKER)
|
|
125
|
+
if idx == -1:
|
|
126
|
+
return text, None
|
|
127
|
+
return text[:idx].rstrip(), text[idx:].lstrip()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _trim_text_to_token_budget(text: str, token_budget: int) -> str:
|
|
131
|
+
if token_budget <= 0:
|
|
132
|
+
return ""
|
|
133
|
+
max_chars = max(1, token_budget * _CHARS_PER_TOKEN)
|
|
134
|
+
if len(text) <= max_chars:
|
|
135
|
+
return text
|
|
136
|
+
body, note = _split_inventory_note(text)
|
|
137
|
+
if note:
|
|
138
|
+
note_len = len(note) + 1
|
|
139
|
+
head_budget = max(0, max_chars - note_len)
|
|
140
|
+
if head_budget <= 0:
|
|
141
|
+
return note
|
|
142
|
+
trimmed_body = body[:head_budget]
|
|
143
|
+
if len(body) > head_budget:
|
|
144
|
+
trimmed_body += "\n[... truncated for context budget — use read_file on spill path to page ...]"
|
|
145
|
+
return f"{trimmed_body}\n{note}"
|
|
146
|
+
trimmed = text[:max_chars]
|
|
147
|
+
if len(text) > max_chars:
|
|
148
|
+
trimmed += "\n[... truncated for context budget — use read_file on spill path to page ...]"
|
|
149
|
+
return trimmed
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def budget_fraction_from_env() -> float:
|
|
153
|
+
raw = os.environ.get("MONKEYBOT_RESULT_BUDGET_FRACTION", "0.8").strip()
|
|
154
|
+
try:
|
|
155
|
+
val = float(raw)
|
|
156
|
+
except ValueError:
|
|
157
|
+
return 0.8
|
|
158
|
+
return min(1.0, max(0.05, val))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def budget_floor_tokens_from_env() -> int:
|
|
162
|
+
raw = os.environ.get("MONKEYBOT_RESULT_BUDGET_FLOOR_TOKENS", "2000").strip()
|
|
163
|
+
try:
|
|
164
|
+
return max(1, int(raw))
|
|
165
|
+
except ValueError:
|
|
166
|
+
return 2000
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class ContextBudgeter:
|
|
171
|
+
"""Allocate remaining context headroom across a batch of tool results."""
|
|
172
|
+
|
|
173
|
+
window_tokens: int
|
|
174
|
+
used_tokens: int
|
|
175
|
+
safety_fraction: float = 0.8
|
|
176
|
+
floor_tokens: int = 2000
|
|
177
|
+
pressure_tier: ContextPressureTier | None = None
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def from_env(cls, *, window_tokens: int, used_tokens: int) -> ContextBudgeter:
|
|
181
|
+
tier = compute_context_pressure_tier(used_tokens, window_tokens)
|
|
182
|
+
base_fraction = budget_fraction_from_env()
|
|
183
|
+
return cls(
|
|
184
|
+
window_tokens=window_tokens,
|
|
185
|
+
used_tokens=used_tokens,
|
|
186
|
+
safety_fraction=_safety_fraction_for_tier(base_fraction, tier),
|
|
187
|
+
floor_tokens=budget_floor_tokens_from_env(),
|
|
188
|
+
pressure_tier=tier,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def remaining_tokens(self) -> int:
|
|
193
|
+
return max(0, self.window_tokens - self.used_tokens)
|
|
194
|
+
|
|
195
|
+
def fit_content_blocks(
|
|
196
|
+
self,
|
|
197
|
+
blocks: list[ContentBlock],
|
|
198
|
+
) -> tuple[list[ContentBlock], bool]:
|
|
199
|
+
"""Trim tool-response blocks to fit remaining headroom; return (blocks, needs_compaction)."""
|
|
200
|
+
if not blocks:
|
|
201
|
+
return blocks, False
|
|
202
|
+
|
|
203
|
+
remaining = self.remaining_tokens
|
|
204
|
+
if remaining <= self.floor_tokens:
|
|
205
|
+
return self._trim_all(blocks, max(1, remaining // max(1, len(blocks)))), True
|
|
206
|
+
|
|
207
|
+
pool = max(1, int(remaining * self.safety_fraction))
|
|
208
|
+
per_item = max(1, pool // len(blocks))
|
|
209
|
+
needs_compaction = remaining < self.floor_tokens * 2
|
|
210
|
+
if self.pressure_tier in ("moderate", "aggressive"):
|
|
211
|
+
needs_compaction = True
|
|
212
|
+
|
|
213
|
+
out: list[ContentBlock] = []
|
|
214
|
+
for block in blocks:
|
|
215
|
+
shaped = self._shape_tool_response(block)
|
|
216
|
+
if shaped is None:
|
|
217
|
+
out.append(block)
|
|
218
|
+
continue
|
|
219
|
+
block, text = shaped
|
|
220
|
+
est = estimate_tokens(text)
|
|
221
|
+
if est <= per_item:
|
|
222
|
+
out.append(_tool_response_with_text(block, text))
|
|
223
|
+
self.used_tokens += est
|
|
224
|
+
continue
|
|
225
|
+
needs_compaction = True
|
|
226
|
+
trimmed = _trim_text_to_token_budget(text, per_item)
|
|
227
|
+
out.append(_tool_response_with_text(block, trimmed))
|
|
228
|
+
self.used_tokens += estimate_tokens(trimmed)
|
|
229
|
+
|
|
230
|
+
return out, needs_compaction
|
|
231
|
+
|
|
232
|
+
def _shape_tool_response(self, block: ContentBlock) -> tuple[ToolResponse, str] | None:
|
|
233
|
+
if not isinstance(block, ToolResponse) or block.is_error:
|
|
234
|
+
return None
|
|
235
|
+
text = text_from_blocks(list(block.result))
|
|
236
|
+
if not skip_tool_result_sanitize(block.tool_name):
|
|
237
|
+
text = sanitize_tool_result_text(text)
|
|
238
|
+
budget = resolve_tool_budget(block.tool_name)
|
|
239
|
+
if budget is not None or self.pressure_tier in ("moderate", "aggressive"):
|
|
240
|
+
text = shape_tool_text(
|
|
241
|
+
text,
|
|
242
|
+
tool_name=block.tool_name,
|
|
243
|
+
budget=budget,
|
|
244
|
+
pressure_tier=self.pressure_tier,
|
|
245
|
+
)
|
|
246
|
+
return block, text
|
|
247
|
+
|
|
248
|
+
def _trim_all(self, blocks: list[ContentBlock], per_item: int) -> list[ContentBlock]:
|
|
249
|
+
out: list[ContentBlock] = []
|
|
250
|
+
for block in blocks:
|
|
251
|
+
shaped = self._shape_tool_response(block)
|
|
252
|
+
if shaped is None:
|
|
253
|
+
out.append(block)
|
|
254
|
+
continue
|
|
255
|
+
block, text = shaped
|
|
256
|
+
trimmed = _trim_text_to_token_budget(text, per_item)
|
|
257
|
+
out.append(_tool_response_with_text(block, trimmed))
|
|
258
|
+
self.used_tokens += estimate_tokens(trimmed)
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def diff_inventory_lines(text: str) -> list[str] | None:
|
|
263
|
+
"""Return changed paths from unified diff headers, or None if not a diff."""
|
|
264
|
+
paths = _DIFF_GIT_RE.findall(text)
|
|
265
|
+
if not paths:
|
|
266
|
+
return None
|
|
267
|
+
return paths
|