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.
Files changed (178) hide show
  1. monkeybot/__init__.py +3 -0
  2. monkeybot/cli/__init__.py +3 -0
  3. monkeybot/cli/__main__.py +8 -0
  4. monkeybot/cli/audio_io.py +8 -0
  5. monkeybot/cli/gateway_manager.py +17 -0
  6. monkeybot/cli/main.py +22 -0
  7. monkeybot/cli/push_to_talk.py +12 -0
  8. monkeybot/cli/realtime_client.py +13 -0
  9. monkeybot/core/__init__.py +19 -0
  10. monkeybot/core/attachments/__init__.py +22 -0
  11. monkeybot/core/attachments/catalog.py +62 -0
  12. monkeybot/core/attachments/config.py +52 -0
  13. monkeybot/core/attachments/freeze.py +158 -0
  14. monkeybot/core/attachments/resolve.py +70 -0
  15. monkeybot/core/attachments/store.py +180 -0
  16. monkeybot/core/attachments/text.py +72 -0
  17. monkeybot/core/attachments/tools.py +54 -0
  18. monkeybot/core/bootstrap.py +242 -0
  19. monkeybot/core/config/__init__.py +71 -0
  20. monkeybot/core/config/realtime_config.py +150 -0
  21. monkeybot/core/config/runtime_env.py +262 -0
  22. monkeybot/core/config/settings.py +341 -0
  23. monkeybot/core/config/validation.py +249 -0
  24. monkeybot/core/config/yaml_loader.py +45 -0
  25. monkeybot/core/context/__init__.py +781 -0
  26. monkeybot/core/context/campaign_context.py +8 -0
  27. monkeybot/core/context/common.py +14 -0
  28. monkeybot/core/context/curator.py +255 -0
  29. monkeybot/core/context/epoch.py +226 -0
  30. monkeybot/core/context/memory_prompt.py +222 -0
  31. monkeybot/core/context/tool_output_policy.py +270 -0
  32. monkeybot/core/context/tool_result_ingress.py +290 -0
  33. monkeybot/core/context/tool_shapers.py +361 -0
  34. monkeybot/core/hooks/__init__.py +261 -0
  35. monkeybot/core/llm/__init__.py +4 -0
  36. monkeybot/core/llm/provider.py +296 -0
  37. monkeybot/core/llm/realtime_provider.py +203 -0
  38. monkeybot/core/llm/usage.py +57 -0
  39. monkeybot/core/logging_utils.py +24 -0
  40. monkeybot/core/mcp/__init__.py +1 -0
  41. monkeybot/core/mcp/mcp_client.py +1215 -0
  42. monkeybot/core/mcp/ports_mcp.py +109 -0
  43. monkeybot/core/memory/__init__.py +24 -0
  44. monkeybot/core/memory/hook.py +413 -0
  45. monkeybot/core/memory/index_format.py +104 -0
  46. monkeybot/core/memory/integrity.py +180 -0
  47. monkeybot/core/memory/organizer.py +270 -0
  48. monkeybot/core/memory/storage_ops.py +139 -0
  49. monkeybot/core/memory/subsystem.py +91 -0
  50. monkeybot/core/messages/__init__.py +16 -0
  51. monkeybot/core/messages/convert_provider.py +41 -0
  52. monkeybot/core/messages/tool_integrity.py +262 -0
  53. monkeybot/core/messages/transform_context.py +84 -0
  54. monkeybot/core/path_safety.py +11 -0
  55. monkeybot/core/persistence/__init__.py +17 -0
  56. monkeybot/core/persistence/backends.py +236 -0
  57. monkeybot/core/persistence/db.py +28 -0
  58. monkeybot/core/persistence/durable_runs.py +286 -0
  59. monkeybot/core/persistence/firestore.py +658 -0
  60. monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
  61. monkeybot/core/persistence/history.py +156 -0
  62. monkeybot/core/persistence/postgres.py +895 -0
  63. monkeybot/core/persistence/runs.py +76 -0
  64. monkeybot/core/persistence/scheduled_loops.py +435 -0
  65. monkeybot/core/persistence/session_turn_locks.py +94 -0
  66. monkeybot/core/persistence/sqlite.py +218 -0
  67. monkeybot/core/persistence/sqlite_backend.py +74 -0
  68. monkeybot/core/persistence/thread_summary.py +61 -0
  69. monkeybot/core/persistence/transcript.py +194 -0
  70. monkeybot/core/persistence/usage.py +149 -0
  71. monkeybot/core/prompts/__init__.py +1 -0
  72. monkeybot/core/prompts/harness_prompt.py +197 -0
  73. monkeybot/core/prompts/prompt.py +215 -0
  74. monkeybot/core/runtime/__init__.py +1 -0
  75. monkeybot/core/runtime/context_budget.py +267 -0
  76. monkeybot/core/runtime/events.py +819 -0
  77. monkeybot/core/runtime/input_admission.py +154 -0
  78. monkeybot/core/runtime/loop.py +2374 -0
  79. monkeybot/core/runtime/provider_stream_mapper.py +159 -0
  80. monkeybot/core/runtime/realtime_loop.py +654 -0
  81. monkeybot/core/runtime/utterance_buffer.py +179 -0
  82. monkeybot/core/subagents/__init__.py +1 -0
  83. monkeybot/core/subagents/subagent_proto.py +331 -0
  84. monkeybot/core/subagents/subagent_worker.py +441 -0
  85. monkeybot/core/subagents/worker_pool.py +403 -0
  86. monkeybot/core/testing/__init__.py +1 -0
  87. monkeybot/core/testing/mocks_provider.py +86 -0
  88. monkeybot/core/testing/mocks_realtime_provider.py +137 -0
  89. monkeybot/core/tools/__init__.py +1 -0
  90. monkeybot/core/tools/core_tool_executor.py +1548 -0
  91. monkeybot/core/tools/inspector.py +226 -0
  92. monkeybot/core/tools/loop_inspector.py +45 -0
  93. monkeybot/core/tools/patch.py +480 -0
  94. monkeybot/core/tools/permission.py +284 -0
  95. monkeybot/core/tools/sandbox_executor.py +255 -0
  96. monkeybot/core/tools/spill_inventory.py +35 -0
  97. monkeybot/core/tools/terminal.py +381 -0
  98. monkeybot/core/tools/text_normalize.py +25 -0
  99. monkeybot/core/tools/types.py +33 -0
  100. monkeybot/core/tools/workspace_service.py +710 -0
  101. monkeybot/core/tools/workspace_tools.py +116 -0
  102. monkeybot/core/types/__init__.py +1 -0
  103. monkeybot/core/types/content_blocks.py +644 -0
  104. monkeybot/core/types/interfaces.py +156 -0
  105. monkeybot/core/types/types_tools.py +29 -0
  106. monkeybot/core/workspace/__init__.py +8 -0
  107. monkeybot/core/workspace/factory.py +45 -0
  108. monkeybot/core/workspace/gcs.py +130 -0
  109. monkeybot/core/workspace/local.py +162 -0
  110. monkeybot/core/workspace/protocol.py +45 -0
  111. monkeybot/core/workspace/s3.py +151 -0
  112. monkeybot/core/workspace_layout.py +27 -0
  113. monkeybot/gateway/__init__.py +1 -0
  114. monkeybot/gateway/bootstrap.py +18 -0
  115. monkeybot/gateway/main.py +47 -0
  116. monkeybot/gateway/realtime/__init__.py +31 -0
  117. monkeybot/gateway/realtime/app.py +321 -0
  118. monkeybot/gateway/realtime/deps.py +52 -0
  119. monkeybot/gateway/realtime/errors.py +81 -0
  120. monkeybot/gateway/realtime/guardrails.py +88 -0
  121. monkeybot/gateway/realtime/manager.py +77 -0
  122. monkeybot/gateway/realtime/metrics.py +144 -0
  123. monkeybot/gateway/realtime/routes.py +864 -0
  124. monkeybot/gateway/realtime/session.py +232 -0
  125. monkeybot/gateway/realtime/wire.py +412 -0
  126. monkeybot/gateway/realtime_main.py +49 -0
  127. monkeybot/gateway/sse/__init__.py +1 -0
  128. monkeybot/gateway/sse/app.py +733 -0
  129. monkeybot/gateway/sse/loop_port.py +31 -0
  130. monkeybot/gateway/sse/models.py +177 -0
  131. monkeybot/gateway/sse/reply_body.py +91 -0
  132. monkeybot/gateway/sse/routes.py +1101 -0
  133. monkeybot/gateway/sse/scheduler_routes.py +200 -0
  134. monkeybot/gateway/sse/scheduler_wiring.py +96 -0
  135. monkeybot/gateway/sse/session_bus.py +226 -0
  136. monkeybot/gateway/sse/sse.py +46 -0
  137. monkeybot/gateway/sse/workspace_layout.py +7 -0
  138. monkeybot/observability/__init__.py +220 -0
  139. monkeybot/observability/_state.py +10 -0
  140. monkeybot/observability/instrumentation.py +153 -0
  141. monkeybot/observability/propagation.py +65 -0
  142. monkeybot/observability/spans.py +455 -0
  143. monkeybot/providers/__init__.py +19 -0
  144. monkeybot/providers/_openai_compat.py +450 -0
  145. monkeybot/providers/_utils.py +473 -0
  146. monkeybot/providers/bedrock.py +145 -0
  147. monkeybot/providers/claude.py +125 -0
  148. monkeybot/providers/gemini.py +677 -0
  149. monkeybot/providers/gemini_live.py +398 -0
  150. monkeybot/providers/huggingface.py +129 -0
  151. monkeybot/providers/nvidia.py +104 -0
  152. monkeybot/providers/ollama.py +152 -0
  153. monkeybot/providers/openai.py +127 -0
  154. monkeybot/providers/pricing.py +60 -0
  155. monkeybot/providers/sampling.py +44 -0
  156. monkeybot/providers/vertex_claude.py +148 -0
  157. monkeybot/scaffold/__init__.py +33 -0
  158. monkeybot/scheduler/__init__.py +13 -0
  159. monkeybot/scheduler/__main__.py +4 -0
  160. monkeybot/scheduler/engine.py +333 -0
  161. monkeybot/scheduler/http_invoker.py +61 -0
  162. monkeybot/scheduler/interval.py +77 -0
  163. monkeybot/scheduler/tick_result.py +34 -0
  164. monkeybot/scheduler/worker.py +87 -0
  165. monkeybot/subagents/__init__.py +1 -0
  166. monkeybot/subagents/worker/__init__.py +1 -0
  167. monkeybot/subagents/worker/__main__.py +22 -0
  168. monkeybot/web_search/__init__.py +82 -0
  169. monkeybot/web_search/backends/__init__.py +5 -0
  170. monkeybot/web_search/backends/duckduckgo.py +32 -0
  171. monkeybot/web_search/backends/firecrawl.py +43 -0
  172. monkeybot/web_search/backends/tavily.py +45 -0
  173. monkeybot/web_search/protocol.py +25 -0
  174. monkeybot/web_search/tool.py +56 -0
  175. monkeybot-2.1.1.dist-info/METADATA +318 -0
  176. monkeybot-2.1.1.dist-info/RECORD +178 -0
  177. monkeybot-2.1.1.dist-info/WHEEL +4 -0
  178. 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